From 5e73cabd041f45337b270d5e78674d88448929e6 Mon Sep 17 00:00:00 2001 From: Ket3r Date: Wed, 29 Sep 2021 21:04:14 +0200 Subject: [PATCH 0001/1175] Fix broken test requirements The ddt package changed the function signature in version 1.4.3 from idata(iterable) to idata(iterable, index_len). Hopefully this was just a mistake and the new argument will be optional in future versions (see issue datadriventests/ddt#97) --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index deaafe214..d5d2346a0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -ddt>=1.1.1 +ddt>=1.1.1, !=1.4.3 mypy flake8 From 53d94b8091b36847bb9e495c76bb5a3ec2a2fdb5 Mon Sep 17 00:00:00 2001 From: Trym Bremnes Date: Thu, 30 Sep 2021 08:54:43 +0200 Subject: [PATCH 0002/1175] Replace wildcard imports with concrete imports All `from import *` has now been replaced by `from import X, Y, ...`. Contributes to #1349 --- git/__init__.py | 22 +++++++++++----------- git/exc.py | 3 +-- git/index/__init__.py | 4 ++-- git/objects/__init__.py | 14 +++++++------- git/refs/__init__.py | 12 ++++++------ test/lib/__init__.py | 7 +++++-- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index ae9254a26..a2213ee0f 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa #@PydevCodeAnalysisIgnore -from git.exc import * # @NoMove @IgnorePep8 +from git.exc import GitError, GitCommandError, GitCommandNotFound, UnmergedEntriesError, CheckoutError, InvalidGitRepositoryError, NoSuchPathError, BadName # @NoMove @IgnorePep8 import inspect import os import sys @@ -39,16 +39,16 @@ def _init_externals() -> None: #{ Imports try: - from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import * # @NoMove @IgnorePep8 - from git.refs import * # @NoMove @IgnorePep8 - from git.diff import * # @NoMove @IgnorePep8 - from git.db import * # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import * # @NoMove @IgnorePep8 - from git.index import * # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import Blob, Commit, Object, Submodule, Tree # @NoMove @IgnorePep8 + from git.refs import Head, Reference, RefLog, RemoteReference, SymbolicReference, TagReference # @NoMove @IgnorePep8 + from git.diff import Diff, DiffIndex, NULL_TREE # @NoMove @IgnorePep8 + from git.db import GitCmdObjectDB, GitDB # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove @IgnorePep8 + from git.index import BlobFilter, IndexEntry, IndexFile # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, diff --git a/git/exc.py b/git/exc.py index e8ff784c7..d29a25f63 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,8 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ -from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 -from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import BadName, BadObject # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode # typing ---------------------------------------------------- diff --git a/git/index/__init__.py b/git/index/__init__.py index 96b721f07..f0ac81e5a 100644 --- a/git/index/__init__.py +++ b/git/index/__init__.py @@ -1,4 +1,4 @@ """Initialize the index package""" # flake8: noqa -from .base import * -from .typ import * +from .base import IndexFile +from .typ import IndexEntry, BlobFilter diff --git a/git/objects/__init__.py b/git/objects/__init__.py index 1d0bb7a51..c4a492274 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -4,14 +4,14 @@ # flake8: noqa import inspect -from .base import * -from .blob import * -from .commit import * +from .base import Object, IndexObject +from .blob import Blob +from .commit import Commit from .submodule import util as smutil -from .submodule.base import * -from .submodule.root import * -from .tag import * -from .tree import * +from .submodule.base import Submodule, UpdateProgress +from .submodule.root import RootModule, RootUpdateProgress +from .tag import TagObject +from .tree import Tree # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base smutil.IndexObject = IndexObject # type: ignore[attr-defined] diff --git a/git/refs/__init__.py b/git/refs/__init__.py index 1486dffe6..075c65c8f 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -1,9 +1,9 @@ # flake8: noqa # import all modules in order, fix the names they require -from .symbolic import * -from .reference import * -from .head import * -from .tag import * -from .remote import * +from .symbolic import SymbolicReference +from .reference import Reference +from .head import HEAD, Head +from .tag import TagReference +from .remote import RemoteReference -from .log import * +from .log import RefLogEntry, RefLog diff --git a/test/lib/__init__.py b/test/lib/__init__.py index 1551ce455..3634df803 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -4,9 +4,12 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -# flake8: noqa import inspect -from .helper import * + +from .helper import (GIT_DAEMON_PORT, SkipTest, StringProcessAdapter, TestBase, + TestCase, fixture, fixture_path, + with_rw_and_rw_remote_repo, with_rw_directory, + with_rw_repo) __all__ = [name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))] From ce4afe46d211cdfb611b8e8109bb0dc160a12540 Mon Sep 17 00:00:00 2001 From: Trym Bremnes Date: Sat, 2 Oct 2021 16:42:35 +0200 Subject: [PATCH 0003/1175] Revert "Replace wildcard imports with concrete imports" This reverts commit 53d94b8091b36847bb9e495c76bb5a3ec2a2fdb5. The reason for the revert is that the commit in question introduced a regression where certain modules, functions and classes that were exposed before were no longer exposed. See https://github.com/gitpython-developers/GitPython/pull/1352#issuecomment-932757204 for additional information. --- git/__init__.py | 22 +++++++++++----------- git/exc.py | 3 ++- git/index/__init__.py | 4 ++-- git/objects/__init__.py | 14 +++++++------- git/refs/__init__.py | 12 ++++++------ test/lib/__init__.py | 7 ++----- 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index a2213ee0f..ae9254a26 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa #@PydevCodeAnalysisIgnore -from git.exc import GitError, GitCommandError, GitCommandNotFound, UnmergedEntriesError, CheckoutError, InvalidGitRepositoryError, NoSuchPathError, BadName # @NoMove @IgnorePep8 +from git.exc import * # @NoMove @IgnorePep8 import inspect import os import sys @@ -39,16 +39,16 @@ def _init_externals() -> None: #{ Imports try: - from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import Blob, Commit, Object, Submodule, Tree # @NoMove @IgnorePep8 - from git.refs import Head, Reference, RefLog, RemoteReference, SymbolicReference, TagReference # @NoMove @IgnorePep8 - from git.diff import Diff, DiffIndex, NULL_TREE # @NoMove @IgnorePep8 - from git.db import GitCmdObjectDB, GitDB # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove @IgnorePep8 - from git.index import BlobFilter, IndexEntry, IndexFile # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import * # @NoMove @IgnorePep8 + from git.refs import * # @NoMove @IgnorePep8 + from git.diff import * # @NoMove @IgnorePep8 + from git.db import * # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # @NoMove @IgnorePep8 + from git.index import * # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, diff --git a/git/exc.py b/git/exc.py index d29a25f63..e8ff784c7 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,7 +5,8 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ -from gitdb.exc import BadName, BadObject # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 +from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode # typing ---------------------------------------------------- diff --git a/git/index/__init__.py b/git/index/__init__.py index f0ac81e5a..96b721f07 100644 --- a/git/index/__init__.py +++ b/git/index/__init__.py @@ -1,4 +1,4 @@ """Initialize the index package""" # flake8: noqa -from .base import IndexFile -from .typ import IndexEntry, BlobFilter +from .base import * +from .typ import * diff --git a/git/objects/__init__.py b/git/objects/__init__.py index c4a492274..1d0bb7a51 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -4,14 +4,14 @@ # flake8: noqa import inspect -from .base import Object, IndexObject -from .blob import Blob -from .commit import Commit +from .base import * +from .blob import * +from .commit import * from .submodule import util as smutil -from .submodule.base import Submodule, UpdateProgress -from .submodule.root import RootModule, RootUpdateProgress -from .tag import TagObject -from .tree import Tree +from .submodule.base import * +from .submodule.root import * +from .tag import * +from .tree import * # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base smutil.IndexObject = IndexObject # type: ignore[attr-defined] diff --git a/git/refs/__init__.py b/git/refs/__init__.py index 075c65c8f..1486dffe6 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -1,9 +1,9 @@ # flake8: noqa # import all modules in order, fix the names they require -from .symbolic import SymbolicReference -from .reference import Reference -from .head import HEAD, Head -from .tag import TagReference -from .remote import RemoteReference +from .symbolic import * +from .reference import * +from .head import * +from .tag import * +from .remote import * -from .log import RefLogEntry, RefLog +from .log import * diff --git a/test/lib/__init__.py b/test/lib/__init__.py index 3634df803..1551ce455 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -4,12 +4,9 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +# flake8: noqa import inspect - -from .helper import (GIT_DAEMON_PORT, SkipTest, StringProcessAdapter, TestBase, - TestCase, fixture, fixture_path, - with_rw_and_rw_remote_repo, with_rw_directory, - with_rw_repo) +from .helper import * __all__ = [name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))] From b17bc980b1546159ceb119f04716f24b043fc3f8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 3 Oct 2021 19:44:18 +0800 Subject: [PATCH 0004/1175] =?UTF-8?q?It's=20python,=20so=20stuff=20breaks?= =?UTF-8?q?=20with=20patches=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …https://github.com/pytest-dev/pytest-cov/pull/472 Break a few to fix a few. --- test-requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d5d2346a0..53d8e606d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,4 +10,5 @@ virtualenv pytest pytest-cov -pytest-sugar \ No newline at end of file +coverage[toml] +pytest-sugar From b0630030a1d2db994fb2fb488efa167b91594864 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 13 Oct 2021 11:16:27 +0300 Subject: [PATCH 0005/1175] Add support for Python 3.10 --- .github/workflows/pythonpackage.yml | 2 +- AUTHORS | 1 + setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4e7aa418c..dd1e9a07e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.7.5, 3.7.12, 3.8, 3.8.0, 3.8.11, 3.8, 3.9, 3.9.0, 3.9.7] # , "3.10.0-rc.2"] + python-version: [3.7, 3.7.5, 3.7.12, 3.8, 3.8.0, 3.8.11, 3.8, 3.9, 3.9.0, 3.9.7, "3.10"] steps: - uses: actions/checkout@v2 diff --git a/AUTHORS b/AUTHORS index 606796d98..55d681813 100644 --- a/AUTHORS +++ b/AUTHORS @@ -44,4 +44,5 @@ Contributors are: -Ram Rachum -Alba Mendez -Robert Westman +-Hugo van Kemenade Portions derived from other open source works and are clearly marked. diff --git a/setup.py b/setup.py index cd1007d74..4f1d0b75e 100755 --- a/setup.py +++ b/setup.py @@ -122,6 +122,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: 3.10" + "Programming Language :: Python :: 3.10", ] ) From a9696eff9bbf8ffe266653f95c9748e40c58a5d1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 13 Oct 2021 11:26:02 +0300 Subject: [PATCH 0006/1175] Sphinx 4.3.0 will be needed for Python 3.10 --- doc/requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 917feb350..ad3c118a2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,6 @@ -sphinx==4.1.2 +# TODO Temporary until Sphinx 4.3.0 released with Python 3.10 support: +# https://github.com/sphinx-doc/sphinx/pull/9712 +sphinx==4.1.2;python_version<="3.9" +git+git://github.com/sphinx-doc/sphinx@f13ad80#egg=sphinx;python_version>="3.10" sphinx_rtd_theme sphinx-autodoc-typehints From a3efd2458afe535ffd9dcc756d8ba0c931d10ff2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 10 Nov 2021 20:23:06 +0200 Subject: [PATCH 0007/1175] Remove Sphinx workaround --- doc/requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index ad3c118a2..41a7c90f1 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,3 @@ -# TODO Temporary until Sphinx 4.3.0 released with Python 3.10 support: -# https://github.com/sphinx-doc/sphinx/pull/9712 -sphinx==4.1.2;python_version<="3.9" -git+git://github.com/sphinx-doc/sphinx@f13ad80#egg=sphinx;python_version>="3.10" +sphinx==4.3.0 sphinx_rtd_theme sphinx-autodoc-typehints From 3b82fa3018a21f0eeb76034ecb8fb4dedea9a966 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 12 Oct 2021 11:23:39 +0200 Subject: [PATCH 0008/1175] Let remote.push return a PushInfoList List-like, so that it's backward compatible. But it has a new method raise_on_error, that throws an exception if anything failed to push. Related to #621 --- git/remote.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index 2cf5678b6..63b4dc510 100644 --- a/git/remote.py +++ b/git/remote.py @@ -116,6 +116,22 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress +class PushInfoList(IterableList): + def __new__(cls) -> 'IterableList[IterableObj]': + return super(IterableList, cls).__new__(cls, 'push_infos') + + def __init__(self) -> None: + super().__init__('push_infos') + self.exception = None + + def raise_on_error(self): + """ + Raise an exception if any ref failed to push. + """ + if self.exception: + raise self.exception + + class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -774,7 +790,7 @@ def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', def _get_push_info(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None], - kill_after_timeout: Union[None, float] = None) -> IterableList[PushInfo]: + kill_after_timeout: Union[None, float] = None) -> PushInfoList: progress = to_progress_instance(progress) # read progress information from stderr @@ -782,7 +798,7 @@ def _get_push_info(self, proc: 'Git.AutoInterrupt', # read the lines manually as it will use carriage returns between the messages # to override the previous one. This is why we read the bytes manually progress_handler = progress.new_message_handler() - output: IterableList[PushInfo] = IterableList('push_infos') + output: PushInfoList = PushInfoList() def stdout_handler(line: str) -> None: try: @@ -796,13 +812,14 @@ def stdout_handler(line: str) -> None: stderr_text = progress.error_lines and '\n'.join(progress.error_lines) or '' try: proc.wait(stderr=stderr_text) - except Exception: + except Exception as e: # This is different than fetch (which fails if there is any std_err # even if there is an output) if not output: raise elif stderr_text: log.warning("Error lines received while fetching: %s", stderr_text) + output.exception = e return output From 1481e7108fb206a95717c331478d4382cda51a6a Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 13 Oct 2021 10:03:53 +0200 Subject: [PATCH 0009/1175] Test that return value of push is a list-like object --- test/test_remote.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 088fdad55..fedfa2070 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -30,7 +30,7 @@ fixture, GIT_DAEMON_PORT ) -from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS +from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList import os.path as osp @@ -128,6 +128,9 @@ def _do_test_fetch_result(self, results, remote): # END for each info def _do_test_push_result(self, results, remote): + self.assertIsInstance(results, list) + self.assertIsInstance(results, IterableList) + self.assertGreater(len(results), 0) self.assertIsInstance(results[0], PushInfo) for info in results: From 9240de9f788396c45199cd3d9fa7fdbd8a5666c4 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 8 Nov 2021 16:20:32 +0000 Subject: [PATCH 0010/1175] Rename exception to error, raise_on_error to raise_if_error --- git/remote.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/git/remote.py b/git/remote.py index 63b4dc510..745436761 100644 --- a/git/remote.py +++ b/git/remote.py @@ -122,14 +122,14 @@ def __new__(cls) -> 'IterableList[IterableObj]': def __init__(self) -> None: super().__init__('push_infos') - self.exception = None + self.error = None - def raise_on_error(self): + def raise_if_error(self): """ Raise an exception if any ref failed to push. """ - if self.exception: - raise self.exception + if self.error: + raise self.error class PushInfo(IterableObj, object): @@ -819,7 +819,7 @@ def stdout_handler(line: str) -> None: raise elif stderr_text: log.warning("Error lines received while fetching: %s", stderr_text) - output.exception = e + output.error = e return output From 699e223c51d99d1fc8d05b2b0fe0ef1e2ee7fd01 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 8 Nov 2021 17:06:37 +0000 Subject: [PATCH 0011/1175] Test raise_if_error --- test/test_remote.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_remote.py b/test/test_remote.py index fedfa2070..761a7a3e7 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -154,6 +154,12 @@ def _do_test_push_result(self, results, remote): # END error checking # END for each info + if any([info.flags & info.ERROR for info in results]): + self.assertRaises(GitCommandError, results.raise_if_error) + else: + # No errors, so this should do nothing + results.raise_if_error() + def _do_test_fetch_info(self, repo): self.assertRaises(ValueError, FetchInfo._from_line, repo, "nonsense", '') self.assertRaises( From 63f4ca304bddf019220912b7b8e2abe585d88fe0 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 11:55:51 +0000 Subject: [PATCH 0012/1175] Add raise_if_error() to tutorial --- test/test_docs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_docs.py b/test/test_docs.py index 220156bce..8897bbb75 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -393,7 +393,8 @@ def test_references_and_objects(self, rw_dir): origin.rename('new_origin') # push and pull behaves similarly to `git push|pull` origin.pull() - origin.push() + origin.push() # attempt push, ignore errors + origin.push().raise_if_error() # push and raise error if it fails # assert not empty_repo.delete_remote(origin).exists() # create and delete remotes # ![25-test_references_and_objects] From 8797904d04abc2df5da93ca7d799da21e5a50cb5 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 12:17:02 +0000 Subject: [PATCH 0013/1175] Fix type handing on PushInfoList --- git/remote.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index 745436761..aae845e5d 100644 --- a/git/remote.py +++ b/git/remote.py @@ -117,14 +117,15 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non class PushInfoList(IterableList): - def __new__(cls) -> 'IterableList[IterableObj]': - return super(IterableList, cls).__new__(cls, 'push_infos') + def __new__(cls) -> 'PushInfoList': + base = super().__new__(cls, 'push_infos') + return cast(PushInfoList, base) def __init__(self) -> None: super().__init__('push_infos') self.error = None - def raise_if_error(self): + def raise_if_error(self) -> None: """ Raise an exception if any ref failed to push. """ From e67e458ece9077f6c6db9fc6a867ac61e0ae6579 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 9 Nov 2021 15:16:44 +0000 Subject: [PATCH 0014/1175] Specify type for PushInfoList.error --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index aae845e5d..c212f6d28 100644 --- a/git/remote.py +++ b/git/remote.py @@ -123,7 +123,7 @@ def __new__(cls) -> 'PushInfoList': def __init__(self) -> None: super().__init__('push_infos') - self.error = None + self.error: Optional[Exception] = None def raise_if_error(self) -> None: """ From 35f7e9486c8bc596506a6872c7e0df37c4a35da3 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 10 Nov 2021 12:40:06 +0000 Subject: [PATCH 0015/1175] Extend IterableList[PushInfo] instead of IterableList --- git/remote.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/git/remote.py b/git/remote.py index c212f6d28..7d5918a5a 100644 --- a/git/remote.py +++ b/git/remote.py @@ -116,23 +116,6 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress -class PushInfoList(IterableList): - def __new__(cls) -> 'PushInfoList': - base = super().__new__(cls, 'push_infos') - return cast(PushInfoList, base) - - def __init__(self) -> None: - super().__init__('push_infos') - self.error: Optional[Exception] = None - - def raise_if_error(self) -> None: - """ - Raise an exception if any ref failed to push. - """ - if self.error: - raise self.error - - class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -252,6 +235,22 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any raise NotImplementedError +class PushInfoList(IterableList[PushInfo]): + def __new__(cls) -> 'PushInfoList': + return cast(PushInfoList, IterableList.__new__(cls, 'push_infos')) + + def __init__(self) -> None: + super().__init__('push_infos') + self.error: Optional[Exception] = None + + def raise_if_error(self) -> None: + """ + Raise an exception if any ref failed to push. + """ + if self.error: + raise self.error + + class FetchInfo(IterableObj, object): """ From 62131f3905ac37ff841142e2bb04bb585401a3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 30 Nov 2021 10:17:52 +0100 Subject: [PATCH 0016/1175] Revert the use of typing_extensions in py3.8+ The original change requiring py3.10 TypeGuard (and matching typing_extensions) has been reverted, so revert the requirement on typing_extensions as well. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a20310fb2..7159416a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.3;python_version<"3.10" +typing-extensions>=3.7.4.3;python_version<"3.8" From 2141eaef76fdfb2775dde45d087b34144d34a1fb Mon Sep 17 00:00:00 2001 From: yogabonito Date: Wed, 1 Dec 2021 00:42:39 +0100 Subject: [PATCH 0017/1175] DOC: fix typo --- doc/source/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 303e89cff..bc386e7c4 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -8,9 +8,9 @@ GitPython Tutorial ================== -GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explains a real-life usecase. +GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explain a real-life use case. -All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. +All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes. All you need is a developer installation of git-python. Meet the Repo type ****************** From d79d20d28b1f9324193309cffd2ab79e0edae925 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 08:59:19 +0800 Subject: [PATCH 0018/1175] Avoid taking a lock for reading This isn't needed as git will replace this file atomicially, hence we always see a fully written file when reading. Only when writing we need to obtain a lock. --- git/ext/gitdb | 2 +- git/index/base.py | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/git/ext/gitdb b/git/ext/gitdb index 03ab3a1d4..1c976835c 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 03ab3a1d40c04d6a944299c21db61cf9ce30f6bb +Subproject commit 1c976835c5d1779a28b9e11afd1656152db26a68 diff --git a/git/index/base.py b/git/index/base.py index 102703e6d..d1f039cd9 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -127,30 +127,17 @@ def __init__(self, repo: 'Repo', file_path: Union[PathLike, None] = None) -> Non def _set_cache_(self, attr: str) -> None: if attr == "entries": - # read the current index - # try memory map for speed - lfd = LockedFD(self._file_path) - ok = False try: - fd = lfd.open(write=False, stream=False) - ok = True + fd = os.open(self._file_path, os.O_RDONLY) except OSError: # in new repositories, there may be no index, which means we are empty self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {} return None - finally: - if not ok: - lfd.rollback() # END exception handling stream = file_contents_ro(fd, stream=True, allow_mmap=True) - try: - self._deserialize(stream) - finally: - lfd.rollback() - # The handles will be closed on destruction - # END read from default index on demand + self._deserialize(stream) else: super(IndexFile, self)._set_cache_(attr) From da7b5b286a8fc75f2d2e9183bf1d13f9d8cdce49 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 09:47:16 +0800 Subject: [PATCH 0019/1175] Ignore mypi errors With each patch level it may bring up new issues that cause CI failure for without being related to the actual change. --- .github/workflows/pythonpackage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dd1e9a07e..881f2ec57 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -50,6 +50,9 @@ jobs: flake8 - name: Check types with mypy + # With new versions of pypi new issues might arise. This is a problem if there is nobody able to fix them, + # so we have to ignore errors until that changes. + continue-on-error: true run: | set -x mypy -p git From 01f09888208341876d1480bd22dc8f4107c100f1 Mon Sep 17 00:00:00 2001 From: NHanser Date: Thu, 23 Dec 2021 12:51:32 +0100 Subject: [PATCH 0020/1175] Use NUL character to extract meta and path from git diff Use NUL character instead of semicolon to extract meta and path. Avoid errors in during git diff when dealing with filenames containing semicolons --- git/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index cea66d7ee..c8c57685b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -509,9 +509,9 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: Union['Popen', 'Git.AutoIn def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None: lines = lines_bytes.decode(defenc) - for line in lines.split(':')[1:]: - meta, _, path = line.partition('\x00') - path = path.rstrip('\x00') + it = iter(lines.split('\x00')) + for meta, path in zip(it, it): + meta = meta[1:] a_blob_id: Optional[str] b_blob_id: Optional[str] old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) From cdf7ffc33fa05ba5afcb915a374c140c7658c839 Mon Sep 17 00:00:00 2001 From: Peter Kempter Date: Wed, 29 Sep 2021 12:08:30 +0200 Subject: [PATCH 0021/1175] Add failing unit test --- test/test_commit.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/test_commit.py b/test/test_commit.py index 67dc7d732..5aeef2e6c 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -4,6 +4,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +import copy from datetime import datetime from io import BytesIO import re @@ -429,3 +430,48 @@ def test_datetimes(self): datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200))) self.assertEqual(commit.committed_datetime, datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime) + + def test_trailers(self): + KEY_1 = "Hello" + VALUE_1 = "World" + KEY_2 = "Key" + VALUE_2 = "Value" + + # Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations + msgs = [] + msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + + for msg in msgs: + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = msg + assert KEY_1 in commit.trailers.keys() + assert KEY_2 in commit.trailers.keys() + assert commit.trailers[KEY_1] == VALUE_1 + assert commit.trailers[KEY_2] == VALUE_2 + + # Check that trailer stays empty for multiple msg combinations + msgs = [] + msgs.append(f"Subject\n") + msgs.append(f"Subject\n\nBody with some\nText\n") + msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n") + msgs.append(f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n") + msgs.append(f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n") + msgs.append(f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n") + + for msg in msgs: + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = msg + assert len(commit.trailers.keys()) == 0 + + # check that only the last key value paragraph is evaluated + commit = self.rorepo.commit('master') + commit = copy.copy(commit) + commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1}\n\n{KEY_2}: {VALUE_2}\n" + assert KEY_1 not in commit.trailers.keys() + assert KEY_2 in commit.trailers.keys() + assert commit.trailers[KEY_2] == VALUE_2 From edbf76f98f8430d711115f2c754de88e268e9303 Mon Sep 17 00:00:00 2001 From: Peter Kempter Date: Wed, 29 Sep 2021 12:08:56 +0200 Subject: [PATCH 0022/1175] Add trailer as commit property With the command `git interpret-trailers` git provides a way to interact with trailer lines in the commit messages that look similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). The new property returns those parsed trailer lines from the message as dictionary. --- git/objects/commit.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index b36cd46d2..780461a0c 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,6 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import datetime +import re from subprocess import Popen from gitdb import IStream from git.util import ( @@ -39,7 +40,7 @@ # typing ------------------------------------------------------------------ -from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast +from typing import Any, IO, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING, cast, Dict from git.types import PathLike, Literal @@ -315,6 +316,44 @@ def stats(self) -> Stats: text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, '--', numstat=True) return Stats._list_from_string(self.repo, text) + @property + def trailers(self) -> Dict: + """Get the trailers of the message as dictionary + + Git messages can contain trailer information that are similar to RFC 822 + e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). + + The trailer is thereby the last paragraph (seperated by a empty line + from the subject/body). This trailer paragraph must contain a ``:`` as + seperator for key and value in every line. + + Valid message with trailer: + + .. code-block:: + + Subject line + + some body information + + another information + + key1: value1 + key2: value2 + + :return: Dictionary containing whitespace stripped trailer information + """ + d: Dict[str, str] = {} + match = re.search(r".+^\s*$\n([\w\n\s:]+?)\s*\Z", str(self.message), re.MULTILINE | re.DOTALL) + if match is None: + return d + last_paragraph = match.group(1) + if not all(':' in line for line in last_paragraph.split('\n')): + return d + for line in last_paragraph.split('\n'): + key, value = line.split(':', 1) + d[key.strip()] = value.strip() + return d + @ classmethod def _iter_from_process_or_stream(cls, repo: 'Repo', proc_or_stream: Union[Popen, IO]) -> Iterator['Commit']: """Parse out commit information into a list of Commit objects From cd8b9b2fd875b5040b1ca9f0c8f5acaffe70ab7f Mon Sep 17 00:00:00 2001 From: Ket3r Date: Thu, 30 Sep 2021 16:07:05 +0200 Subject: [PATCH 0023/1175] Use git interpret-trailers for trailers property The whitespace handling and trailer selection isn't very trivial or good documented. It therefore seemed easier and less error prone to just call git to parse the message for the trailers section and remove superfluos whitespaces. --- git/objects/commit.py | 43 ++++++++++++++++++++++++++----------------- test/test_commit.py | 4 ++-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 780461a0c..bbd485da8 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,8 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import datetime -import re -from subprocess import Popen +from subprocess import Popen, PIPE from gitdb import IStream from git.util import ( hex_to_bin, @@ -14,6 +13,7 @@ finalize_process ) from git.diff import Diffable +from git.cmd import Git from .tree import Tree from . import base @@ -322,10 +322,10 @@ def trailers(self) -> Dict: Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). - - The trailer is thereby the last paragraph (seperated by a empty line - from the subject/body). This trailer paragraph must contain a ``:`` as - seperator for key and value in every line. + + This funcions calls ``git interpret-trailers --parse`` onto the message + to extract the trailer information. The key value pairs are stripped of + leading and trailing whitespaces before they get saved into a dictionary. Valid message with trailer: @@ -338,20 +338,29 @@ def trailers(self) -> Dict: another information key1: value1 - key2: value2 + key2 : value 2 with inner spaces + + dictionary will look like this: + .. code-block:: + + { + "key1": "value1", + "key2": "value 2 with inner spaces" + } :return: Dictionary containing whitespace stripped trailer information + """ - d: Dict[str, str] = {} - match = re.search(r".+^\s*$\n([\w\n\s:]+?)\s*\Z", str(self.message), re.MULTILINE | re.DOTALL) - if match is None: - return d - last_paragraph = match.group(1) - if not all(':' in line for line in last_paragraph.split('\n')): - return d - for line in last_paragraph.split('\n'): - key, value = line.split(':', 1) - d[key.strip()] = value.strip() + d = {} + cmd = ['git', 'interpret-trailers', '--parse'] + proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore + trailer: str = proc.communicate(str(self.message).encode())[0].decode() + if trailer.endswith('\n'): + trailer = trailer[0:-1] + if trailer != '': + for line in trailer.split('\n'): + key, value = line.split(':', 1) + d[key.strip()] = value.strip() return d @ classmethod diff --git a/test/test_commit.py b/test/test_commit.py index 5aeef2e6c..40cf7dd26 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -435,14 +435,14 @@ def test_trailers(self): KEY_1 = "Hello" VALUE_1 = "World" KEY_2 = "Key" - VALUE_2 = "Value" + VALUE_2 = "Value with inner spaces" # Check if KEY 1 & 2 with Value 1 & 2 is extracted from multiple msg variations msgs = [] msgs.append(f"Subject\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") msgs.append(f"Subject\n \nSome body of a function\n \n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") msgs.append(f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") - msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2}: {VALUE_2}\n") + msgs.append(f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n{KEY_1}: {VALUE_1}\n{KEY_2} : {VALUE_2}\n") for msg in msgs: commit = self.rorepo.commit('master') From 3ef81e182fcd3fca3f83216cf81d92d08c19cf5e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 09:57:33 +0800 Subject: [PATCH 0024/1175] Revert "Use NUL character to extract meta and path from git diff" This reverts commit 01f09888208341876d1480bd22dc8f4107c100f1. --- git/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index c8c57685b..cea66d7ee 100644 --- a/git/diff.py +++ b/git/diff.py @@ -509,9 +509,9 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: Union['Popen', 'Git.AutoIn def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None: lines = lines_bytes.decode(defenc) - it = iter(lines.split('\x00')) - for meta, path in zip(it, it): - meta = meta[1:] + for line in lines.split(':')[1:]: + meta, _, path = line.partition('\x00') + path = path.rstrip('\x00') a_blob_id: Optional[str] b_blob_id: Optional[str] old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) From b94cc253fe9e6355881eb299cfae8eea1a57a9c2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 7 Jan 2022 10:08:16 +0800 Subject: [PATCH 0025/1175] prep version bump --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 05f5ca23b..199eda56a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.24 +3.1.25 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 4186ac911..d955aebea 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.25 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/55?closed=1 + 3.1.24 ====== From 53d22bbc14ed871991ef169b59770a4c5b3caa19 Mon Sep 17 00:00:00 2001 From: Takuya Kitazawa Date: Sun, 9 Jan 2022 09:37:29 -0800 Subject: [PATCH 0026/1175] Fix doc string error in Objects.Commit --- git/objects/commit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index bbd485da8..07355e7e6 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -99,8 +99,7 @@ def __init__(self, repo: 'Repo', binsha: bytes, tree: Union[Tree, None] = None, :param binsha: 20 byte sha1 :param parents: tuple( Commit, ... ) is a tuple of commit ids or actual Commits - :param tree: Tree - Tree object + :param tree: Tree object :param author: Actor is the author Actor object :param authored_date: int_seconds_since_epoch @@ -341,6 +340,7 @@ def trailers(self) -> Dict: key2 : value 2 with inner spaces dictionary will look like this: + .. code-block:: { From e16a0040d07afa4ac9c0548aa742ec18ec1395a8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:01:21 +0800 Subject: [PATCH 0027/1175] Assure index file descriptor is closed after reader (#1394) (#1395) A regression that was introduced with d79d20d. --- git/index/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git/index/base.py b/git/index/base.py index d1f039cd9..7cb77f15b 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -135,7 +135,10 @@ def _set_cache_(self, attr: str) -> None: return None # END exception handling - stream = file_contents_ro(fd, stream=True, allow_mmap=True) + try: + stream = file_contents_ro(fd, stream=True, allow_mmap=True) + finally: + os.close(fd) self._deserialize(stream) else: From 851beabc93319d8dd05bff211b13d2b35ef097e0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:10:34 +0800 Subject: [PATCH 0028/1175] bump patch level --- VERSION | 2 +- doc/source/changes.rst | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 199eda56a..265be6386 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.25 +3.1.26 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index d955aebea..82106ee4f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -1,6 +1,16 @@ ========= Changelog -========= + +3.1.26 +====== + +- Fixes a leaked file descriptor when reading the index, which would cause make writing a previously + read index on windows impossible. + See https://github.com/gitpython-developers/GitPython/issues/1395 for details. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/56?closed=1 + 3.1.25 ====== From 35e302da2d9cfa8004414c2b325d194e7d77d9d9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jan 2022 21:14:51 +0800 Subject: [PATCH 0029/1175] fix documentation --- doc/source/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 82106ee4f..35fa4ced2 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -1,5 +1,6 @@ ========= Changelog +========= 3.1.26 ====== From e24f9b70209eb6681f055596846033f7d3215ea5 Mon Sep 17 00:00:00 2001 From: wonder-mice Date: Tue, 11 Jan 2022 00:41:03 -0800 Subject: [PATCH 0030/1175] import unittest adds 0.250s to script launch time This should not be imported at root level, since it adds a lot of initialization overhead without need. --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index b81332ea4..6e6f09557 100644 --- a/git/util.py +++ b/git/util.py @@ -20,7 +20,6 @@ import stat from sys import maxsize import time -from unittest import SkipTest from urllib.parse import urlsplit, urlunsplit import warnings @@ -130,6 +129,7 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None: func(path) # Will scream if still not possible to delete. except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex raise From 67d0631e54c44f4523d3b308040e6a0643b6396d Mon Sep 17 00:00:00 2001 From: wonder-mice Date: Tue, 11 Jan 2022 00:44:25 -0800 Subject: [PATCH 0031/1175] import unittest adds 0.250s to script launch time This should not be imported at root level, since it adds a lot of initialization overhead without need. --- git/objects/submodule/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index d306c91d4..f78204555 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,8 +3,6 @@ import logging import os import stat - -from unittest import SkipTest import uuid import git @@ -934,6 +932,7 @@ def remove(self, module: bool = True, force: bool = False, rmtree(str(wtd)) except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest("FIXME: fails with: PermissionError\n {}".format(ex)) from ex raise # END delete tree if possible @@ -945,6 +944,7 @@ def remove(self, module: bool = True, force: bool = False, rmtree(git_dir) except Exception as ex: if HIDE_WINDOWS_KNOWN_ERRORS: + from unittest import SkipTest raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex else: raise From fac603789d66c0fd7c26e75debb41b06136c5026 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 12 Jan 2022 08:25:26 +0800 Subject: [PATCH 0032/1175] keep track of upcoming changes --- doc/source/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 35fa4ced2..ced8f8584 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ========= +3.1.27 +====== + +- Reduced startup time due to optimized imports. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/57?closed=1 + 3.1.26 ====== From b719e1809c2c81283e930086faebd7d6050cd5d7 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 12 Jan 2022 23:39:10 -0800 Subject: [PATCH 0033/1175] Use bash to open extensionless hooks on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #971. Partly resolve #703. If the hook doesn't have a file extension, then Windows won't know how to run it and you'll get "[WinError 193] %1 is not a valid Win32 application". It's very likely that it's a shell script of some kind, so use bash.exe (commonly installed via Windows Subsystem for Linux). We don't want to run all hooks with bash because they could be .bat files. Update tests to get several hook ones working. More work necessary to get commit-msg hook working. The hook writes to the wrong file because it's not using forward slashes in the path: C:\Users\idbrii\AppData\Local\Temp\bare_test_commit_msg_hook_successy5fo00du\CUsersidbriiAppDataLocalTempbare_test_commit_msg_hook_successy5fo00duCOMMIT_EDITMSG --- git/index/fun.py | 15 ++++++++++++++- test/test_index.py | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index 16ec744e2..59fa1be19 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -3,6 +3,7 @@ # NOTE: Autodoc hates it if this is a docstring from io import BytesIO +from pathlib import Path import os from stat import ( S_IFDIR, @@ -21,6 +22,7 @@ force_text, force_bytes, is_posix, + is_win, safe_decode, ) from git.exc import ( @@ -76,6 +78,10 @@ def hook_path(name: str, git_dir: PathLike) -> str: return osp.join(git_dir, 'hooks', name) +def _has_file_extension(path): + return osp.splitext(path)[1] + + def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: """Run the commit hook of the given name. Silently ignores hooks that do not exist. :param name: name of hook, like 'pre-commit' @@ -89,8 +95,15 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: env = os.environ.copy() env['GIT_INDEX_FILE'] = safe_decode(str(index.path)) env['GIT_EDITOR'] = ':' + cmd = [hp] try: - cmd = subprocess.Popen([hp] + list(args), + if is_win and not _has_file_extension(hp): + # Windows only uses extensions to determine how to open files + # (doesn't understand shebangs). Try using bash to run the hook. + relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix() + cmd = ["bash.exe", relative_hp] + + cmd = subprocess.Popen(cmd + list(args), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/test/test_index.py b/test/test_index.py index 02cb4e813..233a4c643 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -13,6 +13,7 @@ ) import tempfile from unittest import skipIf +import shutil from git import ( IndexFile, @@ -52,8 +53,9 @@ HOOKS_SHEBANG = "#!/usr/bin/env sh\n" +is_win_without_bash = is_win and not shutil.which('bash.exe') + -@skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703") def _make_hook(git_dir, name, content, make_exec=True): """A helper to create a hook""" hp = hook_path(name, git_dir) @@ -881,7 +883,7 @@ def test_pre_commit_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win: + if is_win_without_bash: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, '') @@ -896,6 +898,7 @@ def test_pre_commit_hook_fail(self, rw_repo): else: raise AssertionError("Should have caught a HookExecutionError") + @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703") @with_rw_repo('HEAD', bare=True) def test_commit_msg_hook_success(self, rw_repo): commit_message = "commit default head by Frèderic Çaufl€" @@ -920,7 +923,7 @@ def test_commit_msg_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win: + if is_win_without_bash: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, '') From b3f873a1458223c075fdde6c85eb656648bcdcae Mon Sep 17 00:00:00 2001 From: smokephil Date: Fri, 21 Jan 2022 09:43:40 +0100 Subject: [PATCH 0034/1175] set unassigned stdin to improve pyinstaller compatibility To create a window application with pyinstaller, all suprocess input and output streams must be assigned and must not be None. https://stackoverflow.com/a/51706087/7076612 --- git/cmd.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 8fb10742f..4f0569879 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -12,7 +12,8 @@ from subprocess import ( call, Popen, - PIPE + PIPE, + DEVNULL ) import subprocess import threading @@ -873,7 +874,7 @@ def execute(self, env=env, cwd=cwd, bufsize=-1, - stdin=istream, + stdin=istream or DEVNULL, stderr=PIPE, stdout=stdout_sink, shell=shell is not None and shell or self.USE_SHELL, From cd29f07b2efda24bdc690626ed557590289d11a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 3 Feb 2022 15:34:10 +0800 Subject: [PATCH 0035/1175] Let index.commit refer to correct method for parameter information (#1407) --- git/index/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/index/base.py b/git/index/base.py index 7cb77f15b..209bfa8de 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -974,7 +974,7 @@ def commit(self, commit_date: Union[str, None] = None, skip_hooks: bool = False) -> Commit: """Commit the current default index file, creating a commit object. - For more information on the arguments, see tree.commit. + For more information on the arguments, see Commit.create_from_tree(). :note: If you have manually altered the .entries member of this instance, don't forget to write() your changes to disk beforehand. From d0b48f3f4888d69a7b59024114bff897f24561b2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 12 Feb 2022 11:55:57 +0800 Subject: [PATCH 0036/1175] Create SECURITY.md --- SECURITY.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cf25c09ea --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +Only the latest version of GitPython can receive security updates. If a vulnerability is discovered, a fix can be issued in a new release, while older releases +are likely to be yanked. + +| Version | Supported | +| ------- | ------------------ | +| 3.x.x | :white_check_mark: | +| < 3.0 | :x: | + +## Reporting a Vulnerability + +Please report private portions of a vulnerability to sebastian.thiel@icloud.com that would help to reproduce and fix it. To receive updates on progress and provide +general information to the public, you can create an issue [on the issue tracker](https://github.com/gitpython-developers/GitPython/issues). From 75f4f63ab3856a552f06082aabf98845b5fa21e3 Mon Sep 17 00:00:00 2001 From: theworstcomrade <4lbercik@gmail.com> Date: Fri, 18 Feb 2022 16:28:03 +0100 Subject: [PATCH 0037/1175] Low risk ReDoS vuln https://huntr.dev/bounties/8549d81f-dc45-4af7-9f2a-2d70752d8524/ --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 7d5918a5a..56f3c5b33 100644 --- a/git/remote.py +++ b/git/remote.py @@ -273,7 +273,7 @@ class FetchInfo(IterableObj, object): NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ FAST_FORWARD, ERROR = [1 << x for x in range(8)] - _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') + _re_fetch_result = re.compile(r'^\s*(.) (\[[\w\s\.$@]+\]|[\w\.$@]+)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') _flag_map: Dict[flagKeyLiteral, int] = { '!': ERROR, From 65346820b81e0de7f32369ba5773004df082b793 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 09:12:39 +0800 Subject: [PATCH 0038/1175] update changelog --- doc/source/changes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index ced8f8584..f9717438d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,15 @@ Changelog ========= +3.1.28 +====== + +- Fix a vulenerability that could cause great slowdowns when encountering long remote path names + when pulling/fetching. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/58?closed=1 + 3.1.27 ====== From d438e088278f2df10b3c38bd635d7207cb7548a6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 09:14:20 +0800 Subject: [PATCH 0039/1175] bump patch level --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 265be6386..054a6481f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.26 +3.1.27 From 02b594ecdb3ba36e8477e2ff1dcb065c8626ca3d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 20 Feb 2022 22:01:27 +0800 Subject: [PATCH 0040/1175] fix changelog --- doc/source/changes.rst | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index f9717438d..3f22a4866 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,19 +2,12 @@ Changelog ========= -3.1.28 -====== - -- Fix a vulenerability that could cause great slowdowns when encountering long remote path names - when pulling/fetching. - -See the following for all changes. -https://github.com/gitpython-developers/gitpython/milestone/58?closed=1 - 3.1.27 ====== - Reduced startup time due to optimized imports. +- Fix a vulenerability that could cause great slowdowns when encountering long remote path names + when pulling/fetching. See the following for all changes. https://github.com/gitpython-developers/gitpython/milestone/57?closed=1 From c0f2cf373e8296d07b3a7d7610add0cf3d5957be Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 21 Feb 2022 10:42:25 +0800 Subject: [PATCH 0041/1175] Deprecate GPG signature docs; stop signing releases Related to https://github.com/gitpython-developers/gitdb/issues/77 --- Makefile | 2 +- README.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fe82a694b..9054de2b8 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ release: clean force_release: clean git push --tags origin main python3 setup.py sdist bdist_wheel - twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* \ No newline at end of file + twine upload 27C50E7F590947D7273A741E85194C08421980C9 dist/* \ No newline at end of file diff --git a/README.md b/README.md index dd449d32f..54a735e53 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,12 @@ Please have a look at the [contributions file][contributing]. incrementing the patch level, and possibly by appending `-dev`. Probably you want to `git push` once more. -### How to verify a release +### How to verify a release (DEPRECATED) + +Note that what follows is deprecated and future releases won't be signed anymore. +More details about how it came to that can be found [in this issue](https://github.com/gitpython-developers/gitdb/issues/77). + +---- Please only use releases from `pypi` as you can verify the respective source tarballs. From 56f18ac6d9efc12d0aa9406a0b28c82fcf73aca5 Mon Sep 17 00:00:00 2001 From: Houssam Kherraz Date: Wed, 23 Feb 2022 10:20:19 -0500 Subject: [PATCH 0042/1175] fix iter_commits comment, more in line with iter_items --- git/repo/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index 7713c9152..510eb12bf 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -567,8 +567,8 @@ def iter_commits(self, rev: Union[str, Commit, 'SymbolicReference', None] = None If None, the active branch will be used. :param paths: - is an optional path or a list of paths to limit the returned commits to - Commits that do not contain that path or the paths will not be returned. + is an optional path or a list of paths; if set only commits that include the path + or paths will be returned :param kwargs: Arguments to be passed to git-rev-list - common ones are From c0740570b31f0f0fe499bf4fc5abbf89feb1757d Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 23 Feb 2022 17:03:30 -0500 Subject: [PATCH 0043/1175] fix typos --- doc/source/tutorial.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index bc386e7c4..fcbc18bff 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -66,7 +66,7 @@ Archive the repository contents to a tar file. Advanced Repo Usage =================== -And of course, there is much more you can do with this type, most of the following will be explained in greater detail in specific tutorials. Don't worry if you don't understand some of these examples right away, as they may require a thorough understanding of gits inner workings. +And of course, there is much more you can do with this type, most of the following will be explained in greater detail in specific tutorials. Don't worry if you don't understand some of these examples right away, as they may require a thorough understanding of git's inner workings. Query relevant repository paths ... @@ -363,7 +363,7 @@ Handling Remotes :start-after: # [25-test_references_and_objects] :end-before: # ![25-test_references_and_objects] -You can easily access configuration information for a remote by accessing options as if they where attributes. The modification of remote configuration is more explicit though. +You can easily access configuration information for a remote by accessing options as if they were attributes. The modification of remote configuration is more explicit though. .. literalinclude:: ../../test/test_docs.py :language: python @@ -391,7 +391,7 @@ Here's an example executable that can be used in place of the `ssh_executable` a ID_RSA=/var/lib/openshift/5562b947ecdd5ce939000038/app-deployments/id_rsa exec /usr/bin/ssh -o StrictHostKeyChecking=no -i $ID_RSA "$@" -Please note that the script must be executable (i.e. `chomd +x script.sh`). `StrictHostKeyChecking=no` is used to avoid prompts asking to save the hosts key to `~/.ssh/known_hosts`, which happens in case you run this as daemon. +Please note that the script must be executable (i.e. `chmod +x script.sh`). `StrictHostKeyChecking=no` is used to avoid prompts asking to save the hosts key to `~/.ssh/known_hosts`, which happens in case you run this as daemon. You might also have a look at `Git.update_environment(...)` in case you want to setup a changed environment more permanently. @@ -509,14 +509,14 @@ The type of the database determines certain performance characteristics, such as GitDB ===== -The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. Its uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities small of objects from densely packed repositories:: +The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. It uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities of small objects from densely packed repositories:: repo = Repo("path/to/repo", odbt=GitDB) GitCmdObjectDB ============== -The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than the one of the ``GitDB``:: +The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than ``GitDB``:: repo = Repo("path/to/repo", odbt=GitCmdObjectDB) From b85c2594f31179e135af893d82868e7742464fe6 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 21 Mar 2022 19:19:32 +0300 Subject: [PATCH 0044/1175] Fixed setting ref with non-ascii in path --- git/refs/symbolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 0c0fa4045..1c5506737 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -352,7 +352,7 @@ def set_reference(self, ref: Union[Commit_ish, 'SymbolicReference', str], fd = lfd.open(write=True, stream=True) ok = True try: - fd.write(write_value.encode('ascii') + b'\n') + fd.write(write_value.encode('utf-8') + b'\n') lfd.commit() ok = True finally: From 0b33576f8e7add5671f8927dff228e7f92eec076 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 1 Apr 2022 15:28:02 +0100 Subject: [PATCH 0045/1175] Allow `repo.create_head`'s `commit` arg to be a `SymbolicReference` This matches the signature from `Head.create`. --- git/repo/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 510eb12bf..f8bc8128e 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -420,7 +420,8 @@ def _to_full_tag_path(path: PathLike) -> str: else: return TagReference._common_path_default + '/' + path_str - def create_head(self, path: PathLike, commit: str = 'HEAD', + def create_head(self, path: PathLike, + commit: Union['SymbolicReference', 'str'] = 'HEAD', force: bool = False, logmsg: Optional[str] = None ) -> 'Head': """Create a new head within the repository. From e4360aea32aad11bf3c54b0dc0a6cabb21b5e687 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Wed, 6 Apr 2022 23:08:36 +0900 Subject: [PATCH 0046/1175] feat(cmd): add the `strip_newline` flag This commit adds the `strip_newline` flag to the `Git.execute` method. When this flag is set to `True`, it will trim the trailing `\n`. The default value is `True` for backward compatibility. Setting it to `False` is helpful for, e.g., the `git show` output, especially with the binary file, as the missing `\n` may invalidate the file. --- git/cmd.py | 7 +++++-- test/test_repo.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 4f0569879..9c5da89dc 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -55,7 +55,7 @@ execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size'} + 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline'} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -738,6 +738,7 @@ def execute(self, shell: Union[None, bool] = None, env: Union[None, Mapping[str, str]] = None, max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + strip_newline: bool = True, **subprocess_kwargs: Any ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns @@ -810,6 +811,8 @@ def execute(self, effects on a repository. For example, stale locks in case of git gc could render the repository incapable of accepting changes until the lock is manually removed. + :param strip_newline: + Whether to strip the trailing '\n' of the command output. :return: * str(output) if extended_output = False (Default) @@ -944,7 +947,7 @@ def _kill_process(pid: int) -> None: if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" - if stdout_value.endswith(newline): # type: ignore + if stdout_value.endswith(newline) and strip_newline: # type: ignore stdout_value = stdout_value[:-1] if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] diff --git a/test/test_repo.py b/test/test_repo.py index 6d6176090..14339f57f 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1098,3 +1098,13 @@ def test_rebasing(self, rw_dir): except GitCommandError: pass self.assertEqual(r.currently_rebasing_on(), commitSpanish) + + @with_rw_directory + def test_do_not_strip_newline(self, rw_dir): + r = Repo.init(rw_dir) + fp = osp.join(rw_dir, 'hello.txt') + with open(fp, 'w') as fs: + fs.write("hello\n") + r.git.add(Git.polish_url(https://github.com/fp)) + r.git.commit(message="init") + self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline=False), 'hello\n') From 946b64b62bdc9fb3447b6daf0053b11a2e4c5277 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Wed, 6 Apr 2022 23:23:42 +0900 Subject: [PATCH 0047/1175] chore: add me to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 55d681813..546818f5f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,4 +45,5 @@ Contributors are: -Alba Mendez -Robert Westman -Hugo van Kemenade +-Hiroki Tokunaga Portions derived from other open source works and are clearly marked. From 49150e79c6f7a19a0d61a5ea6864b9ac140264ff Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:04:19 +0900 Subject: [PATCH 0048/1175] chore: `s/strip_newline/&_in_stdout` --- git/cmd.py | 10 +++++----- test/test_repo.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 9c5da89dc..228b9d382 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -55,7 +55,7 @@ execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline'} + 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline_in_stdout'} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -738,7 +738,7 @@ def execute(self, shell: Union[None, bool] = None, env: Union[None, Mapping[str, str]] = None, max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, - strip_newline: bool = True, + strip_newline_in_stdout: bool = True, **subprocess_kwargs: Any ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns @@ -811,8 +811,8 @@ def execute(self, effects on a repository. For example, stale locks in case of git gc could render the repository incapable of accepting changes until the lock is manually removed. - :param strip_newline: - Whether to strip the trailing '\n' of the command output. + :param strip_newline_in_stdout: + Whether to strip the trailing '\n' of the command stdout. :return: * str(output) if extended_output = False (Default) @@ -947,7 +947,7 @@ def _kill_process(pid: int) -> None: if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" - if stdout_value.endswith(newline) and strip_newline: # type: ignore + if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore stdout_value = stdout_value[:-1] if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] diff --git a/test/test_repo.py b/test/test_repo.py index 14339f57f..c5b2680d0 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1100,11 +1100,11 @@ def test_rebasing(self, rw_dir): self.assertEqual(r.currently_rebasing_on(), commitSpanish) @with_rw_directory - def test_do_not_strip_newline(self, rw_dir): + def test_do_not_strip_newline_in_stdout(self, rw_dir): r = Repo.init(rw_dir) fp = osp.join(rw_dir, 'hello.txt') with open(fp, 'w') as fs: fs.write("hello\n") r.git.add(Git.polish_url(https://github.com/fp)) r.git.commit(message="init") - self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline=False), 'hello\n') + self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), 'hello\n') From 2a50f28fa3571e3d2c4d5ea86f4243f715717269 Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:11:28 +0900 Subject: [PATCH 0049/1175] docs: escape with backticks --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 228b9d382..fe161309b 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -812,7 +812,7 @@ def execute(self, render the repository incapable of accepting changes until the lock is manually removed. :param strip_newline_in_stdout: - Whether to strip the trailing '\n' of the command stdout. + Whether to strip the trailing `\n` of the command stdout. :return: * str(output) if extended_output = False (Default) From 17b2b128fb6d6f987b47d60ccb1ab09b8fc238ea Mon Sep 17 00:00:00 2001 From: Hiroki Tokunaga Date: Thu, 7 Apr 2022 10:20:59 +0900 Subject: [PATCH 0050/1175] fix(docs): remove an unexpected blank line --- git/cmd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index fe161309b..1ddf9e03f 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -813,7 +813,6 @@ def execute(self, removed. :param strip_newline_in_stdout: Whether to strip the trailing `\n` of the command stdout. - :return: * str(output) if extended_output = False (Default) * tuple(int(status), str(stdout), str(stderr)) if extended_output = True From 85fe2735b7c9119804813bcbbdd8d14018291ed3 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 4 May 2022 12:48:09 -0400 Subject: [PATCH 0051/1175] Fix #1284: strip usernames from URLs as well as passwords --- git/exc.py | 7 ++++--- git/util.py | 20 +++++++++++++------- test/test_exc.py | 9 ++++++--- test/test_util.py | 30 +++++++++++++++++++++++------- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/git/exc.py b/git/exc.py index e8ff784c7..045ea9d27 100644 --- a/git/exc.py +++ b/git/exc.py @@ -8,6 +8,7 @@ from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode +from git.util import remove_password_if_present # typing ---------------------------------------------------- @@ -54,7 +55,7 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], stdout: Union[bytes, str, None] = None) -> None: if not isinstance(command, (tuple, list)): command = command.split() - self.command = command + self.command = remove_password_if_present(command) self.status = status if status: if isinstance(status, Exception): @@ -66,8 +67,8 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], s = safe_decode(str(status)) status = "'%s'" % s if isinstance(status, str) else s - self._cmd = safe_decode(command[0]) - self._cmdline = ' '.join(safe_decode(i) for i in command) + self._cmd = safe_decode(self.command[0]) + self._cmdline = ' '.join(safe_decode(i) for i in self.command) self._cause = status and " due to: %s" % status or "!" stdout_decode = safe_decode(stdout) stderr_decode = safe_decode(stderr) diff --git a/git/util.py b/git/util.py index 6e6f09557..0711265a6 100644 --- a/git/util.py +++ b/git/util.py @@ -5,7 +5,6 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php from abc import abstractmethod -from .exc import InvalidGitRepositoryError import os.path as osp from .compat import is_win import contextlib @@ -94,6 +93,8 @@ def unbare_repo(func: Callable[..., T]) -> Callable[..., T]: """Methods with this decorator raise InvalidGitRepositoryError if they encounter a bare repository""" + from .exc import InvalidGitRepositoryError + @wraps(func) def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> T: if self.repo.bare: @@ -412,11 +413,12 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: """ Parse any command line argument and if on of the element is an URL with a - password, replace it by stars (in-place). + username and/or password, replace them by stars (in-place). If nothing found just returns the command line as-is. - This should be used for every log line that print a command line. + This should be used for every log line that print a command line, as well as + exception messages. """ new_cmdline = [] for index, to_parse in enumerate(cmdline): @@ -424,12 +426,16 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: try: url = urlsplit(to_parse) # Remove password from the URL if present - if url.password is None: + if url.password is None and url.username is None: continue - edited_url = url._replace( - netloc=url.netloc.replace(url.password, "*****")) - new_cmdline[index] = urlunsplit(edited_url) + if url.password is not None: + url = url._replace( + netloc=url.netloc.replace(url.password, "*****")) + if url.username is not None: + url = url._replace( + netloc=url.netloc.replace(url.username, "*****")) + new_cmdline[index] = urlunsplit(url) except ValueError: # This is not a valid URL continue diff --git a/test/test_exc.py b/test/test_exc.py index f16498ab5..c77be7824 100644 --- a/test/test_exc.py +++ b/test/test_exc.py @@ -22,6 +22,7 @@ HookExecutionError, RepositoryDirtyError, ) +from git.util import remove_password_if_present from test.lib import TestBase import itertools as itt @@ -34,6 +35,7 @@ ('cmd', 'ελληνικα', 'args'), ('θνιψοδε', 'κι', 'αλλα', 'strange', 'args'), ('θνιψοδε', 'κι', 'αλλα', 'non-unicode', 'args'), + ('git', 'clone', '-v', 'https://fakeuser:fakepassword1234@fakerepo.example.com/testrepo'), ) _causes_n_substrings = ( (None, None), # noqa: E241 @IgnorePep8 @@ -81,7 +83,7 @@ def test_CommandError_unicode(self, case): self.assertIsNotNone(c._msg) self.assertIn(' cmdline: ', s) - for a in argv: + for a in remove_password_if_present(argv): self.assertIn(a, s) if not cause: @@ -137,14 +139,15 @@ def test_GitCommandNotFound(self, init_args): @ddt.data( (['cmd1'], None), (['cmd1'], "some cause"), - (['cmd1'], Exception()), + (['cmd1', 'https://fakeuser@fakerepo.example.com/testrepo'], Exception()), ) def test_GitCommandError(self, init_args): argv, cause = init_args c = GitCommandError(argv, cause) s = str(c) - self.assertIn(argv[0], s) + for arg in remove_password_if_present(argv): + self.assertIn(arg, s) if cause: self.assertIn(' failed due to: ', s) self.assertIn(str(cause), s) diff --git a/test/test_util.py b/test/test_util.py index 3961ff356..a213b46c9 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -343,18 +343,34 @@ def test_pickle_tzoffset(self): self.assertEqual(t1._name, t2._name) def test_remove_password_from_command_line(self): + username = "fakeuser" password = "fakepassword1234" - url_with_pass = "https://fakeuser:%7B%7D@fakerepo.example.com/testrepo".format(password) - url_without_pass = "https://fakerepo.example.com/testrepo" + url_with_user_and_pass = "https://%7B%7D:%7B%7D@fakerepo.example.com/testrepo".format(username, password) + url_with_user = "https://%7B%7D@fakerepo.example.com/testrepo".format(username) + url_with_pass = "https://:%7B%7D@fakerepo.example.com/testrepo".format(password) + url_without_user_or_pass = "https://fakerepo.example.com/testrepo" - cmd_1 = ["git", "clone", "-v", url_with_pass] - cmd_2 = ["git", "clone", "-v", url_without_pass] - cmd_3 = ["no", "url", "in", "this", "one"] + cmd_1 = ["git", "clone", "-v", url_with_user_and_pass] + cmd_2 = ["git", "clone", "-v", url_with_user] + cmd_3 = ["git", "clone", "-v", url_with_pass] + cmd_4 = ["git", "clone", "-v", url_without_user_or_pass] + cmd_5 = ["no", "url", "in", "this", "one"] redacted_cmd_1 = remove_password_if_present(cmd_1) + assert username not in " ".join(redacted_cmd_1) assert password not in " ".join(redacted_cmd_1) # Check that we use a copy assert cmd_1 is not redacted_cmd_1 + assert username in " ".join(cmd_1) assert password in " ".join(cmd_1) - assert cmd_2 == remove_password_if_present(cmd_2) - assert cmd_3 == remove_password_if_present(cmd_3) + + redacted_cmd_2 = remove_password_if_present(cmd_2) + assert username not in " ".join(redacted_cmd_2) + assert password not in " ".join(redacted_cmd_2) + + redacted_cmd_3 = remove_password_if_present(cmd_3) + assert username not in " ".join(redacted_cmd_3) + assert password not in " ".join(redacted_cmd_3) + + assert cmd_4 == remove_password_if_present(cmd_4) + assert cmd_5 == remove_password_if_present(cmd_5) From dde3a8bd9229ff25ec8bc03c35d937f43233f48e Mon Sep 17 00:00:00 2001 From: luz paz Date: Sat, 7 May 2022 15:59:10 -0400 Subject: [PATCH 0052/1175] Fix various typos Found via `codespell -q 3 -S ./git/ext/gitdb,./test/fixtures/reflog_master,./test/fixtures/diff_mode_only,./test/fixtures/reflog_HEAD` --- doc/source/changes.rst | 6 +++--- git/config.py | 2 +- git/index/base.py | 8 ++++---- git/index/fun.py | 2 +- git/objects/base.py | 2 +- git/objects/commit.py | 4 ++-- git/objects/submodule/root.py | 2 +- git/objects/util.py | 4 ++-- git/refs/symbolic.py | 2 +- git/refs/tag.py | 4 ++-- git/repo/base.py | 4 ++-- git/repo/fun.py | 2 +- git/types.py | 2 +- pyproject.toml | 2 +- test/fixtures/diff_p | 2 +- test/fixtures/git_config | 2 +- test/fixtures/rev_list_bisect_all | 2 +- test/test_config.py | 2 +- test/test_diff.py | 2 +- test/test_docs.py | 2 +- test/test_git.py | 2 +- test/test_index.py | 2 +- test/test_submodule.py | 4 ++-- 23 files changed, 33 insertions(+), 33 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 3f22a4866..f37c81677 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -69,7 +69,7 @@ https://github.com/gitpython-developers/gitpython/milestone/53?closed=1 - Make Protocol classes ABCs at runtime due to new behaviour/bug in 3.9.7 & 3.10.0-rc1 - - Remove use of typing.TypeGuard until later release, to allow dependant libs time to update. + - Remove use of typing.TypeGuard until later release, to allow dependent libs time to update. - Tracking issue: https://github.com/gitpython-developers/GitPython/issues/1095 @@ -134,7 +134,7 @@ https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 3.1.15 (YANKED) =============== -* add deprectation warning for python 3.5 +* add deprecation warning for python 3.5 See the following for details: https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 @@ -595,7 +595,7 @@ It follows the `semantic version scheme `_, and thus will not - Renamed `ignore_tree_extension_data` keyword argument in `IndexFile.write(...)` to `ignore_extension_data` * If the git command executed during `Remote.push(...)|fetch(...)` returns with an non-zero exit code and GitPython didn't obtain any head-information, the corresponding `GitCommandError` will be raised. This may break previous code which expected - these operations to never raise. However, that behavious is undesirable as it would effectively hide the fact that there + these operations to never raise. However, that behaviour is undesirable as it would effectively hide the fact that there was an error. See `this issue `__ for more information. * If the git executable can't be found in the PATH or at the path provided by `GIT_PYTHON_GIT_EXECUTABLE`, this is made diff --git a/git/config.py b/git/config.py index cbd66022d..1ac3c9cec 100644 --- a/git/config.py +++ b/git/config.py @@ -71,7 +71,7 @@ class MetaParserBuilder(abc.ABCMeta): - """Utlity class wrapping base-class methods into decorators that assure read-only properties""" + """Utility class wrapping base-class methods into decorators that assure read-only properties""" def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': """ Equip all base-class methods with a needs_values decorator, and all non-const methods diff --git a/git/index/base.py b/git/index/base.py index 209bfa8de..00e51bf5e 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -579,7 +579,7 @@ def _process_diff_args(self, # type: ignore[override] def _to_relative_path(self, path: PathLike) -> PathLike: """ :return: Version of path relative to our git directory or raise ValueError - if it is not within our git direcotory""" + if it is not within our git directory""" if not osp.isabs(path): return path if self.repo.bare: @@ -682,7 +682,7 @@ def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule'] into the object database. PathStrings may contain globs, such as 'lib/__init__*' or can be directories - like 'lib', the latter ones will add all the files within the dirctory and + like 'lib', the latter ones will add all the files within the directory and subdirectories. This equals a straight git-add. @@ -779,7 +779,7 @@ def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule'] "At least one Entry has a null-mode - please use index.remove to remove files for clarity") # END null mode should be remove - # HANLDE ENTRY OBJECT CREATION + # HANDLE ENTRY OBJECT CREATION # create objects if required, otherwise go with the existing shas null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA] if null_entries_indices: @@ -813,7 +813,7 @@ def handle_null_entries(self: 'IndexFile') -> None: fprogress(entry.path, False, entry) fprogress(entry.path, True, entry) # END handle progress - # END for each enty + # END for each entry entries_added.extend(entries) # END if there are base entries diff --git a/git/index/fun.py b/git/index/fun.py index 59fa1be19..acab74239 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -314,7 +314,7 @@ def write_tree_from_cache(entries: List[IndexEntry], odb: 'GitCmdObjectDB', sl: # finally create the tree sio = BytesIO() - tree_to_stream(tree_items, sio.write) # writes to stream as bytes, but doesnt change tree_items + tree_to_stream(tree_items, sio.write) # writes to stream as bytes, but doesn't change tree_items sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) diff --git a/git/objects/base.py b/git/objects/base.py index a3b0f230a..66e15a8f5 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -32,7 +32,7 @@ # -------------------------------------------------------------------------- -_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutual git object type %r" +_assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r" __all__ = ("Object", "IndexObject") diff --git a/git/objects/commit.py b/git/objects/commit.py index 07355e7e6..96a2a8e59 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -322,7 +322,7 @@ def trailers(self) -> Dict: Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). - This funcions calls ``git interpret-trailers --parse`` onto the message + This functions calls ``git interpret-trailers --parse`` onto the message to extract the trailer information. The key value pairs are stripped of leading and trailing whitespaces before they get saved into a dictionary. @@ -461,7 +461,7 @@ def create_from_tree(cls, repo: 'Repo', tree: Union[Tree, str], message: str, # * Environment variables override configuration values # * Sensible defaults are set according to the git documentation - # COMMITER AND AUTHOR INFO + # COMMITTER AND AUTHOR INFO cr = repo.config_reader() env = os.environ diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py index 5e84d1616..08e1f9543 100644 --- a/git/objects/submodule/root.py +++ b/git/objects/submodule/root.py @@ -338,7 +338,7 @@ def update(self, previous_commit: Union[Commit_ish, None] = None, sm.update(recursive=False, init=init, to_latest_revision=to_latest_revision, progress=progress, dry_run=dry_run, force=force_reset, keep_going=keep_going) - # update recursively depth first - question is which inconsitent + # update recursively depth first - question is which inconsistent # state will be better in case it fails somewhere. Defective branch # or defective depth. The RootSubmodule type will never process itself, # which was done in the previous expression diff --git a/git/objects/util.py b/git/objects/util.py index 187318fe6..800eccdf4 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -37,7 +37,7 @@ from .submodule.base import Submodule from git.types import Protocol, runtime_checkable else: - # Protocol = Generic[_T] # NNeeded for typing bug #572? + # Protocol = Generic[_T] # Needed for typing bug #572? Protocol = ABC def runtime_checkable(f): @@ -359,7 +359,7 @@ def _list_traverse(self, as_edge: bool = False, *args: Any, **kwargs: Any out: IterableList[Union['Commit', 'Submodule', 'Tree', 'Blob']] = IterableList(id) out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) return out - # overloads in subclasses (mypy does't allow typing self: subclass) + # overloads in subclasses (mypy doesn't allow typing self: subclass) # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]] else: # Raise deprecationwarning, doesn't make sense to use this diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 1c5506737..8d869173e 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -298,7 +298,7 @@ def set_reference(self, ref: Union[Commit_ish, 'SymbolicReference', str], logmsg: Union[str, None] = None) -> 'SymbolicReference': """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. Otherwise an Object, given as Object instance or refspec, is assumed and if valid, - will be set which effectively detaches the refererence if it was a purely + will be set which effectively detaches the reference if it was a purely symbolic one. :param ref: SymbolicReference instance, Object instance or refspec string diff --git a/git/refs/tag.py b/git/refs/tag.py index edfab33d8..8cc79eddd 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -36,7 +36,7 @@ class TagReference(Reference): _common_path_default = Reference._common_path_default + "/" + _common_default @property - def commit(self) -> 'Commit': # type: ignore[override] # LazyMixin has unrelated comit method + def commit(self) -> 'Commit': # type: ignore[override] # LazyMixin has unrelated commit method """:return: Commit object the tag ref points to :raise ValueError: if the tag points to a tree or blob""" @@ -91,7 +91,7 @@ def create(cls: Type['TagReference'], repo: 'Repo', path: PathLike, :param message: Synonym for :param logmsg: - Included for backwards compatability. :param logmsg is used in preference if both given. + Included for backwards compatibility. :param logmsg is used in preference if both given. :param force: If True, to force creation of a tag even though that tag already exists. diff --git a/git/repo/base.py b/git/repo/base.py index f8bc8128e..bea0dcb57 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -711,7 +711,7 @@ def is_dirty(self, index: bool = True, working_tree: bool = True, untracked_file index or the working copy have changes.""" if self._bare: # Bare repositories with no associated working directory are - # always consired to be clean. + # always considered to be clean. return False # start from the one which is fastest to evaluate @@ -760,7 +760,7 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: untracked_files=True, as_process=True, **kwargs) - # Untracked files preffix in porcelain mode + # Untracked files prefix in porcelain mode prefix = "?? " untracked_files = [] for line in proc.stdout: diff --git a/git/repo/fun.py b/git/repo/fun.py index 1a83dd3dc..74c0657d6 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -266,7 +266,7 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: # END handle tag elif token == '@': # try single int - assert ref is not None, "Requre Reference to access reflog" + assert ref is not None, "Require Reference to access reflog" revlog_index = None try: # transform reversed index into the format of our revlog diff --git a/git/types.py b/git/types.py index 64bf3d96d..7f44ba242 100644 --- a/git/types.py +++ b/git/types.py @@ -54,7 +54,7 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None: """For use in exhaustive checking of literal or Enum in if/else chain. - Should only be reached if all memebers not handled OR attempt to pass non-members through chain. + Should only be reached if all members not handled OR attempt to pass non-members through chain. If all members handled, type is Empty. Otherwise, will cause mypy error. If non-members given, should cause mypy error at variable creation. diff --git a/pyproject.toml b/pyproject.toml index 102b6fdc4..da3e605ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] python_files = 'test_*.py' -testpaths = 'test' # space seperated list of paths from root e.g test tests doc/testing +testpaths = 'test' # space separated list of paths from root e.g test tests doc/testing addopts = '--cov=git --cov-report=term --maxfail=10 --force-sugar --disable-warnings' filterwarnings = 'ignore::DeprecationWarning' # --cov coverage diff --git a/test/fixtures/diff_p b/test/fixtures/diff_p index af4759e50..76242b58c 100644 --- a/test/fixtures/diff_p +++ b/test/fixtures/diff_p @@ -397,7 +397,7 @@ index 1d5251d40fb65ac89184ec662a3e1b04d0c24861..98eeddda5ed2b0e215e21128112393bd self.git_dir = git_dir end -- # Converstion hash from Ruby style options to git command line +- # Conversion hash from Ruby style options to git command line - # style options - TRANSFORM = {:max_count => "--max-count=", - :skip => "--skip=", diff --git a/test/fixtures/git_config b/test/fixtures/git_config index b8c178e3f..a8cad56e8 100644 --- a/test/fixtures/git_config +++ b/test/fixtures/git_config @@ -28,7 +28,7 @@ [branch "mainline_performance"] remote = mainline merge = refs/heads/master -# section with value defined before include to be overriden +# section with value defined before include to be overridden [sec] var0 = value0_main [include] diff --git a/test/fixtures/rev_list_bisect_all b/test/fixtures/rev_list_bisect_all index 810b66093..342ea94ae 100644 --- a/test/fixtures/rev_list_bisect_all +++ b/test/fixtures/rev_list_bisect_all @@ -40,7 +40,7 @@ committer David Aguilar 1220418344 -0700 commit: handle --bisect-all output in Commit.list_from_string Rui Abreu Ferrerira pointed out that "git rev-list --bisect-all" - returns a slightly different format which we can easily accomodate + returns a slightly different format which we can easily accommodate by changing the way we parse rev-list output. http://groups.google.com/group/git-python/browse_thread/thread/aed1d5c4b31d5027 diff --git a/test/test_config.py b/test/test_config.py index 8892b8399..50d9b010d 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -175,7 +175,7 @@ def test_base(self): assert num_sections and num_options assert r_config._is_initialized is True - # get value which doesnt exist, with default + # get value which doesn't exist, with default default = "my default value" assert r_config.get_value("doesnt", "exist", default) == default diff --git a/test/test_diff.py b/test/test_diff.py index 9b20893a4..92e27f5d2 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -273,7 +273,7 @@ def test_diff_unsafe_paths(self): self.assertEqual(res[13].b_path, 'b/"with even more quotes"') def test_diff_patch_format(self): - # test all of the 'old' format diffs for completness - it should at least + # test all of the 'old' format diffs for completeness - it should at least # be able to deal with it fixtures = ("diff_2", "diff_2f", "diff_f", "diff_i", "diff_mode_only", "diff_new_mode", "diff_numstat", "diff_p", "diff_rename", diff --git a/test/test_docs.py b/test/test_docs.py index 8897bbb75..08fc84399 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -135,7 +135,7 @@ def update(self, op_code, cur_count, max_count=None, message=''): for fetch_info in origin.fetch(progress=MyProgressPrinter()): print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit)) # create a local branch at the latest fetched master. We specify the name statically, but you have all - # information to do it programatically as well. + # information to do it programmatically as well. bare_master = bare_repo.create_head('master', origin.refs.master) bare_repo.head.set_reference(bare_master) assert not bare_repo.delete_remote(origin).exists() diff --git a/test/test_git.py b/test/test_git.py index 7f52d650f..10e21487a 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -159,7 +159,7 @@ def test_cmd_override(self): prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE exc = GitCommandNotFound try: - # set it to something that doens't exist, assure it raises + # set it to something that doesn't exist, assure it raises type(self.git).GIT_PYTHON_GIT_EXECUTABLE = osp.join( "some", "path", "which", "doesn't", "exist", "gitbinary") self.assertRaises(exc, self.git.version) diff --git a/test/test_index.py b/test/test_index.py index 233a4c643..4a20a8f65 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -936,4 +936,4 @@ def test_commit_msg_hook_fail(self, rw_repo): self.assertEqual(err.stderr, "\n stderr: 'stderr\n'") assert str(err) else: - raise AssertionError("Should have cought a HookExecutionError") + raise AssertionError("Should have caught a HookExecutionError") diff --git a/test/test_submodule.py b/test/test_submodule.py index 3307bc788..a79123dcc 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -546,7 +546,7 @@ def test_root_module(self, rwrepo): assert nsm.module().head.commit.hexsha == nsm.hexsha nsm.module().index.add([nsm]) nsm.module().index.commit("added new file") - rm.update(recursive=False, dry_run=True, progress=prog) # would not change head, and thus doens't fail + rm.update(recursive=False, dry_run=True, progress=prog) # would not change head, and thus doesn't fail # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run # This would only run if our local branch is in the past and we have uncommitted changes @@ -730,7 +730,7 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): assert parent.head.commit.tree[sm.path].binsha == sm.binsha assert sm_too.binsha == sm.binsha, "cached submodule should point to the same commit as updated one" - added_bies = parent.index.add([sm]) # addded base-index-entries + added_bies = parent.index.add([sm]) # added base-index-entries assert len(added_bies) == 1 parent.index.commit("add same submodule entry") commit_sm = parent.head.commit.tree[sm.path] From 21ec529987d10e0010badd37f8da3274167d436f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 18 May 2022 07:43:53 +0800 Subject: [PATCH 0053/1175] Run everything through 'black' That way people who use it won't be deterred, while it unifies style everywhere. --- doc/source/conf.py | 92 ++--- git/__init__.py | 54 +-- git/cmd.py | 592 +++++++++++++++++++------------ git/compat.py | 37 +- git/config.py | 274 +++++++++----- git/db.py | 15 +- git/diff.py | 377 +++++++++++++------- git/exc.py | 78 ++-- git/ext/gitdb | 2 +- git/index/base.py | 479 ++++++++++++++++--------- git/index/fun.py | 185 ++++++---- git/index/typ.py | 67 ++-- git/index/util.py | 35 +- git/objects/__init__.py | 10 +- git/objects/base.py | 61 ++-- git/objects/blob.py | 7 +- git/objects/commit.py | 339 +++++++++++------- git/objects/fun.py | 75 ++-- git/objects/submodule/base.py | 536 +++++++++++++++++++--------- git/objects/submodule/root.py | 204 ++++++++--- git/objects/submodule/util.py | 40 ++- git/objects/tag.py | 50 ++- git/objects/tree.py | 158 ++++++--- git/objects/util.py | 362 ++++++++++++------- git/refs/head.py | 87 +++-- git/refs/log.py | 117 +++--- git/refs/reference.py | 58 +-- git/refs/remote.py | 21 +- git/refs/symbolic.py | 266 +++++++++----- git/refs/tag.py | 42 ++- git/remote.py | 538 ++++++++++++++++++---------- git/repo/base.py | 582 +++++++++++++++++++----------- git/repo/fun.py | 131 ++++--- git/types.py | 61 +++- git/util.py | 453 ++++++++++++++--------- setup.py | 34 +- test/lib/__init__.py | 7 +- test/lib/helper.py | 146 +++++--- test/performance/lib.py | 43 +-- test/performance/test_commit.py | 52 ++- test/performance/test_odb.py | 39 +- test/performance/test_streams.py | 87 +++-- test/test_actor.py | 1 - test/test_base.py | 55 ++- test/test_blob.py | 9 +- test/test_clone.py | 17 +- test/test_commit.py | 252 ++++++++----- test/test_config.py | 289 ++++++++------- test/test_db.py | 3 +- test/test_diff.py | 236 +++++++----- test/test_docs.py | 433 ++++++++++++++-------- test/test_exc.py | 83 +++-- test/test_fun.py | 103 +++--- test/test_git.py | 173 +++++---- test/test_index.py | 311 +++++++++------- test/test_installation.py | 61 +++- test/test_reflog.py | 37 +- test/test_refs.py | 137 ++++--- test/test_remote.py | 218 +++++++----- test/test_repo.py | 477 +++++++++++++++---------- test/test_stats.py | 24 +- test/test_submodule.py | 567 ++++++++++++++++++----------- test/test_tree.py | 38 +- test/test_util.py | 172 +++++---- test/tstrunner.py | 3 +- 65 files changed, 6674 insertions(+), 3918 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 286058fdc..d2803a826 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -20,38 +20,40 @@ # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.append(os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../..')) +# sys.path.append(os.path.abspath('.')) +sys.path.insert(0, os.path.abspath("../..")) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'GitPython' -copyright = 'Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel' +project = "GitPython" +copyright = ( + "Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel" +) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -with open(os.path.join(os.path.dirname(__file__), "..", "..", 'VERSION')) as fd: +with open(os.path.join(os.path.dirname(__file__), "..", "..", "VERSION")) as fd: VERSION = fd.readline().strip() version = VERSION # The full version, including alpha/beta/rc tags. @@ -59,61 +61,60 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['build'] +exclude_trees = ["build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # Options for HTML output # ----------------------- -html_theme = 'sphinx_rtd_theme' -html_theme_options = { -} +html_theme = "sphinx_rtd_theme" +html_theme_options = {} # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -122,72 +123,71 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. -#html_copy_source = True +# html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'gitpythondoc' +htmlhelp_basename = "gitpythondoc" # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ - ('index', 'GitPython.tex', r'GitPython Documentation', - r'Michael Trier', 'manual'), + ("index", "GitPython.tex", r"GitPython Documentation", r"Michael Trier", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True diff --git a/git/__init__.py b/git/__init__.py index ae9254a26..3f26886f7 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -4,8 +4,8 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa -#@PydevCodeAnalysisIgnore -from git.exc import * # @NoMove @IgnorePep8 +# @PydevCodeAnalysisIgnore +from git.exc import * # @NoMove @IgnorePep8 import inspect import os import sys @@ -14,14 +14,14 @@ from typing import Optional from git.types import PathLike -__version__ = 'git' +__version__ = "git" -#{ Initialization +# { Initialization def _init_externals() -> None: """Initialize external projects by putting them into the path""" - if __version__ == 'git' and 'PYOXIDIZER' not in os.environ: - sys.path.insert(1, osp.join(osp.dirname(__file__), 'ext', 'gitdb')) + if __version__ == "git" and "PYOXIDIZER" not in os.environ: + sys.path.insert(1, osp.join(osp.dirname(__file__), "ext", "gitdb")) try: import gitdb @@ -29,26 +29,27 @@ def _init_externals() -> None: raise ImportError("'gitdb' could not be found in your PYTHONPATH") from e # END verify import -#} END initialization + +# } END initialization ################# _init_externals() ################# -#{ Imports +# { Imports try: from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import * # @NoMove @IgnorePep8 - from git.refs import * # @NoMove @IgnorePep8 - from git.diff import * # @NoMove @IgnorePep8 - from git.db import * # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import * # @NoMove @IgnorePep8 - from git.index import * # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 + from git.objects import * # @NoMove @IgnorePep8 + from git.refs import * # @NoMove @IgnorePep8 + from git.diff import * # @NoMove @IgnorePep8 + from git.db import * # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # @NoMove @IgnorePep8 + from git.index import * # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 LockFile, BlockingLockFile, Stats, @@ -56,15 +57,18 @@ def _init_externals() -> None: rmtree, ) except GitError as exc: - raise ImportError('%s: %s' % (exc.__class__.__name__, exc)) from exc + raise ImportError("%s: %s" % (exc.__class__.__name__, exc)) from exc -#} END imports +# } END imports -__all__ = [name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj))] +__all__ = [ + name + for name, obj in locals().items() + if not (name.startswith("_") or inspect.ismodule(obj)) +] -#{ Initialize git executable path +# { Initialize git executable path GIT_OK = None @@ -79,12 +83,14 @@ def refresh(path: Optional[PathLike] = None) -> None: return GIT_OK = True -#} END initialize git executable path + + +# } END initialize git executable path ################# try: refresh() except Exception as exc: - raise ImportError('Failed to initialize: {0}'.format(exc)) from exc + raise ImportError("Failed to initialize: {0}".format(exc)) from exc ################# diff --git a/git/cmd.py b/git/cmd.py index 1ddf9e03f..12409b0c8 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -9,12 +9,7 @@ import logging import os import signal -from subprocess import ( - call, - Popen, - PIPE, - DEVNULL -) +from subprocess import call, Popen, PIPE, DEVNULL import subprocess import threading from textwrap import dedent @@ -29,10 +24,7 @@ from git.exc import CommandError from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present -from .exc import ( - GitCommandError, - GitCommandNotFound -) +from .exc import GitCommandError, GitCommandNotFound from .util import ( LazyMixin, stream_copy, @@ -40,8 +32,24 @@ # typing --------------------------------------------------------------------------- -from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, Iterator, List, Mapping, - Sequence, TYPE_CHECKING, TextIO, Tuple, Union, cast, overload) +from typing import ( + Any, + AnyStr, + BinaryIO, + Callable, + Dict, + IO, + Iterator, + List, + Mapping, + Sequence, + TYPE_CHECKING, + TextIO, + Tuple, + Union, + cast, + overload, +) from git.types import PathLike, Literal, TBD @@ -52,15 +60,26 @@ # --------------------------------------------------------------------------------- -execute_kwargs = {'istream', 'with_extended_output', - 'with_exceptions', 'as_process', 'stdout_as_string', - 'output_stream', 'with_stdout', 'kill_after_timeout', - 'universal_newlines', 'shell', 'env', 'max_chunk_size', 'strip_newline_in_stdout'} +execute_kwargs = { + "istream", + "with_extended_output", + "with_exceptions", + "as_process", + "stdout_as_string", + "output_stream", + "with_stdout", + "kill_after_timeout", + "universal_newlines", + "shell", + "env", + "max_chunk_size", + "strip_newline_in_stdout", +} log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -__all__ = ('Git',) +__all__ = ("Git",) # ============================================================================== @@ -69,18 +88,24 @@ # Documentation ## @{ -def handle_process_output(process: 'Git.AutoInterrupt' | Popen, - stdout_handler: Union[None, - Callable[[AnyStr], None], - Callable[[List[AnyStr]], None], - Callable[[bytes, 'Repo', 'DiffIndex'], None]], - stderr_handler: Union[None, - Callable[[AnyStr], None], - Callable[[List[AnyStr]], None]], - finalizer: Union[None, - Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, - decode_streams: bool = True, - kill_after_timeout: Union[None, float] = None) -> None: + +def handle_process_output( + process: "Git.AutoInterrupt" | Popen, + stdout_handler: Union[ + None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None], + Callable[[bytes, "Repo", "DiffIndex"], None], + ], + stderr_handler: Union[ + None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None] + ], + finalizer: Union[ + None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None] + ] = None, + decode_streams: bool = True, + kill_after_timeout: Union[None, float] = None, +) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -101,8 +126,13 @@ def handle_process_output(process: 'Git.AutoInterrupt' | Popen, should be killed. """ # Use 2 "pump" threads and wait for both to finish. - def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, - handler: Union[None, Callable[[Union[bytes, str]], None]]) -> None: + def pump_stream( + cmdline: List[str], + name: str, + stream: Union[BinaryIO, TextIO], + is_decode: bool, + handler: Union[None, Callable[[Union[bytes, str]], None]], + ) -> None: try: for line in stream: if handler: @@ -114,21 +144,25 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], handler(line) except Exception as ex: - log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") + log.error( + f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}" + ) if "I/O operation on closed file" not in str(ex): # Only reraise if the error was not due to the stream closing - raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex + raise CommandError( + [f"<{name}-pump>"] + remove_password_if_present(cmdline), ex + ) from ex finally: stream.close() - if hasattr(process, 'proc'): - process = cast('Git.AutoInterrupt', process) - cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, 'args', '') + if hasattr(process, "proc"): + process = cast("Git.AutoInterrupt", process) + cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "") p_stdout = process.proc.stdout if process.proc else None p_stderr = process.proc.stderr if process.proc else None else: process = cast(Popen, process) - cmdline = getattr(process, 'args', '') + cmdline = getattr(process, "args", "") p_stdout = process.stdout p_stderr = process.stderr @@ -137,15 +171,16 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], pumps: List[Tuple[str, IO, Callable[..., None] | None]] = [] if p_stdout: - pumps.append(('stdout', p_stdout, stdout_handler)) + pumps.append(("stdout", p_stdout, stdout_handler)) if p_stderr: - pumps.append(('stderr', p_stderr, stderr_handler)) + pumps.append(("stderr", p_stderr, stderr_handler)) threads: List[threading.Thread] = [] for name, stream, handler in pumps: - t = threading.Thread(target=pump_stream, - args=(cmdline, name, stream, decode_streams, handler)) + t = threading.Thread( + target=pump_stream, args=(cmdline, name, stream, decode_streams, handler) + ) t.daemon = True t.start() threads.append(t) @@ -158,12 +193,15 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], if isinstance(process, Git.AutoInterrupt): process._terminate() else: # Don't want to deal with the other case - raise RuntimeError("Thread join() timed out in cmd.handle_process_output()." - f" kill_after_timeout={kill_after_timeout} seconds") + raise RuntimeError( + "Thread join() timed out in cmd.handle_process_output()." + f" kill_after_timeout={kill_after_timeout} seconds" + ) if stderr_handler: error_str: Union[str, bytes] = ( "error: process killed because it timed out." - f" kill_after_timeout={kill_after_timeout} seconds") + f" kill_after_timeout={kill_after_timeout} seconds" + ) if not decode_streams and isinstance(p_stderr, BinaryIO): # Assume stderr_handler needs binary input error_str = cast(str, error_str) @@ -179,19 +217,22 @@ def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], def dashify(string: str) -> str: - return string.replace('_', '-') + return string.replace("_", "-") def slots_to_dict(self: object, exclude: Sequence[str] = ()) -> Dict[str, Any]: return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} -def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None: +def dict_to_slots_and__excluded_are_none( + self: object, d: Mapping[str, Any], excluded: Sequence[str] = () +) -> None: for k, v in d.items(): setattr(self, k, v) for k in excluded: setattr(self, k, None) + ## -- End Utilities -- @} @@ -200,8 +241,11 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc ## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards, # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal -PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] - if is_win else 0) # mypy error if not windows +PROC_CREATIONFLAGS = ( + CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] + if is_win + else 0 +) # mypy error if not windows class Git(LazyMixin): @@ -220,10 +264,18 @@ class Git(LazyMixin): of the command to stdout. Set its value to 'full' to see details about the returned values. """ - __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info", - "_git_options", "_persistent_git_options", "_environment") - _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info') + __slots__ = ( + "_working_dir", + "cat_file_all", + "cat_file_header", + "_version_info", + "_git_options", + "_persistent_git_options", + "_environment", + ) + + _excluded_ = ("cat_file_all", "cat_file_header", "_version_info") def __getstate__(self) -> Dict[str, Any]: return slots_to_dict(self, exclude=self._excluded_) @@ -233,7 +285,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: # CONFIGURATION - git_exec_name = "git" # default that should work on linux and windows + git_exec_name = "git" # default that should work on linux and windows # Enables debugging of GitPython's git commands GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) @@ -282,13 +334,18 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: # warn or raise exception if test failed if not has_git: - err = dedent("""\ + err = ( + dedent( + """\ Bad git executable. The git executable must be specified in one of the following ways: - be included in your $PATH - be set via $%s - explicitly set via git.refresh() - """) % cls._git_exec_env_var + """ + ) + % cls._git_exec_env_var + ) # revert to whatever the old_git was cls.GIT_PYTHON_GIT_EXECUTABLE = old_git @@ -314,7 +371,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: if mode in quiet: pass elif mode in warn or mode in error: - err = dedent("""\ + err = ( + dedent( + """\ %s All git commands will error until this is rectified. @@ -326,32 +385,42 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: Example: export %s=%s - """) % ( - err, - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - cls._refresh_env_var, - quiet[0]) + """ + ) + % ( + err, + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + cls._refresh_env_var, + quiet[0], + ) + ) if mode in warn: print("WARNING: %s" % err) else: raise ImportError(err) else: - err = dedent("""\ + err = ( + dedent( + """\ %s environment variable has been set but it has been set with an invalid value. Use only the following values: - %s: for no warning or exception - %s: for a printed warning - %s: for a raised exception - """) % ( - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error)) + """ + ) + % ( + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + ) + ) raise ImportError(err) # we get here if this was the init refresh and the refresh mode @@ -395,7 +464,7 @@ def polish_url(https://github.com/cls,%20url:%20str,%20is_cygwin:%20Union[None,%20bool]%20=%20None) -> PathLike: Hence we undo the escaping just to be sure. """ url = os.path.expandvars(url) - if url.startswith('~'): + if url.startswith("~"): url = os.path.expanduser(url) url = url.replace("\\\\", "\\").replace("\\", "/") return url @@ -441,7 +510,7 @@ def _terminate(self) -> None: log.info("Ignored error after process had died: %r", ex) # can be that nothing really exists anymore ... - if os is None or getattr(os, 'kill', None) is None: + if os is None or getattr(os, "kill", None) is None: return None # try to kill it @@ -458,7 +527,10 @@ def _terminate(self) -> None: # we simply use the shell and redirect to nul. Its slower than CreateProcess, question # is whether we really want to see all these messages. Its annoying no matter what. if is_win: - call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) + call( + ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), + shell=True, + ) # END exception handling def __del__(self) -> None: @@ -468,15 +540,15 @@ def __getattr__(self, attr: str) -> Any: return getattr(self.proc, attr) # TODO: Bad choice to mimic `proc.wait()` but with different args. - def wait(self, stderr: Union[None, str, bytes] = b'') -> int: + def wait(self, stderr: Union[None, str, bytes] = b"") -> int: """Wait for the process and return its status code. :param stderr: Previously read value of stderr, in case stderr is already closed. :warn: may deadlock if output or error pipes are used and not handled separately. :raise GitCommandError: if the return status is not 0""" if stderr is None: - stderr_b = b'' - stderr_b = force_bytes(data=stderr, encoding='utf-8') + stderr_b = b"" + stderr_b = force_bytes(data=stderr, encoding="utf-8") status: Union[int, None] if self.proc is not None: status = self.proc.wait() @@ -485,21 +557,25 @@ def wait(self, stderr: Union[None, str, bytes] = b'') -> int: status = self.status p_stderr = None - def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: + def read_all_from_possibly_closed_stream( + stream: Union[IO[bytes], None] + ) -> bytes: if stream: try: return stderr_b + force_bytes(stream.read()) except ValueError: - return stderr_b or b'' + return stderr_b or b"" else: - return stderr_b or b'' + return stderr_b or b"" # END status handling if status != 0: errstr = read_all_from_possibly_closed_stream(p_stderr) - log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) - raise GitCommandError(remove_password_if_present(self.args), status, errstr) + log.debug("AutoInterrupt wait stderr: %r" % (errstr,)) + raise GitCommandError( + remove_password_if_present(self.args), status, errstr + ) return status # END auto interrupt @@ -513,12 +589,12 @@ class CatFileContentStream(object): If not all data is read to the end of the objects's lifetime, we read the rest to assure the underlying stream continues to work""" - __slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size') + __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size") def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream self._size = size - self._nbr = 0 # num bytes read + self._nbr = 0 # num bytes read # special case: if the object is empty, has null bytes, get the # final newline right away. @@ -529,7 +605,7 @@ def __init__(self, size: int, stream: IO[bytes]) -> None: def read(self, size: int = -1) -> bytes: bytes_left = self._size - self._nbr if bytes_left == 0: - return b'' + return b"" if size > -1: # assure we don't try to read past our limit size = min(bytes_left, size) @@ -542,13 +618,13 @@ def read(self, size: int = -1) -> bytes: # check for depletion, read our final byte to make the stream usable by others if self._size - self._nbr == 0: - self._stream.read(1) # final newline + self._stream.read(1) # final newline # END finish reading return data def readline(self, size: int = -1) -> bytes: if self._nbr == self._size: - return b'' + return b"" # clamp size to lowest allowed value bytes_left = self._size - self._nbr @@ -589,7 +665,7 @@ def readlines(self, size: int = -1) -> List[bytes]: return out # skipcq: PYL-E0301 - def __iter__(self) -> 'Git.CatFileContentStream': + def __iter__(self) -> "Git.CatFileContentStream": return self def __next__(self) -> bytes: @@ -634,7 +710,7 @@ def __getattr__(self, name: str) -> Any: """A convenience method as it allows to call the command as if it was an object. :return: Callable object that will execute call _call_process with your arguments.""" - if name[0] == '_': + if name[0] == "_": return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) @@ -650,27 +726,31 @@ def set_persistent_git_options(self, **kwargs: Any) -> None: """ self._persistent_git_options = self.transform_kwargs( - split_single_char_options=True, **kwargs) + split_single_char_options=True, **kwargs + ) def _set_cache_(self, attr: str) -> None: - if attr == '_version_info': + if attr == "_version_info": # We only use the first 4 numbers, as everything else could be strings in fact (on windows) - process_version = self._call_process('version') # should be as default *args and **kwargs used - version_numbers = process_version.split(' ')[2] - - self._version_info = cast(Tuple[int, int, int, int], - tuple(int(n) for n in version_numbers.split('.')[:4] if n.isdigit()) - ) + process_version = self._call_process( + "version" + ) # should be as default *args and **kwargs used + version_numbers = process_version.split(" ")[2] + + self._version_info = cast( + Tuple[int, int, int, int], + tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()), + ) else: super(Git, self)._set_cache_(attr) # END handle version info - @ property + @property def working_dir(self) -> Union[None, PathLike]: """:return: Git directory we are working on""" return self._working_dir - @ property + @property def version_info(self) -> Tuple[int, int, int, int]: """ :return: tuple(int, int, int, int) tuple with integers representing the major, minor @@ -678,69 +758,72 @@ def version_info(self) -> Tuple[int, int, int, int]: This value is generated on demand and is cached""" return self._version_info - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[True] - ) -> 'AutoInterrupt': + @overload + def execute( + self, command: Union[str, Sequence[Any]], *, as_process: Literal[True] + ) -> "AutoInterrupt": ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[True] - ) -> Union[str, Tuple[int, str, str]]: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[True], + ) -> Union[str, Tuple[int, str, str]]: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - as_process: Literal[False] = False, - stdout_as_string: Literal[False] = False - ) -> Union[bytes, Tuple[int, bytes, str]]: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[False] = False, + ) -> Union[bytes, Tuple[int, bytes, str]]: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[True] - ) -> str: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[True], + ) -> str: ... - @ overload - def execute(self, - command: Union[str, Sequence[Any]], - *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[False] - ) -> bytes: + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[False], + ) -> bytes: ... - def execute(self, - command: Union[str, Sequence[Any]], - istream: Union[None, BinaryIO] = None, - with_extended_output: bool = False, - with_exceptions: bool = True, - as_process: bool = False, - output_stream: Union[None, BinaryIO] = None, - stdout_as_string: bool = True, - kill_after_timeout: Union[None, float] = None, - with_stdout: bool = True, - universal_newlines: bool = False, - shell: Union[None, bool] = None, - env: Union[None, Mapping[str, str]] = None, - max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, - strip_newline_in_stdout: bool = True, - **subprocess_kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: + def execute( + self, + command: Union[str, Sequence[Any]], + istream: Union[None, BinaryIO] = None, + with_extended_output: bool = False, + with_exceptions: bool = True, + as_process: bool = False, + output_stream: Union[None, BinaryIO] = None, + stdout_as_string: bool = True, + kill_after_timeout: Union[None, float] = None, + with_stdout: bool = True, + universal_newlines: bool = False, + shell: Union[None, bool] = None, + env: Union[None, Mapping[str, str]] = None, + max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + strip_newline_in_stdout: bool = True, + **subprocess_kwargs: Any, + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns the returned information (stdout) @@ -831,8 +914,8 @@ def execute(self, you must update the execute_kwargs tuple housed in this module.""" # Remove password for the command if present redacted_command = remove_password_if_present(command) - if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != 'full' or as_process): - log.info(' '.join(redacted_command)) + if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): + log.info(" ".join(redacted_command)) # Allow the user to have the command executed in their working dir. try: @@ -858,33 +941,47 @@ def execute(self, if is_win: cmd_not_found_exception = OSError if kill_after_timeout is not None: - raise GitCommandError(redacted_command, '"kill_after_timeout" feature is not supported on Windows.') + raise GitCommandError( + redacted_command, + '"kill_after_timeout" feature is not supported on Windows.', + ) else: - cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + cmd_not_found_exception = ( + FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + ) # end handle - stdout_sink = (PIPE - if with_stdout - else getattr(subprocess, 'DEVNULL', None) or open(os.devnull, 'wb')) + stdout_sink = ( + PIPE + if with_stdout + else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") + ) istream_ok = "None" if istream: istream_ok = "" - log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", - redacted_command, cwd, universal_newlines, shell, istream_ok) + log.debug( + "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", + redacted_command, + cwd, + universal_newlines, + shell, + istream_ok, + ) try: - proc = Popen(command, - env=env, - cwd=cwd, - bufsize=-1, - stdin=istream or DEVNULL, - stderr=PIPE, - stdout=stdout_sink, - shell=shell is not None and shell or self.USE_SHELL, - close_fds=is_posix, # unsupported on windows - universal_newlines=universal_newlines, - creationflags=PROC_CREATIONFLAGS, - **subprocess_kwargs - ) + proc = Popen( + command, + env=env, + cwd=cwd, + bufsize=-1, + stdin=istream or DEVNULL, + stderr=PIPE, + stdout=stdout_sink, + shell=shell is not None and shell or self.USE_SHELL, + close_fds=is_posix, # unsupported on windows + universal_newlines=universal_newlines, + creationflags=PROC_CREATIONFLAGS, + **subprocess_kwargs, + ) except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err @@ -897,9 +994,12 @@ def execute(self, return self.AutoInterrupt(proc, command) def _kill_process(pid: int) -> None: - """ Callback method to kill a process. """ - p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE, - creationflags=PROC_CREATIONFLAGS) + """Callback method to kill a process.""" + p = Popen( + ["ps", "--ppid", str(pid)], + stdout=PIPE, + creationflags=PROC_CREATIONFLAGS, + ) child_pids = [] if p.stdout is not None: for line in p.stdout: @@ -909,29 +1009,32 @@ def _kill_process(pid: int) -> None: child_pids.append(int(local_pid)) try: # Windows does not have SIGKILL, so use SIGTERM instead - sig = getattr(signal, 'SIGKILL', signal.SIGTERM) + sig = getattr(signal, "SIGKILL", signal.SIGTERM) os.kill(pid, sig) for child_pid in child_pids: try: os.kill(child_pid, sig) except OSError: pass - kill_check.set() # tell the main routine that the process was killed + kill_check.set() # tell the main routine that the process was killed except OSError: # It is possible that the process gets completed in the duration after timeout # happens and before we try to kill the process. pass return + # end if kill_after_timeout is not None: kill_check = threading.Event() - watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,)) + watchdog = threading.Timer( + kill_after_timeout, _kill_process, args=(proc.pid,) + ) # Wait for the process to return status = 0 - stdout_value: Union[str, bytes] = b'' - stderr_value: Union[str, bytes] = b'' + stdout_value: Union[str, bytes] = b"" + stderr_value: Union[str, bytes] = b"" newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: @@ -941,8 +1044,10 @@ def _kill_process(pid: int) -> None: if kill_after_timeout is not None: watchdog.cancel() if kill_check.is_set(): - stderr_value = ('Timeout: the command "%s" did not complete in %d ' - 'secs.' % (" ".join(redacted_command), kill_after_timeout)) + stderr_value = ( + 'Timeout: the command "%s" did not complete in %d ' + "secs." % (" ".join(redacted_command), kill_after_timeout) + ) if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" @@ -953,12 +1058,16 @@ def _kill_process(pid: int) -> None: status = proc.returncode else: - max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE + max_chunk_size = ( + max_chunk_size + if max_chunk_size and max_chunk_size > 0 + else io.DEFAULT_BUFFER_SIZE + ) stream_copy(proc.stdout, output_stream, max_chunk_size) stdout_value = proc.stdout.read() stderr_value = proc.stderr.read() # strip trailing "\n" - if stderr_value.endswith(newline): # type: ignore + if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] status = proc.wait() # END stdout handling @@ -966,18 +1075,28 @@ def _kill_process(pid: int) -> None: proc.stdout.close() proc.stderr.close() - if self.GIT_PYTHON_TRACE == 'full': + if self.GIT_PYTHON_TRACE == "full": cmdstr = " ".join(redacted_command) def as_text(stdout_value: Union[bytes, str]) -> str: - return not output_stream and safe_decode(stdout_value) or '' + return ( + not output_stream and safe_decode(stdout_value) or "" + ) + # end if stderr_value: - log.info("%s -> %d; stdout: '%s'; stderr: '%s'", - cmdstr, status, as_text(stdout_value), safe_decode(stderr_value)) + log.info( + "%s -> %d; stdout: '%s'; stderr: '%s'", + cmdstr, + status, + as_text(stdout_value), + safe_decode(stderr_value), + ) elif stdout_value: - log.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value)) + log.info( + "%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value) + ) else: log.info("%s -> %d", cmdstr, status) # END handle debug printing @@ -985,7 +1104,9 @@ def as_text(stdout_value: Union[bytes, str]) -> str: if with_exceptions and status != 0: raise GitCommandError(redacted_command, status, stderr_value, stdout_value) - if isinstance(stdout_value, bytes) and stdout_as_string: # could also be output_stream + if ( + isinstance(stdout_value, bytes) and stdout_as_string + ): # could also be output_stream stdout_value = safe_decode(stdout_value) # Allow access to the command's status code @@ -1042,7 +1163,9 @@ def custom_environment(self, **kwargs: Any) -> Iterator[None]: finally: self.update_environment(**old_env) - def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]: + def transform_kwarg( + self, name: str, value: Any, split_single_char_options: bool + ) -> List[str]: if len(name) == 1: if value is True: return ["-%s" % name] @@ -1058,7 +1181,9 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool return ["--%s=%s" % (dashify(name), value)] return [] - def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: + def transform_kwargs( + self, split_single_char_options: bool = True, **kwargs: Any + ) -> List[str]: """Transforms Python style kwargs into git command line options.""" args = [] for k, v in kwargs.items(): @@ -1081,7 +1206,7 @@ def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]: return outlist - def __call__(self, **kwargs: Any) -> 'Git': + def __call__(self, **kwargs: Any) -> "Git": """Specify command line options to the git executable for a subcommand call @@ -1094,28 +1219,34 @@ def __call__(self, **kwargs: Any) -> 'Git': ``Examples``:: git(work_tree='/tmp').difftool()""" self._git_options = self.transform_kwargs( - split_single_char_options=True, **kwargs) + split_single_char_options=True, **kwargs + ) return self @overload - def _call_process(self, method: str, *args: None, **kwargs: None - ) -> str: + def _call_process(self, method: str, *args: None, **kwargs: None) -> str: ... # if no args given, execute called with all defaults @overload - def _call_process(self, method: str, - istream: int, - as_process: Literal[True], - *args: Any, **kwargs: Any - ) -> 'Git.AutoInterrupt': ... + def _call_process( + self, + method: str, + istream: int, + as_process: Literal[True], + *args: Any, + **kwargs: Any, + ) -> "Git.AutoInterrupt": + ... @overload - def _call_process(self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + def _call_process( + self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ... - def _call_process(self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + def _call_process( + self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: """Run the given git command with the specified arguments and return the result as a String @@ -1145,13 +1276,13 @@ def _call_process(self, method: str, *args: Any, **kwargs: Any :return: Same as ``execute`` if no args given used execute default (esp. as_process = False, stdout_as_string = True) - and return str """ + and return str""" # Handle optional arguments prior to calling transform_kwargs # otherwise these'll end up in args, which is bad. exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs} - insert_after_this_arg = opts_kwargs.pop('insert_kwargs_after', None) + insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None) # Prepare the argument list @@ -1164,10 +1295,12 @@ def _call_process(self, method: str, *args: Any, **kwargs: Any try: index = ext_args.index(insert_after_this_arg) except ValueError as err: - raise ValueError("Couldn't find argument '%s' in args %s to insert cmd options after" - % (insert_after_this_arg, str(ext_args))) from err + raise ValueError( + "Couldn't find argument '%s' in args %s to insert cmd options after" + % (insert_after_this_arg, str(ext_args)) + ) from err # end handle error - args_list = ext_args[:index + 1] + opt_args + ext_args[index + 1:] + args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :] # end handle opts_kwargs call = [self.GIT_PYTHON_GIT_EXECUTABLE] @@ -1197,9 +1330,15 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: tokens = header_line.split() if len(tokens) != 3: if not tokens: - raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip())) + raise ValueError( + "SHA could not be resolved, git returned: %r" + % (header_line.strip()) + ) else: - raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) + raise ValueError( + "SHA %s could not be resolved, git returned: %r" + % (tokens[0], header_line.strip()) + ) # END handle actual return value # END error handling @@ -1211,9 +1350,9 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: # required for command to separate refs on stdin, as bytes if isinstance(ref, bytes): # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text - refstr: str = ref.decode('ascii') + refstr: str = ref.decode("ascii") elif not isinstance(ref, str): - refstr = str(ref) # could be ref-object + refstr = str(ref) # could be ref-object else: refstr = ref @@ -1221,8 +1360,9 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: refstr += "\n" return refstr.encode(defenc) - def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any - ) -> 'Git.AutoInterrupt': + def _get_persistent_cmd( + self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any + ) -> "Git.AutoInterrupt": cur_val = getattr(self, attr_name) if cur_val is not None: return cur_val @@ -1232,10 +1372,12 @@ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwarg cmd = self._call_process(cmd_name, *args, **options) setattr(self, attr_name, cmd) - cmd = cast('Git.AutoInterrupt', cmd) + cmd = cast("Git.AutoInterrupt", cmd) return cmd - def __get_object_header(self, cmd: 'Git.AutoInterrupt', ref: AnyStr) -> Tuple[str, str, int]: + def __get_object_header( + self, cmd: "Git.AutoInterrupt", ref: AnyStr + ) -> Tuple[str, str, int]: if cmd.stdin and cmd.stdout: cmd.stdin.write(self._prepare_ref(ref)) cmd.stdin.flush() @@ -1244,7 +1386,7 @@ def __get_object_header(self, cmd: 'Git.AutoInterrupt', ref: AnyStr) -> Tuple[st raise ValueError("cmd stdin was empty") def get_object_header(self, ref: str) -> Tuple[str, str, int]: - """ Use this method to quickly examine the type and size of the object behind + """Use this method to quickly examine the type and size of the object behind the given ref. :note: The method will only suffer from the costs of command invocation @@ -1255,16 +1397,18 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]: return self.__get_object_header(cmd, ref) def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: - """ As get_object_header, but returns object data as well + """As get_object_header, but returns object data as well :return: (hexsha, type_string, size_as_int,data_string) :note: not threadsafe""" hexsha, typename, size, stream = self.stream_object_data(ref) data = stream.read(size) - del(stream) + del stream return (hexsha, typename, size, data) - def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileContentStream']: - """ As get_object_header, but returns the data as a stream + def stream_object_data( + self, ref: str + ) -> Tuple[str, str, int, "Git.CatFileContentStream"]: + """As get_object_header, but returns the data as a stream :return: (hexsha, type_string, size_as_int, stream) :note: This method is not threadsafe, you need one independent Command instance per thread to be safe !""" @@ -1273,7 +1417,7 @@ def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileConte cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout)) - def clear_cache(self) -> 'Git': + def clear_cache(self) -> "Git": """Clear all kinds of internal caches to release resources. Currently persistent commands will be interrupted. diff --git a/git/compat.py b/git/compat.py index 988c04eff..e7ef28c30 100644 --- a/git/compat.py +++ b/git/compat.py @@ -12,8 +12,8 @@ import sys from gitdb.utils.encoding import ( - force_bytes, # @UnusedImport - force_text # @UnusedImport + force_bytes, # @UnusedImport + force_text, # @UnusedImport ) # typing -------------------------------------------------------------------- @@ -29,21 +29,24 @@ Union, overload, ) + # --------------------------------------------------------------------------- -is_win: bool = (os.name == 'nt') -is_posix = (os.name == 'posix') -is_darwin = (os.name == 'darwin') +is_win: bool = os.name == "nt" +is_posix = os.name == "posix" +is_darwin = os.name == "darwin" defenc = sys.getfilesystemencoding() @overload -def safe_decode(s: None) -> None: ... +def safe_decode(s: None) -> None: + ... @overload -def safe_decode(s: AnyStr) -> str: ... +def safe_decode(s: AnyStr) -> str: + ... def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: @@ -51,19 +54,21 @@ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: if isinstance(s, str): return s elif isinstance(s, bytes): - return s.decode(defenc, 'surrogateescape') + return s.decode(defenc, "surrogateescape") elif s is None: return None else: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) @overload -def safe_encode(s: None) -> None: ... +def safe_encode(s: None) -> None: + ... @overload -def safe_encode(s: AnyStr) -> bytes: ... +def safe_encode(s: AnyStr) -> bytes: + ... def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: @@ -75,15 +80,17 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: elif s is None: return None else: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) @overload -def win_encode(s: None) -> None: ... +def win_encode(s: None) -> None: + ... @overload -def win_encode(s: AnyStr) -> bytes: ... +def win_encode(s: AnyStr) -> bytes: + ... def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: @@ -93,5 +100,5 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: elif isinstance(s, bytes): return s elif s is not None: - raise TypeError('Expected bytes or text, but got %r' % (s,)) + raise TypeError("Expected bytes or text, but got %r" % (s,)) return None diff --git a/git/config.py b/git/config.py index 1ac3c9cec..24c2b2013 100644 --- a/git/config.py +++ b/git/config.py @@ -30,8 +30,20 @@ # typing------------------------------------------------------- -from typing import (Any, Callable, Generic, IO, List, Dict, Sequence, - TYPE_CHECKING, Tuple, TypeVar, Union, cast) +from typing import ( + Any, + Callable, + Generic, + IO, + List, + Dict, + Sequence, + TYPE_CHECKING, + Tuple, + TypeVar, + Union, + cast, +) from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T @@ -39,23 +51,25 @@ from git.repo.base import Repo from io import BytesIO -T_ConfigParser = TypeVar('T_ConfigParser', bound='GitConfigParser') -T_OMD_value = TypeVar('T_OMD_value', str, bytes, int, float, bool) +T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") +T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool) if sys.version_info[:3] < (3, 7, 2): # typing.Ordereddict not added until py 3.7.2 from collections import OrderedDict + OrderedDict_OMD = OrderedDict else: from typing import OrderedDict + OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] # ------------------------------------------------------------- -__all__ = ('GitConfigParser', 'SectionConstraint') +__all__ = ("GitConfigParser", "SectionConstraint") -log = logging.getLogger('git.config') +log = logging.getLogger("git.config") log.addHandler(logging.NullHandler()) # invariants @@ -67,26 +81,37 @@ # Section pattern to detect conditional includes. # https://git-scm.com/docs/git-config#_conditional_includes -CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") +CONDITIONAL_INCLUDE_REGEXP = re.compile( + r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"" +) class MetaParserBuilder(abc.ABCMeta): """Utility class wrapping base-class methods into decorators that assure read-only properties""" - def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': + + def __new__( + cls, name: str, bases: Tuple, clsdict: Dict[str, Any] + ) -> "MetaParserBuilder": """ Equip all base-class methods with a needs_values decorator, and all non-const methods with a set_dirty_and_flush_changes decorator in addition to that.""" - kmm = '_mutating_methods_' + kmm = "_mutating_methods_" if kmm in clsdict: mutating_methods = clsdict[kmm] for base in bases: - methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) + methods = ( + t + for t in inspect.getmembers(base, inspect.isroutine) + if not t[0].startswith("_") + ) for name, method in methods: if name in clsdict: continue method_with_values = needs_values(method) if name in mutating_methods: - method_with_values = set_dirty_and_flush_changes(method_with_values) + method_with_values = set_dirty_and_flush_changes( + method_with_values + ) # END mutating methods handling clsdict[name] = method_with_values @@ -102,9 +127,10 @@ def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: """Returns method assuring we read values (on demand) before we try to access them""" @wraps(func) - def assure_data_present(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: self.read() return func(self, *args, **kwargs) + # END wrapper method return assure_data_present @@ -114,11 +140,12 @@ def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[. If so, the instance will be set dirty. Additionally, we flush the changes right to disk""" - def flush_changes(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: rval = non_const_func(self, *args, **kwargs) self._dirty = True self.write() return rval + # END wrapper method flush_changes.__name__ = non_const_func.__name__ return flush_changes @@ -133,9 +160,21 @@ class SectionConstraint(Generic[T_ConfigParser]): :note: If used as a context manager, will release the wrapped ConfigParser.""" + __slots__ = ("_config", "_section_name") - _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", - "remove_section", "remove_option", "options") + _valid_attrs_ = ( + "get_value", + "set_value", + "get", + "set", + "getint", + "getfloat", + "getboolean", + "has_option", + "remove_section", + "remove_option", + "options", + ) def __init__(self, config: T_ConfigParser, section: str) -> None: self._config = config @@ -166,11 +205,13 @@ def release(self) -> None: """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" return self._config.release() - def __enter__(self) -> 'SectionConstraint[T_ConfigParser]': + def __enter__(self) -> "SectionConstraint[T_ConfigParser]": self._config.__enter__() return self - def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: + def __exit__( + self, exception_type: str, exception_value: str, traceback: str + ) -> None: self._config.__exit__(exception_type, exception_value, traceback) @@ -228,16 +269,22 @@ def get_config_path(config_level: Lit_config_levels) -> str: if config_level == "system": return "/etc/gitconfig" elif config_level == "user": - config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", '~'), ".config") + config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join( + os.environ.get("HOME", "~"), ".config" + ) return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) elif config_level == "global": return osp.normpath(osp.expanduser("~/.gitconfig")) elif config_level == "repository": - raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") + raise ValueError( + "No repo to get repository configuration from. Use Repo._get_config_path" + ) else: # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs - assert_never(config_level, # type: ignore[unreachable] - ValueError(f"Invalid configuration level: {config_level!r}")) + assert_never( + config_level, # type: ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}"), + ) class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): @@ -258,30 +305,36 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): must match perfectly. If used as a context manager, will release the locked file.""" - #{ Configuration + # { Configuration # The lock type determines the type of lock to use in new configuration readers. # They must be compatible to the LockFile interface. # A suitable alternative would be the BlockingLockFile t_lock = LockFile - re_comment = re.compile(r'^\s*[#;]') + re_comment = re.compile(r"^\s*[#;]") - #} END configuration + # } END configuration - optvalueonly_source = r'\s*(?P