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 001/152] 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 002/152] 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 003/152] 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 004/152] 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 005/152] 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 006/152] 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 007/152] 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 008/152] 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 009/152] 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 010/152] 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 011/152] 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 012/152] 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 97cdb403fd11778916b006b22679f427a3c1a8ac Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sat, 8 Jul 2023 15:22:33 -0400 Subject: [PATCH 013/152] Made the init repo section of quickdoc --- doc/source/quickstart.rst | 37 +++++++++++++++++++++++++++++++++++++ test/test_quick_doc.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 doc/source/quickstart.rst create mode 100644 test/test_quick_doc.py diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst new file mode 100644 index 000000000..602ca423f --- /dev/null +++ b/doc/source/quickstart.rst @@ -0,0 +1,37 @@ +.. _quickdoc_toplevel: + +.. highlight:: python + +.. _quickdoc-label: + +============================== +GitPython Quick Start Tutorial +============================== + +git.Repo +******** + +There are a few ways to create a :class:`git.Repo ` object + +An existing local path +###################### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_init_repo_object] + :end-before: # ![1-test_init_repo_object] + +Existing local git Repo +####################### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_init_repo_object] + :end-before: # ![2-test_init_repo_object] + +Clone from URL +############## + +For the rest of this tutorial we will use a clone from https://github.com \ No newline at end of file diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py new file mode 100644 index 000000000..a6dfb8e53 --- /dev/null +++ b/test/test_quick_doc.py @@ -0,0 +1,33 @@ +import pytest + +import git +from test.lib import TestBase +from test.lib.helper import with_rw_directory + + +class QuickDoc(TestBase): + def tearDown(self): + import gc + + gc.collect() + + @with_rw_directory + def test_init_repo_object(self, rw_dir): + path_to_dir = rw_dir + + # [1-test_init_repo_object] + from git import Repo + + repo = Repo.init(path_to_dir) + assert repo.__class__ is Repo # Test to confirm repo was initialized + # ![1-test_init_repo_object] + + # [2-test_init_repo_object] + try: + repo = Repo(path_to_dir) + except git.NoSuchPathError: + assert False, f"No such path {path_to_dir}" + # ! [2-test_init_repo_object] + + # [3 - test_init_repo_object] + From 6a9154b1bfcebe7ee28edebec6617993ad6a5569 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sat, 8 Jul 2023 20:52:04 -0400 Subject: [PATCH 014/152] Added git clone & git add --- doc/source/quickstart.rst | 27 +++++++++++++++++++++++++- test/test_quick_doc.py | 40 ++++++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 602ca423f..ebdb2520a 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -16,6 +16,8 @@ There are a few ways to create a :class:`git.Repo ` object An existing local path ###################### +$ git init path/to/dir + .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 @@ -34,4 +36,27 @@ Existing local git Repo Clone from URL ############## -For the rest of this tutorial we will use a clone from https://github.com \ No newline at end of file +For the rest of this tutorial we will use a clone from https://github.com/LeoDaCoda/GitPython-TestFileSys.git + +git clone https://some_repo_url + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_cloned_repo_object] + :end-before: # ![1-test_cloned_repo_object] + +Usage +**************** + +* git add filepath + + + + +* git commit -m message +* git log file +* git status + + + diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index a6dfb8e53..0188367cf 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -1,6 +1,6 @@ import pytest -import git + from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -18,16 +18,46 @@ def test_init_repo_object(self, rw_dir): # [1-test_init_repo_object] from git import Repo - repo = Repo.init(path_to_dir) - assert repo.__class__ is Repo # Test to confirm repo was initialized + repo = Repo.init(path_to_dir) # git init path/to/dir + assert repo.__class__ is Repo # Test to confirm repo was initialized # ![1-test_init_repo_object] # [2-test_init_repo_object] + import git + try: repo = Repo(path_to_dir) except git.NoSuchPathError: assert False, f"No such path {path_to_dir}" - # ! [2-test_init_repo_object] + # ![2-test_init_repo_object] + + @with_rw_directory + def test_cloned_repo_object(self, rw_dir): + local_dir = rw_dir - # [3 - test_init_repo_object] + from git import Repo + import git + # code to clone from url + # [1-test_cloned_repo_object] + repo_url = "https://github.com/LeoDaCoda/GitPython-TestFileSys.git" + + try: + repo = Repo.clone_from(repo_url, local_dir) + except git.CommandError: + assert False, f"Invalid address {repo_url}" + # ![1-test_cloned_repo_object] + + # code to add files + # [2-test_cloned_repo_object] + # We must make a change to a file so that we can add the update to git + + update_file = 'dir1/file2.txt' # we'll use /dir1/file2.txt + with open(f"{local_dir}/{update_file}", 'a') as f: + f.write('\nUpdate version 2') + # ![2-test_cloned_repo_object] + + # [3-test_cloned_repo_object] + add_file = [f"{local_dir}/{update_file}"] + repo.index.add(add_file) # notice the add function requires a list of paths + # [3-test_cloned_repo_object] From 3c42baebf5bd7c509b9962d1490f59e8874f1323 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 05:34:09 -0400 Subject: [PATCH 015/152] Finishing touches for Repo quickstart --- doc/source/quickstart.rst | 85 +++++++++++++++++++++++++++++++++++++-- test/test_quick_doc.py | 71 ++++++++++++++++++++++++++++++-- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index ebdb2520a..0a728e485 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -49,14 +49,91 @@ git clone https://some_repo_url Usage **************** -* git add filepath +* $ git add filepath +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_cloned_repo_object] + :end-before: # ![2-test_cloned_repo_object] + +Now lets add the updated file to git + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [3-test_cloned_repo_object] + :end-before: # ![3-test_cloned_repo_object] + +Notice the add method requires a list as a parameter + +* $ git commit -m message + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [4-test_cloned_repo_object] + :end-before: # ![4-test_cloned_repo_object] + +* $ git log file + +A list of commits associated with a file + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [5-test_cloned_repo_object] + :end-before: # ![5-test_cloned_repo_object] + +Notice this returns a generator object + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [6-test_cloned_repo_object] + :end-before: # ![6-test_cloned_repo_object] + +returns list of :class:`Commit ` objects + +* $ git status + + * Untracked files + + Lets create a new file + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [7-test_cloned_repo_object] + :end-before: # ![7-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [8-test_cloned_repo_object] + :end-before: # ![8-test_cloned_repo_object] + + * Modified files + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [9-test_cloned_repo_object] + :end-before: # ![9-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [10-test_cloned_repo_object] + :end-before: # ![10-test_cloned_repo_object] + returns a list of :class:`Diff ` objects -* git commit -m message -* git log file -* git status + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11-test_cloned_repo_object] + :end-before: # ![11-test_cloned_repo_object] diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 0188367cf..bb3372905 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -51,13 +51,78 @@ def test_cloned_repo_object(self, rw_dir): # [2-test_cloned_repo_object] # We must make a change to a file so that we can add the update to git - update_file = 'dir1/file2.txt' # we'll use /dir1/file2.txt + update_file = 'dir1/file2.txt' # we'll use ./dir1/file2.txt with open(f"{local_dir}/{update_file}", 'a') as f: f.write('\nUpdate version 2') # ![2-test_cloned_repo_object] # [3-test_cloned_repo_object] - add_file = [f"{local_dir}/{update_file}"] + add_file = [f"{update_file}"] # relative path from git root repo.index.add(add_file) # notice the add function requires a list of paths - # [3-test_cloned_repo_object] + # ![3-test_cloned_repo_object] + + # code to commit - not sure how to test this + # [4-test_cloned_repo_object] + repo.index.commit("Update to file2") + # ![4-test_cloned_repo_object] + + # [5-test_cloned_repo_object] + file = 'dir1/file2.txt' # relative path from git root + repo.iter_commits('--all', max_count=100, paths=file) + + # Outputs: + + # ![5-test_cloned_repo_object] + + # [6-test_cloned_repo_object] + commits_for_file_generator = repo.iter_commits('--all', max_count=100, paths=file) + commits_for_file = [c for c in commits_for_file_generator] + commits_for_file + + # Outputs: [, + # ] + # ![6-test_cloned_repo_object] + + # Untracked files - create new file + # [7-test_cloned_repo_object] + # We'll create a file5.txt + + file5 = f'{local_dir}/file5.txt' + with open(file5, 'w') as f: + f.write('file5 version 1') + # ![7-test_cloned_repo_object] + + # [8-test_cloned_repo_object] + repo.untracked_files + # Output: ['file5.txt'] + # ![8-test_cloned_repo_object] + + # Modified files + # [9-test_cloned_repo_object] + # Lets modify one of our tracked files + file3 = f'{local_dir}/Downloads/file3.txt' + with open(file3, 'w') as f: + f.write('file3 version 2') # overwrite file 3 + # ![9-test_cloned_repo_object] + + # [10-test_cloned_repo_object] + repo.index.diff(None) + # Output: [, + # ] + # ![10-test_cloned_repo_object] + + # [11-test_cloned_repo_object] + diffs = repo.index.diff(None) + for d in diffs: + print(d.a_path) + + # Downloads/file3.txt + # file4.txt + # ![11-test_cloned_repo_object] + + + + + + From 10ea113ca6141b8a74e78858e6ff6f52bfd04d9f Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 21:29:26 -0400 Subject: [PATCH 016/152] finished code for quickstart --- doc/source/quickstart.rst | 70 +++++++++++++++++++++++++++++++++++++++ test/test_quick_doc.py | 46 +++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0a728e485..9d63c5674 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -136,4 +136,74 @@ returns list of :class:`Commit ` objects :end-before: # ![11-test_cloned_repo_object] +Trees & Blobs +************** +Latest Commit Tree +################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [12-test_cloned_repo_object] + :end-before: # ![12-test_cloned_repo_object] + +Any Commit Tree +############### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [13-test_cloned_repo_object] + :end-before: # ![13-test_cloned_repo_object] + +Display level 1 Contents +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14-test_cloned_repo_object] + :end-before: # ![14-test_cloned_repo_object] + +Recurse through the Tree +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [15-test_cloned_repo_object] + :end-before: # ![15-test_cloned_repo_object] + +.. code-block:: python + + print_files_from_git(tree) + +.. code-block:: python + + # Output + | Downloads, tree + ----| Downloads/file3.txt, blob + | dir1, tree + ----| dir1/file1.txt, blob + ----| dir1/file2.txt, blob + | file4.txt, blob + + +Print file version +################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] + +.. code-block:: python + + blob = tree[print_file] + print(blob.data_stream.read().decode()) + + # Output + # file 2 version 1 + # Update version 2 diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index bb3372905..2a95bfff5 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -120,7 +120,53 @@ def test_cloned_repo_object(self, rw_dir): # file4.txt # ![11-test_cloned_repo_object] + '''Trees and Blobs''' + # Latest commit tree + # [12-test_cloned_repo_object] + tree = repo.tree() + # ![12-test_cloned_repo_object] + + # Previous commit tree + # [13-test_cloned_repo_object] + prev_commits = [c for c in repo.iter_commits('--all', max_count=10)] + tree = prev_commits[0].tree + # ![13-test_cloned_repo_object] + + # Iterating through tree + # [14-test_cloned_repo_object] + tree = repo.tree() + files_dirs = [fd for fd in tree] + files_dirs + + # Output + # [, + # , + # ] + + # ![14-test_cloned_repo_object] + + # [15-test_cloned_repo_object] + def print_files_from_git(tree, delim='-', i=0): + files_dirs = [fd for fd in tree] + for fd in files_dirs: + print(f'{delim if i != 0 else ""}| {fd.path}, {fd.type}') + if fd.type == "tree": + print_files_from_git(fd, delim * 4, i + 1) + + # ![15-test_cloned_repo_object] + + # Printing text files + # [16-test_cloned_repo_object] + print_file = 'dir1/file2.txt' + tree[print_file] + + # Output + # ![16-test_cloned_repo_object] + + # [17-test_cloned_repo_object] + + # ![17-test_cloned_repo_object] From b0da0a9e53a30dfcefaa7d77fe0bd0104b3a814e Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 21:29:26 -0400 Subject: [PATCH 017/152] finished code for quickstart --- doc/source/quickstart.rst | 70 +++++++++++++++++++++++++++++++++++++++ test/test_quick_doc.py | 46 +++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0a728e485..9d63c5674 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -136,4 +136,74 @@ returns list of :class:`Commit ` objects :end-before: # ![11-test_cloned_repo_object] +Trees & Blobs +************** +Latest Commit Tree +################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [12-test_cloned_repo_object] + :end-before: # ![12-test_cloned_repo_object] + +Any Commit Tree +############### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [13-test_cloned_repo_object] + :end-before: # ![13-test_cloned_repo_object] + +Display level 1 Contents +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14-test_cloned_repo_object] + :end-before: # ![14-test_cloned_repo_object] + +Recurse through the Tree +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [15-test_cloned_repo_object] + :end-before: # ![15-test_cloned_repo_object] + +.. code-block:: python + + print_files_from_git(tree) + +.. code-block:: python + + # Output + | Downloads, tree + ----| Downloads/file3.txt, blob + | dir1, tree + ----| dir1/file1.txt, blob + ----| dir1/file2.txt, blob + | file4.txt, blob + + +Print file version +################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] + +.. code-block:: python + + blob = tree[print_file] + print(blob.data_stream.read().decode()) + + # Output + # file 2 version 1 + # Update version 2 diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index bb3372905..2a95bfff5 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -120,7 +120,53 @@ def test_cloned_repo_object(self, rw_dir): # file4.txt # ![11-test_cloned_repo_object] + '''Trees and Blobs''' + # Latest commit tree + # [12-test_cloned_repo_object] + tree = repo.tree() + # ![12-test_cloned_repo_object] + + # Previous commit tree + # [13-test_cloned_repo_object] + prev_commits = [c for c in repo.iter_commits('--all', max_count=10)] + tree = prev_commits[0].tree + # ![13-test_cloned_repo_object] + + # Iterating through tree + # [14-test_cloned_repo_object] + tree = repo.tree() + files_dirs = [fd for fd in tree] + files_dirs + + # Output + # [, + # , + # ] + + # ![14-test_cloned_repo_object] + + # [15-test_cloned_repo_object] + def print_files_from_git(tree, delim='-', i=0): + files_dirs = [fd for fd in tree] + for fd in files_dirs: + print(f'{delim if i != 0 else ""}| {fd.path}, {fd.type}') + if fd.type == "tree": + print_files_from_git(fd, delim * 4, i + 1) + + # ![15-test_cloned_repo_object] + + # Printing text files + # [16-test_cloned_repo_object] + print_file = 'dir1/file2.txt' + tree[print_file] + + # Output + # ![16-test_cloned_repo_object] + + # [17-test_cloned_repo_object] + + # ![17-test_cloned_repo_object] From fb35ed1d611113637c52a559d6f77aaadb6d403d Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 21:36:28 -0400 Subject: [PATCH 018/152] fixed some indentation --- doc/source/quickstart.rst | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 9d63c5674..1c0832ed5 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -99,41 +99,41 @@ returns list of :class:`Commit ` objects * Untracked files - Lets create a new file + Lets create a new file - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [7-test_cloned_repo_object] - :end-before: # ![7-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [7-test_cloned_repo_object] + :end-before: # ![7-test_cloned_repo_object] - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [8-test_cloned_repo_object] - :end-before: # ![8-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [8-test_cloned_repo_object] + :end-before: # ![8-test_cloned_repo_object] * Modified files - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [9-test_cloned_repo_object] - :end-before: # ![9-test_cloned_repo_object] - - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [10-test_cloned_repo_object] - :end-before: # ![10-test_cloned_repo_object] - - returns a list of :class:`Diff ` objects - - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [11-test_cloned_repo_object] - :end-before: # ![11-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [9-test_cloned_repo_object] + :end-before: # ![9-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [10-test_cloned_repo_object] + :end-before: # ![10-test_cloned_repo_object] + + returns a list of :class:`Diff ` objects + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11-test_cloned_repo_object] + :end-before: # ![11-test_cloned_repo_object] Trees & Blobs From 47c83629cfa0550fae71f2c266bd8b236b63fdc6 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 22:17:03 -0400 Subject: [PATCH 019/152] added quickstart to toctree and fixed sphinx warning --- doc/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 69fb573a4..72db8ee5a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,6 +9,7 @@ GitPython Documentation :maxdepth: 2 intro + quickstart tutorial reference roadmap From b7955ed1f1511dd7d873e4198b3372c104102b4f Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 9 Jul 2023 22:17:03 -0400 Subject: [PATCH 020/152] added quickstart to toctree to fix sphinx warning --- doc/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 69fb573a4..72db8ee5a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,6 +9,7 @@ GitPython Documentation :maxdepth: 2 intro + quickstart tutorial reference roadmap From 5c59e0d63da6180db8a0b349f0ad36fef42aceed Mon Sep 17 00:00:00 2001 From: Sylvain Beucler Date: Mon, 10 Jul 2023 16:10:10 +0200 Subject: [PATCH 021/152] 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 03d26f0c92055759e296a36f2bde1ff9fb439b29 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 13:51:40 -0400 Subject: [PATCH 022/152] Removed code from RST --- doc/source/quickstart.rst | 34 ++++++++++++++-------------------- test/test_quick_doc.py | 25 ++++++++++++++++++++----- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 1c0832ed5..5845bf9e2 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -175,35 +175,29 @@ Recurse through the Tree :start-after: # [15-test_cloned_repo_object] :end-before: # ![15-test_cloned_repo_object] -.. code-block:: python +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] - print_files_from_git(tree) -.. code-block:: python - # Output - | Downloads, tree - ----| Downloads/file3.txt, blob - | dir1, tree - ----| dir1/file1.txt, blob - ----| dir1/file2.txt, blob - | file4.txt, blob +Printing text files +#################### -Print file version -################## +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [17-test_cloned_repo_object] + :end-before: # ![17-test_cloned_repo_object] .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [16-test_cloned_repo_object] - :end-before: # ![16-test_cloned_repo_object] + :start-after: # [18-test_cloned_repo_object] + :end-before: # ![18-test_cloned_repo_object] -.. code-block:: python - blob = tree[print_file] - print(blob.data_stream.read().decode()) - # Output - # file 2 version 1 - # Update version 2 diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 2a95bfff5..f1d644382 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -156,19 +156,34 @@ def print_files_from_git(tree, delim='-', i=0): # ![15-test_cloned_repo_object] - # Printing text files # [16-test_cloned_repo_object] - print_file = 'dir1/file2.txt' - tree[print_file] + print_files_from_git(tree) - # Output - # ![16-test_cloned_repo_object] + # Output + # | Downloads, tree + # ---- | Downloads / file3.txt, blob + # | dir1, tree + # ---- | dir1 / file1.txt, blob + # ---- | dir1 / file2.txt, blob + # | file4.txt, blob + # # ![16-test_cloned_repo_object] + # Printing text files # [17-test_cloned_repo_object] + print_file = 'dir1/file2.txt' + tree[print_file] + # Output # ![17-test_cloned_repo_object] + # [18-test_cloned_repo_object] + blob = tree[print_file] + print(blob.data_stream.read().decode()) + # Output + # file 2 version 1 + # Update version 2 + # ![18-test_cloned_repo_object] From 5d45ce243a12669724e969442e6725a894e30fd4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jul 2023 19:53:19 +0200 Subject: [PATCH 023/152] 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 ====== From 947b8b7330a6a26c1503b0b40bb2fd2bf489e8e8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 10 Jul 2023 20:13:48 +0200 Subject: [PATCH 024/152] try to fix CI by making it deal with tags forcefully. Question is why this is a problem in the first place. --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6d6c67952..2d95e6ffa 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -32,7 +32,7 @@ jobs: python -m pip install --upgrade pip setuptools wheel python --version; git --version git submodule update --init --recursive - git fetch --tags + git fetch --tags --force pip install -r requirements.txt pip install -r test-requirements.txt From a0045d8844b937e703156adfbeb496ebc70c8950 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 15:00:06 -0400 Subject: [PATCH 025/152] Made variable names more intuitive --- test/test_quick_doc.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index f1d644382..49d9a0f36 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -136,8 +136,8 @@ def test_cloned_repo_object(self, rw_dir): # Iterating through tree # [14-test_cloned_repo_object] tree = repo.tree() - files_dirs = [fd for fd in tree] - files_dirs + files_and_dirs = [entry for entry in tree] + files_and_dirs # Output # [, @@ -147,12 +147,11 @@ def test_cloned_repo_object(self, rw_dir): # ![14-test_cloned_repo_object] # [15-test_cloned_repo_object] - def print_files_from_git(tree, delim='-', i=0): - files_dirs = [fd for fd in tree] - for fd in files_dirs: - print(f'{delim if i != 0 else ""}| {fd.path}, {fd.type}') - if fd.type == "tree": - print_files_from_git(fd, delim * 4, i + 1) + def print_files_from_git(root, delim='-', i=0): + for entry in root: + print(f'{delim if i != 0 else ""}| {entry.path}, {entry.type}') + if entry.type == "tree": + print_files_from_git(entry, delim * 4, i + 1) # ![15-test_cloned_repo_object] From 98336551260f0b3093f5be085573b193198a4271 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 15:19:00 -0400 Subject: [PATCH 026/152] Updated the sample repo URL --- test/test_quick_doc.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 49d9a0f36..e76bf3a12 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -12,8 +12,7 @@ def tearDown(self): gc.collect() @with_rw_directory - def test_init_repo_object(self, rw_dir): - path_to_dir = rw_dir + def test_init_repo_object(self, path_to_dir): # [1-test_init_repo_object] from git import Repo @@ -32,19 +31,15 @@ def test_init_repo_object(self, rw_dir): # ![2-test_init_repo_object] @with_rw_directory - def test_cloned_repo_object(self, rw_dir): - local_dir = rw_dir + def test_cloned_repo_object(self, local_dir): from git import Repo import git # code to clone from url # [1-test_cloned_repo_object] - repo_url = "https://github.com/LeoDaCoda/GitPython-TestFileSys.git" + repo_url = "https://github.com/gitpython-developers/QuickStartTutorialFiles.git" - try: - repo = Repo.clone_from(repo_url, local_dir) - except git.CommandError: - assert False, f"Invalid address {repo_url}" + repo = Repo.clone_from(repo_url, local_dir) # ![1-test_cloned_repo_object] # code to add files From 3cda530b1fc1e5ae3c2403a43a7270f6a73f07fb Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 15:24:05 -0400 Subject: [PATCH 027/152] removed try/except and updated sample url --- doc/source/quickstart.rst | 2 +- test/test_quick_doc.py | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 5845bf9e2..5c1c18701 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -38,7 +38,7 @@ Clone from URL For the rest of this tutorial we will use a clone from https://github.com/LeoDaCoda/GitPython-TestFileSys.git -git clone https://some_repo_url +$ git clone https://github.com/gitpython-developers/QuickStartTutorialFiles.git .. literalinclude:: ../../test/test_quick_doc.py :language: python diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index e76bf3a12..64586f186 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -18,16 +18,10 @@ def test_init_repo_object(self, path_to_dir): from git import Repo repo = Repo.init(path_to_dir) # git init path/to/dir - assert repo.__class__ is Repo # Test to confirm repo was initialized - # ![1-test_init_repo_object] + # ![1-test_init_repo_object] # [2-test_init_repo_object] - import git - - try: - repo = Repo(path_to_dir) - except git.NoSuchPathError: - assert False, f"No such path {path_to_dir}" + repo = Repo(path_to_dir) # ![2-test_init_repo_object] @with_rw_directory From e4bbc7a520d83b7e5db208d0fe901cec0125c2f9 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 17:13:44 -0400 Subject: [PATCH 028/152] correct way to get the latest commit tree --- test/test_quick_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 64586f186..f8c973bad 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -113,7 +113,7 @@ def test_cloned_repo_object(self, local_dir): # Latest commit tree # [12-test_cloned_repo_object] - tree = repo.tree() + tree = repo.head.commit.tree # ![12-test_cloned_repo_object] # Previous commit tree From a1dfd4ade535242bb535cbda9b2f02153d2a423e Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 10 Jul 2023 23:56:06 -0400 Subject: [PATCH 029/152] convert from --all flag to all=True --- test/test_quick_doc.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index f8c973bad..701d3994a 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -57,14 +57,14 @@ def test_cloned_repo_object(self, local_dir): # [5-test_cloned_repo_object] file = 'dir1/file2.txt' # relative path from git root - repo.iter_commits('--all', max_count=100, paths=file) + repo.iter_commits(all=True, max_count=10, paths=file) # gets the last 10 commits from all branches # Outputs: # ![5-test_cloned_repo_object] # [6-test_cloned_repo_object] - commits_for_file_generator = repo.iter_commits('--all', max_count=100, paths=file) + commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=file) commits_for_file = [c for c in commits_for_file_generator] commits_for_file @@ -95,7 +95,8 @@ def test_cloned_repo_object(self, local_dir): # ![9-test_cloned_repo_object] # [10-test_cloned_repo_object] - repo.index.diff(None) + repo.index.diff(None) # compares staging area to working directory + repo.index.diff(repo.head.commit) # compares staging area to last commit # Output: [, # ] # ![10-test_cloned_repo_object] @@ -118,7 +119,7 @@ def test_cloned_repo_object(self, local_dir): # Previous commit tree # [13-test_cloned_repo_object] - prev_commits = [c for c in repo.iter_commits('--all', max_count=10)] + prev_commits = [c for c in repo.iter_commits(all=True, max_count=10)] # last 10 commits from all branches tree = prev_commits[0].tree # ![13-test_cloned_repo_object] From a8b58639f57f5a4952f98ee097def5ad9543b566 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 11 Jul 2023 00:02:20 -0400 Subject: [PATCH 030/152] removed unnecessary variables --- test/test_quick_doc.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 701d3994a..9756f0da5 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -46,7 +46,7 @@ def test_cloned_repo_object(self, local_dir): # ![2-test_cloned_repo_object] # [3-test_cloned_repo_object] - add_file = [f"{update_file}"] # relative path from git root + add_file = [update_file] # relative path from git root repo.index.add(add_file) # notice the add function requires a list of paths # ![3-test_cloned_repo_object] @@ -56,15 +56,15 @@ def test_cloned_repo_object(self, local_dir): # ![4-test_cloned_repo_object] # [5-test_cloned_repo_object] - file = 'dir1/file2.txt' # relative path from git root - repo.iter_commits(all=True, max_count=10, paths=file) # gets the last 10 commits from all branches + # relative path from git root + repo.iter_commits(all=True, max_count=10, paths=update_file) # gets the last 10 commits from all branches # Outputs: # ![5-test_cloned_repo_object] # [6-test_cloned_repo_object] - commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=file) + commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=update_file) commits_for_file = [c for c in commits_for_file_generator] commits_for_file @@ -76,8 +76,7 @@ def test_cloned_repo_object(self, local_dir): # [7-test_cloned_repo_object] # We'll create a file5.txt - file5 = f'{local_dir}/file5.txt' - with open(file5, 'w') as f: + with open(f'{local_dir}/file5.txt', 'w') as f: f.write('file5 version 1') # ![7-test_cloned_repo_object] @@ -89,8 +88,8 @@ def test_cloned_repo_object(self, local_dir): # Modified files # [9-test_cloned_repo_object] # Lets modify one of our tracked files - file3 = f'{local_dir}/Downloads/file3.txt' - with open(file3, 'w') as f: + + with open(f'{local_dir}/Downloads/file3.txt', 'w') as f: f.write('file3 version 2') # overwrite file 3 # ![9-test_cloned_repo_object] From abe7e6e5075ba4b9ea4cfc74b6121ad977dc7e4f Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 11 Jul 2023 00:23:18 -0400 Subject: [PATCH 031/152] replaced output cell to generic commit ID --- test/test_quick_doc.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 9756f0da5..0f09d4ed9 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -68,8 +68,8 @@ def test_cloned_repo_object(self, local_dir): commits_for_file = [c for c in commits_for_file_generator] commits_for_file - # Outputs: [, - # ] + # Outputs: [, + # ] # ![6-test_cloned_repo_object] # Untracked files - create new file @@ -124,14 +124,13 @@ def test_cloned_repo_object(self, local_dir): # Iterating through tree # [14-test_cloned_repo_object] - tree = repo.tree() files_and_dirs = [entry for entry in tree] files_and_dirs # Output - # [, - # , - # ] + # [, + # , + # ] # ![14-test_cloned_repo_object] From 1369bdc6d7d06e473b7c211a4070dcee94438e64 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Thu, 13 Jul 2023 01:35:41 -0400 Subject: [PATCH 032/152] replaced hash with generic --- test/test_quick_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 0f09d4ed9..d1aea44e7 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -160,7 +160,7 @@ def print_files_from_git(root, delim='-', i=0): print_file = 'dir1/file2.txt' tree[print_file] - # Output + # Output # ![17-test_cloned_repo_object] # [18-test_cloned_repo_object] From 9cd9431906acff137e441a2dd82d1d6d4e6322d7 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Thu, 13 Jul 2023 01:36:17 -0400 Subject: [PATCH 033/152] draft of description --- doc/source/quickstart.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 5c1c18701..018e13a76 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -7,6 +7,9 @@ ============================== GitPython Quick Start Tutorial ============================== +Welcome to the GitPython Quickstart Guide! Designed for developers seeking a practical and interactive learning experience, this concise resource offers step-by-step code snippets to swiftly initialize/clone repositories, perform essential Git operations, and explore GitPython's capabilities. Get ready to dive in, experiment, and unleash the power of GitPython in your projects! + +All code presented here originated from `***** insert link **** `_ 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. git.Repo ******** From 393bae5184abda11cdaab128049fccba2fcb213f Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Thu, 13 Jul 2023 01:42:24 -0400 Subject: [PATCH 034/152] clarified comment --- doc/source/quickstart.rst | 2 +- test/test_quick_doc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 018e13a76..bd5ddd7b8 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -39,7 +39,7 @@ Existing local git Repo Clone from URL ############## -For the rest of this tutorial we will use a clone from https://github.com/LeoDaCoda/GitPython-TestFileSys.git +For the rest of this tutorial we will use a clone from https://github.com/gitpython-developers/QuickStartTutorialFiles.git $ git clone https://github.com/gitpython-developers/QuickStartTutorialFiles.git diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index d1aea44e7..3dc29a22c 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -40,7 +40,7 @@ def test_cloned_repo_object(self, local_dir): # [2-test_cloned_repo_object] # We must make a change to a file so that we can add the update to git - update_file = 'dir1/file2.txt' # we'll use ./dir1/file2.txt + update_file = 'dir1/file2.txt' # we'll use local_dir/dir1/file2.txt with open(f"{local_dir}/{update_file}", 'a') as f: f.write('\nUpdate version 2') # ![2-test_cloned_repo_object] From aa6d27f9204d68b21cd24366c8a58fb4f9578553 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Thu, 13 Jul 2023 16:15:16 -0400 Subject: [PATCH 035/152] refactored print git tree --- test/test_quick_doc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 3dc29a22c..0aa27a361 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -135,11 +135,11 @@ def test_cloned_repo_object(self, local_dir): # ![14-test_cloned_repo_object] # [15-test_cloned_repo_object] - def print_files_from_git(root, delim='-', i=0): + def print_files_from_git(root, level=0): for entry in root: - print(f'{delim if i != 0 else ""}| {entry.path}, {entry.type}') + print(f'{"-" * 4 * level}| {entry.path}, {entry.type}') if entry.type == "tree": - print_files_from_git(entry, delim * 4, i + 1) + print_files_from_git(entry, level + 1) # ![15-test_cloned_repo_object] @@ -148,10 +148,10 @@ def print_files_from_git(root, delim='-', i=0): # Output # | Downloads, tree - # ---- | Downloads / file3.txt, blob + # ----| Downloads / file3.txt, blob # | dir1, tree - # ---- | dir1 / file1.txt, blob - # ---- | dir1 / file2.txt, blob + # ----| dir1 / file1.txt, blob + # ----| dir1 / file2.txt, blob # | file4.txt, blob # # ![16-test_cloned_repo_object] From 6d78ff1ac33fa2adeb0518feb33a634c09b0b5b5 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 11:58:46 -0400 Subject: [PATCH 036/152] Made trees and blobs the first section --- doc/source/quickstart.rst | 133 +++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index bd5ddd7b8..11f8123bb 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -11,6 +11,74 @@ Welcome to the GitPython Quickstart Guide! Designed for developers seeking a pra All code presented here originated from `***** insert link **** `_ 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. + +Trees & Blobs +************** + +Latest Commit Tree +################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [12-test_cloned_repo_object] + :end-before: # ![12-test_cloned_repo_object] + +Any Commit Tree +############### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [13-test_cloned_repo_object] + :end-before: # ![13-test_cloned_repo_object] + +Display level 1 Contents +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14-test_cloned_repo_object] + :end-before: # ![14-test_cloned_repo_object] + +Recurse through the Tree +######################## + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [15-test_cloned_repo_object] + :end-before: # ![15-test_cloned_repo_object] + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] + + + + +Printing text files +#################### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [17-test_cloned_repo_object] + :end-before: # ![17-test_cloned_repo_object] + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18-test_cloned_repo_object] + :end-before: # ![18-test_cloned_repo_object] + + + + + git.Repo ******** @@ -139,68 +207,3 @@ returns list of :class:`Commit ` objects :end-before: # ![11-test_cloned_repo_object] -Trees & Blobs -************** - -Latest Commit Tree -################## - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [12-test_cloned_repo_object] - :end-before: # ![12-test_cloned_repo_object] - -Any Commit Tree -############### - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [13-test_cloned_repo_object] - :end-before: # ![13-test_cloned_repo_object] - -Display level 1 Contents -######################## - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [14-test_cloned_repo_object] - :end-before: # ![14-test_cloned_repo_object] - -Recurse through the Tree -######################## - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [15-test_cloned_repo_object] - :end-before: # ![15-test_cloned_repo_object] - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [16-test_cloned_repo_object] - :end-before: # ![16-test_cloned_repo_object] - - - - -Printing text files -#################### - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [17-test_cloned_repo_object] - :end-before: # ![17-test_cloned_repo_object] - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [18-test_cloned_repo_object] - :end-before: # ![18-test_cloned_repo_object] - - - From 2c9c0c122d7dddce62d593f564d2b0c6f7a33e69 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 12:05:18 -0400 Subject: [PATCH 037/152] Added warning about index add --- doc/source/quickstart.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 11f8123bb..693562b17 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -138,6 +138,8 @@ Now lets add the updated file to git Notice the add method requires a list as a parameter +Warning: If you experience any trouble with this, try to invoke :class:`git ` instead via repo.git.add(path) + * $ git commit -m message .. literalinclude:: ../../test/test_quick_doc.py From d276107039d69bb3ad32595756b70fd4e51267d1 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 12:08:19 -0400 Subject: [PATCH 038/152] Updated generic sha hash --- test/test_quick_doc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 0aa27a361..5aa5664bc 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -68,8 +68,8 @@ def test_cloned_repo_object(self, local_dir): commits_for_file = [c for c in commits_for_file_generator] commits_for_file - # Outputs: [, - # ] + # Outputs: [, + # ] # ![6-test_cloned_repo_object] # Untracked files - create new file @@ -128,9 +128,9 @@ def test_cloned_repo_object(self, local_dir): files_and_dirs # Output - # [, - # , - # ] + # [, + # , + # ] # ![14-test_cloned_repo_object] From f3968f2e34467735935ee7a39a7d2b2f07229e7d Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 12:19:44 -0400 Subject: [PATCH 039/152] Removed all reference to source code --- doc/source/quickstart.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 693562b17..0cbb4f45c 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -9,8 +9,6 @@ GitPython Quick Start Tutorial ============================== Welcome to the GitPython Quickstart Guide! Designed for developers seeking a practical and interactive learning experience, this concise resource offers step-by-step code snippets to swiftly initialize/clone repositories, perform essential Git operations, and explore GitPython's capabilities. Get ready to dive in, experiment, and unleash the power of GitPython in your projects! -All code presented here originated from `***** insert link **** `_ 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. - Trees & Blobs ************** From 9ca25d767e554681ad9863138911800868c29b49 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 13:30:09 -0400 Subject: [PATCH 040/152] WIP major changes to structure to improve readability --- doc/source/quickstart.rst | 122 ++++++++++++++++++++------------------ test/test_quick_doc.py | 27 ++++++++- 2 files changed, 87 insertions(+), 62 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0cbb4f45c..f33d51600 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -10,6 +10,41 @@ GitPython Quick Start Tutorial Welcome to the GitPython Quickstart Guide! Designed for developers seeking a practical and interactive learning experience, this concise resource offers step-by-step code snippets to swiftly initialize/clone repositories, perform essential Git operations, and explore GitPython's capabilities. Get ready to dive in, experiment, and unleash the power of GitPython in your projects! +git.Repo +******** + +There are a few ways to create a :class:`git.Repo ` object + +Initialize a new git Repo +######################### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_init_repo_object] + :end-before: # ![1-test_init_repo_object] + +Existing local git Repo +####################### + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_init_repo_object] + :end-before: # ![2-test_init_repo_object] + +Clone from URL +############## + +For the rest of this tutorial we will use a clone from https://github.com/gitpython-developers/QuickStartTutorialFiles.git + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_cloned_repo_object] + :end-before: # ![1-test_cloned_repo_object] + + Trees & Blobs ************** @@ -40,6 +75,12 @@ Display level 1 Contents :start-after: # [14-test_cloned_repo_object] :end-before: # ![14-test_cloned_repo_object] +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14.1-test_cloned_repo_object] + :end-before: # ![14.1-test_cloned_repo_object] + Recurse through the Tree ######################## @@ -58,67 +99,10 @@ Recurse through the Tree -Printing text files -#################### - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [17-test_cloned_repo_object] - :end-before: # ![17-test_cloned_repo_object] - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [18-test_cloned_repo_object] - :end-before: # ![18-test_cloned_repo_object] - - - - - -git.Repo -******** - -There are a few ways to create a :class:`git.Repo ` object - -An existing local path -###################### - -$ git init path/to/dir - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [1-test_init_repo_object] - :end-before: # ![1-test_init_repo_object] - -Existing local git Repo -####################### - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [2-test_init_repo_object] - :end-before: # ![2-test_init_repo_object] - -Clone from URL -############## - -For the rest of this tutorial we will use a clone from https://github.com/gitpython-developers/QuickStartTutorialFiles.git - -$ git clone https://github.com/gitpython-developers/QuickStartTutorialFiles.git - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [1-test_cloned_repo_object] - :end-before: # ![1-test_cloned_repo_object] - Usage **************** -* $ git add filepath +* $ git add .. literalinclude:: ../../test/test_quick_doc.py :language: python @@ -146,7 +130,7 @@ Warning: If you experience any trouble with this, try to invoke :class:`git A list of commits associated with a file @@ -166,6 +150,24 @@ Notice this returns a generator object returns list of :class:`Commit ` objects +Printing text files +#################### +Lets print the latest version of ` dir1/file2.txt` + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [17-test_cloned_repo_object] + :end-before: # ![17-test_cloned_repo_object] + +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18-test_cloned_repo_object] + :end-before: # ![18-test_cloned_repo_object] + +Previous version of `/dir1/file2.txt` + * $ git status * Untracked files @@ -207,3 +209,5 @@ returns list of :class:`Commit ` objects :end-before: # ![11-test_cloned_repo_object] + + diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 5aa5664bc..61b8082d0 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -15,6 +15,8 @@ def tearDown(self): def test_init_repo_object(self, path_to_dir): # [1-test_init_repo_object] + # $ git init + from git import Repo repo = Repo.init(path_to_dir) # git init path/to/dir @@ -31,6 +33,8 @@ def test_cloned_repo_object(self, local_dir): import git # code to clone from url # [1-test_cloned_repo_object] + # $ git clone + repo_url = "https://github.com/gitpython-developers/QuickStartTutorialFiles.git" repo = Repo.clone_from(repo_url, local_dir) @@ -128,12 +132,22 @@ def test_cloned_repo_object(self, local_dir): files_and_dirs # Output - # [, - # , - # ] + # [, + # , + # ] # ![14-test_cloned_repo_object] + # [14.1-test_cloned_repo_object] + files_and_dirs = [(entry, entry.name) for entry in tree] + files_and_dirs + + # Output + # [(< git.Tree "SHA1-HEX_HASH" >, 'Downloads', 'tree'), + # (< git.Tree "SHA1-HEX_HASH" >, 'dir1', 'tree'), + # (< git.Blob "SHA1-HEX_HASH" >, 'file4.txt', 'blob')] + # ![14.1-test_cloned_repo_object] + # [15-test_cloned_repo_object] def print_files_from_git(root, level=0): for entry in root: @@ -163,6 +177,13 @@ def print_files_from_git(root, level=0): # Output # ![17-test_cloned_repo_object] + # print pre + # [17.1-test_cloned_repo_object] + commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + blob = tree[print_file] + + # ![17.1-test_cloned_repo_object] + # [18-test_cloned_repo_object] blob = tree[print_file] print(blob.data_stream.read().decode()) From 7fa57e5e30e56e7aa247cf77d1b84ddf5d08d1e7 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Sun, 16 Jul 2023 18:33:24 -0400 Subject: [PATCH 041/152] Added new section to print prev file --- doc/source/quickstart.rst | 6 ++++++ test/test_quick_doc.py | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index f33d51600..29c500a72 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -168,6 +168,12 @@ Lets print the latest version of ` dir1/file2.txt` Previous version of `/dir1/file2.txt` +.. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18.1-test_cloned_repo_object] + :end-before: # ![18.1-test_cloned_repo_object] + * $ git status * Untracked files diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 61b8082d0..0397eb6d9 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -177,13 +177,7 @@ def print_files_from_git(root, level=0): # Output # ![17-test_cloned_repo_object] - # print pre - # [17.1-test_cloned_repo_object] - commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] - blob = tree[print_file] - - # ![17.1-test_cloned_repo_object] - + # print latest file # [18-test_cloned_repo_object] blob = tree[print_file] print(blob.data_stream.read().decode()) @@ -191,7 +185,16 @@ def print_files_from_git(root, level=0): # Output # file 2 version 1 # Update version 2 - # ![18-test_cloned_repo_object] + # print previous tree + # [18.1-test_cloned_repo_object] + commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + tree = commits_for_file[-1].tree # gets the first commit tree + blob = tree[print_file] + + print(blob.data_stream.read().decode()) + # Output + # file 2 version 1 + # ![18.1-test_cloned_repo_object] \ No newline at end of file From 9d878af964947a09e74f29e3a13b5a26d606e86f Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 17 Jul 2023 14:21:17 -0400 Subject: [PATCH 042/152] change to formatting - removed = bash cmds --- doc/source/quickstart.rst | 166 +++++++++++++++++++------------------- test/test_quick_doc.py | 12 +-- 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 29c500a72..01a664e9c 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -102,117 +102,119 @@ Recurse through the Tree Usage **************** -* $ git add - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [2-test_cloned_repo_object] - :end-before: # ![2-test_cloned_repo_object] - -Now lets add the updated file to git - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [3-test_cloned_repo_object] - :end-before: # ![3-test_cloned_repo_object] - -Notice the add method requires a list as a parameter - -Warning: If you experience any trouble with this, try to invoke :class:`git ` instead via repo.git.add(path) - -* $ git commit -m message - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [4-test_cloned_repo_object] - :end-before: # ![4-test_cloned_repo_object] - -* $ git log - -A list of commits associated with a file - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [5-test_cloned_repo_object] - :end-before: # ![5-test_cloned_repo_object] - -Notice this returns a generator object - -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [6-test_cloned_repo_object] - :end-before: # ![6-test_cloned_repo_object] - -returns list of :class:`Commit ` objects - -Printing text files -#################### -Lets print the latest version of ` dir1/file2.txt` +Add file to staging area +######################## -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [17-test_cloned_repo_object] - :end-before: # ![17-test_cloned_repo_object] -.. literalinclude:: ../../test/test_quick_doc.py + .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [18-test_cloned_repo_object] - :end-before: # ![18-test_cloned_repo_object] + :start-after: # [2-test_cloned_repo_object] + :end-before: # ![2-test_cloned_repo_object] -Previous version of `/dir1/file2.txt` + Now lets add the updated file to git -.. literalinclude:: ../../test/test_quick_doc.py + .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [18.1-test_cloned_repo_object] - :end-before: # ![18.1-test_cloned_repo_object] + :start-after: # [3-test_cloned_repo_object] + :end-before: # ![3-test_cloned_repo_object] -* $ git status + Notice the add method requires a list as a parameter - * Untracked files + Warning: If you experience any trouble with this, try to invoke :class:`git ` instead via repo.git.add(path) - Lets create a new file +Commit +###### .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [7-test_cloned_repo_object] - :end-before: # ![7-test_cloned_repo_object] + :start-after: # [4-test_cloned_repo_object] + :end-before: # ![4-test_cloned_repo_object] + +List of commits associated with a file +####################################### .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [8-test_cloned_repo_object] - :end-before: # ![8-test_cloned_repo_object] + :start-after: # [5-test_cloned_repo_object] + :end-before: # ![5-test_cloned_repo_object] - * Modified files + Notice this returns a generator object .. literalinclude:: ../../test/test_quick_doc.py :language: python :dedent: 8 - :start-after: # [9-test_cloned_repo_object] - :end-before: # ![9-test_cloned_repo_object] + :start-after: # [6-test_cloned_repo_object] + :end-before: # ![6-test_cloned_repo_object] + + returns list of :class:`Commit ` objects + +Printing text files +#################### +Lets print the latest version of ` dir1/file2.txt` .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [10-test_cloned_repo_object] - :end-before: # ![10-test_cloned_repo_object] + :language: python + :dedent: 8 + :start-after: # [17-test_cloned_repo_object] + :end-before: # ![17-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [18-test_cloned_repo_object] + :end-before: # ![18-test_cloned_repo_object] - returns a list of :class:`Diff ` objects + Previous version of `/dir1/file2.txt` .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [11-test_cloned_repo_object] - :end-before: # ![11-test_cloned_repo_object] + :language: python + :dedent: 8 + :start-after: # [18.1-test_cloned_repo_object] + :end-before: # ![18.1-test_cloned_repo_object] + +Status +###### + * Untracked files + + Lets create a new file + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [7-test_cloned_repo_object] + :end-before: # ![7-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [8-test_cloned_repo_object] + :end-before: # ![8-test_cloned_repo_object] + + * Modified files + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [9-test_cloned_repo_object] + :end-before: # ![9-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [10-test_cloned_repo_object] + :end-before: # ![10-test_cloned_repo_object] + + returns a list of :class:`Diff ` objects + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11-test_cloned_repo_object] + :end-before: # ![11-test_cloned_repo_object] diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 0397eb6d9..4ab2a59a6 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -50,16 +50,20 @@ def test_cloned_repo_object(self, local_dir): # ![2-test_cloned_repo_object] # [3-test_cloned_repo_object] + # $ git add add_file = [update_file] # relative path from git root repo.index.add(add_file) # notice the add function requires a list of paths # ![3-test_cloned_repo_object] # code to commit - not sure how to test this # [4-test_cloned_repo_object] + # $ git commit -m repo.index.commit("Update to file2") # ![4-test_cloned_repo_object] # [5-test_cloned_repo_object] + # $ git log + # relative path from git root repo.iter_commits(all=True, max_count=10, paths=update_file) # gets the last 10 commits from all branches @@ -78,15 +82,13 @@ def test_cloned_repo_object(self, local_dir): # Untracked files - create new file # [7-test_cloned_repo_object] - # We'll create a file5.txt - - with open(f'{local_dir}/file5.txt', 'w') as f: - f.write('file5 version 1') + f = open(f'{local_dir}/untracked.txt', 'w') # creates an empty file + f.close() # ![7-test_cloned_repo_object] # [8-test_cloned_repo_object] repo.untracked_files - # Output: ['file5.txt'] + # Output: ['untracked.txt'] # ![8-test_cloned_repo_object] # Modified files From 315405d9395ff94348d43912d15471e6dd465100 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Mon, 17 Jul 2023 18:49:34 -0400 Subject: [PATCH 043/152] formatting wip --- test/test_quick_doc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 4ab2a59a6..c09845a6a 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -93,7 +93,7 @@ def test_cloned_repo_object(self, local_dir): # Modified files # [9-test_cloned_repo_object] - # Lets modify one of our tracked files + # Let's modify one of our tracked files with open(f'{local_dir}/Downloads/file3.txt', 'w') as f: f.write('file3 version 2') # overwrite file 3 @@ -174,7 +174,7 @@ def print_files_from_git(root, level=0): # Printing text files # [17-test_cloned_repo_object] print_file = 'dir1/file2.txt' - tree[print_file] + tree[print_file] # the head commit tree # Output # ![17-test_cloned_repo_object] From bccf8bc3ee2384048548e717e64a5d42156ba236 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 00:16:07 -0400 Subject: [PATCH 044/152] added new section for diffs and formatting --- doc/source/quickstart.rst | 24 ++++++++++++++++++++++++ test/test_quick_doc.py | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 01a664e9c..0826dec29 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -216,6 +216,30 @@ Status :start-after: # [11-test_cloned_repo_object] :end-before: # ![11-test_cloned_repo_object] +Diffs +###### + + Compare staging area to head commit + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.1-test_cloned_repo_object] + :end-before: # ![11.1-test_cloned_repo_object] + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.2-test_cloned_repo_object] + :end-before: # ![11.2-test_cloned_repo_object] + + Compare commit to commit + + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [11.3-test_cloned_repo_object] + :end-before: # ![11.3-test_cloned_repo_object] diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index c09845a6a..f79a9645e 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -19,7 +19,7 @@ def test_init_repo_object(self, path_to_dir): from git import Repo - repo = Repo.init(path_to_dir) # git init path/to/dir + repo = Repo.init(path_to_dir) # ![1-test_init_repo_object] # [2-test_init_repo_object] @@ -111,10 +111,43 @@ def test_cloned_repo_object(self, local_dir): for d in diffs: print(d.a_path) + # Output # Downloads/file3.txt - # file4.txt # ![11-test_cloned_repo_object] + # compares staging area to head commit + # [11.1-test_cloned_repo_object] + diffs = repo.index.diff(repo.head.commit) + for d in diffs: + print(d.a_path) + + # Output + + # ![11.1-test_cloned_repo_object] + # [11.2-test_cloned_repo_object] + # lets add untracked.txt + repo.index.add(['untracked.txt']) + diffs = repo.index.diff(repo.head.commit) + for d in diffs: + print(d.a_path) + + # Output + # untracked.txt + # ![11.2-test_cloned_repo_object] + + # Compare commit to commit + # [11.3-test_cloned_repo_object] + first_commit = [c for c in repo.iter_commits(all=True)][-1] + diffs = repo.head.commit.diff(first_commit) + for d in diffs: + print(d.a_path) + + # Output + # dir1/file2.txt + # ![11.3-test_cloned_repo_object] + + + '''Trees and Blobs''' # Latest commit tree @@ -141,7 +174,7 @@ def test_cloned_repo_object(self, local_dir): # ![14-test_cloned_repo_object] # [14.1-test_cloned_repo_object] - files_and_dirs = [(entry, entry.name) for entry in tree] + files_and_dirs = [(entry, entry.name, entry.type) for entry in tree] files_and_dirs # Output From cad1e2e835b0b7876277c0514bcba2ac6fedab81 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 00:19:21 -0400 Subject: [PATCH 045/152] tabbed all code-blocks --- doc/source/quickstart.rst | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0826dec29..2b6c1c99f 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -18,31 +18,31 @@ There are a few ways to create a :class:`git.Repo ` object Initialize a new git Repo ######################### -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [1-test_init_repo_object] - :end-before: # ![1-test_init_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_init_repo_object] + :end-before: # ![1-test_init_repo_object] Existing local git Repo ####################### -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [2-test_init_repo_object] - :end-before: # ![2-test_init_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [2-test_init_repo_object] + :end-before: # ![2-test_init_repo_object] Clone from URL ############## For the rest of this tutorial we will use a clone from https://github.com/gitpython-developers/QuickStartTutorialFiles.git -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [1-test_cloned_repo_object] - :end-before: # ![1-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [1-test_cloned_repo_object] + :end-before: # ![1-test_cloned_repo_object] Trees & Blobs @@ -51,50 +51,50 @@ Trees & Blobs Latest Commit Tree ################## -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [12-test_cloned_repo_object] - :end-before: # ![12-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [12-test_cloned_repo_object] + :end-before: # ![12-test_cloned_repo_object] Any Commit Tree ############### -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [13-test_cloned_repo_object] - :end-before: # ![13-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [13-test_cloned_repo_object] + :end-before: # ![13-test_cloned_repo_object] Display level 1 Contents ######################## -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [14-test_cloned_repo_object] - :end-before: # ![14-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14-test_cloned_repo_object] + :end-before: # ![14-test_cloned_repo_object] -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [14.1-test_cloned_repo_object] - :end-before: # ![14.1-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [14.1-test_cloned_repo_object] + :end-before: # ![14.1-test_cloned_repo_object] Recurse through the Tree ######################## -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [15-test_cloned_repo_object] - :end-before: # ![15-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [15-test_cloned_repo_object] + :end-before: # ![15-test_cloned_repo_object] -.. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [16-test_cloned_repo_object] - :end-before: # ![16-test_cloned_repo_object] + .. literalinclude:: ../../test/test_quick_doc.py + :language: python + :dedent: 8 + :start-after: # [16-test_cloned_repo_object] + :end-before: # ![16-test_cloned_repo_object] From 7e589f3d852461e2c143035c1cc3ceb1a81ecd61 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 00:29:44 -0400 Subject: [PATCH 046/152] fixed tabbing --- doc/source/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 2b6c1c99f..ebebc37d1 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -219,7 +219,7 @@ Status Diffs ###### - Compare staging area to head commit +Compare staging area to head commit .. literalinclude:: ../../test/test_quick_doc.py :language: python @@ -233,7 +233,7 @@ Diffs :start-after: # [11.2-test_cloned_repo_object] :end-before: # ![11.2-test_cloned_repo_object] - Compare commit to commit +Compare commit to commit .. literalinclude:: ../../test/test_quick_doc.py :language: python From 2a45f94d976e3cb91a7e700649eeea12f6655f7c Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 00:38:38 -0400 Subject: [PATCH 047/152] redundant line --- test/test_quick_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index f79a9645e..cea96690e 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -101,7 +101,7 @@ def test_cloned_repo_object(self, local_dir): # [10-test_cloned_repo_object] repo.index.diff(None) # compares staging area to working directory - repo.index.diff(repo.head.commit) # compares staging area to last commit + # Output: [, # ] # ![10-test_cloned_repo_object] From ef4d6d52fe02b7006224765cb65c824b8eca91e5 Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 15:18:24 -0400 Subject: [PATCH 048/152] redundant code cell --- doc/source/quickstart.rst | 7 ------- test/test_quick_doc.py | 13 +------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index ebebc37d1..33ddf5901 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -75,12 +75,6 @@ Display level 1 Contents :start-after: # [14-test_cloned_repo_object] :end-before: # ![14-test_cloned_repo_object] - .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [14.1-test_cloned_repo_object] - :end-before: # ![14.1-test_cloned_repo_object] - Recurse through the Tree ######################## @@ -242,4 +236,3 @@ Compare commit to commit :end-before: # ![11.3-test_cloned_repo_object] - diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index cea96690e..cb782aa3c 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -163,17 +163,6 @@ def test_cloned_repo_object(self, local_dir): # Iterating through tree # [14-test_cloned_repo_object] - files_and_dirs = [entry for entry in tree] - files_and_dirs - - # Output - # [, - # , - # ] - - # ![14-test_cloned_repo_object] - - # [14.1-test_cloned_repo_object] files_and_dirs = [(entry, entry.name, entry.type) for entry in tree] files_and_dirs @@ -181,7 +170,7 @@ def test_cloned_repo_object(self, local_dir): # [(< git.Tree "SHA1-HEX_HASH" >, 'Downloads', 'tree'), # (< git.Tree "SHA1-HEX_HASH" >, 'dir1', 'tree'), # (< git.Blob "SHA1-HEX_HASH" >, 'file4.txt', 'blob')] - # ![14.1-test_cloned_repo_object] + # ![14-test_cloned_repo_object] # [15-test_cloned_repo_object] def print_files_from_git(root, level=0): From 8138b3a56d16f68cfe6a5d9371e2fde3d587161c Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Tue, 18 Jul 2023 15:25:43 -0400 Subject: [PATCH 049/152] generic hash --- test/test_quick_doc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index cb782aa3c..eaee4e581 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -77,7 +77,7 @@ def test_cloned_repo_object(self, local_dir): commits_for_file # Outputs: [, - # ] + # ] # ![6-test_cloned_repo_object] # Untracked files - create new file @@ -198,7 +198,7 @@ def print_files_from_git(root, level=0): print_file = 'dir1/file2.txt' tree[print_file] # the head commit tree - # Output + # Output # ![17-test_cloned_repo_object] # print latest file From 84885a3ea412261adf457aee1c6471606ba7095c Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Wed, 19 Jul 2023 13:47:28 -0400 Subject: [PATCH 050/152] added more resources section --- doc/source/quickstart.rst | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 33ddf5901..2a9e41e62 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -222,17 +222,23 @@ Compare staging area to head commit :end-before: # ![11.1-test_cloned_repo_object] .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [11.2-test_cloned_repo_object] - :end-before: # ![11.2-test_cloned_repo_object] + :language: python + :dedent: 8 + :start-after: # [11.2-test_cloned_repo_object] + :end-before: # ![11.2-test_cloned_repo_object] Compare commit to commit .. literalinclude:: ../../test/test_quick_doc.py - :language: python - :dedent: 8 - :start-after: # [11.3-test_cloned_repo_object] - :end-before: # ![11.3-test_cloned_repo_object] + :language: python + :dedent: 8 + :start-after: # [11.3-test_cloned_repo_object] + :end-before: # ![11.3-test_cloned_repo_object] + +More Resources +**************** +Remember, this is just the beginning! There's a lot more you can achieve with GitPython in your development workflow. +To explore further possibilities and discover advanced features, check out the full :ref:`GitPython tutorial ` +and the :ref:`API Reference `. Happy coding! From cf3a899ebd498bd8053bc17dab1ff4c36edc005e Mon Sep 17 00:00:00 2001 From: LeoDaCoda Date: Wed, 19 Jul 2023 13:50:25 -0400 Subject: [PATCH 051/152] typo --- doc/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 2a9e41e62..c5930eb8a 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -148,7 +148,7 @@ List of commits associated with a file Printing text files #################### -Lets print the latest version of ` dir1/file2.txt` +Lets print the latest version of `/dir1/file2.txt` .. literalinclude:: ../../test/test_quick_doc.py :language: python From 31ac93b2f38f2410e41bf90ad28dff31e79b114e Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:25:10 +0200 Subject: [PATCH 052/152] Do not typecheck submodule It has too many errors. Fixing them should be done in the separate project. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0d5ebf012..4d2014afb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ implicit_reexport = true # strict = true # TODO: remove when 'gitdb' is fully annotated +exclude = ["^git/ext/gitdb"] [[tool.mypy.overrides]] module = "gitdb.*" ignore_missing_imports = true From b55cf65cc96740c6128987ab0c07b43112bdfe31 Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:34:39 +0200 Subject: [PATCH 053/152] Define supported version for mypy --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4d2014afb..57988372a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ filterwarnings = 'ignore::DeprecationWarning' # filterwarnings ignore::WarningType # ignores those warnings [tool.mypy] +python_version = "3.7" disallow_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true From 76394d42ce2a33b4db71fd64763c1e9dae136747 Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:39:32 +0200 Subject: [PATCH 054/152] Ignore remaining [unreachable] type errors --- git/__init__.py | 2 +- git/config.py | 4 ++-- git/repo/base.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index cd6602bf0..6196a42d7 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -76,7 +76,7 @@ def refresh(path: Optional[PathLike] = None) -> None: if not Git.refresh(path=path): return if not FetchInfo.refresh(): - return + return # type: ignore [unreachable] GIT_OK = True diff --git a/git/config.py b/git/config.py index e05a297af..caf1f6241 100644 --- a/git/config.py +++ b/git/config.py @@ -265,8 +265,8 @@ def get_config_path(config_level: Lit_config_levels) -> str: raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") else: # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs - assert_never( - config_level, # type: ignore[unreachable] + assert_never( # type: ignore[unreachable] + config_level, ValueError(f"Invalid configuration level: {config_level!r}"), ) diff --git a/git/repo/base.py b/git/repo/base.py index 1fa98d8c7..4bfead46f 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -549,8 +549,8 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa return osp.normpath(osp.join(repo_dir, "config")) else: - assert_never( - config_level, # type:ignore[unreachable] + assert_never( # type:ignore[unreachable] + config_level, ValueError(f"Invalid configuration level: {config_level!r}"), ) From c6dab191d1f96373aaae5c6c117f13c1006631de Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:49:02 +0200 Subject: [PATCH 055/152] Allow explicit casting even when slightly redundant --- git/cmd.py | 2 +- git/objects/commit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index dfce9024d..5b0b6b816 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -154,7 +154,7 @@ def pump_stream( p_stdout = process.proc.stdout if process.proc else None p_stderr = process.proc.stderr if process.proc else None else: - process = cast(Popen, process) + process = cast(Popen, process) # type: ignore [redundant-cast] cmdline = getattr(process, "args", "") p_stdout = process.stdout p_stderr = process.stderr diff --git a/git/objects/commit.py b/git/objects/commit.py index 138db0afe..6cca65c1f 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -460,7 +460,7 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, if proc_or_stream.stdout is not None: stream = proc_or_stream.stdout elif hasattr(proc_or_stream, "readline"): - proc_or_stream = cast(IO, proc_or_stream) + proc_or_stream = cast(IO, proc_or_stream) # type: ignore [redundant-cast] stream = proc_or_stream readline = stream.readline From 6035db092decf72e6a01e175d044f0343818b51c Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:51:50 +0200 Subject: [PATCH 056/152] Run black and exclude submodule --- README.md | 2 ++ git/cmd.py | 7 ++----- git/config.py | 3 +-- git/diff.py | 3 +-- git/exc.py | 2 -- git/index/fun.py | 1 - git/objects/commit.py | 4 +--- git/objects/fun.py | 1 - git/objects/util.py | 2 +- git/remote.py | 1 - git/repo/base.py | 3 +-- git/util.py | 2 -- pyproject.toml | 1 + 13 files changed, 10 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 676d2c6d6..30c54b57f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ To typecheck, run: `mypy -p git` To test, run: `pytest` +For automatic code formatting run: `black git` + Configuration for flake8 is in the ./.flake8 file. Configurations for mypy, pytest and coverage.py are in ./pyproject.toml. diff --git a/git/cmd.py b/git/cmd.py index 5b0b6b816..84d888494 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -122,6 +122,7 @@ def handle_process_output( To specify a timeout in seconds for the git command, after which the process should be killed. """ + # Use 2 "pump" threads and wait for both to finish. def pump_stream( cmdline: List[str], @@ -488,10 +489,7 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> """ # Options can be of the form `foo` or `--foo bar` `--foo=bar`, # so we need to check if they start with "--foo" or if they are equal to "foo". - bare_unsafe_options = [ - option.lstrip("-") - for option in unsafe_options - ] + bare_unsafe_options = [option.lstrip("-") for option in unsafe_options] for option in options: for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options): if option.startswith(unsafe_option) or option == bare_option: @@ -1194,7 +1192,6 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any @classmethod def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: - outlist = [] if isinstance(arg_list, (list, tuple)): for arg in arg_list: diff --git a/git/config.py b/git/config.py index caf1f6241..1973111eb 100644 --- a/git/config.py +++ b/git/config.py @@ -248,7 +248,6 @@ def items_all(self) -> List[Tuple[str, List[_T]]]: def get_config_path(config_level: Lit_config_levels) -> str: - # we do not support an absolute path of the gitconfig on windows , # use the global config instead if is_win and config_level == "system": @@ -655,7 +654,7 @@ def write_section(name: str, section_dict: _OMD) -> None: values: Sequence[str] # runtime only gets str in tests, but should be whatever _OMD stores v: str - for (key, values) in section_dict.items_all(): + for key, values in section_dict.items_all(): if key == "__name__": continue diff --git a/git/diff.py b/git/diff.py index c1a5bd26f..1424ff3ad 100644 --- a/git/diff.py +++ b/git/diff.py @@ -145,7 +145,7 @@ def diff( args.append("--full-index") # get full index paths, not only filenames # remove default '-M' arg (check for renames) if user is overriding it - if not any(x in kwargs for x in ('find_renames', 'no_renames', 'M')): + if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): args.append("-M") if create_patch: @@ -338,7 +338,6 @@ def __init__( change_type: Optional[Lit_change_type], score: Optional[int], ) -> None: - assert a_rawpath is None or isinstance(a_rawpath, bytes) assert b_rawpath is None or isinstance(b_rawpath, bytes) self.a_rawpath = a_rawpath diff --git a/git/exc.py b/git/exc.py index 9b69a5889..775528bf6 100644 --- a/git/exc.py +++ b/git/exc.py @@ -139,7 +139,6 @@ def __init__( valid_files: Sequence[PathLike], failed_reasons: List[str], ) -> None: - Exception.__init__(self, message) self.failed_files = failed_files self.failed_reasons = failed_reasons @@ -170,7 +169,6 @@ def __init__( stderr: Union[bytes, str, None] = None, stdout: Union[bytes, str, None] = None, ) -> None: - super(HookExecutionError, self).__init__(command, status, stderr, stdout) self._msg = "Hook('%s') failed%s" diff --git a/git/index/fun.py b/git/index/fun.py index d0925ed51..3dc5e96d2 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -394,7 +394,6 @@ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> out.append(_tree_entry_to_baseindexentry(theirs, 0)) # END handle modification else: - if ours[0] != base[0] or ours[1] != base[1]: # they deleted it, we changed it, conflict out.append(_tree_entry_to_baseindexentry(base, 1)) diff --git a/git/objects/commit.py b/git/objects/commit.py index 6cca65c1f..6db3ea0f3 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -345,9 +345,7 @@ def trailers(self) -> Dict[str, str]: Dictionary containing whitespace stripped trailer information. Only contains the latest instance of each trailer key. """ - return { - k: v[0] for k, v in self.trailers_dict.items() - } + return {k: v[0] for k, v in self.trailers_dict.items()} @property def trailers_list(self) -> List[Tuple[str, str]]: diff --git a/git/objects/fun.py b/git/objects/fun.py index e91403a8b..043eec721 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -190,7 +190,6 @@ def traverse_trees_recursive( # is a tree. If the match is a non-tree item, put it into the result. # Processed items will be set None for ti, tree_data in enumerate(trees_data): - for ii, item in enumerate(tree_data): if not item: continue diff --git a/git/objects/util.py b/git/objects/util.py index af279154c..d72c04d17 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -143,7 +143,7 @@ def utctz_to_altz(utctz: str) -> int: :param utctz: git utc timezone string, i.e. +0200 """ int_utctz = int(utctz) - seconds = ((abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60) + seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60 return seconds if int_utctz < 0 else -seconds diff --git a/git/remote.py b/git/remote.py index 5886a69f0..95a2b8ac6 100644 --- a/git/remote.py +++ b/git/remote.py @@ -826,7 +826,6 @@ def _get_fetch_info_from_stderr( progress: Union[Callable[..., Any], RemoteProgress, None], kill_after_timeout: Union[None, float] = None, ) -> IterableList["FetchInfo"]: - progress = to_progress_instance(progress) # skip first line as it is some remote info we are not interested in diff --git a/git/repo/base.py b/git/repo/base.py index 4bfead46f..723613c6f 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -498,7 +498,7 @@ def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None: def create_tag( self, path: PathLike, - ref: Union[str, 'SymbolicReference'] = "HEAD", + ref: Union[str, "SymbolicReference"] = "HEAD", message: Optional[str] = None, force: bool = False, **kwargs: Any, @@ -548,7 +548,6 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa else: return osp.normpath(osp.join(repo_dir, "config")) else: - assert_never( # type:ignore[unreachable] config_level, ValueError(f"Invalid configuration level: {config_level!r}"), diff --git a/git/util.py b/git/util.py index 30028b1c2..5bfe11cd8 100644 --- a/git/util.py +++ b/git/util.py @@ -1083,7 +1083,6 @@ def __getattr__(self, attr: str) -> T_IterableObj: return list.__getattribute__(self, attr) def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore - assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str" if isinstance(index, int): @@ -1098,7 +1097,6 @@ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_Iterabl # END handle getattr def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None: - assert isinstance(index, (int, str)), "Index of IterableList should be an int or str" delindex = cast(int, index) diff --git a/pyproject.toml b/pyproject.toml index 57988372a..32c9d4a26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,3 +45,4 @@ omit = ["*/git/ext/*"] [tool.black] line-length = 120 target-version = ['py37'] +exclude = "git/ext/gitdb" From 3908e79baf27b3d65265ca75db216f9368748351 Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 16:54:10 +0200 Subject: [PATCH 057/152] Add missing type annotation --- git/index/fun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/index/fun.py b/git/index/fun.py index 3dc5e96d2..4a2f3cb6d 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -76,7 +76,7 @@ def hook_path(name: str, git_dir: PathLike) -> str: return osp.join(git_dir, "hooks", name) -def _has_file_extension(path): +def _has_file_extension(path: str) -> str: return osp.splitext(path)[1] From f01ee4f8d0b83f06fc7ba5458ac896ac3b81184a Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 20 Jul 2023 17:21:41 +0200 Subject: [PATCH 058/152] Apply straight-forward typing fixes --- git/cmd.py | 2 +- git/index/base.py | 2 +- git/index/fun.py | 8 ++++---- git/objects/util.py | 2 +- git/util.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 84d888494..3d170facd 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -211,7 +211,7 @@ def dashify(string: str) -> str: return string.replace("_", "-") -def slots_to_dict(self: object, exclude: Sequence[str] = ()) -> Dict[str, Any]: +def slots_to_dict(self: "Git", exclude: Sequence[str] = ()) -> Dict[str, Any]: return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} diff --git a/git/index/base.py b/git/index/base.py index dd8f9aa2e..193baf3ad 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -656,7 +656,7 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry def _entries_for_paths( self, paths: List[str], - path_rewriter: Callable, + path_rewriter: Union[Callable, None], fprogress: Callable, entries: List[BaseIndexEntry], ) -> List[BaseIndexEntry]: diff --git a/git/index/fun.py b/git/index/fun.py index 4a2f3cb6d..b50f1f465 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -102,7 +102,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix() cmd = ["bash.exe", relative_hp] - cmd = subprocess.Popen( + process = subprocess.Popen( cmd + list(args), env=env, stdout=subprocess.PIPE, @@ -116,13 +116,13 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: else: stdout_list: List[str] = [] stderr_list: List[str] = [] - handle_process_output(cmd, stdout_list.append, stderr_list.append, finalize_process) + handle_process_output(process, stdout_list.append, stderr_list.append, finalize_process) stdout = "".join(stdout_list) stderr = "".join(stderr_list) - if cmd.returncode != 0: + if process.returncode != 0: stdout = force_text(stdout, defenc) stderr = force_text(stderr, defenc) - raise HookExecutionError(hp, cmd.returncode, stderr, stdout) + raise HookExecutionError(hp, process.returncode, stderr, stdout) # end handle return code diff --git a/git/objects/util.py b/git/objects/util.py index d72c04d17..56938507e 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -147,7 +147,7 @@ def utctz_to_altz(utctz: str) -> int: return seconds if int_utctz < 0 else -seconds -def altz_to_utctz_str(altz: int) -> str: +def altz_to_utctz_str(altz: float) -> str: """Convert a timezone offset west of UTC in seconds into a git timezone offset string :param altz: timezone offset in seconds west of UTC diff --git a/git/util.py b/git/util.py index 5bfe11cd8..0ef8bdeb7 100644 --- a/git/util.py +++ b/git/util.py @@ -1049,7 +1049,7 @@ class IterableList(List[T_IterableObj]): __slots__ = ("_id_attr", "_prefix") - def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[IterableObj]": + def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[T_IterableObj]": return super(IterableList, cls).__new__(cls) def __init__(self, id_attr: str, prefix: str = "") -> None: From 41ecc6a4f80ecaa07ceac59861820c4b88dd5d1e Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Fri, 21 Jul 2023 09:21:46 +0200 Subject: [PATCH 059/152] Disable merge_includes in config writers --- git/repo/base.py | 2 +- test/test_refs.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 723613c6f..ab2026549 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -600,7 +600,7 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo system = system wide configuration file global = user level configuration file repository = configuration file for this repository only""" - return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self) + return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False) def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit: """The Commit object for the specified revision diff --git a/test/test_refs.py b/test/test_refs.py index 5bb83100e..4c421767e 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -15,6 +15,7 @@ SymbolicReference, GitCommandError, RefLog, + GitConfigParser, ) from git.objects.tag import TagObject from test.lib import TestBase, with_rw_repo @@ -172,6 +173,26 @@ def test_heads(self, rwrepo): assert log[0].oldhexsha == pcommit.NULL_HEX_SHA assert log[0].newhexsha == pcommit.hexsha + @with_rw_repo("HEAD", bare=False) + def test_set_tracking_branch_with_import(self, rwrepo): + # prepare included config file + included_config = osp.join(rwrepo.git_dir, "config.include") + with GitConfigParser(included_config, read_only=False) as writer: + writer.set_value("test", "value", "test") + assert osp.exists(included_config) + + with rwrepo.config_writer() as writer: + writer.set_value("include", "path", included_config) + + for head in rwrepo.heads: + head.set_tracking_branch(None) + assert head.tracking_branch() is None + remote_ref = rwrepo.remotes[0].refs[0] + assert head.set_tracking_branch(remote_ref) is head + assert head.tracking_branch() == remote_ref + head.set_tracking_branch(None) + assert head.tracking_branch() is None + def test_refs(self): types_found = set() for ref in self.rorepo.refs: From 186c1ae12be1bb76087dd4fa53a1ac0979c8aa9f Mon Sep 17 00:00:00 2001 From: Patrick Hagemeister Date: Thu, 27 Jul 2023 06:24:20 +0000 Subject: [PATCH 060/152] Creating a lock now uses python built-in "open()" method to work around docker virtiofs issue --- git/util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/git/util.py b/git/util.py index 0ef8bdeb7..a3748f0fe 100644 --- a/git/util.py +++ b/git/util.py @@ -935,11 +935,7 @@ def _obtain_lock_or_raise(self) -> None: ) try: - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL - if is_win: - flags |= os.O_SHORT_LIVED - fd = os.open(lock_file, flags, 0) - os.close(fd) + open(lock_file, mode='w', closefd=True) except OSError as e: raise IOError(str(e)) from e From 9f74c05b7d0f224bb170ad77675d0d2b0ff82a0d Mon Sep 17 00:00:00 2001 From: Lydia Date: Sun, 27 Aug 2023 18:28:59 +0200 Subject: [PATCH 061/152] feat: full typing for "progress" parameter This commit also fix a few inconsistency, most especially: - How mypy/Pylance are checking for if-statements - Tools complaining about not subscriptable class - Exporting imports --- git/repo/base.py | 5 +++-- git/types.py | 22 +++++++++++----------- requirements-dev.txt | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index ab2026549..113fca459 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -60,6 +60,7 @@ PathLike, Lit_config_levels, Commit_ish, + CallableProgress, Tree_ish, assert_never, ) @@ -1258,7 +1259,7 @@ def _clone( def clone( self, path: PathLike, - progress: Optional[Callable] = None, + progress: Optional[CallableProgress] = None, multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, allow_unsafe_options: bool = False, @@ -1297,7 +1298,7 @@ def clone_from( cls, url: PathLike, to_path: PathLike, - progress: Optional[Callable] = None, + progress: CallableProgress = None, env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, diff --git a/git/types.py b/git/types.py index 9064ecbf9..9f8621721 100644 --- a/git/types.py +++ b/git/types.py @@ -8,42 +8,39 @@ from typing import ( Dict, NoReturn, - Sequence, + Sequence as Sequence, Tuple, Union, Any, + Optional, + Callable, TYPE_CHECKING, TypeVar, ) # noqa: F401 -if sys.version_info[:2] >= (3, 8): +if sys.version_info >= (3, 8): from typing import ( Literal, - SupportsIndex, TypedDict, Protocol, + SupportsIndex as SupportsIndex, runtime_checkable, ) # noqa: F401 else: from typing_extensions import ( Literal, - SupportsIndex, # noqa: F401 + SupportsIndex as SupportsIndex, TypedDict, Protocol, runtime_checkable, ) # noqa: F401 -# if sys.version_info[:2] >= (3, 10): +# if sys.version_info >= (3, 10): # from typing import TypeGuard # noqa: F401 # else: # from typing_extensions import TypeGuard # noqa: F401 - -if sys.version_info[:2] < (3, 9): - PathLike = Union[str, os.PathLike] -else: - # os.PathLike only becomes subscriptable from Python 3.9 onwards - PathLike = Union[str, os.PathLike[str]] +PathLike = Union[str, "os.PathLike[str]"] if TYPE_CHECKING: from git.repo import Repo @@ -62,6 +59,9 @@ Lit_config_levels = Literal["system", "global", "user", "repository"] +# Progress parameter type alias ----------------------------------------- + +CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]] # def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]: # # return inp in get_args(Lit_config_level) # only py >= 3.8 diff --git a/requirements-dev.txt b/requirements-dev.txt index bacde3498..946b4c94f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pytest-icdiff # pytest-profiling -tox \ No newline at end of file +tox From 6029211d729a0dd81e08fcc9c1a3ab7fe9af85c9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 30 Aug 2023 09:32:11 -0400 Subject: [PATCH 062/152] Fix CVE-2023-40590 This fixes the path search bug where the current directory is included on Windows, by setting NoDefaultCurrentDirectoryInExePath for the caller. (Setting for the callee env would not work.) This sets it only on Windows, only for the duration of the Popen call, and then automatically unsets it or restores its old value. NoDefaultCurrentDirectoryInExePath is documented at: https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw It automatically affects the behavior of subprocess.Popen on Windows, due to the way Popen uses the Windows API. (In contrast, it does not, at least currently on CPython, affect the behavior of shutil.which. But shutil.which is not being used to find git.exe.) --- git/cmd.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 3d170facd..3665eb029 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php from __future__ import annotations import re -from contextlib import contextmanager +import contextlib import io import logging import os @@ -14,6 +14,7 @@ import subprocess import threading from textwrap import dedent +import unittest.mock from git.compat import ( defenc, @@ -963,8 +964,11 @@ def execute( redacted_command, '"kill_after_timeout" feature is not supported on Windows.', ) + # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value. + patch_caller_env = unittest.mock.patch.dict(os.environ, {"NoDefaultCurrentDirectoryInExePath": "1"}) else: cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + patch_caller_env = contextlib.nullcontext() # end handle stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") @@ -980,21 +984,21 @@ def execute( istream_ok, ) try: - proc = Popen( - command, - env=env, - cwd=cwd, - bufsize=-1, - stdin=istream or DEVNULL, - stderr=PIPE, - stdout=stdout_sink, - shell=shell is not None and shell or self.USE_SHELL, - close_fds=is_posix, # unsupported on windows - universal_newlines=universal_newlines, - creationflags=PROC_CREATIONFLAGS, - **subprocess_kwargs, - ) - + with patch_caller_env: + proc = Popen( + command, + env=env, + cwd=cwd, + bufsize=-1, + stdin=istream or DEVNULL, + stderr=PIPE, + stdout=stdout_sink, + shell=shell is not None and shell or self.USE_SHELL, + close_fds=is_posix, # unsupported on windows + universal_newlines=universal_newlines, + creationflags=PROC_CREATIONFLAGS, + **subprocess_kwargs, + ) except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err else: @@ -1144,7 +1148,7 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: del self._environment[key] return old_env - @contextmanager + @contextlib.contextmanager def custom_environment(self, **kwargs: Any) -> Iterator[None]: """ A context manager around the above ``update_environment`` method to restore the From 94e0fb0794b88b78ceed94ff18ee7d68587d890d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 30 Aug 2023 11:53:29 -0400 Subject: [PATCH 063/152] Add a unit test for CVE-2023-40590 This adds test_it_executes_git_not_from_cwd to verify that the execute method does not use "git.exe" in the current directory on Windows, nor "git" in the current directory on Unix-like systems, when those files are executable. It adds a _chdir helper context manager to support this, because contextlib.chdir is only available on Python 3.11 and later. --- test/test_git.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index c5d871f08..01572dc24 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -4,10 +4,12 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +import contextlib import os +import shutil import subprocess import sys -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from unittest import mock from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd @@ -20,6 +22,17 @@ from git.compat import is_win +@contextlib.contextmanager +def _chdir(new_dir): + """Context manager to temporarily change directory. Not reentrant.""" + old_dir = os.getcwd() + os.chdir(new_dir) + try: + yield + finally: + os.chdir(old_dir) + + class TestGit(TestBase): @classmethod def setUpClass(cls): @@ -75,6 +88,23 @@ def test_it_transforms_kwargs_into_git_command_arguments(self): def test_it_executes_git_to_shell_and_returns_result(self): self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") + def test_it_executes_git_not_from_cwd(self): + with TemporaryDirectory() as tmpdir: + if is_win: + # Copy an actual binary executable that is not git. + other_exe_path = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe") + impostor_path = os.path.join(tmpdir, "git.exe") + shutil.copy(other_exe_path, impostor_path) + else: + # Create a shell script that doesn't do anything. + impostor_path = os.path.join(tmpdir, "git") + with open(impostor_path, mode="w", encoding="utf-8") as file: + print("#!/bin/sh", file=file) + os.chmod(impostor_path, 0o755) + + with _chdir(tmpdir): + self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") + def test_it_accepts_stdin(self): filename = fixture_path("cat_file_blob") with open(filename, "r") as fh: From 7611cd909b890b971d23bce3bd4244ad1c381f22 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 30 Aug 2023 12:34:22 -0400 Subject: [PATCH 064/152] Don't check form of version number This changes the regex in test_it_executes_git_not_from_cwd so that (unlike test_it_executes_git_to_shell_and_returns_result) it only checks that the output starts with the words "git version", and not the form of whatever follows those words. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 01572dc24..540ea9f41 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -103,7 +103,7 @@ def test_it_executes_git_not_from_cwd(self): os.chmod(impostor_path, 0o755) with _chdir(tmpdir): - self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") + self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b") def test_it_accepts_stdin(self): filename = fixture_path("cat_file_blob") From 70924c4265c2d3629d978dd7bfc9ab1678d91e7d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 1 Sep 2023 08:11:22 +0200 Subject: [PATCH 065/152] Skip now permanently failing test with note on how to fix it --- test/test_repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_repo.py b/test/test_repo.py index 5c66aeeb1..08ed13a00 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -13,7 +13,7 @@ import pickle import sys import tempfile -from unittest import mock, skipIf, SkipTest +from unittest import mock, skipIf, SkipTest, skip import pytest @@ -251,6 +251,7 @@ def test_clone_from_with_path_contains_unicode(self): self.fail("Raised UnicodeEncodeError") @with_rw_directory + @skip("the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control") def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" try: From 993f04588aa362fdce7c7f2f0848b5daedd8cb72 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 1 Sep 2023 08:12:54 +0200 Subject: [PATCH 066/152] prepare for next release --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 381c34a62..f8d874e2b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.32 +3.1.33 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 3bc02e770..45062ac13 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.33 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/63?closed=1 + 3.1.32 ====== From f882cd8422fbb2517eebbf45824eb07951b948f3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 1 Sep 2023 08:48:02 +0200 Subject: [PATCH 067/152] update instructions for how to create a release --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 30c54b57f..1743bd3d2 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,7 @@ Please have a look at the [contributions file][contributing]. - Run `git tag -s ` to tag the version in Git - Run `make release` - Close the milestone mentioned in the _changelog_ and create a new one. _Do not reuse milestones by renaming them_. -- set the upcoming version in the `VERSION` file, usually be - incrementing the patch level, and possibly by appending `-dev`. Probably you - want to `git push` once more. +- Got to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog. ### How to verify a release (DEPRECATED) From 3e829eb516a60212bae81a6549361be4748e22d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saugat=20Pachhai=20=28=E0=A4=B8=E0=A5=8C=E0=A4=97=E0=A4=BE?= =?UTF-8?q?=E0=A4=A4=29?= Date: Sat, 2 Sep 2023 14:21:03 +0545 Subject: [PATCH 068/152] util: close lockfile after opening successfully Otherwise, this will leak file handles and can be a problem in Windows. Also, `closefd=true` is the default here, so need to pass it explicitly. Regression from #1619. I noticed after [our tests started raising `ResourceWarning`][1]. ```python Traceback (most recent call last): File "/opt/hostedtoolcache/Python/3.8.17/x64/lib/python3.8/site-packages/git/util.py", line 938, in _obtain_lock_or_raise open(lock_file, mode='w', closefd=True) ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/pytest-of-runner/pytest-0/popen-gw0/external0/project.git/.git/config.lock' mode='w' encoding='UTF-8'> ``` [1]: https://github.com/iterative/dvc/actions/runs/6055520480/job/16434544764#step:6:869 --- git/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index a3748f0fe..f6dedf0f2 100644 --- a/git/util.py +++ b/git/util.py @@ -935,7 +935,8 @@ def _obtain_lock_or_raise(self) -> None: ) try: - open(lock_file, mode='w', closefd=True) + with open(lock_file, mode='w'): + pass except OSError as e: raise IOError(str(e)) from e From 2a2ae776825f249a3bb7efd9b08650486226b027 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 2 Sep 2023 11:29:43 +0200 Subject: [PATCH 069/152] prepare patch release --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index f8d874e2b..e03213a2b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.33 +3.1.34 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 45062ac13..38e673f3f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.34 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/64?closed=1 + 3.1.33 ====== From b32e01e33d5e4913b349d2ccfc08e3981d8c9621 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:50:12 +0000 Subject: [PATCH 070/152] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index a1ecb6785..6ba63f019 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Force LF line endings run: git config --global core.autocrlf input - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 9999 - uses: cygwin/cygwin-install-action@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c78a4053a..5e79664a8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.x" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 2d95e6ffa..68988f2a7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -18,7 +18,7 @@ jobs: python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 9999 - name: Set up Python ${{ matrix.python-version }} From d5e763ea76d28cab14e16a4bc23572986fbf1797 Mon Sep 17 00:00:00 2001 From: "Wenhan Zhu (Cosmos)" Date: Tue, 5 Sep 2023 19:30:17 -0400 Subject: [PATCH 071/152] Fix 'Tree' object has no attribute '_name' when submodule path is normal path --- git/objects/submodule/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 7db64d705..0d20305c6 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1402,6 +1402,10 @@ def iter_items( # END handle keyerror # END handle critical error + # Make sure we are looking at a submodule object + if type(sm) != git.objects.submodule.base.Submodule: + continue + # fill in remaining info - saves time as it doesn't have to be parsed again sm._name = n if pc != repo.commit(): From 64ebb9fcdfbe48d5d61141a557691fd91f1e88d6 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 5 Sep 2023 09:51:50 +0200 Subject: [PATCH 072/152] Fix CVE-2023-41040 This change adds a check during reference resolving to see if it contains an up-level reference ('..'). If it does, it raises an exception. This fixes CVE-2023-41040, which allows an attacker to access files outside the repository's directory. --- git/refs/symbolic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 33c3bf15b..5c293aa7b 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -168,6 +168,8 @@ def _get_ref_info_helper( """Return: (str(sha), str(target_ref_path)) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" + if ".." in str(ref_path): + raise ValueError(f"Invalid reference '{ref_path}'") tokens: Union[None, List[str], Tuple[str, str]] = None repodir = _git_dir(repo, ref_path) try: From 65b8c6a2ccacdf26e751cd3bc3c5a7c9e5796b56 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 5 Sep 2023 13:49:38 +0200 Subject: [PATCH 073/152] Add test for CVE-2023-41040 --- test/test_refs.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_refs.py b/test/test_refs.py index 4c421767e..e7526c3b2 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -5,6 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php from itertools import chain +from pathlib import Path from git import ( Reference, @@ -20,9 +21,11 @@ from git.objects.tag import TagObject from test.lib import TestBase, with_rw_repo from git.util import Actor +from gitdb.exc import BadName import git.refs as refs import os.path as osp +import tempfile class TestRefs(TestBase): @@ -616,3 +619,15 @@ def test_dereference_recursive(self): def test_reflog(self): assert isinstance(self.rorepo.heads.master.log(), RefLog) + + def test_refs_outside_repo(self): + # Create a file containing a valid reference outside the repository. Attempting + # to access it should raise an exception, due to it containing a parent directory + # reference ('..'). This tests for CVE-2023-41040. + git_dir = Path(self.rorepo.git_dir) + repo_parent_dir = git_dir.parent.parent + with tempfile.NamedTemporaryFile(dir=repo_parent_dir) as ref_file: + ref_file.write(b"91b464cd624fe22fbf54ea22b85a7e5cca507cfe") + ref_file.flush() + ref_file_name = Path(ref_file.name).name + self.assertRaises(BadName, self.rorepo.commit, f"../../{ref_file_name}") From 537af83c344420994a6a34dd18623f132398d062 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 6 Sep 2023 17:44:25 -0400 Subject: [PATCH 074/152] Only set safe.directory on Cygwin (which needs it) This stops setting the current directory as an explicit safe directory on CI for non-Windows systems, where this is not needed because the repository has the ownership Git expects. The step name is updated accordingly to reflect its now narrower purpose. This also adds shell quoting to $(pwd) in the Cygwin workflow. In practice, on CI, the path is very unlikely to contain whitespace, but double-quoting $ expansions on which splitting and globbing are unwanted is more robust and better expresses intent. This also has the benefit that users who use the CI workflows as a guide to commands they run locally, where on Windows they may very well have spaces somewhere in this absolute path, will use a correct command. --- .github/workflows/cygwin-test.yml | 6 +++--- .github/workflows/pythonpackage.yml | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 6ba63f019..618bec405 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -12,7 +12,7 @@ jobs: SHELLOPTS: igncr TMP: "/tmp" TEMP: "/tmp" - + steps: - name: Force LF line endings run: git config --global core.autocrlf input @@ -24,8 +24,8 @@ jobs: packages: python39 python39-pip python39-virtualenv git - name: Tell git to trust this repo shell: bash.exe -eo pipefail -o igncr "{0}" - run: | - /usr/bin/git config --global --add safe.directory $(pwd) + run: | + /usr/bin/git config --global --add safe.directory "$(pwd)" /usr/bin/git config --global protocol.file.allow always - name: Install dependencies and prepare tests shell: bash.exe -eo pipefail -o igncr "{0}" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 68988f2a7..bc50cab29 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -52,9 +52,8 @@ jobs: set -x mypy -p git - - name: Tell git to trust this repo - run: | - /usr/bin/git config --global --add safe.directory $(pwd) + - name: Tell git to allow file protocol even for submodules + run: | /usr/bin/git config --global protocol.file.allow always - name: Test with pytest From 92d9ae22132c97f764f0108da30062c24392cc2b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 6 Sep 2023 18:02:20 -0400 Subject: [PATCH 075/152] Use env vars on CI to set protocol.file.allow Instead of setting a global git configuration. This makes no significant difference for security on CI, but it is an iterative step toward a more specific way of setting them that will apply on CI and locally and require less configuration. In addition, this shows an approach more similar to what users who do not want to carefully review the security impact of changing the global setting can use locally (and which is more secure). --- .github/workflows/cygwin-test.yml | 5 ++++- .github/workflows/pythonpackage.yml | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 618bec405..b612ca94a 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -26,7 +26,6 @@ jobs: shell: bash.exe -eo pipefail -o igncr "{0}" run: | /usr/bin/git config --global --add safe.directory "$(pwd)" - /usr/bin/git config --global protocol.file.allow always - name: Install dependencies and prepare tests shell: bash.exe -eo pipefail -o igncr "{0}" run: | @@ -47,4 +46,8 @@ jobs: shell: bash.exe -eo pipefail -o igncr "{0}" run: | /usr/bin/python -m pytest + env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: protocol.file.allow + GIT_CONFIG_VALUE_0: always continue-on-error: false diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index bc50cab29..207714810 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -52,14 +52,14 @@ jobs: set -x mypy -p git - - name: Tell git to allow file protocol even for submodules - run: | - /usr/bin/git config --global protocol.file.allow always - - name: Test with pytest run: | set -x pytest + env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: protocol.file.allow + GIT_CONFIG_VALUE_0: always continue-on-error: false - name: Documentation From 4f594cd2cbf68caabb7d3f22397104f5aa1b49b7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 6 Sep 2023 18:59:12 -0400 Subject: [PATCH 076/152] Set protocol.file.allow only in tests that need it Instead of setting environment variables just on CI and for the the entire pytest command, this has the two test cases that need protocol.file.allow to be set to "always" (instead of "user") set them, via a shared fixture, just while those tests are running. Both on CI and for local test runs, this makes it no longer necessary to set this in a global configuration or through environment variables, reducing the setup needed to run the tests. --- .github/workflows/cygwin-test.yml | 4 ---- .github/workflows/pythonpackage.yml | 4 ---- test/test_submodule.py | 22 +++++++++++++++++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index b612ca94a..808dc5608 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -46,8 +46,4 @@ jobs: shell: bash.exe -eo pipefail -o igncr "{0}" run: | /usr/bin/python -m pytest - env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: protocol.file.allow - GIT_CONFIG_VALUE_0: always continue-on-error: false diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 207714810..a6af507d1 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -56,10 +56,6 @@ jobs: run: | set -x pytest - env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: protocol.file.allow - GIT_CONFIG_VALUE_0: always continue-on-error: false - name: Documentation diff --git a/test/test_submodule.py b/test/test_submodule.py index 982226411..a039faaf3 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +import contextlib import os import shutil import tempfile from pathlib import Path import sys -from unittest import skipIf +from unittest import mock, skipIf import pytest @@ -31,6 +32,23 @@ import os.path as osp +@contextlib.contextmanager +def _allow_file_protocol(): + """Temporarily set protocol.file.allow to always, using environment variables.""" + pair_index = int(os.getenv("GIT_CONFIG_COUNT", "0")) + + # This is recomputed each time the context is entered, for compatibility with + # existing GIT_CONFIG_* environment variables, even if changed in this process. + patcher = mock.patch.dict(os.environ, { + "GIT_CONFIG_COUNT": str(pair_index + 1), + f"GIT_CONFIG_KEY_{pair_index}": "protocol.file.allow", + f"GIT_CONFIG_VALUE_{pair_index}": "always", + }) + + with patcher: + yield + + class TestRootProgress(RootUpdateProgress): """Just prints messages, for now without checking the correctness of the states""" @@ -709,6 +727,7 @@ def test_add_empty_repo(self, rwdir): # end for each checkout mode @with_rw_directory + @_allow_file_protocol() def test_list_only_valid_submodules(self, rwdir): repo_path = osp.join(rwdir, "parent") repo = git.Repo.init(repo_path) @@ -737,6 +756,7 @@ def test_list_only_valid_submodules(self, rwdir): """, ) @with_rw_directory + @_allow_file_protocol() def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): parent = git.Repo.init(osp.join(rwdir, "parent")) parent.git.submodule("add", self._small_repo_url(), "module") From f6c326288a04d17907081b065c436332e60115de Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 6 Sep 2023 23:24:34 +0000 Subject: [PATCH 077/152] Redesign new decorator to better separate concerns _allow_file_protocol was effectively a _patch_git_config fixture, being no no shorter, simpler, or clearer by hard-coding the specific name and value to patch. So this changes it to be that. As a secondary issue, it previously was called with no arguments, then that would be used as a decorator. That was unintutive and it was easy to omit the parentheses accidentally. This resolves that. --- test/test_submodule.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_submodule.py b/test/test_submodule.py index a039faaf3..d906a5d5b 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -33,16 +33,16 @@ @contextlib.contextmanager -def _allow_file_protocol(): - """Temporarily set protocol.file.allow to always, using environment variables.""" +def _patch_git_config(name, value): + """Temporarily add a git config name-value pair, using environment variables.""" pair_index = int(os.getenv("GIT_CONFIG_COUNT", "0")) # This is recomputed each time the context is entered, for compatibility with # existing GIT_CONFIG_* environment variables, even if changed in this process. patcher = mock.patch.dict(os.environ, { "GIT_CONFIG_COUNT": str(pair_index + 1), - f"GIT_CONFIG_KEY_{pair_index}": "protocol.file.allow", - f"GIT_CONFIG_VALUE_{pair_index}": "always", + f"GIT_CONFIG_KEY_{pair_index}": name, + f"GIT_CONFIG_VALUE_{pair_index}": value, }) with patcher: @@ -727,7 +727,7 @@ def test_add_empty_repo(self, rwdir): # end for each checkout mode @with_rw_directory - @_allow_file_protocol() + @_patch_git_config("protocol.file.allow", "always") def test_list_only_valid_submodules(self, rwdir): repo_path = osp.join(rwdir, "parent") repo = git.Repo.init(repo_path) @@ -756,7 +756,7 @@ def test_list_only_valid_submodules(self, rwdir): """, ) @with_rw_directory - @_allow_file_protocol() + @_patch_git_config("protocol.file.allow", "always") def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): parent = git.Repo.init(osp.join(rwdir, "parent")) parent.git.submodule("add", self._small_repo_url(), "module") From d88372a11ac145d92013dcc64b7d21a5a6ad3a91 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 7 Sep 2023 06:12:08 -0400 Subject: [PATCH 078/152] Add test for Windows env var upcasing regression --- test/test_git.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 540ea9f41..8ed9b64fe 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -10,7 +10,7 @@ import subprocess import sys from tempfile import TemporaryDirectory, TemporaryFile -from unittest import mock +from unittest import mock, skipUnless from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd from test.lib import TestBase, fixture_path @@ -105,6 +105,27 @@ def test_it_executes_git_not_from_cwd(self): with _chdir(tmpdir): self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b") + @skipUnless(is_win, "The regression only affected Windows, and this test logic is OS-specific.") + def test_it_avoids_upcasing_unrelated_environment_variable_names(self): + old_name = "28f425ca_d5d8_4257_b013_8d63166c8158" + if old_name == old_name.upper(): + raise RuntimeError("test bug or strange locale: old_name invariant under upcasing") + os.putenv(old_name, "1") # It has to be done this lower-level way to set it lower-case. + + script_lines = [ + "import subprocess, git", + + # Importing git should be enough, but this really makes sure Git.execute is called. + f"repo = git.Repo({self.rorepo.working_dir!r})", + "git.Git(repo.working_dir).execute(['git', 'version'])", + + f"print(subprocess.check_output(['set', {old_name!r}], shell=True, text=True))", + ] + cmdline = [sys.executable, "-c", "\n".join(script_lines)] + pair_text = subprocess.check_output(cmdline, shell=False, text=True) + new_name = pair_text.split("=")[0] + self.assertEqual(new_name, old_name) + def test_it_accepts_stdin(self): filename = fixture_path("cat_file_blob") with open(filename, "r") as fh: From 7296e5c021450743e5fe824e94b830a73eebc4c8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 7 Sep 2023 06:36:34 -0400 Subject: [PATCH 079/152] Make test helper script a file, for readability --- test/fixtures/env_case.py | 13 +++++++++++++ test/test_git.py | 14 +++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/env_case.py diff --git a/test/fixtures/env_case.py b/test/fixtures/env_case.py new file mode 100644 index 000000000..120e59289 --- /dev/null +++ b/test/fixtures/env_case.py @@ -0,0 +1,13 @@ +import subprocess +import sys + +import git + + +_, working_dir, env_var_name = sys.argv + +# Importing git should be enough, but this really makes sure Git.execute is called. +repo = git.Repo(working_dir) # Hold the reference. +git.Git(repo.working_dir).execute(["git", "version"]) + +print(subprocess.check_output(["set", env_var_name], shell=True, text=True)) diff --git a/test/test_git.py b/test/test_git.py index 8ed9b64fe..804cd22e4 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -112,16 +112,12 @@ def test_it_avoids_upcasing_unrelated_environment_variable_names(self): raise RuntimeError("test bug or strange locale: old_name invariant under upcasing") os.putenv(old_name, "1") # It has to be done this lower-level way to set it lower-case. - script_lines = [ - "import subprocess, git", - - # Importing git should be enough, but this really makes sure Git.execute is called. - f"repo = git.Repo({self.rorepo.working_dir!r})", - "git.Git(repo.working_dir).execute(['git', 'version'])", - - f"print(subprocess.check_output(['set', {old_name!r}], shell=True, text=True))", + cmdline = [ + sys.executable, + fixture_path("env_case.py"), + self.rorepo.working_dir, + old_name, ] - cmdline = [sys.executable, "-c", "\n".join(script_lines)] pair_text = subprocess.check_output(cmdline, shell=False, text=True) new_name = pair_text.split("=")[0] self.assertEqual(new_name, old_name) From c7fad20be5df0a86636459bf673ff9242a82e1fc Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 7 Sep 2023 04:32:36 -0400 Subject: [PATCH 080/152] Fix Windows env var upcasing regression This uses a simple hand-rolled context manager to patch the NoDefaultCurrentDirectoryInExePath variable, instead of unittest.mock.patch.dict. The latter set unrelated environment variables to the original (same) values via os.environ, and as a result, their names were all converted to upper-case on Windows. Because only environment variables that are actually set through os.environ have their names upcased, the only variable whose name should be upcased now is NoDefaultCurrentDirectoryInExePath, which should be fine (it has a single established use/meaning in Windows, where it's treated case-insensitively as environment variables in Windows *usually* are). --- git/cmd.py | 9 ++++----- git/util.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 3665eb029..d6f8f946a 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -14,7 +14,6 @@ import subprocess import threading from textwrap import dedent -import unittest.mock from git.compat import ( defenc, @@ -24,7 +23,7 @@ is_win, ) from git.exc import CommandError -from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present +from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present, patch_env from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError from .util import ( @@ -965,10 +964,10 @@ def execute( '"kill_after_timeout" feature is not supported on Windows.', ) # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value. - patch_caller_env = unittest.mock.patch.dict(os.environ, {"NoDefaultCurrentDirectoryInExePath": "1"}) + maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1") else: cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable - patch_caller_env = contextlib.nullcontext() + maybe_patch_caller_env = contextlib.nullcontext() # end handle stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") @@ -984,7 +983,7 @@ def execute( istream_ok, ) try: - with patch_caller_env: + with maybe_patch_caller_env: proc = Popen( command, env=env, diff --git a/git/util.py b/git/util.py index f6dedf0f2..f80580cff 100644 --- a/git/util.py +++ b/git/util.py @@ -158,6 +158,20 @@ def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: os.chdir(old_dir) +@contextlib.contextmanager +def patch_env(name: str, value: str) -> Generator[None, None, None]: + """Context manager to temporarily patch an environment variable.""" + old_value = os.getenv(name) + os.environ[name] = value + try: + yield + finally: + if old_value is None: + del os.environ[name] + else: + os.environ[name] = old_value + + def rmtree(path: PathLike) -> None: """Remove the given recursively. @@ -935,7 +949,7 @@ def _obtain_lock_or_raise(self) -> None: ) try: - with open(lock_file, mode='w'): + with open(lock_file, mode="w"): pass except OSError as e: raise IOError(str(e)) from e From eebdb25ee6e88d8fce83ea0970bd08f5e5301f65 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 7 Sep 2023 06:59:02 -0400 Subject: [PATCH 081/152] Eliminate duplication of git.util.cwd logic --- git/util.py | 1 + test/test_git.py | 16 ++-------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/git/util.py b/git/util.py index f80580cff..dee467dd3 100644 --- a/git/util.py +++ b/git/util.py @@ -150,6 +150,7 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T: @contextlib.contextmanager def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: + """Context manager to temporarily change directory. Not reentrant.""" old_dir = os.getcwd() os.chdir(new_dir) try: diff --git a/test/test_git.py b/test/test_git.py index 804cd22e4..f1d35a355 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -4,7 +4,6 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -import contextlib import os import shutil import subprocess @@ -15,24 +14,13 @@ from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd from test.lib import TestBase, fixture_path from test.lib import with_rw_directory -from git.util import finalize_process +from git.util import cwd, finalize_process import os.path as osp from git.compat import is_win -@contextlib.contextmanager -def _chdir(new_dir): - """Context manager to temporarily change directory. Not reentrant.""" - old_dir = os.getcwd() - os.chdir(new_dir) - try: - yield - finally: - os.chdir(old_dir) - - class TestGit(TestBase): @classmethod def setUpClass(cls): @@ -102,7 +90,7 @@ def test_it_executes_git_not_from_cwd(self): print("#!/bin/sh", file=file) os.chmod(impostor_path, 0o755) - with _chdir(tmpdir): + with cwd(tmpdir): self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b") @skipUnless(is_win, "The regression only affected Windows, and this test logic is OS-specific.") From 9da24d46c64eaf4c7db65c0f67324801fafbf30d Mon Sep 17 00:00:00 2001 From: "Wenhan Zhu (Cosmos)" Date: Wed, 6 Sep 2023 20:50:57 -0400 Subject: [PATCH 082/152] add test for submodule path not owned by submodule case --- test/test_submodule.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_submodule.py b/test/test_submodule.py index d906a5d5b..8c98a671e 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -906,6 +906,28 @@ def assert_exists(sm, value=True): assert osp.isdir(sm_module_path) == dry_run # end for each dry-run mode + @with_rw_directory + def test_ignore_non_submodule_file(self, rwdir): + parent = git.Repo.init(rwdir) + + smp = osp.join(rwdir, "module") + os.mkdir(smp) + + with open(osp.join(smp, "a"), "w", encoding="utf-8") as f: + f.write('test\n') + + with open(osp.join(rwdir, ".gitmodules"), "w", encoding="utf-8") as f: + f.write("[submodule \"a\"]\n") + f.write(" path = module\n") + f.write(" url = https://github.com/chaconinc/DbConnector\n") + + parent.git.add(Git.polish_url(https://github.com/osp.join(smp,%20%22a"))) + parent.git.add(Git.polish_url(https://github.com/osp.join(rwdir,%20%22.gitmodules"))) + + parent.git.commit(message='test') + + assert len(parent.submodules) == 0 + @with_rw_directory def test_remove_norefs(self, rwdir): parent = git.Repo.init(osp.join(rwdir, "parent")) From fafb4f6651eac242a7e143831fbe23d10beaf89b Mon Sep 17 00:00:00 2001 From: "Wenhan Zhu (Cosmos)" Date: Wed, 6 Sep 2023 20:54:07 -0400 Subject: [PATCH 083/152] updated docs to better describe testing procedure with new repo --- AUTHORS | 1 + README.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8ccc09fc0..ba5636db8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -51,4 +51,5 @@ Contributors are: -Luke Twist -Joseph Hale -Santos Gallegos +-Wenhan Zhu Portions derived from other open source works and are clearly marked. diff --git a/README.md b/README.md index 1743bd3d2..94fcc76d9 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,9 @@ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525). ### RUNNING TESTS -_Important_: Right after cloning this repository, please be sure to have executed -the `./init-tests-after-clone.sh` script in the repository root. Otherwise -you will encounter test failures. +_Important_: Right after cloning this repository, please be sure to have +executed `git fetch --tags` followed by the `./init-tests-after-clone.sh` +script in the repository root. Otherwise you will encounter test failures. On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` exists in `Git\mingw64\libexec\git-core\`; CYGWIN has no daemon, but should get along fine From c8e303ffd3204195fc7f768f7b17dc5bde3dd53f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 7 Sep 2023 15:35:40 +0200 Subject: [PATCH 084/152] prepare next release --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e03213a2b..d87cdbb81 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.34 +3.1.35 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 38e673f3f..6302176bd 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.35 +====== + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/65?closed=1 + 3.1.34 ====== From dba42451187243ffe6d56a08e032dbcbb9ac615f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 16:55:44 -0400 Subject: [PATCH 085/152] Fix installation test for Python 3.12 and Windows Starting in Python 3.12, global and virtual Python environments no longer automatically ship setuptools (per the "ensurepip" item in https://docs.python.org/3.12/whatsnew/3.12.html#removed). Projects that use setuptools as a build backend are still supported, including with setup.py using techniques such as "pip install .". In Windows, the "bin" subdir of a virtual environment dir is called "Scripts" instead. Unlike in a global environment (where no names are universal, and "python3" and "pip3" are more common for the Python 3 commands on some popular Unix-like systems), in a virtual environment the "python" and "pip" commands are always present and "python3" and "pip3" are not guaranteed to be present. This commit changes test_installation accordingly. The CI workflows and documentation still need to be updated. --- test/test_installation.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_installation.py b/test/test_installation.py index c092aef5e..fea978ead 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -4,6 +4,7 @@ import ast import os import subprocess +from git.compat import is_win from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -12,8 +13,9 @@ class TestInstallation(TestBase): def setUp_venv(self, rw_dir): self.venv = rw_dir subprocess.run(["virtualenv", self.venv], stdout=subprocess.PIPE) - self.python = os.path.join(self.venv, "bin/python3") - self.pip = os.path.join(self.venv, "bin/pip3") + bin_name = "Scripts" if is_win else "bin" + self.python = os.path.join(self.venv, bin_name, "python") + self.pip = os.path.join(self.venv, bin_name, "pip") self.sources = os.path.join(self.venv, "src") self.cwd = os.path.dirname(os.path.dirname(__file__)) os.symlink(self.cwd, self.sources, target_is_directory=True) @@ -32,14 +34,14 @@ def test_installation(self, rw_dir): msg=result.stderr or result.stdout or "Can't install requirements", ) result = subprocess.run( - [self.python, "setup.py", "install"], + [self.pip, "install", "."], stdout=subprocess.PIPE, cwd=self.sources, ) self.assertEqual( 0, result.returncode, - msg=result.stderr or result.stdout or "Can't build - setup.py failed", + msg=result.stderr or result.stdout or "Can't install project", ) result = subprocess.run([self.python, "-c", "import git"], stdout=subprocess.PIPE, cwd=self.sources) self.assertEqual( From 72e48aaea59738172ded5c964ddb4f06233ce9b7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 20:11:52 -0400 Subject: [PATCH 086/152] Update installation instructions in readme This changes the installation instructions in README.md to recommend "pip install ." instead of "python setup.py install". The former is compatible with Python 3.12 which doesn't have setuptools installed by default (so setup.py, which imports it, can be indirectly but not directly used). This also matches the corresponding change made in the installation unit test. While doing so, I've also clarified the instructions, and added the implied "cd" command as well as the "git fetch --tags" command in the position where a later section was recently updated to mention it should have been run. Using "pip install ." creates the opportunity to pass "-e" to make an editable install, which users who clone the repository to work on changes should do, because the effect of an editable install is only partially simulated by pytest, and so that manual testing of changes actually uses the changes intended for testing. This increases the length and detail of the instructions, so I've added h4 subsections to clarify the separations between them and make it easier for readers to find the part they're looking for. In doing so, I've reordered these subsections accordingly. Because greater detail can create the impression that all important steps are mentioned, I've made the general good advice to use a virtual environment explicit. For brevity, I have not added venv commands. --- README.md | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 94fcc76d9..c44985241 100644 --- a/README.md +++ b/README.md @@ -49,30 +49,49 @@ The installer takes care of installing them for you. ### INSTALL -If you have downloaded the source code: +GitPython and its required package dependencies can be installed in any of the following ways, all of which should typically be done in a [virtual environment](https://docs.python.org/3/tutorial/venv.html). -```bash -python setup.py install -``` +#### From PyPI -or if you want to obtain a copy from the Pypi repository: +To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run: ```bash pip install GitPython ``` -Both commands will install the required package dependencies. +(A distribution package can also be downloaded for manual installation at [the PyPI page](https://pypi.org/project/GitPython/).) + +#### From downloaded source code + +If you have downloaded the source code, run this from inside the unpacked `GitPython` directory: -A distribution package can be obtained for manual installation at: . +```bash +pip install . +``` -If you like to clone from source, you can do it like so: +#### By cloning the source code repository + +To clone the [the GitHub repository](https://github.com/gitpython-developers/GitPython) from source to work on the code, you can do it like so: ```bash git clone https://github.com/gitpython-developers/GitPython -git submodule update --init --recursive +cd GitPython +git fetch --tags ./init-tests-after-clone.sh ``` +If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`): + +```bash +gh repo clone GitPython +``` + +Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html). Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): + +```bash +pip install -e . +``` + ### Limitations #### Leakage of System Resources From b095aa9f61691151e68efb972659ac018c8e939d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 20:27:04 -0400 Subject: [PATCH 087/152] Use more compatible hashbangs This adds a #! line to the top of setup.py, because it is a script with the executable bit set. Although neither recent nor current documentation in the project recommends to run "./setup.py", this should probably have the intuitive effect of attempting to run the script with a Python interpreter rather than a Unix-style shell. It also uses the "env trick" in init-tests-after-clone.sh so that script runs with whatever bash interpreter is found in a normal PATH search. While "sh" is expected to be found in /bin on all Unix-like systems, that is not always the case for "bash". This change slightly improves compatibility by supporting systems that don't ship with bash but on which it has been installed. --- init-tests-after-clone.sh | 8 +++++--- setup.py | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh index e852f3cd9..95ced98b7 100755 --- a/init-tests-after-clone.sh +++ b/init-tests-after-clone.sh @@ -1,7 +1,9 @@ -#!/bin/bash -e +#!/usr/bin/env bash + +set -e if [[ -z "$TRAVIS" ]]; then - read -p "This operation will destroy locally modified files. Continue ? [N/y]: " answer + read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer if [[ ! $answer =~ [yY] ]]; then exit 2 fi @@ -13,4 +15,4 @@ git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard HEAD~1 git reset --hard __testing_point__ -git submodule update --init --recursive \ No newline at end of file +git submodule update --init --recursive diff --git a/setup.py b/setup.py index ebece64eb..403482520 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + from typing import Sequence from setuptools import setup, find_packages from setuptools.command.build_py import build_py as _build_py From 63c46240334d36129a78866a54694ea8fa5edd16 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 21:51:13 -0400 Subject: [PATCH 088/152] Don't duplicate packages across requirements files - Remove "gitdb" from test-requirements.txt, because it already a dependency of the project (listed in requirements.txt, which is used to build the value passed for install_requires in setup.py). - Remove "black" from requirements-dev.txt, because it is listed in test-requirement.txt (which requirements-dev.txt sources). --- requirements-dev.txt | 1 - test-requirements.txt | 2 -- 2 files changed, 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 946b4c94f..f6705341c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,6 @@ # libraries for additional local testing/linting - to be added to test-requirements.txt when all pass flake8-type-checking;python_version>="3.8" # checks for TYPE_CHECKING only imports -black pytest-icdiff # pytest-profiling diff --git a/test-requirements.txt b/test-requirements.txt index 6c6d57060..e202d35c5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,5 +11,3 @@ pytest pytest-cov coverage[toml] pytest-sugar - -gitdb From 3aacb3717ad78ec40e5b168a7ce8109aee6f156e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 23:13:20 -0400 Subject: [PATCH 089/152] Use a "test" extra instead of tests_require Because tests_require is deprecated since setuptools 41.5.0 with the intention that it will be removed in some future version (noted in https://setuptools.pypa.io/en/latest/references/keywords.html). It is somewhat unintuitive for GitPython to have a "test" extra, as it makes it so "GitPython[test]" can be specified for installation from PyPI to get test dependencies, even though the PyPI package doesn't include the unit test themselves. However, this makes the statement in README.md that the installer takes care of both requirements.txt and test-requirements.txt dependencies fully true, instead of moving further away from that. Because it is now possible to make an editable GitPython install with test as well as minimal dependencies installed, this commit also updates the readme to document and recommend this. --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++--------- setup.py | 2 +- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c44985241..4ff04c2e4 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,11 @@ gh repo clone GitPython Having cloned the repo, create and activate your [virtual environment](https://docs.python.org/3/tutorial/venv.html). Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): ```bash -pip install -e . +pip install -e ".[test]" ``` +In the less common case that you do not want to install test dependencies, `pip install -e .` can be used instead. + ### Limitations #### Leakage of System Resources @@ -120,20 +122,49 @@ On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the ` exists in `Git\mingw64\libexec\git-core\`; CYGWIN has no daemon, but should get along fine with MINGW's. -Ensure testing libraries are installed. -In the root directory, run: `pip install -r test-requirements.txt` +#### Install test dependencies + +Ensure testing libraries are installed. This is taken care of already if you installed with: + +```bash +pip install -e ".[test]" +``` + +Otherwise, you can run: + +```bash +pip install -r test-requirements.txt +``` + +#### Test commands + +To test, run: -To lint, run: `pre-commit run --all-files` +```bash +pytest +``` + +To lint, run: -To typecheck, run: `mypy -p git` +```bash +pre-commit run --all-files +``` -To test, run: `pytest` +To typecheck, run: -For automatic code formatting run: `black git` +```bash +mypy -p git +``` + +For automatic code formatting, run: + +```bash +black git +``` -Configuration for flake8 is in the ./.flake8 file. +Configuration for flake8 is in the `./.flake8` file. -Configurations for mypy, pytest and coverage.py are in ./pyproject.toml. +Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`. The same linting and testing will also be performed against different supported python versions upon submitting a pull request (or on each push if you have a fork with a "main" branch and actions enabled). diff --git a/setup.py b/setup.py index 403482520..5516a67d1 100755 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: package_dir={"git": "git"}, python_requires=">=3.7", install_requires=requirements, - tests_require=requirements + test_requirements, + extras_require={"test": test_requirements}, zip_safe=False, long_description=long_description, long_description_content_type="text/markdown", From e1d8b40e91eedabc3a2c2c1ac653f86b662323a9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 23:33:06 -0400 Subject: [PATCH 090/152] Use "build" for building Instead of directly running setup.py. This allows Python 3.12 (as well as previous versions) to be used for building. Although setuptools could be added as a development dependency to run setup.py, using "build" instead is recommended in https://setuptools.pypa.io/en/latest/userguide/quickstart.html. Those docs likewise recommend only listing "wheel" in the build-system section of pyproject.toml if setup.py actually imports the wheel module. So this removes that. (Running "make release", which now uses "build", will continue to build wheels.) The "build" package is not conceptually a testing dependency, but test-requirements.txt is currently the de facto list of all stable development dependencies for regular use. --- Makefile | 2 +- pyproject.toml | 2 +- test-requirements.txt | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2af8de084..6bde85af1 100644 --- a/Makefile +++ b/Makefile @@ -16,5 +16,5 @@ release: clean force_release: clean git push --tags origin main - python3 setup.py sdist bdist_wheel + python -m build --sdist --wheel twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index 32c9d4a26..42bb31eda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] diff --git a/test-requirements.txt b/test-requirements.txt index e202d35c5..67496031e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ black pre-commit +build virtualenv pytest From b9b6d8c07eb9afb88f092e507db9ae467e5e0986 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 9 Sep 2023 23:55:26 -0400 Subject: [PATCH 091/152] Ungroup and sort test_requirements.txt This is cleanup related to the previous commit. As that file grows, it is harder to tell immediately if a particular package is in it when not alphabetized. (The groups were also not intuitive, with ddt listed separately from other unit test related dependencies.) --- test-requirements.txt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 67496031e..214ad78ec 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,14 +1,10 @@ +black +build +coverage[toml] ddt>=1.1.1, !=1.4.3 mypy - -black - pre-commit - -build -virtualenv - pytest pytest-cov -coverage[toml] pytest-sugar +virtualenv From 21c5f8749590480de7cbcfe87352b60759328a75 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 00:08:58 -0400 Subject: [PATCH 092/152] Don't preinstall dependencies in test_installation This removes the step in test_installation that did the equivalent of "pip install -r requirements.txt", because installing GitPython is sufficient to install all its required dependencies, and it is more important to test that than to test requirements.txt directly. Removing this causes the test to fail if installing the project doesn't entail installation of the requirements necessary to import the git module or to cause gitdb to be found in a sys.path search. --- test/test_installation.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/test_installation.py b/test/test_installation.py index fea978ead..d856ebc94 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -23,16 +23,6 @@ def setUp_venv(self, rw_dir): @with_rw_directory def test_installation(self, rw_dir): self.setUp_venv(rw_dir) - result = subprocess.run( - [self.pip, "install", "-r", "requirements.txt"], - stdout=subprocess.PIPE, - cwd=self.sources, - ) - self.assertEqual( - 0, - result.returncode, - msg=result.stderr or result.stdout or "Can't install requirements", - ) result = subprocess.run( [self.pip, "install", "."], stdout=subprocess.PIPE, From 6b54890cfc14f1c574e8bf85d1ebf9be4c92be3e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 01:18:44 -0400 Subject: [PATCH 093/152] Test changed setup, and Python 3.12, on CI Key changes: - Update the two CI workflows to install the project and its dependencies in accordance with the changed recommendations in README.md. (This is to test that those recommendations work, which the changed test_installation test case partially but not completely tests. The old approach to installation still works too, so this change on CI is not required to keep CI working.) - Add Python 3.12 to the CI test matrix in pythonpackage.yml, testing it on Ubuntu. (The Cygwin workflow still tests only 3.9.) Maintenance changes, made to avoid decreasing readability with the other changes (and hopefully even increase it somewhat): - Separate commands into more steps, grouping them by more specific purposes. - Decrease the ways the two workflows differ from each other that do not represent actual intended behavioral differences. This is to make the important differences easier to stop, and to make it easier to determine when the same change has or has not been made to both workflows. --- .github/workflows/cygwin-test.yml | 41 ++++++++++++++++++------ .github/workflows/pythonpackage.yml | 48 +++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 808dc5608..ceb988cec 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -12,38 +12,61 @@ jobs: SHELLOPTS: igncr TMP: "/tmp" TEMP: "/tmp" + defaults: + run: + shell: bash.exe -eo pipefail -o igncr "{0}" steps: - name: Force LF line endings run: git config --global core.autocrlf input + - uses: actions/checkout@v4 with: fetch-depth: 9999 + - uses: cygwin/cygwin-install-action@v4 with: packages: python39 python39-pip python39-virtualenv git + + - name: Show python and git versions + run: | + set -x + /usr/bin/python --version + /usr/bin/git version + - name: Tell git to trust this repo - shell: bash.exe -eo pipefail -o igncr "{0}" run: | /usr/bin/git config --global --add safe.directory "$(pwd)" - - name: Install dependencies and prepare tests - shell: bash.exe -eo pipefail -o igncr "{0}" + + - name: Prepare this repo for tests run: | set -x - /usr/bin/python -m pip install --upgrade pip setuptools wheel - /usr/bin/python --version; /usr/bin/git --version + /usr/bin/git submodule update --init --recursive /usr/bin/git fetch --tags - /usr/bin/python -m pip install -r requirements.txt - /usr/bin/python -m pip install -r test-requirements.txt TRAVIS=yes ./init-tests-after-clone.sh + + - name: Further prepare git configuration for tests + run: | + set -x + /usr/bin/git config --global user.email "travis@ci.com" /usr/bin/git config --global user.name "Travis Runner" # If we rewrite the user's config by accident, we will mess it up # and cause subsequent tests to fail cat test/fixtures/.gitconfig >> ~/.gitconfig + + - name: Update PyPA packages + run: | + set -x + /usr/bin/python -m pip install --upgrade pip setuptools wheel + + - name: Install project and test dependencies + run: | + set -x + /usr/bin/python -m pip install ".[test]" + - name: Test with pytest - shell: bash.exe -eo pipefail -o igncr "{0}" run: | + set -x /usr/bin/python -m pytest - continue-on-error: false diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a6af507d1..9f15ceb2e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,42 +15,70 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + include: + - experimental: false + - python-version: "3.12" + experimental: true steps: - uses: actions/checkout@v4 with: fetch-depth: 9999 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies and prepare tests + allow-prereleases: ${{ matrix.experimental }} + + - name: Show python and git versions + run: | + set -x + python --version + git version + + - name: Prepare this repo for tests run: | set -x - python -m pip install --upgrade pip setuptools wheel - python --version; git --version git submodule update --init --recursive git fetch --tags --force - - pip install -r requirements.txt - pip install -r test-requirements.txt TRAVIS=yes ./init-tests-after-clone.sh + - name: Prepare git configuration for tests + run: | + set -x + git config --global user.email "travis@ci.com" git config --global user.name "Travis Runner" # If we rewrite the user's config by accident, we will mess it up # and cause subsequent tests to fail cat test/fixtures/.gitconfig >> ~/.gitconfig + - name: Update PyPA packages + run: | + set -x + + python -m pip install --upgrade pip + if pip freeze --all | grep --quiet '^setuptools=='; then + # Python prior to 3.12 ships setuptools. Upgrade it if present. + python -m pip install --upgrade setuptools + fi + python -m pip install --upgrade wheel + + - name: Install project and test dependencies + run: | + set -x + pip install ".[test]" + - 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 + # With new versions of mypy 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 - name: Test with pytest run: | From 055355d0028147c27b0423d713803748e60af6c3 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 04:23:25 -0400 Subject: [PATCH 094/152] Don't use "set -x" for "pytest" command on Cygwin The omission of "set -x" was intentional and is currently necessary on Cygwin (but not on Ubuntu), per aafb92a. --- .github/workflows/cygwin-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index ceb988cec..2e2e6d12d 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -68,5 +68,4 @@ jobs: - name: Test with pytest run: | - set -x /usr/bin/python -m pytest From a352404576552116bd16d9ca40cbcb903d3af020 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 13:24:40 -0400 Subject: [PATCH 095/152] List Python 3.12 as supported in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 5516a67d1..44623d049 100755 --- a/setup.py +++ b/setup.py @@ -124,5 +124,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], ) From 415a8ebdf8d0648cd17218e817963721d8df54da Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 13:28:16 -0400 Subject: [PATCH 096/152] Small clarity improvements in setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 44623d049..bc53bf6c8 100755 --- a/setup.py +++ b/setup.py @@ -8,8 +8,8 @@ import os import sys -with open(os.path.join(os.path.dirname(__file__), "VERSION")) as v: - VERSION = v.readline().strip() +with open(os.path.join(os.path.dirname(__file__), "VERSION")) as ver_file: + VERSION = ver_file.readline().strip() with open("requirements.txt") as reqs_file: requirements = reqs_file.read().splitlines() @@ -49,7 +49,7 @@ def _stamp_version(filename: str) -> None: with open(filename) as f: for line in f: if "__version__ =" in line: - line = line.replace("\"git\"", "'%s'" % VERSION) + line = line.replace('"git"', "'%s'" % VERSION) found = True out.append(line) except OSError: From 4eef3ecd5b2eb65f1e98457a35df8700166f67b0 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 00:41:04 -0400 Subject: [PATCH 097/152] Have actions/checkout do the full fetch Setting the "fetch-depth" to 0 does a deep (i.e., ordinary) fetch, fetching all commits and tags. Setting "submodules" to "recursive" clones and checks out all submodules. These options allow commands that were doing those things to be removed from the later steps. --- .github/workflows/cygwin-test.yml | 6 ++---- .github/workflows/pythonpackage.yml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 2e2e6d12d..533022b91 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -22,7 +22,8 @@ jobs: - uses: actions/checkout@v4 with: - fetch-depth: 9999 + fetch-depth: 0 + submodules: recursive - uses: cygwin/cygwin-install-action@v4 with: @@ -41,9 +42,6 @@ jobs: - name: Prepare this repo for tests run: | set -x - - /usr/bin/git submodule update --init --recursive - /usr/bin/git fetch --tags TRAVIS=yes ./init-tests-after-clone.sh - name: Further prepare git configuration for tests diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 9f15ceb2e..cce39d17a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -24,7 +24,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 9999 + fetch-depth: 0 + submodules: recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -41,9 +42,6 @@ jobs: - name: Prepare this repo for tests run: | set -x - - git submodule update --init --recursive - git fetch --tags --force TRAVIS=yes ./init-tests-after-clone.sh - name: Prepare git configuration for tests From 5f128e8c23e730748eb73bc2eb9f8ee552064e93 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 01:42:40 -0400 Subject: [PATCH 098/152] Move effect of "set -x" into default shell command This also adds "--noprofile --norc" to the Cygwin shell command as a speed optimization (bash doesn't need to source its scripts). That only changes the Cygwin workflow; in the Ubuntu workflow, "--noprofile --norc" had already been included by default when no shell was specified, so having it there is to *keep* the optimized behavior that was already in use. --- .github/workflows/cygwin-test.yml | 11 +++-------- .github/workflows/pythonpackage.yml | 13 +++---------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 533022b91..962791ae7 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -14,7 +14,7 @@ jobs: TEMP: "/tmp" defaults: run: - shell: bash.exe -eo pipefail -o igncr "{0}" + shell: bash.exe --noprofile --norc -exo pipefail -o igncr "{0}" steps: - name: Force LF line endings @@ -31,23 +31,19 @@ jobs: - name: Show python and git versions run: | - set -x /usr/bin/python --version /usr/bin/git version - name: Tell git to trust this repo run: | - /usr/bin/git config --global --add safe.directory "$(pwd)" + /usr/bin/git config --global --add safe.directory "$(pwd)" - name: Prepare this repo for tests run: | - set -x TRAVIS=yes ./init-tests-after-clone.sh - name: Further prepare git configuration for tests run: | - set -x - /usr/bin/git config --global user.email "travis@ci.com" /usr/bin/git config --global user.name "Travis Runner" # If we rewrite the user's config by accident, we will mess it up @@ -56,14 +52,13 @@ jobs: - name: Update PyPA packages run: | - set -x /usr/bin/python -m pip install --upgrade pip setuptools wheel - name: Install project and test dependencies run: | - set -x /usr/bin/python -m pip install ".[test]" - name: Test with pytest run: | + set +x /usr/bin/python -m pytest diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index cce39d17a..a5467ef94 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,6 +20,9 @@ jobs: - experimental: false - python-version: "3.12" experimental: true + defaults: + run: + shell: /bin/bash --noprofile --norc -exo pipefail {0} steps: - uses: actions/checkout@v4 @@ -35,19 +38,15 @@ jobs: - name: Show python and git versions run: | - set -x python --version git version - name: Prepare this repo for tests run: | - set -x TRAVIS=yes ./init-tests-after-clone.sh - name: Prepare git configuration for tests run: | - set -x - git config --global user.email "travis@ci.com" git config --global user.name "Travis Runner" # If we rewrite the user's config by accident, we will mess it up @@ -56,8 +55,6 @@ jobs: - name: Update PyPA packages run: | - set -x - python -m pip install --upgrade pip if pip freeze --all | grep --quiet '^setuptools=='; then # Python prior to 3.12 ships setuptools. Upgrade it if present. @@ -67,12 +64,10 @@ jobs: - name: Install project and test dependencies run: | - set -x pip install ".[test]" - name: Check types with mypy run: | - set -x mypy -p git # With new versions of mypy 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. @@ -80,12 +75,10 @@ jobs: - name: Test with pytest run: | - set -x pytest continue-on-error: false - name: Documentation run: | - set -x pip install -r doc/requirements.txt make -C doc html From d99b2d4629f2257ac719f77b93e1a2d7affd08c6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 11 Sep 2023 11:26:57 +0200 Subject: [PATCH 099/152] prepare next release --- VERSION | 2 +- doc/source/changes.rst | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d87cdbb81..b402c1a8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.35 +3.1.36 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 6302176bd..06ec4b72c 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ========= +3.1.36 +====== + +Note that this release should be a no-op, it's mainly for testing the changed release-process. + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/66?closed=1 + 3.1.35 ====== From f86f09e12c5616680e19b45c940144520349c9e3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 11 Sep 2023 12:53:21 +0200 Subject: [PATCH 100/152] Make publish process possible on MacOS There the system python interpreter is referred to as `python3`, at least when installed by homebrew. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6bde85af1..c3c00e948 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,6 @@ release: clean make force_release force_release: clean - git push --tags origin main - python -m build --sdist --wheel + python3 -m build --sdist --wheel twine upload dist/* + git push --tags origin main From 5343aa01e9d90481e4570797e99faf6a98ba8f6c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 13:39:34 -0400 Subject: [PATCH 101/152] Let "make" install build and twine if in a virtual environment If a virtual environment (created by venv or virtualenv) is active, running "make release" or "make force_release" now automatically installs/upgrades the "build" and "twine" packages in it. This is only done if "make" is run in a virtual environment. This can be a fresh environment: neither the project nor its dependencies need to be installed in it. Because the "build" module is not currently used in any tests and running "make" in a virtual environment takes care of installing "build" (and "twine"), "build" is now removed from test-requirements.txt. The publishing instructions in the readme are updated accordingly, to mention the optional step of creating and activating a virtual environment, and to briefly clarify why one might want to do that. Running "make" outside a virtual environment remains supported, except that, due to recent changes, whatever environment it is run in needs to have a usable "build" module. --- Makefile | 2 ++ README.md | 14 ++++++++------ test-requirements.txt | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index c3c00e948..f2cbf826a 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,8 @@ release: clean make force_release force_release: clean + # IF we're in a virtual environment, add build tools + test -z "$$VIRTUAL_ENV" || pip install -U build twine python3 -m build --sdist --wheel twine upload dist/* git push --tags origin main diff --git a/README.md b/README.md index 4ff04c2e4..ca470a851 100644 --- a/README.md +++ b/README.md @@ -188,13 +188,15 @@ Please have a look at the [contributions file][contributing]. ### How to make a new release -- Update/verify the **version** in the `VERSION` file -- Update/verify that the `doc/source/changes.rst` changelog file was updated -- Commit everything -- Run `git tag -s ` to tag the version in Git -- Run `make release` +- Update/verify the **version** in the `VERSION` file. +- Update/verify that the `doc/source/changes.rst` changelog file was updated. +- Commit everything. +- Run `git tag -s ` to tag the version in Git. +- _Optionally_ create and activate a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) using `venv` or `virtualenv`.\ +(When run in a virtual environment, the next step will automatically take care of installing `build` and `twine` in it.) +- Run `make release`. - Close the milestone mentioned in the _changelog_ and create a new one. _Do not reuse milestones by renaming them_. -- Got to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog. +- Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog. ### How to verify a release (DEPRECATED) diff --git a/test-requirements.txt b/test-requirements.txt index 214ad78ec..62f409824 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,4 @@ black -build coverage[toml] ddt>=1.1.1, !=1.4.3 mypy From 74d9c08bc4b8a61cd9f31e2294e1ff74d7b2be08 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 23:11:43 -0400 Subject: [PATCH 102/152] Better explain the env_case test This expands and adds comments in test_it_avoids_upcasing_unrelated_environment_variable_names and its supporting fixture env_case.py so it is clear exactly what is being tested and how/why the test works to test it. --- test/fixtures/env_case.py | 7 ++++++- test/test_git.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/test/fixtures/env_case.py b/test/fixtures/env_case.py index 120e59289..fe85ac41d 100644 --- a/test/fixtures/env_case.py +++ b/test/fixtures/env_case.py @@ -1,13 +1,18 @@ +# Steps 3 and 4 for test_it_avoids_upcasing_unrelated_environment_variable_names. + import subprocess import sys +# Step 3a: Import the module, in case that upcases the environment variable name. import git _, working_dir, env_var_name = sys.argv -# Importing git should be enough, but this really makes sure Git.execute is called. +# Step 3b: Use Git.execute explicitly, in case that upcases the environment variable. +# (Importing git should be enough, but this ensures Git.execute is called.) repo = git.Repo(working_dir) # Hold the reference. git.Git(repo.working_dir).execute(["git", "version"]) +# Step 4: Create the non-Python grandchild that accesses the variable case-sensitively. print(subprocess.check_output(["set", env_var_name], shell=True, text=True)) diff --git a/test/test_git.py b/test/test_git.py index f1d35a355..3b4a633b6 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -98,15 +98,25 @@ def test_it_avoids_upcasing_unrelated_environment_variable_names(self): old_name = "28f425ca_d5d8_4257_b013_8d63166c8158" if old_name == old_name.upper(): raise RuntimeError("test bug or strange locale: old_name invariant under upcasing") - os.putenv(old_name, "1") # It has to be done this lower-level way to set it lower-case. + # Step 1: Set the environment variable in this parent process. Because os.putenv is a thin + # wrapper around a system API, os.environ never sees the variable in this parent + # process, so the name is not upcased even on Windows. + os.putenv(old_name, "1") + + # Step 2: Create the child process that inherits the environment variable. It will see it + # in os.environ with an upcased name, but if it is not mutated through os.environ + # then it will pass it on to its own child processes with the original name. The + # child process will use GitPython, and we are testing that it passes the variable + # with the exact original name to its own child processes. cmdline = [ sys.executable, fixture_path("env_case.py"), self.rorepo.working_dir, old_name, ] - pair_text = subprocess.check_output(cmdline, shell=False, text=True) + pair_text = subprocess.check_output(cmdline, shell=False, text=True) # Steps 3 and 4. + new_name = pair_text.split("=")[0] self.assertEqual(new_name, old_name) From a59aaead478213b6fd83dd39c607baa2ead97e09 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 23:14:51 -0400 Subject: [PATCH 103/152] Condense an overly long comment To better focus on the key information. --- test/test_git.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 3b4a633b6..60cd60bd9 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -104,11 +104,9 @@ def test_it_avoids_upcasing_unrelated_environment_variable_names(self): # process, so the name is not upcased even on Windows. os.putenv(old_name, "1") - # Step 2: Create the child process that inherits the environment variable. It will see it - # in os.environ with an upcased name, but if it is not mutated through os.environ - # then it will pass it on to its own child processes with the original name. The - # child process will use GitPython, and we are testing that it passes the variable - # with the exact original name to its own child processes. + # Step 2: Create the child process that inherits the environment variable. The child uses + # GitPython, and we are testing that it passes the variable with the exact original + # name to its own child process (the grandchild). cmdline = [ sys.executable, fixture_path("env_case.py"), From 22b8dba6b4c73d04f3dbfabbb858fb5316fec711 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 23:28:14 -0400 Subject: [PATCH 104/152] Improve git.util.cwd docstring This frames the reentrancy claim in terms of what is similar to and different from contextlib.chdir (which we would just use, but it is only available on Python 3.11 and later). --- git/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index dee467dd3..636e79806 100644 --- a/git/util.py +++ b/git/util.py @@ -150,7 +150,10 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T: @contextlib.contextmanager def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: - """Context manager to temporarily change directory. Not reentrant.""" + """Context manager to temporarily change directory. + + This is similar to contextlib.chdir introduced in Python 3.11, but the context + manager object returned by a single call to this function is not reentrant.""" old_dir = os.getcwd() os.chdir(new_dir) try: From c547555026e099defd361e95e8c06acd5f2c0a2f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 12 Sep 2023 00:56:01 -0400 Subject: [PATCH 105/152] Clarify test relationship to env_case.py fixture --- test/test_git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_git.py b/test/test_git.py index 60cd60bd9..4d57a2d86 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -109,11 +109,11 @@ def test_it_avoids_upcasing_unrelated_environment_variable_names(self): # name to its own child process (the grandchild). cmdline = [ sys.executable, - fixture_path("env_case.py"), + fixture_path("env_case.py"), # Contains steps 3 and 4. self.rorepo.working_dir, old_name, ] - pair_text = subprocess.check_output(cmdline, shell=False, text=True) # Steps 3 and 4. + pair_text = subprocess.check_output(cmdline, shell=False, text=True) # Run steps 3 and 4. new_name = pair_text.split("=")[0] self.assertEqual(new_name, old_name) From 4aafbbf4a21d3a46bed6e2dafb46354f7ce6e346 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 12 Sep 2023 00:58:45 -0400 Subject: [PATCH 106/152] Remove spurious executable permissions This unsets the executable bit on test/fixtures/diff_mode_only, which was the only of the fixture files to be marked executable, and which is not a script. --- test/fixtures/diff_mode_only | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 test/fixtures/diff_mode_only diff --git a/test/fixtures/diff_mode_only b/test/fixtures/diff_mode_only old mode 100755 new mode 100644 From d6d8ecdb10d5c9d1820c4b314694b54f7755757f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 12 Sep 2023 08:02:41 +0200 Subject: [PATCH 107/152] leave another note in Makefile to help remember using virtualenv --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f2cbf826a..389337d08 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,6 @@ release: clean force_release: clean # IF we're in a virtual environment, add build tools test -z "$$VIRTUAL_ENV" || pip install -U build twine - python3 -m build --sdist --wheel + python3 -m build --sdist --wheel || echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" twine upload dist/* git push --tags origin main From 6fb231828ea845e7f0b80e52280f3efb98a25545 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 13:55:56 -0400 Subject: [PATCH 108/152] Reference HEAD in Makefile (more portable than head) This fixes "fatal: ambiguous argument 'head'", which occurs on some systems, inclding GNU/Linux systems, with "git rev-parse head". --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 389337d08..f3a5d3749 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,9 @@ clean: release: clean # Check if latest tag is the current head we're releasing echo "Latest tag = $$(git tag | sort -nr | head -n1)" - echo "HEAD SHA = $$(git rev-parse head)" + echo "HEAD SHA = $$(git rev-parse HEAD)" echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse head)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + @test "$$(git rev-parse HEAD)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" make force_release force_release: clean From 335d03b174569b4bc840e2020886a5f65adc56b7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 14:54:04 -0400 Subject: [PATCH 109/152] Have Makefile use git tag to sort the tags --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f3a5d3749..592f5658d 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,10 @@ clean: release: clean # Check if latest tag is the current head we're releasing - echo "Latest tag = $$(git tag | sort -nr | head -n1)" + echo "Latest tag = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1)" echo "HEAD SHA = $$(git rev-parse HEAD)" - echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse HEAD)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + echo "Latest tag SHA = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" + @test "$$(git rev-parse HEAD)" = "$$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" make force_release force_release: clean From b1c61d933d36a8e7fb6fb4109d2b07bd06bfbf32 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 05:44:16 -0400 Subject: [PATCH 110/152] Make "git tag" sort our SemVer-ish tags correctly This sorts numerically for each of major, minor, and patch, rather than, e.g., rating 2.1.15 as a higher version than 2.1.2. It also rates things like X-beta and X-rc as lower versions than X, but X-patched (not SemVer, but present in this project) as higher versions than X. --- Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 592f5658d..4e1927a9c 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,14 @@ clean: release: clean # Check if latest tag is the current head we're releasing - echo "Latest tag = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1)" - echo "HEAD SHA = $$(git rev-parse HEAD)" - echo "Latest tag SHA = $$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse HEAD)" = "$$(git tag -l '[0-9]*' --sort=-v:refname | head -n1 | xargs git rev-parse)" + @config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ + latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ + head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ + printf '%-14s = %s\n' 'Latest tag' "$$latest_tag" \ + 'HEAD SHA' "$$head_sha" \ + 'Latest tag SHA' "$$latest_tag_sha" && \ + test "$$head_sha" = "$$latest_tag_sha" + make force_release force_release: clean From cc202cc0fcbbb365e86a8dbc99bb5d6381619671 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:13:44 -0400 Subject: [PATCH 111/152] Improve when and how Makefile suggests virtual env The avoids showing the message when the build command was already run in a virtual environment. It also keeps the command failing, so the subsequent twine command is not attempted. (Just adding "|| echo ..." caused the command to succeed, because "echo ..." itself succeeds except in the rare case it cannot write to standard output.) --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4e1927a9c..1c2c03a88 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,13 @@ release: clean force_release: clean # IF we're in a virtual environment, add build tools test -z "$$VIRTUAL_ENV" || pip install -U build twine - python3 -m build --sdist --wheel || echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" + + # Build the sdist and wheel that will be uploaded to PyPI. + python3 -m build --sdist --wheel || \ + test -z "$$VIRTUAL_ENV" && \ + echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ + false + + # Upload to PyPI and push the tag. twine upload dist/* git push --tags origin main From b54c34647bf6281d7f5f757d9850484bdb25f800 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:33:10 -0400 Subject: [PATCH 112/152] Use "python" in the virtual env, "python3" outside This changes the build command to run with "python" when in a virtual environment, since all virtual environments support this even when "python" outside it is absent or refers to the wrong version. On Windows, virtual environments don't contain a python3 command, but a global python3 command may be present, so the errors are confusing. This fixes that by avoiding such errors altogether. --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1c2c03a88..cd5eba36b 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,13 @@ force_release: clean test -z "$$VIRTUAL_ENV" || pip install -U build twine # Build the sdist and wheel that will be uploaded to PyPI. - python3 -m build --sdist --wheel || \ - test -z "$$VIRTUAL_ENV" && \ + if test -n "$$VIRTUAL_ENV"; then \ + python -m build --sdist --wheel; \ + else \ + python3 -m build --sdist --wheel || \ echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ - false + false; \ + fi # Upload to PyPI and push the tag. twine upload dist/* From ae9405a339e7b01fb539e1837e4c3f450250cfb7 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 06:41:03 -0400 Subject: [PATCH 113/152] LF line endings for scripts that may need them This fixes how init-tests-after-clone.sh appears in .gitattributes so it gets LF (Unix-style) line endings on all systems as intended, and adds Makefile to be treated the same way. --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 6d2618f2f..739b2be29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ test/fixtures/* eol=lf -init-tests-after-clone.sh +init-tests-after-clone.sh eol=lf +Makefile eol=lf From f5da163bed2ababec006c0806187070341cc390e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 07:45:26 -0400 Subject: [PATCH 114/152] Have "make release" check other release preconditions As documented in the release instructions in README.md. --- Makefile | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index cd5eba36b..52d080788 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,29 @@ clean: rm -rf build/ dist/ .eggs/ .tox/ release: clean - # Check if latest tag is the current head we're releasing - @config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ + # Check that VERSION and changes.rst exist and have no uncommitted changes + test -f VERSION + test -f doc/source/changes.rst + git status -s VERSION doc/source/changes.rst + @test -z "$$(git status -s VERSION doc/source/changes.rst)" + + # Check that ALL changes are commited (can comment out if absolutely necessary) + git status -s + @test -z "$$(git status -s)" + + # Check that latest tag matches version and is the current head we're releasing + @version_file="$$(cat VERSION)" && \ + changes_file="$$(awk '/^[0-9]/ {print $$0; exit}' doc/source/changes.rst)" && \ + config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ - printf '%-14s = %s\n' 'Latest tag' "$$latest_tag" \ + printf '%-14s = %s\n' 'VERSION file' "$$version_file" \ + 'changes.rst' "$$changes_file" \ + 'Latest tag' "$$latest_tag" \ 'HEAD SHA' "$$head_sha" \ 'Latest tag SHA' "$$latest_tag_sha" && \ + test "$$version_file" = "$$changes_file" && \ + test "$$latest_tag" = "$$version_file" && \ test "$$head_sha" = "$$latest_tag_sha" make force_release From 5cf7f9777bc3f5bb92c57116071fd2bed3ac0b70 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 13 Sep 2023 09:05:36 -0400 Subject: [PATCH 115/152] Fix non-venv branch always failing cc202cc put an end to the problem where, when run outside a virtual environment, it would always "succeed", because all "|| echo ..." required to succeed was for the echo command reporting the error to succeed. Unfortunately, that commit created the oppposite problem, causing that case to always fail! This commit fixes it, so it fails when there is an error, and succeeds when there is no error. --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 52d080788..fdefb0439 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,7 @@ force_release: clean python -m build --sdist --wheel; \ else \ python3 -m build --sdist --wheel || \ - echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && \ - false; \ + { echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && false; }; \ fi # Upload to PyPI and push the tag. From 35f233949ec0cef92ce4d51129de18f358f8f525 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 14 Sep 2023 08:48:45 +0200 Subject: [PATCH 116/152] delete sublime-text project, nobody uses it and it's probably very outdated --- etc/sublime-text/git-python.sublime-project | 62 --------------------- 1 file changed, 62 deletions(-) delete mode 100644 etc/sublime-text/git-python.sublime-project diff --git a/etc/sublime-text/git-python.sublime-project b/etc/sublime-text/git-python.sublime-project deleted file mode 100644 index 3dab9f656..000000000 --- a/etc/sublime-text/git-python.sublime-project +++ /dev/null @@ -1,62 +0,0 @@ -{ - "folders": - [ - // GIT-PYTHON - ///////////// - { - "follow_symlinks": true, - "path": "../..", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "git/ext", - "dist", - ".tox", - "doc/build", - "*.egg-info" - ] - }, - // GITDB - //////// - { - "follow_symlinks": true, - "path": "../../git/ext/gitdb", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "gitdb/ext", - "dist", - "doc/build", - ".tox", - ] - }, - // // SMMAP - // //////// - { - "follow_symlinks": true, - "path": "../../git/ext/gitdb/gitdb/ext/smmap", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - ] - }, - ] -} From 6495d84142b60ba81a5b4268a0dfc0785c22d60a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 04:24:17 -0400 Subject: [PATCH 117/152] Extract checks from release target to script This extracts the check logic from the release target in Makefile to a new script, check-version.sh. The code is also modified, mainly to account for different ways output is displayed and errors are reported and treated in a Makefile versus a standalone shell script. (The .sh suffix is for consistency with the naming of init-tests-after-clone.sh and is *not* intended to suggest sourcing the script; this script should be executed, not sourced.) --- .gitattributes | 1 + Makefile | 26 +------------------------- check-version.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 25 deletions(-) create mode 100755 check-version.sh diff --git a/.gitattributes b/.gitattributes index 739b2be29..eb503040b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ test/fixtures/* eol=lf init-tests-after-clone.sh eol=lf +check-version.sh eol=lf Makefile eol=lf diff --git a/Makefile b/Makefile index fdefb0439..d11dd4de9 100644 --- a/Makefile +++ b/Makefile @@ -7,31 +7,7 @@ clean: rm -rf build/ dist/ .eggs/ .tox/ release: clean - # Check that VERSION and changes.rst exist and have no uncommitted changes - test -f VERSION - test -f doc/source/changes.rst - git status -s VERSION doc/source/changes.rst - @test -z "$$(git status -s VERSION doc/source/changes.rst)" - - # Check that ALL changes are commited (can comment out if absolutely necessary) - git status -s - @test -z "$$(git status -s)" - - # Check that latest tag matches version and is the current head we're releasing - @version_file="$$(cat VERSION)" && \ - changes_file="$$(awk '/^[0-9]/ {print $$0; exit}' doc/source/changes.rst)" && \ - config_opts="$$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" && \ - latest_tag=$$(git $$config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1) && \ - head_sha=$$(git rev-parse HEAD) latest_tag_sha=$$(git rev-parse "$$latest_tag") && \ - printf '%-14s = %s\n' 'VERSION file' "$$version_file" \ - 'changes.rst' "$$changes_file" \ - 'Latest tag' "$$latest_tag" \ - 'HEAD SHA' "$$head_sha" \ - 'Latest tag SHA' "$$latest_tag_sha" && \ - test "$$version_file" = "$$changes_file" && \ - test "$$latest_tag" = "$$version_file" && \ - test "$$head_sha" = "$$latest_tag_sha" - + ./check-version.sh make force_release force_release: clean diff --git a/check-version.sh b/check-version.sh new file mode 100755 index 000000000..5d3157033 --- /dev/null +++ b/check-version.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# This script checks if we appear ready to build and publish a new release. +# See the release instructions in README.md for the steps to make this pass. + +set -eEfuo pipefail +trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR + +readonly version_path='VERSION' +readonly changes_path='doc/source/changes.rst' + +printf 'Checking current directory.\n' +test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. + +printf 'Checking that %s and %s exist and have no committed changes.\n' \ + "$version_path" "$changes_path" +test -f "$version_path" +test -f "$changes_path" +git status -s -- "$version_path" "$changes_path" +test -z "$(git status -s -- "$version_path" "$changes_path")" + +# This section can be commented out, if absolutely necessary. +printf 'Checking that ALL changes are committed.\n' +git status -s +test -z "$(git status -s)" + +printf 'Gathering current version, latest tag, and current HEAD commit info.\n' +version_version="$(cat "$version_path")" +changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" +config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" +latest_tag="$(git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1)" +head_sha="$(git rev-parse HEAD)" +latest_tag_sha="$(git rev-parse "$latest_tag")" + +# Display a table of all the current version, tag, and HEAD commit information. +printf '%-14s = %s\n' 'VERSION file' "$version_version" \ + 'changes.rst' "$changes_version" \ + 'Latest tag' "$latest_tag" \ + 'HEAD SHA' "$head_sha" \ + 'Latest tag SHA' "$latest_tag_sha" + +# Check that latest tag matches version and is the current HEAD we're releasing +test "$version_version" = "$changes_version" +test "$latest_tag" = "$version_version" +test "$head_sha" = "$latest_tag_sha" +printf 'OK, everything looks good.\n' From 4b1c56409e905b852e3c93de142e109b147eee5e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 05:38:24 -0400 Subject: [PATCH 118/152] Extract build from force_release target to script This moves the conditional build dependency installation logic and build logic from the force_release Makefile target to a shell script build-release.sh, which force_release calls. The code is changed to clean it up, and also to account for differences between how output is displayed and errors reported in Makefiles and shell scripts. (As in check-version.sh, the .sh suffix does not signify anything about how the script is to be used: like the other shell scripts in the project, this should be executed, no sourced.) --- .gitattributes | 3 ++- Makefile | 13 +------------ build-release.sh | 22 ++++++++++++++++++++++ check-version.sh | 3 ++- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100755 build-release.sh diff --git a/.gitattributes b/.gitattributes index eb503040b..a66dc90ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ test/fixtures/* eol=lf init-tests-after-clone.sh eol=lf -check-version.sh eol=lf Makefile eol=lf +check-version.sh eol=lf +build-release.sh eol=lf diff --git a/Makefile b/Makefile index d11dd4de9..38090244c 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,6 @@ release: clean make force_release force_release: clean - # IF we're in a virtual environment, add build tools - test -z "$$VIRTUAL_ENV" || pip install -U build twine - - # Build the sdist and wheel that will be uploaded to PyPI. - if test -n "$$VIRTUAL_ENV"; then \ - python -m build --sdist --wheel; \ - else \ - python3 -m build --sdist --wheel || \ - { echo "Use a virtual-env with 'python -m venv env && source env/bin/activate' instead" && false; }; \ - fi - - # Upload to PyPI and push the tag. + ./build-release.sh twine upload dist/* git push --tags origin main diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 000000000..ebb45e062 --- /dev/null +++ b/build-release.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# +# This script builds a release. If run in a venv, it auto-installs its tools. +# You may want to run "make release" instead of running this script directly. + +set -eEu + +if test -n "${VIRTUAL_ENV:-}"; then + deps=(build twine) # Install twine along with build, as we need it later. + printf 'Virtual environment detected. Adding packages: %s\n' "${deps[*]}" + pip install -U "${deps[@]}" + printf 'Starting the build.\n' + python -m build --sdist --wheel +else + suggest_venv() { + venv_cmd='python -m venv env && source env/bin/activate' + printf "Use a virtual-env with '%s' instead.\n" "$venv_cmd" + } + trap suggest_venv ERR # This keeps the original exit (error) code. + printf 'Starting the build.\n' + python3 -m build --sdist --wheel # Outside a venv, use python3. +fi diff --git a/check-version.sh b/check-version.sh index 5d3157033..802492d93 100755 --- a/check-version.sh +++ b/check-version.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # -# This script checks if we appear ready to build and publish a new release. +# This script checks if we are in a consistent state to build a new release. # See the release instructions in README.md for the steps to make this pass. +# You may want to run "make release" instead of running this script directly. set -eEfuo pipefail trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR From 729372f6f87639f0c4d8211ee7d173100117a257 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 07:16:08 -0400 Subject: [PATCH 119/152] Prevent buggy interaction between MinGW and WSL This changes the hashbangs in Makefile helper scripts to be static. Often, "#!/usr/bin/env bash" is a better hashbang for bash scripts than "#!/bin/bash", because it automatically works on Unix-like systems that are not GNU/Linux and do not have bash in /bin, but on which it has been installed in another $PATH directory, such as /usr/local/bin. (It can also be helpful on macOS, where /bin/bash is usually an old version of bash, while a package manager such as brew may have been used to install a newer version elsewhere.) Windows systems with WSL installed often have a deprecated bash.exe in the System32 directory that runs commands and scripts inside an installed WSL system. (wsl.exe should be used instead.) Anytime that bash is used due to a "#!/usr/bin/env bash" hashbang, it is wrong, because that only happens if the caller is some Unix-style script running natively or otherwise outside WSL. Normally this is not a reason to prefer a "#!/bin/bash" hashbang, because normally any environment in which one can run a script in a way that determines its interpreter from its hashbang is an environment in which a native (or otherwise appropriate) bash precedes the System32 bash in a PATH search. However, MinGW make, a popular make implementation used on Windows, is an exception. The goal of this change is not to sacrifice support for some Unix-like systems to better support Windows, which wouldn't necessarily be justified. Rather, this is to avoid extremely confusing wrong behavior that in some cases would have bizarre effects that are very hard to detect. I discovered this problem because the VIRTUAL_ENV variable was not inheried by the bash interpreter (because it was, fortunately, not passed through to WSL). But if "python3 -m build" finds a global "build" package, things might get much further before any problem is noticed. --- build-release.sh | 2 +- check-version.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-release.sh b/build-release.sh index ebb45e062..cbf0e91a9 100755 --- a/build-release.sh +++ b/build-release.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # # This script builds a release. If run in a venv, it auto-installs its tools. # You may want to run "make release" instead of running this script directly. diff --git a/check-version.sh b/check-version.sh index 802492d93..e74ec2606 100755 --- a/check-version.sh +++ b/check-version.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # # This script checks if we are in a consistent state to build a new release. # See the release instructions in README.md for the steps to make this pass. From ba84db487a31c593fe0618a63f80709e405039c9 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 08:11:42 -0400 Subject: [PATCH 120/152] Fix message wording that was opposite of intended This also makes a correct but confusing comment clearer. --- check-version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check-version.sh b/check-version.sh index e74ec2606..373403c0e 100755 --- a/check-version.sh +++ b/check-version.sh @@ -13,7 +13,7 @@ readonly changes_path='doc/source/changes.rst' printf 'Checking current directory.\n' test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. -printf 'Checking that %s and %s exist and have no committed changes.\n' \ +printf 'Checking that %s and %s exist and have no uncommitted changes.\n' \ "$version_path" "$changes_path" test -f "$version_path" test -f "$changes_path" @@ -40,7 +40,7 @@ printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 'HEAD SHA' "$head_sha" \ 'Latest tag SHA' "$latest_tag_sha" -# Check that latest tag matches version and is the current HEAD we're releasing +# Check that the latest tag and current version match the HEAD we're releasing. test "$version_version" = "$changes_version" test "$latest_tag" = "$version_version" test "$head_sha" = "$latest_tag_sha" From de40e6864d5cbf8cd604b6f718b876c4d8c5a323 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 14 Sep 2023 08:57:34 -0400 Subject: [PATCH 121/152] Ignore some other virtual environment directories Like ".venv" and "venv", ".env" and "env" are common, plus "env" appears in the example command shown for making a virtual environment for the purpose of building a release, under some circumstances when "make release" or "make force_release" fail. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 72da84eee..0bd307639 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.py[co] *.swp *~ +.env/ +env/ .venv/ venv/ /*.egg-info From 693d041869497137085171cdabbaf43e33fb9c84 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:40:36 +0200 Subject: [PATCH 122/152] make `.gitattributes` file more generic That way shell scripts will be handled correctly by default, anywhere. --- .gitattributes | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index a66dc90ca..3f3d2f050 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ test/fixtures/* eol=lf -init-tests-after-clone.sh eol=lf -Makefile eol=lf -check-version.sh eol=lf -build-release.sh eol=lf +*.sh eol=lf +/Makefile eol=lf From 962f747d9c2a877e70886f2ebee975b9be4bb672 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:47:31 +0200 Subject: [PATCH 123/152] submodules don't contribute to the release; ignore their changes --- check-version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check-version.sh b/check-version.sh index 373403c0e..81112b326 100755 --- a/check-version.sh +++ b/check-version.sh @@ -22,8 +22,8 @@ test -z "$(git status -s -- "$version_path" "$changes_path")" # This section can be commented out, if absolutely necessary. printf 'Checking that ALL changes are committed.\n' -git status -s -test -z "$(git status -s)" +git status -s --ignore-submodules +test -z "$(git status -s --ignore-submodules)" printf 'Gathering current version, latest tag, and current HEAD commit info.\n' version_version="$(cat "$version_path")" From d18d90a2c04abeff4bcc8d642fcd33be1c1eb35b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 07:53:55 +0200 Subject: [PATCH 124/152] Use 'echo' where possible to avoid explicit newlines --- check-version.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/check-version.sh b/check-version.sh index 81112b326..d29d457c0 100755 --- a/check-version.sh +++ b/check-version.sh @@ -5,27 +5,26 @@ # You may want to run "make release" instead of running this script directly. set -eEfuo pipefail -trap 'printf "%s: Check failed. Stopping.\n" "$0" >&2' ERR +trap 'echo "$0: Check failed. Stopping." >&2' ERR readonly version_path='VERSION' readonly changes_path='doc/source/changes.rst' -printf 'Checking current directory.\n' +echo 'Checking current directory.' test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)" # Ugly, but portable. -printf 'Checking that %s and %s exist and have no uncommitted changes.\n' \ - "$version_path" "$changes_path" +echo "Checking that $version_path and $changes_path exist and have no uncommitted changes." test -f "$version_path" test -f "$changes_path" git status -s -- "$version_path" "$changes_path" test -z "$(git status -s -- "$version_path" "$changes_path")" # This section can be commented out, if absolutely necessary. -printf 'Checking that ALL changes are committed.\n' +echo 'Checking that ALL changes are committed.' git status -s --ignore-submodules test -z "$(git status -s --ignore-submodules)" -printf 'Gathering current version, latest tag, and current HEAD commit info.\n' +echo 'Gathering current version, latest tag, and current HEAD commit info.' version_version="$(cat "$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" @@ -44,4 +43,4 @@ printf '%-14s = %s\n' 'VERSION file' "$version_version" \ test "$version_version" = "$changes_version" test "$latest_tag" = "$version_version" test "$head_sha" = "$latest_tag_sha" -printf 'OK, everything looks good.\n' +echo 'OK, everything looks good.' From 5919f8d04bccfaf0c98ae032437635d1a2de656b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 08:06:51 +0200 Subject: [PATCH 125/152] Be explicit on how to interpret the data table --- check-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check-version.sh b/check-version.sh index d29d457c0..6d10d6785 100755 --- a/check-version.sh +++ b/check-version.sh @@ -24,7 +24,6 @@ echo 'Checking that ALL changes are committed.' git status -s --ignore-submodules test -z "$(git status -s --ignore-submodules)" -echo 'Gathering current version, latest tag, and current HEAD commit info.' version_version="$(cat "$version_path")" changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" @@ -33,6 +32,7 @@ head_sha="$(git rev-parse HEAD)" latest_tag_sha="$(git rev-parse "$latest_tag")" # Display a table of all the current version, tag, and HEAD commit information. +echo $'\nThe VERSION must be the same in all locations, and so must the HEAD and tag SHA' printf '%-14s = %s\n' 'VERSION file' "$version_version" \ 'changes.rst' "$changes_version" \ 'Latest tag' "$latest_tag" \ From 1e0b3f91f6dffad6bfc262528c14bf459c6a63c8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Sep 2023 08:32:30 +0200 Subject: [PATCH 126/152] refinements to `build-reelase.sh` - use `echo` where feasible to avoid explicit newlines - use `function` syntax out of habit - deduplicate release invocation - make `venv` based invocation less verbose - make help-text in non-venv more prominent --- build-release.sh | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/build-release.sh b/build-release.sh index cbf0e91a9..5840e4472 100755 --- a/build-release.sh +++ b/build-release.sh @@ -5,18 +5,22 @@ set -eEu +function release_with() { + $1 -m build --sdist --wheel +} + if test -n "${VIRTUAL_ENV:-}"; then deps=(build twine) # Install twine along with build, as we need it later. - printf 'Virtual environment detected. Adding packages: %s\n' "${deps[*]}" - pip install -U "${deps[@]}" - printf 'Starting the build.\n' - python -m build --sdist --wheel + echo "Virtual environment detected. Adding packages: ${deps[*]}" + pip install --quiet --upgrade "${deps[@]}" + echo 'Starting the build.' + release_with python else - suggest_venv() { + function suggest_venv() { venv_cmd='python -m venv env && source env/bin/activate' - printf "Use a virtual-env with '%s' instead.\n" "$venv_cmd" + printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" } trap suggest_venv ERR # This keeps the original exit (error) code. - printf 'Starting the build.\n' - python3 -m build --sdist --wheel # Outside a venv, use python3. + echo 'Starting the build.' + release_with python3 # Outside a venv, use python3. fi From ae8c018f3ab62f7ada3a56af29d4f647809654c6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 08:07:23 -0400 Subject: [PATCH 127/152] Fix URLs that were redirecting to another license All the opensource.org BSD license URLs at the top of source code files in this project had originally pointed to a page on the 3-clause BSD license that this project used and continues to use. But over time the site was apparently reorganized and the link became a redirect to the page about the 2-clause BSD license. Because it is identified only as the "BSD license" in the comments in this project that contain the links, this unfortunately makes it so those top-of-file comments all wrongly claim that the project is 2-clause BSD licensed. This fixes the links by replacing them with the current URL of the opensource.org page on the 3-clause BSD license. The current URL contains "bsd-3-clause" in it, so this specific problem is unlikely to recur with that URL (and even if it did, the text "bsd-3-clause is information that may clue readers in to what is going on). --- git/__init__.py | 2 +- git/cmd.py | 2 +- git/compat.py | 2 +- git/config.py | 2 +- git/diff.py | 2 +- git/exc.py | 2 +- git/index/base.py | 2 +- git/objects/base.py | 2 +- git/objects/blob.py | 2 +- git/objects/commit.py | 2 +- git/objects/tag.py | 2 +- git/objects/tree.py | 2 +- git/objects/util.py | 2 +- git/remote.py | 2 +- git/repo/base.py | 2 +- git/types.py | 2 +- git/util.py | 2 +- test/__init__.py | 2 +- test/lib/__init__.py | 2 +- test/lib/helper.py | 2 +- test/performance/test_commit.py | 2 +- test/test_actor.py | 2 +- test/test_base.py | 2 +- test/test_blob.py | 2 +- test/test_clone.py | 2 +- test/test_commit.py | 2 +- test/test_config.py | 2 +- test/test_db.py | 2 +- test/test_diff.py | 5 ++--- test/test_docs.py | 2 +- test/test_exc.py | 2 +- test/test_git.py | 2 +- test/test_index.py | 4 ++-- test/test_installation.py | 2 +- test/test_refs.py | 2 +- test/test_remote.py | 2 +- test/test_repo.py | 2 +- test/test_stats.py | 2 +- test/test_submodule.py | 2 +- test/test_tree.py | 2 +- test/test_util.py | 2 +- 41 files changed, 43 insertions(+), 44 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index 6196a42d7..e2d123fa5 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa # @PydevCodeAnalysisIgnore from git.exc import * # @NoMove @IgnorePep8 diff --git a/git/cmd.py b/git/cmd.py index d6f8f946a..9921dd6c9 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from __future__ import annotations import re import contextlib diff --git a/git/compat.py b/git/compat.py index e7ef28c30..624f26116 100644 --- a/git/compat.py +++ b/git/compat.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """utilities to help provide compatibility with python 3""" # flake8: noqa diff --git a/git/config.py b/git/config.py index 1973111eb..880eb9301 100644 --- a/git/config.py +++ b/git/config.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """Module containing module parser implementation able to properly read and write configuration files""" diff --git a/git/diff.py b/git/diff.py index 1424ff3ad..3e3de7bc1 100644 --- a/git/diff.py +++ b/git/diff.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import re from git.cmd import handle_process_output diff --git a/git/exc.py b/git/exc.py index 775528bf6..0786a8e8a 100644 --- a/git/exc.py +++ b/git/exc.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """ Module containing all exceptions thrown throughout the git package, """ from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 diff --git a/git/index/base.py b/git/index/base.py index 193baf3ad..cf016df6a 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from contextlib import ExitStack import datetime diff --git a/git/objects/base.py b/git/objects/base.py index eb9a8ac3d..1d07fd0f6 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.exc import WorkTreeRepositoryUnsupported from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex diff --git a/git/objects/blob.py b/git/objects/blob.py index 1881f210c..96ce486f5 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from mimetypes import guess_type from . import base diff --git a/git/objects/commit.py b/git/objects/commit.py index 6db3ea0f3..88c485d09 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import datetime import re from subprocess import Popen, PIPE diff --git a/git/objects/tag.py b/git/objects/tag.py index 3956a89e7..56fd05d1a 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """ Module containing all object based types. """ from . import base from .util import get_object_type_by_name, parse_actor_and_date diff --git a/git/objects/tree.py b/git/objects/tree.py index a9b491e23..4f490af54 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.util import IterableList, join_path import git.diff as git_diff diff --git a/git/objects/util.py b/git/objects/util.py index 56938507e..992a53d9c 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ """Module for general utility functions""" # flake8: noqa F401 diff --git a/git/remote.py b/git/remote.py index 95a2b8ac6..fc2b2ceba 100644 --- a/git/remote.py +++ b/git/remote.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # Module implementing a remote object allowing easy access to git remotes import logging diff --git a/git/repo/base.py b/git/repo/base.py index 113fca459..fda3cdc88 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from __future__ import annotations import logging import os diff --git a/git/types.py b/git/types.py index 9f8621721..21276b5f1 100644 --- a/git/types.py +++ b/git/types.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa import os diff --git a/git/util.py b/git/util.py index 636e79806..638807a36 100644 --- a/git/util.py +++ b/git/util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from abc import abstractmethod import os.path as osp diff --git a/test/__init__.py b/test/__init__.py index 757cbad1f..a3d514523 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ diff --git a/test/lib/__init__.py b/test/lib/__init__.py index a4e57b8e0..299317c0b 100644 --- a/test/lib/__init__.py +++ b/test/lib/__init__.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ # flake8: noqa import inspect diff --git a/test/lib/helper.py b/test/lib/helper.py index c04c5cd90..64c70a8f4 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib from functools import wraps import gc diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py index 38b529af7..dbe2ad43e 100644 --- a/test/performance/test_commit.py +++ b/test/performance/test_commit.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO from time import time import sys diff --git a/test/test_actor.py b/test/test_actor.py index ce0c74fc9..f495ac084 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase from git import Actor diff --git a/test/test_base.py b/test/test_base.py index 30029367d..b77c8117d 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import sys import tempfile diff --git a/test/test_blob.py b/test/test_blob.py index b94dcec23..692522b52 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase from git import Blob diff --git a/test/test_clone.py b/test/test_clone.py index 304ab33cb..1b4a6c332 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from pathlib import Path import re diff --git a/test/test_commit.py b/test/test_commit.py index 4871902ec..d13db1410 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import copy from datetime import datetime from io import BytesIO diff --git a/test/test_config.py b/test/test_config.py index b159ebe2d..481e129c6 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import glob import io diff --git a/test/test_db.py b/test/test_db.py index 228c70e7c..ebf73b535 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from git.db import GitCmdObjectDB from git.exc import BadObject from test.lib import TestBase diff --git a/test/test_diff.py b/test/test_diff.py index 504337744..dacbdc3bc 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import ddt import shutil import tempfile @@ -414,7 +414,7 @@ def test_diff_interface(self): @with_rw_directory def test_rename_override(self, rw_dir): - """Test disabling of diff rename detection""" + """Test disabling of diff rename detection""" # create and commit file_a.txt repo = Repo.init(rw_dir) @@ -480,4 +480,3 @@ def test_rename_override(self, rw_dir): self.assertEqual(True, diff.renamed_file) self.assertEqual('file_a.txt', diff.rename_from) self.assertEqual('file_b.txt', diff.rename_to) - diff --git a/test/test_docs.py b/test/test_docs.py index 20027c191..505b50f77 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import sys diff --git a/test/test_exc.py b/test/test_exc.py index f998ff4d5..9e125d246 100644 --- a/test/test_exc.py +++ b/test/test_exc.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009, 2016 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import re diff --git a/test/test_git.py b/test/test_git.py index 4d57a2d86..2c392155a 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import shutil import subprocess diff --git a/test/test_index.py b/test/test_index.py index 3bebb382b..9b7ba52a6 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO import os @@ -953,4 +953,4 @@ def test_index_add_pathlike(self, rw_repo): file = git_dir / "file.txt" file.touch() - rw_repo.index.add(file) \ No newline at end of file + rw_repo.index.add(file) diff --git a/test/test_installation.py b/test/test_installation.py index d856ebc94..1c9d2359c 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -1,5 +1,5 @@ # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import ast import os diff --git a/test/test_refs.py b/test/test_refs.py index e7526c3b2..afd273df9 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from itertools import chain from pathlib import Path diff --git a/test/test_remote.py b/test/test_remote.py index 9636ca486..e0dcb4131 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import random import tempfile diff --git a/test/test_repo.py b/test/test_repo.py index 08ed13a00..abae5ad78 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -3,7 +3,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import glob import io from io import BytesIO diff --git a/test/test_stats.py b/test/test_stats.py index 1f6896555..335ce483b 100644 --- a/test/test_stats.py +++ b/test/test_stats.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from test.lib import TestBase, fixture from git import Stats diff --git a/test/test_submodule.py b/test/test_submodule.py index 8c98a671e..5a7f26207 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import contextlib import os import shutil diff --git a/test/test_tree.py b/test/test_tree.py index 22c9c7d78..e59705645 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO from unittest import skipIf diff --git a/test/test_util.py b/test/test_util.py index c17efce35..517edd65c 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -2,7 +2,7 @@ # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# the BSD License: https://opensource.org/license/bsd-3-clause/ import os import pickle From e1af18377fd69f9c1007f8abf6ccb95b3c5a6558 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 09:42:40 -0400 Subject: [PATCH 128/152] Assorted small fixes/improvements to root dir docs This contains misc. formatting fixes and minor proofreading in the top-level documentation files, plus making the text in the LICENSE section of README.md have links both to external information about the license and to the license file itself. Note that the changes to the license file are just removal of excess whitespace (the extra blank line at the end, and spaces appearing at the end of lines). --- AUTHORS | 1 + CONTRIBUTING.md | 2 +- LICENSE | 35 +++++++++++++++++------------------ README.md | 7 ++++--- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/AUTHORS b/AUTHORS index ba5636db8..3e99ff785 100644 --- a/AUTHORS +++ b/AUTHORS @@ -52,4 +52,5 @@ Contributors are: -Joseph Hale -Santos Gallegos -Wenhan Zhu + Portions derived from other open source works and are clearly marked. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56af0df2a..e108f1b80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ The following is a short step-by-step rundown of what one typically would do to contribute. -- [fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. +- [Fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. - For setting up the environment to run the self tests, please run `init-tests-after-clone.sh`. - Please try to **write a test that fails unless the contribution is present.** - Try to avoid massive commits and prefer to take small steps, with one commit for each. diff --git a/LICENSE b/LICENSE index 5a9a6f8d3..ba8a219fe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,29 @@ Copyright (C) 2008, 2009 Michael Trier and contributors All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of the GitPython project nor the names of -its contributors may be used to endorse or promote products derived +* Neither the name of the GitPython project nor the names of +its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/README.md b/README.md index ca470a851..69d69c56f 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Please have a look at the [contributions file][contributing]. - [User Documentation](http://gitpython.readthedocs.org) - [Questions and Answers](http://stackexchange.com/filters/167317/gitpython) -- Please post on stackoverflow and use the `gitpython` tag +- Please post on Stack Overflow and use the `gitpython` tag - [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues) - Post reproducible bugs and feature requests as a new issue. Please be sure to provide the following information if posting bugs: @@ -267,6 +267,7 @@ gpg --edit-key 4C08421980C9 ### LICENSE -New BSD License. See the LICENSE file. +[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file](https://github.com/gitpython-developers/GitPython/blob/main/license). -[contributing]: https://github.com/gitpython-developers/GitPython/blob/master/CONTRIBUTING.md +[contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md +[license]: https://github.com/gitpython-developers/GitPython/blob/main/license From ad76c99d73ecee3b834beea81ad78665b849adf8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 10:01:38 -0400 Subject: [PATCH 129/152] Use venv instead of virtualenv in test_installation This eliminates the test dependency on virtualenv by using the standard library venv module instead in test_installation. --- test-requirements.txt | 1 - test/test_installation.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 62f409824..b00dd6f06 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,4 +6,3 @@ pre-commit pytest pytest-cov pytest-sugar -virtualenv diff --git a/test/test_installation.py b/test/test_installation.py index d856ebc94..6cd97246e 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -4,6 +4,8 @@ import ast import os import subprocess +import sys + from git.compat import is_win from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -12,7 +14,7 @@ class TestInstallation(TestBase): def setUp_venv(self, rw_dir): self.venv = rw_dir - subprocess.run(["virtualenv", self.venv], stdout=subprocess.PIPE) + subprocess.run([sys.executable, "-m", "venv", self.venv], stdout=subprocess.PIPE) bin_name = "Scripts" if is_win else "bin" self.python = os.path.join(self.venv, bin_name, "python") self.pip = os.path.join(self.venv, bin_name, "pip") From 3fbbfd7770c734d2997f16a3f8967ae8f3910dd1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 23:25:19 -0400 Subject: [PATCH 130/152] Omit py_modules in setup This removes the py_modules keyword argument in the call to setup, and further shortens/simplifies setup.py by removing the now-unused build_py_modules function. The packages keyword argument already covers this, because we have no loose modules that are included in the distribution, and the call to find_packages: - Omits everything in test/ because it is directed to do so in the call. - Omits the gitdb/ directory (currently existing as a git submodule, not to be confused with Python submodules), because the ext/ directory that contains it does not itself directly contain an __init__.py file, so it is not a traditional package, yet ext/ is contained and found inside the directory git/ that *is* a traditional package, so the ext/ directory is not a namespace package either. - Includes all other modules, recursively, because they are all in a recursive traditional package structure under git/ that find_packages recognizes. To verify that this includes the same files in the built wheel and sdist distributions, I have listed the contents of the wheel with "unzip -l" and the sdist .tar.gz file with "tar tf" both before and after this change, verifying they list all the same entries. --- setup.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/setup.py b/setup.py index bc53bf6c8..90df8d7ea 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ from setuptools import setup, find_packages from setuptools.command.build_py import build_py as _build_py from setuptools.command.sdist import sdist as _sdist -import fnmatch import os import sys @@ -62,24 +61,6 @@ def _stamp_version(filename: str) -> None: print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr) -def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: - # create list of py_modules from tree - res = set() - _prefix = os.path.basename(basedir) - for root, _, files in os.walk(basedir): - for f in files: - _f, _ext = os.path.splitext(f) - if _ext not in [".py"]: - continue - _f = os.path.join(root, _f) - _f = os.path.relpath(_f, basedir) - _f = "{}.{}".format(_prefix, _f.replace(os.sep, ".")) - if any(fnmatch.fnmatch(_f, x) for x in excludes): - continue - res.add(_f) - return list(res) - - setup( name="GitPython", cmdclass={"build_py": build_py, "sdist": sdist}, @@ -91,7 +72,6 @@ def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: url="https://github.com/gitpython-developers/GitPython", packages=find_packages(exclude=["test", "test.*"]), include_package_data=True, - py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={"git": "git"}, python_requires=">=3.7", install_requires=requirements, From 407151878361badba63bb809c75cc3a877778de8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 17:57:38 -0400 Subject: [PATCH 131/152] Don't track code coverage temporary files While running tests with coverage enabled, files of the form .coverage.MachineName.####.###### are created temporarily. When tests complete normally (whether or not there are failures), these files are automatically removed. However, when tests are cancelled with SIGINT (Ctrl+C), they are sometimes retained. This adds another pattern to .gitignore so that, in addition to not tracking the .coverage file that is retained after tests, these other temporary files are also not tracked. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bd307639..e8b16da9d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ venv/ /lib/GitPython.egg-info cover/ .coverage +.coverage.* /build /dist /doc/_build From cd052b20a71bec0d605151eeb6b7ac87fbeb3e4a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 03:26:38 -0400 Subject: [PATCH 132/152] Start setting up tox It is not completely working yet. --- .gitignore | 1 - requirements-dev.txt | 3 --- tox.ini | 24 ++++++++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 0bd307639..139bf8ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,3 @@ nbproject .pytest_cache/ monkeytype.sqlite3 output.txt -tox.ini diff --git a/requirements-dev.txt b/requirements-dev.txt index f6705341c..e3030c597 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,6 +7,3 @@ flake8-type-checking;python_version>="3.8" # checks for TYPE_CHECKING only pytest-icdiff # pytest-profiling - - -tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..0b5591139 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +requires = tox>=4 +env_list = py{37,38,39,310,311,312}, lint, mypy, black + +[testenv] +description = Run unit tests +package = wheel +extras = test +commands = pytest --color=yes {posargs} + +[testenv:lint] +description = Lint via pre-commit +basepython = py39 +commands = pre-commit run --all-files + +[testenv:mypy] +description = Typecheck with mypy +basepython = py39 +commands = mypy -p git + +[testenv:black] +description = Check style with black +basepython = py39 +commands = black --check --diff git From 2cc2db77574b8197cce215ab703c9d383ea645c1 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 19:49:00 -0400 Subject: [PATCH 133/152] Pass through SSH_ env vars to tox envs This fixes a problem where the tests for fetching a nonexistent ref prompt like "Enter passphrase for key '/home/USERNAME/.ssh/id_rsa':" and block, if the repository on the machine where the tests are being run has the remote set up using an SSH URL. This passes through all environment variables whose names start with SSH_, even though it should be enough to pass SSH_AGENT_PID and SSH_AUTH_SOCK through, at least for this particular issue. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 0b5591139..9b918f560 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ env_list = py{37,38,39,310,311,312}, lint, mypy, black description = Run unit tests package = wheel extras = test +pass_env = SSH_* commands = pytest --color=yes {posargs} [testenv:lint] From 4bea7cf4cfdbb9d69f24a245bdc8a7e0638524a2 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 20:36:31 -0400 Subject: [PATCH 134/152] Don't have mypy failure fail the whole tox run Other environments would still be run even after mypy has failed, but to avoid having tox runs be unnecessarily inconsistent with the mypy step in the pythonpackage.yml CI workflow, and also because GitPython is not currently expected to pass mypy checks, this keeps mypy errors from causing the whole tox run to be reported as failed. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9b918f560..a81cd2b45 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = pre-commit run --all-files description = Typecheck with mypy basepython = py39 commands = mypy -p git +ignore_outcome = true [testenv:black] description = Check style with black From e6ec6c87b8ed66e30f7addbd109ab6ec5d74326c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 18 Sep 2023 22:58:58 -0400 Subject: [PATCH 135/152] Add tox environment to build HTML documentation The main use of this, similar to the step at the end of pythonpackage.yml, is to find errors produced by building. However, actual documentation *is* built, and unlike other tox environments, running this one actually writes outside the .tox/ directory, creating the documentation in the usual target location. For that reason, this environment is omitted from the env_list, so that it does not run by default and unexpectedly overwrite documentation that may recently have been built before changes are made that could cause generated documentation to be different. --- tox.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index a81cd2b45..8d64b929b 100644 --- a/tox.ini +++ b/tox.ini @@ -11,16 +11,25 @@ commands = pytest --color=yes {posargs} [testenv:lint] description = Lint via pre-commit -basepython = py39 +base_python = py39 commands = pre-commit run --all-files [testenv:mypy] description = Typecheck with mypy -basepython = py39 +base_python = py39 commands = mypy -p git ignore_outcome = true [testenv:black] description = Check style with black -basepython = py39 +base_python = py39 commands = black --check --diff git + +# Run "tox -e html" for this. It is deliberately excluded from env_list, as +# unlike the other environments, this one writes outside the .tox/ directory. +[testenv:html] +description = Build HTML documentation +base_python = py39 +deps = -r doc/requirements.txt +allowlist_externals = make +commands = make -C doc html From a774182c05731797cfe3353ff9618c0cd80b6f35 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:02:52 -0400 Subject: [PATCH 136/152] Fix black exclusions to omit .gitignore dirs This replaces "exclude" with "extend-exclude" in the black configuration, so that it keeps its default exclusions, of which all directories listed in .gitignore are automatically a part. That makes it possible to run "black ." to format just the files that should be formatted (git/ files, test/ files, and setup.py), while automatically omitting .venv/, .tox/, build/, and so on. This commit does not change how black is run yet, it just fixes the way its exclusions are configured. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 42bb31eda..fa06458eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,4 @@ omit = ["*/git/ext/*"] [tool.black] line-length = 120 target-version = ['py37'] -exclude = "git/ext/gitdb" +extend-exclude = "git/ext/gitdb" From e39ecb7269fe266311a4bf766c626de5b95a9f9f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:06:58 -0400 Subject: [PATCH 137/152] Small manual formatting improvements This adds a trailing "," in a few multi-line function calls in test/, where putting one argument per line was intended and is clearer. This is so that when black is run over test/, it recognizes the form and avoids collapsing it. --- test/test_commit.py | 4 ++-- test/test_docs.py | 2 +- test/test_repo.py | 2 +- test/test_submodule.py | 2 +- test/test_util.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_commit.py b/test/test_commit.py index d13db1410..560497547 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -173,12 +173,12 @@ def check_entries(path, changes): ".github/workflows/Future.yml" : { 'insertions': 57, 'deletions': 0, - 'lines': 57 + 'lines': 57, }, ".github/workflows/test_pytest.yml" : { 'insertions': 0, 'deletions': 55, - 'lines': 55 + 'lines': 55, }, } assert path in expected diff --git a/test/test_docs.py b/test/test_docs.py index 505b50f77..4c23e9f81 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -481,7 +481,7 @@ def test_references_and_objects(self, rw_dir): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find SHA for submodule", - raises=ValueError + raises=ValueError, ) def test_submodules(self): # [1-test_submodules] diff --git a/test/test_repo.py b/test/test_repo.py index abae5ad78..1b46fba7c 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -1115,7 +1115,7 @@ def test_repo_odbtype(self): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find submodule SHA", - raises=ValueError + raises=ValueError, ) def test_submodules(self): self.assertEqual(len(self.rorepo.submodules), 1) # non-recursive diff --git a/test/test_submodule.py b/test/test_submodule.py index 5a7f26207..f7195626f 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -469,7 +469,7 @@ def test_base_bare(self, rwrepo): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin GitPython can't find submodule SHA", - raises=ValueError + raises=ValueError, ) @skipIf( HIDE_WINDOWS_KNOWN_ERRORS, diff --git a/test/test_util.py b/test/test_util.py index 517edd65c..42edc57cf 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -159,7 +159,7 @@ def test_lock_file(self): @pytest.mark.xfail( sys.platform == "cygwin", reason="Cygwin fails here for some reason, always", - raises=AssertionError + raises=AssertionError, ) def test_blocking_lock_file(self): my_file = tempfile.mktemp() From 288cf03e120ed6f7e62d6b0e5c974649e50e69de Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 09:09:08 -0400 Subject: [PATCH 138/152] Don't limit black to git/ This changes the documentation in README.md to recommend running "black ." and changes the command to that in tox.ini, so that more paths are covered (in practice, test/ and setup.py). --- README.md | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69d69c56f..dbec36024 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ mypy -p git For automatic code formatting, run: ```bash -black git +black . ``` Configuration for flake8 is in the `./.flake8` file. diff --git a/tox.ini b/tox.ini index 8d64b929b..82a41e22c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ ignore_outcome = true [testenv:black] description = Check style with black base_python = py39 -commands = black --check --diff git +commands = black --check --diff . # Run "tox -e html" for this. It is deliberately excluded from env_list, as # unlike the other environments, this one writes outside the .tox/ directory. From 15c736dc79922a1cead221f6fbda5378564e0b6d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 19 Sep 2023 10:20:28 -0400 Subject: [PATCH 139/152] Reformat tests with black This actually runs black on the whole project, but the only changes are in test/ (as expected). --- test/performance/test_streams.py | 1 - test/test_commit.py | 17 ++++++++-------- test/test_diff.py | 28 +++++++++++++-------------- test/test_index.py | 2 +- test/test_quick_doc.py | 27 ++++++++++++-------------- test/test_repo.py | 28 ++++++++++++++++----------- test/test_submodule.py | 33 ++++++++++++++++++++++---------- 7 files changed, 75 insertions(+), 61 deletions(-) diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py index 5588212e0..25e081578 100644 --- a/test/performance/test_streams.py +++ b/test/performance/test_streams.py @@ -15,7 +15,6 @@ class TestObjDBPerformance(TestBigRepoR): - large_data_size_bytes = 1000 * 1000 * 10 # some MiB should do it moderate_data_size_bytes = 1000 * 1000 * 1 # just 1 MiB diff --git a/test/test_commit.py b/test/test_commit.py index 560497547..f6fb49d50 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -93,7 +93,6 @@ def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info= class TestCommit(TestCommitSerialization): def test_bake(self): - commit = self.rorepo.commit("2454ae89983a4496a445ce347d7a41c0bb0ea7ae") # commits have no dict self.assertRaises(AttributeError, setattr, commit, "someattr", 1) @@ -170,15 +169,15 @@ def test_renames(self): def check_entries(path, changes): expected = { - ".github/workflows/Future.yml" : { - 'insertions': 57, - 'deletions': 0, - 'lines': 57, + ".github/workflows/Future.yml": { + "insertions": 57, + "deletions": 0, + "lines": 57, }, - ".github/workflows/test_pytest.yml" : { - 'insertions': 0, - 'deletions': 55, - 'lines': 55, + ".github/workflows/test_pytest.yml": { + "insertions": 0, + "deletions": 55, + "lines": 55, }, } assert path in expected diff --git a/test/test_diff.py b/test/test_diff.py index dacbdc3bc..9c3888f03 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -419,7 +419,7 @@ def test_rename_override(self, rw_dir): # create and commit file_a.txt repo = Repo.init(rw_dir) file_a = osp.join(rw_dir, "file_a.txt") - with open(file_a, "w", encoding='utf-8') as outfile: + with open(file_a, "w", encoding="utf-8") as outfile: outfile.write("hello world\n") repo.git.add(Git.polish_url(https://github.com/file_a)) repo.git.commit(message="Added file_a.txt") @@ -429,21 +429,21 @@ def test_rename_override(self, rw_dir): # create and commit file_b.txt with similarity index of 52 file_b = osp.join(rw_dir, "file_b.txt") - with open(file_b, "w", encoding='utf-8') as outfile: + with open(file_b, "w", encoding="utf-8") as outfile: outfile.write("hello world\nhello world") repo.git.add(Git.polish_url(https://github.com/file_b)) repo.git.commit(message="Removed file_a.txt. Added file_b.txt") - commit_a = repo.commit('HEAD') - commit_b = repo.commit('HEAD~1') + commit_a = repo.commit("HEAD") + commit_b = repo.commit("HEAD~1") # check default diff command with renamed files enabled diffs = commit_b.diff(commit_a) self.assertEqual(1, len(diffs)) diff = diffs[0] self.assertEqual(True, diff.renamed_file) - self.assertEqual('file_a.txt', diff.rename_from) - self.assertEqual('file_b.txt', diff.rename_to) + self.assertEqual("file_a.txt", diff.rename_from) + self.assertEqual("file_b.txt", diff.rename_to) # check diff with rename files disabled diffs = commit_b.diff(commit_a, no_renames=True) @@ -452,31 +452,31 @@ def test_rename_override(self, rw_dir): # check fileA.txt deleted diff = diffs[0] self.assertEqual(True, diff.deleted_file) - self.assertEqual('file_a.txt', diff.a_path) + self.assertEqual("file_a.txt", diff.a_path) # check fileB.txt added diff = diffs[1] self.assertEqual(True, diff.new_file) - self.assertEqual('file_b.txt', diff.a_path) + self.assertEqual("file_b.txt", diff.a_path) # check diff with high similarity index - diffs = commit_b.diff(commit_a, split_single_char_options=False, M='75%') + diffs = commit_b.diff(commit_a, split_single_char_options=False, M="75%") self.assertEqual(2, len(diffs)) # check fileA.txt deleted diff = diffs[0] self.assertEqual(True, diff.deleted_file) - self.assertEqual('file_a.txt', diff.a_path) + self.assertEqual("file_a.txt", diff.a_path) # check fileB.txt added diff = diffs[1] self.assertEqual(True, diff.new_file) - self.assertEqual('file_b.txt', diff.a_path) + self.assertEqual("file_b.txt", diff.a_path) # check diff with low similarity index - diffs = commit_b.diff(commit_a, split_single_char_options=False, M='40%') + diffs = commit_b.diff(commit_a, split_single_char_options=False, M="40%") self.assertEqual(1, len(diffs)) diff = diffs[0] self.assertEqual(True, diff.renamed_file) - self.assertEqual('file_a.txt', diff.rename_from) - self.assertEqual('file_b.txt', diff.rename_to) + self.assertEqual("file_a.txt", diff.rename_from) + self.assertEqual("file_b.txt", diff.rename_to) diff --git a/test/test_index.py b/test/test_index.py index 9b7ba52a6..fba9c78ec 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -946,7 +946,7 @@ def test_commit_msg_hook_fail(self, rw_repo): else: raise AssertionError("Should have caught a HookExecutionError") - @with_rw_repo('HEAD') + @with_rw_repo("HEAD") def test_index_add_pathlike(self, rw_repo): git_dir = Path(rw_repo.git_dir) diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index eaee4e581..9dc7b8d2e 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -13,14 +13,13 @@ def tearDown(self): @with_rw_directory def test_init_repo_object(self, path_to_dir): - # [1-test_init_repo_object] # $ git init from git import Repo repo = Repo.init(path_to_dir) - # ![1-test_init_repo_object] + # ![1-test_init_repo_object] # [2-test_init_repo_object] repo = Repo(path_to_dir) @@ -28,9 +27,9 @@ def test_init_repo_object(self, path_to_dir): @with_rw_directory def test_cloned_repo_object(self, local_dir): - from git import Repo import git + # code to clone from url # [1-test_cloned_repo_object] # $ git clone @@ -44,9 +43,9 @@ def test_cloned_repo_object(self, local_dir): # [2-test_cloned_repo_object] # We must make a change to a file so that we can add the update to git - update_file = 'dir1/file2.txt' # we'll use local_dir/dir1/file2.txt - with open(f"{local_dir}/{update_file}", 'a') as f: - f.write('\nUpdate version 2') + update_file = "dir1/file2.txt" # we'll use local_dir/dir1/file2.txt + with open(f"{local_dir}/{update_file}", "a") as f: + f.write("\nUpdate version 2") # ![2-test_cloned_repo_object] # [3-test_cloned_repo_object] @@ -82,7 +81,7 @@ def test_cloned_repo_object(self, local_dir): # Untracked files - create new file # [7-test_cloned_repo_object] - f = open(f'{local_dir}/untracked.txt', 'w') # creates an empty file + f = open(f"{local_dir}/untracked.txt", "w") # creates an empty file f.close() # ![7-test_cloned_repo_object] @@ -95,8 +94,8 @@ def test_cloned_repo_object(self, local_dir): # [9-test_cloned_repo_object] # Let's modify one of our tracked files - with open(f'{local_dir}/Downloads/file3.txt', 'w') as f: - f.write('file3 version 2') # overwrite file 3 + with open(f"{local_dir}/Downloads/file3.txt", "w") as f: + f.write("file3 version 2") # overwrite file 3 # ![9-test_cloned_repo_object] # [10-test_cloned_repo_object] @@ -126,7 +125,7 @@ def test_cloned_repo_object(self, local_dir): # ![11.1-test_cloned_repo_object] # [11.2-test_cloned_repo_object] # lets add untracked.txt - repo.index.add(['untracked.txt']) + repo.index.add(["untracked.txt"]) diffs = repo.index.diff(repo.head.commit) for d in diffs: print(d.a_path) @@ -146,9 +145,7 @@ def test_cloned_repo_object(self, local_dir): # dir1/file2.txt # ![11.3-test_cloned_repo_object] - - - '''Trees and Blobs''' + """Trees and Blobs""" # Latest commit tree # [12-test_cloned_repo_object] @@ -195,7 +192,7 @@ def print_files_from_git(root, level=0): # Printing text files # [17-test_cloned_repo_object] - print_file = 'dir1/file2.txt' + print_file = "dir1/file2.txt" tree[print_file] # the head commit tree # Output @@ -221,4 +218,4 @@ def print_files_from_git(root, level=0): # Output # file 2 version 1 - # ![18.1-test_cloned_repo_object] \ No newline at end of file + # ![18.1-test_cloned_repo_object] diff --git a/test/test_repo.py b/test/test_repo.py index 1b46fba7c..6432b8c6f 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -251,7 +251,9 @@ def test_clone_from_with_path_contains_unicode(self): self.fail("Raised UnicodeEncodeError") @with_rw_directory - @skip("the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control") + @skip( + "the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control" + ) def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" try: @@ -391,7 +393,9 @@ def test_clone_from_unsafe_options_allowed(self, rw_repo): for i, unsafe_option in enumerate(unsafe_options): destination = tmp_dir / str(i) assert not destination.exists() - Repo.clone_from(rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True) + Repo.clone_from( + rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True + ) assert destination.exists() @with_rw_repo("HEAD") @@ -755,8 +759,8 @@ def test_blame_complex_revision(self, git): @mock.patch.object(Git, "_call_process") def test_blame_accepts_rev_opts(self, git): res = self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) - expected_args = ['blame', 'HEAD', '-M', '-C', '-C', '--', 'README.md'] - boilerplate_kwargs = {"p" : True, "stdout_as_string": False} + expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] + boilerplate_kwargs = {"p": True, "stdout_as_string": False} git.assert_called_once_with(*expected_args, **boilerplate_kwargs) @skipIf( @@ -1415,14 +1419,16 @@ def test_ignored_items_reported(self): gi = tmp_dir / "repo" / ".gitignore" - with open(gi, 'w') as file: - file.write('ignored_file.txt\n') - file.write('ignored_dir/\n') + with open(gi, "w") as file: + file.write("ignored_file.txt\n") + file.write("ignored_dir/\n") - assert temp_repo.ignored(['included_file.txt', 'included_dir/file.txt']) == [] - assert temp_repo.ignored(['ignored_file.txt']) == ['ignored_file.txt'] - assert temp_repo.ignored(['included_file.txt', 'ignored_file.txt']) == ['ignored_file.txt'] - assert temp_repo.ignored(['included_file.txt', 'ignored_file.txt', 'included_dir/file.txt', 'ignored_dir/file.txt']) == ['ignored_file.txt', 'ignored_dir/file.txt'] + assert temp_repo.ignored(["included_file.txt", "included_dir/file.txt"]) == [] + assert temp_repo.ignored(["ignored_file.txt"]) == ["ignored_file.txt"] + assert temp_repo.ignored(["included_file.txt", "ignored_file.txt"]) == ["ignored_file.txt"] + assert temp_repo.ignored( + ["included_file.txt", "ignored_file.txt", "included_dir/file.txt", "ignored_dir/file.txt"] + ) == ["ignored_file.txt", "ignored_dir/file.txt"] def test_ignored_raises_error_w_symlink(self): with tempfile.TemporaryDirectory() as tdir: diff --git a/test/test_submodule.py b/test/test_submodule.py index f7195626f..88717e220 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -39,11 +39,14 @@ def _patch_git_config(name, value): # This is recomputed each time the context is entered, for compatibility with # existing GIT_CONFIG_* environment variables, even if changed in this process. - patcher = mock.patch.dict(os.environ, { - "GIT_CONFIG_COUNT": str(pair_index + 1), - f"GIT_CONFIG_KEY_{pair_index}": name, - f"GIT_CONFIG_VALUE_{pair_index}": value, - }) + patcher = mock.patch.dict( + os.environ, + { + "GIT_CONFIG_COUNT": str(pair_index + 1), + f"GIT_CONFIG_KEY_{pair_index}": name, + f"GIT_CONFIG_VALUE_{pair_index}": value, + }, + ) with patcher: yield @@ -914,17 +917,17 @@ def test_ignore_non_submodule_file(self, rwdir): os.mkdir(smp) with open(osp.join(smp, "a"), "w", encoding="utf-8") as f: - f.write('test\n') + f.write("test\n") with open(osp.join(rwdir, ".gitmodules"), "w", encoding="utf-8") as f: - f.write("[submodule \"a\"]\n") + f.write('[submodule "a"]\n') f.write(" path = module\n") f.write(" url = https://github.com/chaconinc/DbConnector\n") parent.git.add(Git.polish_url(https://github.com/osp.join(smp,%20%22a"))) parent.git.add(Git.polish_url(https://github.com/osp.join(rwdir,%20%22.gitmodules"))) - parent.git.commit(message='test') + parent.git.commit(message="test") assert len(parent.submodules) == 0 @@ -1200,7 +1203,12 @@ def test_submodule_add_unsafe_options_allowed(self, rw_repo): # The options will be allowed, but the command will fail. with self.assertRaises(GitCommandError): Submodule.add( - rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True + rw_repo, + "new", + "new", + str(tmp_dir), + clone_multi_options=[unsafe_option], + allow_unsafe_options=True, ) assert not tmp_file.exists() @@ -1211,7 +1219,12 @@ def test_submodule_add_unsafe_options_allowed(self, rw_repo): for unsafe_option in unsafe_options: with self.assertRaises(GitCommandError): Submodule.add( - rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True + rw_repo, + "new", + "new", + str(tmp_dir), + clone_multi_options=[unsafe_option], + allow_unsafe_options=True, ) @with_rw_repo("HEAD") From bf7af69306ed8f14b33528165473ca3591a76246 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 21:29:33 -0400 Subject: [PATCH 140/152] Upgrade flake8 in pre-commit and fix new warnings Upgrading flake8 from 6.0.0 to 6.1.0 causes its pycodestyle dependency to be upgraded from 2.10.* to 2.11.*, which is desirable because: - Spurious "E231 missing whitespace after ':'" warnings on 3.12 due to the lack of full compatibility with Python 3.12 are gone. - New warnings appear, at least one of which, "E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`", does identify something we can improve. This upgrades flake8 in pre-commit and fixes the new warnings. --- .pre-commit-config.yaml | 2 +- git/objects/submodule/base.py | 2 +- git/refs/log.py | 2 +- git/util.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 581cb69b2..888e62d91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 0d20305c6..c7e7856f0 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1403,7 +1403,7 @@ def iter_items( # END handle critical error # Make sure we are looking at a submodule object - if type(sm) != git.objects.submodule.base.Submodule: + if type(sm) is not git.objects.submodule.base.Submodule: continue # fill in remaining info - saves time as it doesn't have to be parsed again diff --git a/git/refs/log.py b/git/refs/log.py index 1f86356a4..1c2a2c470 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -244,7 +244,7 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry": for i in range(index + 1): line = fp.readline() if not line: - raise IndexError(f"Index file ended at line {i+1}, before given index was reached") + raise IndexError(f"Index file ended at line {i + 1}, before given index was reached") # END abort on eof # END handle runup diff --git a/git/util.py b/git/util.py index 638807a36..48901ba0c 100644 --- a/git/util.py +++ b/git/util.py @@ -1136,7 +1136,7 @@ class IterableClassWatcher(type): def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: for base in bases: - if type(base) == IterableClassWatcher: + if type(base) is IterableClassWatcher: warnings.warn( f"GitPython Iterable subclassed by {name}. " "Iterable is deprecated due to naming clash since v3.1.18" From c1ec9cbd2a995585e53c90d61ae30102aaaff2a5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 22:25:40 -0400 Subject: [PATCH 141/152] Update flake8 additional dependencies, fix warning This bumps the versions of the flake8 plugins specified with pinned versions as additional dependencies of flake8 for pre-commit. Doing so gains a warning about a call to warnings.warn with no stacklevel argument. This appears to be the uncommon case where the implifit effect of stacklevel=1 is intended, so I have made that explicit, which clarifies this intent and dismisses the warning. --- .pre-commit-config.yaml | 4 ++-- git/repo/base.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 888e62d91..4c92656e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,8 @@ repos: - id: flake8 additional_dependencies: [ - flake8-bugbear==22.12.6, - flake8-comprehensions==3.10.1, + flake8-bugbear==23.9.16, + flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] exclude: ^doc|^git/ext/|^test/ diff --git a/git/repo/base.py b/git/repo/base.py index fda3cdc88..bc1b8876d 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -206,7 +206,8 @@ def __init__( if expand_vars and re.search(self.re_envvars, epath): warnings.warn( "The use of environment variables in paths is deprecated" - + "\nfor security reasons and may be removed in the future!!" + + "\nfor security reasons and may be removed in the future!!", + stacklevel=1, ) epath = expand_path(epath, expand_vars) if epath is not None: From 48441a94189494789f751e75f9e1919b9b700b13 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 23:40:44 -0400 Subject: [PATCH 142/152] Lint test/ (not just git/), fix warnings and a bug This expands flake8 linting to include the test suite, and fixes the resulting warnings. Four code changes are especially notable: - Unit tests from which documentation is automatically generated contain a few occurrences of "# @NoEffect". These suppressions are extended to "# noqa: B015 # @NoEffect" so the corresponding suppression is also applied for flake8. This is significant because it actually changes what appears in the code examples in the generated documentation. However, since the "@NoEffect" annotation was considered acceptable, this may be okay too. The resulting examples did not become excessively long. - Expressions like "[c for c in commits_for_file_generator]" appear in some unit tests from which documentation is automatically generated. The simpler form "list(commits_for_file_generator)" can replace them. This changes the examples in the documentation, but for the better, since that form is simpler (and also a better practice in general, thus a better thing to show readers). So I made those substitutions. - In test_repo.TestRepo.test_git_work_tree_env, the code intended to unpatch environment variables after the test was ineffective, instead causing os.environ to refer to an ordinary dict object that does not affect environment variables when written to. This uses unittest.mock.patch.dict instead, so the variables are unpatched and subsequent writes to environment variables in the test process are effective. - In test_submodule.TestSubmodule._do_base_tests, the expression statement "csm.module().head.ref.tracking_branch() is not None" appeared to be intended as an assertion, in spite of having been annoated @NoEffect. This is in light of the context and because, if the goal were only to exercise the function call, the "is not None" part would be superfluous. So I made it an assertion. --- .flake8 | 2 +- .pre-commit-config.yaml | 2 +- test/test_commit.py | 14 +++++++------- test/test_diff.py | 1 - test/test_docs.py | 8 ++++---- test/test_quick_doc.py | 14 ++++++-------- test/test_remote.py | 2 +- test/test_repo.py | 16 +++++++--------- test/test_submodule.py | 9 +++++---- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/.flake8 b/.flake8 index 08001ffac..ed5d036bf 100644 --- a/.flake8 +++ b/.flake8 @@ -26,7 +26,7 @@ ignore = E265,E266,E731,E704, D, RST, RST3 -exclude = .tox,.venv,build,dist,doc,git/ext/,test +exclude = .tox,.venv,build,dist,doc,git/ext/ rst-roles = # for flake8-RST-docstrings attr,class,func,meth,mod,obj,ref,term,var # used by sphinx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c92656e4..5a34b8af0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] - exclude: ^doc|^git/ext/|^test/ + exclude: ^doc|^git/ext/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/test/test_commit.py b/test/test_commit.py index f6fb49d50..527aea334 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -277,7 +277,7 @@ def __init__(self, *args, **kwargs): super(Child, self).__init__(*args, **kwargs) child_commits = list(Child.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS"))) - assert type(child_commits[0]) == Child + assert type(child_commits[0]) is Child def test_iter_items(self): # pretty not allowed @@ -525,12 +525,12 @@ def test_trailers(self): # 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", + "Subject\n", + "Subject\n\nBody with some\nText\n", + "Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", + "Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", + "Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", + "Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n", ] for msg in msgs: diff --git a/test/test_diff.py b/test/test_diff.py index 9c3888f03..5aa4408bf 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -7,7 +7,6 @@ import ddt import shutil import tempfile -import unittest from git import ( Repo, GitCommandError, diff --git a/test/test_docs.py b/test/test_docs.py index 4c23e9f81..d6b88855f 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -263,9 +263,9 @@ def test_references_and_objects(self, rw_dir): # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree - hc != hct # @NoEffect - hc != repo.tags[0] # @NoEffect - hc == repo.head.reference.commit # @NoEffect + hc != hct # noqa: B015 # @NoEffect + hc != repo.tags[0] # noqa: B015 # @NoEffect + hc == repo.head.reference.commit # noqa: B015 # @NoEffect # ![8-test_references_and_objects] # [9-test_references_and_objects] @@ -369,7 +369,7 @@ def test_references_and_objects(self, rw_dir): # The index contains all blobs in a flat list assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == "blob"]) # Access blob objects - for (_path, _stage), entry in index.entries.items(): + for (_path, _stage), _entry in index.entries.items(): pass new_file_path = os.path.join(repo.working_tree_dir, "new-file-name") open(new_file_path, "w").close() diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 9dc7b8d2e..342a7f293 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -1,6 +1,3 @@ -import pytest - - from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -25,10 +22,11 @@ def test_init_repo_object(self, path_to_dir): repo = Repo(path_to_dir) # ![2-test_init_repo_object] + del repo # Avoids "assigned to but never used" warning. Doesn't go in the docs. + @with_rw_directory def test_cloned_repo_object(self, local_dir): from git import Repo - import git # code to clone from url # [1-test_cloned_repo_object] @@ -72,7 +70,7 @@ def test_cloned_repo_object(self, local_dir): # [6-test_cloned_repo_object] commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=update_file) - commits_for_file = [c for c in commits_for_file_generator] + commits_for_file = list(commits_for_file_generator) commits_for_file # Outputs: [, @@ -136,7 +134,7 @@ def test_cloned_repo_object(self, local_dir): # Compare commit to commit # [11.3-test_cloned_repo_object] - first_commit = [c for c in repo.iter_commits(all=True)][-1] + first_commit = list(repo.iter_commits(all=True))[-1] diffs = repo.head.commit.diff(first_commit) for d in diffs: print(d.a_path) @@ -154,7 +152,7 @@ def test_cloned_repo_object(self, local_dir): # Previous commit tree # [13-test_cloned_repo_object] - prev_commits = [c for c in repo.iter_commits(all=True, max_count=10)] # last 10 commits from all branches + prev_commits = list(repo.iter_commits(all=True, max_count=10)) # last 10 commits from all branches tree = prev_commits[0].tree # ![13-test_cloned_repo_object] @@ -210,7 +208,7 @@ def print_files_from_git(root, level=0): # print previous tree # [18.1-test_cloned_repo_object] - commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + commits_for_file = list(repo.iter_commits(all=True, paths=print_file)) tree = commits_for_file[-1].tree # gets the first commit tree blob = tree[print_file] diff --git a/test/test_remote.py b/test/test_remote.py index e0dcb4131..7144b2791 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -160,7 +160,7 @@ 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]): + 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 diff --git a/test/test_repo.py b/test/test_repo.py index 6432b8c6f..d3bc864cd 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -252,7 +252,8 @@ def test_clone_from_with_path_contains_unicode(self): @with_rw_directory @skip( - "the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control" + """The referenced repository was removed, and one needs to set up a new + password controlled repo under the org's control.""" ) def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" @@ -758,9 +759,9 @@ def test_blame_complex_revision(self, git): @mock.patch.object(Git, "_call_process") def test_blame_accepts_rev_opts(self, git): - res = self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] boilerplate_kwargs = {"p": True, "stdout_as_string": False} + self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) git.assert_called_once_with(*expected_args, **boilerplate_kwargs) @skipIf( @@ -971,7 +972,7 @@ def _assert_rev_parse(self, name): # history with number ni = 11 history = [obj.parents[0]] - for pn in range(ni): + for _ in range(ni): history.append(history[-1].parents[0]) # END get given amount of commits @@ -1329,6 +1330,7 @@ def test_git_work_tree_env(self, rw_dir): # move .git directory to a subdirectory # set GIT_DIR and GIT_WORK_TREE appropriately # check that repo.working_tree_dir == rw_dir + self.rorepo.clone(join_path_native(rw_dir, "master_repo")) repo_dir = join_path_native(rw_dir, "master_repo") @@ -1338,16 +1340,12 @@ def test_git_work_tree_env(self, rw_dir): os.mkdir(new_subdir) os.rename(old_git_dir, new_git_dir) - oldenv = os.environ.copy() - os.environ["GIT_DIR"] = new_git_dir - os.environ["GIT_WORK_TREE"] = repo_dir + to_patch = {"GIT_DIR": new_git_dir, "GIT_WORK_TREE": repo_dir} - try: + with mock.patch.dict(os.environ, to_patch): r = Repo() self.assertEqual(r.working_tree_dir, repo_dir) self.assertEqual(r.working_dir, repo_dir) - finally: - os.environ = oldenv @with_rw_directory def test_rebasing(self, rw_dir): diff --git a/test/test_submodule.py b/test/test_submodule.py index 88717e220..35ff0d7a8 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -111,7 +111,7 @@ def _do_base_tests(self, rwrepo): # force it to reread its information del smold._url - smold.url == sm.url # @NoEffect + smold.url == sm.url # noqa: B015 # @NoEffect # test config_reader/writer methods sm.config_reader() @@ -248,7 +248,7 @@ def _do_base_tests(self, rwrepo): assert csm.module_exists() # tracking branch once again - csm.module().head.ref.tracking_branch() is not None # @NoEffect + assert csm.module().head.ref.tracking_branch() is not None # this flushed in a sub-submodule assert len(list(rwrepo.iter_submodules())) == 2 @@ -480,8 +480,9 @@ def test_base_bare(self, rwrepo): File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute raise GitCommandNotFound(command, err) git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module""", - ) # noqa E501 + cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module + """, # noqa E501 + ) @with_rw_repo(k_subm_current, bare=False) def test_root_module(self, rwrepo): # Can query everything without problems From 98877c58cd402ea94d4235ab9d4074db76dca74d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 02:46:48 -0400 Subject: [PATCH 143/152] Refactor "finally" cleanup in tests, fix minor bug This refactors code in the test suite that uses try-finally, to simplify and clarify what it is doing. An exception to this being a refactoring is that a possible bug is fixed where a throwaway environment variable "FOO" was patched for a test and never unpatched. There are two patterns refactored here: - try-(try-except)-finally to try-except-finally. When the entire suite of the try-block in try-finally is itself a try-except, that has the same effect as try-except-finally. (Python always attempts to run the finally suite in either case, and does so after any applicable except suite is run.) - Replacing try-finally with an appropriate context manager. (These changes do not fix, nor introduce, any flake8 errors, but they were found by manual search for possible problems related to recent flake8 findings but outside of what flake8 can catch. To limit scope, this only refactors try-finally in the test suite.) --- test/lib/helper.py | 32 +++++++++++++++----------------- test/test_git.py | 17 ++++++----------- test/test_repo.py | 9 ++------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/test/lib/helper.py b/test/lib/helper.py index 64c70a8f4..e8464b7d4 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -94,17 +94,16 @@ def wrapper(self): os.mkdir(path) keep = False try: - try: - return func(self, path) - except Exception: - log.info( - "Test %s.%s failed, output is at %r\n", - type(self).__name__, - func.__name__, - path, - ) - keep = True - raise + return func(self, path) + except Exception: + log.info( + "Test %s.%s failed, output is at %r\n", + type(self).__name__, + func.__name__, + path, + ) + keep = True + raise finally: # Need to collect here to be sure all handles have been closed. It appears # a windows-only issue. In fact things should be deleted, as well as @@ -147,12 +146,11 @@ def repo_creator(self): prev_cwd = os.getcwd() os.chdir(rw_repo.working_dir) try: - try: - return func(self, rw_repo) - except: # noqa E722 - log.info("Keeping repo after failure: %s", repo_dir) - repo_dir = None - raise + return func(self, rw_repo) + except: # noqa E722 + log.info("Keeping repo after failure: %s", repo_dir) + repo_dir = None + raise finally: os.chdir(prev_cwd) rw_repo.git.clear_cache() diff --git a/test/test_git.py b/test/test_git.py index 2c392155a..31065d8fd 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -195,17 +195,12 @@ def test_version(self): # END verify number types def test_cmd_override(self): - prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE - exc = GitCommandNotFound - try: - # 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) - finally: - type(self.git).GIT_PYTHON_GIT_EXECUTABLE = prev_cmd - # END undo adjustment + with mock.patch.object( + type(self.git), + "GIT_PYTHON_GIT_EXECUTABLE", + osp.join("some", "path", "which", "doesn't", "exist", "gitbinary"), + ): + self.assertRaises(GitCommandNotFound, self.git.version) def test_refresh(self): # test a bad git path refresh diff --git a/test/test_repo.py b/test/test_repo.py index d3bc864cd..15899ec50 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -847,18 +847,13 @@ def test_comparison_and_hash(self): @with_rw_directory def test_tilde_and_env_vars_in_repo_path(self, rw_dir): - ph = os.environ.get("HOME") - try: + with mock.patch.dict(os.environ, {"HOME": rw_dir}): os.environ["HOME"] = rw_dir Repo.init(osp.join("~", "test.git"), bare=True) + with mock.patch.dict(os.environ, {"FOO": rw_dir}): os.environ["FOO"] = rw_dir Repo.init(osp.join("$FOO", "test.git"), bare=True) - finally: - if ph: - os.environ["HOME"] = ph - del os.environ["FOO"] - # end assure HOME gets reset to what it was def test_git_cmd(self): # test CatFileContentStream, just to be very sure we have no fencepost errors From 46d3d0520d8877b1979e6385bada9d9e1e0731ec Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Thu, 21 Sep 2023 12:00:59 +0200 Subject: [PATCH 144/152] Add more checks for the validity of refnames This change adds checks based on the rules described in [0] in order to more robustly check a refname's validity. [0]: https://git-scm.com/docs/git-check-ref-format --- git/refs/symbolic.py | 50 ++++++++++++++++++++++++++++++++++++++++++-- test/test_refs.py | 36 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 5c293aa7b..819615103 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -161,6 +161,51 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> return hexsha # END recursive dereferencing + @staticmethod + def _check_ref_name_valid(ref_path: PathLike) -> None: + # Based on the rules described in https://git-scm.com/docs/git-check-ref-format/#_description + previous: Union[str, None] = None + one_before_previous: Union[str, None] = None + for c in str(ref_path): + if c in " ~^:?*[\\": + raise ValueError( + f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^)," + f" colons (:), question marks (?), asterisks (*), open brackets ([) or backslashes (\\)" + ) + elif c == ".": + if previous is None or previous == "/": + raise ValueError( + f"Invalid reference '{ref_path}': references cannot start with a period (.) or contain '/.'" + ) + elif previous == ".": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '..'") + elif c == "/": + if previous == "/": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '//'") + elif previous is None: + raise ValueError( + f"Invalid reference '{ref_path}': references cannot start with forward slashes '/'" + ) + elif c == "{" and previous == "@": + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '@{{'") + elif ord(c) < 32 or ord(c) == 127: + raise ValueError(f"Invalid reference '{ref_path}': references cannot contain ASCII control characters") + + one_before_previous = previous + previous = c + + if previous == ".": + raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a period (.)") + elif previous == "/": + raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)") + elif previous == "@" and one_before_previous is None: + raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'") + elif any([component.endswith(".lock") for component in str(ref_path).split("/")]): + raise ValueError( + f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with" + f" '.lock'" + ) + @classmethod def _get_ref_info_helper( cls, repo: "Repo", ref_path: Union[PathLike, None] @@ -168,8 +213,9 @@ def _get_ref_info_helper( """Return: (str(sha), str(target_ref_path)) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" - if ".." in str(ref_path): - raise ValueError(f"Invalid reference '{ref_path}'") + if ref_path: + cls._check_ref_name_valid(ref_path) + tokens: Union[None, List[str], Tuple[str, str]] = None repodir = _git_dir(repo, ref_path) try: diff --git a/test/test_refs.py b/test/test_refs.py index afd273df9..80166f651 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -631,3 +631,39 @@ def test_refs_outside_repo(self): ref_file.flush() ref_file_name = Path(ref_file.name).name self.assertRaises(BadName, self.rorepo.commit, f"../../{ref_file_name}") + + def test_validity_ref_names(self): + check_ref = SymbolicReference._check_ref_name_valid + # Based on the rules specified in https://git-scm.com/docs/git-check-ref-format/#_description + # Rule 1 + self.assertRaises(ValueError, check_ref, ".ref/begins/with/dot") + self.assertRaises(ValueError, check_ref, "ref/component/.begins/with/dot") + self.assertRaises(ValueError, check_ref, "ref/ends/with/a.lock") + self.assertRaises(ValueError, check_ref, "ref/component/ends.lock/with/period_lock") + # Rule 2 + check_ref("valid_one_level_refname") + # Rule 3 + self.assertRaises(ValueError, check_ref, "ref/contains/../double/period") + # Rule 4 + for c in " ~^:": + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character") + for code in range(0, 32): + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(code)}/ASCII/control_character") + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(127)}/ASCII/control_character") + # Rule 5 + for c in "*?[": + self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character") + # Rule 6 + self.assertRaises(ValueError, check_ref, "/ref/begins/with/slash") + self.assertRaises(ValueError, check_ref, "ref/ends/with/slash/") + self.assertRaises(ValueError, check_ref, "ref/contains//double/slash/") + # Rule 7 + self.assertRaises(ValueError, check_ref, "ref/ends/with/dot.") + # Rule 8 + self.assertRaises(ValueError, check_ref, "ref/contains@{/at_brace") + # Rule 9 + self.assertRaises(ValueError, check_ref, "@") + # Rule 10 + self.assertRaises(ValueError, check_ref, "ref/contain\\s/backslash") + # Valid reference name should not raise + check_ref("valid/ref/name") From c5693204f772102f289c2e4269a399370a38d82b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 06:40:03 -0400 Subject: [PATCH 145/152] Make an old mock.patch.dict on os.environ clearer This clarifies that the object's contents are patched, rather than patching the attribute used to get the object in the first place. It is also in keeping with the style of patching os.environ elsewhere in the test suite. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 31065d8fd..481309538 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -245,7 +245,7 @@ def test_insert_after_kwarg_raises(self): def test_env_vars_passed_to_git(self): editor = "non_existent_editor" - with mock.patch.dict("os.environ", {"GIT_EDITOR": editor}): # @UndefinedVariable + with mock.patch.dict(os.environ, {"GIT_EDITOR": editor}): self.assertEqual(self.git.var("GIT_EDITOR"), editor) @with_rw_directory From 592ec8492326ccef7b4af022bffcc385eac8adb8 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 06:57:27 -0400 Subject: [PATCH 146/152] Fix rollback bug in SymbolicReference.set_reference This fixes the initialization of the "ok" flag by setting it to False before the operation that might fail is attempted. (This is a fix for the *specific* problem reported in #1669 about how the pattern SymbolicReference.set_reference attempts to use with LockedFD is not correctly implemented.) --- 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 5c293aa7b..6361713de 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -370,7 +370,7 @@ def set_reference( lfd = LockedFD(fpath) fd = lfd.open(write=True, stream=True) - ok = True + ok = False try: fd.write(write_value.encode("utf-8") + b"\n") lfd.commit() From ff84b26445b147ee9e2c75d82903b0c6b09e2b7a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 03:00:04 -0400 Subject: [PATCH 147/152] Refactor try-finally cleanup in git/ This is, in part, to help avoid (or be able to notice) other bugs like the rollback bug that affected SymbolicReference. However, it also includes a simplification of try-(try-except)-finally to try-except-finally. --- git/config.py | 17 ++++++++--------- git/index/base.py | 8 +++----- git/refs/symbolic.py | 8 +++----- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/git/config.py b/git/config.py index 880eb9301..76b149179 100644 --- a/git/config.py +++ b/git/config.py @@ -406,15 +406,14 @@ def release(self) -> None: return try: - try: - self.write() - except IOError: - log.error("Exception during destruction of GitConfigParser", exc_info=True) - except ReferenceError: - # This happens in PY3 ... and usually means that some state cannot be written - # as the sections dict cannot be iterated - # Usually when shutting down the interpreter, don'y know how to fix this - pass + self.write() + except IOError: + log.error("Exception during destruction of GitConfigParser", exc_info=True) + except ReferenceError: + # This happens in PY3 ... and usually means that some state cannot be + # written as the sections dict cannot be iterated + # Usually when shutting down the interpreter, don't know how to fix this + pass finally: if self._lock is not None: self._lock._release_lock() diff --git a/git/index/base.py b/git/index/base.py index cf016df6a..0cdeb1ce5 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -224,13 +224,11 @@ def write( lfd = LockedFD(file_path or self._file_path) stream = lfd.open(write=True, stream=True) - ok = False try: self._serialize(stream, ignore_extension_data) - ok = True - finally: - if not ok: - lfd.rollback() + except BaseException: + lfd.rollback() + raise lfd.commit() diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 6361713de..734bf32d8 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -370,14 +370,12 @@ def set_reference( lfd = LockedFD(fpath) fd = lfd.open(write=True, stream=True) - ok = False try: fd.write(write_value.encode("utf-8") + b"\n") lfd.commit() - ok = True - finally: - if not ok: - lfd.rollback() + except BaseException: + lfd.rollback() + raise # Adjust the reflog if logmsg is not None: self.log_append(oldbinsha, logmsg) From e480985aa4d358d0cc167d4552910e85944b8966 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 07:05:40 -0400 Subject: [PATCH 148/152] Tweak rollback logic in log.to_file This modifies the exception handling in log.to_file so it catches BaseException rather than Exception and rolls back. Ordinarily we do not want to catch BaseException, since this means catching things like SystemExit, KeyboardInterupt, etc., but the other cases of rolling back with LockedFD do it that strongly (both before when try-finally was used with a flag, and now with try-except catching BaseException to roll back the temporary-file write and reraise). Having this behave subtly different does not appear intentional. (This is also closer to what will happen if LockedFD becomes a context manager and these pieces of code use it in a with-statement: even exceptions not inheriting from Exception will cause __exit__ to be called.) --- git/refs/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/git/refs/log.py b/git/refs/log.py index 1f86356a4..9acc0e360 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -262,8 +262,7 @@ def to_file(self, filepath: PathLike) -> None: try: self._serialize(fp) lfd.commit() - except Exception: - # on failure it rolls back automatically, but we make it clear + except BaseException: lfd.rollback() raise # END handle change From a4701a0f17308ec8d4b5871e6e2a95c4e2ca5b91 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 19:12:03 -0400 Subject: [PATCH 149/152] Remove `@NoEffect` annotations + Add missing asserts, where an expression statement was by itself that was intended as an assertion. This turned out to be the case in the places `@NoEffect` appeared in rendered documentation, making it so no per-file-ignores or other broadened suppressions were needed. + Fix misspellings (including one case affecting documentation). + Add a FIXME comment for investigating a free-standing expression statement with no obvious side effects that may have been meant as an assertion but would fail if turned into one. --- test/test_docs.py | 10 +++++----- test/test_refs.py | 4 ++-- test/test_submodule.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_docs.py b/test/test_docs.py index d6b88855f..79e1f1be4 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -167,7 +167,7 @@ def update(self, op_code, cur_count, max_count=None, message=""): open(new_file_path, "wb").close() # create new file in working tree cloned_repo.index.add([new_file_path]) # add it to the index # Commit the changes to deviate masters history - cloned_repo.index.commit("Added a new file in the past - for later merege") + cloned_repo.index.commit("Added a new file in the past - for later merge") # prepare a merge master = cloned_repo.heads.master # right-hand side is ahead of us, in the future @@ -198,7 +198,7 @@ def update(self, op_code, cur_count, max_count=None, message=""): # .gitmodules was written and added to the index, which is now being committed cloned_repo.index.commit("Added submodule") - assert sm.exists() and sm.module_exists() # this submodule is defintely available + assert sm.exists() and sm.module_exists() # this submodule is definitely available sm.remove(module=True, configuration=False) # remove the working tree assert sm.exists() and not sm.module_exists() # the submodule itself is still available @@ -263,9 +263,9 @@ def test_references_and_objects(self, rw_dir): # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree - hc != hct # noqa: B015 # @NoEffect - hc != repo.tags[0] # noqa: B015 # @NoEffect - hc == repo.head.reference.commit # noqa: B015 # @NoEffect + assert hc != hct + assert hc != repo.tags[0] + assert hc == repo.head.reference.commit # ![8-test_references_and_objects] # [9-test_references_and_objects] diff --git a/test/test_refs.py b/test/test_refs.py index afd273df9..7598deb08 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -386,7 +386,7 @@ def test_head_reset(self, rw_repo): head_tree = head.commit.tree self.assertRaises(ValueError, setattr, head, "commit", head_tree) assert head.commit == old_commit # and the ref did not change - # we allow heds to point to any object + # we allow heads to point to any object head.object = head_tree assert head.object == head_tree # cannot query tree as commit @@ -489,7 +489,7 @@ def test_head_reset(self, rw_repo): cur_head.reference.commit, ) # it works if the new ref points to the same reference - assert SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path # @NoEffect + assert SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path SymbolicReference.delete(rw_repo, symref) # would raise if the symref wouldn't have been deletedpbl symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference) diff --git a/test/test_submodule.py b/test/test_submodule.py index 35ff0d7a8..4a9c9c582 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -111,7 +111,7 @@ def _do_base_tests(self, rwrepo): # force it to reread its information del smold._url - smold.url == sm.url # noqa: B015 # @NoEffect + smold.url == sm.url # noqa: B015 # FIXME: Should this be an assertion? # test config_reader/writer methods sm.config_reader() From 832b6eeb4a14e669099c486862c9f568215d5afb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:04:21 +0200 Subject: [PATCH 150/152] remove unnecessary list comprehension to fix CI --- 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 4d087e7a7..549160444 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -200,7 +200,7 @@ def _check_ref_name_valid(ref_path: PathLike) -> None: raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)") elif previous == "@" and one_before_previous is None: raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'") - elif any([component.endswith(".lock") for component in str(ref_path).split("/")]): + elif any(component.endswith(".lock") for component in str(ref_path).split("/")): raise ValueError( f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with" f" '.lock'" From 0bd2890ef42a7506b81a96c3c94b064917ed0d7b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:33:54 +0200 Subject: [PATCH 151/152] prepare next release --- VERSION | 2 +- doc/source/changes.rst | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b402c1a8b..1f1a39706 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.36 +3.1.37 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 06ec4b72c..a789b068d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,15 @@ Changelog ========= +3.1.37 +====== + +This release contains another security fix that further improves validation of symbolic references +and thus properly fixes this CVE: https://github.com/advisories/GHSA-cwvm-v4w8-q58c . + +See the following for all changes. +https://github.com/gitpython-developers/gitpython/milestone/67?closed=1 + 3.1.36 ====== From b27a89f683cda85ebd78243c055e876282df89ee Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 22 Sep 2023 09:37:09 +0200 Subject: [PATCH 152/152] fix makefile to compare commit hashes only Otherwise, with signed commits, the latest-tag-HEAD comparison would always fail. --- check-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check-version.sh b/check-version.sh index 6d10d6785..c50bf498b 100755 --- a/check-version.sh +++ b/check-version.sh @@ -29,7 +29,7 @@ changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")" config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)" latest_tag="$(git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1)" head_sha="$(git rev-parse HEAD)" -latest_tag_sha="$(git rev-parse "$latest_tag")" +latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")" # Display a table of all the current version, tag, and HEAD commit information. echo $'\nThe VERSION must be the same in all locations, and so must the HEAD and tag SHA'