From ea43defd777a9c0751fc44a9c6a622fc2dbd18a0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Apr 2021 18:44:45 +0800 Subject: [PATCH 0001/1225] Run actions on main branch --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5e94cd05e..eb5c894e9 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: @@ -56,4 +56,4 @@ jobs: run: | set -x pip install -r doc/requirements.txt - make -C doc html \ No newline at end of file + make -C doc html From 78b99d35f04dc96596a751376656f1df1fba09c1 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 8 Aug 2021 21:12:35 +0100 Subject: [PATCH 0002/1225] fix setup.py classifiers, improvefnmatchprocess handler types --- .gitignore | 1 + git/cmd.py | 42 ++++++++++++++++++++++++++++-------------- requirements-dev.txt | 3 +++ setup.py | 4 ++-- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 2bae74e5f..72da84eee 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ nbproject .pytest_cache/ monkeytype.sqlite3 output.txt +tox.ini diff --git a/git/cmd.py b/git/cmd.py index b84c43df3..353cbf033 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -3,7 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - +from __future__ import annotations from contextlib import contextmanager import io import logging @@ -68,7 +68,7 @@ # Documentation ## @{ -def handle_process_output(process: Union[subprocess.Popen, 'Git.AutoInterrupt'], +def handle_process_output(process: 'Git.AutoInterrupt' | Popen, stdout_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None], @@ -78,7 +78,8 @@ def handle_process_output(process: Union[subprocess.Popen, 'Git.AutoInterrupt'], Callable[[List[AnyStr]], None]], finalizer: Union[None, Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, - decode_streams: bool = True) -> None: + decode_streams: bool = True, + timeout: float = 10.0) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -93,9 +94,10 @@ def handle_process_output(process: Union[subprocess.Popen, 'Git.AutoInterrupt'], their contents to handlers. Set it to False if `universal_newline == True` (then streams are in text-mode) or if decoding must happen later (i.e. for Diffs). + :param timeout: float, timeout to pass to t.join() in case it hangs. Default = 10.0 seconds """ # Use 2 "pump" threads and wait for both to finish. - def pump_stream(cmdline: str, name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, + def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, handler: Union[None, Callable[[Union[bytes, str]], None]]) -> None: try: for line in stream: @@ -107,22 +109,34 @@ def pump_stream(cmdline: str, name: str, stream: Union[BinaryIO, TextIO], is_dec else: handler(line) except Exception as ex: - log.error("Pumping %r of cmd(%s) failed due to: %r", name, remove_password_if_present(cmdline), ex) - raise CommandError(['<%s-pump>' % name] + remove_password_if_present(cmdline), ex) from ex + log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)})} failed due to: {ex!r}") + raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex finally: stream.close() - cmdline = getattr(process, 'args', '') # PY3+ only + + + if hasattr(process, 'proc'): + process = cast('Git.AutoInterrupt', process) + cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, 'args', '') + p_stdout = process.proc.stdout + p_stderr = process.proc.stderr + else: + process = cast(Popen, process) + cmdline = getattr(process, 'args', '') + p_stdout = process.stdout + p_stderr = process.stderr + if not isinstance(cmdline, (tuple, list)): cmdline = cmdline.split() - pumps = [] - if process.stdout: - pumps.append(('stdout', process.stdout, stdout_handler)) - if process.stderr: - pumps.append(('stderr', process.stderr, stderr_handler)) + pumps: List[Tuple[str, IO, Callable[..., None] | None]] = [] + if p_stdout: + pumps.append(('stdout', p_stdout, stdout_handler)) + if p_stderr: + pumps.append(('stderr', p_stderr, stderr_handler)) - threads = [] + threads: List[threading.Thread] = [] for name, stream, handler in pumps: t = threading.Thread(target=pump_stream, @@ -134,7 +148,7 @@ def pump_stream(cmdline: str, name: str, stream: Union[BinaryIO, TextIO], is_dec ## FIXME: Why Join?? Will block if `stdin` needs feeding... # for t in threads: - t.join() + t.join(timeout=timeout) if finalizer: return finalizer(process) diff --git a/requirements-dev.txt b/requirements-dev.txt index e6d19427e..f3aad629a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,6 @@ flake8-type-checking;python_version>="3.8" # checks for TYPE_CHECKING only # pytest-flake8 pytest-icdiff # pytest-profiling + + +tox \ No newline at end of file diff --git a/setup.py b/setup.py index ae6319f9e..14e36dff9 100755 --- a/setup.py +++ b/setup.py @@ -113,12 +113,12 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", - "Typing:: Typed", + "Typing :: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10" ] ) From 38f5157253beb5801be80812e9b013a3cdd0bdc9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 8 Aug 2021 21:42:34 +0100 Subject: [PATCH 0003/1225] add type check to conf_encoding (in thoery could be bool or int) --- git/cmd.py | 8 +- git/config.py | 11 +- git/objects/commit.py | 2 + git/remote.py | 944 ------------------------------------------ 4 files changed, 6 insertions(+), 959 deletions(-) delete mode 100644 git/remote.py diff --git a/git/cmd.py b/git/cmd.py index 353cbf033..ff1dfa343 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -109,18 +109,16 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], else: handler(line) except Exception as ex: - log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)})} failed due to: {ex!r}") + log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex finally: stream.close() - - if hasattr(process, 'proc'): process = cast('Git.AutoInterrupt', process) cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, 'args', '') - p_stdout = process.proc.stdout - p_stderr = process.proc.stderr + 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) cmdline = getattr(process, 'args', '') diff --git a/git/config.py b/git/config.py index cf32d4ba1..cbd66022d 100644 --- a/git/config.py +++ b/git/config.py @@ -31,7 +31,7 @@ # typing------------------------------------------------------- from typing import (Any, Callable, Generic, IO, List, Dict, Sequence, - TYPE_CHECKING, Tuple, TypeVar, Union, cast, overload) + TYPE_CHECKING, Tuple, TypeVar, Union, cast) from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T @@ -709,15 +709,6 @@ def read_only(self) -> bool: """:return: True if this instance may change the configuration file""" return self._read_only - @overload - def get_value(self, section: str, option: str, default: None = None) -> Union[int, float, str, bool]: ... - - @overload - def get_value(self, section: str, option: str, default: str) -> str: ... - - @overload - def get_value(self, section: str, option: str, default: float) -> float: ... - def get_value(self, section: str, option: str, default: Union[int, float, str, bool, None] = None ) -> Union[int, float, str, bool]: # can default or return type include bool? diff --git a/git/objects/commit.py b/git/objects/commit.py index b689167f5..b36cd46d2 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -446,6 +446,8 @@ def create_from_tree(cls, repo: 'Repo', tree: Union[Tree, str], message: str, # assume utf8 encoding enc_section, enc_option = cls.conf_encoding.split('.') conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding) + if not isinstance(conf_encoding, str): + raise TypeError("conf_encoding could not be coerced to str") # if the tree is no object, make sure we create one - otherwise # the created commit object is invalid diff --git a/git/remote.py b/git/remote.py deleted file mode 100644 index 3888506fd..000000000 --- a/git/remote.py +++ /dev/null @@ -1,944 +0,0 @@ -# remote.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -# Module implementing a remote object allowing easy access to git remotes -import logging -import re - -from git.cmd import handle_process_output, Git -from git.compat import (defenc, force_text) -from git.exc import GitCommandError -from git.util import ( - LazyMixin, - IterableObj, - IterableList, - RemoteProgress, - CallableRemoteProgress, -) -from git.util import ( - join_path, -) - -from .config import ( - GitConfigParser, - SectionConstraint, - cp, -) -from .refs import ( - Head, - Reference, - RemoteReference, - SymbolicReference, - TagReference -) - -# typing------------------------------------------------------- - -from typing import (Any, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, - TYPE_CHECKING, Type, Union, cast, overload) - -from git.types import PathLike, Literal, Commit_ish - -if TYPE_CHECKING: - from git.repo.base import Repo - from git.objects.submodule.base import UpdateProgress - # from git.objects.commit import Commit - # from git.objects import Blob, Tree, TagObject - -flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] - -# def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: -# return inp in [' ', '!', '+', '-', '=', '*', 't', '?'] - - -# ------------------------------------------------------------- - - -log = logging.getLogger('git.remote') -log.addHandler(logging.NullHandler()) - - -__all__ = ('RemoteProgress', 'PushInfo', 'FetchInfo', 'Remote') - -#{ Utilities - - -def add_progress(kwargs: Any, git: Git, - progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] - ) -> Any: - """Add the --progress flag to the given kwargs dict if supported by the - git command. If the actual progress in the given progress instance is not - given, we do not request any progress - :return: possibly altered kwargs""" - if progress is not None: - v = git.version_info[:2] - if v >= (1, 7): - kwargs['progress'] = True - # END handle --progress - # END handle progress - return kwargs - -#} END utilities - - -@ overload -def to_progress_instance(progress: None) -> RemoteProgress: - ... - - -@ overload -def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: - ... - - -@ overload -def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: - ... - - -def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, None] - ) -> Union[RemoteProgress, CallableRemoteProgress]: - """Given the 'progress' return a suitable object derived from - RemoteProgress(). - """ - # new API only needs progress as a function - if callable(progress): - return CallableRemoteProgress(progress) - - # where None is passed create a parser that eats the progress - elif progress is None: - return RemoteProgress() - - # assume its the old API with an instance of RemoteProgress. - return progress - - -class PushInfo(IterableObj, object): - """ - Carries information about the result of a push operation of a single head:: - - info = remote.push()[0] - info.flags # bitflags providing more information about the result - info.local_ref # Reference pointing to the local reference that was pushed - # It is None if the ref was deleted. - info.remote_ref_string # path to the remote reference located on the remote side - info.remote_ref # Remote Reference on the local side corresponding to - # the remote_ref_string. It can be a TagReference as well. - info.old_commit # commit at which the remote_ref was standing before we pushed - # it to local_ref.commit. Will be None if an error was indicated - info.summary # summary line providing human readable english text about the push - """ - __slots__ = ('local_ref', 'remote_ref_string', 'flags', '_old_commit_sha', '_remote', 'summary') - _id_attribute_ = 'pushinfo' - - NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ - FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)] - - _flag_map = {'X': NO_MATCH, - '-': DELETED, - '*': 0, - '+': FORCED_UPDATE, - ' ': FAST_FORWARD, - '=': UP_TO_DATE, - '!': ERROR} - - def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote_ref_string: str, remote: 'Remote', - old_commit: Optional[str] = None, summary: str = '') -> None: - """ Initialize a new instance - local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None """ - self.flags = flags - self.local_ref = local_ref - self.remote_ref_string = remote_ref_string - self._remote = remote - self._old_commit_sha = old_commit - self.summary = summary - - @ property - def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]: - return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None - - @ property - def remote_ref(self) -> Union[RemoteReference, TagReference]: - """ - :return: - Remote Reference or TagReference in the local repository corresponding - to the remote_ref_string kept in this instance.""" - # translate heads to a local remote, tags stay as they are - if self.remote_ref_string.startswith("refs/tags"): - return TagReference(self._remote.repo, self.remote_ref_string) - elif self.remote_ref_string.startswith("refs/heads"): - remote_ref = Reference(self._remote.repo, self.remote_ref_string) - return RemoteReference(self._remote.repo, "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name)) - else: - raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) - # END - - @ classmethod - def _from_line(cls, remote: 'Remote', line: str) -> 'PushInfo': - """Create a new PushInfo instance as parsed from line which is expected to be like - refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes""" - control_character, from_to, summary = line.split('\t', 3) - flags = 0 - - # control character handling - try: - flags |= cls._flag_map[control_character] - except KeyError as e: - raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e - # END handle control character - - # from_to handling - from_ref_string, to_ref_string = from_to.split(':') - if flags & cls.DELETED: - from_ref: Union[SymbolicReference, None] = None - else: - if from_ref_string == "(delete)": - from_ref = None - else: - from_ref = Reference.from_path(remote.repo, from_ref_string) - - # commit handling, could be message or commit info - old_commit: Optional[str] = None - if summary.startswith('['): - if "[rejected]" in summary: - flags |= cls.REJECTED - elif "[remote rejected]" in summary: - flags |= cls.REMOTE_REJECTED - elif "[remote failure]" in summary: - flags |= cls.REMOTE_FAILURE - elif "[no match]" in summary: - flags |= cls.ERROR - elif "[new tag]" in summary: - flags |= cls.NEW_TAG - elif "[new branch]" in summary: - flags |= cls.NEW_HEAD - # uptodate encoded in control character - else: - # fast-forward or forced update - was encoded in control character, - # but we parse the old and new commit - split_token = "..." - if control_character == " ": - split_token = ".." - old_sha, _new_sha = summary.split(' ')[0].split(split_token) - # have to use constructor here as the sha usually is abbreviated - old_commit = old_sha - # END message handling - - return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) - - @ classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any - ) -> NoReturn: # -> Iterator['PushInfo']: - raise NotImplementedError - - -class FetchInfo(IterableObj, object): - - """ - Carries information about the results of a fetch operation of a single head:: - - info = remote.fetch()[0] - info.ref # Symbolic Reference or RemoteReference to the changed - # remote head or FETCH_HEAD - info.flags # additional flags to be & with enumeration members, - # i.e. info.flags & info.REJECTED - # is 0 if ref is SymbolicReference - info.note # additional notes given by git-fetch intended for the user - info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, - # field is set to the previous location of ref, otherwise None - info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref - """ - __slots__ = ('ref', 'old_commit', 'flags', 'note', 'remote_ref_path') - _id_attribute_ = 'fetchinfo' - - NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ - FAST_FORWARD, ERROR = [1 << x for x in range(8)] - - _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') - - _flag_map: Dict[flagKeyLiteral, int] = { - '!': ERROR, - '+': FORCED_UPDATE, - '*': 0, - '=': HEAD_UPTODATE, - ' ': FAST_FORWARD, - '-': TAG_UPDATE, - } - - @ classmethod - def refresh(cls) -> Literal[True]: - """This gets called by the refresh function (see the top level - __init__). - """ - # clear the old values in _flag_map - try: - del cls._flag_map["t"] - except KeyError: - pass - - try: - del cls._flag_map["-"] - except KeyError: - pass - - # set the value given the git version - if Git().version_info[:2] >= (2, 10): - cls._flag_map["t"] = cls.TAG_UPDATE - else: - cls._flag_map["-"] = cls.TAG_UPDATE - - return True - - def __init__(self, ref: SymbolicReference, flags: int, note: str = '', - old_commit: Union[Commit_ish, None] = None, - remote_ref_path: Optional[PathLike] = None) -> None: - """ - Initialize a new instance - """ - self.ref = ref - self.flags = flags - self.note = note - self.old_commit = old_commit - self.remote_ref_path = remote_ref_path - - def __str__(self) -> str: - return self.name - - @ property - def name(self) -> str: - """:return: Name of our remote ref""" - return self.ref.name - - @ property - def commit(self) -> Commit_ish: - """:return: Commit of our remote ref""" - return self.ref.commit - - @ classmethod - def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': - """Parse information from the given line as returned by git-fetch -v - and return a new FetchInfo object representing this information. - - We can handle a line as follows: - "%c %-\\*s %-\\*s -> %s%s" - - Where c is either ' ', !, +, -, \\*, or = - ! means error - + means success forcing update - - means a tag was updated - * means birth of new branch or tag - = means the head was up to date ( and not moved ) - ' ' means a fast-forward - - fetch line is the corresponding line from FETCH_HEAD, like - acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo""" - match = cls._re_fetch_result.match(line) - if match is None: - raise ValueError("Failed to parse line: %r" % line) - - # parse lines - remote_local_ref_str: str - control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() - # assert is_flagKeyLiteral(control_character), f"{control_character}" - control_character = cast(flagKeyLiteral, control_character) - try: - _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") - ref_type_name, fetch_note = fetch_note.split(' ', 1) - except ValueError as e: # unpack error - raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e - - # parse flags from control_character - flags = 0 - try: - flags |= cls._flag_map[control_character] - except KeyError as e: - raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e - # END control char exception handling - - # parse operation string for more info - makes no sense for symbolic refs, but we parse it anyway - old_commit: Union[Commit_ish, None] = None - is_tag_operation = False - if 'rejected' in operation: - flags |= cls.REJECTED - if 'new tag' in operation: - flags |= cls.NEW_TAG - is_tag_operation = True - if 'tag update' in operation: - flags |= cls.TAG_UPDATE - is_tag_operation = True - if 'new branch' in operation: - flags |= cls.NEW_HEAD - if '...' in operation or '..' in operation: - split_token = '...' - if control_character == ' ': - split_token = split_token[:-1] - old_commit = repo.rev_parse(operation.split(split_token)[0]) - # END handle refspec - - # handle FETCH_HEAD and figure out ref type - # If we do not specify a target branch like master:refs/remotes/origin/master, - # the fetch result is stored in FETCH_HEAD which destroys the rule we usually - # have. In that case we use a symbolic reference which is detached - ref_type: Optional[Type[SymbolicReference]] = None - if remote_local_ref_str == "FETCH_HEAD": - ref_type = SymbolicReference - elif ref_type_name == "tag" or is_tag_operation: - # the ref_type_name can be branch, whereas we are still seeing a tag operation. It happens during - # testing, which is based on actual git operations - ref_type = TagReference - elif ref_type_name in ("remote-tracking", "branch"): - # note: remote-tracking is just the first part of the 'remote-tracking branch' token. - # We don't parse it correctly, but its enough to know what to do, and its new in git 1.7something - ref_type = RemoteReference - elif '/' in ref_type_name: - # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*', and is thus pretty - # much anything the user wants, we will have trouble to determine what's going on - # For now, we assume the local ref is a Head - ref_type = Head - else: - raise TypeError("Cannot handle reference type: %r" % ref_type_name) - # END handle ref type - - # create ref instance - if ref_type is SymbolicReference: - remote_local_ref = ref_type(repo, "FETCH_HEAD") - else: - # determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories. - # It is not clear sometimes where exactly the item is, unless we have an absolute path as indicated - # by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the - # 'tags/' subdirectory in its path. - # We don't want to test for actual existence, but try to figure everything out analytically. - ref_path: Optional[PathLike] = None - remote_local_ref_str = remote_local_ref_str.strip() - - if remote_local_ref_str.startswith(Reference._common_path_default + "/"): - # always use actual type if we get absolute paths - # Will always be the case if something is fetched outside of refs/remotes (if its not a tag) - ref_path = remote_local_ref_str - if ref_type is not TagReference and not \ - remote_local_ref_str.startswith(RemoteReference._common_path_default + "/"): - ref_type = Reference - # END downgrade remote reference - elif ref_type is TagReference and 'tags/' in remote_local_ref_str: - # even though its a tag, it is located in refs/remotes - ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str) - else: - ref_path = join_path(ref_type._common_path_default, remote_local_ref_str) - # END obtain refpath - - # even though the path could be within the git conventions, we make - # sure we respect whatever the user wanted, and disabled path checking - remote_local_ref = ref_type(repo, ref_path, check_path=False) - # END create ref instance - - note = (note and note.strip()) or '' - - return cls(remote_local_ref, flags, note, old_commit, local_remote_ref) - - @ classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any - ) -> NoReturn: # -> Iterator['FetchInfo']: - raise NotImplementedError - - -class Remote(LazyMixin, IterableObj): - - """Provides easy read and write access to a git remote. - - Everything not part of this interface is considered an option for the current - remote, allowing constructs like remote.pushurl to query the pushurl. - - NOTE: When querying configuration, the configuration accessor will be cached - to speed up subsequent accesses.""" - - __slots__ = ("repo", "name", "_config_reader") - _id_attribute_ = "name" - - def __init__(self, repo: 'Repo', name: str) -> None: - """Initialize a remote instance - - :param repo: The repository we are a remote of - :param name: the name of the remote, i.e. 'origin'""" - self.repo = repo - self.name = name - self.url: str - - def __getattr__(self, attr: str) -> Any: - """Allows to call this instance like - remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" - if attr == "_config_reader": - return super(Remote, self).__getattr__(attr) - - # sometimes, probably due to a bug in python itself, we are being called - # even though a slot of the same name exists - try: - return self._config_reader.get(attr) - except cp.NoOptionError: - return super(Remote, self).__getattr__(attr) - # END handle exception - - def _config_section_name(self) -> str: - return 'remote "%s"' % self.name - - def _set_cache_(self, attr: str) -> None: - if attr == "_config_reader": - # NOTE: This is cached as __getattr__ is overridden to return remote config values implicitly, such as - # in print(r.pushurl) - self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name()) - else: - super(Remote, self)._set_cache_(attr) - - def __str__(self) -> str: - return self.name - - def __repr__(self) -> str: - return '' % (self.__class__.__name__, self.name) - - def __eq__(self, other: object) -> bool: - return isinstance(other, type(self)) and self.name == other.name - - def __ne__(self, other: object) -> bool: - return not (self == other) - - def __hash__(self) -> int: - return hash(self.name) - - def exists(self) -> bool: - """ - :return: True if this is a valid, existing remote. - Valid remotes have an entry in the repository's configuration""" - try: - self.config_reader.get('url') - return True - except cp.NoOptionError: - # we have the section at least ... - return True - except cp.NoSectionError: - return False - # end - - @ classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator['Remote']: - """:return: Iterator yielding Remote objects of the given repository""" - for section in repo.config_reader("repository").sections(): - if not section.startswith('remote '): - continue - lbound = section.find('"') - rbound = section.rfind('"') - if lbound == -1 or rbound == -1: - raise ValueError("Remote-Section has invalid format: %r" % section) - yield Remote(repo, section[lbound + 1:rbound]) - # END for each configuration section - - def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> 'Remote': - """Configure URLs on current remote (cf command git remote set_url) - - This command manages URLs on the remote. - - :param new_url: string being the URL to add as an extra remote URL - :param old_url: when set, replaces this URL with new_url for the remote - :return: self - """ - scmd = 'set-url' - kwargs['insert_kwargs_after'] = scmd - if old_url: - self.repo.git.remote(scmd, self.name, new_url, old_url, **kwargs) - else: - self.repo.git.remote(scmd, self.name, new_url, **kwargs) - return self - - def add_url(self, url: str, **kwargs: Any) -> 'Remote': - """Adds a new url on current remote (special case of git remote set_url) - - This command adds new URLs to a given remote, making it possible to have - multiple URLs for a single remote. - - :param url: string being the URL to add as an extra remote URL - :return: self - """ - return self.set_url(url, add=True) - - def delete_url(self, url: str, **kwargs: Any) -> 'Remote': - """Deletes a new url on current remote (special case of git remote set_url) - - This command deletes new URLs to a given remote, making it possible to have - multiple URLs for a single remote. - - :param url: string being the URL to delete from the remote - :return: self - """ - return self.set_url(url, delete=True) - - @ property - def urls(self) -> Iterator[str]: - """:return: Iterator yielding all configured URL targets on a remote as strings""" - try: - remote_details = self.repo.git.remote("get-url", "--all", self.name) - assert isinstance(remote_details, str) - for line in remote_details.split('\n'): - yield line - except GitCommandError as ex: - ## We are on git < 2.7 (i.e TravisCI as of Oct-2016), - # so `get-utl` command does not exist yet! - # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319 - # and: http://stackoverflow.com/a/32991784/548792 - # - if 'Unknown subcommand: get-url' in str(ex): - try: - remote_details = self.repo.git.remote("show", self.name) - assert isinstance(remote_details, str) - for line in remote_details.split('\n'): - if ' Push URL:' in line: - yield line.split(': ')[-1] - except GitCommandError as _ex: - if any(msg in str(_ex) for msg in ['correct access rights', 'cannot run ssh']): - # If ssh is not setup to access this repository, see issue 694 - remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name) - assert isinstance(remote_details, str) - for line in remote_details.split('\n'): - yield line - else: - raise _ex - else: - raise ex - - @ property - def refs(self) -> IterableList[RemoteReference]: - """ - :return: - IterableList of RemoteReference objects. It is prefixed, allowing - you to omit the remote path portion, i.e.:: - remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" - out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) - out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) - return out_refs - - @ property - def stale_refs(self) -> IterableList[Reference]: - """ - :return: - IterableList RemoteReference objects that do not have a corresponding - head in the remote reference anymore as they have been deleted on the - remote side, but are still available locally. - - The IterableList is prefixed, hence the 'origin' must be omitted. See - 'refs' property for an example. - - To make things more complicated, it can be possible for the list to include - other kinds of references, for example, tag references, if these are stale - as well. This is a fix for the issue described here: - https://github.com/gitpython-developers/GitPython/issues/260 - """ - out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) - for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: - # expecting - # * [would prune] origin/new_branch - token = " * [would prune] " - if not line.startswith(token): - continue - ref_name = line.replace(token, "") - # sometimes, paths start with a full ref name, like refs/tags/foo, see #260 - if ref_name.startswith(Reference._common_path_default + '/'): - out_refs.append(Reference.from_path(self.repo, ref_name)) - else: - fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name) - out_refs.append(RemoteReference(self.repo, fqhn)) - # end special case handling - # END for each line - return out_refs - - @ classmethod - def create(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote': - """Create a new remote to the given repository - :param repo: Repository instance that is to receive the new remote - :param name: Desired name of the remote - :param url: URL which corresponds to the remote's name - :param kwargs: Additional arguments to be passed to the git-remote add command - :return: New Remote instance - :raise GitCommandError: in case an origin with that name already exists""" - scmd = 'add' - kwargs['insert_kwargs_after'] = scmd - repo.git.remote(scmd, name, Git.polish_url(url), **kwargs) - return cls(repo, name) - - # add is an alias - add = create - - @ classmethod - def remove(cls, repo: 'Repo', name: str) -> str: - """Remove the remote with the given name - :return: the passed remote name to remove - """ - repo.git.remote("rm", name) - if isinstance(name, cls): - name._clear_cache() - return name - - # alias - rm = remove - - def rename(self, new_name: str) -> 'Remote': - """Rename self to the given new_name - :return: self """ - if self.name == new_name: - return self - - self.repo.git.remote("rename", self.name, new_name) - self.name = new_name - self._clear_cache() - - return self - - def update(self, **kwargs: Any) -> 'Remote': - """Fetch all changes for this remote, including new branches which will - be forced in ( in case your local remote branch is not part the new remote branches - ancestry anymore ). - - :param kwargs: - Additional arguments passed to git-remote update - - :return: self """ - scmd = 'update' - kwargs['insert_kwargs_after'] = scmd - self.repo.git.remote(scmd, self.name, **kwargs) - return self - - def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', - progress: Union[Callable[..., Any], RemoteProgress, None] - ) -> IterableList['FetchInfo']: - - progress = to_progress_instance(progress) - - # skip first line as it is some remote info we are not interested in - output: IterableList['FetchInfo'] = IterableList('name') - - # lines which are no progress are fetch info lines - # this also waits for the command to finish - # Skip some progress lines that don't provide relevant information - fetch_info_lines = [] - # Basically we want all fetch info lines which appear to be in regular form, and thus have a - # command character. Everything else we ignore, - cmds = set(FetchInfo._flag_map.keys()) - - progress_handler = progress.new_message_handler() - handle_process_output(proc, None, progress_handler, finalizer=None, decode_streams=False) - - stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' - proc.wait(stderr=stderr_text) - if stderr_text: - log.warning("Error lines received while fetching: %s", stderr_text) - - for line in progress.other_lines: - line = force_text(line) - for cmd in cmds: - if len(line) > 1 and line[0] == ' ' and line[1] == cmd: - fetch_info_lines.append(line) - continue - - # read head information - fetch_head = SymbolicReference(self.repo, "FETCH_HEAD") - with open(fetch_head.abspath, 'rb') as fp: - fetch_head_info = [line.decode(defenc) for line in fp.readlines()] - - l_fil = len(fetch_info_lines) - l_fhi = len(fetch_head_info) - if l_fil != l_fhi: - msg = "Fetch head lines do not match lines provided via progress information\n" - msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n" - msg += "Will ignore extra progress lines or fetch head lines." - msg %= (l_fil, l_fhi) - log.debug(msg) - log.debug("info lines: " + str(fetch_info_lines)) - log.debug("head info : " + str(fetch_head_info)) - if l_fil < l_fhi: - fetch_head_info = fetch_head_info[:l_fil] - else: - fetch_info_lines = fetch_info_lines[:l_fhi] - # end truncate correct list - # end sanity check + sanitization - - for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info): - try: - output.append(FetchInfo._from_line(self.repo, err_line, fetch_line)) - except ValueError as exc: - log.debug("Caught error while parsing line: %s", exc) - log.warning("Git informed while fetching: %s", err_line.strip()) - return output - - def _get_push_info(self, proc: 'Git.AutoInterrupt', - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: - progress = to_progress_instance(progress) - - # read progress information from stderr - # we hope stdout can hold all the data, it should ... - # read the lines manually as it will use carriage returns between the messages - # to override the previous one. This is why we read the bytes manually - progress_handler = progress.new_message_handler() - output: IterableList[PushInfo] = IterableList('push_infos') - - def stdout_handler(line: str) -> None: - try: - output.append(PushInfo._from_line(self, line)) - except ValueError: - # If an error happens, additional info is given which we parse below. - pass - - handle_process_output(proc, stdout_handler, progress_handler, finalizer=None, decode_streams=False) - stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' - try: - proc.wait(stderr=stderr_text) - except Exception: - if not output: - raise - elif stderr_text: - log.warning("Error lines received while fetching: %s", stderr_text) - - return output - - def _assert_refspec(self) -> None: - """Turns out we can't deal with remotes if the refspec is missing""" - config = self.config_reader - unset = 'placeholder' - try: - if config.get_value('fetch', default=unset) is unset: - msg = "Remote '%s' has no refspec set.\n" - msg += "You can set it as follows:" - msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'." - raise AssertionError(msg % (self.name, self.name)) - finally: - config.release() - - def fetch(self, refspec: Union[str, List[str], None] = None, - progress: Union[RemoteProgress, None, 'UpdateProgress'] = None, - verbose: bool = True, **kwargs: Any) -> IterableList[FetchInfo]: - """Fetch the latest changes for this remote - - :param refspec: - A "refspec" is used by fetch and push to describe the mapping - between remote ref and local ref. They are combined with a colon in - the format :, preceded by an optional plus sign, +. - For example: git fetch $URL refs/heads/master:refs/heads/origin means - "grab the master branch head from the $URL and store it as my origin - branch head". And git push $URL refs/heads/master:refs/heads/to-upstream - means "publish my master branch head as to-upstream branch at $URL". - See also git-push(1). - - Taken from the git manual - - Fetch supports multiple refspecs (as the - underlying git-fetch does) - supplying a list rather than a string - for 'refspec' will make use of this facility. - :param progress: See 'push' method - :param verbose: Boolean for verbose output - :param kwargs: Additional arguments to be passed to git-fetch - :return: - IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed - information about the fetch results - - :note: - As fetch does not provide progress information to non-ttys, we cannot make - it available here unfortunately as in the 'push' method.""" - if refspec is None: - # No argument refspec, then ensure the repo's config has a fetch refspec. - self._assert_refspec() - - kwargs = add_progress(kwargs, self.repo.git, progress) - if isinstance(refspec, list): - args: Sequence[Optional[str]] = refspec - else: - args = [refspec] - - proc = self.repo.git.fetch(self, *args, as_process=True, with_stdout=False, - universal_newlines=True, v=verbose, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress) - if hasattr(self.repo.odb, 'update_cache'): - self.repo.odb.update_cache() - return res - - def pull(self, refspec: Union[str, List[str], None] = None, - progress: Union[RemoteProgress, 'UpdateProgress', None] = None, - **kwargs: Any) -> IterableList[FetchInfo]: - """Pull changes from the given branch, being the same as a fetch followed - by a merge of branch with your local branch. - - :param refspec: see 'fetch' method - :param progress: see 'push' method - :param kwargs: Additional arguments to be passed to git-pull - :return: Please see 'fetch' method """ - if refspec is None: - # No argument refspec, then ensure the repo's config has a fetch refspec. - self._assert_refspec() - kwargs = add_progress(kwargs, self.repo.git, progress) - proc = self.repo.git.pull(self, refspec, with_stdout=False, as_process=True, - universal_newlines=True, v=True, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress) - if hasattr(self.repo.odb, 'update_cache'): - self.repo.odb.update_cache() - return res - - def push(self, refspec: Union[str, List[str], None] = None, - progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None, - **kwargs: Any) -> IterableList[PushInfo]: - """Push changes from source branch in refspec to target branch in refspec. - - :param refspec: see 'fetch' method - :param progress: - Can take one of many value types: - - * None to discard progress information - * A function (callable) that is called with the progress information. - Signature: ``progress(op_code, cur_count, max_count=None, message='')``. - `Click here `__ for a description of all arguments - given to the function. - * An instance of a class derived from ``git.RemoteProgress`` that - overrides the ``update()`` function. - - :note: No further progress information is returned after push returns. - :param kwargs: Additional arguments to be passed to git-push - :return: - list(PushInfo, ...) list of PushInfo instances, each - one informing about an individual head which had been updated on the remote - side. - If the push contains rejected heads, these will have the PushInfo.ERROR bit set - in their flags. - If the operation fails completely, the length of the returned IterableList will - be 0.""" - kwargs = add_progress(kwargs, self.repo.git, progress) - proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, - universal_newlines=True, **kwargs) - return self._get_push_info(proc, progress) - - @ property - def config_reader(self) -> SectionConstraint[GitConfigParser]: - """ - :return: - GitConfigParser compatible object able to read options for only our remote. - Hence you may simple type config.get("pushurl") to obtain the information""" - return self._config_reader - - def _clear_cache(self) -> None: - try: - del(self._config_reader) - except AttributeError: - pass - # END handle exception - - @ property - def config_writer(self) -> SectionConstraint: - """ - :return: GitConfigParser compatible object able to write options for this remote. - :note: - You can only own one writer at a time - delete it to release the - configuration file and make it usable by others. - - To assure consistent results, you should only query options through the - writer. Once you are done writing, you are free to use the config reader - once again.""" - writer = self.repo.config_writer() - - # clear our cache to assure we re-read the possibly changed configuration - self._clear_cache() - return SectionConstraint(writer, self._config_section_name()) From 22e05c4dc83291321f97ee9d2a369e77f9a4eb1f Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 8 Aug 2021 21:49:34 +0100 Subject: [PATCH 0004/1225] type fix --- git/objects/remote.py | 944 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 944 insertions(+) create mode 100644 git/objects/remote.py diff --git a/git/objects/remote.py b/git/objects/remote.py new file mode 100644 index 000000000..dbff76e55 --- /dev/null +++ b/git/objects/remote.py @@ -0,0 +1,944 @@ +# remote.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +# Module implementing a remote object allowing easy access to git remotes +import logging +import re + +from git.cmd import handle_process_output, Git +from git.compat import (defenc, force_text) +from git.exc import GitCommandError +from git.util import ( + LazyMixin, + IterableObj, + IterableList, + RemoteProgress, + CallableRemoteProgress, +) +from git.util import ( + join_path, +) + +from git.config import ( + GitConfigParser, + SectionConstraint, + cp, +) +from git.refs import ( + Head, + Reference, + RemoteReference, + SymbolicReference, + TagReference +) + +# typing------------------------------------------------------- + +from typing import (Any, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, + TYPE_CHECKING, Type, Union, cast, overload) + +from git.types import PathLike, Literal, Commit_ish + +if TYPE_CHECKING: + from git.repo.base import Repo + from git.objects.submodule.base import UpdateProgress + # from git.objects.commit import Commit + # from git.objects import Blob, Tree, TagObject + +flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] + +# def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: +# return inp in [' ', '!', '+', '-', '=', '*', 't', '?'] + + +# ------------------------------------------------------------- + + +log = logging.getLogger('git.remote') +log.addHandler(logging.NullHandler()) + + +__all__ = ('RemoteProgress', 'PushInfo', 'FetchInfo', 'Remote') + +#{ Utilities + + +def add_progress(kwargs: Any, git: Git, + progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] + ) -> Any: + """Add the --progress flag to the given kwargs dict if supported by the + git command. If the actual progress in the given progress instance is not + given, we do not request any progress + :return: possibly altered kwargs""" + if progress is not None: + v = git.version_info[:2] + if v >= (1, 7): + kwargs['progress'] = True + # END handle --progress + # END handle progress + return kwargs + +#} END utilities + + +@ overload +def to_progress_instance(progress: None) -> RemoteProgress: + ... + + +@ overload +def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: + ... + + +@ overload +def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: + ... + + +def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, None] + ) -> Union[RemoteProgress, CallableRemoteProgress]: + """Given the 'progress' return a suitable object derived from + RemoteProgress(). + """ + # new API only needs progress as a function + if callable(progress): + return CallableRemoteProgress(progress) + + # where None is passed create a parser that eats the progress + elif progress is None: + return RemoteProgress() + + # assume its the old API with an instance of RemoteProgress. + return progress + + +class PushInfo(IterableObj, object): + """ + Carries information about the result of a push operation of a single head:: + + info = remote.push()[0] + info.flags # bitflags providing more information about the result + info.local_ref # Reference pointing to the local reference that was pushed + # It is None if the ref was deleted. + info.remote_ref_string # path to the remote reference located on the remote side + info.remote_ref # Remote Reference on the local side corresponding to + # the remote_ref_string. It can be a TagReference as well. + info.old_commit # commit at which the remote_ref was standing before we pushed + # it to local_ref.commit. Will be None if an error was indicated + info.summary # summary line providing human readable english text about the push + """ + __slots__ = ('local_ref', 'remote_ref_string', 'flags', '_old_commit_sha', '_remote', 'summary') + _id_attribute_ = 'pushinfo' + + NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ + FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)] + + _flag_map = {'X': NO_MATCH, + '-': DELETED, + '*': 0, + '+': FORCED_UPDATE, + ' ': FAST_FORWARD, + '=': UP_TO_DATE, + '!': ERROR} + + def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote_ref_string: str, remote: 'Remote', + old_commit: Optional[str] = None, summary: str = '') -> None: + """ Initialize a new instance + local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None """ + self.flags = flags + self.local_ref = local_ref + self.remote_ref_string = remote_ref_string + self._remote = remote + self._old_commit_sha = old_commit + self.summary = summary + + @ property + def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]: + return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None + + @ property + def remote_ref(self) -> Union[RemoteReference, TagReference]: + """ + :return: + Remote Reference or TagReference in the local repository corresponding + to the remote_ref_string kept in this instance.""" + # translate heads to a local remote, tags stay as they are + if self.remote_ref_string.startswith("refs/tags"): + return TagReference(self._remote.repo, self.remote_ref_string) + elif self.remote_ref_string.startswith("refs/heads"): + remote_ref = Reference(self._remote.repo, self.remote_ref_string) + return RemoteReference(self._remote.repo, "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name)) + else: + raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) + # END + + @ classmethod + def _from_line(cls, remote: 'Remote', line: str) -> 'PushInfo': + """Create a new PushInfo instance as parsed from line which is expected to be like + refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes""" + control_character, from_to, summary = line.split('\t', 3) + flags = 0 + + # control character handling + try: + flags |= cls._flag_map[control_character] + except KeyError as e: + raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e + # END handle control character + + # from_to handling + from_ref_string, to_ref_string = from_to.split(':') + if flags & cls.DELETED: + from_ref: Union[SymbolicReference, None] = None + else: + if from_ref_string == "(delete)": + from_ref = None + else: + from_ref = Reference.from_path(remote.repo, from_ref_string) + + # commit handling, could be message or commit info + old_commit: Optional[str] = None + if summary.startswith('['): + if "[rejected]" in summary: + flags |= cls.REJECTED + elif "[remote rejected]" in summary: + flags |= cls.REMOTE_REJECTED + elif "[remote failure]" in summary: + flags |= cls.REMOTE_FAILURE + elif "[no match]" in summary: + flags |= cls.ERROR + elif "[new tag]" in summary: + flags |= cls.NEW_TAG + elif "[new branch]" in summary: + flags |= cls.NEW_HEAD + # uptodate encoded in control character + else: + # fast-forward or forced update - was encoded in control character, + # but we parse the old and new commit + split_token = "..." + if control_character == " ": + split_token = ".." + old_sha, _new_sha = summary.split(' ')[0].split(split_token) + # have to use constructor here as the sha usually is abbreviated + old_commit = old_sha + # END message handling + + return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) + + @ classmethod + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any + ) -> NoReturn: # -> Iterator['PushInfo']: + raise NotImplementedError + + +class FetchInfo(IterableObj, object): + + """ + Carries information about the results of a fetch operation of a single head:: + + info = remote.fetch()[0] + info.ref # Symbolic Reference or RemoteReference to the changed + # remote head or FETCH_HEAD + info.flags # additional flags to be & with enumeration members, + # i.e. info.flags & info.REJECTED + # is 0 if ref is SymbolicReference + info.note # additional notes given by git-fetch intended for the user + info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, + # field is set to the previous location of ref, otherwise None + info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref + """ + __slots__ = ('ref', 'old_commit', 'flags', 'note', 'remote_ref_path') + _id_attribute_ = 'fetchinfo' + + NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ + FAST_FORWARD, ERROR = [1 << x for x in range(8)] + + _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') + + _flag_map: Dict[flagKeyLiteral, int] = { + '!': ERROR, + '+': FORCED_UPDATE, + '*': 0, + '=': HEAD_UPTODATE, + ' ': FAST_FORWARD, + '-': TAG_UPDATE, + } + + @ classmethod + def refresh(cls) -> Literal[True]: + """This gets called by the refresh function (see the top level + __init__). + """ + # clear the old values in _flag_map + try: + del cls._flag_map["t"] + except KeyError: + pass + + try: + del cls._flag_map["-"] + except KeyError: + pass + + # set the value given the git version + if Git().version_info[:2] >= (2, 10): + cls._flag_map["t"] = cls.TAG_UPDATE + else: + cls._flag_map["-"] = cls.TAG_UPDATE + + return True + + def __init__(self, ref: SymbolicReference, flags: int, note: str = '', + old_commit: Union[Commit_ish, None] = None, + remote_ref_path: Optional[PathLike] = None) -> None: + """ + Initialize a new instance + """ + self.ref = ref + self.flags = flags + self.note = note + self.old_commit = old_commit + self.remote_ref_path = remote_ref_path + + def __str__(self) -> str: + return self.name + + @ property + def name(self) -> str: + """:return: Name of our remote ref""" + return self.ref.name + + @ property + def commit(self) -> Commit_ish: + """:return: Commit of our remote ref""" + return self.ref.commit + + @ classmethod + def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': + """Parse information from the given line as returned by git-fetch -v + and return a new FetchInfo object representing this information. + + We can handle a line as follows: + "%c %-\\*s %-\\*s -> %s%s" + + Where c is either ' ', !, +, -, \\*, or = + ! means error + + means success forcing update + - means a tag was updated + * means birth of new branch or tag + = means the head was up to date ( and not moved ) + ' ' means a fast-forward + + fetch line is the corresponding line from FETCH_HEAD, like + acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo""" + match = cls._re_fetch_result.match(line) + if match is None: + raise ValueError("Failed to parse line: %r" % line) + + # parse lines + remote_local_ref_str: str + control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() + # assert is_flagKeyLiteral(control_character), f"{control_character}" + control_character = cast(flagKeyLiteral, control_character) + try: + _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") + ref_type_name, fetch_note = fetch_note.split(' ', 1) + except ValueError as e: # unpack error + raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e + + # parse flags from control_character + flags = 0 + try: + flags |= cls._flag_map[control_character] + except KeyError as e: + raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e + # END control char exception handling + + # parse operation string for more info - makes no sense for symbolic refs, but we parse it anyway + old_commit: Union[Commit_ish, None] = None + is_tag_operation = False + if 'rejected' in operation: + flags |= cls.REJECTED + if 'new tag' in operation: + flags |= cls.NEW_TAG + is_tag_operation = True + if 'tag update' in operation: + flags |= cls.TAG_UPDATE + is_tag_operation = True + if 'new branch' in operation: + flags |= cls.NEW_HEAD + if '...' in operation or '..' in operation: + split_token = '...' + if control_character == ' ': + split_token = split_token[:-1] + old_commit = repo.rev_parse(operation.split(split_token)[0]) + # END handle refspec + + # handle FETCH_HEAD and figure out ref type + # If we do not specify a target branch like master:refs/remotes/origin/master, + # the fetch result is stored in FETCH_HEAD which destroys the rule we usually + # have. In that case we use a symbolic reference which is detached + ref_type: Optional[Type[SymbolicReference]] = None + if remote_local_ref_str == "FETCH_HEAD": + ref_type = SymbolicReference + elif ref_type_name == "tag" or is_tag_operation: + # the ref_type_name can be branch, whereas we are still seeing a tag operation. It happens during + # testing, which is based on actual git operations + ref_type = TagReference + elif ref_type_name in ("remote-tracking", "branch"): + # note: remote-tracking is just the first part of the 'remote-tracking branch' token. + # We don't parse it correctly, but its enough to know what to do, and its new in git 1.7something + ref_type = RemoteReference + elif '/' in ref_type_name: + # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*', and is thus pretty + # much anything the user wants, we will have trouble to determine what's going on + # For now, we assume the local ref is a Head + ref_type = Head + else: + raise TypeError("Cannot handle reference type: %r" % ref_type_name) + # END handle ref type + + # create ref instance + if ref_type is SymbolicReference: + remote_local_ref = ref_type(repo, "FETCH_HEAD") + else: + # determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories. + # It is not clear sometimes where exactly the item is, unless we have an absolute path as indicated + # by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the + # 'tags/' subdirectory in its path. + # We don't want to test for actual existence, but try to figure everything out analytically. + ref_path: Optional[PathLike] = None + remote_local_ref_str = remote_local_ref_str.strip() + + if remote_local_ref_str.startswith(Reference._common_path_default + "/"): + # always use actual type if we get absolute paths + # Will always be the case if something is fetched outside of refs/remotes (if its not a tag) + ref_path = remote_local_ref_str + if ref_type is not TagReference and not \ + remote_local_ref_str.startswith(RemoteReference._common_path_default + "/"): + ref_type = Reference + # END downgrade remote reference + elif ref_type is TagReference and 'tags/' in remote_local_ref_str: + # even though its a tag, it is located in refs/remotes + ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str) + else: + ref_path = join_path(ref_type._common_path_default, remote_local_ref_str) + # END obtain refpath + + # even though the path could be within the git conventions, we make + # sure we respect whatever the user wanted, and disabled path checking + remote_local_ref = ref_type(repo, ref_path, check_path=False) + # END create ref instance + + note = (note and note.strip()) or '' + + return cls(remote_local_ref, flags, note, old_commit, local_remote_ref) + + @ classmethod + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any + ) -> NoReturn: # -> Iterator['FetchInfo']: + raise NotImplementedError + + +class Remote(LazyMixin, IterableObj): + + """Provides easy read and write access to a git remote. + + Everything not part of this interface is considered an option for the current + remote, allowing constructs like remote.pushurl to query the pushurl. + + NOTE: When querying configuration, the configuration accessor will be cached + to speed up subsequent accesses.""" + + __slots__ = ("repo", "name", "_config_reader") + _id_attribute_ = "name" + + def __init__(self, repo: 'Repo', name: str) -> None: + """Initialize a remote instance + + :param repo: The repository we are a remote of + :param name: the name of the remote, i.e. 'origin'""" + self.repo = repo + self.name = name + self.url: str + + def __getattr__(self, attr: str) -> Any: + """Allows to call this instance like + remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" + if attr == "_config_reader": + return super(Remote, self).__getattr__(attr) + + # sometimes, probably due to a bug in python itself, we are being called + # even though a slot of the same name exists + try: + return self._config_reader.get(attr) + except cp.NoOptionError: + return super(Remote, self).__getattr__(attr) + # END handle exception + + def _config_section_name(self) -> str: + return 'remote "%s"' % self.name + + def _set_cache_(self, attr: str) -> None: + if attr == "_config_reader": + # NOTE: This is cached as __getattr__ is overridden to return remote config values implicitly, such as + # in print(r.pushurl) + self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name()) + else: + super(Remote, self)._set_cache_(attr) + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return '' % (self.__class__.__name__, self.name) + + def __eq__(self, other: object) -> bool: + return isinstance(other, type(self)) and self.name == other.name + + def __ne__(self, other: object) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash(self.name) + + def exists(self) -> bool: + """ + :return: True if this is a valid, existing remote. + Valid remotes have an entry in the repository's configuration""" + try: + self.config_reader.get('url') + return True + except cp.NoOptionError: + # we have the section at least ... + return True + except cp.NoSectionError: + return False + # end + + @ classmethod + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator['Remote']: + """:return: Iterator yielding Remote objects of the given repository""" + for section in repo.config_reader("repository").sections(): + if not section.startswith('remote '): + continue + lbound = section.find('"') + rbound = section.rfind('"') + if lbound == -1 or rbound == -1: + raise ValueError("Remote-Section has invalid format: %r" % section) + yield Remote(repo, section[lbound + 1:rbound]) + # END for each configuration section + + def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> 'Remote': + """Configure URLs on current remote (cf command git remote set_url) + + This command manages URLs on the remote. + + :param new_url: string being the URL to add as an extra remote URL + :param old_url: when set, replaces this URL with new_url for the remote + :return: self + """ + scmd = 'set-url' + kwargs['insert_kwargs_after'] = scmd + if old_url: + self.repo.git.remote(scmd, self.name, new_url, old_url, **kwargs) + else: + self.repo.git.remote(scmd, self.name, new_url, **kwargs) + return self + + def add_url(self, url: str, **kwargs: Any) -> 'Remote': + """Adds a new url on current remote (special case of git remote set_url) + + This command adds new URLs to a given remote, making it possible to have + multiple URLs for a single remote. + + :param url: string being the URL to add as an extra remote URL + :return: self + """ + return self.set_url(url, add=True) + + def delete_url(self, url: str, **kwargs: Any) -> 'Remote': + """Deletes a new url on current remote (special case of git remote set_url) + + This command deletes new URLs to a given remote, making it possible to have + multiple URLs for a single remote. + + :param url: string being the URL to delete from the remote + :return: self + """ + return self.set_url(url, delete=True) + + @ property + def urls(self) -> Iterator[str]: + """:return: Iterator yielding all configured URL targets on a remote as strings""" + try: + remote_details = self.repo.git.remote("get-url", "--all", self.name) + assert isinstance(remote_details, str) + for line in remote_details.split('\n'): + yield line + except GitCommandError as ex: + ## We are on git < 2.7 (i.e TravisCI as of Oct-2016), + # so `get-utl` command does not exist yet! + # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319 + # and: http://stackoverflow.com/a/32991784/548792 + # + if 'Unknown subcommand: get-url' in str(ex): + try: + remote_details = self.repo.git.remote("show", self.name) + assert isinstance(remote_details, str) + for line in remote_details.split('\n'): + if ' Push URL:' in line: + yield line.split(': ')[-1] + except GitCommandError as _ex: + if any(msg in str(_ex) for msg in ['correct access rights', 'cannot run ssh']): + # If ssh is not setup to access this repository, see issue 694 + remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name) + assert isinstance(remote_details, str) + for line in remote_details.split('\n'): + yield line + else: + raise _ex + else: + raise ex + + @ property + def refs(self) -> IterableList[RemoteReference]: + """ + :return: + IterableList of RemoteReference objects. It is prefixed, allowing + you to omit the remote path portion, i.e.:: + remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" + out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) + return out_refs + + @ property + def stale_refs(self) -> IterableList[Reference]: + """ + :return: + IterableList RemoteReference objects that do not have a corresponding + head in the remote reference anymore as they have been deleted on the + remote side, but are still available locally. + + The IterableList is prefixed, hence the 'origin' must be omitted. See + 'refs' property for an example. + + To make things more complicated, it can be possible for the list to include + other kinds of references, for example, tag references, if these are stale + as well. This is a fix for the issue described here: + https://github.com/gitpython-developers/GitPython/issues/260 + """ + out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: + # expecting + # * [would prune] origin/new_branch + token = " * [would prune] " + if not line.startswith(token): + continue + ref_name = line.replace(token, "") + # sometimes, paths start with a full ref name, like refs/tags/foo, see #260 + if ref_name.startswith(Reference._common_path_default + '/'): + out_refs.append(Reference.from_path(self.repo, ref_name)) + else: + fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name) + out_refs.append(RemoteReference(self.repo, fqhn)) + # end special case handling + # END for each line + return out_refs + + @ classmethod + def create(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote': + """Create a new remote to the given repository + :param repo: Repository instance that is to receive the new remote + :param name: Desired name of the remote + :param url: URL which corresponds to the remote's name + :param kwargs: Additional arguments to be passed to the git-remote add command + :return: New Remote instance + :raise GitCommandError: in case an origin with that name already exists""" + scmd = 'add' + kwargs['insert_kwargs_after'] = scmd + repo.git.remote(scmd, name, Git.polish_url(url), **kwargs) + return cls(repo, name) + + # add is an alias + add = create + + @ classmethod + def remove(cls, repo: 'Repo', name: str) -> str: + """Remove the remote with the given name + :return: the passed remote name to remove + """ + repo.git.remote("rm", name) + if isinstance(name, cls): + name._clear_cache() + return name + + # alias + rm = remove + + def rename(self, new_name: str) -> 'Remote': + """Rename self to the given new_name + :return: self """ + if self.name == new_name: + return self + + self.repo.git.remote("rename", self.name, new_name) + self.name = new_name + self._clear_cache() + + return self + + def update(self, **kwargs: Any) -> 'Remote': + """Fetch all changes for this remote, including new branches which will + be forced in ( in case your local remote branch is not part the new remote branches + ancestry anymore ). + + :param kwargs: + Additional arguments passed to git-remote update + + :return: self """ + scmd = 'update' + kwargs['insert_kwargs_after'] = scmd + self.repo.git.remote(scmd, self.name, **kwargs) + return self + + def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', + progress: Union[Callable[..., Any], RemoteProgress, None] + ) -> IterableList['FetchInfo']: + + progress = to_progress_instance(progress) + + # skip first line as it is some remote info we are not interested in + output: IterableList['FetchInfo'] = IterableList('name') + + # lines which are no progress are fetch info lines + # this also waits for the command to finish + # Skip some progress lines that don't provide relevant information + fetch_info_lines = [] + # Basically we want all fetch info lines which appear to be in regular form, and thus have a + # command character. Everything else we ignore, + cmds = set(FetchInfo._flag_map.keys()) + + progress_handler = progress.new_message_handler() + handle_process_output(proc, None, progress_handler, finalizer=None, decode_streams=False) + + stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' + proc.wait(stderr=stderr_text) + if stderr_text: + log.warning("Error lines received while fetching: %s", stderr_text) + + for line in progress.other_lines: + line = force_text(line) + for cmd in cmds: + if len(line) > 1 and line[0] == ' ' and line[1] == cmd: + fetch_info_lines.append(line) + continue + + # read head information + fetch_head = SymbolicReference(self.repo, "FETCH_HEAD") + with open(fetch_head.abspath, 'rb') as fp: + fetch_head_info = [line.decode(defenc) for line in fp.readlines()] + + l_fil = len(fetch_info_lines) + l_fhi = len(fetch_head_info) + if l_fil != l_fhi: + msg = "Fetch head lines do not match lines provided via progress information\n" + msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n" + msg += "Will ignore extra progress lines or fetch head lines." + msg %= (l_fil, l_fhi) + log.debug(msg) + log.debug("info lines: " + str(fetch_info_lines)) + log.debug("head info : " + str(fetch_head_info)) + if l_fil < l_fhi: + fetch_head_info = fetch_head_info[:l_fil] + else: + fetch_info_lines = fetch_info_lines[:l_fhi] + # end truncate correct list + # end sanity check + sanitization + + for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info): + try: + output.append(FetchInfo._from_line(self.repo, err_line, fetch_line)) + except ValueError as exc: + log.debug("Caught error while parsing line: %s", exc) + log.warning("Git informed while fetching: %s", err_line.strip()) + return output + + def _get_push_info(self, proc: 'Git.AutoInterrupt', + progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: + progress = to_progress_instance(progress) + + # read progress information from stderr + # we hope stdout can hold all the data, it should ... + # read the lines manually as it will use carriage returns between the messages + # to override the previous one. This is why we read the bytes manually + progress_handler = progress.new_message_handler() + output: IterableList[PushInfo] = IterableList('push_infos') + + def stdout_handler(line: str) -> None: + try: + output.append(PushInfo._from_line(self, line)) + except ValueError: + # If an error happens, additional info is given which we parse below. + pass + + handle_process_output(proc, stdout_handler, progress_handler, finalizer=None, decode_streams=False) + stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' + try: + proc.wait(stderr=stderr_text) + except Exception: + if not output: + raise + elif stderr_text: + log.warning("Error lines received while fetching: %s", stderr_text) + + return output + + def _assert_refspec(self) -> None: + """Turns out we can't deal with remotes if the refspec is missing""" + config = self.config_reader + unset = 'placeholder' + try: + if config.get_value('fetch', default=unset) is unset: + msg = "Remote '%s' has no refspec set.\n" + msg += "You can set it as follows:" + msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'." + raise AssertionError(msg % (self.name, self.name)) + finally: + config.release() + + def fetch(self, refspec: Union[str, List[str], None] = None, + progress: Union[RemoteProgress, None, 'UpdateProgress'] = None, + verbose: bool = True, **kwargs: Any) -> IterableList[FetchInfo]: + """Fetch the latest changes for this remote + + :param refspec: + A "refspec" is used by fetch and push to describe the mapping + between remote ref and local ref. They are combined with a colon in + the format :, preceded by an optional plus sign, +. + For example: git fetch $URL refs/heads/master:refs/heads/origin means + "grab the master branch head from the $URL and store it as my origin + branch head". And git push $URL refs/heads/master:refs/heads/to-upstream + means "publish my master branch head as to-upstream branch at $URL". + See also git-push(1). + + Taken from the git manual + + Fetch supports multiple refspecs (as the + underlying git-fetch does) - supplying a list rather than a string + for 'refspec' will make use of this facility. + :param progress: See 'push' method + :param verbose: Boolean for verbose output + :param kwargs: Additional arguments to be passed to git-fetch + :return: + IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed + information about the fetch results + + :note: + As fetch does not provide progress information to non-ttys, we cannot make + it available here unfortunately as in the 'push' method.""" + if refspec is None: + # No argument refspec, then ensure the repo's config has a fetch refspec. + self._assert_refspec() + + kwargs = add_progress(kwargs, self.repo.git, progress) + if isinstance(refspec, list): + args: Sequence[Optional[str]] = refspec + else: + args = [refspec] + + proc = self.repo.git.fetch(self, *args, as_process=True, with_stdout=False, + universal_newlines=True, v=verbose, **kwargs) + res = self._get_fetch_info_from_stderr(proc, progress) + if hasattr(self.repo.odb, 'update_cache'): + self.repo.odb.update_cache() + return res + + def pull(self, refspec: Union[str, List[str], None] = None, + progress: Union[RemoteProgress, 'UpdateProgress', None] = None, + **kwargs: Any) -> IterableList[FetchInfo]: + """Pull changes from the given branch, being the same as a fetch followed + by a merge of branch with your local branch. + + :param refspec: see 'fetch' method + :param progress: see 'push' method + :param kwargs: Additional arguments to be passed to git-pull + :return: Please see 'fetch' method """ + if refspec is None: + # No argument refspec, then ensure the repo's config has a fetch refspec. + self._assert_refspec() + kwargs = add_progress(kwargs, self.repo.git, progress) + proc = self.repo.git.pull(self, refspec, with_stdout=False, as_process=True, + universal_newlines=True, v=True, **kwargs) + res = self._get_fetch_info_from_stderr(proc, progress) + if hasattr(self.repo.odb, 'update_cache'): + self.repo.odb.update_cache() + return res + + def push(self, refspec: Union[str, List[str], None] = None, + progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None, + **kwargs: Any) -> IterableList[PushInfo]: + """Push changes from source branch in refspec to target branch in refspec. + + :param refspec: see 'fetch' method + :param progress: + Can take one of many value types: + + * None to discard progress information + * A function (callable) that is called with the progress information. + Signature: ``progress(op_code, cur_count, max_count=None, message='')``. + `Click here `__ for a description of all arguments + given to the function. + * An instance of a class derived from ``git.RemoteProgress`` that + overrides the ``update()`` function. + + :note: No further progress information is returned after push returns. + :param kwargs: Additional arguments to be passed to git-push + :return: + list(PushInfo, ...) list of PushInfo instances, each + one informing about an individual head which had been updated on the remote + side. + If the push contains rejected heads, these will have the PushInfo.ERROR bit set + in their flags. + If the operation fails completely, the length of the returned IterableList will + be 0.""" + kwargs = add_progress(kwargs, self.repo.git, progress) + proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, + universal_newlines=True, **kwargs) + return self._get_push_info(proc, progress) + + @ property + def config_reader(self) -> SectionConstraint[GitConfigParser]: + """ + :return: + GitConfigParser compatible object able to read options for only our remote. + Hence you may simple type config.get("pushurl") to obtain the information""" + return self._config_reader + + def _clear_cache(self) -> None: + try: + del(self._config_reader) + except AttributeError: + pass + # END handle exception + + @ property + def config_writer(self) -> SectionConstraint: + """ + :return: GitConfigParser compatible object able to write options for this remote. + :note: + You can only own one writer at a time - delete it to release the + configuration file and make it usable by others. + + To assure consistent results, you should only query options through the + writer. Once you are done writing, you are free to use the config reader + once again.""" + writer = self.repo.config_writer() + + # clear our cache to assure we re-read the possibly changed configuration + self._clear_cache() + return SectionConstraint(writer, self._config_section_name()) From 07078e9f11499ab0001e0eb1e6000b52e8a5fb81 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 8 Aug 2021 21:51:31 +0100 Subject: [PATCH 0005/1225] type fixo --- git/{objects => }/remote.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename git/{objects => }/remote.py (100%) diff --git a/git/objects/remote.py b/git/remote.py similarity index 100% rename from git/objects/remote.py rename to git/remote.py From bf0c332700e90c5c8864c059562b7861941f48e1 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 8 Aug 2021 21:55:33 +0100 Subject: [PATCH 0006/1225] add pypy to test matrix --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dd94ab9d5..9604587e7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4"] + python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4", "pypy37"] steps: - uses: actions/checkout@v2 From 4381f6cb175c2749f05ca45f6cfa4f3e277a13c3 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 8 Aug 2021 21:57:38 +0100 Subject: [PATCH 0007/1225] update 3.10 to rc1 in test matrix --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 9604587e7..25e3c3dd5 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4", "pypy37"] + python-version: [3.7, 3.8, 3.9, "3.10.0-rc.1"] steps: - uses: actions/checkout@v2 From 079d7fd6994bc6751bef4797a027b9e6daf966f4 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 09:55:56 +0100 Subject: [PATCH 0008/1225] try fix for Protocol buy in 3.10 --- git/objects/util.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index 16d4c0ac8..9f98db56b 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -22,10 +22,10 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, NamedTuple, overload, Sequence, +from typing import (Any, Callable, Deque, Iterator, Generic, NamedTuple, overload, Sequence, TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast) -from git.types import Has_id_attribute, Literal, Protocol, runtime_checkable +from git.types import Has_id_attribute, Literal if TYPE_CHECKING: from io import BytesIO, StringIO @@ -35,6 +35,12 @@ from .tree import Tree, TraversedTreeTup from subprocess import Popen from .submodule.base import Submodule + from git.types import Protocol, runtime_checkable +else: + Protocol = Generic + + def runtime_checkable(f): + return f class TraverseNT(NamedTuple): From 1349ddc19f5a7f6aa56b0bc53d2f2c002128d360 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 09:58:16 +0100 Subject: [PATCH 0009/1225] try fix for Protocol buy in 3.10 2 --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 9f98db56b..d227f3465 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -37,7 +37,7 @@ from .submodule.base import Submodule from git.types import Protocol, runtime_checkable else: - Protocol = Generic + Protocol = Generic[Any] def runtime_checkable(f): return f From 2f42966cd1ec287d1c2011224940131dbda2383d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 10:08:42 +0100 Subject: [PATCH 0010/1225] try fix for Protocol buy in 3.10 3 --- git/objects/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index d227f3465..4b830e0e4 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -25,7 +25,7 @@ from typing import (Any, Callable, Deque, Iterator, Generic, NamedTuple, overload, Sequence, TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast) -from git.types import Has_id_attribute, Literal +from git.types import Has_id_attribute, Literal, _T if TYPE_CHECKING: from io import BytesIO, StringIO @@ -37,7 +37,7 @@ from .submodule.base import Submodule from git.types import Protocol, runtime_checkable else: - Protocol = Generic[Any] + Protocol = Generic[_T] def runtime_checkable(f): return f From c35ab1dd61e91bd55d939302d1f02e1c58985826 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 17:14:31 +0100 Subject: [PATCH 0011/1225] upgrade sphinx for 3.10 compat --- doc/requirements.txt | 2 +- git/objects/util.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 20598a39c..917feb350 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,3 @@ -sphinx==4.1.1 +sphinx==4.1.2 sphinx_rtd_theme sphinx-autodoc-typehints diff --git a/git/objects/util.py b/git/objects/util.py index 4b830e0e4..187318fe6 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """Module for general utility functions""" -from abc import abstractmethod +from abc import ABC, abstractmethod import warnings from git.util import ( IterableList, @@ -22,10 +22,10 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, Generic, NamedTuple, overload, Sequence, +from typing import (Any, Callable, Deque, Iterator, Generic, NamedTuple, overload, Sequence, # NOQA: F401 TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast) -from git.types import Has_id_attribute, Literal, _T +from git.types import Has_id_attribute, Literal, _T # NOQA: F401 if TYPE_CHECKING: from io import BytesIO, StringIO @@ -37,7 +37,8 @@ from .submodule.base import Submodule from git.types import Protocol, runtime_checkable else: - Protocol = Generic[_T] + # Protocol = Generic[_T] # NNeeded for typing bug #572? + Protocol = ABC def runtime_checkable(f): return f From 5835f013e88d5e29fa73fe7eac8f620cfd3fc0a1 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 18:02:25 +0100 Subject: [PATCH 0012/1225] Update changelog and version --- .github/workflows/pythonpackage.yml | 2 +- VERSION | 2 +- doc/source/changes.rst | 77 +++++++++++++++++++---------- git/cmd.py | 2 + 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 25e3c3dd5..4f871bb34 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-rc.1"] + python-version: [3.7, 3.8, 3.9, "3.10.0-b4"] steps: - uses: actions/checkout@v2 diff --git a/VERSION b/VERSION index 589ccc9b9..5c5bdc27d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.20 +3.1.21 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 09da1eb27..833222fca 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,24 +2,51 @@ Changelog ========= -3.1.20 +3.1.21 ====== -* This is the second typed release with a lot of improvements under the hood. +* This is the second typed release with a lot of improvements under the hood. + +* General: + - Remove python 3.6 support + - Remove distutils inline with deprecation in standard library. + - Update sphinx to 4.1.12 and use autodoc-typehints. + +* Typing: + - Add types to ALL functions. + - Ensure py.typed is collected. + - Increase mypy strictness with disallow_untyped_defs, warn_redundant_casts, warn_unreachable. + - Use typing.NamedTuple and typing.OrderedDict now 3.6 dropped. + - Remove use of typing.TypeGuard until later release, to allow dependant libs time to update. + - Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 + +* Runtime improvements: + - Add clone_multi_options support to submodule.add() + - Delay calling get_user_id() unless essential, to support sand-boxed environments. + - Add timeout to handle_process_output(), in case thread.join() hangs. + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/52?closed=1 + + +3.1.20 (YANKED) +====== + +* This is the second typed release with a lot of improvements under the hood. * Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 - + See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/52?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/52?closed=1 3.1.19 (YANKED) =============== -* This is the second typed release with a lot of improvements under the hood. +* This is the second typed release with a lot of improvements under the hood. * Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 - + See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/51?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/51?closed=1 3.1.18 ====== @@ -27,7 +54,7 @@ https://github.com/gitpython-developers/gitpython/milestone/51?closed=1 * drop support for python 3.5 to reduce maintenance burden on typing. Lower patch levels of python 3.5 would break, too. See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/50?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/50?closed=1 3.1.17 ====== @@ -37,7 +64,7 @@ https://github.com/gitpython-developers/gitpython/milestone/50?closed=1 * Add more static typing information See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/49?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/49?closed=1 3.1.16 (YANKED) =============== @@ -46,7 +73,7 @@ https://github.com/gitpython-developers/gitpython/milestone/49?closed=1 * Add more static typing information See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 3.1.15 (YANKED) =============== @@ -54,7 +81,7 @@ https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 * add deprectation warning for python 3.5 See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 3.1.14 ====== @@ -65,19 +92,19 @@ https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 * Drop python 3.4 support See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/46?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/46?closed=1 3.1.13 ====== See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/45?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/45?closed=1 3.1.12 ====== See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/44?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/44?closed=1 3.1.11 ====== @@ -85,20 +112,20 @@ https://github.com/gitpython-developers/gitpython/milestone/44?closed=1 Fixes regression of 3.1.10. See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/43?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/43?closed=1 3.1.10 ====== See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/42?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/42?closed=1 3.1.9 ===== See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/41?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/41?closed=1 3.1.8 @@ -109,7 +136,7 @@ https://github.com/gitpython-developers/gitpython/milestone/41?closed=1 See the following for more details: -https://github.com/gitpython-developers/gitpython/milestone/40?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/40?closed=1 3.1.7 @@ -135,13 +162,13 @@ https://github.com/gitpython-developers/gitpython/milestone/40?closed=1 * package size was reduced significantly not placing tests into the package anymore. See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/39?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/39?closed=1 3.1.3 ===== See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/38?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/38?closed=1 3.1.2 ===== @@ -190,7 +217,7 @@ Bugfixes Bugfixes -------- -* Fixed Repo.__repr__ when subclassed +* Fixed Repo.__repr__ when subclassed (`#968 `_) * Removed compatibility shims for Python < 3.4 and old mock library * Replaced usage of deprecated unittest aliases and Logger.warn @@ -213,7 +240,7 @@ Bugfixes -------- * Fixed warning for usage of environment variables for paths containing ``$`` or ``%`` - (`#832 `_, + (`#832 `_, `#961 `_) * Added support for parsing Git internal date format (@ ) (`#965 `_) @@ -371,7 +398,7 @@ Notable fixes * The `GIT_DIR` environment variable does not override the `path` argument when initializing a `Repo` object anymore. However, if said `path` unset, `GIT_DIR` will be used to fill the void. - + All issues and PRs can be viewed in all detail when following this URL: https://github.com/gitpython-developers/GitPython/issues?q=is%3Aclosed+milestone%3A%22v2.1.0+-+proper+windows+support%22 @@ -401,7 +428,7 @@ https://github.com/gitpython-developers/GitPython/issues?q=is%3Aclosed+milestone 2.0.7 - New Features ==================== -* `IndexFile.commit(...,skip_hooks=False)` added. This parameter emulates the +* `IndexFile.commit(...,skip_hooks=False)` added. This parameter emulates the behaviour of `--no-verify` on the command-line. 2.0.6 - Fixes and Features @@ -441,7 +468,7 @@ https://github.com/gitpython-developers/GitPython/issues?q=is%3Aclosed+milestone commit messages contained ``\r`` characters * Fix: progress handler exceptions are not caught anymore, which would usually just hide bugs previously. -* Fix: The `Git.execute` method will now redirect `stdout` to `devnull` if `with_stdout` is false, +* Fix: The `Git.execute` method will now redirect `stdout` to `devnull` if `with_stdout` is false, which is the intended behaviour based on the parameter's documentation. 2.0.2 - Fixes diff --git a/git/cmd.py b/git/cmd.py index ff1dfa343..068ad134d 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -147,6 +147,8 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], # for t in threads: t.join(timeout=timeout) + if t.is_alive(): + raise RuntimeError(f"Thread join() timed out in cmd.handle_process_output(). Timeout={timeout} seconds") if finalizer: return finalizer(process) From 1a71d9abe019e9bb8689ee7189c4dcd62bd21df8 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 18:05:59 +0100 Subject: [PATCH 0013/1225] Change CI to 3.10.0-beta.4, to get docs to pass --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4f871bb34..dd94ab9d5 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-b4"] + python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4"] steps: - uses: actions/checkout@v2 From 6835c910174daebdfbfcd735d7476d7929c2a8c0 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 18:12:34 +0100 Subject: [PATCH 0014/1225] Update changes.rst --- doc/source/changes.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 833222fca..16741ad90 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -9,7 +9,7 @@ Changelog * General: - Remove python 3.6 support - - Remove distutils inline with deprecation in standard library. + - Remove distutils ahead of deprecation in standard library. - Update sphinx to 4.1.12 and use autodoc-typehints. * Typing: @@ -17,6 +17,7 @@ Changelog - Ensure py.typed is collected. - Increase mypy strictness with disallow_untyped_defs, warn_redundant_casts, warn_unreachable. - Use typing.NamedTuple and typing.OrderedDict now 3.6 dropped. + - Make Protocol classes ABCs at runtime due to new bug in 3.10.0-rc1 - Remove use of typing.TypeGuard until later release, to allow dependant libs time to update. - Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 @@ -30,7 +31,7 @@ https://github.com/gitpython-developers/gitpython/milestone/52?closed=1 3.1.20 (YANKED) -====== +=============== * This is the second typed release with a lot of improvements under the hood. * Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 From 3439a3ec3582f7317ee46418c24996951409cedc Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 9 Aug 2021 18:19:37 +0100 Subject: [PATCH 0015/1225] Change CI python 3.10 to rc1 again. Spinx broken either way --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dd94ab9d5..25e3c3dd5 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4"] + python-version: [3.7, 3.8, 3.9, "3.10.0-rc.1"] steps: - uses: actions/checkout@v2 From 5da76e8b4466459a3b6a400c4750a622879acce8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 5 Sep 2021 11:27:23 +0800 Subject: [PATCH 0016/1225] Assure CWD is readable after acquiring it Fixes #1334 --- git/cmd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git/cmd.py b/git/cmd.py index 226b8710b..7de5b9e1e 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -783,6 +783,8 @@ def execute(self, # Allow the user to have the command executed in their working dir. try: cwd = self._working_dir or os.getcwd() # type: Union[None, str] + if not os.access(str(cwd), os.X_OK): + cwd = None except FileNotFoundError: cwd = None From 40f4cebbc095d043463b3e72d740acbcb84491d8 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:00:58 +0100 Subject: [PATCH 0017/1225] Update pythonpackage.yml Try python 3.10.0.rc.2 --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 25e3c3dd5..0878a1a58 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-rc.1"] + python-version: [3.7, 3.8, 3.9, "3.10.0-rc.2"] steps: - uses: actions/checkout@v2 From 4ed0531c04cea95e93fc4829ae6b01577697172f Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:03:41 +0100 Subject: [PATCH 0018/1225] Update pythonpackage.yml Rmv 3.10.0 from test matrix --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0878a1a58..369286570 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10.0-rc.2"] + python-version: [3.7, 3.8, 3.9] # , "3.10.0-rc.2"] steps: - uses: actions/checkout@v2 From d6017fbe075dcd0f1e146ad460449c89bfdcdc0b Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:04:27 +0100 Subject: [PATCH 0019/1225] Update setup.py Comment out python 3.10 for next release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14e36dff9..bf24ccce0 100755 --- a/setup.py +++ b/setup.py @@ -119,6 +119,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10" + # "Programming Language :: Python :: 3.10" ] ) From cb7cbe583e08aeb26adcec3c0b4179833aeee797 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:08:14 +0100 Subject: [PATCH 0020/1225] Update pythonpackage.yml try force tests on 3.9.7 --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 369286570..fa0e51d9c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9] # , "3.10.0-rc.2"] + python-version: [3.7, 3.8, 3.9.7] # , "3.10.0-rc.2"] steps: - uses: actions/checkout@v2 From f7fddc1e8ec8eec8a37272d48b7357110ee9648c Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:11:21 +0100 Subject: [PATCH 0021/1225] Update pythonpackage.yml Add minor versions to test matrix --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index fa0e51d9c..1af0a9db9 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9.7] # , "3.10.0-rc.2"] + python-version: [3.7, 3.7.0, 3.7.2, 3.7.12, 3.8.0, 3.8.11, 3.8, 3.9.0, 3.9.7] # , "3.10.0-rc.2"] steps: - uses: actions/checkout@v2 From bb9b50ff2671cda598ff19653d3de49e03b6d163 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:13:25 +0100 Subject: [PATCH 0022/1225] Update pythonpackage.yml 3.7.0 not available --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1af0a9db9..4e7aa418c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.7.0, 3.7.2, 3.7.12, 3.8.0, 3.8.11, 3.8, 3.9.0, 3.9.7] # , "3.10.0-rc.2"] + python-version: [3.7, 3.7.5, 3.7.12, 3.8, 3.8.0, 3.8.11, 3.8, 3.9, 3.9.0, 3.9.7] # , "3.10.0-rc.2"] steps: - uses: actions/checkout@v2 From e488ce376d7bb92d3d9bb1c6d2408f1ec2a2d5f4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:56:48 +0100 Subject: [PATCH 0023/1225] Update setup.py Import README.md --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bf24ccce0..2425c3f19 100755 --- a/setup.py +++ b/setup.py @@ -16,6 +16,8 @@ with open('test-requirements.txt') as reqs_file: test_requirements = reqs_file.read().splitlines() +with open('README.md') as rm_file: + long_description = rm_file.read() class build_py(_build_py): @@ -82,7 +84,7 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: name="GitPython", cmdclass={'build_py': build_py, 'sdist': sdist}, version=VERSION, - description="Python Git Library", + description="""GitPython is a python library used to interact with Git repositories""", author="Sebastian Thiel, Michael Trier", author_email="byronimo@gmail.com, mtrier@gmail.com", license="BSD", @@ -96,6 +98,7 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: tests_require=requirements + test_requirements, zip_safe=False, long_description="""GitPython is a python library used to interact with Git repositories""", + long_description_content_type="text/markdown", classifiers=[ # Picked from # http://pypi.python.org/pypi?:action=list_classifiers From 58820a5e1481e3d3907eda24422f635893342047 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 20:01:57 +0100 Subject: [PATCH 0024/1225] Update setup.py format path -> os.path in prep for pathlib --- setup.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 2425c3f19..11696e758 100755 --- a/setup.py +++ b/setup.py @@ -5,9 +5,8 @@ import fnmatch import os import sys -from os import path -with open(path.join(path.dirname(__file__), 'VERSION')) as v: +with open(os.path.join(os.path.dirname(__file__), 'VERSION')) as v: VERSION = v.readline().strip() with open('requirements.txt') as reqs_file: @@ -22,8 +21,8 @@ class build_py(_build_py): def run(self) -> None: - init = path.join(self.build_lib, 'git', '__init__.py') - if path.exists(init): + init = os.path.join(self.build_lib, 'git', '__init__.py') + if os.path.exists(init): os.unlink(init) _build_py.run(self) _stamp_version(init) @@ -34,10 +33,10 @@ class sdist(_sdist): def make_release_tree(self, base_dir: str, files: Sequence) -> None: _sdist.make_release_tree(self, base_dir, files) - orig = path.join('git', '__init__.py') - assert path.exists(orig), orig - dest = path.join(base_dir, orig) - if hasattr(os, 'link') and path.exists(dest): + orig = os.path.join('git', '__init__.py') + assert os.path.exists(orig), orig + dest = os.path.join(base_dir, orig) + if hasattr(os, 'link') and os.path.exists(dest): os.unlink(dest) self.copy_file(orig, dest) _stamp_version(dest) From bc2edef856254dc52109260ad44c4f5f4a208f9b Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 20:04:38 +0100 Subject: [PATCH 0025/1225] Update setup.py flake8 fix --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 11696e758..cd1007d74 100755 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ with open('README.md') as rm_file: long_description = rm_file.read() + class build_py(_build_py): def run(self) -> None: From 0db50a27352a28404790ceac2f7abfc85f8e8680 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 20:45:45 +0100 Subject: [PATCH 0026/1225] Update changes.rst Update changes for 3.1.21 --- doc/source/changes.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 16741ad90..8c5a84885 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -8,22 +8,37 @@ Changelog * This is the second typed release with a lot of improvements under the hood. * General: + - Remove python 3.6 support + - Remove distutils ahead of deprecation in standard library. + - Update sphinx to 4.1.12 and use autodoc-typehints. + + - Include README as long_description on PiPI * Typing: + - Add types to ALL functions. + - Ensure py.typed is collected. + - Increase mypy strictness with disallow_untyped_defs, warn_redundant_casts, warn_unreachable. + - Use typing.NamedTuple and typing.OrderedDict now 3.6 dropped. - - Make Protocol classes ABCs at runtime due to new bug in 3.10.0-rc1 + + - Make Protocol classes ABCs at runtime due to new behaviour/bug in 3.9.7 & 3.10.0-rc1 + - Remove use of typing.TypeGuard until later release, to allow dependant libs time to update. + - Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 * Runtime improvements: + - Add clone_multi_options support to submodule.add() + - Delay calling get_user_id() unless essential, to support sand-boxed environments. + - Add timeout to handle_process_output(), in case thread.join() hangs. See the following for details: From 9d6ddd3ceb3da321d5194fbcd9312815606073a1 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 20:50:21 +0100 Subject: [PATCH 0027/1225] Update changes.rst --- doc/source/changes.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 8c5a84885..cc3c91b1d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -15,7 +15,10 @@ Changelog - Update sphinx to 4.1.12 and use autodoc-typehints. - - Include README as long_description on PiPI + - Include README as long_description on PyPI + + - Test against earliest and latest minor version available on Github Actions (e.g. 3.9.0 and 3.9.7) + * Typing: From 856c0825abd856f01b21a2614f89df20f8f81443 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 10 Sep 2021 10:11:57 +0800 Subject: [PATCH 0028/1225] bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5c5bdc27d..be706e820 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.21 +3.1.22 From 03f198f66cceca745a67658b7d16bf4b7e40b9ab Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 10 Sep 2021 10:14:01 +0800 Subject: [PATCH 0029/1225] Fix version discrepancy --- VERSION | 2 +- doc/source/changes.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index be706e820..60b9d63bc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.22 +3.1.23 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index cc3c91b1d..bb7a23baa 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,7 +2,7 @@ Changelog ========= -3.1.21 +31.23 ====== * This is the second typed release with a lot of improvements under the hood. From 146202cdcbed8239651ccc62d36a8e5af3ceff8c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 10 Sep 2021 08:15:55 +0200 Subject: [PATCH 0030/1225] Fix title --- doc/source/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index bb7a23baa..ac73b1722 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,7 +2,7 @@ Changelog ========= -31.23 +3.1.23 ====== * This is the second typed release with a lot of improvements under the hood. From 10f24aee9c9b49f2ea1060536eab296446a06efd Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 13:36:15 +0200 Subject: [PATCH 0031/1225] change default fetch timeout to 60 s --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 642ef9ed6..13c5e7a55 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -79,7 +79,7 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, finalizer: Union[None, Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, decode_streams: bool = True, - timeout: float = 10.0) -> None: + timeout: float = 60.0) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns From 9925785cbd29e02a4e38dfd29112a3a9533fc170 Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 13:45:24 +0200 Subject: [PATCH 0032/1225] allow for timeout propagation --- git/remote.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/git/remote.py b/git/remote.py index 55772f4a0..e3a819cfc 100644 --- a/git/remote.py +++ b/git/remote.py @@ -707,7 +707,8 @@ def update(self, **kwargs: Any) -> 'Remote': return self def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', - progress: Union[Callable[..., Any], RemoteProgress, None] + progress: Union[Callable[..., Any], RemoteProgress, None], + timeout: float = 60.0 ) -> IterableList['FetchInfo']: progress = to_progress_instance(progress) @@ -724,7 +725,8 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', cmds = set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() - handle_process_output(proc, None, progress_handler, finalizer=None, decode_streams=False) + handle_process_output(proc, None, progress_handler, finalizer=None, decode_streams=False, + timeout=timeout) stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' proc.wait(stderr=stderr_text) @@ -769,7 +771,8 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', return output def _get_push_info(self, proc: 'Git.AutoInterrupt', - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: + progress: Union[Callable[..., Any], RemoteProgress, None], + timeout: float = 60.0) -> IterableList[PushInfo]: progress = to_progress_instance(progress) # read progress information from stderr @@ -786,7 +789,8 @@ def stdout_handler(line: str) -> None: # If an error happens, additional info is given which we parse below. pass - handle_process_output(proc, stdout_handler, progress_handler, finalizer=None, decode_streams=False) + handle_process_output(proc, stdout_handler, progress_handler, finalizer=None, decode_streams=False, + timeout=timeout) stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' try: proc.wait(stderr=stderr_text) @@ -813,7 +817,8 @@ def _assert_refspec(self) -> None: def fetch(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, None, 'UpdateProgress'] = None, - verbose: bool = True, **kwargs: Any) -> IterableList[FetchInfo]: + verbose: bool = True, timeout: float = 60.0, + **kwargs: Any) -> IterableList[FetchInfo]: """Fetch the latest changes for this remote :param refspec: @@ -853,13 +858,14 @@ def fetch(self, refspec: Union[str, List[str], None] = None, proc = self.repo.git.fetch(self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress) + res = self._get_fetch_info_from_stderr(proc, progress, timeout=timeout) if hasattr(self.repo.odb, 'update_cache'): self.repo.odb.update_cache() return res def pull(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', None] = None, + timeout: float = 60.0, **kwargs: Any) -> IterableList[FetchInfo]: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. @@ -874,14 +880,14 @@ def pull(self, refspec: Union[str, List[str], None] = None, kwargs = add_progress(kwargs, self.repo.git, progress) proc = self.repo.git.pull(self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress) + res = self._get_fetch_info_from_stderr(proc, progress, timeout=timeout) if hasattr(self.repo.odb, 'update_cache'): self.repo.odb.update_cache() return res def push(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None, - **kwargs: Any) -> IterableList[PushInfo]: + timeout: float = 60.0, **kwargs: Any) -> IterableList[PushInfo]: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method @@ -909,7 +915,7 @@ def push(self, refspec: Union[str, List[str], None] = None, kwargs = add_progress(kwargs, self.repo.git, progress) proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, universal_newlines=True, **kwargs) - return self._get_push_info(proc, progress) + return self._get_push_info(proc, progress, timeout=timeout) @ property def config_reader(self) -> SectionConstraint[GitConfigParser]: From bd4ee0f2f4b18889134cdc63fc934902628da1ba Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 13:50:57 +0200 Subject: [PATCH 0033/1225] add test timeout with the old 10 s timeout --- test/test_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_remote.py b/test/test_remote.py index c29fac65c..13da128f7 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -401,7 +401,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo): res = remote.push(all=True) self._do_test_push_result(res, remote) - remote.pull('master') + remote.pull('master', timeout=10.0) # cleanup - delete created tags and branches as we are in an innerloop on # the same repository @@ -467,7 +467,7 @@ def test_base(self, rw_repo, remote_repo): # Only for remotes - local cases are the same or less complicated # as additional progress information will never be emitted if remote.name == "daemon_origin": - self._do_test_fetch(remote, rw_repo, remote_repo) + self._do_test_fetch(remote, rw_repo, remote_repo, timeout=10.0) ran_fetch_test = True # END fetch test From 5d2dfb18777a95165f588d099111b2a553c6a8ca Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 13:57:34 +0200 Subject: [PATCH 0034/1225] also test a call to 'push' with 10s timeout --- test/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 13da128f7..243eec290 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -406,7 +406,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo): # cleanup - delete created tags and branches as we are in an innerloop on # the same repository TagReference.delete(rw_repo, new_tag, other_tag) - remote.push(":%s" % other_tag.path) + remote.push(":%s" % other_tag.path, timeout=10.0) @skipIf(HIDE_WINDOWS_FREEZE_ERRORS, "FIXME: Freezes!") @with_rw_and_rw_remote_repo('0.1.6') From 5ec7967b64f1aea7e3258e0c1c8033639d0320ff Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 14:05:24 +0200 Subject: [PATCH 0035/1225] propagate kwargs in do_test_fetch --- test/test_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_remote.py b/test/test_remote.py index 243eec290..e5fe8dd00 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -164,7 +164,7 @@ def _commit_random_file(self, repo): index.commit("Committing %s" % new_file) return new_file - def _do_test_fetch(self, remote, rw_repo, remote_repo): + def _do_test_fetch(self, remote, rw_repo, remote_repo, **kwargs): # specialized fetch testing to de-clutter the main test self._do_test_fetch_info(rw_repo) @@ -183,7 +183,7 @@ def get_info(res, remote, name): # put remote head to master as it is guaranteed to exist remote_repo.head.reference = remote_repo.heads.master - res = fetch_and_test(remote) + res = fetch_and_test(remote, **kwargs) # all up to date for info in res: self.assertTrue(info.flags & info.HEAD_UPTODATE) From d6cdafe223fe2e4ec17c52d4bd5ad7affc599814 Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 17:06:53 +0200 Subject: [PATCH 0036/1225] reset default timeout to None --- git/cmd.py | 2 +- git/remote.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 13c5e7a55..0deb4ffcc 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -79,7 +79,7 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, finalizer: Union[None, Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, decode_streams: bool = True, - timeout: float = 60.0) -> None: + timeout: Union[None, float] = None) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns diff --git a/git/remote.py b/git/remote.py index e3a819cfc..ce5d82b5b 100644 --- a/git/remote.py +++ b/git/remote.py @@ -708,7 +708,7 @@ def update(self, **kwargs: Any) -> 'Remote': def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - timeout: float = 60.0 + timeout: Union[None, float] = None, ) -> IterableList['FetchInfo']: progress = to_progress_instance(progress) @@ -772,7 +772,7 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', def _get_push_info(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - timeout: float = 60.0) -> IterableList[PushInfo]: + timeout: Union[None, float] = None) -> IterableList[PushInfo]: progress = to_progress_instance(progress) # read progress information from stderr @@ -817,7 +817,7 @@ def _assert_refspec(self) -> None: def fetch(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, None, 'UpdateProgress'] = None, - verbose: bool = True, timeout: float = 60.0, + verbose: bool = True, timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[FetchInfo]: """Fetch the latest changes for this remote @@ -865,7 +865,7 @@ def fetch(self, refspec: Union[str, List[str], None] = None, def pull(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', None] = None, - timeout: float = 60.0, + timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[FetchInfo]: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. @@ -887,7 +887,7 @@ def pull(self, refspec: Union[str, List[str], None] = None, def push(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None, - timeout: float = 60.0, **kwargs: Any) -> IterableList[PushInfo]: + timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[PushInfo]: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method From b7cd5207ba05c733d8e29def0757edf4d7cc24f7 Mon Sep 17 00:00:00 2001 From: sroet Date: Fri, 10 Sep 2021 17:28:00 +0200 Subject: [PATCH 0037/1225] update docstring --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 0deb4ffcc..f1b3194a3 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -94,7 +94,7 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, their contents to handlers. Set it to False if `universal_newline == True` (then streams are in text-mode) or if decoding must happen later (i.e. for Diffs). - :param timeout: float, timeout to pass to t.join() in case it hangs. Default = 10.0 seconds + :param timeout: float, or None timeout to pass to t.join() in case it hangs. Default = None. """ # Use 2 "pump" threads and wait for both to finish. def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, From ef0ca654f859d6caaf2a2029cb691d5beec79ed5 Mon Sep 17 00:00:00 2001 From: sroet Date: Mon, 13 Sep 2021 17:05:45 +0200 Subject: [PATCH 0038/1225] reuse kill_after_timeout kwarg --- git/cmd.py | 66 +++++++++++++++++++++++++++++++++++---------------- git/remote.py | 36 +++++++++++++++++++--------- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index f1b3194a3..db06d5f7c 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -79,7 +79,7 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, finalizer: Union[None, Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, decode_streams: bool = True, - timeout: Union[None, float] = None) -> None: + kill_after_timeout: Union[None, float] = None) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -94,7 +94,10 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, their contents to handlers. Set it to False if `universal_newline == True` (then streams are in text-mode) or if decoding must happen later (i.e. for Diffs). - :param timeout: float, or None timeout to pass to t.join() in case it hangs. Default = None. + :param kill_after_timeout: + float or None, Default = None + 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], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, @@ -108,9 +111,12 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], handler(line_str) else: handler(line) + except Exception as ex: log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") - raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex + if "I/O operation on closed file" not in str(ex): + # Only reraise if the error was not due to the stream closing + raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex finally: stream.close() @@ -146,9 +152,16 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], ## FIXME: Why Join?? Will block if `stdin` needs feeding... # for t in threads: - t.join(timeout=timeout) + t.join(timeout=kill_after_timeout) if t.is_alive(): - raise RuntimeError(f"Thread join() timed out in cmd.handle_process_output(). Timeout={timeout} seconds") + if hasattr(process, 'proc'): # Assume it is a Git.AutoInterrupt: + process._terminate() + else: # Don't want to deal with the other case + raise RuntimeError(f"Thread join() timed out in cmd.handle_process_output()." + " kill_after_timeout={kill_after_timeout} seconds") + if stderr_handler: + stderr_handler("error: process killed because it timed out." + f" kill_after_timeout={kill_after_timeout} seconds") if finalizer: return finalizer(process) @@ -386,13 +399,15 @@ class AutoInterrupt(object): The wait method was overridden to perform automatic status code checking and possibly raise.""" - __slots__ = ("proc", "args") + __slots__ = ("proc", "args", "status") def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.proc = proc self.args = args + self.status = None - def __del__(self) -> None: + def _terminate(self) -> None: + """Terminate the underlying process""" if self.proc is None: return @@ -408,6 +423,7 @@ def __del__(self) -> None: # did the process finish already so we have a return code ? try: if proc.poll() is not None: + self.status = proc.poll() return None except OSError as ex: log.info("Ignored error after process had died: %r", ex) @@ -419,7 +435,7 @@ def __del__(self) -> None: # try to kill it try: proc.terminate() - proc.wait() # ensure process goes away + self.status = proc.wait() # ensure process goes away except OSError as ex: log.info("Ignored error after process had died: %r", ex) except AttributeError: @@ -431,6 +447,11 @@ def __del__(self) -> None: call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) # END exception handling + + + def __del__(self) -> None: + self._terminate() + def __getattr__(self, attr: str) -> Any: return getattr(self.proc, attr) @@ -447,21 +468,26 @@ def wait(self, stderr: Union[None, str, bytes] = b'') -> int: if self.proc is not None: status = self.proc.wait() + p_stderr = self.proc.stderr + else: #Assume the underlying proc was killed earlier or never existed + status = self.status + p_stderr = None - def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: - if stream: - try: - return stderr_b + force_bytes(stream.read()) - except ValueError: - return stderr_b or b'' - else: + def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: + if stream: + try: + return stderr_b + force_bytes(stream.read()) + except ValueError: return stderr_b or b'' + else: + return stderr_b or b'' - if status != 0: - errstr = read_all_from_possibly_closed_stream(self.proc.stderr) - log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) - raise GitCommandError(remove_password_if_present(self.args), status, errstr) # END status handling + + if status != 0: + errstr = read_all_from_possibly_closed_stream(p_stderr) + log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) + raise GitCommandError(remove_password_if_present(self.args), status, errstr) return status # END auto interrupt @@ -694,7 +720,7 @@ def execute(self, as_process: bool = False, output_stream: Union[None, BinaryIO] = None, stdout_as_string: bool = True, - kill_after_timeout: Union[None, int] = None, + kill_after_timeout: Union[None, float] = None, with_stdout: bool = True, universal_newlines: bool = False, shell: Union[None, bool] = None, diff --git a/git/remote.py b/git/remote.py index ce5d82b5b..bfa4db592 100644 --- a/git/remote.py +++ b/git/remote.py @@ -708,7 +708,7 @@ def update(self, **kwargs: Any) -> 'Remote': def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - timeout: Union[None, float] = None, + kill_after_timeout: Union[None, float] = None, ) -> IterableList['FetchInfo']: progress = to_progress_instance(progress) @@ -726,7 +726,7 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', progress_handler = progress.new_message_handler() handle_process_output(proc, None, progress_handler, finalizer=None, decode_streams=False, - timeout=timeout) + kill_after_timeout=kill_after_timeout) stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' proc.wait(stderr=stderr_text) @@ -772,7 +772,7 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', def _get_push_info(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - timeout: Union[None, float] = None) -> IterableList[PushInfo]: + kill_after_timeout: Union[None, float] = None) -> IterableList[PushInfo]: progress = to_progress_instance(progress) # read progress information from stderr @@ -790,7 +790,7 @@ def stdout_handler(line: str) -> None: pass handle_process_output(proc, stdout_handler, progress_handler, finalizer=None, decode_streams=False, - timeout=timeout) + kill_after_timeout=kill_after_timeout) stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' try: proc.wait(stderr=stderr_text) @@ -817,7 +817,8 @@ def _assert_refspec(self) -> None: def fetch(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, None, 'UpdateProgress'] = None, - verbose: bool = True, timeout: Union[None, float] = None, + verbose: bool = True, + kill_after_timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[FetchInfo]: """Fetch the latest changes for this remote @@ -838,6 +839,9 @@ def fetch(self, refspec: Union[str, List[str], None] = None, for 'refspec' will make use of this facility. :param progress: See 'push' method :param verbose: Boolean for verbose output + :param kill_after_timeout: + To specify a timeout in seconds for the git command, after which the process + should be killed. It is set to None by default. :param kwargs: Additional arguments to be passed to git-fetch :return: IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed @@ -858,20 +862,22 @@ def fetch(self, refspec: Union[str, List[str], None] = None, proc = self.repo.git.fetch(self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress, timeout=timeout) + res = self._get_fetch_info_from_stderr(proc, progress, + kill_after_timeout=kill_after_timeout) if hasattr(self.repo.odb, 'update_cache'): self.repo.odb.update_cache() return res def pull(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', None] = None, - timeout: Union[None, float] = None, + kill_after_timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[FetchInfo]: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. :param refspec: see 'fetch' method :param progress: see 'push' method + :param kill_after_timeout: see 'fetch' method :param kwargs: Additional arguments to be passed to git-pull :return: Please see 'fetch' method """ if refspec is None: @@ -880,14 +886,16 @@ def pull(self, refspec: Union[str, List[str], None] = None, kwargs = add_progress(kwargs, self.repo.git, progress) proc = self.repo.git.pull(self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs) - res = self._get_fetch_info_from_stderr(proc, progress, timeout=timeout) + res = self._get_fetch_info_from_stderr(proc, progress, + kill_after_timeout=kill_after_timeout) if hasattr(self.repo.odb, 'update_cache'): self.repo.odb.update_cache() return res def push(self, refspec: Union[str, List[str], None] = None, progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None, - timeout: Union[None, float] = None, **kwargs: Any) -> IterableList[PushInfo]: + kill_after_timeout: Union[None, float] = None, + **kwargs: Any) -> IterableList[PushInfo]: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method @@ -903,6 +911,9 @@ def push(self, refspec: Union[str, List[str], None] = None, overrides the ``update()`` function. :note: No further progress information is returned after push returns. + :param kill_after_timeout: + To specify a timeout in seconds for the git command, after which the process + should be killed. It is set to None by default. :param kwargs: Additional arguments to be passed to git-push :return: list(PushInfo, ...) list of PushInfo instances, each @@ -914,8 +925,11 @@ def push(self, refspec: Union[str, List[str], None] = None, be 0.""" kwargs = add_progress(kwargs, self.repo.git, progress) proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, - universal_newlines=True, **kwargs) - return self._get_push_info(proc, progress, timeout=timeout) + universal_newlines=True, + kill_after_timeout=kill_after_timeout, + **kwargs) + return self._get_push_info(proc, progress, + kill_after_timeout=kill_after_timeout) @ property def config_reader(self) -> SectionConstraint[GitConfigParser]: From 0a58afea0d7c3ff57916ddd694d052123e29087f Mon Sep 17 00:00:00 2001 From: sroet Date: Mon, 13 Sep 2021 17:59:24 +0200 Subject: [PATCH 0039/1225] update tests and add a comment about different behaviour of 'push' vs 'fetch' --- git/remote.py | 2 ++ test/test_remote.py | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index bfa4db592..9917c4310 100644 --- a/git/remote.py +++ b/git/remote.py @@ -795,6 +795,8 @@ def stdout_handler(line: str) -> None: try: proc.wait(stderr=stderr_text) except Exception: + # This is different than fetch (which fails if there is any std_err + # even if there is an output) if not output: raise elif stderr_text: diff --git a/test/test_remote.py b/test/test_remote.py index e5fe8dd00..1cbc2eb21 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -6,6 +6,7 @@ import random import tempfile +import pytest from unittest import skipIf from git import ( @@ -401,12 +402,12 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo): res = remote.push(all=True) self._do_test_push_result(res, remote) - remote.pull('master', timeout=10.0) + remote.pull('master', kill_after_timeout=10.0) # cleanup - delete created tags and branches as we are in an innerloop on # the same repository TagReference.delete(rw_repo, new_tag, other_tag) - remote.push(":%s" % other_tag.path, timeout=10.0) + remote.push(":%s" % other_tag.path, kill_after_timeout=10.0) @skipIf(HIDE_WINDOWS_FREEZE_ERRORS, "FIXME: Freezes!") @with_rw_and_rw_remote_repo('0.1.6') @@ -467,7 +468,8 @@ def test_base(self, rw_repo, remote_repo): # Only for remotes - local cases are the same or less complicated # as additional progress information will never be emitted if remote.name == "daemon_origin": - self._do_test_fetch(remote, rw_repo, remote_repo, timeout=10.0) + self._do_test_fetch(remote, rw_repo, remote_repo, + kill_after_timeout=10.0) ran_fetch_test = True # END fetch test @@ -651,3 +653,15 @@ def test_push_error(self, repo): rem = repo.remote('origin') with self.assertRaisesRegex(GitCommandError, "src refspec __BAD_REF__ does not match any"): rem.push('__BAD_REF__') + + +class TestTimeouts(TestBase): + @with_rw_repo('HEAD', bare=False) + def test_timeout_funcs(self, repo): + for function in ["pull", "fetch"]: #"can't get push to reliably timeout + f = getattr(repo.remotes.origin, function) + assert f is not None # Make sure these functions exist + + with self.assertRaisesRegex(GitCommandError, + "kill_after_timeout=0.01 s"): + f(kill_after_timeout=0.01) From cd2d53844ae50998fa81f9ce42e7bc66b60f8366 Mon Sep 17 00:00:00 2001 From: sroet Date: Mon, 13 Sep 2021 18:04:27 +0200 Subject: [PATCH 0040/1225] go for pytest.raises and test that the functions run --- test/test_remote.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_remote.py b/test/test_remote.py index 1cbc2eb21..10f0bb4bd 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -661,7 +661,7 @@ def test_timeout_funcs(self, repo): for function in ["pull", "fetch"]: #"can't get push to reliably timeout f = getattr(repo.remotes.origin, function) assert f is not None # Make sure these functions exist - - with self.assertRaisesRegex(GitCommandError, - "kill_after_timeout=0.01 s"): + _ = f() # Make sure the function runs + with pytest.raises(GitCommandError, + match="kill_after_timeout=0.01 s"): f(kill_after_timeout=0.01) From 144817a7da2c61cb0b678602d229a351f08df336 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 12:27:17 +0200 Subject: [PATCH 0041/1225] make flake8 and mypy happy --- git/cmd.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index db06d5f7c..7523ead57 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -154,14 +154,22 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], for t in threads: t.join(timeout=kill_after_timeout) if t.is_alive(): - if hasattr(process, 'proc'): # Assume it is a Git.AutoInterrupt: + if isinstance(process, Git.AutoInterrupt): process._terminate() else: # Don't want to deal with the other case - raise RuntimeError(f"Thread join() timed out in cmd.handle_process_output()." - " kill_after_timeout={kill_after_timeout} seconds") + raise RuntimeError("Thread join() timed out in cmd.handle_process_output()." + f" kill_after_timeout={kill_after_timeout} seconds") if stderr_handler: - stderr_handler("error: process killed because it timed out." - f" kill_after_timeout={kill_after_timeout} seconds") + error_str: Union[str, bytes] = ( + "error: process killed because it timed out." + f" kill_after_timeout={kill_after_timeout} seconds") + if not decode_streams and isinstance(p_stderr, BinaryIO): + # Assume stderr_handler needs binary input + error_str = cast(str, error_str) + error_str = error_str.encode() + # We ignore typing on the next line because mypy does not like + # the way we infered that stderr takes str of bytes + stderr_handler(error_str) # type: ignore if finalizer: return finalizer(process) @@ -404,7 +412,7 @@ class AutoInterrupt(object): def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.proc = proc self.args = args - self.status = None + self.status: Union[int, None] = None def _terminate(self) -> None: """Terminate the underlying process""" @@ -447,8 +455,6 @@ def _terminate(self) -> None: call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) # END exception handling - - def __del__(self) -> None: self._terminate() @@ -465,11 +471,11 @@ def wait(self, stderr: Union[None, str, bytes] = b'') -> int: if stderr is None: stderr_b = b'' stderr_b = force_bytes(data=stderr, encoding='utf-8') - + status: Union[int, None] if self.proc is not None: status = self.proc.wait() p_stderr = self.proc.stderr - else: #Assume the underlying proc was killed earlier or never existed + else: # Assume the underlying proc was killed earlier or never existed status = self.status p_stderr = None From 415147046b6950cdc1812dce076a4de78eead162 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 12:34:23 +0200 Subject: [PATCH 0042/1225] fix typo's --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 7523ead57..9279bb0c3 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -168,7 +168,7 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], error_str = cast(str, error_str) error_str = error_str.encode() # We ignore typing on the next line because mypy does not like - # the way we infered that stderr takes str of bytes + # the way we inferred that stderr takes str or bytes stderr_handler(error_str) # type: ignore if finalizer: From 42b05b0f5f3ae7ddd0590a42fd120ffdf2b34903 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 13:30:33 +0200 Subject: [PATCH 0043/1225] make test timeout stricter --- test/test_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_remote.py b/test/test_remote.py index 10f0bb4bd..8f0206646 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -663,5 +663,5 @@ def test_timeout_funcs(self, repo): assert f is not None # Make sure these functions exist _ = f() # Make sure the function runs with pytest.raises(GitCommandError, - match="kill_after_timeout=0.01 s"): - f(kill_after_timeout=0.01) + match="kill_after_timeout=0 s"): + f(kill_after_timeout=0) From e95ee636504a42bd5d8c83314a676253a2de9ad6 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 13:59:22 +0200 Subject: [PATCH 0044/1225] fetch is also to quick on CI, only test pull --- test/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 8f0206646..9fe649ad7 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -658,7 +658,7 @@ def test_push_error(self, repo): class TestTimeouts(TestBase): @with_rw_repo('HEAD', bare=False) def test_timeout_funcs(self, repo): - for function in ["pull", "fetch"]: #"can't get push to reliably timeout + for function in ["pull"]: #"can't get fetch and push to reliably timeout f = getattr(repo.remotes.origin, function) assert f is not None # Make sure these functions exist _ = f() # Make sure the function runs From 4588efd0e086a240f3e1c826be63a2bd30eedf36 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 14:00:30 +0200 Subject: [PATCH 0045/1225] two spaces before comments --- test/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 9fe649ad7..4b06a88ac 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -658,7 +658,7 @@ def test_push_error(self, repo): class TestTimeouts(TestBase): @with_rw_repo('HEAD', bare=False) def test_timeout_funcs(self, repo): - for function in ["pull"]: #"can't get fetch and push to reliably timeout + for function in ["pull"]: # can't get fetch and push to reliably timeout f = getattr(repo.remotes.origin, function) assert f is not None # Make sure these functions exist _ = f() # Make sure the function runs From 893ddabd312535bfd906822e42f0223c40655163 Mon Sep 17 00:00:00 2001 From: sroet Date: Tue, 14 Sep 2021 14:09:29 +0200 Subject: [PATCH 0046/1225] set timeout to a non-zero value --- test/test_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_remote.py b/test/test_remote.py index 4b06a88ac..4c1d02c86 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -663,5 +663,5 @@ def test_timeout_funcs(self, repo): assert f is not None # Make sure these functions exist _ = f() # Make sure the function runs with pytest.raises(GitCommandError, - match="kill_after_timeout=0 s"): - f(kill_after_timeout=0) + match="kill_after_timeout=0.001 s"): + f(kill_after_timeout=0.001) From aa5076626ca9f2ff1279c6b8e67408be9d0fa690 Mon Sep 17 00:00:00 2001 From: sroet Date: Wed, 15 Sep 2021 11:55:17 +0200 Subject: [PATCH 0047/1225] Add a way to force status codes inside AutoInterrupt._terminate, and let tests use it --- git/cmd.py | 19 ++++++++++++------- test/test_remote.py | 14 ++++++++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 9279bb0c3..8fb10742f 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -409,6 +409,10 @@ class AutoInterrupt(object): __slots__ = ("proc", "args", "status") + # If this is non-zero it will override any status code during + # _terminate, used to prevent race conditions in testing + _status_code_if_terminate: int = 0 + def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.proc = proc self.args = args @@ -427,11 +431,10 @@ def _terminate(self) -> None: proc.stdout.close() if proc.stderr: proc.stderr.close() - # did the process finish already so we have a return code ? try: if proc.poll() is not None: - self.status = proc.poll() + self.status = self._status_code_if_terminate or proc.poll() return None except OSError as ex: log.info("Ignored error after process had died: %r", ex) @@ -443,7 +446,9 @@ def _terminate(self) -> None: # try to kill it try: proc.terminate() - self.status = proc.wait() # ensure process goes away + status = proc.wait() # ensure process goes away + + self.status = self._status_code_if_terminate or status except OSError as ex: log.info("Ignored error after process had died: %r", ex) except AttributeError: @@ -849,7 +854,7 @@ def execute(self, if is_win: cmd_not_found_exception = OSError - if kill_after_timeout: + if kill_after_timeout is not None: raise GitCommandError(redacted_command, '"kill_after_timeout" feature is not supported on Windows.') else: cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable @@ -916,7 +921,7 @@ def _kill_process(pid: int) -> None: return # end - if kill_after_timeout: + if kill_after_timeout is not None: kill_check = threading.Event() watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,)) @@ -927,10 +932,10 @@ def _kill_process(pid: int) -> None: newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: - if kill_after_timeout: + if kill_after_timeout is not None: watchdog.start() stdout_value, stderr_value = proc.communicate() - if kill_after_timeout: + if kill_after_timeout is not None: watchdog.cancel() if kill_check.is_set(): stderr_value = ('Timeout: the command "%s" did not complete in %d ' diff --git a/test/test_remote.py b/test/test_remote.py index 4c1d02c86..088fdad55 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -658,10 +658,16 @@ def test_push_error(self, repo): class TestTimeouts(TestBase): @with_rw_repo('HEAD', bare=False) def test_timeout_funcs(self, repo): - for function in ["pull"]: # can't get fetch and push to reliably timeout + # Force error code to prevent a race condition if the python thread is + # slow + default = Git.AutoInterrupt._status_code_if_terminate + Git.AutoInterrupt._status_code_if_terminate = -15 + for function in ["pull", "fetch"]: # can't get push to timeout f = getattr(repo.remotes.origin, function) assert f is not None # Make sure these functions exist - _ = f() # Make sure the function runs + _ = f() # Make sure the function runs with pytest.raises(GitCommandError, - match="kill_after_timeout=0.001 s"): - f(kill_after_timeout=0.001) + match="kill_after_timeout=0 s"): + f(kill_after_timeout=0) + + Git.AutoInterrupt._status_code_if_terminate = default From 2d15c5a601e698e8f7859e821950cad0701b756d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 18 Sep 2021 09:30:26 +0800 Subject: [PATCH 0048/1225] =?UTF-8?q?prepare=20new=20release,=20bump=20ver?= =?UTF-8?q?sion=20patch=20level=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …which could probably have been a minor version last time. --- VERSION | 2 +- doc/source/changes.rst | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 60b9d63bc..05f5ca23b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.23 +3.1.24 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index ac73b1722..4186ac911 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,9 +2,19 @@ Changelog ========= -3.1.23 + +3.1.24 ====== +* Newly added timeout flag is not be enabled by default, and was renamed to kill_after_timeout + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/54?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/53?closed=1 + +3.1.23 (YANKED) +=============== + * This is the second typed release with a lot of improvements under the hood. * General: @@ -45,7 +55,7 @@ Changelog - Add timeout to handle_process_output(), in case thread.join() hangs. See the following for details: -https://github.com/gitpython-developers/gitpython/milestone/52?closed=1 +https://github.com/gitpython-developers/gitpython/milestone/53?closed=1 3.1.20 (YANKED) From 5f4b4dbff46fae4c899f5573aea5a7266a41eeeb Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Mon, 20 Sep 2021 13:53:42 -0700 Subject: [PATCH 0049/1225] Fix typing issues with delete_head and Remote.add delete_head and Head.delete historically accept either Head objects or a str name of a head. Adjust the typing to match. This unfortunately requires suppressing type warnings in the signature of RemoteReference.delete, since it inherits from Head but does not accept str (since it needs access to the richer data of RemoteReference). Using assignment to make add an alias for create unfortunately confuses mypy, since it loses track of the fact that it's a classmethod and starts treating it like a staticmethod. Replace with a stub wrapper instead. --- git/refs/head.py | 2 +- git/refs/remote.py | 7 ++++++- git/remote.py | 4 +++- git/repo/base.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/git/refs/head.py b/git/refs/head.py index 56a87182f..d1d72c7bd 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -129,7 +129,7 @@ class Head(Reference): k_config_remote_ref = "merge" # branch to merge from remote @classmethod - def delete(cls, repo: 'Repo', *heads: 'Head', force: bool = False, **kwargs: Any) -> None: + def delete(cls, repo: 'Repo', *heads: 'Union[Head, str]', force: bool = False, **kwargs: Any) -> None: """Delete the given heads :param force: diff --git a/git/refs/remote.py b/git/refs/remote.py index 9b74d87fb..1b416bd0a 100644 --- a/git/refs/remote.py +++ b/git/refs/remote.py @@ -37,8 +37,13 @@ def iter_items(cls, repo: 'Repo', common_path: Union[PathLike, None] = None, # super is Reference return super(RemoteReference, cls).iter_items(repo, common_path) + # The Head implementation of delete also accepts strs, but this + # implementation does not. mypy doesn't have a way of representing + # tightening the types of arguments in subclasses and recommends Any or + # "type: ignore". (See https://github.com/python/typing/issues/241) @ classmethod - def delete(cls, repo: 'Repo', *refs: 'RemoteReference', **kwargs: Any) -> None: + def delete(cls, repo: 'Repo', *refs: 'RemoteReference', # type: ignore + **kwargs: Any) -> None: """Delete the given remote references :note: diff --git a/git/remote.py b/git/remote.py index 9917c4310..2cf5678b6 100644 --- a/git/remote.py +++ b/git/remote.py @@ -665,7 +665,9 @@ def create(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote': return cls(repo, name) # add is an alias - add = create + @ classmethod + def add(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote': + return cls.create(repo, name, url, **kwargs) @ classmethod def remove(cls, repo: 'Repo', name: str) -> str: diff --git a/git/repo/base.py b/git/repo/base.py index e308fd8a2..7713c9152 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -429,7 +429,7 @@ def create_head(self, path: PathLike, commit: str = 'HEAD', :return: newly created Head Reference""" return Head.create(self, path, commit, logmsg, force) - def delete_head(self, *heads: 'Head', **kwargs: Any) -> None: + def delete_head(self, *heads: 'Union[str, Head]', **kwargs: Any) -> None: """Delete the given heads :param kwargs: Additional keyword arguments to be passed to git-branch""" From 5e73cabd041f45337b270d5e78674d88448929e6 Mon Sep 17 00:00:00 2001 From: Ket3r Date: Wed, 29 Sep 2021 21:04:14 +0200 Subject: [PATCH 0050/1225] Fix broken test requirements The ddt package changed the function signature in version 1.4.3 from idata(iterable) to idata(iterable, index_len). Hopefully this was just a mistake and the new argument will be optional in future versions (see issue datadriventests/ddt#97) --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index deaafe214..d5d2346a0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -ddt>=1.1.1 +ddt>=1.1.1, !=1.4.3 mypy flake8 From 53d94b8091b36847bb9e495c76bb5a3ec2a2fdb5 Mon Sep 17 00:00:00 2001 From: Trym Bremnes Date: Thu, 30 Sep 2021 08:54:43 +0200 Subject: [PATCH 0051/1225] Replace wildcard imports with concrete imports All `from import *` has now been replaced by `from import X, Y, ...`. Contributes to #1349 --- git/__init__.py | 22 +++++++++++----------- git/exc.py | 3 +-- git/index/__init__.py | 4 ++-- git/objects/__init__.py | 14 +++++++------- git/refs/__init__.py | 12 ++++++------ test/lib/__init__.py | 7 +++++-- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index ae9254a26..a2213ee0f 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa #@PydevCodeAnalysisIgnore -from git.exc import * # @NoMove @IgnorePep8 +from git.exc import GitError, GitCommandError, GitCommandNotFound, UnmergedEntriesError, CheckoutError, InvalidGitRepositoryError, NoSuchPathError, BadName # @NoMove @IgnorePep8 import inspect import os import sys @@ -39,16 +39,16 @@ def _init_externals() -> None: #{ Imports try: - from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import * # @NoMove @IgnorePep8 - from git.refs import * # @NoMove @IgnorePep8 - from git.diff import * # @NoMove @IgnorePep8 - from git.db import * # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import * # @NoMove @IgnorePep8 - from git.index import * # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import Blob, Commit, Object, Submodule, Tree # @NoMove @IgnorePep8 + from git.refs import Head, Reference, RefLog, RemoteReference, SymbolicReference, TagReference # @NoMove @IgnorePep8 + from git.diff import Diff, DiffIndex, NULL_TREE # @NoMove @IgnorePep8 + from git.db import GitCmdObjectDB, GitDB # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove @IgnorePep8 + from git.index import BlobFilter, IndexEntry, IndexFile # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, diff --git a/git/exc.py b/git/exc.py index e8ff784c7..d29a25f63 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,8 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ -from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 -from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import BadName, BadObject # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode # typing ---------------------------------------------------- diff --git a/git/index/__init__.py b/git/index/__init__.py index 96b721f07..f0ac81e5a 100644 --- a/git/index/__init__.py +++ b/git/index/__init__.py @@ -1,4 +1,4 @@ """Initialize the index package""" # flake8: noqa -from .base import * -from .typ import * +from .base import IndexFile +from .typ import IndexEntry, BlobFilter diff --git a/git/objects/__init__.py b/git/objects/__init__.py index 1d0bb7a51..c4a492274 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -4,14 +4,14 @@ # flake8: noqa import inspect -from .base import * -from .blob import * -from .commit import * +from .base import Object, IndexObject +from .blob import Blob +from .commit import Commit from .submodule import util as smutil -from .submodule.base import * -from .submodule.root import * -from .tag import * -from .tree import * +from .submodule.base import Submodule, UpdateProgress +from .submodule.root import RootModule, RootUpdateProgress +from .tag import TagObject +from .tree import Tree # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base smutil.IndexObject = IndexObject # type: ignore[attr-defined] diff --git a/git/refs/__init__.py b/git/refs/__init__.py index 1486dffe6..075c65c8f 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -1,9 +1,9 @@ # flake8: noqa # import all modules in order, fix the names they require -from .symbolic import * -from .reference import * -from .head import * -from .tag import * -from .remote import * +from .symbolic import SymbolicReference +from .reference import Reference +from .head import HEAD, Head +from .tag import TagReference +from .remote import RemoteReference -from .log import * +from .log import RefLogEntry, RefLog diff --git a/test/lib/__init__.py b/test/lib/__init__.py index 1551ce455..3634df803 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -4,9 +4,12 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -# flake8: noqa import inspect -from .helper import * + +from .helper import (GIT_DAEMON_PORT, SkipTest, StringProcessAdapter, TestBase, + TestCase, fixture, fixture_path, + with_rw_and_rw_remote_repo, with_rw_directory, + with_rw_repo) __all__ = [name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))] From ce4afe46d211cdfb611b8e8109bb0dc160a12540 Mon Sep 17 00:00:00 2001 From: Trym Bremnes Date: Sat, 2 Oct 2021 16:42:35 +0200 Subject: [PATCH 0052/1225] Revert "Replace wildcard imports with concrete imports" This reverts commit 53d94b8091b36847bb9e495c76bb5a3ec2a2fdb5. The reason for the revert is that the commit in question introduced a regression where certain modules, functions and classes that were exposed before were no longer exposed. See https://github.com/gitpython-developers/GitPython/pull/1352#issuecomment-932757204 for additional information. --- git/__init__.py | 22 +++++++++++----------- git/exc.py | 3 ++- git/index/__init__.py | 4 ++-- git/objects/__init__.py | 14 +++++++------- git/refs/__init__.py | 12 ++++++------ test/lib/__init__.py | 7 ++----- 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index a2213ee0f..ae9254a26 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa #@PydevCodeAnalysisIgnore -from git.exc import GitError, GitCommandError, GitCommandNotFound, UnmergedEntriesError, CheckoutError, InvalidGitRepositoryError, NoSuchPathError, BadName # @NoMove @IgnorePep8 +from git.exc import * # @NoMove @IgnorePep8 import inspect import os import sys @@ -39,16 +39,16 @@ def _init_externals() -> None: #{ Imports try: - from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import Blob, Commit, Object, Submodule, Tree # @NoMove @IgnorePep8 - from git.refs import Head, Reference, RefLog, RemoteReference, SymbolicReference, TagReference # @NoMove @IgnorePep8 - from git.diff import Diff, DiffIndex, NULL_TREE # @NoMove @IgnorePep8 - from git.db import GitCmdObjectDB, GitDB # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove @IgnorePep8 - from git.index import BlobFilter, IndexEntry, IndexFile # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import * # @NoMove @IgnorePep8 + from git.refs import * # @NoMove @IgnorePep8 + from git.diff import * # @NoMove @IgnorePep8 + from git.db import * # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # @NoMove @IgnorePep8 + from git.index import * # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, diff --git a/git/exc.py b/git/exc.py index d29a25f63..e8ff784c7 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,7 +5,8 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ -from gitdb.exc import BadName, BadObject # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode # typing ---------------------------------------------------- diff --git a/git/index/__init__.py b/git/index/__init__.py index f0ac81e5a..96b721f07 100644 --- a/git/index/__init__.py +++ b/git/index/__init__.py @@ -1,4 +1,4 @@ """Initialize the index package""" # flake8: noqa -from .base import IndexFile -from .typ import IndexEntry, BlobFilter +from .base import * +from .typ import * diff --git a/git/objects/__init__.py b/git/objects/__init__.py index c4a492274..1d0bb7a51 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -4,14 +4,14 @@ # flake8: noqa import inspect -from .base import Object, IndexObject -from .blob import Blob -from .commit import Commit +from .base import * +from .blob import * +from .commit import * from .submodule import util as smutil -from .submodule.base import Submodule, UpdateProgress -from .submodule.root import RootModule, RootUpdateProgress -from .tag import TagObject -from .tree import Tree +from .submodule.base import * +from .submodule.root import * +from .tag import * +from .tree import * # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base smutil.IndexObject = IndexObject # type: ignore[attr-defined] diff --git a/git/refs/__init__.py b/git/refs/__init__.py index 075c65c8f..1486dffe6 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -1,9 +1,9 @@ # flake8: noqa # import all modules in order, fix the names they require -from .symbolic import SymbolicReference -from .reference import Reference -from .head import HEAD, Head -from .tag import TagReference -from .remote import RemoteReference +from .symbolic import * +from .reference import * +from .head import * +from .tag import * +from .remote import * -from .log import RefLogEntry, RefLog +from .log import * diff --git a/test/lib/__init__.py b/test/lib/__init__.py index 3634df803..1551ce455 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -4,12 +4,9 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +# flake8: noqa import inspect - -from .helper import (GIT_DAEMON_PORT, SkipTest, StringProcessAdapter, TestBase, - TestCase, fixture, fixture_path, - with_rw_and_rw_remote_repo, with_rw_directory, - with_rw_repo) +from .helper import * __all__ = [name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))] From b17bc980b1546159ceb119f04716f24b043fc3f8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 3 Oct 2021 19:44:18 +0800 Subject: [PATCH 0053/1225] =?UTF-8?q?It's=20python,=20so=20stuff=20breaks?= =?UTF-8?q?=20with=20patches=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …https://github.com/pytest-dev/pytest-cov/pull/472 Break a few to fix a few. --- test-requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d5d2346a0..53d8e606d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,4 +10,5 @@ virtualenv pytest pytest-cov -pytest-sugar \ No newline at end of file +coverage[toml] +pytest-sugar From b0630030a1d2db994fb2fb488efa167b91594864 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 13 Oct 2021 11:16:27 +0300 Subject: [PATCH 0054/1225] Add support for Python 3.10 --- .github/workflows/pythonpackage.yml | 2 +- AUTHORS | 1 + setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4e7aa418c..dd1e9a07e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.7.5, 3.7.12, 3.8, 3.8.0, 3.8.11, 3.8, 3.9, 3.9.0, 3.9.7] # , "3.10.0-rc.2"] + python-version: [3.7, 3.7.5, 3.7.12, 3.8, 3.8.0, 3.8.11, 3.8, 3.9, 3.9.0, 3.9.7, "3.10"] steps: - uses: actions/checkout@v2 diff --git a/AUTHORS b/AUTHORS index 606796d98..55d681813 100644 --- a/AUTHORS +++ b/AUTHORS @@ -44,4 +44,5 @@ Contributors are: -Ram Rachum -Alba Mendez -Robert Westman +-Hugo van Kemenade Portions derived from other open source works and are clearly marked. diff --git a/setup.py b/setup.py index cd1007d74..4f1d0b75e 100755 --- a/setup.py +++ b/setup.py @@ -122,6 +122,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: 3.10" + "Programming Language :: Python :: 3.10", ] ) From a9696eff9bbf8ffe266653f95c9748e40c58a5d1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 13 Oct 2021 11:26:02 +0300 Subject: [PATCH 0055/1225] Sphinx 4.3.0 will be needed for Python 3.10 --- doc/requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 917feb350..ad3c118a2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,6 @@ -sphinx==4.1.2 +# TODO Temporary until Sphinx 4.3.0 released with Python 3.10 support: +# https://github.com/sphinx-doc/sphinx/pull/9712 +sphinx==4.1.2;python_version<="3.9" +git+git://github.com/sphinx-doc/sphinx@f13ad80#egg=sphinx;python_version>="3.10" sphinx_rtd_theme sphinx-autodoc-typehints From a3efd2458afe535ffd9dcc756d8ba0c931d10ff2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 10 Nov 2021 20:23:06 +0200 Subject: [PATCH 0056/1225] Remove Sphinx workaround --- doc/requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index ad3c118a2..41a7c90f1 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,3 @@ -# TODO Temporary until Sphinx 4.3.0 released with Python 3.10 support: -# https://github.com/sphinx-doc/sphinx/pull/9712 -sphinx==4.1.2;python_version<="3.9" -git+git://github.com/sphinx-doc/sphinx@f13ad80#egg=sphinx;python_version>="3.10" +sphinx==4.3.0 sphinx_rtd_theme sphinx-autodoc-typehints From 3b82fa3018a21f0eeb76034ecb8fb4dedea9a966 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 12 Oct 2021 11:23:39 +0200 Subject: [PATCH 0057/1225] Let remote.push return a PushInfoList List-like, so that it's backward compatible. But it has a new method raise_on_error, that throws an exception if anything failed to push. Related to #621 --- git/remote.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index 2cf5678b6..63b4dc510 100644 --- a/git/remote.py +++ b/git/remote.py @@ -116,6 +116,22 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress +class PushInfoList(IterableList): + def __new__(cls) -> 'IterableList[IterableObj]': + return super(IterableList, cls).__new__(cls, 'push_infos') + + def __init__(self) -> None: + super().__init__('push_infos') + self.exception = None + + def raise_on_error(self): + """ + Raise an exception if any ref failed to push. + """ + if self.exception: + raise self.exception + + class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -774,7 +790,7 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', def _get_push_info(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - kill_after_timeout: Union[None, float] = None) -> IterableList[PushInfo]: + kill_after_timeout: Union[None, float] = None) -> PushInfoList: progress = to_progress_instance(progress) # read progress information from stderr @@ -782,7 +798,7 @@ def _get_push_info(self, proc: 'Git.AutoInterrupt', # read the lines manually as it will use carriage returns between the messages # to override the previous one. This is why we read the bytes manually progress_handler = progress.new_message_handler() - output: IterableList[PushInfo] = IterableList('push_infos') + output: PushInfoList = PushInfoList() def stdout_handler(line: str) -> None: try: @@ -796,13 +812,14 @@ def stdout_handler(line: str) -> None: stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' try: proc.wait(stderr=stderr_text) - except Exception: + except Exception as e: # This is different than fetch (which fails if there is any std_err # even if there is an output) if not output: raise elif stderr_text: log.warning("Error lines received while fetching: %s", stderr_text) + output.exception = e return output From 1481e7108fb206a95717c331478d4382cda51a6a Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 13 Oct 2021 10:03:53 +0200 Subject: [PATCH 0058/1225] Test that return value of push is a list-like object --- test/test_remote.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 088fdad55..fedfa2070 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -30,7 +30,7 @@ fixture, GIT_DAEMON_PORT ) -from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS +from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList import os.path as osp @@ -128,6 +128,9 @@ def _do_test_fetch_result(self, results, remote): # END for each info def _do_test_push_result(self, results, remote): + self.assertIsInstance(results, list) + self.assertIsInstance(results, IterableList) + self.assertGreater(len(results), 0) self.assertIsInstance(results[0], PushInfo) for info in results: From 9240de9f788396c45199cd3d9fa7fdbd8a5666c4 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 8 Nov 2021 16:20:32 +0000 Subject: [PATCH 0059/1225] Rename exception to error, raise_on_error to raise_if_error --- git/remote.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/git/remote.py b/git/remote.py index 63b4dc510..745436761 100644 --- a/git/remote.py +++ b/git/remote.py @@ -122,14 +122,14 @@ def __new__(cls) -> 'IterableList[IterableObj]': def __init__(self) -> None: super().__init__('push_infos') - self.exception = None + self.error = None - def raise_on_error(self): + def raise_if_error(self): """ Raise an exception if any ref failed to push. """ - if self.exception: - raise self.exception + if self.error: + raise self.error class PushInfo(IterableObj, object): @@ -819,7 +819,7 @@ def stdout_handler(line: str) -> None: raise elif stderr_text: log.warning("Error lines received while fetching: %s", stderr_text) - output.exception = e + output.error = e return output From 699e223c51d99d1fc8d05b2b0fe0ef1e2ee7fd01 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 8 Nov 2021 17:06:37 +0000 Subject: [PATCH 0060/1225] Test raise_if_error --- test/test_remote.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_remote.py b/test/test_remote.py index fedfa2070..761a7a3e7 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -154,6 +154,12 @@ def _do_test_push_result(self, results, remote): # END error checking # END for each info + if any([info.flags & info.ERROR for info in results]): + self.assertRaises(GitCommandError, results.raise_if_error) + else: + # No errors, so this should do nothing + results.raise_if_error() + def _do_test_fetch_info(self, repo): self.assertRaises(ValueError, FetchInfo._from_line, repo, "nonsense", '') self.assertRaises( From 63f4ca304bddf019220912b7b8e2abe585d88fe0 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 11:55:51 +0000 Subject: [PATCH 0061/1225] Add raise_if_error() to tutorial --- test/test_docs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_docs.py b/test/test_docs.py index 220156bce..8897bbb75 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -393,7 +393,8 @@ def test_references_and_objects(self, rw_dir): origin.rename('new_origin') # push and pull behaves similarly to `git push|pull` origin.pull() - origin.push() + origin.push() # attempt push, ignore errors + origin.push().raise_if_error() # push and raise error if it fails # assert not empty_repo.delete_remote(origin).exists() # create and delete remotes # ![25-test_references_and_objects] From 8797904d04abc2df5da93ca7d799da21e5a50cb5 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 12:17:02 +0000 Subject: [PATCH 0062/1225] Fix type handing on PushInfoList --- git/remote.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index 745436761..aae845e5d 100644 --- a/git/remote.py +++ b/git/remote.py @@ -117,14 +117,15 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non class PushInfoList(IterableList): - def __new__(cls) -> 'IterableList[IterableObj]': - return super(IterableList, cls).__new__(cls, 'push_infos') + def __new__(cls) -> 'PushInfoList': + base = super().__new__(cls, 'push_infos') + return cast(PushInfoList, base) def __init__(self) -> None: super().__init__('push_infos') self.error = None - def raise_if_error(self): + def raise_if_error(self) -> None: """ Raise an exception if any ref failed to push. """ From e67e458ece9077f6c6db9fc6a867ac61e0ae6579 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 15:16:44 +0000 Subject: [PATCH 0063/1225] Specify type for PushInfoList.error --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index aae845e5d..c212f6d28 100644 --- a/git/remote.py +++ b/git/remote.py @@ -123,7 +123,7 @@ def __new__(cls) -> 'PushInfoList': def __init__(self) -> None: super().__init__('push_infos') - self.error = None + self.error: Optional[Exception] = None def raise_if_error(self) -> None: """ From 35f7e9486c8bc596506a6872c7e0df37c4a35da3 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 10 Nov 2021 12:40:06 +0000 Subject: [PATCH 0064/1225] Extend IterableList[PushInfo] instead of IterableList --- git/remote.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/git/remote.py b/git/remote.py index c212f6d28..7d5918a5a 100644 --- a/git/remote.py +++ b/git/remote.py @@ -116,23 +116,6 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress -class PushInfoList(IterableList): - def __new__(cls) -> 'PushInfoList': - base = super().__new__(cls, 'push_infos') - return cast(PushInfoList, base) - - def __init__(self) -> None: - super().__init__('push_infos') - self.error: Optional[Exception] = None - - def raise_if_error(self) -> None: - """ - Raise an exception if any ref failed to push. - """ - if self.error: - raise self.error - - class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -252,6 +235,22 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any raise NotImplementedError +class PushInfoList(IterableList[PushInfo]): + def __new__(cls) -> 'PushInfoList': + return cast(PushInfoList, IterableList.__new__(cls, 'push_infos')) + + def __init__(self) -> None: + super().__init__('push_infos') + self.error: Optional[Exception] = None + + def raise_if_error(self) -> None: + """ + Raise an exception if any ref failed to push. + """ + if self.error: + raise self.error + + class FetchInfo(IterableObj, object): """ From 62131f3905ac37ff841142e2bb04bb585401a3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 30 Nov 2021 10:17:52 +0100 Subject: [PATCH 0065/1225] Revert the use of typing_extensions in py3.8+ The original change requiring py3.10 TypeGuard (and matching typing_extensions) has been reverted, so revert the requirement on typing_extensions as well. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a20310fb2..7159416a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.3;python_version<"3.10" +typing-extensions>=3.7.4.3;python_version<"3.8" From 2141eaef76fdfb2775dde45d087b34144d34a1fb Mon Sep 17 00:00:00 2001 From: yogabonito Date: Wed, 1 Dec 2021 00:42:39 +0100 Subject: [PATCH 0066/1225] DOC: fix typo --- doc/source/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 303e89cff..bc386e7c4 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -8,9 +8,9 @@ GitPython Tutorial ================== -GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explains a real-life usecase. +GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explain a real-life use case. -All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. +All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes. All you need is a developer installation of git-python. Meet the Repo type ****************** From d79d20d28b1f9324193309cffd2ab79e0edae925 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 08:59:19 +0800 Subject: [PATCH 0067/1225] Avoid taking a lock for reading This isn't needed as git will replace this file atomicially, hence we always see a fully written file when reading. Only when writing we need to obtain a lock. --- git/ext/gitdb | 2 +- git/index/base.py | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/git/ext/gitdb b/git/ext/gitdb index 03ab3a1d4..1c976835c 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 03ab3a1d40c04d6a944299c21db61cf9ce30f6bb +Subproject commit 1c976835c5d1779a28b9e11afd1656152db26a68 diff --git a/git/index/base.py b/git/index/base.py index 102703e6d..d1f039cd9 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -127,30 +127,17 @@ def __init__(self, repo: 'Repo', file_path: Union[PathLike, None] = None) -> Non def _set_cache_(self, attr: str) -> None: if attr == "entries": - # read the current index - # try memory map for speed - lfd = LockedFD(self._file_path) - ok = False try: - fd = lfd.open(write=False, stream=False) - ok = True + fd = os.open(self._file_path, os.O_RDONLY) except OSError: # in new repositories, there may be no index, which means we are empty self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {} return None - finally: - if not ok: - lfd.rollback() # END exception handling stream = file_contents_ro(fd, stream=True, allow_mmap=True) - try: - self._deserialize(stream) - finally: - lfd.rollback() - # The handles will be closed on destruction - # END read from default index on demand + self._deserialize(stream) else: super(IndexFile, self)._set_cache_(attr) From da7b5b286a8fc75f2d2e9183bf1d13f9d8cdce49 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 09:47:16 +0800 Subject: [PATCH 0068/1225] Ignore mypi errors With each patch level it may bring up new issues that cause CI failure for without being related to the actual change. --- .github/workflows/pythonpackage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dd1e9a07e..881f2ec57 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -50,6 +50,9 @@ jobs: flake8 - name: Check types with mypy + # With new versions of pypi new issues might arise. This is a problem if there is nobody able to fix them, + # so we have to ignore errors until that changes. + continue-on-error: true run: | set -x mypy -p git From 01f09888208341876d1480bd22dc8f4107c100f1 Mon Sep 17 00:00:00 2001 From: NHanser Date: Thu, 23 Dec 2021 12:51:32 +0100 Subject: [PATCH 0069/1225] Use NUL character to extract meta and path from git diff Use NUL character instead of semicolon to extract meta and path. Avoid errors in during git diff when dealing with filenames containing semicolons --- git/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index cea66d7ee..c8c57685b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -509,9 +509,9 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: Union['Popen', 'Git.AutoIn def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None: lines = lines_bytes.decode(defenc) - for line in lines.split(':')[1:]: - meta, _, path = line.partition('\x00') - path = path.rstrip('\x00') + it = iter(lines.split('\x00')) + for meta, path in zip(it, it): + meta = meta[1:] a_blob_id: Optional[str] b_blob_id: Optional[str] old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) From cdf7ffc33fa05ba5afcb915a374c140c7658c839 Mon Sep 17 00:00:00 2001 From: Peter Kempter Date: Wed, 29 Sep 2021 12:08:30 +0200 Subject: [PATCH 0070/1225] Add failing unit test --- test/test_commit.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/test_commit.py b/test/test_commit.py index 67dc7d732..5aeef2e6c 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -4,6 +4,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +import copy from datetime import datetime from io import BytesIO import re @@ -429,3 +430,48 @@ def test_datetimes(self): datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200))) self.assertEqual(commit.committed_datetime, datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime) + + def test_trailers(self): + KEY_1 = "Hello" + VALUE_1 = "World" + KEY_2 = "Key" + VALUE_2 = "Value" + + # Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations + msgs = [] + msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + + for msg in msgs: + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = msg + assert KEY_1 in commit.trailers.keys() + assert KEY_2 in commit.trailers.keys() + assert commit.trailers[KEY_1] == VALUE_1 + assert commit.trailers[KEY_2] == VALUE_2 + + # Check that trailer stays empty for multiple msg combinations + msgs = [] + msgs.append(f"Subject\n") + msgs.append(f"Subject\n\nBody with some\nText\n") + msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n") + msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n") + msgs.append(f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n") + msgs.append(f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n") + + for msg in msgs: + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = msg + assert len(commit.trailers.keys()) == 0 + + # check that only the last key value paragraph is evaluated + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1}\n\n{KEY_2}: {VALUE_2}\n" + assert KEY_1 not in commit.trailers.keys() + assert KEY_2 in commit.trailers.keys() + assert commit.trailers[KEY_2] == VALUE_2 From edbf76f98f8430d711115f2c754de88e268e9303 Mon Sep 17 00:00:00 2001 From: Peter Kempter Date: Wed, 29 Sep 2021 12:08:56 +0200 Subject: [PATCH 0071/1225] Add trailer as commit property With the command `git interpret-trailers` git provides a way to interact with trailer lines in the commit messages that look similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). The new property returns those parsed trailer lines from the message as dictionary. --- git/objects/commit.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index b36cd46d2..780461a0c 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,6 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import datetime +import re from subprocess import Popen from gitdb import IStream from git.util import ( @@ -39,7 +40,7 @@ # typing ------------------------------------------------------------------ -from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast +from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast, Dict from git.types import PathLike, Literal @@ -315,6 +316,44 @@ def stats(self) -> Stats: text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, '--', numstat=True) return Stats._list_from_string(self.repo, text) + @property + def trailers(self) -> Dict: + """Get the trailers of the message as dictionary + + Git messages can contain trailer information that are similar to RFC 822 + e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). + + The trailer is thereby the last paragraph (seperated by a empty line + from the subject/body). This trailer paragraph must contain a ``:`` as + seperator for key and value in every line. + + Valid message with trailer: + + .. code-block:: + + Subject line + + some body information + + another information + + key1: value1 + key2: value2 + + :return: Dictionary containing whitespace stripped trailer information + """ + d: Dict[str, str] = {} + match = re.search(r".+^\s*$\n([\w\n\s:]+?)\s*\Z", str(self.message), re.MULTILINE | re.DOTALL) + if match is None: + return d + last_paragraph = match.group(1) + if not all(':' in line for line in last_paragraph.split('\n')): + return d + for line in last_paragraph.split('\n'): + key, value = line.split(':', 1) + d[key.strip()] = value.strip() + return d + @ classmethod def _iter_from_process_or_stream(cls, repo: 'Repo', proc_or_stream: Union[Popen, IO]) -> Iterator['Commit']: """Parse out commit information into a list of Commit objects From cd8b9b2fd875b5040b1ca9f0c8f5acaffe70ab7f Mon Sep 17 00:00:00 2001 From: Ket3r Date: Thu, 30 Sep 2021 16:07:05 +0200 Subject: [PATCH 0072/1225] Use git interpret-trailers for trailers property The whitespace handling and trailer selection isn't very trivial or good documented. It therefore seemed easier and less error prone to just call git to parse the message for the trailers section and remove superfluos whitespaces. --- git/objects/commit.py | 43 ++++++++++++++++++++++++++----------------- test/test_commit.py | 4 ++-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 780461a0c..bbd485da8 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,8 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import datetime -import re -from subprocess import Popen +from subprocess import Popen, PIPE from gitdb import IStream from git.util import ( hex_to_bin, @@ -14,6 +13,7 @@ finalize_process ) from git.diff import Diffable +from git.cmd import Git from .tree import Tree from . import base @@ -322,10 +322,10 @@ def trailers(self) -> Dict: Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). - - The trailer is thereby the last paragraph (seperated by a empty line - from the subject/body). This trailer paragraph must contain a ``:`` as - seperator for key and value in every line. + + This funcions calls ``git interpret-trailers --parse`` onto the message + to extract the trailer information. The key value pairs are stripped of + leading and trailing whitespaces before they get saved into a dictionary. Valid message with trailer: @@ -338,20 +338,29 @@ def trailers(self) -> Dict: another information key1: value1 - key2: value2 + key2 : value 2 with inner spaces + + dictionary will look like this: + .. code-block:: + + { + "key1": "value1", + "key2": "value 2 with inner spaces" + } :return: Dictionary containing whitespace stripped trailer information + """ - d: Dict[str, str] = {} - match = re.search(r".+^\s*$\n([\w\n\s:]+?)\s*\Z", str(self.message), re.MULTILINE | re.DOTALL) - if match is None: - return d - last_paragraph = match.group(1) - if not all(':' in line for line in last_paragraph.split('\n')): - return d - for line in last_paragraph.split('\n'): - key, value = line.split(':', 1) - d[key.strip()] = value.strip() + d = {} + cmd = ['git', 'interpret-trailers', '--parse'] + proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore + trailer: str = proc.communicate(str(self.message).encode())[0].decode() + if trailer.endswith('\n'): + trailer = trailer[0:-1] + if trailer != '': + for line in trailer.split('\n'): + key, value = line.split(':', 1) + d[key.strip()] = value.strip() return d @ classmethod diff --git a/test/test_commit.py b/test/test_commit.py index 5aeef2e6c..40cf7dd26 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -435,14 +435,14 @@ def test_trailers(self): KEY_1 = "Hello" VALUE_1 = "World" KEY_2 = "Key" - VALUE_2 = "Value" + VALUE_2 = "Value with inner spaces" # Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations msgs = [] msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") msgs.append(f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") - msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2} : {VALUE_2}\n") for msg in msgs: commit = self.rorepo.commit('master') From 3ef81e182fcd3fca3f83216cf81d92d08c19cf5e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 09:57:33 +0800 Subject: [PATCH 0073/1225] Revert "Use NUL character to extract meta and path from git diff" This reverts commit 01f09888208341876d1480bd22dc8f4107c100f1. --- git/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index c8c57685b..cea66d7ee 100644 --- a/git/diff.py +++ b/git/diff.py @@ -509,9 +509,9 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: Union['Popen', 'Git.AutoIn def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None: lines = lines_bytes.decode(defenc) - it = iter(lines.split('\x00')) - for meta, path in zip(it, it): - meta = meta[1:] + for line in lines.split(':')[1:]: + meta, _, path = line.partition('\x00') + path = path.rstrip('\x00') a_blob_id: Optional[str] b_blob_id: Optional[str] old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) From b94cc253fe9e6355881eb299cfae8eea1a57a9c2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 10:08:16 +0800 Subject: [PATCH 0074/1225] prep version bump --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 05f5ca23b..199eda56a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.24 +3.1.25 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 4186ac911..d955aebea 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.25 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/55?closed=1 + 3.1.24 ====== From 53d22bbc14ed871991ef169b59770a4c5b3caa19 Mon Sep 17 00:00:00 2001 From: Takuya Kitazawa Date: Sun, 9 Jan 2022 09:37:29 -0800 Subject: [PATCH 0075/1225] Fix doc string error in Objects.Commit --- git/objects/commit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index bbd485da8..07355e7e6 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -99,8 +99,7 @@ def __init__(self, repo: 'Repo', binsha: bytes, tree: Union[Tree, None] = None, :param binsha: 20 byte sha1 :param parents: tuple( Commit, ... ) is a tuple of commit ids or actual Commits - :param tree: Tree - Tree object + :param tree: Tree object :param author: Actor is the author Actor object :param authored_date: int_seconds_since_epoch @@ -341,6 +340,7 @@ def trailers(self) -> Dict: key2 : value 2 with inner spaces dictionary will look like this: + .. code-block:: { From e16a0040d07afa4ac9c0548aa742ec18ec1395a8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:01:21 +0800 Subject: [PATCH 0076/1225] Assure index file descriptor is closed after reader (#1394) (#1395) A regression that was introduced with d79d20d. --- git/index/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git/index/base.py b/git/index/base.py index d1f039cd9..7cb77f15b 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -135,7 +135,10 @@ def _set_cache_(self, attr: str) -> None: return None # END exception handling - stream = file_contents_ro(fd, stream=True, allow_mmap=True) + try: + stream = file_contents_ro(fd, stream=True, allow_mmap=True) + finally: + os.close(fd) self._deserialize(stream) else: From 851beabc93319d8dd05bff211b13d2b35ef097e0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:10:34 +0800 Subject: [PATCH 0077/1225] bump patch level --- VERSION | 2 +- doc/source/changes.rst | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 199eda56a..265be6386 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.25 +3.1.26 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index d955aebea..82106ee4f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -1,6 +1,16 @@ ========= Changelog -========= + +3.1.26 +====== + +- Fixes a leaked file descriptor when reading the index, which would cause make writing a previously + read index on windows impossible. + See https://github.com/gitpython-developers/GitPython/issues/1395 for details. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/56?closed=1 + 3.1.25 ====== From 35e302da2d9cfa8004414c2b325d194e7d77d9d9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:14:51 +0800 Subject: [PATCH 0078/1225] fix documentation --- doc/source/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 82106ee4f..35fa4ced2 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -1,5 +1,6 @@ ========= Changelog +========= 3.1.26 ====== From e24f9b70209eb6681f055596846033f7d3215ea5 Mon Sep 17 00:00:00 2001 From: wonder-mice Date: Tue, 11 Jan 2022 00:41:03 -0800 Subject: [PATCH 0079/1225] import unittest adds 0.250s to script launch time This should not be imported at root level, since it adds a lot of initialization overhead without need. --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index b81332ea4..6e6f09557 100644 --- a/git/util.py +++ b/git/util.py @@ -20,7 +20,6 @@ import stat from sys import maxsize import time -from unittest import SkipTest from urllib.parse import urlsplit, urlunsplit import warnings @@ -130,6 +129,7 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None: func(path) # Will scream if still not possible to delete. except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex raise From 67d0631e54c44f4523d3b308040e6a0643b6396d Mon Sep 17 00:00:00 2001 From: wonder-mice Date: Tue, 11 Jan 2022 00:44:25 -0800 Subject: [PATCH 0080/1225] import unittest adds 0.250s to script launch time This should not be imported at root level, since it adds a lot of initialization overhead without need. --- git/objects/submodule/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index d306c91d4..f78204555 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,8 +3,6 @@ import logging import os import stat - -from unittest import SkipTest import uuid import git @@ -934,6 +932,7 @@ def remove(self, module: bool = True, force: bool = False, rmtree(str(wtd)) except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex raise # END delete tree if possible @@ -945,6 +944,7 @@ def remove(self, module: bool = True, force: bool = False, rmtree(git_dir) except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex else: raise From fac603789d66c0fd7c26e75debb41b06136c5026 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 12 Jan 2022 08:25:26 +0800 Subject: [PATCH 0081/1225] keep track of upcoming changes --- doc/source/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 35fa4ced2..ced8f8584 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ========= +3.1.27 +====== + +- Reduced startup time due to optimized imports. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/57?closed=1 + 3.1.26 ====== From b719e1809c2c81283e930086faebd7d6050cd5d7 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 12 Jan 2022 23:39:10 -0800 Subject: [PATCH 0082/1225] Use bash to open extensionless hooks on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #971. Partly resolve #703. If the hook doesn't have a file extension, then Windows won't know how to run it and you'll get "[WinError 193] %1 is not a valid Win32 application". It's very likely that it's a shell script of some kind, so use bash.exe (commonly installed via Windows Subsystem for Linux). We don't want to run all hooks with bash because they could be .bat files. Update tests to get several hook ones working. More work necessary to get commit-msg hook working. The hook writes to the wrong file because it's not using forward slashes in the path: C:\Users\idbrii\AppData\Local\Temp\bare_test_commit_msg_hook_successy5fo00du\CUsersidbriiAppDataLocalTempbare_test_commit_msg_hook_successy5fo00duCOMMIT_EDITMSG --- git/index/fun.py | 15 ++++++++++++++- test/test_index.py | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index 16ec744e2..59fa1be19 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -3,6 +3,7 @@ # NOTE: Autodoc hates it if this is a docstring from io import BytesIO +from pathlib import Path import os from stat import ( S_IFDIR, @@ -21,6 +22,7 @@ force_text, force_bytes, is_posix, + is_win, safe_decode, ) from git.exc import ( @@ -76,6 +78,10 @@ def hook_path(name: str, git_dir: PathLike) -> str: return osp.join(git_dir, 'hooks', name) +def _has_file_extension(path): + return osp.splitext(path)[1] + + def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: """Run the commit hook of the given name. Silently ignores hooks that do not exist. :param name: name of hook, like 'pre-commit' @@ -89,8 +95,15 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: env = os.environ.copy() env['GIT_INDEX_FILE'] = safe_decode(str(index.path)) env['GIT_EDITOR'] = ':' + cmd = [hp] try: - cmd = subprocess.Popen([hp] + list(args), + if is_win and not _has_file_extension(hp): + # Windows only uses extensions to determine how to open files + # (doesn't understand shebangs). Try using bash to run the hook. + relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix() + cmd = ["bash.exe", relative_hp] + + cmd = subprocess.Popen(cmd + list(args), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/test/test_index.py b/test/test_index.py index 02cb4e813..233a4c643 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -13,6 +13,7 @@ ) import tempfile from unittest import skipIf +import shutil from git import ( IndexFile, @@ -52,8 +53,9 @@ HOOKS_SHEBANG = "#!/usr/bin/env sh\n" +is_win_without_bash = is_win and not shutil.which('bash.exe') + -@skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703") def _make_hook(git_dir, name, content, make_exec=True): """A helper to create a hook""" hp = hook_path(name, git_dir) @@ -881,7 +883,7 @@ def test_pre_commit_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win: + if is_win_without_bash: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, '') @@ -896,6 +898,7 @@ def test_pre_commit_hook_fail(self, rw_repo): else: raise AssertionError("Should have caught a HookExecutionError") + @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703") @with_rw_repo('HEAD', bare=True) def test_commit_msg_hook_success(self, rw_repo): commit_message = "commit default head by Frèderic Çaufl€" @@ -920,7 +923,7 @@ def test_commit_msg_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win: + if is_win_without_bash: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, '') From b3f873a1458223c075fdde6c85eb656648bcdcae Mon Sep 17 00:00:00 2001 From: smokephil Date: Fri, 21 Jan 2022 09:43:40 +0100 Subject: [PATCH 0083/1225] set unassigned stdin to improve pyinstaller compatibility To create a window application with pyinstaller, all suprocess input and output streams must be assigned and must not be None. https://stackoverflow.com/a/51706087/7076612 --- git/cmd.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 8fb10742f..4f0569879 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -12,7 +12,8 @@ from subprocess import ( call, Popen, - PIPE + PIPE, + DEVNULL ) import subprocess import threading @@ -873,7 +874,7 @@ def execute(self, env=env, cwd=cwd, bufsize=-1, - stdin=istream, + stdin=istream or DEVNULL, stderr=PIPE, stdout=stdout_sink, shell=shell is not None and shell or self.USE_SHELL, From cd29f07b2efda24bdc690626ed557590289d11a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 3 Feb 2022 15:34:10 +0800 Subject: [PATCH 0084/1225] Let index.commit refer to correct method for parameter information (#1407) --- git/index/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/index/base.py b/git/index/base.py index 7cb77f15b..209bfa8de 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -974,7 +974,7 @@ def commit(self, commit_date: Union[str, None] = None, skip_hooks: bool = False) -> Commit: """Commit the current default index file, creating a commit object. - For more information on the arguments, see tree.commit. + For more information on the arguments, see Commit.create_from_tree(). :note: If you have manually altered the .entries member of this instance, don't forget to write() your changes to disk beforehand. From d0b48f3f4888d69a7b59024114bff897f24561b2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 12 Feb 2022 11:55:57 +0800 Subject: [PATCH 0085/1225] Create SECURITY.md --- SECURITY.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cf25c09ea --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +Only the latest version of GitPython can receive security updates. If a vulnerability is discovered, a fix can be issued in a new release, while older releases +are likely to be yanked. + +| Version | Supported | +| ------- | ------------------ | +| 3.x.x | :white_check_mark: | +| < 3.0 | :x: | + +## Reporting a Vulnerability + +Please report private portions of a vulnerability to sebastian.thiel@icloud.com that would help to reproduce and fix it. To receive updates on progress and provide +general information to the public, you can create an issue [on the issue tracker](https://github.com/gitpython-developers/GitPython/issues). From 75f4f63ab3856a552f06082aabf98845b5fa21e3 Mon Sep 17 00:00:00 2001 From: theworstcomrade <4lbercik@gmail.com> Date: Fri, 18 Feb 2022 16:28:03 +0100 Subject: [PATCH 0086/1225] Low risk ReDoS vuln https://huntr.dev/bounties/8549d81f-dc45-4af7-9f2a-2d70752d8524/ --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 7d5918a5a..56f3c5b33 100644 --- a/git/remote.py +++ b/git/remote.py @@ -273,7 +273,7 @@ class FetchInfo(IterableObj, object): NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ FAST_FORWARD, ERROR = [1 << x for x in range(8)] - _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') + _re_fetch_result = re.compile(r'^\s*(.) (\[[\w\s\.$@]+\]|[\w\.$@]+)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') _flag_map: Dict[flagKeyLiteral, int] = { '!': ERROR, From 65346820b81e0de7f32369ba5773004df082b793 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 09:12:39 +0800 Subject: [PATCH 0087/1225] update changelog --- doc/source/changes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index ced8f8584..f9717438d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,15 @@ Changelog ========= +3.1.28 +====== + +- Fix a vulenerability that could cause great slowdowns when encountering long remote path names + when pulling/fetching. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/58?closed=1 + 3.1.27 ====== From d438e088278f2df10b3c38bd635d7207cb7548a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 09:14:20 +0800 Subject: [PATCH 0088/1225] bump patch level --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 265be6386..054a6481f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.26 +3.1.27 From 02b594ecdb3ba36e8477e2ff1dcb065c8626ca3d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 22:01:27 +0800 Subject: [PATCH 0089/1225] fix changelog --- doc/source/changes.rst | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index f9717438d..3f22a4866 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,19 +2,12 @@ Changelog ========= -3.1.28 -====== - -- Fix a vulenerability that could cause great slowdowns when encountering long remote path names - when pulling/fetching. - -See the following for all changes. -https://github.com/gitpython-developers/gitpython/milestone/58?closed=1 - 3.1.27 ====== - Reduced startup time due to optimized imports. +- Fix a vulenerability that could cause great slowdowns when encountering long remote path names + when pulling/fetching. See the following for all changes. https://github.com/gitpython-developers/gitpython/milestone/57?closed=1 From c0f2cf373e8296d07b3a7d7610add0cf3d5957be Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 21 Feb 2022 10:42:25 +0800 Subject: [PATCH 0090/1225] Deprecate GPG signature docs; stop signing releases Related to https://github.com/gitpython-developers/gitdb/issues/77 --- Makefile | 2 +- README.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fe82a694b..9054de2b8 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ release: clean force_release: clean git push --tags origin main python3 setup.py sdist bdist_wheel - twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* \ No newline at end of file + twine upload 27C50E7F590947D7273A741E85194C08421980C9 dist/* \ No newline at end of file diff --git a/README.md b/README.md index dd449d32f..54a735e53 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,12 @@ Please have a look at the [contributions file][contributing]. incrementing the patch level, and possibly by appending `-dev`. Probably you want to `git push` once more. -### How to verify a release +### How to verify a release (DEPRECATED) + +Note that what follows is deprecated and future releases won't be signed anymore. +More details about how it came to that can be found [in this issue](https://github.com/gitpython-developers/gitdb/issues/77). + +---- Please only use releases from `pypi` as you can verify the respective source tarballs. From 56f18ac6d9efc12d0aa9406a0b28c82fcf73aca5 Mon Sep 17 00:00:00 2001 From: Houssam Kherraz Date: Wed, 23 Feb 2022 10:20:19 -0500 Subject: [PATCH 0091/1225] fix iter_commits comment, more in line with iter_items --- git/repo/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index 7713c9152..510eb12bf 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -567,8 +567,8 @@ def iter_commits(self, rev: Union[str, Commit, 'SymbolicReference', None] = None If None, the active branch will be used. :param paths: - is an optional path or a list of paths to limit the returned commits to - Commits that do not contain that path or the paths will not be returned. + is an optional path or a list of paths; if set only commits that include the path + or paths will be returned :param kwargs: Arguments to be passed to git-rev-list - common ones are From c0740570b31f0f0fe499bf4fc5abbf89feb1757d Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 23 Feb 2022 17:03:30 -0500 Subject: [PATCH 0092/1225] fix typos --- doc/source/tutorial.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index bc386e7c4..fcbc18bff 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -66,7 +66,7 @@ Archive the repository contents to a tar file. Advanced Repo Usage =================== -And of course, there is much more you can do with this type, most of the following will be explained in greater detail in specific tutorials. Don't worry if you don't understand some of these examples right away, as they may require a thorough understanding of gits inner workings. +And of course, there is much more you can do with this type, most of the following will be explained in greater detail in specific tutorials. Don't worry if you don't understand some of these examples right away, as they may require a thorough understanding of git's inner workings. Query relevant repository paths ... @@ -363,7 +363,7 @@ Handling Remotes :start-after: # [25-test_references_and_objects] :end-before: # ![25-test_references_and_objects] -You can easily access configuration information for a remote by accessing options as if they where attributes. The modification of remote configuration is more explicit though. +You can easily access configuration information for a remote by accessing options as if they were attributes. The modification of remote configuration is more explicit though. .. literalinclude:: ../../test/test_docs.py :language: python @@ -391,7 +391,7 @@ Here's an example executable that can be used in place of the `ssh_executable` a ID_RSA=/var/lib/openshift/5562b947ecdd5ce939000038/app-deployments/id_rsa exec /usr/bin/ssh -o StrictHostKeyChecking=no -i $ID_RSA "$@" -Please note that the script must be executable (i.e. `chomd +x script.sh`). `StrictHostKeyChecking=no` is used to avoid prompts asking to save the hosts key to `~/.ssh/known_hosts`, which happens in case you run this as daemon. +Please note that the script must be executable (i.e. `chmod +x script.sh`). `StrictHostKeyChecking=no` is used to avoid prompts asking to save the hosts key to `~/.ssh/known_hosts`, which happens in case you run this as daemon. You might also have a look at `Git.update_environment(...)` in case you want to setup a changed environment more permanently. @@ -509,14 +509,14 @@ The type of the database determines certain performance characteristics, such as GitDB ===== -The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. Its uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities small of objects from densely packed repositories:: +The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. It uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities of small objects from densely packed repositories:: repo = Repo("path/to/repo", odbt=GitDB) GitCmdObjectDB ============== -The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than the one of the ``GitDB``:: +The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than ``GitDB``:: repo = Repo("path/to/repo", odbt=GitCmdObjectDB) From b85c2594f31179e135af893d82868e7742464fe6 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 21 Mar 2022 19:19:32 +0300 Subject: [PATCH 0093/1225] Fixed setting ref with non-ascii in path --- git/refs/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 0c0fa4045..1c5506737 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -352,7 +352,7 @@ def set_reference(self, ref: Union[Commit_ish, 'SymbolicReference', str], fd = lfd.open(write=True, stream=True) ok = True try: - fd.write(write_value.encode('ascii') + b'\n') + fd.write(write_value.encode('utf-8') + b'\n') lfd.commit() ok = True finally: From 0b33576f8e7add5671f8927dff228e7f92eec076 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 1 Apr 2022 15:28:02 +0100 Subject: [PATCH 0094/1225] Allow `repo.create_head`'s `commit` arg to be a `SymbolicReference` This matches the signature from `Head.create`. --- git/repo/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 510eb12bf..f8bc8128e 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -420,7 +420,8 @@ def _to_full_tag_path(path: PathLike) -> str: else: return TagReference._common_path_default + '/' + path_str - def create_head(self, path: PathLike, commit: str = 'HEAD', + def create_head(self, path: PathLike, + commit: Union['SymbolicReference', 'str'] = 'HEAD', force: bool = False, logmsg: Optional[str] = None ) -> 'Head': """Create a new head within the repository. From e4360aea32aad11bf3c54b0dc0a6cabb21b5e687 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Wed, 6 Apr 2022 23:08:36 +0900 Subject: [PATCH 0095/1225] feat(cmd): add the `strip_newline` flag This commit adds the `strip_newline` flag to the `Git.execute` method. When this flag is set to `True`, it will trim the trailing `\n`. The default value is `True` for backward compatibility. Setting it to `False` is helpful for, e.g., the `git show` output, especially with the binary file, as the missing `\n` may invalidate the file. --- git/cmd.py | 7 +++++-- test/test_repo.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 4f0569879..9c5da89dc 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -55,7 +55,7 @@ execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size'} + 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline'} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -738,6 +738,7 @@ def execute(self, shell: Union[None, bool] = None, env: Union[None, Mapping[str, str]] = None, max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + strip_newline: bool = True, **subprocess_kwargs: Any ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns @@ -810,6 +811,8 @@ def execute(self, effects on a repository. For example, stale locks in case of git gc could render the repository incapable of accepting changes until the lock is manually removed. + :param strip_newline: + Whether to strip the trailing '\n' of the command output. :return: * str(output) if extended_output = False (Default) @@ -944,7 +947,7 @@ def _kill_process(pid: int) -> None: if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" - if stdout_value.endswith(newline): # type: ignore + if stdout_value.endswith(newline) and strip_newline: # type: ignore stdout_value = stdout_value[:-1] if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] diff --git a/test/test_repo.py b/test/test_repo.py index 6d6176090..14339f57f 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1098,3 +1098,13 @@ def test_rebasing(self, rw_dir): except GitCommandError: pass self.assertEqual(r.currently_rebasing_on(), commitSpanish) + + @with_rw_directory + def test_do_not_strip_newline(self, rw_dir): + r = Repo.init(rw_dir) + fp = osp.join(rw_dir, 'hello.txt') + with open(fp, 'w') as fs: + fs.write("hello\n") + r.git.add(Git.polish_url(fp)) + r.git.commit(message="init") + self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline=False), 'hello\n') From 946b64b62bdc9fb3447b6daf0053b11a2e4c5277 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Wed, 6 Apr 2022 23:23:42 +0900 Subject: [PATCH 0096/1225] chore: add me to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 55d681813..546818f5f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,4 +45,5 @@ Contributors are: -Alba Mendez -Robert Westman -Hugo van Kemenade +-Hiroki Tokunaga Portions derived from other open source works and are clearly marked. From 49150e79c6f7a19a0d61a5ea6864b9ac140264ff Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:04:19 +0900 Subject: [PATCH 0097/1225] chore: `s/strip_newline/&_in_stdout` --- git/cmd.py | 10 +++++----- test/test_repo.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 9c5da89dc..228b9d382 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -55,7 +55,7 @@ execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline'} + 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline_in_stdout'} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -738,7 +738,7 @@ def execute(self, shell: Union[None, bool] = None, env: Union[None, Mapping[str, str]] = None, max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, - strip_newline: bool = True, + strip_newline_in_stdout: bool = True, **subprocess_kwargs: Any ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns @@ -811,8 +811,8 @@ def execute(self, effects on a repository. For example, stale locks in case of git gc could render the repository incapable of accepting changes until the lock is manually removed. - :param strip_newline: - Whether to strip the trailing '\n' of the command output. + :param strip_newline_in_stdout: + Whether to strip the trailing '\n' of the command stdout. :return: * str(output) if extended_output = False (Default) @@ -947,7 +947,7 @@ def _kill_process(pid: int) -> None: if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" - if stdout_value.endswith(newline) and strip_newline: # type: ignore + if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore stdout_value = stdout_value[:-1] if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] diff --git a/test/test_repo.py b/test/test_repo.py index 14339f57f..c5b2680d0 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1100,11 +1100,11 @@ def test_rebasing(self, rw_dir): self.assertEqual(r.currently_rebasing_on(), commitSpanish) @with_rw_directory - def test_do_not_strip_newline(self, rw_dir): + def test_do_not_strip_newline_in_stdout(self, rw_dir): r = Repo.init(rw_dir) fp = osp.join(rw_dir, 'hello.txt') with open(fp, 'w') as fs: fs.write("hello\n") r.git.add(Git.polish_url(fp)) r.git.commit(message="init") - self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline=False), 'hello\n') + self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), 'hello\n') From 2a50f28fa3571e3d2c4d5ea86f4243f715717269 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:11:28 +0900 Subject: [PATCH 0098/1225] docs: escape with backticks --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 228b9d382..fe161309b 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -812,7 +812,7 @@ def execute(self, render the repository incapable of accepting changes until the lock is manually removed. :param strip_newline_in_stdout: - Whether to strip the trailing '\n' of the command stdout. + Whether to strip the trailing `\n` of the command stdout. :return: * str(output) if extended_output = False (Default) From 17b2b128fb6d6f987b47d60ccb1ab09b8fc238ea Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:20:59 +0900 Subject: [PATCH 0099/1225] fix(docs): remove an unexpected blank line --- git/cmd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index fe161309b..1ddf9e03f 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -813,7 +813,6 @@ def execute(self, removed. :param strip_newline_in_stdout: Whether to strip the trailing `\n` of the command stdout. - :return: * str(output) if extended_output = False (Default) * tuple(int(status), str(stdout), str(stderr)) if extended_output = True From 85fe2735b7c9119804813bcbbdd8d14018291ed3 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 4 May 2022 12:48:09 -0400 Subject: [PATCH 0100/1225] Fix #1284: strip usernames from URLs as well as passwords --- git/exc.py | 7 ++++--- git/util.py | 20 +++++++++++++------- test/test_exc.py | 9 ++++++--- test/test_util.py | 30 +++++++++++++++++++++++------- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/git/exc.py b/git/exc.py index e8ff784c7..045ea9d27 100644 --- a/git/exc.py +++ b/git/exc.py @@ -8,6 +8,7 @@ from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode +from git.util import remove_password_if_present # typing ---------------------------------------------------- @@ -54,7 +55,7 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], stdout: Union[bytes, str, None] = None) -> None: if not isinstance(command, (tuple, list)): command = command.split() - self.command = command + self.command = remove_password_if_present(command) self.status = status if status: if isinstance(status, Exception): @@ -66,8 +67,8 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], s = safe_decode(str(status)) status = "'%s'" % s if isinstance(status, str) else s - self._cmd = safe_decode(command[0]) - self._cmdline = ' '.join(safe_decode(i) for i in command) + self._cmd = safe_decode(self.command[0]) + self._cmdline = ' '.join(safe_decode(i) for i in self.command) self._cause = status and " due to: %s" % status or "!" stdout_decode = safe_decode(stdout) stderr_decode = safe_decode(stderr) diff --git a/git/util.py b/git/util.py index 6e6f09557..0711265a6 100644 --- a/git/util.py +++ b/git/util.py @@ -5,7 +5,6 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php from abc import abstractmethod -from .exc import InvalidGitRepositoryError import os.path as osp from .compat import is_win import contextlib @@ -94,6 +93,8 @@ def unbare_repo(func: Callable[..., T]) -> Callable[..., T]: """Methods with this decorator raise InvalidGitRepositoryError if they encounter a bare repository""" + from .exc import InvalidGitRepositoryError + @wraps(func) def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> T: if self.repo.bare: @@ -412,11 +413,12 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: """ Parse any command line argument and if on of the element is an URL with a - password, replace it by stars (in-place). + username and/or password, replace them by stars (in-place). If nothing found just returns the command line as-is. - This should be used for every log line that print a command line. + This should be used for every log line that print a command line, as well as + exception messages. """ new_cmdline = [] for index, to_parse in enumerate(cmdline): @@ -424,12 +426,16 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: try: url = urlsplit(to_parse) # Remove password from the URL if present - if url.password is None: + if url.password is None and url.username is None: continue - edited_url = url._replace( - netloc=url.netloc.replace(url.password, "*****")) - new_cmdline[index] = urlunsplit(edited_url) + if url.password is not None: + url = url._replace( + netloc=url.netloc.replace(url.password, "*****")) + if url.username is not None: + url = url._replace( + netloc=url.netloc.replace(url.username, "*****")) + new_cmdline[index] = urlunsplit(url) except ValueError: # This is not a valid URL continue diff --git a/test/test_exc.py b/test/test_exc.py index f16498ab5..c77be7824 100644 --- a/test/test_exc.py +++ b/test/test_exc.py @@ -22,6 +22,7 @@ HookExecutionError, RepositoryDirtyError, ) +from git.util import remove_password_if_present from test.lib import TestBase import itertools as itt @@ -34,6 +35,7 @@ ('cmd', 'ελληνικα', 'args'), ('θνιψοδε', 'κι', 'αλλα', 'strange', 'args'), ('θνιψοδε', 'κι', 'αλλα', 'non-unicode', 'args'), + ('git', 'clone', '-v', 'https://fakeuser:fakepassword1234@fakerepo.example.com/testrepo'), ) _causes_n_substrings = ( (None, None), # noqa: E241 @IgnorePep8 @@ -81,7 +83,7 @@ def test_CommandError_unicode(self, case): self.assertIsNotNone(c._msg) self.assertIn(' cmdline: ', s) - for a in argv: + for a in remove_password_if_present(argv): self.assertIn(a, s) if not cause: @@ -137,14 +139,15 @@ def test_GitCommandNotFound(self, init_args): @ddt.data( (['cmd1'], None), (['cmd1'], "some cause"), - (['cmd1'], Exception()), + (['cmd1', 'https://fakeuser@fakerepo.example.com/testrepo'], Exception()), ) def test_GitCommandError(self, init_args): argv, cause = init_args c = GitCommandError(argv, cause) s = str(c) - self.assertIn(argv[0], s) + for arg in remove_password_if_present(argv): + self.assertIn(arg, s) if cause: self.assertIn(' failed due to: ', s) self.assertIn(str(cause), s) diff --git a/test/test_util.py b/test/test_util.py index 3961ff356..a213b46c9 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -343,18 +343,34 @@ def test_pickle_tzoffset(self): self.assertEqual(t1._name, t2._name) def test_remove_password_from_command_line(self): + username = "fakeuser" password = "fakepassword1234" - url_with_pass = "https://fakeuser:{}@fakerepo.example.com/testrepo".format(password) - url_without_pass = "https://fakerepo.example.com/testrepo" + url_with_user_and_pass = "https://{}:{}@fakerepo.example.com/testrepo".format(username, password) + url_with_user = "https://{}@fakerepo.example.com/testrepo".format(username) + url_with_pass = "https://:{}@fakerepo.example.com/testrepo".format(password) + url_without_user_or_pass = "https://fakerepo.example.com/testrepo" - cmd_1 = ["git", "clone", "-v", url_with_pass] - cmd_2 = ["git", "clone", "-v", url_without_pass] - cmd_3 = ["no", "url", "in", "this", "one"] + cmd_1 = ["git", "clone", "-v", url_with_user_and_pass] + cmd_2 = ["git", "clone", "-v", url_with_user] + cmd_3 = ["git", "clone", "-v", url_with_pass] + cmd_4 = ["git", "clone", "-v", url_without_user_or_pass] + cmd_5 = ["no", "url", "in", "this", "one"] redacted_cmd_1 = remove_password_if_present(cmd_1) + assert username not in " ".join(redacted_cmd_1) assert password not in " ".join(redacted_cmd_1) # Check that we use a copy assert cmd_1 is not redacted_cmd_1 + assert username in " ".join(cmd_1) assert password in " ".join(cmd_1) - assert cmd_2 == remove_password_if_present(cmd_2) - assert cmd_3 == remove_password_if_present(cmd_3) + + redacted_cmd_2 = remove_password_if_present(cmd_2) + assert username not in " ".join(redacted_cmd_2) + assert password not in " ".join(redacted_cmd_2) + + redacted_cmd_3 = remove_password_if_present(cmd_3) + assert username not in " ".join(redacted_cmd_3) + assert password not in " ".join(redacted_cmd_3) + + assert cmd_4 == remove_password_if_present(cmd_4) + assert cmd_5 == remove_password_if_present(cmd_5) From dde3a8bd9229ff25ec8bc03c35d937f43233f48e Mon Sep 17 00:00:00 2001 From: luz paz Date: Sat, 7 May 2022 15:59:10 -0400 Subject: [PATCH 0101/1225] Fix various typos Found via `codespell -q 3 -S ./git/ext/gitdb,./test/fixtures/reflog_master,./test/fixtures/diff_mode_only,./test/fixtures/reflog_HEAD` --- doc/source/changes.rst | 6 +++--- git/config.py | 2 +- git/index/base.py | 8 ++++---- git/index/fun.py | 2 +- git/objects/base.py | 2 +- git/objects/commit.py | 4 ++-- git/objects/submodule/root.py | 2 +- git/objects/util.py | 4 ++-- git/refs/symbolic.py | 2 +- git/refs/tag.py | 4 ++-- git/repo/base.py | 4 ++-- git/repo/fun.py | 2 +- git/types.py | 2 +- pyproject.toml | 2 +- test/fixtures/diff_p | 2 +- test/fixtures/git_config | 2 +- test/fixtures/rev_list_bisect_all | 2 +- test/test_config.py | 2 +- test/test_diff.py | 2 +- test/test_docs.py | 2 +- test/test_git.py | 2 +- test/test_index.py | 2 +- test/test_submodule.py | 4 ++-- 23 files changed, 33 insertions(+), 33 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 3f22a4866..f37c81677 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -69,7 +69,7 @@ https://github.com/gitpython-developers/gitpython/milestone/53?closed=1 - Make Protocol classes ABCs at runtime due to new behaviour/bug in 3.9.7 & 3.10.0-rc1 - - Remove use of typing.TypeGuard until later release, to allow dependant libs time to update. + - Remove use of typing.TypeGuard until later release, to allow dependent libs time to update. - Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 @@ -134,7 +134,7 @@ https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 3.1.15 (YANKED) =============== -* add deprectation warning for python 3.5 +* add deprecation warning for python 3.5 See the following for details: https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 @@ -595,7 +595,7 @@ It follows the `semantic version scheme `_, and thus will not - Renamed `ignore_tree_extension_data` keyword argument in `IndexFile.write(...)` to `ignore_extension_data` * If the git command executed during `Remote.push(...)|fetch(...)` returns with an non-zero exit code and GitPython didn't obtain any head-information, the corresponding `GitCommandError` will be raised. This may break previous code which expected - these operations to never raise. However, that behavious is undesirable as it would effectively hide the fact that there + these operations to never raise. However, that behaviour is undesirable as it would effectively hide the fact that there was an error. See `this issue `__ for more information. * If the git executable can't be found in the PATH or at the path provided by `GIT_PYTHON_GIT_EXECUTABLE`, this is made diff --git a/git/config.py b/git/config.py index cbd66022d..1ac3c9cec 100644 --- a/git/config.py +++ b/git/config.py @@ -71,7 +71,7 @@ class MetaParserBuilder(abc.ABCMeta): - """Utlity class wrapping base-class methods into decorators that assure read-only properties""" + """Utility class wrapping base-class methods into decorators that assure read-only properties""" def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': """ Equip all base-class methods with a needs_values decorator, and all non-const methods diff --git a/git/index/base.py b/git/index/base.py index 209bfa8de..00e51bf5e 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -579,7 +579,7 @@ def _process_diff_args(self, # type: ignore[override] def _to_relative_path(self, path: PathLike) -> PathLike: """ :return: Version of path relative to our git directory or raise ValueError - if it is not within our git direcotory""" + if it is not within our git directory""" if not osp.isabs(path): return path if self.repo.bare: @@ -682,7 +682,7 @@ def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule'] into the object database. PathStrings may contain globs, such as 'lib/__init__*' or can be directories - like 'lib', the latter ones will add all the files within the dirctory and + like 'lib', the latter ones will add all the files within the directory and subdirectories. This equals a straight git-add. @@ -779,7 +779,7 @@ def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule'] "At least one Entry has a null-mode - please use index.remove to remove files for clarity") # END null mode should be remove - # HANLDE ENTRY OBJECT CREATION + # HANDLE ENTRY OBJECT CREATION # create objects if required, otherwise go with the existing shas null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA] if null_entries_indices: @@ -813,7 +813,7 @@ def handle_null_entries(self: 'IndexFile') -> None: fprogress(entry.path, False, entry) fprogress(entry.path, True, entry) # END handle progress - # END for each enty + # END for each entry entries_added.extend(entries) # END if there are base entries diff --git a/git/index/fun.py b/git/index/fun.py index 59fa1be19..acab74239 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -314,7 +314,7 @@ def write_tree_from_cache(entries: List[IndexEntry], odb: 'GitCmdObjectDB', sl: # finally create the tree sio = BytesIO() - tree_to_stream(tree_items, sio.write) # writes to stream as bytes, but doesnt change tree_items + tree_to_stream(tree_items, sio.write) # writes to stream as bytes, but doesn't change tree_items sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) diff --git a/git/objects/base.py b/git/objects/base.py index a3b0f230a..66e15a8f5 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -32,7 +32,7 @@ # -------------------------------------------------------------------------- -_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutual git object type %r" +_assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r" __all__ = ("Object", "IndexObject") diff --git a/git/objects/commit.py b/git/objects/commit.py index 07355e7e6..96a2a8e59 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -322,7 +322,7 @@ def trailers(self) -> Dict: Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). - This funcions calls ``git interpret-trailers --parse`` onto the message + This functions calls ``git interpret-trailers --parse`` onto the message to extract the trailer information. The key value pairs are stripped of leading and trailing whitespaces before they get saved into a dictionary. @@ -461,7 +461,7 @@ def create_from_tree(cls, repo: 'Repo', tree: Union[Tree, str], message: str, # * Environment variables override configuration values # * Sensible defaults are set according to the git documentation - # COMMITER AND AUTHOR INFO + # COMMITTER AND AUTHOR INFO cr = repo.config_reader() env = os.environ diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py index 5e84d1616..08e1f9543 100644 --- a/git/objects/submodule/root.py +++ b/git/objects/submodule/root.py @@ -338,7 +338,7 @@ def update(self, previous_commit: Union[Commit_ish, None] = None, sm.update(recursive=False, init=init, to_latest_revision=to_latest_revision, progress=progress, dry_run=dry_run, force=force_reset, keep_going=keep_going) - # update recursively depth first - question is which inconsitent + # update recursively depth first - question is which inconsistent # state will be better in case it fails somewhere. Defective branch # or defective depth. The RootSubmodule type will never process itself, # which was done in the previous expression diff --git a/git/objects/util.py b/git/objects/util.py index 187318fe6..800eccdf4 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -37,7 +37,7 @@ from .submodule.base import Submodule from git.types import Protocol, runtime_checkable else: - # Protocol = Generic[_T] # NNeeded for typing bug #572? + # Protocol = Generic[_T] # Needed for typing bug #572? Protocol = ABC def runtime_checkable(f): @@ -359,7 +359,7 @@ def _list_traverse(self, as_edge: bool = False, *args: Any, **kwargs: Any out: IterableList[Union['Commit', 'Submodule', 'Tree', 'Blob']] = IterableList(id) out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) return out - # overloads in subclasses (mypy does't allow typing self: subclass) + # overloads in subclasses (mypy doesn't allow typing self: subclass) # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]] else: # Raise deprecationwarning, doesn't make sense to use this diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 1c5506737..8d869173e 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -298,7 +298,7 @@ def set_reference(self, ref: Union[Commit_ish, 'SymbolicReference', str], logmsg: Union[str, None] = None) -> 'SymbolicReference': """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. Otherwise an Object, given as Object instance or refspec, is assumed and if valid, - will be set which effectively detaches the refererence if it was a purely + will be set which effectively detaches the reference if it was a purely symbolic one. :param ref: SymbolicReference instance, Object instance or refspec string diff --git a/git/refs/tag.py b/git/refs/tag.py index edfab33d8..8cc79eddd 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -36,7 +36,7 @@ class TagReference(Reference): _common_path_default = Reference._common_path_default + "/" + _common_default @property - def commit(self) -> 'Commit': # type: ignore[override] # LazyMixin has unrelated comit method + def commit(self) -> 'Commit': # type: ignore[override] # LazyMixin has unrelated commit method """:return: Commit object the tag ref points to :raise ValueError: if the tag points to a tree or blob""" @@ -91,7 +91,7 @@ def create(cls: Type['TagReference'], repo: 'Repo', path: PathLike, :param message: Synonym for :param logmsg: - Included for backwards compatability. :param logmsg is used in preference if both given. + Included for backwards compatibility. :param logmsg is used in preference if both given. :param force: If True, to force creation of a tag even though that tag already exists. diff --git a/git/repo/base.py b/git/repo/base.py index f8bc8128e..bea0dcb57 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -711,7 +711,7 @@ def is_dirty(self, index: bool = True, working_tree: bool = True, untracked_file index or the working copy have changes.""" if self._bare: # Bare repositories with no associated working directory are - # always consired to be clean. + # always considered to be clean. return False # start from the one which is fastest to evaluate @@ -760,7 +760,7 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: untracked_files=True, as_process=True, **kwargs) - # Untracked files preffix in porcelain mode + # Untracked files prefix in porcelain mode prefix = "?? " untracked_files = [] for line in proc.stdout: diff --git a/git/repo/fun.py b/git/repo/fun.py index 1a83dd3dc..74c0657d6 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -266,7 +266,7 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: # END handle tag elif token == '@': # try single int - assert ref is not None, "Requre Reference to access reflog" + assert ref is not None, "Require Reference to access reflog" revlog_index = None try: # transform reversed index into the format of our revlog diff --git a/git/types.py b/git/types.py index 64bf3d96d..7f44ba242 100644 --- a/git/types.py +++ b/git/types.py @@ -54,7 +54,7 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None: """For use in exhaustive checking of literal or Enum in if/else chain. - Should only be reached if all memebers not handled OR attempt to pass non-members through chain. + Should only be reached if all members not handled OR attempt to pass non-members through chain. If all members handled, type is Empty. Otherwise, will cause mypy error. If non-members given, should cause mypy error at variable creation. diff --git a/pyproject.toml b/pyproject.toml index 102b6fdc4..da3e605ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] python_files = 'test_*.py' -testpaths = 'test' # space seperated list of paths from root e.g test tests doc/testing +testpaths = 'test' # space separated list of paths from root e.g test tests doc/testing addopts = '--cov=git --cov-report=term --maxfail=10 --force-sugar --disable-warnings' filterwarnings = 'ignore::DeprecationWarning' # --cov coverage diff --git a/test/fixtures/diff_p b/test/fixtures/diff_p index af4759e50..76242b58c 100644 --- a/test/fixtures/diff_p +++ b/test/fixtures/diff_p @@ -397,7 +397,7 @@ index 1d5251d40fb65ac89184ec662a3e1b04d0c24861..98eeddda5ed2b0e215e21128112393bd self.git_dir = git_dir end -- # Converstion hash from Ruby style options to git command line +- # Conversion hash from Ruby style options to git command line - # style options - TRANSFORM = {:max_count => "--max-count=", - :skip => "--skip=", diff --git a/test/fixtures/git_config b/test/fixtures/git_config index b8c178e3f..a8cad56e8 100644 --- a/test/fixtures/git_config +++ b/test/fixtures/git_config @@ -28,7 +28,7 @@ [branch "mainline_performance"] remote = mainline merge = refs/heads/master -# section with value defined before include to be overriden +# section with value defined before include to be overridden [sec] var0 = value0_main [include] diff --git a/test/fixtures/rev_list_bisect_all b/test/fixtures/rev_list_bisect_all index 810b66093..342ea94ae 100644 --- a/test/fixtures/rev_list_bisect_all +++ b/test/fixtures/rev_list_bisect_all @@ -40,7 +40,7 @@ committer David Aguilar 1220418344 -0700 commit: handle --bisect-all output in Commit.list_from_string Rui Abreu Ferrerira pointed out that "git rev-list --bisect-all" - returns a slightly different format which we can easily accomodate + returns a slightly different format which we can easily accommodate by changing the way we parse rev-list output. http://groups.google.com/group/git-python/browse_thread/thread/aed1d5c4b31d5027 diff --git a/test/test_config.py b/test/test_config.py index 8892b8399..50d9b010d 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -175,7 +175,7 @@ def test_base(self): assert num_sections and num_options assert r_config._is_initialized is True - # get value which doesnt exist, with default + # get value which doesn't exist, with default default = "my default value" assert r_config.get_value("doesnt", "exist", default) == default diff --git a/test/test_diff.py b/test/test_diff.py index 9b20893a4..92e27f5d2 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -273,7 +273,7 @@ def test_diff_unsafe_paths(self): self.assertEqual(res[13].b_path, 'b/"with even more quotes"') def test_diff_patch_format(self): - # test all of the 'old' format diffs for completness - it should at least + # test all of the 'old' format diffs for completeness - it should at least # be able to deal with it fixtures = ("diff_2", "diff_2f", "diff_f", "diff_i", "diff_mode_only", "diff_new_mode", "diff_numstat", "diff_p", "diff_rename", diff --git a/test/test_docs.py b/test/test_docs.py index 8897bbb75..08fc84399 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -135,7 +135,7 @@ def update(self, op_code, cur_count, max_count=None, message=''): for fetch_info in origin.fetch(progress=MyProgressPrinter()): print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit)) # create a local branch at the latest fetched master. We specify the name statically, but you have all - # information to do it programatically as well. + # information to do it programmatically as well. bare_master = bare_repo.create_head('master', origin.refs.master) bare_repo.head.set_reference(bare_master) assert not bare_repo.delete_remote(origin).exists() diff --git a/test/test_git.py b/test/test_git.py index 7f52d650f..10e21487a 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -159,7 +159,7 @@ def test_cmd_override(self): prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE exc = GitCommandNotFound try: - # set it to something that doens't exist, assure it raises + # set it to something that doesn't exist, assure it raises type(self.git).GIT_PYTHON_GIT_EXECUTABLE = osp.join( "some", "path", "which", "doesn't", "exist", "gitbinary") self.assertRaises(exc, self.git.version) diff --git a/test/test_index.py b/test/test_index.py index 233a4c643..4a20a8f65 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -936,4 +936,4 @@ def test_commit_msg_hook_fail(self, rw_repo): self.assertEqual(err.stderr, "\n stderr: 'stderr\n'") assert str(err) else: - raise AssertionError("Should have cought a HookExecutionError") + raise AssertionError("Should have caught a HookExecutionError") diff --git a/test/test_submodule.py b/test/test_submodule.py index 3307bc788..a79123dcc 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -546,7 +546,7 @@ def test_root_module(self, rwrepo): assert nsm.module().head.commit.hexsha == nsm.hexsha nsm.module().index.add([nsm]) nsm.module().index.commit("added new file") - rm.update(recursive=False, dry_run=True, progress=prog) # would not change head, and thus doens't fail + rm.update(recursive=False, dry_run=True, progress=prog) # would not change head, and thus doesn't fail # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run # This would only run if our local branch is in the past and we have uncommitted changes @@ -730,7 +730,7 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): assert parent.head.commit.tree[sm.path].binsha == sm.binsha assert sm_too.binsha == sm.binsha, "cached submodule should point to the same commit as updated one" - added_bies = parent.index.add([sm]) # addded base-index-entries + added_bies = parent.index.add([sm]) # added base-index-entries assert len(added_bies) == 1 parent.index.commit("add same submodule entry") commit_sm = parent.head.commit.tree[sm.path] From 21ec529987d10e0010badd37f8da3274167d436f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 18 May 2022 07:43:53 +0800 Subject: [PATCH 0102/1225] Run everything through 'black' That way people who use it won't be deterred, while it unifies style everywhere. --- doc/source/conf.py | 92 ++--- git/__init__.py | 54 +-- git/cmd.py | 592 +++++++++++++++++++------------ git/compat.py | 37 +- git/config.py | 274 +++++++++----- git/db.py | 15 +- git/diff.py | 377 +++++++++++++------- git/exc.py | 78 ++-- git/ext/gitdb | 2 +- git/index/base.py | 479 ++++++++++++++++--------- git/index/fun.py | 185 ++++++---- git/index/typ.py | 67 ++-- git/index/util.py | 35 +- git/objects/__init__.py | 10 +- git/objects/base.py | 61 ++-- git/objects/blob.py | 7 +- git/objects/commit.py | 339 +++++++++++------- git/objects/fun.py | 75 ++-- git/objects/submodule/base.py | 536 +++++++++++++++++++--------- git/objects/submodule/root.py | 204 ++++++++--- git/objects/submodule/util.py | 40 ++- git/objects/tag.py | 50 ++- git/objects/tree.py | 158 ++++++--- git/objects/util.py | 362 ++++++++++++------- git/refs/head.py | 87 +++-- git/refs/log.py | 117 +++--- git/refs/reference.py | 58 +-- git/refs/remote.py | 21 +- git/refs/symbolic.py | 266 +++++++++----- git/refs/tag.py | 42 ++- git/remote.py | 538 ++++++++++++++++++---------- git/repo/base.py | 582 +++++++++++++++++++----------- git/repo/fun.py | 131 ++++--- git/types.py | 61 +++- git/util.py | 453 ++++++++++++++--------- setup.py | 34 +- test/lib/__init__.py | 7 +- test/lib/helper.py | 146 +++++--- test/performance/lib.py | 43 +-- test/performance/test_commit.py | 52 ++- test/performance/test_odb.py | 39 +- test/performance/test_streams.py | 87 +++-- test/test_actor.py | 1 - test/test_base.py | 55 ++- test/test_blob.py | 9 +- test/test_clone.py | 17 +- test/test_commit.py | 252 ++++++++----- test/test_config.py | 289 ++++++++------- test/test_db.py | 3 +- test/test_diff.py | 236 +++++++----- test/test_docs.py | 433 ++++++++++++++-------- test/test_exc.py | 83 +++-- test/test_fun.py | 103 +++--- test/test_git.py | 173 +++++---- test/test_index.py | 311 +++++++++------- test/test_installation.py | 61 +++- test/test_reflog.py | 37 +- test/test_refs.py | 137 ++++--- test/test_remote.py | 218 +++++++----- test/test_repo.py | 477 +++++++++++++++---------- test/test_stats.py | 24 +- test/test_submodule.py | 567 ++++++++++++++++++----------- test/test_tree.py | 38 +- test/test_util.py | 172 +++++---- test/tstrunner.py | 3 +- 65 files changed, 6674 insertions(+), 3918 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 286058fdc..d2803a826 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -20,38 +20,40 @@ # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.append(os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../..')) +# sys.path.append(os.path.abspath('.')) +sys.path.insert(0, os.path.abspath("../..")) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'GitPython' -copyright = 'Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel' +project = "GitPython" +copyright = ( + "Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel" +) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -with open(os.path.join(os.path.dirname(__file__), "..", "..", 'VERSION')) as fd: +with open(os.path.join(os.path.dirname(__file__), "..", "..", "VERSION")) as fd: VERSION = fd.readline().strip() version = VERSION # The full version, including alpha/beta/rc tags. @@ -59,61 +61,60 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['build'] +exclude_trees = ["build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # Options for HTML output # ----------------------- -html_theme = 'sphinx_rtd_theme' -html_theme_options = { -} +html_theme = "sphinx_rtd_theme" +html_theme_options = {} # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -122,72 +123,71 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. -#html_copy_source = True +# html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'gitpythondoc' +htmlhelp_basename = "gitpythondoc" # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ - ('index', 'GitPython.tex', r'GitPython Documentation', - r'Michael Trier', 'manual'), + ("index", "GitPython.tex", r"GitPython Documentation", r"Michael Trier", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True diff --git a/git/__init__.py b/git/__init__.py index ae9254a26..3f26886f7 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -4,8 +4,8 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa -#@PydevCodeAnalysisIgnore -from git.exc import * # @NoMove @IgnorePep8 +# @PydevCodeAnalysisIgnore +from git.exc import * # @NoMove @IgnorePep8 import inspect import os import sys @@ -14,14 +14,14 @@ from typing import Optional from git.types import PathLike -__version__ = 'git' +__version__ = "git" -#{ Initialization +# { Initialization def _init_externals() -> None: """Initialize external projects by putting them into the path""" - if __version__ == 'git' and 'PYOXIDIZER' not in os.environ: - sys.path.insert(1, osp.join(osp.dirname(__file__), 'ext', 'gitdb')) + if __version__ == "git" and "PYOXIDIZER" not in os.environ: + sys.path.insert(1, osp.join(osp.dirname(__file__), "ext", "gitdb")) try: import gitdb @@ -29,26 +29,27 @@ def _init_externals() -> None: raise ImportError("'gitdb' could not be found in your PYTHONPATH") from e # END verify import -#} END initialization + +# } END initialization ################# _init_externals() ################# -#{ Imports +# { Imports try: from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import * # @NoMove @IgnorePep8 - from git.refs import * # @NoMove @IgnorePep8 - from git.diff import * # @NoMove @IgnorePep8 - from git.db import * # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import * # @NoMove @IgnorePep8 - from git.index import * # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.objects import * # @NoMove @IgnorePep8 + from git.refs import * # @NoMove @IgnorePep8 + from git.diff import * # @NoMove @IgnorePep8 + from git.db import * # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # @NoMove @IgnorePep8 + from git.index import * # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, @@ -56,15 +57,18 @@ def _init_externals() -> None: rmtree, ) except GitError as exc: - raise ImportError('%s: %s' % (exc.__class__.__name__, exc)) from exc + raise ImportError("%s: %s" % (exc.__class__.__name__, exc)) from exc -#} END imports +# } END imports -__all__ = [name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj))] +__all__ = [ + name + for name, obj in locals().items() + if not (name.startswith("_") or inspect.ismodule(obj)) +] -#{ Initialize git executable path +# { Initialize git executable path GIT_OK = None @@ -79,12 +83,14 @@ def refresh(path: Optional[PathLike] = None) -> None: return GIT_OK = True -#} END initialize git executable path + + +# } END initialize git executable path ################# try: refresh() except Exception as exc: - raise ImportError('Failed to initialize: {0}'.format(exc)) from exc + raise ImportError("Failed to initialize: {0}".format(exc)) from exc ################# diff --git a/git/cmd.py b/git/cmd.py index 1ddf9e03f..12409b0c8 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -9,12 +9,7 @@ import logging import os import signal -from subprocess import ( - call, - Popen, - PIPE, - DEVNULL -) +from subprocess import call, Popen, PIPE, DEVNULL import subprocess import threading from textwrap import dedent @@ -29,10 +24,7 @@ from git.exc import CommandError from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present -from .exc import ( - GitCommandError, - GitCommandNotFound -) +from .exc import GitCommandError, GitCommandNotFound from .util import ( LazyMixin, stream_copy, @@ -40,8 +32,24 @@ # typing --------------------------------------------------------------------------- -from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, Iterator, List, Mapping, - Sequence, TYPE_CHECKING, TextIO, Tuple, Union, cast, overload) +from typing import ( + Any, + AnyStr, + BinaryIO, + Callable, + Dict, + IO, + Iterator, + List, + Mapping, + Sequence, + TYPE_CHECKING, + TextIO, + Tuple, + Union, + cast, + overload, +) from git.types import PathLike, Literal, TBD @@ -52,15 +60,26 @@ # --------------------------------------------------------------------------------- -execute_kwargs = {'istream', 'with_extended_output', - 'with_exceptions', 'as_process', 'stdout_as_string', - 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline_in_stdout'} +execute_kwargs = { + "istream", + "with_extended_output", + "with_exceptions", + "as_process", + "stdout_as_string", + "output_stream", + "with_stdout", + "kill_after_timeout", + "universal_newlines", + "shell", + "env", + "max_chunk_size", + "strip_newline_in_stdout", +} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -__all__ = ('Git',) +__all__ = ("Git",) # ============================================================================== @@ -69,18 +88,24 @@ # Documentation ## @{ -def handle_process_output(process: 'Git.AutoInterrupt' | Popen, - stdout_handler: Union[None, - Callable[[AnyStr], None], - Callable[[List[AnyStr]], None], - Callable[[bytes, 'Repo', 'DiffIndex'], None]], - stderr_handler: Union[None, - Callable[[AnyStr], None], - Callable[[List[AnyStr]], None]], - finalizer: Union[None, - Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, - decode_streams: bool = True, - kill_after_timeout: Union[None, float] = None) -> None: + +def handle_process_output( + process: "Git.AutoInterrupt" | Popen, + stdout_handler: Union[ + None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None], + Callable[[bytes, "Repo", "DiffIndex"], None], + ], + stderr_handler: Union[ + None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None] + ], + finalizer: Union[ + None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None] + ] = None, + decode_streams: bool = True, + kill_after_timeout: Union[None, float] = None, +) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -101,8 +126,13 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, should be killed. """ # Use 2 "pump" threads and wait for both to finish. - def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, - handler: Union[None, Callable[[Union[bytes, str]], None]]) -> None: + def pump_stream( + cmdline: List[str], + name: str, + stream: Union[BinaryIO, TextIO], + is_decode: bool, + handler: Union[None, Callable[[Union[bytes, str]], None]], + ) -> None: try: for line in stream: if handler: @@ -114,21 +144,25 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], handler(line) except Exception as ex: - log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") + log.error( + f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}" + ) if "I/O operation on closed file" not in str(ex): # Only reraise if the error was not due to the stream closing - raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex + raise CommandError( + [f"<{name}-pump>"] + remove_password_if_present(cmdline), ex + ) from ex finally: stream.close() - if hasattr(process, 'proc'): - process = cast('Git.AutoInterrupt', process) - cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, 'args', '') + if hasattr(process, "proc"): + process = cast("Git.AutoInterrupt", process) + cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "") 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) - cmdline = getattr(process, 'args', '') + cmdline = getattr(process, "args", "") p_stdout = process.stdout p_stderr = process.stderr @@ -137,15 +171,16 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], pumps: List[Tuple[str, IO, Callable[..., None] | None]] = [] if p_stdout: - pumps.append(('stdout', p_stdout, stdout_handler)) + pumps.append(("stdout", p_stdout, stdout_handler)) if p_stderr: - pumps.append(('stderr', p_stderr, stderr_handler)) + pumps.append(("stderr", p_stderr, stderr_handler)) threads: List[threading.Thread] = [] for name, stream, handler in pumps: - t = threading.Thread(target=pump_stream, - args=(cmdline, name, stream, decode_streams, handler)) + t = threading.Thread( + target=pump_stream, args=(cmdline, name, stream, decode_streams, handler) + ) t.daemon = True t.start() threads.append(t) @@ -158,12 +193,15 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], if isinstance(process, Git.AutoInterrupt): process._terminate() else: # Don't want to deal with the other case - raise RuntimeError("Thread join() timed out in cmd.handle_process_output()." - f" kill_after_timeout={kill_after_timeout} seconds") + raise RuntimeError( + "Thread join() timed out in cmd.handle_process_output()." + f" kill_after_timeout={kill_after_timeout} seconds" + ) if stderr_handler: error_str: Union[str, bytes] = ( "error: process killed because it timed out." - f" kill_after_timeout={kill_after_timeout} seconds") + f" kill_after_timeout={kill_after_timeout} seconds" + ) if not decode_streams and isinstance(p_stderr, BinaryIO): # Assume stderr_handler needs binary input error_str = cast(str, error_str) @@ -179,19 +217,22 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], def dashify(string: str) -> str: - return string.replace('_', '-') + return string.replace("_", "-") def slots_to_dict(self: object, exclude: Sequence[str] = ()) -> Dict[str, Any]: return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} -def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None: +def dict_to_slots_and__excluded_are_none( + self: object, d: Mapping[str, Any], excluded: Sequence[str] = () +) -> None: for k, v in d.items(): setattr(self, k, v) for k in excluded: setattr(self, k, None) + ## -- End Utilities -- @} @@ -200,8 +241,11 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc ## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards, # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal -PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] - if is_win else 0) # mypy error if not windows +PROC_CREATIONFLAGS = ( + CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] + if is_win + else 0 +) # mypy error if not windows class Git(LazyMixin): @@ -220,10 +264,18 @@ class Git(LazyMixin): of the command to stdout. Set its value to 'full' to see details about the returned values. """ - __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info", - "_git_options", "_persistent_git_options", "_environment") - _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info') + __slots__ = ( + "_working_dir", + "cat_file_all", + "cat_file_header", + "_version_info", + "_git_options", + "_persistent_git_options", + "_environment", + ) + + _excluded_ = ("cat_file_all", "cat_file_header", "_version_info") def __getstate__(self) -> Dict[str, Any]: return slots_to_dict(self, exclude=self._excluded_) @@ -233,7 +285,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: # CONFIGURATION - git_exec_name = "git" # default that should work on linux and windows + git_exec_name = "git" # default that should work on linux and windows # Enables debugging of GitPython's git commands GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) @@ -282,13 +334,18 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: # warn or raise exception if test failed if not has_git: - err = dedent("""\ + err = ( + dedent( + """\ Bad git executable. The git executable must be specified in one of the following ways: - be included in your $PATH - be set via $%s - explicitly set via git.refresh() - """) % cls._git_exec_env_var + """ + ) + % cls._git_exec_env_var + ) # revert to whatever the old_git was cls.GIT_PYTHON_GIT_EXECUTABLE = old_git @@ -314,7 +371,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: if mode in quiet: pass elif mode in warn or mode in error: - err = dedent("""\ + err = ( + dedent( + """\ %s All git commands will error until this is rectified. @@ -326,32 +385,42 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: Example: export %s=%s - """) % ( - err, - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - cls._refresh_env_var, - quiet[0]) + """ + ) + % ( + err, + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + cls._refresh_env_var, + quiet[0], + ) + ) if mode in warn: print("WARNING: %s" % err) else: raise ImportError(err) else: - err = dedent("""\ + err = ( + dedent( + """\ %s environment variable has been set but it has been set with an invalid value. Use only the following values: - %s: for no warning or exception - %s: for a printed warning - %s: for a raised exception - """) % ( - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error)) + """ + ) + % ( + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + ) + ) raise ImportError(err) # we get here if this was the init refresh and the refresh mode @@ -395,7 +464,7 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: Hence we undo the escaping just to be sure. """ url = os.path.expandvars(url) - if url.startswith('~'): + if url.startswith("~"): url = os.path.expanduser(url) url = url.replace("\\\\", "\\").replace("\\", "/") return url @@ -441,7 +510,7 @@ def _terminate(self) -> None: log.info("Ignored error after process had died: %r", ex) # can be that nothing really exists anymore ... - if os is None or getattr(os, 'kill', None) is None: + if os is None or getattr(os, "kill", None) is None: return None # try to kill it @@ -458,7 +527,10 @@ def _terminate(self) -> None: # we simply use the shell and redirect to nul. Its slower than CreateProcess, question # is whether we really want to see all these messages. Its annoying no matter what. if is_win: - call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) + call( + ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), + shell=True, + ) # END exception handling def __del__(self) -> None: @@ -468,15 +540,15 @@ def __getattr__(self, attr: str) -> Any: return getattr(self.proc, attr) # TODO: Bad choice to mimic `proc.wait()` but with different args. - def wait(self, stderr: Union[None, str, bytes] = b'') -> int: + def wait(self, stderr: Union[None, str, bytes] = b"") -> int: """Wait for the process and return its status code. :param stderr: Previously read value of stderr, in case stderr is already closed. :warn: may deadlock if output or error pipes are used and not handled separately. :raise GitCommandError: if the return status is not 0""" if stderr is None: - stderr_b = b'' - stderr_b = force_bytes(data=stderr, encoding='utf-8') + stderr_b = b"" + stderr_b = force_bytes(data=stderr, encoding="utf-8") status: Union[int, None] if self.proc is not None: status = self.proc.wait() @@ -485,21 +557,25 @@ def wait(self, stderr: Union[None, str, bytes] = b'') -> int: status = self.status p_stderr = None - def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: + def read_all_from_possibly_closed_stream( + stream: Union[IO[bytes], None] + ) -> bytes: if stream: try: return stderr_b + force_bytes(stream.read()) except ValueError: - return stderr_b or b'' + return stderr_b or b"" else: - return stderr_b or b'' + return stderr_b or b"" # END status handling if status != 0: errstr = read_all_from_possibly_closed_stream(p_stderr) - log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) - raise GitCommandError(remove_password_if_present(self.args), status, errstr) + log.debug("AutoInterrupt wait stderr: %r" % (errstr,)) + raise GitCommandError( + remove_password_if_present(self.args), status, errstr + ) return status # END auto interrupt @@ -513,12 +589,12 @@ class CatFileContentStream(object): If not all data is read to the end of the objects's lifetime, we read the rest to assure the underlying stream continues to work""" - __slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size') + __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size") def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream self._size = size - self._nbr = 0 # num bytes read + self._nbr = 0 # num bytes read # special case: if the object is empty, has null bytes, get the # final newline right away. @@ -529,7 +605,7 @@ def __init__(self, size: int, stream: IO[bytes]) -> None: def read(self, size: int = -1) -> bytes: bytes_left = self._size - self._nbr if bytes_left == 0: - return b'' + return b"" if size > -1: # assure we don't try to read past our limit size = min(bytes_left, size) @@ -542,13 +618,13 @@ def read(self, size: int = -1) -> bytes: # check for depletion, read our final byte to make the stream usable by others if self._size - self._nbr == 0: - self._stream.read(1) # final newline + self._stream.read(1) # final newline # END finish reading return data def readline(self, size: int = -1) -> bytes: if self._nbr == self._size: - return b'' + return b"" # clamp size to lowest allowed value bytes_left = self._size - self._nbr @@ -589,7 +665,7 @@ def readlines(self, size: int = -1) -> List[bytes]: return out # skipcq: PYL-E0301 - def __iter__(self) -> 'Git.CatFileContentStream': + def __iter__(self) -> "Git.CatFileContentStream": return self def __next__(self) -> bytes: @@ -634,7 +710,7 @@ def __getattr__(self, name: str) -> Any: """A convenience method as it allows to call the command as if it was an object. :return: Callable object that will execute call _call_process with your arguments.""" - if name[0] == '_': + if name[0] == "_": return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) @@ -650,27 +726,31 @@ def set_persistent_git_options(self, **kwargs: Any) -> None: """ self._persistent_git_options = self.transform_kwargs( - split_single_char_options=True, **kwargs) + split_single_char_options=True, **kwargs + ) def _set_cache_(self, attr: str) -> None: - if attr == '_version_info': + if attr == "_version_info": # We only use the first 4 numbers, as everything else could be strings in fact (on windows) - process_version = self._call_process('version') # should be as default *args and **kwargs used - version_numbers = process_version.split(' ')[2] - - self._version_info = cast(Tuple[int, int, int, int], - tuple(int(n) for n in version_numbers.split('.')[:4] if n.isdigit()) - ) + process_version = self._call_process( + "version" + ) # should be as default *args and **kwargs used + version_numbers = process_version.split(" ")[2] + + self._version_info = cast( + Tuple[int, int, int, int], + tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()), + ) else: super(Git, self)._set_cache_(attr) # END handle version info - @ property + @property def working_dir(self) -> Union[None, PathLike]: """:return: Git directory we are working on""" return self._working_dir - @ property + @property def version_info(self) -> Tuple[int, int, int, int]: """ :return: tuple(int, int, int, int) tuple with integers representing the major, minor @@ -678,69 +758,72 @@ def version_info(self) -> Tuple[int, int, int, int]: This value is generated on demand and is cached""" return self._version_info - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[True] - ) -> 'AutoInterrupt': + @overload + def execute( + self, command: Union[str, Sequence[Any]], *, as_process: Literal[True] + ) -> "AutoInterrupt": ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[True] - ) -> Union[str, Tuple[int, str, str]]: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[True], + ) -> Union[str, Tuple[int, str, str]]: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[False] = False - ) -> Union[bytes, Tuple[int, bytes, str]]: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[False] = False, + ) -> Union[bytes, Tuple[int, bytes, str]]: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[True] - ) -> str: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[True], + ) -> str: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[False] - ) -> bytes: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[False], + ) -> bytes: ... - def execute(self, - command: Union[str, Sequence[Any]], - istream: Union[None, BinaryIO] = None, - with_extended_output: bool = False, - with_exceptions: bool = True, - as_process: bool = False, - output_stream: Union[None, BinaryIO] = None, - stdout_as_string: bool = True, - kill_after_timeout: Union[None, float] = None, - with_stdout: bool = True, - universal_newlines: bool = False, - shell: Union[None, bool] = None, - env: Union[None, Mapping[str, str]] = None, - max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, - strip_newline_in_stdout: bool = True, - **subprocess_kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: + def execute( + self, + command: Union[str, Sequence[Any]], + istream: Union[None, BinaryIO] = None, + with_extended_output: bool = False, + with_exceptions: bool = True, + as_process: bool = False, + output_stream: Union[None, BinaryIO] = None, + stdout_as_string: bool = True, + kill_after_timeout: Union[None, float] = None, + with_stdout: bool = True, + universal_newlines: bool = False, + shell: Union[None, bool] = None, + env: Union[None, Mapping[str, str]] = None, + max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + strip_newline_in_stdout: bool = True, + **subprocess_kwargs: Any, + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns the returned information (stdout) @@ -831,8 +914,8 @@ def execute(self, you must update the execute_kwargs tuple housed in this module.""" # Remove password for the command if present redacted_command = remove_password_if_present(command) - if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != 'full' or as_process): - log.info(' '.join(redacted_command)) + if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): + log.info(" ".join(redacted_command)) # Allow the user to have the command executed in their working dir. try: @@ -858,33 +941,47 @@ def execute(self, if is_win: cmd_not_found_exception = OSError if kill_after_timeout is not None: - raise GitCommandError(redacted_command, '"kill_after_timeout" feature is not supported on Windows.') + raise GitCommandError( + redacted_command, + '"kill_after_timeout" feature is not supported on Windows.', + ) else: - cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + cmd_not_found_exception = ( + FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + ) # end handle - stdout_sink = (PIPE - if with_stdout - else getattr(subprocess, 'DEVNULL', None) or open(os.devnull, 'wb')) + stdout_sink = ( + PIPE + if with_stdout + else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") + ) istream_ok = "None" if istream: istream_ok = "" - log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", - redacted_command, cwd, universal_newlines, shell, istream_ok) + log.debug( + "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", + redacted_command, + cwd, + universal_newlines, + shell, + 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 - ) + 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 @@ -897,9 +994,12 @@ def execute(self, return self.AutoInterrupt(proc, command) def _kill_process(pid: int) -> None: - """ Callback method to kill a process. """ - p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE, - creationflags=PROC_CREATIONFLAGS) + """Callback method to kill a process.""" + p = Popen( + ["ps", "--ppid", str(pid)], + stdout=PIPE, + creationflags=PROC_CREATIONFLAGS, + ) child_pids = [] if p.stdout is not None: for line in p.stdout: @@ -909,29 +1009,32 @@ def _kill_process(pid: int) -> None: child_pids.append(int(local_pid)) try: # Windows does not have SIGKILL, so use SIGTERM instead - sig = getattr(signal, 'SIGKILL', signal.SIGTERM) + sig = getattr(signal, "SIGKILL", signal.SIGTERM) os.kill(pid, sig) for child_pid in child_pids: try: os.kill(child_pid, sig) except OSError: pass - kill_check.set() # tell the main routine that the process was killed + kill_check.set() # tell the main routine that the process was killed except OSError: # It is possible that the process gets completed in the duration after timeout # happens and before we try to kill the process. pass return + # end if kill_after_timeout is not None: kill_check = threading.Event() - watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,)) + watchdog = threading.Timer( + kill_after_timeout, _kill_process, args=(proc.pid,) + ) # Wait for the process to return status = 0 - stdout_value: Union[str, bytes] = b'' - stderr_value: Union[str, bytes] = b'' + stdout_value: Union[str, bytes] = b"" + stderr_value: Union[str, bytes] = b"" newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: @@ -941,8 +1044,10 @@ def _kill_process(pid: int) -> None: if kill_after_timeout is not None: watchdog.cancel() if kill_check.is_set(): - stderr_value = ('Timeout: the command "%s" did not complete in %d ' - 'secs.' % (" ".join(redacted_command), kill_after_timeout)) + stderr_value = ( + 'Timeout: the command "%s" did not complete in %d ' + "secs." % (" ".join(redacted_command), kill_after_timeout) + ) if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" @@ -953,12 +1058,16 @@ def _kill_process(pid: int) -> None: status = proc.returncode else: - max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE + max_chunk_size = ( + max_chunk_size + if max_chunk_size and max_chunk_size > 0 + else io.DEFAULT_BUFFER_SIZE + ) stream_copy(proc.stdout, output_stream, max_chunk_size) stdout_value = proc.stdout.read() stderr_value = proc.stderr.read() # strip trailing "\n" - if stderr_value.endswith(newline): # type: ignore + if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] status = proc.wait() # END stdout handling @@ -966,18 +1075,28 @@ def _kill_process(pid: int) -> None: proc.stdout.close() proc.stderr.close() - if self.GIT_PYTHON_TRACE == 'full': + if self.GIT_PYTHON_TRACE == "full": cmdstr = " ".join(redacted_command) def as_text(stdout_value: Union[bytes, str]) -> str: - return not output_stream and safe_decode(stdout_value) or '' + return ( + not output_stream and safe_decode(stdout_value) or "" + ) + # end if stderr_value: - log.info("%s -> %d; stdout: '%s'; stderr: '%s'", - cmdstr, status, as_text(stdout_value), safe_decode(stderr_value)) + log.info( + "%s -> %d; stdout: '%s'; stderr: '%s'", + cmdstr, + status, + as_text(stdout_value), + safe_decode(stderr_value), + ) elif stdout_value: - log.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value)) + log.info( + "%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value) + ) else: log.info("%s -> %d", cmdstr, status) # END handle debug printing @@ -985,7 +1104,9 @@ def as_text(stdout_value: Union[bytes, str]) -> str: if with_exceptions and status != 0: raise GitCommandError(redacted_command, status, stderr_value, stdout_value) - if isinstance(stdout_value, bytes) and stdout_as_string: # could also be output_stream + if ( + isinstance(stdout_value, bytes) and stdout_as_string + ): # could also be output_stream stdout_value = safe_decode(stdout_value) # Allow access to the command's status code @@ -1042,7 +1163,9 @@ def custom_environment(self, **kwargs: Any) -> Iterator[None]: finally: self.update_environment(**old_env) - def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]: + def transform_kwarg( + self, name: str, value: Any, split_single_char_options: bool + ) -> List[str]: if len(name) == 1: if value is True: return ["-%s" % name] @@ -1058,7 +1181,9 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool return ["--%s=%s" % (dashify(name), value)] return [] - def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: + def transform_kwargs( + self, split_single_char_options: bool = True, **kwargs: Any + ) -> List[str]: """Transforms Python style kwargs into git command line options.""" args = [] for k, v in kwargs.items(): @@ -1081,7 +1206,7 @@ def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]: return outlist - def __call__(self, **kwargs: Any) -> 'Git': + def __call__(self, **kwargs: Any) -> "Git": """Specify command line options to the git executable for a subcommand call @@ -1094,28 +1219,34 @@ def __call__(self, **kwargs: Any) -> 'Git': ``Examples``:: git(work_tree='/tmp').difftool()""" self._git_options = self.transform_kwargs( - split_single_char_options=True, **kwargs) + split_single_char_options=True, **kwargs + ) return self @overload - def _call_process(self, method: str, *args: None, **kwargs: None - ) -> str: + def _call_process(self, method: str, *args: None, **kwargs: None) -> str: ... # if no args given, execute called with all defaults @overload - def _call_process(self, method: str, - istream: int, - as_process: Literal[True], - *args: Any, **kwargs: Any - ) -> 'Git.AutoInterrupt': ... + def _call_process( + self, + method: str, + istream: int, + as_process: Literal[True], + *args: Any, + **kwargs: Any, + ) -> "Git.AutoInterrupt": + ... @overload - def _call_process(self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + def _call_process( + self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ... - def _call_process(self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + def _call_process( + self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: """Run the given git command with the specified arguments and return the result as a String @@ -1145,13 +1276,13 @@ def _call_process(self, method: str, *args: Any, **kwargs: Any :return: Same as ``execute`` if no args given used execute default (esp. as_process = False, stdout_as_string = True) - and return str """ + and return str""" # Handle optional arguments prior to calling transform_kwargs # otherwise these'll end up in args, which is bad. exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs} - insert_after_this_arg = opts_kwargs.pop('insert_kwargs_after', None) + insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None) # Prepare the argument list @@ -1164,10 +1295,12 @@ def _call_process(self, method: str, *args: Any, **kwargs: Any try: index = ext_args.index(insert_after_this_arg) except ValueError as err: - raise ValueError("Couldn't find argument '%s' in args %s to insert cmd options after" - % (insert_after_this_arg, str(ext_args))) from err + raise ValueError( + "Couldn't find argument '%s' in args %s to insert cmd options after" + % (insert_after_this_arg, str(ext_args)) + ) from err # end handle error - args_list = ext_args[:index + 1] + opt_args + ext_args[index + 1:] + args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :] # end handle opts_kwargs call = [self.GIT_PYTHON_GIT_EXECUTABLE] @@ -1197,9 +1330,15 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: tokens = header_line.split() if len(tokens) != 3: if not tokens: - raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip())) + raise ValueError( + "SHA could not be resolved, git returned: %r" + % (header_line.strip()) + ) else: - raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) + raise ValueError( + "SHA %s could not be resolved, git returned: %r" + % (tokens[0], header_line.strip()) + ) # END handle actual return value # END error handling @@ -1211,9 +1350,9 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: # required for command to separate refs on stdin, as bytes if isinstance(ref, bytes): # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text - refstr: str = ref.decode('ascii') + refstr: str = ref.decode("ascii") elif not isinstance(ref, str): - refstr = str(ref) # could be ref-object + refstr = str(ref) # could be ref-object else: refstr = ref @@ -1221,8 +1360,9 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: refstr += "\n" return refstr.encode(defenc) - def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any - ) -> 'Git.AutoInterrupt': + def _get_persistent_cmd( + self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any + ) -> "Git.AutoInterrupt": cur_val = getattr(self, attr_name) if cur_val is not None: return cur_val @@ -1232,10 +1372,12 @@ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwarg cmd = self._call_process(cmd_name, *args, **options) setattr(self, attr_name, cmd) - cmd = cast('Git.AutoInterrupt', cmd) + cmd = cast("Git.AutoInterrupt", cmd) return cmd - def __get_object_header(self, cmd: 'Git.AutoInterrupt', ref: AnyStr) -> Tuple[str, str, int]: + def __get_object_header( + self, cmd: "Git.AutoInterrupt", ref: AnyStr + ) -> Tuple[str, str, int]: if cmd.stdin and cmd.stdout: cmd.stdin.write(self._prepare_ref(ref)) cmd.stdin.flush() @@ -1244,7 +1386,7 @@ def __get_object_header(self, cmd: 'Git.AutoInterrupt', ref: AnyStr) -> Tuple[st raise ValueError("cmd stdin was empty") def get_object_header(self, ref: str) -> Tuple[str, str, int]: - """ Use this method to quickly examine the type and size of the object behind + """Use this method to quickly examine the type and size of the object behind the given ref. :note: The method will only suffer from the costs of command invocation @@ -1255,16 +1397,18 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]: return self.__get_object_header(cmd, ref) def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: - """ As get_object_header, but returns object data as well + """As get_object_header, but returns object data as well :return: (hexsha, type_string, size_as_int,data_string) :note: not threadsafe""" hexsha, typename, size, stream = self.stream_object_data(ref) data = stream.read(size) - del(stream) + del stream return (hexsha, typename, size, data) - def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileContentStream']: - """ As get_object_header, but returns the data as a stream + def stream_object_data( + self, ref: str + ) -> Tuple[str, str, int, "Git.CatFileContentStream"]: + """As get_object_header, but returns the data as a stream :return: (hexsha, type_string, size_as_int, stream) :note: This method is not threadsafe, you need one independent Command instance per thread to be safe !""" @@ -1273,7 +1417,7 @@ def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileConte cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout)) - def clear_cache(self) -> 'Git': + def clear_cache(self) -> "Git": """Clear all kinds of internal caches to release resources. Currently persistent commands will be interrupted. diff --git a/git/compat.py b/git/compat.py index 988c04eff..e7ef28c30 100644 --- a/git/compat.py +++ b/git/compat.py @@ -12,8 +12,8 @@ import sys from gitdb.utils.encoding import ( - force_bytes, # @UnusedImport - force_text # @UnusedImport + force_bytes, # @UnusedImport + force_text, # @UnusedImport ) # typing -------------------------------------------------------------------- @@ -29,21 +29,24 @@ Union, overload, ) + # --------------------------------------------------------------------------- -is_win: bool = (os.name == 'nt') -is_posix = (os.name == 'posix') -is_darwin = (os.name == 'darwin') +is_win: bool = os.name == "nt" +is_posix = os.name == "posix" +is_darwin = os.name == "darwin" defenc = sys.getfilesystemencoding() @overload -def safe_decode(s: None) -> None: ... +def safe_decode(s: None) -> None: + ... @overload -def safe_decode(s: AnyStr) -> str: ... +def safe_decode(s: AnyStr) -> str: + ... def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: @@ -51,19 +54,21 @@ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: if isinstance(s, str): return s elif isinstance(s, bytes): - return s.decode(defenc, 'surrogateescape') + return s.decode(defenc, "surrogateescape") elif s is None: return None else: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) @overload -def safe_encode(s: None) -> None: ... +def safe_encode(s: None) -> None: + ... @overload -def safe_encode(s: AnyStr) -> bytes: ... +def safe_encode(s: AnyStr) -> bytes: + ... def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: @@ -75,15 +80,17 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: elif s is None: return None else: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) @overload -def win_encode(s: None) -> None: ... +def win_encode(s: None) -> None: + ... @overload -def win_encode(s: AnyStr) -> bytes: ... +def win_encode(s: AnyStr) -> bytes: + ... def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: @@ -93,5 +100,5 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: elif isinstance(s, bytes): return s elif s is not None: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) return None diff --git a/git/config.py b/git/config.py index 1ac3c9cec..24c2b2013 100644 --- a/git/config.py +++ b/git/config.py @@ -30,8 +30,20 @@ # typing------------------------------------------------------- -from typing import (Any, Callable, Generic, IO, List, Dict, Sequence, - TYPE_CHECKING, Tuple, TypeVar, Union, cast) +from typing import ( + Any, + Callable, + Generic, + IO, + List, + Dict, + Sequence, + TYPE_CHECKING, + Tuple, + TypeVar, + Union, + cast, +) from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T @@ -39,23 +51,25 @@ from git.repo.base import Repo from io import BytesIO -T_ConfigParser = TypeVar('T_ConfigParser', bound='GitConfigParser') -T_OMD_value = TypeVar('T_OMD_value', str, bytes, int, float, bool) +T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") +T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool) if sys.version_info[:3] < (3, 7, 2): # typing.Ordereddict not added until py 3.7.2 from collections import OrderedDict + OrderedDict_OMD = OrderedDict else: from typing import OrderedDict + OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] # ------------------------------------------------------------- -__all__ = ('GitConfigParser', 'SectionConstraint') +__all__ = ("GitConfigParser", "SectionConstraint") -log = logging.getLogger('git.config') +log = logging.getLogger("git.config") log.addHandler(logging.NullHandler()) # invariants @@ -67,26 +81,37 @@ # Section pattern to detect conditional includes. # https://git-scm.com/docs/git-config#_conditional_includes -CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") +CONDITIONAL_INCLUDE_REGEXP = re.compile( + r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"" +) class MetaParserBuilder(abc.ABCMeta): """Utility class wrapping base-class methods into decorators that assure read-only properties""" - def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': + + def __new__( + cls, name: str, bases: Tuple, clsdict: Dict[str, Any] + ) -> "MetaParserBuilder": """ Equip all base-class methods with a needs_values decorator, and all non-const methods with a set_dirty_and_flush_changes decorator in addition to that.""" - kmm = '_mutating_methods_' + kmm = "_mutating_methods_" if kmm in clsdict: mutating_methods = clsdict[kmm] for base in bases: - methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) + methods = ( + t + for t in inspect.getmembers(base, inspect.isroutine) + if not t[0].startswith("_") + ) for name, method in methods: if name in clsdict: continue method_with_values = needs_values(method) if name in mutating_methods: - method_with_values = set_dirty_and_flush_changes(method_with_values) + method_with_values = set_dirty_and_flush_changes( + method_with_values + ) # END mutating methods handling clsdict[name] = method_with_values @@ -102,9 +127,10 @@ def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: """Returns method assuring we read values (on demand) before we try to access them""" @wraps(func) - def assure_data_present(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: self.read() return func(self, *args, **kwargs) + # END wrapper method return assure_data_present @@ -114,11 +140,12 @@ def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[. If so, the instance will be set dirty. Additionally, we flush the changes right to disk""" - def flush_changes(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: rval = non_const_func(self, *args, **kwargs) self._dirty = True self.write() return rval + # END wrapper method flush_changes.__name__ = non_const_func.__name__ return flush_changes @@ -133,9 +160,21 @@ class SectionConstraint(Generic[T_ConfigParser]): :note: If used as a context manager, will release the wrapped ConfigParser.""" + __slots__ = ("_config", "_section_name") - _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", - "remove_section", "remove_option", "options") + _valid_attrs_ = ( + "get_value", + "set_value", + "get", + "set", + "getint", + "getfloat", + "getboolean", + "has_option", + "remove_section", + "remove_option", + "options", + ) def __init__(self, config: T_ConfigParser, section: str) -> None: self._config = config @@ -166,11 +205,13 @@ def release(self) -> None: """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" return self._config.release() - def __enter__(self) -> 'SectionConstraint[T_ConfigParser]': + def __enter__(self) -> "SectionConstraint[T_ConfigParser]": self._config.__enter__() return self - def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: + def __exit__( + self, exception_type: str, exception_value: str, traceback: str + ) -> None: self._config.__exit__(exception_type, exception_value, traceback) @@ -228,16 +269,22 @@ def get_config_path(config_level: Lit_config_levels) -> str: if config_level == "system": return "/etc/gitconfig" elif config_level == "user": - config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", '~'), ".config") + config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join( + os.environ.get("HOME", "~"), ".config" + ) return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) elif config_level == "global": return osp.normpath(osp.expanduser("~/.gitconfig")) elif config_level == "repository": - raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") + 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] - ValueError(f"Invalid configuration level: {config_level!r}")) + assert_never( + config_level, # type: ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}"), + ) class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): @@ -258,30 +305,36 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): must match perfectly. If used as a context manager, will release the locked file.""" - #{ Configuration + # { Configuration # The lock type determines the type of lock to use in new configuration readers. # They must be compatible to the LockFile interface. # A suitable alternative would be the BlockingLockFile t_lock = LockFile - re_comment = re.compile(r'^\s*[#;]') + re_comment = re.compile(r"^\s*[#;]") - #} END configuration + # } END configuration - optvalueonly_source = r'\s*(?P