diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6d6c67952..2d95e6ffa 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -32,7 +32,7 @@ jobs: python -m pip install --upgrade pip setuptools wheel python --version; git --version git submodule update --init --recursive - git fetch --tags + git fetch --tags --force pip install -r requirements.txt pip install -r test-requirements.txt diff --git a/README.md b/README.md index 676d2c6d6..30c54b57f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ To typecheck, run: `mypy -p git` To test, run: `pytest` +For automatic code formatting run: `black git` + Configuration for flake8 is in the ./.flake8 file. Configurations for mypy, pytest and coverage.py are in ./pyproject.toml. diff --git a/VERSION b/VERSION index 381c34a62..f8d874e2b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.32 +3.1.33 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 3bc02e770..45062ac13 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.33 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/63?closed=1 + 3.1.32 ====== diff --git a/doc/source/index.rst b/doc/source/index.rst index 69fb573a4..72db8ee5a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,6 +9,7 @@ GitPython Documentation :maxdepth: 2 intro + quickstart tutorial reference roadmap diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst new file mode 100644 index 000000000..c5930eb8a --- /dev/null +++ b/doc/source/quickstart.rst @@ -0,0 +1,244 @@ +.. _quickdoc_toplevel: + +.. highlight:: python + +.. _quickdoc-label: + +============================== +GitPython Quick Start Tutorial +============================== +Welcome to the GitPython Quickstart Guide! Designed for developers seeking a practical and interactive learning experience, this concise resource offers step-by-step code snippets to swiftly initialize/clone repositories, perform essential Git operations, and explore GitPython's capabilities. Get ready to dive in, experiment, and unleash the power of GitPython in your projects! + + +git.Repo +******** + +There are a few ways to create a :class:`git.Repo ` object + +Initialize a new git Repo +######################### + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_init_repo_object] + :end-before: # ![1-test_init_repo_object] + +Existing local git Repo +####################### + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_init_repo_object] + :end-before: # ![2-test_init_repo_object] + +Clone from URL +############## + +For the rest of this tutorial we will use a clone from https://github.com/gitpython-developers/QuickStartTutorialFiles.git + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_cloned_repo_object] + :end-before: # ![1-test_cloned_repo_object] + + +Trees & Blobs +************** + +Latest Commit Tree +################## + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [12-test_cloned_repo_object] + :end-before: # ![12-test_cloned_repo_object] + +Any Commit Tree +############### + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [13-test_cloned_repo_object] + :end-before: # ![13-test_cloned_repo_object] + +Display level 1 Contents +######################## + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14-test_cloned_repo_object] + :end-before: # ![14-test_cloned_repo_object] + +Recurse through the Tree +######################## + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [15-test_cloned_repo_object] + :end-before: # ![15-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] + + + + +Usage +**************** + +Add file to staging area +######################## + + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_cloned_repo_object] + :end-before: # ![2-test_cloned_repo_object] + + Now lets add the updated file to git + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [3-test_cloned_repo_object] + :end-before: # ![3-test_cloned_repo_object] + + Notice the add method requires a list as a parameter + + Warning: If you experience any trouble with this, try to invoke :class:`git ` instead via repo.git.add(path) + +Commit +###### + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [4-test_cloned_repo_object] + :end-before: # ![4-test_cloned_repo_object] + +List of commits associated with a file +####################################### + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [5-test_cloned_repo_object] + :end-before: # ![5-test_cloned_repo_object] + + Notice this returns a generator object + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [6-test_cloned_repo_object] + :end-before: # ![6-test_cloned_repo_object] + + returns list of :class:`Commit ` objects + +Printing text files +#################### +Lets print the latest version of `/dir1/file2.txt` + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [17-test_cloned_repo_object] + :end-before: # ![17-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18-test_cloned_repo_object] + :end-before: # ![18-test_cloned_repo_object] + + Previous version of `/dir1/file2.txt` + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18.1-test_cloned_repo_object] + :end-before: # ![18.1-test_cloned_repo_object] + +Status +###### + * Untracked files + + Lets create a new file + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [7-test_cloned_repo_object] + :end-before: # ![7-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [8-test_cloned_repo_object] + :end-before: # ![8-test_cloned_repo_object] + + * Modified files + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [9-test_cloned_repo_object] + :end-before: # ![9-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [10-test_cloned_repo_object] + :end-before: # ![10-test_cloned_repo_object] + + returns a list of :class:`Diff ` objects + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11-test_cloned_repo_object] + :end-before: # ![11-test_cloned_repo_object] + +Diffs +###### + +Compare staging area to head commit + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.1-test_cloned_repo_object] + :end-before: # ![11.1-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.2-test_cloned_repo_object] + :end-before: # ![11.2-test_cloned_repo_object] + +Compare commit to commit + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.3-test_cloned_repo_object] + :end-before: # ![11.3-test_cloned_repo_object] + + +More Resources +**************** + +Remember, this is just the beginning! There's a lot more you can achieve with GitPython in your development workflow. +To explore further possibilities and discover advanced features, check out the full :ref:`GitPython tutorial ` +and the :ref:`API Reference `. Happy coding! diff --git a/git/__init__.py b/git/__init__.py index cd6602bf0..6196a42d7 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -76,7 +76,7 @@ def refresh(path: Optional[PathLike] = None) -> None: if not Git.refresh(path=path): return if not FetchInfo.refresh(): - return + return # type: ignore [unreachable] GIT_OK = True diff --git a/git/cmd.py b/git/cmd.py index dfce9024d..3665eb029 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php from __future__ import annotations import re -from contextlib import contextmanager +import contextlib import io import logging import os @@ -14,6 +14,7 @@ import subprocess import threading from textwrap import dedent +import unittest.mock from git.compat import ( defenc, @@ -122,6 +123,7 @@ def handle_process_output( To specify a timeout in seconds for the git command, after which the process should be killed. """ + # Use 2 "pump" threads and wait for both to finish. def pump_stream( cmdline: List[str], @@ -154,7 +156,7 @@ def pump_stream( p_stdout = process.proc.stdout if process.proc else None p_stderr = process.proc.stderr if process.proc else None else: - process = cast(Popen, process) + process = cast(Popen, process) # type: ignore [redundant-cast] cmdline = getattr(process, "args", "") p_stdout = process.stdout p_stderr = process.stderr @@ -210,7 +212,7 @@ def dashify(string: str) -> str: return string.replace("_", "-") -def slots_to_dict(self: object, exclude: Sequence[str] = ()) -> Dict[str, Any]: +def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]: return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} @@ -488,10 +490,7 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> """ # Options can be of the form `foo` or `--foo bar` `--foo=bar`, # so we need to check if they start with "--foo" or if they are equal to "foo". - bare_unsafe_options = [ - option.lstrip("-") - for option in unsafe_options - ] + bare_unsafe_options = [option.lstrip("-") for option in unsafe_options] for option in options: for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options): if option.startswith(unsafe_option) or option == bare_option: @@ -965,8 +964,11 @@ def execute( redacted_command, '"kill_after_timeout" feature is not supported on Windows.', ) + # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value. + patch_caller_env = unittest.mock.patch.dict(os.environ, {"NoDefaultCurrentDirectoryInExePath": "1"}) else: cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + patch_caller_env = contextlib.nullcontext() # end handle stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") @@ -982,21 +984,21 @@ def execute( istream_ok, ) try: - proc = Popen( - command, - env=env, - cwd=cwd, - bufsize=-1, - stdin=istream or DEVNULL, - stderr=PIPE, - stdout=stdout_sink, - shell=shell is not None and shell or self.USE_SHELL, - close_fds=is_posix, # unsupported on windows - universal_newlines=universal_newlines, - creationflags=PROC_CREATIONFLAGS, - **subprocess_kwargs, - ) - + with patch_caller_env: + proc = Popen( + command, + env=env, + cwd=cwd, + bufsize=-1, + stdin=istream or DEVNULL, + stderr=PIPE, + stdout=stdout_sink, + shell=shell is not None and shell or self.USE_SHELL, + close_fds=is_posix, # unsupported on windows + universal_newlines=universal_newlines, + creationflags=PROC_CREATIONFLAGS, + **subprocess_kwargs, + ) except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err else: @@ -1146,7 +1148,7 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: del self._environment[key] return old_env - @contextmanager + @contextlib.contextmanager def custom_environment(self, **kwargs: Any) -> Iterator[None]: """ A context manager around the above ``update_environment`` method to restore the @@ -1194,7 +1196,6 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any @classmethod def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: - outlist = [] if isinstance(arg_list, (list, tuple)): for arg in arg_list: diff --git a/git/config.py b/git/config.py index e05a297af..1973111eb 100644 --- a/git/config.py +++ b/git/config.py @@ -248,7 +248,6 @@ def items_all(self) -> List[Tuple[str, List[_T]]]: def get_config_path(config_level: Lit_config_levels) -> str: - # we do not support an absolute path of the gitconfig on windows , # use the global config instead if is_win and config_level == "system": @@ -265,8 +264,8 @@ def get_config_path(config_level: Lit_config_levels) -> str: raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") else: # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs - assert_never( - config_level, # type: ignore[unreachable] + assert_never( # type: ignore[unreachable] + config_level, ValueError(f"Invalid configuration level: {config_level!r}"), ) @@ -655,7 +654,7 @@ def write_section(name: str, section_dict: _OMD) -> None: values: Sequence[str] # runtime only gets str in tests, but should be whatever _OMD stores v: str - for (key, values) in section_dict.items_all(): + for key, values in section_dict.items_all(): if key == "__name__": continue diff --git a/git/diff.py b/git/diff.py index c1a5bd26f..1424ff3ad 100644 --- a/git/diff.py +++ b/git/diff.py @@ -145,7 +145,7 @@ def diff( args.append("--full-index") # get full index paths, not only filenames # remove default '-M' arg (check for renames) if user is overriding it - if not any(x in kwargs for x in ('find_renames', 'no_renames', 'M')): + if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): args.append("-M") if create_patch: @@ -338,7 +338,6 @@ def __init__( change_type: Optional[Lit_change_type], score: Optional[int], ) -> None: - assert a_rawpath is None or isinstance(a_rawpath, bytes) assert b_rawpath is None or isinstance(b_rawpath, bytes) self.a_rawpath = a_rawpath diff --git a/git/exc.py b/git/exc.py index 9b69a5889..775528bf6 100644 --- a/git/exc.py +++ b/git/exc.py @@ -139,7 +139,6 @@ def __init__( valid_files: Sequence[PathLike], failed_reasons: List[str], ) -> None: - Exception.__init__(self, message) self.failed_files = failed_files self.failed_reasons = failed_reasons @@ -170,7 +169,6 @@ def __init__( stderr: Union[bytes, str, None] = None, stdout: Union[bytes, str, None] = None, ) -> None: - super(HookExecutionError, self).__init__(command, status, stderr, stdout) self._msg = "Hook('%s') failed%s" diff --git a/git/index/base.py b/git/index/base.py index dd8f9aa2e..193baf3ad 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -656,7 +656,7 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry def _entries_for_paths( self, paths: List[str], - path_rewriter: Callable, + path_rewriter: Union[Callable, None], fprogress: Callable, entries: List[BaseIndexEntry], ) -> List[BaseIndexEntry]: diff --git a/git/index/fun.py b/git/index/fun.py index d0925ed51..b50f1f465 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -76,7 +76,7 @@ def hook_path(name: str, git_dir: PathLike) -> str: return osp.join(git_dir, "hooks", name) -def _has_file_extension(path): +def _has_file_extension(path: str) -> str: return osp.splitext(path)[1] @@ -102,7 +102,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix() cmd = ["bash.exe", relative_hp] - cmd = subprocess.Popen( + process = subprocess.Popen( cmd + list(args), env=env, stdout=subprocess.PIPE, @@ -116,13 +116,13 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: else: stdout_list: List[str] = [] stderr_list: List[str] = [] - handle_process_output(cmd, stdout_list.append, stderr_list.append, finalize_process) + handle_process_output(process, stdout_list.append, stderr_list.append, finalize_process) stdout = "".join(stdout_list) stderr = "".join(stderr_list) - if cmd.returncode != 0: + if process.returncode != 0: stdout = force_text(stdout, defenc) stderr = force_text(stderr, defenc) - raise HookExecutionError(hp, cmd.returncode, stderr, stdout) + raise HookExecutionError(hp, process.returncode, stderr, stdout) # end handle return code @@ -394,7 +394,6 @@ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> out.append(_tree_entry_to_baseindexentry(theirs, 0)) # END handle modification else: - if ours[0] != base[0] or ours[1] != base[1]: # they deleted it, we changed it, conflict out.append(_tree_entry_to_baseindexentry(base, 1)) diff --git a/git/objects/commit.py b/git/objects/commit.py index 138db0afe..6db3ea0f3 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -345,9 +345,7 @@ def trailers(self) -> Dict[str, str]: Dictionary containing whitespace stripped trailer information. Only contains the latest instance of each trailer key. """ - return { - k: v[0] for k, v in self.trailers_dict.items() - } + return {k: v[0] for k, v in self.trailers_dict.items()} @property def trailers_list(self) -> List[Tuple[str, str]]: @@ -460,7 +458,7 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, if proc_or_stream.stdout is not None: stream = proc_or_stream.stdout elif hasattr(proc_or_stream, "readline"): - proc_or_stream = cast(IO, proc_or_stream) + proc_or_stream = cast(IO, proc_or_stream) # type: ignore [redundant-cast] stream = proc_or_stream readline = stream.readline diff --git a/git/objects/fun.py b/git/objects/fun.py index e91403a8b..043eec721 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -190,7 +190,6 @@ def traverse_trees_recursive( # is a tree. If the match is a non-tree item, put it into the result. # Processed items will be set None for ti, tree_data in enumerate(trees_data): - for ii, item in enumerate(tree_data): if not item: continue diff --git a/git/objects/util.py b/git/objects/util.py index af279154c..56938507e 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -143,11 +143,11 @@ def utctz_to_altz(utctz: str) -> int: :param utctz: git utc timezone string, i.e. +0200 """ int_utctz = int(utctz) - seconds = ((abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60) + seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60 return seconds if int_utctz < 0 else -seconds -def altz_to_utctz_str(altz: int) -> str: +def altz_to_utctz_str(altz: float) -> str: """Convert a timezone offset west of UTC in seconds into a git timezone offset string :param altz: timezone offset in seconds west of UTC diff --git a/git/remote.py b/git/remote.py index 5886a69f0..95a2b8ac6 100644 --- a/git/remote.py +++ b/git/remote.py @@ -826,7 +826,6 @@ def _get_fetch_info_from_stderr( progress: Union[Callable[..., Any], RemoteProgress, None], kill_after_timeout: Union[None, float] = None, ) -> IterableList["FetchInfo"]: - progress = to_progress_instance(progress) # skip first line as it is some remote info we are not interested in diff --git a/git/repo/base.py b/git/repo/base.py index 1fa98d8c7..113fca459 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -60,6 +60,7 @@ PathLike, Lit_config_levels, Commit_ish, + CallableProgress, Tree_ish, assert_never, ) @@ -498,7 +499,7 @@ def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None: def create_tag( self, path: PathLike, - ref: Union[str, 'SymbolicReference'] = "HEAD", + ref: Union[str, "SymbolicReference"] = "HEAD", message: Optional[str] = None, force: bool = False, **kwargs: Any, @@ -548,9 +549,8 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa else: return osp.normpath(osp.join(repo_dir, "config")) else: - - assert_never( - config_level, # type:ignore[unreachable] + assert_never( # type:ignore[unreachable] + config_level, ValueError(f"Invalid configuration level: {config_level!r}"), ) @@ -601,7 +601,7 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo system = system wide configuration file global = user level configuration file repository = configuration file for this repository only""" - return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self) + return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False) def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit: """The Commit object for the specified revision @@ -1259,7 +1259,7 @@ def _clone( def clone( self, path: PathLike, - progress: Optional[Callable] = None, + progress: Optional[CallableProgress] = None, multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, allow_unsafe_options: bool = False, @@ -1298,7 +1298,7 @@ def clone_from( cls, url: PathLike, to_path: PathLike, - progress: Optional[Callable] = None, + progress: CallableProgress = None, env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, diff --git a/git/types.py b/git/types.py index 9064ecbf9..9f8621721 100644 --- a/git/types.py +++ b/git/types.py @@ -8,42 +8,39 @@ from typing import ( Dict, NoReturn, - Sequence, + Sequence as Sequence, Tuple, Union, Any, + Optional, + Callable, TYPE_CHECKING, TypeVar, ) # noqa: F401 -if sys.version_info[:2] >= (3, 8): +if sys.version_info >= (3, 8): from typing import ( Literal, - SupportsIndex, TypedDict, Protocol, + SupportsIndex as SupportsIndex, runtime_checkable, ) # noqa: F401 else: from typing_extensions import ( Literal, - SupportsIndex, # noqa: F401 + SupportsIndex as SupportsIndex, TypedDict, Protocol, runtime_checkable, ) # noqa: F401 -# if sys.version_info[:2] >= (3, 10): +# if sys.version_info >= (3, 10): # from typing import TypeGuard # noqa: F401 # else: # from typing_extensions import TypeGuard # noqa: F401 - -if sys.version_info[:2] < (3, 9): - PathLike = Union[str, os.PathLike] -else: - # os.PathLike only becomes subscriptable from Python 3.9 onwards - PathLike = Union[str, os.PathLike[str]] +PathLike = Union[str, "os.PathLike[str]"] if TYPE_CHECKING: from git.repo import Repo @@ -62,6 +59,9 @@ Lit_config_levels = Literal["system", "global", "user", "repository"] +# Progress parameter type alias ----------------------------------------- + +CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]] # def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]: # # return inp in get_args(Lit_config_level) # only py >= 3.8 diff --git a/git/util.py b/git/util.py index 30028b1c2..a3748f0fe 100644 --- a/git/util.py +++ b/git/util.py @@ -935,11 +935,7 @@ def _obtain_lock_or_raise(self) -> None: ) try: - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL - if is_win: - flags |= os.O_SHORT_LIVED - fd = os.open(lock_file, flags, 0) - os.close(fd) + open(lock_file, mode='w', closefd=True) except OSError as e: raise IOError(str(e)) from e @@ -1049,7 +1045,7 @@ class IterableList(List[T_IterableObj]): __slots__ = ("_id_attr", "_prefix") - def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[IterableObj]": + def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[T_IterableObj]": return super(IterableList, cls).__new__(cls) def __init__(self, id_attr: str, prefix: str = "") -> None: @@ -1083,7 +1079,6 @@ def __getattr__(self, attr: str) -> T_IterableObj: return list.__getattribute__(self, attr) def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore - assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str" if isinstance(index, int): @@ -1098,7 +1093,6 @@ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_Iterabl # END handle getattr def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None: - assert isinstance(index, (int, str)), "Index of IterableList should be an int or str" delindex = cast(int, index) diff --git a/pyproject.toml b/pyproject.toml index 0d5ebf012..32c9d4a26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ filterwarnings = 'ignore::DeprecationWarning' # filterwarnings ignore::WarningType # ignores those warnings [tool.mypy] +python_version = "3.7" disallow_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true @@ -29,6 +30,7 @@ implicit_reexport = true # strict = true # TODO: remove when 'gitdb' is fully annotated +exclude = ["^git/ext/gitdb"] [[tool.mypy.overrides]] module = "gitdb.*" ignore_missing_imports = true @@ -43,3 +45,4 @@ omit = ["*/git/ext/*"] [tool.black] line-length = 120 target-version = ['py37'] +exclude = "git/ext/gitdb" diff --git a/requirements-dev.txt b/requirements-dev.txt index bacde3498..946b4c94f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pytest-icdiff # pytest-profiling -tox \ No newline at end of file +tox diff --git a/test/test_git.py b/test/test_git.py index c5d871f08..540ea9f41 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -4,10 +4,12 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +import contextlib import os +import shutil import subprocess import sys -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from unittest import mock from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd @@ -20,6 +22,17 @@ from git.compat import is_win +@contextlib.contextmanager +def _chdir(new_dir): + """Context manager to temporarily change directory. Not reentrant.""" + old_dir = os.getcwd() + os.chdir(new_dir) + try: + yield + finally: + os.chdir(old_dir) + + class TestGit(TestBase): @classmethod def setUpClass(cls): @@ -75,6 +88,23 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): def test_it_executes_git_to_shell_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") + def test_it_executes_git_not_from_cwd(self): + with TemporaryDirectory() as tmpdir: + if is_win: + # Copy an actual binary executable that is not git. + other_exe_path = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe") + impostor_path = os.path.join(tmpdir, "git.exe") + shutil.copy(other_exe_path, impostor_path) + else: + # Create a shell script that doesn't do anything. + impostor_path = os.path.join(tmpdir, "git") + with open(impostor_path, mode="w", encoding="utf-8") as file: + print("#!/bin/sh", file=file) + os.chmod(impostor_path, 0o755) + + with _chdir(tmpdir): + self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b") + def test_it_accepts_stdin(self): filename = fixture_path("cat_file_blob") with open(filename, "r") as fh: diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py new file mode 100644 index 000000000..eaee4e581 --- /dev/null +++ b/test/test_quick_doc.py @@ -0,0 +1,224 @@ +import pytest + + +from test.lib import TestBase +from test.lib.helper import with_rw_directory + + +class QuickDoc(TestBase): + def tearDown(self): + import gc + + gc.collect() + + @with_rw_directory + def test_init_repo_object(self, path_to_dir): + + # [1-test_init_repo_object] + # $ git init + + from git import Repo + + repo = Repo.init(path_to_dir) + # ![1-test_init_repo_object] + + # [2-test_init_repo_object] + repo = Repo(path_to_dir) + # ![2-test_init_repo_object] + + @with_rw_directory + def test_cloned_repo_object(self, local_dir): + + from git import Repo + import git + # code to clone from url + # [1-test_cloned_repo_object] + # $ git clone + + repo_url = "https://github.com/gitpython-developers/QuickStartTutorialFiles.git" + + repo = Repo.clone_from(repo_url, local_dir) + # ![1-test_cloned_repo_object] + + # code to add files + # [2-test_cloned_repo_object] + # We must make a change to a file so that we can add the update to git + + update_file = 'dir1/file2.txt' # we'll use local_dir/dir1/file2.txt + with open(f"{local_dir}/{update_file}", 'a') as f: + f.write('\nUpdate version 2') + # ![2-test_cloned_repo_object] + + # [3-test_cloned_repo_object] + # $ git add + add_file = [update_file] # relative path from git root + repo.index.add(add_file) # notice the add function requires a list of paths + # ![3-test_cloned_repo_object] + + # code to commit - not sure how to test this + # [4-test_cloned_repo_object] + # $ git commit -m + repo.index.commit("Update to file2") + # ![4-test_cloned_repo_object] + + # [5-test_cloned_repo_object] + # $ git log + + # relative path from git root + repo.iter_commits(all=True, max_count=10, paths=update_file) # gets the last 10 commits from all branches + + # Outputs: + + # ![5-test_cloned_repo_object] + + # [6-test_cloned_repo_object] + commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=update_file) + commits_for_file = [c for c in commits_for_file_generator] + commits_for_file + + # Outputs: [, + # ] + # ![6-test_cloned_repo_object] + + # Untracked files - create new file + # [7-test_cloned_repo_object] + f = open(f'{local_dir}/untracked.txt', 'w') # creates an empty file + f.close() + # ![7-test_cloned_repo_object] + + # [8-test_cloned_repo_object] + repo.untracked_files + # Output: ['untracked.txt'] + # ![8-test_cloned_repo_object] + + # Modified files + # [9-test_cloned_repo_object] + # Let's modify one of our tracked files + + with open(f'{local_dir}/Downloads/file3.txt', 'w') as f: + f.write('file3 version 2') # overwrite file 3 + # ![9-test_cloned_repo_object] + + # [10-test_cloned_repo_object] + repo.index.diff(None) # compares staging area to working directory + + # Output: [, + # ] + # ![10-test_cloned_repo_object] + + # [11-test_cloned_repo_object] + diffs = repo.index.diff(None) + for d in diffs: + print(d.a_path) + + # Output + # Downloads/file3.txt + # ![11-test_cloned_repo_object] + + # compares staging area to head commit + # [11.1-test_cloned_repo_object] + diffs = repo.index.diff(repo.head.commit) + for d in diffs: + print(d.a_path) + + # Output + + # ![11.1-test_cloned_repo_object] + # [11.2-test_cloned_repo_object] + # lets add untracked.txt + repo.index.add(['untracked.txt']) + diffs = repo.index.diff(repo.head.commit) + for d in diffs: + print(d.a_path) + + # Output + # untracked.txt + # ![11.2-test_cloned_repo_object] + + # Compare commit to commit + # [11.3-test_cloned_repo_object] + first_commit = [c for c in repo.iter_commits(all=True)][-1] + diffs = repo.head.commit.diff(first_commit) + for d in diffs: + print(d.a_path) + + # Output + # dir1/file2.txt + # ![11.3-test_cloned_repo_object] + + + + '''Trees and Blobs''' + + # Latest commit tree + # [12-test_cloned_repo_object] + tree = repo.head.commit.tree + # ![12-test_cloned_repo_object] + + # Previous commit tree + # [13-test_cloned_repo_object] + prev_commits = [c for c in repo.iter_commits(all=True, max_count=10)] # last 10 commits from all branches + tree = prev_commits[0].tree + # ![13-test_cloned_repo_object] + + # Iterating through tree + # [14-test_cloned_repo_object] + files_and_dirs = [(entry, entry.name, entry.type) for entry in tree] + files_and_dirs + + # Output + # [(< git.Tree "SHA1-HEX_HASH" >, 'Downloads', 'tree'), + # (< git.Tree "SHA1-HEX_HASH" >, 'dir1', 'tree'), + # (< git.Blob "SHA1-HEX_HASH" >, 'file4.txt', 'blob')] + # ![14-test_cloned_repo_object] + + # [15-test_cloned_repo_object] + def print_files_from_git(root, level=0): + for entry in root: + print(f'{"-" * 4 * level}| {entry.path}, {entry.type}') + if entry.type == "tree": + print_files_from_git(entry, level + 1) + + # ![15-test_cloned_repo_object] + + # [16-test_cloned_repo_object] + print_files_from_git(tree) + + # Output + # | Downloads, tree + # ----| Downloads / file3.txt, blob + # | dir1, tree + # ----| dir1 / file1.txt, blob + # ----| dir1 / file2.txt, blob + # | file4.txt, blob + # # ![16-test_cloned_repo_object] + + # Printing text files + # [17-test_cloned_repo_object] + print_file = 'dir1/file2.txt' + tree[print_file] # the head commit tree + + # Output + # ![17-test_cloned_repo_object] + + # print latest file + # [18-test_cloned_repo_object] + blob = tree[print_file] + print(blob.data_stream.read().decode()) + + # Output + # file 2 version 1 + # Update version 2 + # ![18-test_cloned_repo_object] + + # print previous tree + # [18.1-test_cloned_repo_object] + commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + tree = commits_for_file[-1].tree # gets the first commit tree + blob = tree[print_file] + + print(blob.data_stream.read().decode()) + + # Output + # file 2 version 1 + # ![18.1-test_cloned_repo_object] \ No newline at end of file diff --git a/test/test_refs.py b/test/test_refs.py index 5bb83100e..4c421767e 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -15,6 +15,7 @@ SymbolicReference, GitCommandError, RefLog, + GitConfigParser, ) from git.objects.tag import TagObject from test.lib import TestBase, with_rw_repo @@ -172,6 +173,26 @@ def test_heads(self, rwrepo): assert log[0].oldhexsha == pcommit.NULL_HEX_SHA assert log[0].newhexsha == pcommit.hexsha + @with_rw_repo("HEAD", bare=False) + def test_set_tracking_branch_with_import(self, rwrepo): + # prepare included config file + included_config = osp.join(rwrepo.git_dir, "config.include") + with GitConfigParser(included_config, read_only=False) as writer: + writer.set_value("test", "value", "test") + assert osp.exists(included_config) + + with rwrepo.config_writer() as writer: + writer.set_value("include", "path", included_config) + + for head in rwrepo.heads: + head.set_tracking_branch(None) + assert head.tracking_branch() is None + remote_ref = rwrepo.remotes[0].refs[0] + assert head.set_tracking_branch(remote_ref) is head + assert head.tracking_branch() == remote_ref + head.set_tracking_branch(None) + assert head.tracking_branch() is None + def test_refs(self): types_found = set() for ref in self.rorepo.refs: diff --git a/test/test_repo.py b/test/test_repo.py index 5c66aeeb1..08ed13a00 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -13,7 +13,7 @@ import pickle import sys import tempfile -from unittest import mock, skipIf, SkipTest +from unittest import mock, skipIf, SkipTest, skip import pytest @@ -251,6 +251,7 @@ def test_clone_from_with_path_contains_unicode(self): self.fail("Raised UnicodeEncodeError") @with_rw_directory + @skip("the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control") def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" try: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy