From 1570f3d70e7a80e0fdc2f8edc3060b3e54a9cea3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 13:59:39 +0000 Subject: [PATCH 01/14] Bump cygwin/cygwin-install-action from 3 to 4 Bumps [cygwin/cygwin-install-action](https://github.com/cygwin/cygwin-install-action) from 3 to 4. - [Release notes](https://github.com/cygwin/cygwin-install-action/releases) - [Commits](https://github.com/cygwin/cygwin-install-action/compare/v3...v4) --- updated-dependencies: - dependency-name: cygwin/cygwin-install-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cygwin-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 0018e7dfc..a1ecb6785 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 9999 - - uses: cygwin/cygwin-install-action@v3 + - uses: cygwin/cygwin-install-action@v4 with: packages: python39 python39-pip python39-virtualenv git - name: Tell git to trust this repo From d2f7284189b5c1f867ebd58b0b879fa34406eefc Mon Sep 17 00:00:00 2001 From: Twist Date: Fri, 21 Apr 2023 22:16:32 +0100 Subject: [PATCH 02/14] Add trailers_list and trailers_list methods to fix the commit trailers functionality. Update trailers tests. --- git/objects/commit.py | 101 +++++++++++++++++++++++++++++++++--------- test/test_commit.py | 83 +++++++++++++++++++--------------- 2 files changed, 126 insertions(+), 58 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 547e8fe82..50da0a105 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -26,6 +26,7 @@ import os from io import BytesIO import logging +from collections import defaultdict # typing ------------------------------------------------------------------ @@ -335,8 +336,70 @@ def stats(self) -> Stats: return Stats._list_from_string(self.repo, text) @property - def trailers(self) -> Dict: - """Get the trailers of the message as dictionary + def trailers(self) -> Dict[str, str]: + """Get the trailers of the message as a 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). + + WARNING: This function only returns the latest instance of each trailer key + and will be deprecated soon. Please see either ``Commit.trailers_list`` or ``Commit.trailers_dict``. + + :return: + Dictionary containing whitespace stripped trailer information. + Only the latest instance of each trailer key. + """ + return { + k: v[0] for k, v in self.trailers_dict.items() + } + + @property + def trailers_list(self) -> List[str]: + """Get the trailers of the message as a list + + 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 functions calls ``git interpret-trailers --parse`` onto the message + to extract the trailer information, returns the raw trailer data as a list. + + Valid message with trailer:: + + Subject line + + some body information + + another information + + key1: value1.1 + key1: value1.2 + key2 : value 2 with inner spaces + + + Returned list will look like this:: + + [ + "key1: value1.1", + "key1: value1.2", + "key2 : value 2 with inner spaces", + ] + + + :return: + List containing whitespace stripped trailer information. + """ + 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() + trailer = trailer.strip() + if trailer: + return [t.strip() for t in trailer.split("\n")] + + return [] + + @property + def trailers_dict(self) -> Dict[str, List[str]]: + """Get the trailers of the message as a 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). @@ -345,9 +408,7 @@ def trailers(self) -> Dict: 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: - - .. code-block:: + Valid message with trailer:: Subject line @@ -355,32 +416,28 @@ def trailers(self) -> Dict: another information - key1: value1 + key1: value1.1 + key1: value1.2 key2 : value 2 with inner spaces - dictionary will look like this: - .. code-block:: + Returned dictionary will look like this:: { - "key1": "value1", - "key2": "value 2 with inner spaces" + "key1": ["value1.1", "value1.2"], + "key2": ["value 2 with inner spaces"], } - :return: Dictionary containing whitespace stripped trailer information + :return: + Dictionary containing whitespace stripped trailer information. + Mapping trailer keys to a list of their corresponding values. """ - 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 + d = defaultdict(list) + for trailer in self.trailers_list: + key, value = trailer.split(":", 1) + d[key.strip()].append(value.strip()) + return dict(d) @classmethod def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]: diff --git a/test/test_commit.py b/test/test_commit.py index 1efc68897..ca1e4752b 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -494,52 +494,63 @@ def test_datetimes(self): def test_trailers(self): KEY_1 = "Hello" - VALUE_1 = "World" + VALUE_1_1 = "World" + VALUE_1_2 = "Another-World" KEY_2 = "Key" 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" - ) - + # Check the following trailer example is extracted from multiple msg variations + TRAILER = f"{KEY_1}: {VALUE_1_1}\n{KEY_2}: {VALUE_2}\n{KEY_1}: {VALUE_1_2}" + msgs = [ + f"Subject\n\n{TRAILER}\n", + f"Subject\n \nSome body of a function\n \n{TRAILER}\n", + f"Subject\n \nSome body of a function\n\nnon-key: non-value\n\n{TRAILER}\n", + ( + # check when trailer has inconsistent whitespace + f"Subject\n \nSome multiline\n body of a function\n\nnon-key: non-value\n\n" + f"{KEY_1}:{VALUE_1_1}\n{KEY_2} : {VALUE_2}\n{KEY_1}: {VALUE_1_2}\n" + ), + ] for msg in msgs: - commit = self.rorepo.commit("master") - commit = copy.copy(commit) + commit = copy.copy(self.rorepo.commit("master")) 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") + assert commit.trailers_list == [ + f"{KEY_1}: {VALUE_1_1}", + f"{KEY_2}: {VALUE_2}", + f"{KEY_1}: {VALUE_1_2}", + ] + assert commit.trailers_dict == { + KEY_1: [VALUE_1_1, VALUE_1_2], + KEY_2: [VALUE_2], + } + assert commit.trailers == { + KEY_1: VALUE_1_1, + KEY_2: VALUE_2, + } + + # check that trailer stays empty for multiple msg combinations + msgs = [ + f"Subject\n", + f"Subject\n\nBody with some\nText\n", + f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", + f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", + f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", + 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 = copy.copy(self.rorepo.commit("master")) commit.message = msg - assert len(commit.trailers.keys()) == 0 + assert commit.trailers_list == [] + assert commit.trailers_dict == {} + assert commit.trailers == {} # 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 + commit = copy.copy(self.rorepo.commit("master")) + commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n" + assert commit.trailers_list == [f"{KEY_2}: {VALUE_2}"] + assert commit.trailers_dict == {KEY_2: [VALUE_2]} + assert commit.trailers == {KEY_2: VALUE_2} def test_commit_co_authors(self): commit = copy.copy(self.rorepo.commit("4251bd5")) From 78424b56654ad476da4bd2faf88d3875c5265e0d Mon Sep 17 00:00:00 2001 From: Twist Date: Sat, 22 Apr 2023 17:19:20 +0100 Subject: [PATCH 03/14] Deprecate Commit.trailers. --- git/objects/commit.py | 18 ------------------ test/test_commit.py | 6 ------ 2 files changed, 24 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 50da0a105..1e3f751bc 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -335,24 +335,6 @@ def stats(self) -> Stats: text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True) return Stats._list_from_string(self.repo, text) - @property - def trailers(self) -> Dict[str, str]: - """Get the trailers of the message as a 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). - - WARNING: This function only returns the latest instance of each trailer key - and will be deprecated soon. Please see either ``Commit.trailers_list`` or ``Commit.trailers_dict``. - - :return: - Dictionary containing whitespace stripped trailer information. - Only the latest instance of each trailer key. - """ - return { - k: v[0] for k, v in self.trailers_dict.items() - } - @property def trailers_list(self) -> List[str]: """Get the trailers of the message as a list diff --git a/test/test_commit.py b/test/test_commit.py index ca1e4752b..8d2ee754b 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -523,10 +523,6 @@ def test_trailers(self): KEY_1: [VALUE_1_1, VALUE_1_2], KEY_2: [VALUE_2], } - assert commit.trailers == { - KEY_1: VALUE_1_1, - KEY_2: VALUE_2, - } # check that trailer stays empty for multiple msg combinations msgs = [ @@ -543,14 +539,12 @@ def test_trailers(self): commit.message = msg assert commit.trailers_list == [] assert commit.trailers_dict == {} - assert commit.trailers == {} # check that only the last key value paragraph is evaluated commit = copy.copy(self.rorepo.commit("master")) commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n" assert commit.trailers_list == [f"{KEY_2}: {VALUE_2}"] assert commit.trailers_dict == {KEY_2: [VALUE_2]} - assert commit.trailers == {KEY_2: VALUE_2} def test_commit_co_authors(self): commit = copy.copy(self.rorepo.commit("4251bd5")) From abde3eafd293e8fa2ef2dc22d58ba5d80f1702e9 Mon Sep 17 00:00:00 2001 From: Twist Date: Sat, 22 Apr 2023 17:47:08 +0100 Subject: [PATCH 04/14] Update Commit.trailer_list to return tuples. --- git/objects/commit.py | 27 ++++++++++++++++----------- test/test_commit.py | 8 ++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 1e3f751bc..b41a79951 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -336,7 +336,7 @@ def stats(self) -> Stats: return Stats._list_from_string(self.repo, text) @property - def trailers_list(self) -> List[str]: + def trailers_list(self) -> List[Tuple[str, str]]: """Get the trailers of the message as a list Git messages can contain trailer information that are similar to RFC 822 @@ -361,23 +361,29 @@ def trailers_list(self) -> List[str]: Returned list will look like this:: [ - "key1: value1.1", - "key1: value1.2", - "key2 : value 2 with inner spaces", + ("key1", "value1.1"), + ("key1", "value1.2"), + ("key2", "value 2 with inner spaces"), ] :return: - List containing whitespace stripped trailer information. + List containing key-value tuples of whitespace stripped trailer information. """ 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() trailer = trailer.strip() - if trailer: - return [t.strip() for t in trailer.split("\n")] - return [] + if not trailer: + return [] + + trailer_list = [] + for t in trailer.split("\n"): + key, val = t.split(":", 1) + trailer_list.append((key.strip(), val.strip())) + + return trailer_list @property def trailers_dict(self) -> Dict[str, List[str]]: @@ -416,9 +422,8 @@ def trailers_dict(self) -> Dict[str, List[str]]: Mapping trailer keys to a list of their corresponding values. """ d = defaultdict(list) - for trailer in self.trailers_list: - key, value = trailer.split(":", 1) - d[key.strip()].append(value.strip()) + for key, val in self.trailers_list: + d[key].append(val) return dict(d) @classmethod diff --git a/test/test_commit.py b/test/test_commit.py index 8d2ee754b..4871902ec 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -515,9 +515,9 @@ def test_trailers(self): commit = copy.copy(self.rorepo.commit("master")) commit.message = msg assert commit.trailers_list == [ - f"{KEY_1}: {VALUE_1_1}", - f"{KEY_2}: {VALUE_2}", - f"{KEY_1}: {VALUE_1_2}", + (KEY_1, VALUE_1_1), + (KEY_2, VALUE_2), + (KEY_1, VALUE_1_2), ] assert commit.trailers_dict == { KEY_1: [VALUE_1_1, VALUE_1_2], @@ -543,7 +543,7 @@ def test_trailers(self): # check that only the last key value paragraph is evaluated commit = copy.copy(self.rorepo.commit("master")) commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n" - assert commit.trailers_list == [f"{KEY_2}: {VALUE_2}"] + assert commit.trailers_list == [(KEY_2, VALUE_2)] assert commit.trailers_dict == {KEY_2: [VALUE_2]} def test_commit_co_authors(self): From ed36bd903e1fdf45c53b52dbcb1b2d8444965d98 Mon Sep 17 00:00:00 2001 From: Twist Date: Sat, 22 Apr 2023 17:54:04 +0100 Subject: [PATCH 05/14] Specify encoding in Commit.trailer_list. --- git/objects/commit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index b41a79951..f164becbc 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -372,7 +372,7 @@ def trailers_list(self) -> List[Tuple[str, str]]: """ 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() + trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8") trailer = trailer.strip() if not trailer: From 9ef07a731caf39684891bce5cc92c4e91829138d Mon Sep 17 00:00:00 2001 From: Twist Date: Sun, 23 Apr 2023 14:16:53 +0100 Subject: [PATCH 06/14] Revert the removal of Commit.trailers property. --- git/objects/commit.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/git/objects/commit.py b/git/objects/commit.py index f164becbc..138db0afe 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -335,6 +335,20 @@ def stats(self) -> Stats: text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True) return Stats._list_from_string(self.repo, text) + @property + def trailers(self) -> Dict[str, str]: + """Get the trailers of the message as a dictionary + + :note: This property is deprecated, please use either ``Commit.trailers_list`` or ``Commit.trailers_dict``. + + :return: + Dictionary containing whitespace stripped trailer information. + Only contains the latest instance of each trailer key. + """ + return { + k: v[0] for k, v in self.trailers_dict.items() + } + @property def trailers_list(self) -> List[Tuple[str, str]]: """Get the trailers of the message as a list From 2a0305b349f61bddae674eb8f4efe0fd3c916bbf Mon Sep 17 00:00:00 2001 From: Christopher Head Date: Mon, 15 May 2023 10:13:37 -0700 Subject: [PATCH 07/14] Name top-level exceptions as private variables `exc` is private to the module. Naming it `_exc` eliminates a collision with the `exc` submodule (one which would not be observable at runtime due to the import failing, but which confuses linters). --- git/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index f746e1fca..cd6602bf0 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -56,8 +56,8 @@ def _init_externals() -> None: Actor, rmtree, ) -except GitError as exc: - raise ImportError("%s: %s" % (exc.__class__.__name__, exc)) from exc +except GitError as _exc: + raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc # } END imports @@ -87,6 +87,6 @@ def refresh(path: Optional[PathLike] = None) -> None: ################# try: refresh() -except Exception as exc: - raise ImportError("Failed to initialize: {0}".format(exc)) from exc +except Exception as _exc: + raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc ################# From 6fc11e6e36e524a6749e15046eca3a8601745822 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 May 2023 07:33:14 +0200 Subject: [PATCH 08/14] update README to reflect the status quo on `git` command usage --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 82c5c9e0f..96801b046 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,8 @@ implementation of 'git' in [Rust](https://www.rust-lang.org). GitPython is a python library used to interact with git repositories, high-level like git-porcelain, or low-level like git-plumbing. -It provides abstractions of git objects for easy access of repository data, and additionally -allows you to access the git repository more directly using either a pure python implementation, -or the faster, but more resource intensive _git command_ implementation. - -The object database implementation is optimized for handling large quantities of objects and large datasets, -which is achieved by using low-level structures and data streaming. +It provides abstractions of git objects for easy access of repository data often backed by calling the `git` +command-line program. ### DEVELOPMENT STATUS @@ -41,8 +37,7 @@ The project is open to contributions of all kinds, as well as new maintainers. ### REQUIREMENTS -GitPython needs the `git` executable to be installed on the system and available -in your `PATH` for most operations. +GitPython needs the `git` executable to be installed on the system and available in your `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. From 9cd7ddb96022dd30cfe7b64378e3b32a3747c1dd Mon Sep 17 00:00:00 2001 From: Jean-Luc Tibaux <90517741+eUgEntOptIc44@users.noreply.github.com> Date: Wed, 5 Jul 2023 09:23:14 +0000 Subject: [PATCH 09/14] Improve the 'long_description' displayed on pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81ae0132d..ebece64eb 100755 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, - long_description="""GitPython is a Python library used to interact with Git repositories""", + long_description=long_description, long_description_content_type="text/markdown", classifiers=[ # Picked from From 0c543cd0ddedeaee27ca5e7c4c22b25a8fd5becb Mon Sep 17 00:00:00 2001 From: Jean-Luc Tibaux <90517741+eUgEntOptIc44@users.noreply.github.com> Date: Wed, 5 Jul 2023 09:23:38 +0000 Subject: [PATCH 10/14] Improve readability of README.md --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 96801b046..676d2c6d6 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,19 @@ The installer takes care of installing them for you. If you have downloaded the source code: - python setup.py install +```bash +python setup.py install +``` or if you want to obtain a copy from the Pypi repository: - pip install GitPython +```bash +pip install GitPython +``` Both commands will install the required package dependencies. -A distribution package can be obtained for manual installation at: - - http://pypi.python.org/pypi/GitPython +A distribution package can be obtained for manual installation at: . If you like to clone from source, you can do it like so: @@ -157,7 +159,7 @@ tarballs. This script shows how to verify the tarball was indeed created by the authors of this project: -``` +```bash curl https://files.pythonhosted.org/packages/09/bc/ae32e07e89cc25b9e5c793d19a1e5454d30a8e37d95040991160f942519e/GitPython-3.1.8-py3-none-any.whl > gitpython.whl curl https://files.pythonhosted.org/packages/09/bc/ae32e07e89cc25b9e5c793d19a1e5454d30a8e37d95040991160f942519e/GitPython-3.1.8-py3-none-any.whl.asc > gitpython-signature.asc gpg --verify gitpython-signature.asc gitpython.whl @@ -165,7 +167,7 @@ gpg --verify gitpython-signature.asc gitpython.whl which outputs -``` +```bash gpg: Signature made Fr 4 Sep 10:04:50 2020 CST gpg: using RSA key 27C50E7F590947D7273A741E85194C08421980C9 gpg: Good signature from "Sebastian Thiel (YubiKey USB-C) " [ultimate] @@ -175,19 +177,19 @@ gpg: aka "Sebastian Thiel (In Rust I trust) Date: Fri, 7 Jul 2023 06:36:12 +0300 Subject: [PATCH 11/14] Don't rely on __del__ --- git/index/base.py | 17 ++++++++--------- git/index/util.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index cda08de25..8523717c6 100644 --- a/git/index/base.py +++ b/git/index/base.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 +from contextlib import ExitStack import datetime import glob from io import BytesIO @@ -360,20 +361,19 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile # as it considers existing entries. moving it essentially clears the index. # Unfortunately there is no 'soft' way to do it. # The TemporaryFileSwap assure the original file get put back - if repo.git_dir: - index_handler = TemporaryFileSwap(join_path_native(repo.git_dir, "index")) try: - repo.git.read_tree(*arg_list, **kwargs) - index = cls(repo, tmp_index) - index.entries # force it to read the file as we will delete the temp-file - del index_handler # release as soon as possible + with ExitStack() as stack: + if repo.git_dir: + stack.enter_context(TemporaryFileSwap(join_path_native(repo.git_dir, "index"))) + repo.git.read_tree(*arg_list, **kwargs) + index = cls(repo, tmp_index) + index.entries # force it to read the file as we will delete the temp-file + return index finally: if osp.exists(tmp_index): os.remove(tmp_index) # END index merge handling - return index - # UTILITIES @unbare_repo def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]: @@ -1156,7 +1156,6 @@ def checkout( unknown_lines = [] def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None: - stderr_IO = proc.stderr if not stderr_IO: return None # return early if stderr empty diff --git a/git/index/util.py b/git/index/util.py index bfc7fadd6..6cf838f3b 100644 --- a/git/index/util.py +++ b/git/index/util.py @@ -3,6 +3,7 @@ import os import struct import tempfile +from types import TracebackType from git.compat import is_win @@ -11,7 +12,7 @@ # typing ---------------------------------------------------------------------- -from typing import Any, Callable, TYPE_CHECKING +from typing import Any, Callable, TYPE_CHECKING, Optional, Type from git.types import PathLike, _T @@ -47,12 +48,21 @@ def __init__(self, file_path: PathLike) -> None: except OSError: pass - def __del__(self) -> None: + def __enter__(self) -> "TemporaryFileSwap": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: if osp.isfile(self.tmp_file_path): if is_win and osp.exists(self.file_path): os.remove(self.file_path) os.rename(self.tmp_file_path, self.file_path) - # END temp file exists + + return False # { Decorators From a3859ee6f72e604d46a63dcd9fa3098adcc35cb0 Mon Sep 17 00:00:00 2001 From: Roey Darwish Dror Date: Fri, 7 Jul 2023 16:47:07 +0300 Subject: [PATCH 12/14] fixes --- git/index/base.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 8523717c6..dd8f9aa2e 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -353,26 +353,22 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile # tmp file created in git home directory to be sure renaming # works - /tmp/ dirs could be on another device - tmp_index = tempfile.mktemp("", "", repo.git_dir) - arg_list.append("--index-output=%s" % tmp_index) - arg_list.extend(treeish) - - # move current index out of the way - otherwise the merge may fail - # as it considers existing entries. moving it essentially clears the index. - # Unfortunately there is no 'soft' way to do it. - # The TemporaryFileSwap assure the original file get put back - try: - with ExitStack() as stack: - if repo.git_dir: - stack.enter_context(TemporaryFileSwap(join_path_native(repo.git_dir, "index"))) - repo.git.read_tree(*arg_list, **kwargs) - index = cls(repo, tmp_index) - index.entries # force it to read the file as we will delete the temp-file - return index - finally: - if osp.exists(tmp_index): - os.remove(tmp_index) - # END index merge handling + with ExitStack() as stack: + tmp_index = stack.enter_context(tempfile.NamedTemporaryFile(dir=repo.git_dir)) + arg_list.append("--index-output=%s" % tmp_index.name) + arg_list.extend(treeish) + + # move current index out of the way - otherwise the merge may fail + # as it considers existing entries. moving it essentially clears the index. + # Unfortunately there is no 'soft' way to do it. + # The TemporaryFileSwap assure the original file get put back + + stack.enter_context(TemporaryFileSwap(join_path_native(repo.git_dir, "index"))) + repo.git.read_tree(*arg_list, **kwargs) + index = cls(repo, tmp_index.name) + index.entries # force it to read the file as we will delete the temp-file + return index + # END index merge handling # UTILITIES @unbare_repo From 5c59e0d63da6180db8a0b349f0ad36fef42aceed Mon Sep 17 00:00:00 2001 From: Sylvain Beucler Date: Mon, 10 Jul 2023 16:10:10 +0200 Subject: [PATCH 13/14] Block insecure non-multi options in clone/clone_from Follow-up to #1521 --- git/repo/base.py | 2 ++ test/test_repo.py | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 2fc9cf1fe..1fa98d8c7 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -1203,6 +1203,8 @@ def _clone( if not allow_unsafe_protocols: Git.check_unsafe_protocols(str(url)) + if not allow_unsafe_options: + Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options) if not allow_unsafe_options and multi_options: Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options) diff --git a/test/test_repo.py b/test/test_repo.py index 07c1e9adf..5c66aeeb1 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -282,6 +282,17 @@ def test_clone_unsafe_options(self, rw_repo): rw_repo.clone(tmp_dir, multi_options=[unsafe_option]) assert not tmp_file.exists() + unsafe_options = [ + {"upload-pack": f"touch {tmp_file}"}, + {"u": f"touch {tmp_file}"}, + {"config": "protocol.ext.allow=always"}, + {"c": "protocol.ext.allow=always"}, + ] + for unsafe_option in unsafe_options: + with self.assertRaises(UnsafeOptionError): + rw_repo.clone(tmp_dir, **unsafe_option) + assert not tmp_file.exists() + @with_rw_repo("HEAD") def test_clone_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -341,6 +352,17 @@ def test_clone_from_unsafe_options(self, rw_repo): Repo.clone_from(rw_repo.working_dir, tmp_dir, multi_options=[unsafe_option]) assert not tmp_file.exists() + unsafe_options = [ + {"upload-pack": f"touch {tmp_file}"}, + {"u": f"touch {tmp_file}"}, + {"config": "protocol.ext.allow=always"}, + {"c": "protocol.ext.allow=always"}, + ] + for unsafe_option in unsafe_options: + with self.assertRaises(UnsafeOptionError): + Repo.clone_from(rw_repo.working_dir, tmp_dir, **unsafe_option) + assert not tmp_file.exists() + @with_rw_repo("HEAD") def test_clone_from_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -1410,4 +1432,4 @@ def test_ignored_raises_error_w_symlink(self): os.symlink(tmp_dir / "target", tmp_dir / "symlink") with pytest.raises(GitCommandError): - temp_repo.ignored(tmp_dir / "symlink/file.txt") \ No newline at end of file + temp_repo.ignored(tmp_dir / "symlink/file.txt") From 5d45ce243a12669724e969442e6725a894e30fd4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jul 2023 19:53:19 +0200 Subject: [PATCH 14/14] prepare 3.1.32 release --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 339bdc849..381c34a62 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.31 +3.1.32 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 4ee613bcc..3bc02e770 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.32 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/62?closed=1 + 3.1.31 ====== pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy