From ea43defd777a9c0751fc44a9c6a622fc2dbd18a0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Apr 2021 18:44:45 +0800 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 40f4cebbc095d043463b3e72d740acbcb84491d8 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Sep 2021 19:00:58 +0100 Subject: [PATCH 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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