From 1d808917168ee292a9e4c5fb575a79133d253bb3 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 11 Jun 2025 14:05:31 +0200 Subject: [PATCH 01/45] fix updating submodules with relative urls This fixes running repo.update_submodules(init=True) on repositories that are using relative for the modules. Fixes #730 --- git/objects/submodule/base.py | 5 +++++ test/test_submodule.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index fa60bcdaf..0e55b8fa9 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -353,6 +353,11 @@ def _clone_repo( os.makedirs(module_abspath_dir) module_checkout_path = osp.join(str(repo.working_tree_dir), path) + if url.startswith("../"): + remote_name = repo.active_branch.tracking_branch().remote_name + repo_remote_url = repo.remote(remote_name).url + url = os.path.join(repo_remote_url, url) + clone = git.Repo.clone_from( url, module_checkout_path, diff --git a/test/test_submodule.py b/test/test_submodule.py index d88f9dab0..f44f086c2 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -753,6 +753,22 @@ def test_add_empty_repo(self, rwdir): ) # END for each checkout mode + @with_rw_directory + @_patch_git_config("protocol.file.allow", "always") + def test_update_submodule_with_relative_path(self, rwdir): + repo_path = osp.join(rwdir, "parent") + repo = git.Repo.init(repo_path) + module_repo_path = osp.join(rwdir, "module") + module_repo = git.Repo.init(module_repo_path) + module_repo.git.commit(m="test", allow_empty=True) + repo.git.submodule("add", "../module", "module") + repo.index.commit("add submodule") + + cloned_repo_path = osp.join(rwdir, "cloned_repo") + cloned_repo = git.Repo.clone_from(repo_path, cloned_repo_path) + + cloned_repo.submodule_update(init=True, recursive=True) + @with_rw_directory @_patch_git_config("protocol.file.allow", "always") def test_list_only_valid_submodules(self, rwdir): From 088d090ed3016aca68f3376736bf9f2b18d0fe7e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 12 Jun 2025 02:39:11 -0400 Subject: [PATCH 02/45] Run `cat_file.py` fixture without site customizations This fixes a new `TestGit::test_handle_process_output` test failure on Cygwin where a `CoverageWarning` was printed to stderr in the Python interpreter subprocess running the `cat_file.py` fixture. We usually run the test suite with `pytest-cov` enabled. This is configured in `pyproject.toml` to happen by default. `pytest-cov` uses the `coverage` module, but it adds some more functionality. This includes instrumenting subprocesses, which is achieved by installing its `pytest-cov.pth` file into `site-packages` to be run by all Python interpreter instances. This causes interpeters to check for environment variables such as `COV_CORE_SOURCE` and to conditionally initialize `pytest_cov`. For details, see: https://pytest-cov.readthedocs.io/en/latest/subprocess-support.html `coverage` 7.9.0 was recently released. One of the changes is to start issuing a warning if it can't import the C tracer core. See: https://github.com/nedbat/coveragepy/releases/tag/7.9.0 If this warning is issued in the `cat_file.py` subprocess used in `test_handle_process_output`, it causes the test to fail, because the subprocess writes two more lines to its standard error stream, which cause the line count to come out as two more than expected: /cygdrive/d/a/GitPython/GitPython/.venv/lib/python3.9/site-packages/coverage/core.py:96: CoverageWarning: Couldn't import C tracer: No module named 'coverage.tracer' (no-ctracer) warn(f"Couldn't import C tracer: {IMPORT_ERROR}", slug="no-ctracer", once=True) On most platforms, there is no failure, because the condition the warnings describe does not occur, so there are no warnings. But on Cygwin it does occur, resulting in a new test failure, showing > self.assertEqual(len(actual_lines[2]), expected_line_count, repr(actual_lines[2])) E AssertionError: 5004 != 5002 : ["/cygdrive/d/a/GitPython/GitPython/.venv/lib/python3.9/site-packages/coverage/core.py:96: CoverageWarning: Couldn't import C tracer: No module named 'coverage.tracer' (no-ctracer)\n", ' warn(f"Couldn\'t import C tracer: {IMPORT_ERROR}", slug="no-ctracer", once=True)\n', 'From github.com:jantman/gitpython_issue_301\n', ' = [up to date] master -> origin/master\n', ' = [up to date] testcommit1 -> origin/testcommit1\n', ' = [up to date] testcommit10 -> origin/testcommit10\n', ... where the first two elements of the list are from the lines of the warning message, and the others are as expected. (The above is a highly abridged extract, with the `...` at the end standing for many more list items obtained through the `cat_file.py` fixture.) This new failure is triggered specifically by the new `coverage` package version. It is not due to any recent changes in GitPython. It can be observed by rerunning CI checks that have previously passed, or in: https://github.com/EliahKagan/GitPython/actions/runs/15598239952/job/43940156308#step:14:355 There is more than one possible way to fix this, including fixing the underlying condition being warned about on Cygwin, or sanitizing environment variables for the subprocess. The approach taken here instead is based on the idea that the `cat_file.py` fixture is very simple, and that it is conceptually just a standalone Python script that doesn't do anything meant to depend on the current Python environment. Accordingly, this passes the `-S` option to the interpreter for the `cat_file.py` subprocess, so that interpreter refrains from loading the `site` module. This includes, among other simplifying effects, that the subprocess performs no `.pth` customizations. --- test/test_git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_git.py b/test/test_git.py index 5bcf89bdd..4a54d0d9b 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -773,6 +773,7 @@ def stderr_handler(line): cmdline = [ sys.executable, + "-S", # Keep any `CoverageWarning` messages out of the subprocess stderr. fixture_path("cat_file.py"), str(fixture_path("issue-301_stderr")), ] From 66955cc76a9b33716baa6bdcc575d0446cf9175e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 14 Jun 2025 14:42:31 -0400 Subject: [PATCH 03/45] Minor CI clarifications - In the Cygwin CI workflow, move `runs-on` below `strategy`, for greater consistency with other workflows. - In the Cygwin CI jobs, use `pwsh` rather than `bash` for the `git config` command run outside of Cygwin, since `pwsh` is the default shell for such commands, it's the shell the Cygwin setup action uses, and it avoids creating the wrong impression that `bash` is needed. - Use "virtual environment" instead of "virtualenv" in some step names to avoid possible confusion with the `virtualenv` pacakge. - Remove comments in the PyPA package upgrade steps, which are more self-documenting since 727f4e9 (#2043). --- .github/workflows/alpine-test.yml | 3 +-- .github/workflows/cygwin-test.yml | 9 ++++----- .github/workflows/pythonpackage.yml | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml index a3361798d..ceba11fb8 100644 --- a/.github/workflows/alpine-test.yml +++ b/.github/workflows/alpine-test.yml @@ -47,13 +47,12 @@ jobs: # and cause subsequent tests to fail cat test/fixtures/.gitconfig >> ~/.gitconfig - - name: Set up virtualenv + - name: Set up virtual environment run: | python -m venv .venv - name: Update PyPA packages run: | - # Get the latest pip, wheel, and prior to Python 3.12, setuptools. . .venv/bin/activate python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 6943db09c..28a41a362 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -7,8 +7,6 @@ permissions: jobs: test: - runs-on: windows-latest - strategy: matrix: selection: [fast, perf] @@ -20,6 +18,8 @@ jobs: fail-fast: false + runs-on: windows-latest + env: CHERE_INVOKING: "1" CYGWIN_NOWINPATH: "1" @@ -32,7 +32,7 @@ jobs: - name: Force LF line endings run: | git config --global core.autocrlf false # Affects the non-Cygwin git. - shell: bash # Use Git Bash instead of Cygwin Bash for this step. + shell: pwsh # Do this outside Cygwin, to affect actions/checkout. - uses: actions/checkout@v4 with: @@ -67,7 +67,7 @@ jobs: # and cause subsequent tests to fail cat test/fixtures/.gitconfig >> ~/.gitconfig - - name: Set up virtualenv + - name: Set up virtual environment run: | python3.9 -m venv --without-pip .venv echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV" @@ -78,7 +78,6 @@ jobs: - name: Update PyPA packages run: | - # Get the latest pip, wheel, and prior to Python 3.12, setuptools. python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel - name: Install project and test dependencies diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c56d45df7..f3d977760 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -71,7 +71,6 @@ jobs: - name: Update PyPA packages run: | - # Get the latest pip, wheel, and prior to Python 3.12, setuptools. python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel - name: Install project and test dependencies From 8e24edfb4688180287c970b023516c2b71946778 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sat, 14 Jun 2025 14:57:40 -0400 Subject: [PATCH 04/45] Use *-wheel packages as a better fix for #2004 This installs the `python-pip-wheel`, `python-setuptools-wheel`, and `python-wheel-wheel` packages on Cygwini CI, which provide `.whl` files for `pip`, `setuptools`, and `wheel`. By making those wheels available, this fixes #2004 better than the previous workaround, allowing `ensurepip` to run without the error: Traceback (most recent call last): File "/usr/lib/python3.9/runpy.py", line 188, in _run_module_as_main mod_name, mod_spec, code = _get_module_details(mod_name, _Error) File "/usr/lib/python3.9/runpy.py", line 147, in _get_module_details return _get_module_details(pkg_main_name, error) File "/usr/lib/python3.9/runpy.py", line 111, in _get_module_details __import__(pkg_name) File "/usr/lib/python3.9/ensurepip/__init__.py", line [30](https://github.com/EliahKagan/GitPython/actions/runs/13454947366/job/37596811693#step:10:31), in _SETUPTOOLS_VERSION = _get_most_recent_wheel_version("setuptools") File "/usr/lib/python3.9/ensurepip/__init__.py", line 27, in _get_most_recent_wheel_version return str(max(_wheels[pkg], key=distutils.version.LooseVersion)) ValueError: max() arg is an empty sequence This change takes the place of the main changes in #2007 and #2009. In particular, it should allow `test_installation` to pass again. This also delists non-wheel Cygwin packages such as `python39-pip`, which are not needed (or at least no longer needed). (The python-{pip,setuptools,wheel}-wheel packages are, as their names suggest, intentionally not specific to Python 3.9. However, this technique will not necessarily carry over to Python 3.12, depending on what versions are supplied and other factors. This may be relevant when another attempt like #1988 is made to test/support Python 3.12 on Cygwin. At least for now, though, this seems worthwhile for fixing the Cygwin 3.9 environment, making it more similar to working local Cygwin environments and letting the workflow be more usable as guidance to how to set up a local Cygwin environment for GitPython development, and letting the installation test pass automatically.) --- .github/workflows/cygwin-test.yml | 8 ++------ test/test_installation.py | 8 -------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 28a41a362..2d0378490 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -41,7 +41,7 @@ jobs: - name: Install Cygwin uses: cygwin/cygwin-install-action@v5 with: - packages: python39 python39-pip python39-virtualenv git wget + packages: git python39 python-pip-wheel python-setuptools-wheel python-wheel-wheel add-to-path: false # No need to change $PATH outside the Cygwin environment. - name: Arrange for verbose output @@ -69,13 +69,9 @@ jobs: - name: Set up virtual environment run: | - python3.9 -m venv --without-pip .venv + python3.9 -m venv .venv echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV" - - name: Bootstrap pip in virtualenv - run: | - wget -qO- https://bootstrap.pypa.io/get-pip.py | python - - name: Update PyPA packages run: | python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel diff --git a/test/test_installation.py b/test/test_installation.py index a35826bd0..ae6472e98 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -4,19 +4,11 @@ import ast import os import subprocess -import sys - -import pytest from test.lib import TestBase, VirtualEnvironment, with_rw_directory class TestInstallation(TestBase): - @pytest.mark.xfail( - sys.platform == "cygwin" and "CI" in os.environ, - reason="Trouble with pip on Cygwin CI, see issue #2004", - raises=subprocess.CalledProcessError, - ) @with_rw_directory def test_installation(self, rw_dir): venv = self._set_up_venv(rw_dir) From 953d1616e7385ec6aac1e7a6212c689874c9409c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 19:04:15 -0400 Subject: [PATCH 05/45] Comment what `TestInstallation._set_up_venv` does So that the meaning of the `venv.sources` accesses in `test_installation` is more readily clear. --- test/test_installation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_installation.py b/test/test_installation.py index ae6472e98..8231b3512 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -64,10 +64,14 @@ def test_installation(self, rw_dir): @staticmethod def _set_up_venv(rw_dir): + # Initialize the virtual environment. venv = VirtualEnvironment(rw_dir, with_pip=True) + + # Make its src directory a symlink to our own top-level source tree. os.symlink( os.path.dirname(os.path.dirname(__file__)), venv.sources, target_is_directory=True, ) + return venv From 84632c78512e070a7aaa9ff1dd046c77293a58a4 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 19:06:58 -0400 Subject: [PATCH 06/45] Extract `subprocess.run` logic repeated in `test_installation` This creates a function (technically, a callable `partial` object) for `test_installation` to use instead of repeating `subproces.run` keyword arguments all the time. This relates directly to steps in `_set_up_venv`, and it's makes about as much sense to do it there as in `test_installation`, so it is placed (and described) in `_set_up_venv`. --- test/test_installation.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/test/test_installation.py b/test/test_installation.py index 8231b3512..b428f413a 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -2,6 +2,7 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ import ast +import functools import os import subprocess @@ -11,35 +12,23 @@ class TestInstallation(TestBase): @with_rw_directory def test_installation(self, rw_dir): - venv = self._set_up_venv(rw_dir) + venv, run = self._set_up_venv(rw_dir) - result = subprocess.run( - [venv.pip, "install", "."], - stdout=subprocess.PIPE, - cwd=venv.sources, - ) + result = run([venv.pip, "install", "."]) self.assertEqual( 0, result.returncode, msg=result.stderr or result.stdout or "Can't install project", ) - result = subprocess.run( - [venv.python, "-c", "import git"], - stdout=subprocess.PIPE, - cwd=venv.sources, - ) + result = run([venv.python, "-c", "import git"]) self.assertEqual( 0, result.returncode, msg=result.stderr or result.stdout or "Self-test failed", ) - result = subprocess.run( - [venv.python, "-c", "import gitdb; import smmap"], - stdout=subprocess.PIPE, - cwd=venv.sources, - ) + result = run([venv.python, "-c", "import gitdb; import smmap"]) self.assertEqual( 0, result.returncode, @@ -49,11 +38,7 @@ def test_installation(self, rw_dir): # Even IF gitdb or any other dependency is supplied during development by # inserting its location into PYTHONPATH or otherwise patched into sys.path, # make sure it is not wrongly inserted as the *first* entry. - result = subprocess.run( - [venv.python, "-c", "import sys; import git; print(sys.path)"], - stdout=subprocess.PIPE, - cwd=venv.sources, - ) + result = run([venv.python, "-c", "import sys; import git; print(sys.path)"]) syspath = result.stdout.decode("utf-8").splitlines()[0] syspath = ast.literal_eval(syspath) self.assertEqual( @@ -74,4 +59,11 @@ def _set_up_venv(rw_dir): target_is_directory=True, ) - return venv + # Create a convenience function to run commands in it. + run = functools.partial( + subprocess.run, + stdout=subprocess.PIPE, + cwd=venv.sources, + ) + + return venv, run From a2ba4804a6544642b400dced6eac0f4f1ad28750 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 19:11:16 -0400 Subject: [PATCH 07/45] Have `test_installation` test that operations produce no warnings By setting the `PYTHONWARNINGS` environment variable to `error` in each of the subprocess invocations. This is strictly stronger than passing `-Werror` for the `python` commands, because it automatically applies to subprocesses (unless they are created with a sanitized environment or otherwise with one in which `PYTHONWARNINGS` has been customized), and because it works for `pip` automatically. Importantly, this will cause warnings internal to Python subprocesses created by `pip` to be treated as errors. It should thus surface any warnings coming from the `setuptools` backend. --- test/test_installation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_installation.py b/test/test_installation.py index b428f413a..5ba8a82b0 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -64,6 +64,7 @@ def _set_up_venv(rw_dir): subprocess.run, stdout=subprocess.PIPE, cwd=venv.sources, + env={**os.environ, "PYTHONWARNINGS": "error"}, ) return venv, run From a0e08fe90135149de203a3adfb21a5a254cb1bdb Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 19:48:39 -0400 Subject: [PATCH 08/45] Show more information when `test_installation` fails Previously, it attempted to show stderr unless empty, first falling back to stdout unless empty, then falling back to the prewritten summary identifying the specific assertion. This now has the `test_installation` assertions capture stderr as well as stdout, handle standard streams as text rather than binary, and show more information when failing, always distinguishing where the information came from: the summary, then labeled captured stdout (empty or not), then labeled captured stderr (empty or not). That applies to all but the last assertion, which does not try to show information differently when it fails, but is simplified to do the right thing now that `subprocess.run` is using text streams. (This subtly changes its semantics, but overall it should be as effective as before at finding the `sys.path` woe it anticipates.) --- test/test_installation.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/test_installation.py b/test/test_installation.py index 5ba8a82b0..39797c134 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -15,31 +15,19 @@ def test_installation(self, rw_dir): venv, run = self._set_up_venv(rw_dir) result = run([venv.pip, "install", "."]) - self.assertEqual( - 0, - result.returncode, - msg=result.stderr or result.stdout or "Can't install project", - ) + self._check_result(result, "Can't install project") result = run([venv.python, "-c", "import git"]) - self.assertEqual( - 0, - result.returncode, - msg=result.stderr or result.stdout or "Self-test failed", - ) + self._check_result(result, "Self-test failed") result = run([venv.python, "-c", "import gitdb; import smmap"]) - self.assertEqual( - 0, - result.returncode, - msg=result.stderr or result.stdout or "Dependencies not installed", - ) + self._check_result(result, "Dependencies not installed") # Even IF gitdb or any other dependency is supplied during development by # inserting its location into PYTHONPATH or otherwise patched into sys.path, # make sure it is not wrongly inserted as the *first* entry. result = run([venv.python, "-c", "import sys; import git; print(sys.path)"]) - syspath = result.stdout.decode("utf-8").splitlines()[0] + syspath = result.stdout.splitlines()[0] syspath = ast.literal_eval(syspath) self.assertEqual( "", @@ -63,8 +51,17 @@ def _set_up_venv(rw_dir): run = functools.partial( subprocess.run, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, cwd=venv.sources, env={**os.environ, "PYTHONWARNINGS": "error"}, ) return venv, run + + def _check_result(self, result, failure_summary): + self.assertEqual( + 0, + result.returncode, + f"{failure_summary}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) From 6826b594e2ef43b0972f29f335e7be51c9574303 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 20:05:56 -0400 Subject: [PATCH 09/45] Improve failure message whitespace clarity --- test/test_installation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_installation.py b/test/test_installation.py index 39797c134..da0b86ed2 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -63,5 +63,11 @@ def _check_result(self, result, failure_summary): self.assertEqual( 0, result.returncode, - f"{failure_summary}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}", + self._prepare_failure_message(result, failure_summary), ) + + @staticmethod + def _prepare_failure_message(result, failure_summary): + stdout = result.stdout.rstrip() + stderr = result.stderr.rstrip() + return f"{failure_summary}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}" From d0868bd5d6a43c3d95a3eaddab1389a0ef76d8f0 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 20:25:55 -0400 Subject: [PATCH 10/45] Remove deprecated license classifier GitPython project metadata specify the project's license primarily through the value of `license`, currently passed as an argument to `setuptools.setup`, which holds the string `"BSD-3-Clause"`. This is an SPDX license identifier readily understood by both humans and machines. The PyPI trove classifier "License :: OSI Approved :: BSD License" has also been specified in `setup.py`. However, this is not ideal, because: 1. It does not identify a specific license. There are multiple "BSD" licenses in use, with BSD-2-Clause and BSD-3-Clause both in very wide use. 2. It is no longer recommended to use a trove classifier to indicate a license. The use of license classifiers (even unambiguous ones) has been deprecated. See: - https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use - https://peps.python.org/pep-0639/#deprecate-license-classifiers This commit removes the classifier. The license itself is of course unchanged, as is the `license` value of `"BSD-3-Clause"`. (An expected effect of this change is that, starting in the next release of GitPython, PyPI may show "License: BSD-3-Clause" instead of the current text "License: BSD License (BSD-3-Clause)".) This change fixes a warning issued by a subprocess of `pip` when installing the package. The warning, until this change, could be observed by running `pip install . -v` or `pip install -e . -v` and examining the verbose output, or by running `pip install .` or `pip install -e .` with the `PYTHONWARNINGS` environment variable set to `error`: SetuptoolsDeprecationWarning: License classifiers are deprecated. !! ******************************************************************************** Please consider removing the following classifiers in favor of a SPDX license expression: License :: OSI Approved :: BSD License See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details. ******************************************************************************** !! (In preceding commits, `test_installation` has been augmented to set that environment variable, surfacing the error. This change should allow that test to pass, unless it finds other problems.) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index f28fedb85..a7b1eab00 100755 --- a/setup.py +++ b/setup.py @@ -95,7 +95,6 @@ def _stamp_version(filename: str) -> None: # "Development Status :: 7 - Inactive", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", From b21c32a302dcd91d52636e5f91e89ae129654bd2 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 15 Jun 2025 23:37:27 -0400 Subject: [PATCH 11/45] Pass assertion `msg` as a keyword argument in `test_installation` It was originally made explicit like this, but it ended up becoming position in my last few commits. This restores that clearer aspect of how it was written before, while keeping all the other changes. --- test/test_installation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_installation.py b/test/test_installation.py index da0b86ed2..7c82bd403 100644 --- a/test/test_installation.py +++ b/test/test_installation.py @@ -63,7 +63,7 @@ def _check_result(self, result, failure_summary): self.assertEqual( 0, result.returncode, - self._prepare_failure_message(result, failure_summary), + msg=self._prepare_failure_message(result, failure_summary), ) @staticmethod From ac3437df0c5b5ad597915db15276dcb326d3d970 Mon Sep 17 00:00:00 2001 From: Tom Bedor Date: Tue, 17 Jun 2025 09:03:52 -0700 Subject: [PATCH 12/45] Add clearer error version for unsupported index error --- 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 59cce6ae6..d03ec6759 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -207,7 +207,7 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: version, num_entries = unpacked # TODO: Handle version 3: extended data, see read-cache.c. - assert version in (1, 2) + assert version in (1, 2), "Unsupported git index version %i, only 1 and 2 are supported" % version return version, num_entries From bb5c22629aba914a00b8f33a10522324ca7bb049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:20:34 +0000 Subject: [PATCH 13/45] Bump Vampire/setup-wsl from 5.0.1 to 6.0.0 Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/vampire/setup-wsl/releases) - [Commits](https://github.com/vampire/setup-wsl/compare/v5.0.1...v6.0.0) --- updated-dependencies: - dependency-name: Vampire/setup-wsl dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .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 f3d977760..4457a341f 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -51,7 +51,7 @@ jobs: - name: Set up WSL (Windows) if: matrix.os-type == 'windows' - uses: Vampire/setup-wsl@v5.0.1 + uses: Vampire/setup-wsl@v6.0.0 with: wsl-version: 1 distribution: Alpine From 496392b9bf781904421cbd171c0c5395a6fe330c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:21:27 +0000 Subject: [PATCH 14/45] Bump cygwin/cygwin-install-action from 5 to 6 Bumps [cygwin/cygwin-install-action](https://github.com/cygwin/cygwin-install-action) from 5 to 6. - [Release notes](https://github.com/cygwin/cygwin-install-action/releases) - [Commits](https://github.com/cygwin/cygwin-install-action/compare/v5...v6) --- updated-dependencies: - dependency-name: cygwin/cygwin-install-action dependency-version: '6' 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 2d0378490..cc9e1edf0 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v5 + uses: cygwin/cygwin-install-action@v6 with: packages: git python39 python-pip-wheel python-setuptools-wheel python-wheel-wheel add-to-path: false # No need to change $PATH outside the Cygwin environment. From a4aadb0c04bd13af824c14dcc39f88345aa5c440 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Sun, 20 Jul 2025 14:17:49 +0200 Subject: [PATCH 15/45] Fix name collision --- git/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/config.py b/git/config.py index 5fc099a27..345290a39 100644 --- a/git/config.py +++ b/git/config.py @@ -87,15 +87,15 @@ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParse mutating_methods = clsdict[kmm] for base in bases: methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) - for name, method in methods: - if name in clsdict: + for method_name, method in methods: + if method_name in clsdict: continue method_with_values = needs_values(method) - if name in mutating_methods: + if method_name in mutating_methods: method_with_values = set_dirty_and_flush_changes(method_with_values) # END mutating methods handling - clsdict[name] = method_with_values + clsdict[method_name] = method_with_values # END for each name/method pair # END for each base # END if mutating methods configuration is set From 80fd2c16211738156e65258381a17cdc429ddd08 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Sun, 20 Jul 2025 14:17:58 +0200 Subject: [PATCH 16/45] Don't treat sphinx warnings as errors Workaround for python/cpython#100520 (rst syntax error in configparser docstrings), which was fixed in CPython 3.10+. Docutils raises warnings about the invalid docstrings, and `-W` instructs sphinx to treat this as errors. We can't control or silence these warnings, so we accept them and don't treat them as errors. See the discussion in gitpython-developers/GitPython#2060 for details. --- doc/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Makefile b/doc/Makefile index ddeadbd7e..7e0d325fe 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. BUILDDIR = build -SPHINXOPTS = -W +SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = From ec2e2c8b894512e7a2364774d77cdd9db73f0566 Mon Sep 17 00:00:00 2001 From: Tom Webber Date: Tue, 22 Jul 2025 12:23:18 +0200 Subject: [PATCH 17/45] Allow relative path url in submodules for submodule_update --- git/objects/submodule/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 0e55b8fa9..5031a2e71 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -11,6 +11,7 @@ import stat import sys import uuid +import urllib import git from git.cmd import Git @@ -799,9 +800,13 @@ def update( + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name), ) if not dry_run: + if self.url.startswith("."): + url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url) + else: + url = self.url mrepo = self._clone_repo( self.repo, - self.url, + url, self.path, self.name, n=True, From 1ee1e781929074afd66bff1eae007bbee41d117e Mon Sep 17 00:00:00 2001 From: Tom Webber Date: Wed, 23 Jul 2025 07:12:27 +0200 Subject: [PATCH 18/45] Add test case for cloning submodules with relative path --- test/test_submodule.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/test_submodule.py b/test/test_submodule.py index f44f086c2..4a248eb60 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -1350,3 +1350,23 @@ def test_submodule_update_unsafe_options_allowed(self, rw_repo): for unsafe_option in unsafe_options: with self.assertRaises(GitCommandError): submodule.update(clone_multi_options=[unsafe_option], allow_unsafe_options=True) + + @with_rw_directory + @_patch_git_config("protocol.file.allow", "always") + def test_submodule_update_relative_url(self, rwdir): + parent_path = osp.join(rwdir, "parent") + parent_repo = git.Repo.init(parent_path) + submodule_path = osp.join(rwdir, "module") + submodule_repo = git.Repo.init(submodule_path) + submodule_repo.git.commit(m="initial commit", allow_empty=True) + + parent_repo.git.submodule("add", "../module", "module") + parent_repo.index.commit("add submodule with relative URL") + + cloned_path = osp.join(rwdir, "cloned_repo") + cloned_repo = git.Repo.clone_from(parent_path, cloned_path) + + cloned_repo.submodule_update(init=True, recursive=True) + + has_module = any(sm.name == "module" for sm in cloned_repo.submodules) + assert has_module, "Relative submodule was not updated properly" From 6ba2c0a2f9ee7feffd7e079621c4845820180c9a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 24 Jul 2025 05:37:18 +0200 Subject: [PATCH 19/45] Prepare a new release --- VERSION | 2 +- doc/source/changes.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e6af1c454..3c91929a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.44 +3.1.45 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 00a3c660e..151059ed2 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ========= +3.1.45 +====== + +See the following for all changes. +https://github.com/gitpython-developers/GitPython/releases/tag/3.1.45 + 3.1.44 ====== From de6d177aaa8ccd6d82576ab0b3de5074cf7647fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:05:41 +0000 Subject: [PATCH 20/45] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [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/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/alpine-test.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml index ceba11fb8..a9c29117e 100644 --- a/.github/workflows/alpine-test.yml +++ b/.github/workflows/alpine-test.yml @@ -26,7 +26,7 @@ jobs: adduser runner docker shell: sh -exo pipefail {0} # Run this as root, not the "runner" user. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2bee952af..9191471c3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,7 +47,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index cc9e1edf0..5c42c8583 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -34,7 +34,7 @@ jobs: git config --global core.autocrlf false # Affects the non-Cygwin git. shell: pwsh # Do this outside Cygwin, to affect actions/checkout. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ceba0dd85..16978f9a8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4457a341f..4e5d82a55 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -39,7 +39,7 @@ jobs: shell: bash --noprofile --norc -exo pipefail {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 From fe81519436a8ab8b735a40a3973c8c5bd9cfec47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:42:38 +0000 Subject: [PATCH 21/45] Bump git/ext/gitdb from `335c0f6` to `39d7dbf` Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `335c0f6` to `39d7dbf`. - [Release notes](https://github.com/gitpython-developers/gitdb/releases) - [Commits](https://github.com/gitpython-developers/gitdb/compare/335c0f66173eecdc7b2597c2b6c3d1fde795df30...39d7dbf285df058e44ea501c23ea8d31ae8bce0e) --- updated-dependencies: - dependency-name: git/ext/gitdb dependency-version: 39d7dbf285df058e44ea501c23ea8d31ae8bce0e dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- git/ext/gitdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/ext/gitdb b/git/ext/gitdb index 335c0f661..39d7dbf28 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 335c0f66173eecdc7b2597c2b6c3d1fde795df30 +Subproject commit 39d7dbf285df058e44ea501c23ea8d31ae8bce0e From ca51dad69071898af377c8e62210c69e8d211c69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:41:33 +0000 Subject: [PATCH 22/45] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16978f9a8..ed535a914 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4e5d82a55..7088310e5 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -44,7 +44,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: ${{ matrix.experimental }} From 9f913ec0cb0c6f7ab3eea7245657d01048fd7065 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:00:47 +0000 Subject: [PATCH 23/45] Bump git/ext/gitdb from `39d7dbf` to `f8fdfec` Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `39d7dbf` to `f8fdfec`. - [Release notes](https://github.com/gitpython-developers/gitdb/releases) - [Commits](https://github.com/gitpython-developers/gitdb/compare/39d7dbf285df058e44ea501c23ea8d31ae8bce0e...f8fdfec0fd0a0aed9171c6cf2c5cb8d73e2bb305) --- updated-dependencies: - dependency-name: git/ext/gitdb dependency-version: f8fdfec0fd0a0aed9171c6cf2c5cb8d73e2bb305 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- git/ext/gitdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/ext/gitdb b/git/ext/gitdb index 39d7dbf28..f8fdfec0f 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit 39d7dbf285df058e44ea501c23ea8d31ae8bce0e +Subproject commit f8fdfec0fd0a0aed9171c6cf2c5cb8d73e2bb305 From 7c55a2b839e05f10a9dc3cf2bc53785350372c88 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Tue, 30 Sep 2025 17:47:14 +0300 Subject: [PATCH 24/45] Fix type hint for `SymbolicReference.reference` property Signed-off-by: Emmanuel Ferdman --- git/refs/symbolic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 1b90a3115..74bb1fe0a 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -40,6 +40,7 @@ from git.config import GitConfigParser from git.objects.commit import Actor from git.refs.log import RefLogEntry + from git.refs.reference import Reference from git.repo import Repo @@ -404,7 +405,7 @@ def object(self) -> AnyGitObject: def object(self, object: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference": return self.set_object(object) - def _get_reference(self) -> "SymbolicReference": + def _get_reference(self) -> "Reference": """ :return: :class:`~git.refs.reference.Reference` object we point to @@ -416,7 +417,7 @@ def _get_reference(self) -> "SymbolicReference": sha, target_ref_path = self._get_ref_info(self.repo, self.path) if target_ref_path is None: raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) - return self.from_path(self.repo, target_ref_path) + return cast("Reference", self.from_path(self.repo, target_ref_path)) def set_reference( self, @@ -502,7 +503,7 @@ def set_reference( # Aliased reference @property - def reference(self) -> "SymbolicReference": + def reference(self) -> "Reference": return self._get_reference() @reference.setter From bcdcccdc7ea7d50ec5831aad961ba80df0f1379b Mon Sep 17 00:00:00 2001 From: Brunno Vanelli Date: Tue, 7 Oct 2025 20:49:46 +0200 Subject: [PATCH 25/45] feat: Add support for hasconfig git rule. --- git/config.py | 8 ++++++-- test/test_config.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/git/config.py b/git/config.py index 345290a39..ffe1c8ccd 100644 --- a/git/config.py +++ b/git/config.py @@ -66,7 +66,7 @@ CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") """The configuration level of a configuration file.""" -CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") +CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"") """Section pattern to detect conditional includes. See: https://git-scm.com/docs/git-config#_conditional_includes @@ -590,7 +590,11 @@ def _included_paths(self) -> List[Tuple[str, str]]: if fnmatch.fnmatchcase(branch_name, value): paths += self.items(section) - + elif keyword == "hasconfig:remote.*.url": + for remote in self._repo.remotes: + if fnmatch.fnmatch(remote.url, value): + paths += self.items(section) + break return paths def read(self) -> None: # type: ignore[override] diff --git a/test/test_config.py b/test/test_config.py index 8e1007d9e..56ac0f304 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -373,6 +373,41 @@ def test_conditional_includes_from_branch_name_error(self, rw_dir): assert not config._has_includes() assert config._included_paths() == [] + @with_rw_directory + def test_conditional_includes_remote_url(self, rw_dir): + # Initiate mocked repository. + repo = mock.Mock() + repo.remotes = [mock.Mock(url="https://github.com/foo/repo")] + + # Initiate config files. + path1 = osp.join(rw_dir, "config1") + path2 = osp.join(rw_dir, "config2") + template = '[includeIf "hasconfig:remote.*.url:{}"]\n path={}\n' + + # Ensure that config with hasconfig and full url is correct. + with open(path1, "w") as stream: + stream.write(template.format("https://github.com/foo/repo", path2)) + + with GitConfigParser(path1, repo=repo) as config: + assert config._has_includes() + assert config._included_paths() == [("path", path2)] + + # Ensure that config with hasconfig and incorrect url is incorrect. + with open(path1, "w") as stream: + stream.write(template.format("incorrect", path2)) + + with GitConfigParser(path1, repo=repo) as config: + assert not config._has_includes() + assert config._included_paths() == [] + + # Ensure that config with hasconfig and url using glob pattern is correct. + with open(path1, "w") as stream: + stream.write(template.format("**/**github.com*/**", path2)) + + with GitConfigParser(path1, repo=repo) as config: + assert config._has_includes() + assert config._included_paths() == [("path", path2)] + def test_rename(self): file_obj = self._to_memcache(fixture_path("git_config")) with GitConfigParser(file_obj, read_only=False, merge_includes=False) as cw: From 6cf863374820a1bcf1fa14b3c2ea87214752bf74 Mon Sep 17 00:00:00 2001 From: Brunno Vanelli Date: Tue, 7 Oct 2025 21:19:31 +0200 Subject: [PATCH 26/45] fix: Use fnmatch instead. --- git/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/config.py b/git/config.py index ffe1c8ccd..200c81bb7 100644 --- a/git/config.py +++ b/git/config.py @@ -592,7 +592,7 @@ def _included_paths(self) -> List[Tuple[str, str]]: paths += self.items(section) elif keyword == "hasconfig:remote.*.url": for remote in self._repo.remotes: - if fnmatch.fnmatch(remote.url, value): + if fnmatch.fnmatchcase(remote.url, value): paths += self.items(section) break return paths From a6247a585600c09894a9fae85e11f7581bfccbe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:32:58 +0000 Subject: [PATCH 27/45] Bump github/codeql-action from 3 to 4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9191471c3..32d5e84e4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -85,6 +85,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" From 9dd0081213d57f41919ed37e93656410277bfb0b Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 21 Oct 2025 11:11:16 +0200 Subject: [PATCH 28/45] Use actual return type in annotation for method submodule_update Fixes #2077 --- git/repo/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 7e918df8c..6ea96aad2 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -520,7 +520,7 @@ def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: """ return RootModule(self).traverse(*args, **kwargs) - def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: + def submodule_update(self, *args: Any, **kwargs: Any) -> RootModule: """Update the submodules, keeping the repository consistent as it will take the previous state into consideration. From 74ff8e5e1cb814fbf3b916111d7181bd6e3f3906 Mon Sep 17 00:00:00 2001 From: Yikai Zhao Date: Sun, 2 Nov 2025 10:25:33 +0800 Subject: [PATCH 29/45] Support index format v3 --- git/index/fun.py | 16 +++++++++++----- git/index/typ.py | 15 ++++++++++++++- test/test_index.py | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index d03ec6759..0b3d79cf1 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -36,7 +36,7 @@ ) from git.util import IndexFileSHA1Writer, finalize_process -from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT +from .typ import CE_EXTENDED, BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT from .util import pack, unpack # typing ----------------------------------------------------------------------------- @@ -158,7 +158,7 @@ def write_cache( write = stream_sha.write # Header - version = 2 + version = 3 if any(entry.extended_flags for entry in entries) else 2 write(b"DIRC") write(pack(">LL", version, len(entries))) @@ -172,6 +172,8 @@ def write_cache( plen = len(path) & CE_NAMEMASK # Path length assert plen == len(path), "Path %s too long to fit into index" % entry.path flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values. + if entry.extended_flags: + flags |= CE_EXTENDED write( pack( ">LLLLLL20sH", @@ -185,6 +187,8 @@ def write_cache( flags, ) ) + if entry.extended_flags: + write(pack(">H", entry.extended_flags)) write(path) real_size = (tell() - beginoffset + 8) & ~7 write(b"\0" * ((beginoffset + real_size) - tell())) @@ -206,8 +210,7 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2))) version, num_entries = unpacked - # TODO: Handle version 3: extended data, see read-cache.c. - assert version in (1, 2), "Unsupported git index version %i, only 1 and 2 are supported" % version + assert version in (1, 2, 3), "Unsupported git index version %i, only 1, 2, and 3 are supported" % version return version, num_entries @@ -260,12 +263,15 @@ def read_cache( ctime = unpack(">8s", read(8))[0] mtime = unpack(">8s", read(8))[0] (dev, ino, mode, uid, gid, size, sha, flags) = unpack(">LLLLLL20sH", read(20 + 4 * 6 + 2)) + extended_flags = 0 + if flags & CE_EXTENDED: + extended_flags = unpack(">H", read(2))[0] path_size = flags & CE_NAMEMASK path = read(path_size).decode(defenc) real_size = (tell() - beginoffset + 8) & ~7 read((beginoffset + real_size) - tell()) - entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size)) + entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size, extended_flags)) # entry_key would be the method to use, but we save the effort. entries[(path, entry.stage)] = entry count += 1 diff --git a/git/index/typ.py b/git/index/typ.py index 974252528..4bcb604ab 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -32,6 +32,9 @@ CE_VALID = 0x8000 CE_STAGESHIFT = 12 +CE_EXT_SKIP_WORKTREE = 0x4000 +CE_EXT_INTENT_TO_ADD = 0x2000 + # } END invariants @@ -87,6 +90,8 @@ class BaseIndexEntryHelper(NamedTuple): uid: int = 0 gid: int = 0 size: int = 0 + # version 3 extended flags, only when (flags & CE_EXTENDED) is set + extended_flags: int = 0 class BaseIndexEntry(BaseIndexEntryHelper): @@ -102,7 +107,7 @@ def __new__( cls, inp_tuple: Union[ Tuple[int, bytes, int, PathLike], - Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int], + Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int, int], ], ) -> "BaseIndexEntry": """Override ``__new__`` to allow construction from a tuple for backwards @@ -134,6 +139,14 @@ def stage(self) -> int: """ return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT + @property + def skip_worktree(self) -> bool: + return (self.extended_flags & CE_EXT_SKIP_WORKTREE) > 0 + + @property + def intent_to_add(self) -> bool: + return (self.extended_flags & CE_EXT_INTENT_TO_ADD) > 0 + @classmethod def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry": """:return: Fully equipped BaseIndexEntry at the given stage""" diff --git a/test/test_index.py b/test/test_index.py index cf3b90fa6..6d90d7965 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -1218,6 +1218,31 @@ def test_index_add_non_normalized_path(self, rw_repo): rw_repo.index.add(non_normalized_path) + @with_rw_directory + def test_index_version_v3(self, tmp_dir): + tmp_dir = Path(tmp_dir) + with cwd(tmp_dir): + subprocess.run(["git", "init", "-q"], check=True) + file = tmp_dir / "file.txt" + file.write_text("hello") + subprocess.run(["git", "add", "-N", "file.txt"], check=True) + + repo = Repo(tmp_dir) + + assert len(repo.index.entries) == 1 + entry = list(repo.index.entries.values())[0] + assert entry.path == "file.txt" + assert entry.intent_to_add + + file2 = tmp_dir / "file2.txt" + file2.write_text("world") + repo.index.add(["file2.txt"]) + repo.index.write() + + status_str = subprocess.check_output(["git", "status", "--porcelain"], text=True) + assert " A file.txt\n" in status_str + assert "A file2.txt\n" in status_str + class TestIndexUtils: @pytest.mark.parametrize("file_path_type", [str, Path]) From 3150ebdaa43df5be2c27e717807381724131b128 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:08:06 +0000 Subject: [PATCH 30/45] Bump git/ext/gitdb from `f8fdfec` to `65321a2` Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `f8fdfec` to `65321a2`. - [Release notes](https://github.com/gitpython-developers/gitdb/releases) - [Commits](https://github.com/gitpython-developers/gitdb/compare/f8fdfec0fd0a0aed9171c6cf2c5cb8d73e2bb305...65321a28b586df60b9d1508228e2f53a35f938eb) --- updated-dependencies: - dependency-name: git/ext/gitdb dependency-version: 65321a28b586df60b9d1508228e2f53a35f938eb dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- git/ext/gitdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/ext/gitdb b/git/ext/gitdb index f8fdfec0f..65321a28b 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit f8fdfec0fd0a0aed9171c6cf2c5cb8d73e2bb305 +Subproject commit 65321a28b586df60b9d1508228e2f53a35f938eb From 8a884fea3ff91f1444a36785cc22c8d7fc6bf329 Mon Sep 17 00:00:00 2001 From: Yikai Zhao Date: Sat, 8 Nov 2025 14:01:41 +0800 Subject: [PATCH 31/45] improve unit test --- test/fixtures/index_extended_flags | Bin 0 -> 436 bytes test/test_index.py | 37 +++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/index_extended_flags diff --git a/test/fixtures/index_extended_flags b/test/fixtures/index_extended_flags new file mode 100644 index 0000000000000000000000000000000000000000..f03713b684711d4a5aedab0bafd6b254137c9e5d GIT binary patch literal 436 zcmZ?q402{*U|l!>^Ob+tC2sPz z@}frL4{5V^!oJTI8&x~7IWT1AWtQlbFff4htEWE|gV7LkPHLi=!|-hGquHA-UUT;D z)?8N}b>q;Jp5TcNoDK}drAhjUDJiKbK+8Y?WR7Zr=1~|8b=Nnd%;P~auOvSoVcry8 zhad+>tlD0`4JmGI|5U?^ V?tRm?Z1&1ue!#pbGJMwrUjTmQph*A# literal 0 HcmV?d00001 diff --git a/test/test_index.py b/test/test_index.py index 6d90d7965..bb05d3108 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -1218,30 +1218,47 @@ def test_index_add_non_normalized_path(self, rw_repo): rw_repo.index.add(non_normalized_path) + def test_index_file_v3(self): + index = IndexFile(self.rorepo, fixture_path("index_extended_flags")) + assert index.entries + assert index.version == 3 + assert len(index.entries) == 4 + assert index.entries[('init.t', 0)].skip_worktree + + # Write the data - it must match the original. + with tempfile.NamedTemporaryFile() as tmpfile: + index.write(tmpfile.name) + assert Path(tmpfile.name).read_bytes() == Path(fixture_path("index_extended_flags")).read_bytes() + @with_rw_directory - def test_index_version_v3(self, tmp_dir): + def test_index_file_v3_with_git_command(self, tmp_dir): tmp_dir = Path(tmp_dir) with cwd(tmp_dir): - subprocess.run(["git", "init", "-q"], check=True) + git = Git(tmp_dir) + git.init() + file = tmp_dir / "file.txt" file.write_text("hello") - subprocess.run(["git", "add", "-N", "file.txt"], check=True) + git.add("--intent-to-add", "file.txt") # intent-to-add sets extended flag repo = Repo(tmp_dir) + index = repo.index - assert len(repo.index.entries) == 1 - entry = list(repo.index.entries.values())[0] + assert len(index.entries) == 1 + assert index.version == 3 + entry = list(index.entries.values())[0] assert entry.path == "file.txt" assert entry.intent_to_add file2 = tmp_dir / "file2.txt" file2.write_text("world") - repo.index.add(["file2.txt"]) - repo.index.write() + index.add(["file2.txt"]) + index.write() - status_str = subprocess.check_output(["git", "status", "--porcelain"], text=True) - assert " A file.txt\n" in status_str - assert "A file2.txt\n" in status_str + status_str = git.status(porcelain=True) + status_lines = status_str.splitlines() + assert " A file.txt" in status_lines + assert "A file2.txt" in status_lines class TestIndexUtils: From 107b1b44e91a19ebbe5e41cd7312ce7838534732 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 9 Nov 2025 08:41:10 +0100 Subject: [PATCH 32/45] make linter happy --- test/test_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_index.py b/test/test_index.py index bb05d3108..711b43a0b 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -1223,7 +1223,7 @@ def test_index_file_v3(self): assert index.entries assert index.version == 3 assert len(index.entries) == 4 - assert index.entries[('init.t', 0)].skip_worktree + assert index.entries[("init.t", 0)].skip_worktree # Write the data - it must match the original. with tempfile.NamedTemporaryFile() as tmpfile: From 98e860d1c3a0855e2ff29bf24c5adaca7a57366f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:52:55 +0000 Subject: [PATCH 33/45] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [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/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/alpine-test.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/cygwin-test.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml index a9c29117e..b7de7482e 100644 --- a/.github/workflows/alpine-test.yml +++ b/.github/workflows/alpine-test.yml @@ -26,7 +26,7 @@ jobs: adduser runner docker shell: sh -exo pipefail {0} # Run this as root, not the "runner" user. - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 32d5e84e4..e243416a8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,7 +47,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 5c42c8583..327e1f10c 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -34,7 +34,7 @@ jobs: git config --global core.autocrlf false # Affects the non-Cygwin git. shell: pwsh # Do this outside Cygwin, to affect actions/checkout. - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ed535a914..956b38963 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7088310e5..975c2e29d 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -39,7 +39,7 @@ jobs: shell: bash --noprofile --norc -exo pipefail {0} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 From 497ca401fe094fcae11410a46518e8f56d7bd665 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 09:04:19 +0000 Subject: [PATCH 34/45] Pin mypy==1.18.2 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 75e9e81fa..460597539 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ coverage[toml] ddt >= 1.1.1, != 1.4.3 mock ; python_version < "3.8" -mypy +mypy==1.18.2 # pin mypy to avoid new errors pre-commit pytest >= 7.3.1 pytest-cov From 50762f112fef28230deea55c2d0ca344c6c6cb2c Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 09:08:42 +0000 Subject: [PATCH 35/45] Fail remote pipeline when mypy fails --- .github/workflows/pythonpackage.yml | 1 - pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 975c2e29d..4666f3480 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -103,7 +103,6 @@ jobs: PYTHON_VERSION: ${{ matrix.python-version }} # 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: | diff --git a/pyproject.toml b/pyproject.toml index 58ed81f17..149f2dc92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ testpaths = "test" # Space separated list of paths from root e.g test tests doc # filterwarnings ignore::WarningType # ignores those warnings [tool.mypy] -python_version = "3.8" files = ["git/", "test/deprecation/"] disallow_untyped_defs = true no_implicit_optional = true From 8469a1292f51d5e211e69849844f418d773268e1 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 09:37:54 +0000 Subject: [PATCH 36/45] Fix or ignore all mypy errors --- git/config.py | 6 ++---- git/diff.py | 6 ++++-- git/index/typ.py | 4 ++-- git/objects/commit.py | 2 +- git/objects/submodule/base.py | 3 ++- git/refs/head.py | 6 +----- git/refs/log.py | 2 +- git/refs/symbolic.py | 3 +-- git/refs/tag.py | 8 ++++---- git/repo/base.py | 10 +++------- git/repo/fun.py | 2 +- git/types.py | 6 +++--- git/util.py | 8 +++++--- test/deprecation/test_types.py | 2 +- test/lib/helper.py | 4 ++-- test/test_submodule.py | 2 +- 16 files changed, 34 insertions(+), 40 deletions(-) diff --git a/git/config.py b/git/config.py index 200c81bb7..458151d05 100644 --- a/git/config.py +++ b/git/config.py @@ -574,7 +574,7 @@ def _included_paths(self) -> List[Tuple[str, str]]: if keyword.endswith("/i"): value = re.sub( r"[a-zA-Z]", - lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()), + lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]", value, ) if self._repo.git_dir: @@ -633,8 +633,6 @@ def read(self) -> None: # type: ignore[override] file_path = cast(IO[bytes], file_path) self._read(file_path, file_path.name) else: - # Assume a path if it is not a file-object. - file_path = cast(PathLike, file_path) try: with open(file_path, "rb") as fp: file_ok = True @@ -768,7 +766,7 @@ def _assure_writable(self, method_name: str) -> None: if self.read_only: raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) - def add_section(self, section: str) -> None: + def add_section(self, section: str | cp._UNNAMED_SECTION) -> None: """Assures added options will stay in order.""" return super().add_section(section) diff --git a/git/diff.py b/git/diff.py index 9c6ae59e0..2b1fd928c 100644 --- a/git/diff.py +++ b/git/diff.py @@ -21,15 +21,17 @@ Any, Iterator, List, + Literal, Match, Optional, + Sequence, Tuple, TYPE_CHECKING, TypeVar, Union, cast, ) -from git.types import Literal, PathLike +from git.types import PathLike if TYPE_CHECKING: from subprocess import Popen @@ -289,7 +291,7 @@ class DiffIndex(List[T_Diff]): The class improves the diff handling convenience. """ - change_type = ("A", "C", "D", "R", "M", "T") + change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") """Change type invariant identifying possible ways a blob can have changed: * ``A`` = Added diff --git a/git/index/typ.py b/git/index/typ.py index 4bcb604ab..927633a9f 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -192,7 +192,7 @@ def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry": Instance of type :class:`BaseIndexEntry`. """ time = pack(">LL", 0, 0) - return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0)) + return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0)) # type: ignore[arg-type] @classmethod def from_blob(cls, blob: Blob, stage: int = 0) -> "IndexEntry": @@ -211,5 +211,5 @@ def from_blob(cls, blob: Blob, stage: int = 0) -> "IndexEntry": 0, 0, blob.size, - ) + ) # type: ignore[arg-type] ) diff --git a/git/objects/commit.py b/git/objects/commit.py index fbe0ee9c0..8c51254a2 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -900,7 +900,7 @@ def co_authors(self) -> List[Actor]: if self.message: results = re.findall( r"^Co-authored-by: (.*) <(.*?)>$", - self.message, + str(self.message), re.MULTILINE, ) for author in results: diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 5031a2e71..b4a4ca467 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -25,6 +25,7 @@ ) from git.objects.base import IndexObject, Object from git.objects.util import TraversableIterableObj +from ...refs.remote import RemoteReference from git.util import ( IterableList, RemoteProgress, @@ -355,7 +356,7 @@ def _clone_repo( module_checkout_path = osp.join(str(repo.working_tree_dir), path) if url.startswith("../"): - remote_name = repo.active_branch.tracking_branch().remote_name + remote_name = cast(RemoteReference, repo.active_branch.tracking_branch()).remote_name repo_remote_url = repo.remote(remote_name).url url = os.path.join(repo_remote_url, url) diff --git a/git/refs/head.py b/git/refs/head.py index 683634451..3c43993e7 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -22,7 +22,6 @@ from git.types import Commit_ish, PathLike if TYPE_CHECKING: - from git.objects import Commit from git.refs import RemoteReference from git.repo import Repo @@ -44,9 +43,6 @@ class HEAD(SymbolicReference): __slots__ = () - # TODO: This can be removed once SymbolicReference.commit has static type hints. - commit: "Commit" - def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None: if path != self._HEAD_NAME: raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) @@ -149,7 +145,7 @@ class Head(Reference): k_config_remote_ref = "merge" # Branch to merge from remote. @classmethod - def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None: + def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None: # type: ignore[override] """Delete the given heads. :param force: diff --git a/git/refs/log.py b/git/refs/log.py index 8f2f2cd38..4e3666993 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -145,7 +145,7 @@ def from_line(cls, line: bytes) -> "RefLogEntry": actor = Actor._from_string(info[82 : email_end + 1]) time, tz_offset = parse_date(info[email_end + 2 :]) # skipcq: PYL-W0621 - return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg)) + return RefLogEntry(oldhexsha, newhexsha, actor, (time, tz_offset), msg) # type: ignore[call-arg] class RefLog(List[RefLogEntry], Serializable): diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 74bb1fe0a..f0d2abcf4 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -916,8 +916,7 @@ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_Refere SymbolicReference, ): try: - instance: T_References - instance = ref_type(repo, path) + instance = cast(T_References, ref_type(repo, path)) if instance.__class__ is SymbolicReference and instance.is_detached: raise ValueError("SymbolicRef was detached, we drop it") else: diff --git a/git/refs/tag.py b/git/refs/tag.py index 1e38663ae..4525b09cb 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -45,8 +45,8 @@ class TagReference(Reference): _common_default = "tags" _common_path_default = Reference._common_path_default + "/" + _common_default - @property - def commit(self) -> "Commit": # type: ignore[override] # LazyMixin has unrelated commit method + @property # type: ignore[misc] + def commit(self) -> "Commit": # LazyMixin has unrelated commit method """:return: Commit object the tag ref points to :raise ValueError: @@ -80,8 +80,8 @@ def tag(self) -> Union["TagObject", None]: return None # Make object read-only. It should be reasonably hard to adjust an existing tag. - @property - def object(self) -> AnyGitObject: # type: ignore[override] + @property # type: ignore[misc] + def object(self) -> AnyGitObject: return Reference._get_object(self) @classmethod diff --git a/git/repo/base.py b/git/repo/base.py index 6ea96aad2..1ef7114af 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -684,11 +684,7 @@ def _config_reader( git_dir: Optional[PathLike] = None, ) -> GitConfigParser: if config_level is None: - files = [ - self._get_config_path(cast(Lit_config_levels, f), git_dir) - for f in self.config_level - if cast(Lit_config_levels, f) - ] + files = [self._get_config_path(f, git_dir) for f in self.config_level if f] else: files = [self._get_config_path(config_level, git_dir)] return GitConfigParser(files, read_only=True, repo=self) @@ -1484,7 +1480,7 @@ def clone( self.common_dir, path, type(self.odb), - progress, + progress, # type: ignore[arg-type] multi_options, allow_unsafe_protocols=allow_unsafe_protocols, allow_unsafe_options=allow_unsafe_options, @@ -1545,7 +1541,7 @@ def clone_from( url, to_path, GitCmdObjectDB, - progress, + progress, # type: ignore[arg-type] multi_options, allow_unsafe_protocols=allow_unsafe_protocols, allow_unsafe_options=allow_unsafe_options, diff --git a/git/repo/fun.py b/git/repo/fun.py index 1c995c6c6..3f00e60ea 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -286,7 +286,7 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: # END handle refname else: if ref is not None: - obj = cast("Commit", ref.commit) + obj = ref.commit # END handle ref # END initialize obj on first token diff --git a/git/types.py b/git/types.py index cce184530..c6dbb717b 100644 --- a/git/types.py +++ b/git/types.py @@ -13,7 +13,7 @@ Sequence as Sequence, Tuple, TYPE_CHECKING, - Type, + TypeAlias, TypeVar, Union, ) @@ -117,7 +117,7 @@ object types. """ -GitObjectTypeString = Literal["commit", "tag", "blob", "tree"] +GitObjectTypeString: TypeAlias = Literal["commit", "tag", "blob", "tree"] """Literal strings identifying git object types and the :class:`~git.objects.base.Object`-based types that represent them. @@ -130,7 +130,7 @@ https://git-scm.com/docs/gitglossary#def_object_type """ -Lit_commit_ish: Type[Literal["commit", "tag"]] +Lit_commit_ish: TypeAlias = Literal["commit", "tag"] """Deprecated. Type of literal strings identifying typically-commitish git object types. Prior to a bugfix, this type had been defined more broadly. Any usage is in practice diff --git a/git/util.py b/git/util.py index 0aff5eb64..54a5b7bd1 100644 --- a/git/util.py +++ b/git/util.py @@ -1143,7 +1143,7 @@ def _obtain_lock(self) -> None: # END endless loop -class IterableList(List[T_IterableObj]): +class IterableList(List[T_IterableObj]): # type: ignore[type-var] """List of iterable objects allowing to query an object by id or by named index:: heads = repo.heads @@ -1214,14 +1214,16 @@ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_Iterabl raise ValueError("Index should be an int or str") else: try: + if not isinstance(index, str): + raise AttributeError(f"{index} is not a valid attribute") return getattr(self, index) except AttributeError as e: - raise IndexError("No item found with id %r" % (self._prefix + index)) from e + raise IndexError(f"No item found with id {self._prefix}{index}") from e # END handle getattr def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None: delindex = cast(int, index) - if not isinstance(index, int): + if isinstance(index, str): delindex = -1 name = self._prefix + index for i, item in enumerate(self): diff --git a/test/deprecation/test_types.py b/test/deprecation/test_types.py index f97375a85..d3c6af645 100644 --- a/test/deprecation/test_types.py +++ b/test/deprecation/test_types.py @@ -36,7 +36,7 @@ def test_can_access_lit_commit_ish_but_it_is_not_usable() -> None: assert 'Literal["commit", "tag"]' in message, "Has new definition." assert "GitObjectTypeString" in message, "Has new type name for old definition." - _: Lit_commit_ish = "commit" # type: ignore[valid-type] + _: Lit_commit_ish = "commit" # It should be as documented (even though deliberately unusable in static checks). assert Lit_commit_ish == Literal["commit", "tag"] diff --git a/test/lib/helper.py b/test/lib/helper.py index 241d27341..b4615f400 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -149,7 +149,7 @@ def repo_creator(self): os.chdir(rw_repo.working_dir) try: return func(self, rw_repo) - except: # noqa: E722 B001 + except: # noqa: E722 _logger.info("Keeping repo after failure: %s", repo_dir) repo_dir = None raise @@ -309,7 +309,7 @@ def remote_repo_creator(self): with cwd(rw_repo.working_dir): try: return func(self, rw_repo, rw_daemon_repo) - except: # noqa: E722 B001 + except: # noqa: E722 _logger.info( "Keeping repos after failure: \n rw_repo_dir: %s \n rw_daemon_repo_dir: %s", rw_repo_dir, diff --git a/test/test_submodule.py b/test/test_submodule.py index 4a248eb60..a92dd8fd4 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -932,7 +932,7 @@ def assert_exists(sm, value=True): csm.repo.index.commit("Have to commit submodule change for algorithm to pick it up") assert csm.url == "bar" - self.assertRaises( + self.assertRaises( # noqa: B017 Exception, rsm.update, recursive=True, From a9756bc0c8997482a7f69cc8e46a9f461afea8f6 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 09:46:47 +0000 Subject: [PATCH 37/45] Fix typing so that code can run --- git/config.py | 2 +- git/objects/submodule/base.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/git/config.py b/git/config.py index 458151d05..bccf61258 100644 --- a/git/config.py +++ b/git/config.py @@ -766,7 +766,7 @@ def _assure_writable(self, method_name: str) -> None: if self.read_only: raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) - def add_section(self, section: str | cp._UNNAMED_SECTION) -> None: + def add_section(self, section: "cp._SectionName") -> None: """Assures added options will stay in order.""" return super().add_section(section) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index b4a4ca467..20f3e9ccf 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -25,7 +25,6 @@ ) from git.objects.base import IndexObject, Object from git.objects.util import TraversableIterableObj -from ...refs.remote import RemoteReference from git.util import ( IterableList, RemoteProgress, @@ -67,7 +66,7 @@ if TYPE_CHECKING: from git.index import IndexFile from git.objects.commit import Commit - from git.refs import Head + from git.refs import Head, RemoteReference from git.repo import Repo # ----------------------------------------------------------------------------- @@ -356,7 +355,7 @@ def _clone_repo( module_checkout_path = osp.join(str(repo.working_tree_dir), path) if url.startswith("../"): - remote_name = cast(RemoteReference, repo.active_branch.tracking_branch()).remote_name + remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name repo_remote_url = repo.remote(remote_name).url url = os.path.join(repo_remote_url, url) From 0aba3e7bdec7544a86b6e6ba4b0ad8e2ac5cd2c7 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 10:02:07 +0000 Subject: [PATCH 38/45] Stop Lit_commit_ish being imported --- git/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/types.py b/git/types.py index c6dbb717b..100fff43f 100644 --- a/git/types.py +++ b/git/types.py @@ -13,7 +13,6 @@ Sequence as Sequence, Tuple, TYPE_CHECKING, - TypeAlias, TypeVar, Union, ) @@ -117,7 +116,7 @@ object types. """ -GitObjectTypeString: TypeAlias = Literal["commit", "tag", "blob", "tree"] +GitObjectTypeString = Literal["commit", "tag", "blob", "tree"] """Literal strings identifying git object types and the :class:`~git.objects.base.Object`-based types that represent them. @@ -130,7 +129,8 @@ https://git-scm.com/docs/gitglossary#def_object_type """ -Lit_commit_ish: TypeAlias = Literal["commit", "tag"] +if TYPE_CHECKING: + Lit_commit_ish = Literal["commit", "tag"] """Deprecated. Type of literal strings identifying typically-commitish git object types. Prior to a bugfix, this type had been defined more broadly. Any usage is in practice From 019f270785fd01558b48d21fe1469b9a2132d04b Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 10:02:45 +0000 Subject: [PATCH 39/45] Set __test__ = False in not tested classes --- test/test_remote.py | 5 ++++- test/test_submodule.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 5ddb41bc0..b1d686f05 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -44,7 +44,7 @@ class TestRemoteProgress(RemoteProgress): __slots__ = ("_seen_lines", "_stages_per_op", "_num_progress_messages") - def __init__(self): + def __init__(self) -> None: super().__init__() self._seen_lines = [] self._stages_per_op = {} @@ -103,6 +103,9 @@ def assert_received_message(self): assert self._num_progress_messages +TestRemoteProgress.__test__ = False # type: ignore + + class TestRemote(TestBase): def tearDown(self): gc.collect() diff --git a/test/test_submodule.py b/test/test_submodule.py index a92dd8fd4..edff064c4 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -58,6 +58,7 @@ def update(self, op, cur_count, max_count, message=""): print(op, cur_count, max_count, message) +TestRootProgress.__test__ = False prog = TestRootProgress() From ca5a2e817829861c5a0830806c0a40a33a5ab83f Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 11:40:23 +0000 Subject: [PATCH 40/45] Add missing parentheses around tuple constructor --- git/refs/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/log.py b/git/refs/log.py index 4e3666993..4751cff99 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -145,7 +145,7 @@ def from_line(cls, line: bytes) -> "RefLogEntry": actor = Actor._from_string(info[82 : email_end + 1]) time, tz_offset = parse_date(info[email_end + 2 :]) # skipcq: PYL-W0621 - return RefLogEntry(oldhexsha, newhexsha, actor, (time, tz_offset), msg) # type: ignore[call-arg] + return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg)) # type: ignore [arg-type] class RefLog(List[RefLogEntry], Serializable): From c75790837d0fd5bb9a7ba26a48b957b5d70987fb Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 11:43:21 +0000 Subject: [PATCH 41/45] Install mypy for Python >= 3.9 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 460597539..e6e01c683 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ coverage[toml] ddt >= 1.1.1, != 1.4.3 mock ; python_version < "3.8" -mypy==1.18.2 # pin mypy to avoid new errors +mypy==1.18.2 ; python_version >= "3.9" # pin mypy version to avoid new errors pre-commit pytest >= 7.3.1 pytest-cov From 9decf740ad2f1d89b55bda3a42880fa4f7b652ea Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 11:49:10 +0000 Subject: [PATCH 42/45] Skip mypy when Python < 3.9 --- .github/workflows/pythonpackage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4666f3480..9e05b3fe6 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -95,6 +95,7 @@ jobs: continue-on-error: true - name: Check types with mypy + if: matrix.python-version != '3.7' && matrix.python-version != '3.8' run: | mypy --python-version="${PYTHON_VERSION%t}" # Version only, with no "t" for free-threaded. env: From a1f094c81fcf4a6b559c2a26fc622c89e4f19735 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Fri, 28 Nov 2025 12:10:04 +0000 Subject: [PATCH 43/45] Use git.types.Literal instead of typing.Literal --- git/diff.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index 2b1fd928c..23cb5675e 100644 --- a/git/diff.py +++ b/git/diff.py @@ -21,7 +21,6 @@ Any, Iterator, List, - Literal, Match, Optional, Sequence, @@ -31,7 +30,7 @@ Union, cast, ) -from git.types import PathLike +from git.types import PathLike, Literal if TYPE_CHECKING: from subprocess import Popen @@ -291,7 +290,7 @@ class DiffIndex(List[T_Diff]): The class improves the diff handling convenience. """ - change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") + change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821 """Change type invariant identifying possible ways a blob can have changed: * ``A`` = Added From b5c834af59531456551d406eb857934e7e87f1ce Mon Sep 17 00:00:00 2001 From: George Ogden Date: Sat, 29 Nov 2025 11:49:23 +0000 Subject: [PATCH 44/45] Remve comment about skipping mypy --- .github/workflows/pythonpackage.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 9e05b3fe6..ac764d9a7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -102,8 +102,6 @@ jobs: MYPY_FORCE_COLOR: "1" TERM: "xterm-256color" # For color: https://github.com/python/mypy/issues/13817 PYTHON_VERSION: ${{ matrix.python-version }} - # 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. - name: Test with pytest run: | From eb15123b82dbd13f9cc88606b8a580c424335fe7 Mon Sep 17 00:00:00 2001 From: George Ogden Date: Sat, 29 Nov 2025 11:51:47 +0000 Subject: [PATCH 45/45] Use cast to allow silent getattrs --- git/util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/git/util.py b/git/util.py index 54a5b7bd1..1f1595f1c 100644 --- a/git/util.py +++ b/git/util.py @@ -1214,9 +1214,7 @@ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_Iterabl raise ValueError("Index should be an int or str") else: try: - if not isinstance(index, str): - raise AttributeError(f"{index} is not a valid attribute") - return getattr(self, index) + return getattr(self, cast(str, index)) except AttributeError as e: raise IndexError(f"No item found with id {self._prefix}{index}") from e # END handle getattr