From dba42451187243ffe6d56a08e032dbcbb9ac615f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 16:55:44 -0400
Subject: [PATCH 001/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 20:11:52 -0400
Subject: [PATCH 002/994] 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: <http://pypi.python.org/pypi/GitPython>.
+```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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 20:27:04 -0400
Subject: [PATCH 003/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 21:51:13 -0400
Subject: [PATCH 004/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 23:13:20 -0400
Subject: [PATCH 005/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 23:33:06 -0400
Subject: [PATCH 006/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 9 Sep 2023 23:55:26 -0400
Subject: [PATCH 007/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 10 Sep 2023 00:08:58 -0400
Subject: [PATCH 008/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 10 Sep 2023 01:18:44 -0400
Subject: [PATCH 009/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 10 Sep 2023 04:23:25 -0400
Subject: [PATCH 010/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 10 Sep 2023 13:24:40 -0400
Subject: [PATCH 011/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 10 Sep 2023 13:28:16 -0400
Subject: [PATCH 012/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 00:41:04 -0400
Subject: [PATCH 013/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 01:42:40 -0400
Subject: [PATCH 014/994] 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 <sebastian.thiel@icloud.com>
Date: Mon, 11 Sep 2023 11:26:57 +0200
Subject: [PATCH 015/994] 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 <sebastian.thiel@icloud.com>
Date: Mon, 11 Sep 2023 12:53:21 +0200
Subject: [PATCH 016/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 13:39:34 -0400
Subject: [PATCH 017/994] 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 <version>` 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 <version>` 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 23:11:43 -0400
Subject: [PATCH 018/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 23:14:51 -0400
Subject: [PATCH 019/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 23:28:14 -0400
Subject: [PATCH 020/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 12 Sep 2023 00:56:01 -0400
Subject: [PATCH 021/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 12 Sep 2023 00:58:45 -0400
Subject: [PATCH 022/994] 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 <sebastian.thiel@icloud.com>
Date: Tue, 12 Sep 2023 08:02:41 +0200
Subject: [PATCH 023/994] 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 c86284565ce97de07870379e52a567343d7c4662 Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Tue, 12 Sep 2023 10:12:58 -0600
Subject: [PATCH 024/994] Fix dynamically-set __all__ variable

---
 git/__init__.py | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 6196a42d7..cc0ca2136 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -61,8 +61,19 @@ def _init_externals() -> None:
 
 # } END imports
 
-__all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
-
+# __all__ must be statically defined by py.typed support
+# __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
+__all__ = ['BadName', 'safe_decode',
+    'remove_password_if_present', 'List', 'Sequence', 'Tuple', 'Union', 'TYPE_CHECKING',
+    'PathLike', 'GitError', 'InvalidGitRepositoryError', 'WorkTreeRepositoryUnsupported',
+    'NoSuchPathError', 'UnsafeProtocolError', 'UnsafeOptionError', 'CommandError', 'GitCommandNotFound',
+    'GitCommandError', 'CheckoutError', 'CacheError', 'UnmergedEntriesError', 'HookExecutionError',
+    'RepositoryDirtyError', 'Optional', 'GitConfigParser', 'Object', 'IndexObject', 'Blob', 'Commit',
+    'Submodule', 'UpdateProgress', 'RootModule', 'RootUpdateProgress', 'TagObject', 'TreeModifier',
+    'Tree', 'SymbolicReference', 'Reference', 'HEAD', 'Head', 'TagReference', 'Tag', 'RemoteReference',
+    'RefLog', 'RefLogEntry', 'Diffable', 'DiffIndex', 'Diff', 'NULL_TREE', 'GitCmdObjectDB', 'GitDB',
+    'Git', 'Repo', 'RemoteProgress', 'PushInfo', 'FetchInfo', 'Remote', 'IndexFile', 'StageType',
+    'BlobFilter', 'BaseIndexEntry', 'IndexEntry', 'LockFile', 'BlockingLockFile', 'Stats', 'Actor', 'rmtree']
 
 # { Initialize git executable path
 GIT_OK = None

From 6fb231828ea845e7f0b80e52280f3efb98a25545 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 13:55:56 -0400
Subject: [PATCH 025/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 11 Sep 2023 14:54:04 -0400
Subject: [PATCH 026/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 05:44:16 -0400
Subject: [PATCH 027/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 06:13:44 -0400
Subject: [PATCH 028/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 06:33:10 -0400
Subject: [PATCH 029/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 06:41:03 -0400
Subject: [PATCH 030/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 07:45:26 -0400
Subject: [PATCH 031/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 09:05:36 -0400
Subject: [PATCH 032/994] 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 <sebastian.thiel@icloud.com>
Date: Thu, 14 Sep 2023 08:48:45 +0200
Subject: [PATCH 033/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 04:24:17 -0400
Subject: [PATCH 034/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 05:38:24 -0400
Subject: [PATCH 035/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 07:16:08 -0400
Subject: [PATCH 036/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 08:11:42 -0400
Subject: [PATCH 037/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 08:57:34 -0400
Subject: [PATCH 038/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 15 Sep 2023 07:40:36 +0200
Subject: [PATCH 039/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 15 Sep 2023 07:47:31 +0200
Subject: [PATCH 040/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 15 Sep 2023 07:53:55 +0200
Subject: [PATCH 041/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 15 Sep 2023 08:06:51 +0200
Subject: [PATCH 042/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 15 Sep 2023 08:32:30 +0200
Subject: [PATCH 043/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 08:07:23 -0400
Subject: [PATCH 044/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 09:42:40 -0400
Subject: [PATCH 045/994] 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 <me _at_ jhale.dev>
 -Santos Gallegos <stsewd _at_ proton.me>
 -Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
+
 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 <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 10:01:38 -0400
Subject: [PATCH 046/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 23:25:19 -0400
Subject: [PATCH 047/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 18 Sep 2023 17:57:38 -0400
Subject: [PATCH 048/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 18 Sep 2023 03:26:38 -0400
Subject: [PATCH 049/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 18 Sep 2023 19:49:00 -0400
Subject: [PATCH 050/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 18 Sep 2023 20:36:31 -0400
Subject: [PATCH 051/994] 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 <degeneracypressure@gmail.com>
Date: Mon, 18 Sep 2023 22:58:58 -0400
Subject: [PATCH 052/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 19 Sep 2023 09:02:52 -0400
Subject: [PATCH 053/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 19 Sep 2023 09:06:58 -0400
Subject: [PATCH 054/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 19 Sep 2023 09:09:08 -0400
Subject: [PATCH 055/994] 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 <degeneracypressure@gmail.com>
Date: Tue, 19 Sep 2023 10:20:28 -0400
Subject: [PATCH 056/994] 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(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(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 <path/to/dir>
 
         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 <url> <local_dir>
@@ -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 <git.Blob "SHA1-HEX-HASH">
@@ -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(osp.join(smp, "a")))
         parent.git.add(Git.polish_url(osp.join(rwdir, ".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 <degeneracypressure@gmail.com>
Date: Wed, 20 Sep 2023 21:29:33 -0400
Subject: [PATCH 057/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 20 Sep 2023 22:25:40 -0400
Subject: [PATCH 058/994] 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 <degeneracypressure@gmail.com>
Date: Wed, 20 Sep 2023 23:40:44 -0400
Subject: [PATCH 059/994] 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: [<git.Commit "SHA1-HEX_HASH-2">,
@@ -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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 02:46:48 -0400
Subject: [PATCH 060/994] 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 <facundo.tuesca@trailofbits.com>
Date: Thu, 21 Sep 2023 12:00:59 +0200
Subject: [PATCH 061/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 06:40:03 -0400
Subject: [PATCH 062/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 06:57:27 -0400
Subject: [PATCH 063/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 03:00:04 -0400
Subject: [PATCH 064/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 07:05:40 -0400
Subject: [PATCH 065/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 21 Sep 2023 19:12:03 -0400
Subject: [PATCH 066/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 22 Sep 2023 09:04:21 +0200
Subject: [PATCH 067/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 22 Sep 2023 09:33:54 +0200
Subject: [PATCH 068/994] 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 <sebastian.thiel@icloud.com>
Date: Fri, 22 Sep 2023 09:37:09 +0200
Subject: [PATCH 069/994] 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'

From 4e701bdae829bb02abd7f9acf5e6508e242b6977 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 05:54:47 -0400
Subject: [PATCH 070/994] Add missing assert keywords

This turns two tuple expression statements--each of an equality
comparison and message--that were not being checked or otherwise
used, and that were intended to be assertions, into assertions.
---
 test/test_submodule.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 4a9c9c582..0aa80e5ce 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1031,8 +1031,8 @@ def test_branch_renames(self, rw_dir):
         # This doesn't fail as our own submodule binsha didn't change, and the reset is only triggered if
         # to latest revision is True.
         parent_repo.submodule_update(to_latest_revision=False)
-        sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head"
-        sm_mod.commit() == sm_fb.commit, "Head wasn't reset"
+        assert sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head"
+        assert sm_mod.commit() == sm_fb.commit, "Head wasn't reset"
 
         self.assertRaises(RepositoryDirtyError, parent_repo.submodule_update, to_latest_revision=True)
         parent_repo.submodule_update(to_latest_revision=True, force_reset=True)

From 45773c27fa40fddf72b40970836b3649094cd994 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Sep 2023 08:16:29 -0400
Subject: [PATCH 071/994] Instrument workflows to investigate skipped tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This makes jobs from both test workflows give more information
relevant to examining which tests are skipped (and if any tests
xfail, those too) in what environments:

- Values of os.name and git.util.is_win.

- The name of each test that runs, with its status.

The latter doesn't increase the output length as much as might be
expected, because due to the way the output is handled, the
pytest-sugar pretty output format without -v looked like:

 test/test_actor.py ✓                                              0%
 test/test_actor.py ✓✓                                             0% ▏
 test/test_actor.py ✓✓✓                                            1% ▏
 test/test_actor.py ✓✓✓✓                                           1% ▏

When instead it was intended to fit on a single line. Still, the
current output with -v has extra newlines, increasing length and
worsening readability, so it should be improved on if possible.
---
 .github/workflows/cygwin-test.yml   | 6 +++++-
 .github/workflows/pythonpackage.yml | 6 +++++-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 962791ae7..3f93f6830 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -58,7 +58,11 @@ jobs:
       run: |
         /usr/bin/python -m pip install ".[test]"
 
+    - name: Check 'is_win'
+      run: |
+        /usr/bin/python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")'
+
     - name: Test with pytest
       run: |
         set +x
-        /usr/bin/python -m pytest
+        /usr/bin/python -m pytest -v
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index a5467ef94..c57f31cdc 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -73,9 +73,13 @@ jobs:
       # so we have to ignore errors until that changes.
       continue-on-error: true
 
+    - name: Check 'is_win'
+      run: |
+        python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")'
+
     - name: Test with pytest
       run: |
-        pytest
+        pytest -v
       continue-on-error: false
 
     - name: Documentation

From 6fbe5118514618d34ea8912e99fbde3fd6d7a557 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 16:09:01 -0400
Subject: [PATCH 072/994] Show version and platform info in one place

Instead of splitting it in into two places where at least one of
the places is highly likely to be missed, this puts it together
just before the first steps that makes nontrivial use of the
installed packages. Grouping it together, it can't really be shown
earlier, because one of the pieces of information is obtained
using the git module (to examine that behavior of the code).

This also presents the information more clearly. "set -x" makes
this easy, so the commands are rewritten to take advantage of it.
---
 .github/workflows/cygwin-test.yml   | 13 ++++++-------
 .github/workflows/pythonpackage.yml | 17 ++++++++---------
 2 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 3f93f6830..1563afc95 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -29,11 +29,6 @@ jobs:
       with:
         packages: python39 python39-pip python39-virtualenv git
 
-    - name: Show python and git versions
-      run: |
-        /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)"
@@ -58,9 +53,13 @@ jobs:
       run: |
         /usr/bin/python -m pip install ".[test]"
 
-    - name: Check 'is_win'
+    - name: Show version and platform information
       run: |
-        /usr/bin/python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")'
+        /usr/bin/git version
+        /usr/bin/python --version
+        /usr/bin/python -c 'import sys; print(sys.platform)'
+        /usr/bin/python -c 'import os; print(os.name)'
+        /usr/bin/python -c 'import git; print(git.compat.is_win)'
 
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index c57f31cdc..696057ae2 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -36,11 +36,6 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
-    - name: Show python and git versions
-      run: |
-        python --version
-        git version
-
     - name: Prepare this repo for tests
       run: |
         TRAVIS=yes ./init-tests-after-clone.sh
@@ -66,6 +61,14 @@ jobs:
       run: |
         pip install ".[test]"
 
+    - name: Show version and platform information
+      run: |
+        git version
+        python --version
+        python -c 'import sys; print(sys.platform)'
+        python -c 'import os; print(os.name)'
+        python -c 'import git; print(git.compat.is_win)'
+
     - name: Check types with mypy
       run: |
         mypy -p git
@@ -73,10 +76,6 @@ jobs:
       # so we have to ignore errors until that changes.
       continue-on-error: true
 
-    - name: Check 'is_win'
-      run: |
-        python -c 'import os, git; print(f"os.name={os.name}, is_win={git.compat.is_win}")'
-
     - name: Test with pytest
       run: |
         pytest -v

From bd3307ad428025e72fb66a9ca4e8231aa0917f9e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 16:29:43 -0400
Subject: [PATCH 073/994] Make "Update PyPA packages" step clearer

---
 .github/workflows/pythonpackage.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 696057ae2..a311798e2 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -50,12 +50,12 @@ jobs:
 
     - name: Update PyPA packages
       run: |
-        python -m pip install --upgrade pip
+        python -m pip install --upgrade pip wheel
+
+        # Python prior to 3.12 ships setuptools. Upgrade it if present.
         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: |

From 680d7957373c6bf193388907e3dbb770f3867ffe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Sep 2023 17:13:01 -0400
Subject: [PATCH 074/994] Show all the failures

Don't stop after the first 10.
---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index fa06458eb..0466ed4c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
 [tool.pytest.ini_options]
 python_files = 'test_*.py'
 testpaths = 'test'  # space separated list of paths from root e.g test tests doc/testing
-addopts = '--cov=git --cov-report=term --maxfail=10 --force-sugar --disable-warnings'
+addopts = '--cov=git --cov-report=term --force-sugar --disable-warnings'
 filterwarnings = 'ignore::DeprecationWarning'
 # --cov   coverage
 # --cov-report term  # send report to terminal term-missing -> terminal with line numbers  html  xml

From 75cf5402d0d6c6712c1b0f5bd114cc9fd8780edc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 01:39:33 -0400
Subject: [PATCH 075/994] Keep sugar for local use, but use instafail on CI

There are two benefits of the pytest-sugar plugin:

1. Pretty output.
2. Show details on each failure immediately instead of at the end.

The first benefit is effectively local-only, because extra newlines
are appearing when it runs on CI, both with and without -v.

The second benefit applies both locally and on CI.

So this adds the pytest-instafail plugin and uses it on CI to get
the second benefit. It is not set up to run automatically, and
pytest-sugar still is (though no longer forced), so local testing
retains no benefit and we don't have a clash.

The name "instafail" refers only to instantly *seeing* failures:
it does not cause the pytest runner to stop earlier than otherwise.
---
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 pyproject.toml                      | 2 +-
 test-requirements.txt               | 1 +
 4 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 1563afc95..cae828099 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -64,4 +64,4 @@ jobs:
     - name: Test with pytest
       run: |
         set +x
-        /usr/bin/python -m pytest -v
+        /usr/bin/python -m pytest -p no:sugar -v --instafail
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index a311798e2..9ac1088f7 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -78,7 +78,7 @@ jobs:
 
     - name: Test with pytest
       run: |
-        pytest -v
+        pytest -v -p no:sugar --instafail
       continue-on-error: false
 
     - name: Documentation
diff --git a/pyproject.toml b/pyproject.toml
index 0466ed4c4..f4fc33fec 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
 [tool.pytest.ini_options]
 python_files = 'test_*.py'
 testpaths = 'test'  # space separated list of paths from root e.g test tests doc/testing
-addopts = '--cov=git --cov-report=term --force-sugar --disable-warnings'
+addopts = '--cov=git --cov-report=term --disable-warnings'
 filterwarnings = 'ignore::DeprecationWarning'
 # --cov   coverage
 # --cov-report term  # send report to terminal term-missing -> terminal with line numbers  html  xml
diff --git a/test-requirements.txt b/test-requirements.txt
index b00dd6f06..1c08c736f 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,4 +5,5 @@ mypy
 pre-commit
 pytest
 pytest-cov
+pytest-instafail
 pytest-sugar

From eb56e7bdf15344739d3c2d671c1ca7dc185b8abe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 02:01:25 -0400
Subject: [PATCH 076/994] Pass -v twice to see full skip reasons

+ Reorder pytest arguments so both workflows are consistent.
---
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index cae828099..8d1145e76 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -64,4 +64,4 @@ jobs:
     - name: Test with pytest
       run: |
         set +x
-        /usr/bin/python -m pytest -p no:sugar -v --instafail
+        /usr/bin/python -m pytest -p no:sugar --instafail -vv
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 9ac1088f7..c0402e4bb 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -78,7 +78,7 @@ jobs:
 
     - name: Test with pytest
       run: |
-        pytest -v -p no:sugar --instafail
+        pytest -p no:sugar --instafail -vv
       continue-on-error: false
 
     - name: Documentation

From 9c7ff1e4918128ff28ba02cb2771b440a392644c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 02:04:52 -0400
Subject: [PATCH 077/994] Force pytest color output on CI

While pytest-sugar output gets mangled with extra newlines on CI,
colorized output seems to work fine and improves readability.
---
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 8d1145e76..337c0a809 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -64,4 +64,4 @@ jobs:
     - name: Test with pytest
       run: |
         set +x
-        /usr/bin/python -m pytest -p no:sugar --instafail -vv
+        /usr/bin/python -m pytest --color=yes -p no:sugar --instafail -vv
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index c0402e4bb..392c894bc 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -78,7 +78,7 @@ jobs:
 
     - name: Test with pytest
       run: |
-        pytest -p no:sugar --instafail -vv
+        pytest --color=yes -p no:sugar --instafail -vv
       continue-on-error: false
 
     - name: Documentation

From 0eb38bcedf3703c2e3aacae27ea4cbafce33e941 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Sep 2023 06:33:10 -0400
Subject: [PATCH 078/994] Fix test_blocking_lock_file for cygwin

This permits the longer delay in test_blocking_lock_file--which was
already allowed for native Windows--on Cygwin, where it is also
needed. That lets the xfail mark for Cygwin be removed.

This also updates the comments to avoid implying that the need for
the delay is AppVeyor-specific (it seems needed on CI and locally).
---
 test/test_util.py | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 42edc57cf..308ba311b 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -12,7 +12,6 @@
 from unittest import mock, skipIf
 from datetime import datetime
 
-import pytest
 import ddt
 
 from git.cmd import dashify
@@ -156,11 +155,6 @@ def test_lock_file(self):
         lock_file._obtain_lock_or_raise()
         lock_file._release_lock()
 
-    @pytest.mark.xfail(
-        sys.platform == "cygwin",
-        reason="Cygwin fails here for some reason, always",
-        raises=AssertionError,
-    )
     def test_blocking_lock_file(self):
         my_file = tempfile.mktemp()
         lock_file = BlockingLockFile(my_file)
@@ -173,9 +167,8 @@ def test_blocking_lock_file(self):
         self.assertRaises(IOError, wait_lock._obtain_lock)
         elapsed = time.time() - start
         extra_time = 0.02
-        if is_win:
-            # for Appveyor
-            extra_time *= 6  # NOTE: Indeterministic failures here...
+        if is_win or sys.platform == "cygwin":
+            extra_time *= 6  # NOTE: Indeterministic failures without this...
         self.assertLess(elapsed, wait_time + extra_time)
 
     def test_user_id(self):

From 715dba473a202ef3631b6c4bd724b8ff4e6c6d0b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Sep 2023 02:51:58 -0400
Subject: [PATCH 079/994] Run cygpath tests on Cygwin, not native Windows

They were not running on Cygwin, because git.util.is_win is False
on Cygwin. They were running on native Windows, with a number of
them always failing; these failures had sometimes been obscured by
the --maxfail=10 that had formerly been used (from pyproject.toml).

Many of them (not all the same ones) fail on Cygwin, and it might
be valuable for cygpath to work on other platforms, especially
native Windows. But I think it still makes sense to limit the tests
to Cygwin at this time, because all the uses of cygpath in the
project are in code that only runs after a check that the platform
is Cygwin. Part of that check, as it is implemented, explicitly
excludes native Windows (is_win must be false).
---
 test/test_util.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 308ba311b..41d874678 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -9,7 +9,7 @@
 import sys
 import tempfile
 import time
-from unittest import mock, skipIf
+from unittest import mock, skipUnless
 from datetime import datetime
 
 import ddt
@@ -84,14 +84,14 @@ def setup(self):
             "array": [42],
         }
 
-    @skipIf(not is_win, "Paths specifically for Windows.")
+    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.idata(_norm_cygpath_pairs + _unc_cygpath_pairs)
     def test_cygpath_ok(self, case):
         wpath, cpath = case
         cwpath = cygpath(wpath)
         self.assertEqual(cwpath, cpath, wpath)
 
-    @skipIf(not is_win, "Paths specifically for Windows.")
+    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
         (r"./bar", "bar"),
         (r".\bar", "bar"),
@@ -104,7 +104,7 @@ def test_cygpath_norm_ok(self, case):
         cwpath = cygpath(wpath)
         self.assertEqual(cwpath, cpath or wpath, wpath)
 
-    @skipIf(not is_win, "Paths specifically for Windows.")
+    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
         r"C:",
         r"C:Relative",
@@ -117,7 +117,7 @@ def test_cygpath_invalids(self, wpath):
         cwpath = cygpath(wpath)
         self.assertEqual(cwpath, wpath.replace("\\", "/"), wpath)
 
-    @skipIf(not is_win, "Paths specifically for Windows.")
+    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.idata(_norm_cygpath_pairs)
     def test_decygpath(self, case):
         wpath, cpath = case

From d6a2d2807de99715ce85887b8992dbcafcefcee9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 04:08:02 -0400
Subject: [PATCH 080/994] Mark some cygpath tests xfail

Two of the groups of cygpath tests in test_util.py generate tests
that fail on Cygwin. There is no easy way to still run, but xfail,
just the specific tests that fail, because the groups of tests are
generated with `@ddt` parameterization, but neither the unittest
nor pytest xfail mechanisms interact with that. If
`@pytest.mark.parametrized` were used, this could be done. But
that does not work on methods of test classes that derive from
unittest.TestCase, including those in this project that indirectly
derive from it by deriving from TestBase. The TestBase base class
cannot be removed without overhauling many tests, due to fixtures
it provides such as rorepo.

So this marks too many tests as xfail, but in doing so allows test
runs to pass while still exercising and showing status on all the
tests, allowing result changes to be observed easily.
---
 test/test_util.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/test/test_util.py b/test/test_util.py
index 41d874678..f2135a272 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -13,6 +13,7 @@
 from datetime import datetime
 
 import ddt
+import pytest
 
 from git.cmd import dashify
 from git.compat import is_win
@@ -84,6 +85,10 @@ def setup(self):
             "array": [42],
         }
 
+    @pytest.mark.xfail(
+        reason="Many return paths prefixed /proc/cygdrive instead.",
+        raises=AssertionError,
+    )
     @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.idata(_norm_cygpath_pairs + _unc_cygpath_pairs)
     def test_cygpath_ok(self, case):
@@ -91,6 +96,10 @@ def test_cygpath_ok(self, case):
         cwpath = cygpath(wpath)
         self.assertEqual(cwpath, cpath, wpath)
 
+    @pytest.mark.xfail(
+        reason=r'2nd example r".\bar" -> "bar" fails, returns "./bar"',
+        raises=AssertionError,
+    )
     @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
         (r"./bar", "bar"),

From 881456bdceb61d51fa84ea286e6ca0e3587e8dc5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 06:54:56 -0400
Subject: [PATCH 081/994] Run test_commit_msg_hook_success on more systems

This changes a default Windows skip of test_commit_msg_hook_success
to an xfail, and makes it more specific, expecting failure only
when either bash.exe is unavailable (definitely expected) or when
bash.exe is the WSL bash wrapper in System32, which fails for some
reason even though it's not at all clear it ought to.

This showcases the failures rather than skipping, and also lets the
test pass on Windows systems where bash.exe is something else,
including the Git Bash bash.exe that native Windows CI would use.
---
 test/test_index.py | 28 ++++++++++++++++++++++------
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index fba9c78ec..f4858a586 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -7,10 +7,14 @@
 
 from io import BytesIO
 import os
+import os.path as osp
+from pathlib import Path
 from stat import S_ISLNK, ST_MODE
+import shutil
 import tempfile
 from unittest import skipIf
-import shutil
+
+import pytest
 
 from git import (
     IndexFile,
@@ -23,6 +27,7 @@
     GitCommandError,
     CheckoutError,
 )
+from git.cmd import Git
 from git.compat import is_win
 from git.exc import HookExecutionError, InvalidGitRepositoryError
 from git.index.fun import hook_path
@@ -34,15 +39,22 @@
 from git.util import HIDE_WINDOWS_KNOWN_ERRORS, hex_to_bin
 from gitdb.base import IStream
 
-import os.path as osp
-from git.cmd import Git
+HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
 
-from pathlib import Path
 
-HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
+def _found_in(cmd, directory):
+    """Check if a command is resolved in a directory (without following symlinks)."""
+    path = shutil.which(cmd)
+    return path and Path(path).parent == Path(directory)
+
 
 is_win_without_bash = is_win and not shutil.which("bash.exe")
 
+is_win_with_wsl_bash = is_win and _found_in(
+    cmd="bash.exe",
+    directory=Path(os.getenv("WINDIR")) / "System32",
+)
+
 
 def _make_hook(git_dir, name, content, make_exec=True):
     """A helper to create a hook"""
@@ -910,7 +922,11 @@ def test_pre_commit_hook_fail(self, rw_repo):
         else:
             raise AssertionError("Should have caught a HookExecutionError")
 
-    @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703")
+    @pytest.mark.xfail(
+        is_win_without_bash or is_win_with_wsl_bash,
+        reason="Specifically seems to fail on WSL bash (in spite of #1399)",
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD", bare=True)
     def test_commit_msg_hook_success(self, rw_repo):
         commit_message = "commit default head by Frèderic Çaufl€"

From c6a586ab4817a9dcb8c8290a6a3e7071b1834f32 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 17 Sep 2023 07:07:52 -0400
Subject: [PATCH 082/994] No longer skip test_index_mutation on Cygwin

As it seems to be working now on Cygwin (maybe not native Windows).
---
 test/test_index.py | 16 ++--------------
 1 file changed, 2 insertions(+), 14 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index f4858a586..06db3aedd 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -12,7 +12,6 @@
 from stat import S_ISLNK, ST_MODE
 import shutil
 import tempfile
-from unittest import skipIf
 
 import pytest
 
@@ -27,16 +26,13 @@
     GitCommandError,
     CheckoutError,
 )
-from git.cmd import Git
 from git.compat import is_win
 from git.exc import HookExecutionError, InvalidGitRepositoryError
 from git.index.fun import hook_path
 from git.index.typ import BaseIndexEntry, IndexEntry
 from git.objects import Blob
-from test.lib import TestBase, fixture_path, fixture, with_rw_repo
-from test.lib import with_rw_directory
-from git.util import Actor, rmtree
-from git.util import HIDE_WINDOWS_KNOWN_ERRORS, hex_to_bin
+from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
+from git.util import Actor, hex_to_bin, rmtree
 from gitdb.base import IStream
 
 HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
@@ -434,14 +430,6 @@ def _count_existing(self, repo, files):
 
     # END num existing helper
 
-    @skipIf(
-        HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(),
-        """FIXME: File "C:\\projects\\gitpython\\git\\test\\test_index.py", line 642, in test_index_mutation
-                self.assertEqual(fd.read(), link_target)
-                AssertionError: '!<symlink>\xff\xfe/\x00e\x00t\x00c\x00/\x00t\x00h\x00a\x00t\x00\x00\x00'
-                != '/etc/that'
-                """,
-    )
     @with_rw_repo("0.1.6")
     def test_index_mutation(self, rw_repo):
         index = rw_repo.index

From fc022304d2f4bc8770f75b0c5fc289dccab0ae5b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Sep 2023 02:27:31 -0400
Subject: [PATCH 083/994] Report encoding error in test_add_unicode as error

This makes the test explicitly error out, rather than skipping, if
it appears the environment doesn't support encoding Unicode
filenames. Platforms these days should be capable of that, and
reporting it as an error lessens the risk of missing a bug in the
test code (that method or a fixture) if one is ever introduced.
Erroring out will also make it easier to see the details in the
chained UnicodeDecodeError exception.

This does not affect the behavior of GitPython itself. It only
changes how a test reports an unusual condition that keeps the test\
from being usefully run.
---
 test/test_base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/test_base.py b/test/test_base.py
index b77c8117d..90e701c4b 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -7,7 +7,7 @@
 import os
 import sys
 import tempfile
-from unittest import SkipTest, skipIf
+from unittest import skipIf
 
 from git import Repo
 from git.objects import Blob, Tree, Commit, TagObject
@@ -126,7 +126,7 @@ def test_add_unicode(self, rw_repo):
         try:
             file_path.encode(sys.getfilesystemencoding())
         except UnicodeEncodeError as e:
-            raise SkipTest("Environment doesn't support unicode filenames") from e
+            raise RuntimeError("Environment doesn't support unicode filenames") from e
 
         with open(file_path, "wb") as fp:
             fp.write(b"something")

From 203da23e5fe2ae5a0ae13d0f9b2a276ae584ea7b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Sep 2023 03:19:23 -0400
Subject: [PATCH 084/994] Add a few FIXMEs re: better use of xfail

---
 test/test_config.py | 1 +
 test/test_util.py   | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/test/test_config.py b/test/test_config.py
index 481e129c6..f805570d5 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -100,6 +100,7 @@ def test_includes_order(self):
             # values must be considered as soon as they get them
             assert r_config.get_value("diff", "tool") == "meld"
             try:
+                # FIXME: Split this assertion out somehow and mark it xfail (or fix it).
                 assert r_config.get_value("sec", "var1") == "value1_main"
             except AssertionError as e:
                 raise SkipTest("Known failure -- included values are not in effect right away") from e
diff --git a/test/test_util.py b/test/test_util.py
index f2135a272..2b1e518ed 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -85,6 +85,7 @@ def setup(self):
             "array": [42],
         }
 
+    # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them).
     @pytest.mark.xfail(
         reason="Many return paths prefixed /proc/cygdrive instead.",
         raises=AssertionError,
@@ -103,7 +104,7 @@ def test_cygpath_ok(self, case):
     @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
         (r"./bar", "bar"),
-        (r".\bar", "bar"),
+        (r".\bar", "bar"),  # FIXME: Mark only this one xfail, somehow (or fix it).
         (r"../bar", "../bar"),
         (r"..\bar", "../bar"),
         (r"../bar/.\foo/../chu", "../bar/chu"),

From cf5f1dca139b809a744badb9f9740dd6d0a70c56 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 03:21:05 -0400
Subject: [PATCH 085/994] Report <2.5.1 in test_linked_worktree_traversal as
 error

Although GitPython does not require git >=2.5.1 in general, and
this does *not* change that, this makes the unavailability of git
2.5.1 or later an error in test_linked_worktree_traversal, where it
is needed to exercise that test, rather than skipping the test.

A few systems, such as CentOS 7, may have downstream patched
versions of git that remain safe to use yet are numbered <2.5.1
and do not have the necesary feature to run this test. But by now,
users of those systems likely anticipate that other software would
rely on the presence of features added in git 2.5.1, which was
released over 7 years ago. As such, I think it is more useful to
give an error for that test, so the test's inability to be run on
the system is clear, than to automatically skip the test, which is
likely to go unnoticed.
---
 test/test_fun.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_fun.py b/test/test_fun.py
index d76e189ed..f39955aa0 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -2,7 +2,6 @@
 from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR
 from os import stat
 import os.path as osp
-from unittest import SkipTest
 
 from git import Git
 from git.index import IndexFile
@@ -279,7 +278,7 @@ def test_linked_worktree_traversal(self, rw_dir):
         """Check that we can identify a linked worktree based on a .git file"""
         git = Git(rw_dir)
         if git.version_info[:3] < (2, 5, 1):
-            raise SkipTest("worktree feature unsupported")
+            raise RuntimeError("worktree feature unsupported (test needs git 2.5.1 or later)")
 
         rw_master = self.rorepo.clone(join_path_native(rw_dir, "master_repo"))
         branch = rw_master.create_head("aaaaaaaa")

From 89232365fb0204c32d1f7c23b9eb39fe4401eb7f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 04:13:36 -0400
Subject: [PATCH 086/994] Change skipIf(not ...) to skipUnless(...)

---
 test/test_submodule.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 0aa80e5ce..432c02686 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -7,7 +7,7 @@
 import tempfile
 from pathlib import Path
 import sys
-from unittest import mock, skipIf
+from unittest import mock, skipIf, skipUnless
 
 import pytest
 
@@ -1039,7 +1039,7 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset"
         assert sm_mod.head.ref.name == sm_pfb.name
 
-    @skipIf(not is_win, "Specifically for Windows.")
+    @skipUnless(is_win, "Specifically for Windows.")
     def test_to_relative_path_with_super_at_root_drive(self):
         class Repo(object):
             working_tree_dir = "D:\\"

From b198bf1e7d9c9d9db675c6c4e94d2f136a0a7923 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 05:43:00 -0400
Subject: [PATCH 087/994] Express known test_depth failure with xfail

Rather than skipping, so it becomes known if the situation changes.
---
 test/test_submodule.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 432c02686..54b1e796b 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1050,9 +1050,8 @@ class Repo(object):
         msg = '_to_relative_path should be "submodule_path" but was "%s"' % relative_path
         assert relative_path == "submodule_path", msg
 
-    @skipIf(
-        True,
-        "for some unknown reason the assertion fails, even though it in fact is working in more common setup",
+    @pytest.mark.xfail(
+        reason="for some unknown reason the assertion fails, even though it in fact is working in more common setup",
     )
     @with_rw_directory
     def test_depth(self, rwdir):

From cd175a598ed457833bc06adba776e2bbb1d9014b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 07:09:43 -0400
Subject: [PATCH 088/994] Remove no-effect `@skipIf` on test_untracked_files

It looked like test_untracked_files was sometimes skipped, and
specifically that it would be skipped on Cygwin. But the `@skipIf`
on it had the condition:

    HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin()

HIDE_WINDOWS_KNOWN_ERRORS can only ever be true if it is set to a
truthy value directly (not an intended use as it's a "constant"),
or on native Windows systems: no matter how the environment
variable related to it is set, it's only checked if is_win, which
is set by checking os.name, which is only "nt" on native Windows
systems, not Cygwin.

So whenever HIDE_WINDOWS_KNOWN_ERRORS is true Git.is_cygwin() will
be false. Thus this condition is never true and the test was never
being skipped anyway: it was running and passing on Cygwin.
---
 test/test_repo.py | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index 15899ec50..fe3419e33 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, skip
+from unittest import mock, SkipTest, skip
 
 import pytest
 
@@ -42,7 +42,7 @@
 )
 from git.repo.fun import touch
 from test.lib import TestBase, with_rw_repo, fixture
-from git.util import HIDE_WINDOWS_KNOWN_ERRORS, cygpath
+from git.util import cygpath
 from test.lib import with_rw_directory
 from git.util import join_path_native, rmtree, rmfile, bin_to_hex
 
@@ -764,16 +764,6 @@ def test_blame_accepts_rev_opts(self, git):
         self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"])
         git.assert_called_once_with(*expected_args, **boilerplate_kwargs)
 
-    @skipIf(
-        HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(),
-        """FIXME: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute
-                    raise GitCommandError(command, status, stderr_value, stdout_value)
-                GitCommandError: Cmd('git') failed due to: exit code(128)
-                  cmdline: git add 1__��ava verb��ten 1_test _myfile 1_test_other_file
-                          1_��ava-----verb��ten
-                  stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files'
-                """,
-    )
     @with_rw_repo("HEAD", bare=False)
     def test_untracked_files(self, rwrepo):
         for run, repo_add in enumerate((rwrepo.index.add, rwrepo.git.add)):

From f38cc000fe4d51f0f9d1bdedec86f6fcdb57f359 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 07:32:46 -0400
Subject: [PATCH 089/994] Make 2 more too-low git version skips into errors

In the tests only, and not in any way affecting the feature set or
requirements of GitPython itself.

This is similar to, and with the same reasoning as, cf5f1dc.
---
 test/test_repo.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index fe3419e33..4257d8a47 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, SkipTest, skip
+from unittest import mock, skip
 
 import pytest
 
@@ -1235,7 +1235,7 @@ def test_merge_base(self):
     def test_is_ancestor(self):
         git = self.rorepo.git
         if git.version_info[:3] < (1, 8, 0):
-            raise SkipTest("git merge-base --is-ancestor feature unsupported")
+            raise RuntimeError("git merge-base --is-ancestor feature unsupported (test needs git 1.8.0 or later)")
 
         repo = self.rorepo
         c1 = "f6aa8d1"
@@ -1283,7 +1283,7 @@ def test_git_work_tree_dotgit(self, rw_dir):
         based on it."""
         git = Git(rw_dir)
         if git.version_info[:3] < (2, 5, 1):
-            raise SkipTest("worktree feature unsupported")
+            raise RuntimeError("worktree feature unsupported (test needs git 2.5.1 or later)")
 
         rw_master = self.rorepo.clone(join_path_native(rw_dir, "master_repo"))
         branch = rw_master.create_head("aaaaaaaa")

From 8fd56e78366470e0b07db48daf623b188e7245ea Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 08:03:58 -0400
Subject: [PATCH 090/994] Update test_root_module Windows skip reason

The current cause of failure is different from what is documented
in the skip reason.
---
 test/test_submodule.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 54b1e796b..4c087caa1 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -477,11 +477,11 @@ def test_base_bare(self, rwrepo):
     @skipIf(
         HIDE_WINDOWS_KNOWN_ERRORS,
         """
-        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
+        E  PermissionError:
+        [WinError 32] The process cannot access the file because it is being used by another process:
+        'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\git\\ext\\gitdb'
+        -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\path\\prefix\\git\\ext\\gitdb'
+        """,
     )
     @with_rw_repo(k_subm_current, bare=False)
     def test_root_module(self, rwrepo):

From c1798f5ead60f74de422732d5229244d00325f24 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 08:06:45 -0400
Subject: [PATCH 091/994] Change test_root_module Windows skip to xfail

And rewrite the reason to give more useful information. (The new
reason also doesn't state the exception type, because that is now
specified, and checked by pytest, by being passed as "raises".)
---
 test/test_submodule.py | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 4c087caa1..256eb7dbf 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -474,14 +474,13 @@ def test_base_bare(self, rwrepo):
         reason="Cygwin GitPython can't find submodule SHA",
         raises=ValueError,
     )
-    @skipIf(
+    @pytest.mark.xfail(
         HIDE_WINDOWS_KNOWN_ERRORS,
-        """
-        E  PermissionError:
-        [WinError 32] The process cannot access the file because it is being used by another process:
-        'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\git\\ext\\gitdb'
-        -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\non_bare_test_root_modulep0eqt8_r\\path\\prefix\\git\\ext\\gitdb'
-        """,
+        reason=(
+            '"The process cannot access the file because it is being used by another process"'
+            + " on first call to rm.update"
+        ),
+        raises=PermissionError,
     )
     @with_rw_repo(k_subm_current, bare=False)
     def test_root_module(self, rwrepo):

From ba567521a5a01b93420cab4021d5663af66231ae Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 08:16:05 -0400
Subject: [PATCH 092/994] Update test_git_submodules_and_add_sm_with_new_commit
 skip reason

This is working on Cygwin, so that old reason no longer applies.
(The test was not being skipped on Cygwin, and was passing.)

It is not working on native Windows, due to a PermissionError from
attempting to move a file that is still open (which Windows doesn't
allow). That may have been the original native Windows skip reason,
but the old AppVeyor CI link for it is broken or not public. This
makes the reason clear, though maybe I should add more details.
---
 test/test_submodule.py | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 256eb7dbf..25b0daa01 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -750,13 +750,12 @@ def test_list_only_valid_submodules(self, rwdir):
 
     @skipIf(
         HIDE_WINDOWS_KNOWN_ERRORS,
-        """FIXME on cygwin: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute
-                raise GitCommandError(command, status, stderr_value, stdout_value)
-            GitCommandError: Cmd('git') failed due to: exit code(128)
-              cmdline: git add 1__Xava verbXXten 1_test _myfile 1_test_other_file 1_XXava-----verbXXten
-              stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files'
-             FIXME on appveyor: see https://ci.appveyor.com/project/Byron/gitpython/build/1.0.185
-                """,
+        """
+        E   PermissionError:
+        [WinError 32] The process cannot access the file because it is being used by another process:
+        'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module'
+        -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module_moved'
+        """  # noqa: E501,
     )
     @with_rw_directory
     @_patch_git_config("protocol.file.allow", "always")

From 8704d1b81ba080cd4baa74968ac4cf84a84e4cbe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 08:22:32 -0400
Subject: [PATCH 093/994] Change test_git_submodules_and_add_sm_with_new_commit
 Windows skip to xfail

And improve details.

The xfail is only for native Windows, not Cygwin (same as the old
skip was, and still via checking HIDE_WINDOWS_KNOWN_ERRORS).

This change is analogous to the change in c1798f5, but for
test_git_submodules_and_add_sm_with_new_commit rather than
test_root_module.
---
 test/test_submodule.py | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 25b0daa01..72d7255b0 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -7,7 +7,7 @@
 import tempfile
 from pathlib import Path
 import sys
-from unittest import mock, skipIf, skipUnless
+from unittest import mock, skipUnless
 
 import pytest
 
@@ -748,14 +748,13 @@ def test_list_only_valid_submodules(self, rwdir):
         repo = git.Repo(repo_path)
         assert len(repo.submodules) == 0
 
-    @skipIf(
+    @pytest.mark.xfail(
         HIDE_WINDOWS_KNOWN_ERRORS,
-        """
-        E   PermissionError:
-        [WinError 32] The process cannot access the file because it is being used by another process:
-        'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module'
-        -> 'C:\\Users\\ek\\AppData\\Local\\Temp\\test_git_submodules_and_add_sm_with_new_commitu6d08von\\parent\\module_moved'
-        """  # noqa: E501,
+        reason=(
+            '"The process cannot access the file because it is being used by another process"'
+            + " on first call to sm.move"
+        ),
+        raises=PermissionError,
     )
     @with_rw_directory
     @_patch_git_config("protocol.file.allow", "always")

From 1d6abdca134082bf0bce2e590a52d8c08bf04d9b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 08:34:54 -0400
Subject: [PATCH 094/994] Run the tests in test_tree on Windows

This stops skipping them, as they are now working.
---
 test/test_tree.py | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/test/test_tree.py b/test/test_tree.py
index e59705645..c5ac8d539 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -5,24 +5,14 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
 from io import BytesIO
-from unittest import skipIf
 
 from git.objects import Tree, Blob
 from test.lib import TestBase
-from git.util import HIDE_WINDOWS_KNOWN_ERRORS
 
 import os.path as osp
 
 
 class TestTree(TestBase):
-    @skipIf(
-        HIDE_WINDOWS_KNOWN_ERRORS,
-        """
-        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 cat-file --batch-check""",
-    )
     def test_serializable(self):
         # tree at the given commit contains a submodule as well
         roottree = self.rorepo.tree("6c1faef799095f3990e9970bc2cb10aa0221cf9c")
@@ -51,14 +41,6 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
-    @skipIf(
-        HIDE_WINDOWS_KNOWN_ERRORS,
-        """
-        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 cat-file --batch-check""",
-    )
     def test_traverse(self):
         root = self.rorepo.tree("0.1.6")
         num_recursive = 0

From 5609faa5a1c22654dfc007f7bf229e1b08087aa8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 09:01:31 -0400
Subject: [PATCH 095/994] Add missing raises keyword for test_depth xfail

I had forgotten to do this earlier when converting from skip to
xfail. Besides consistency with the other uses of xfail in the test
suite, the benefit of passing "raises" is that pytest checks that
the failure gave the expected exception and makes it a non-expected
failure if it didn't.
---
 test/test_submodule.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 72d7255b0..79ff2c5f2 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1049,6 +1049,7 @@ class Repo(object):
 
     @pytest.mark.xfail(
         reason="for some unknown reason the assertion fails, even though it in fact is working in more common setup",
+        raises=AssertionError,
     )
     @with_rw_directory
     def test_depth(self, rwdir):

From ed95e8e72f6aa697c1ec69595335168e717da013 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 09:07:53 -0400
Subject: [PATCH 096/994] Consolidate test_repo module import statements

---
 test/test_repo.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index 4257d8a47..364b895fb 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -41,10 +41,8 @@
     UnsafeProtocolError,
 )
 from git.repo.fun import touch
-from test.lib import TestBase, with_rw_repo, fixture
-from git.util import cygpath
-from test.lib import with_rw_directory
-from git.util import join_path_native, rmtree, rmfile, bin_to_hex
+from git.util import bin_to_hex, cygpath, join_path_native, rmfile, rmtree
+from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo
 
 import os.path as osp
 

From ceb4dd3a7e89ac281a6c0d4d815fc13c00cbdf9f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 10:33:23 -0400
Subject: [PATCH 097/994] Show more CI system information

---
 .github/workflows/cygwin-test.yml   | 1 +
 .github/workflows/pythonpackage.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 337c0a809..d0be6bc39 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -55,6 +55,7 @@ jobs:
 
     - name: Show version and platform information
       run: |
+        /usr/bin/uname -a
         /usr/bin/git version
         /usr/bin/python --version
         /usr/bin/python -c 'import sys; print(sys.platform)'
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 392c894bc..0dc9d217a 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -63,6 +63,7 @@ jobs:
 
     - name: Show version and platform information
       run: |
+        uname -a
         git version
         python --version
         python -c 'import sys; print(sys.platform)'

From 3276aac711186a5dd0bd74ba1be8bb6f4ad3d03a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 10:49:50 -0400
Subject: [PATCH 098/994] Use Cygwin's bash and git for more CI steps

---
 .github/workflows/cygwin-test.yml   | 34 +++++++++++++++++------------
 .github/workflows/pythonpackage.yml |  1 +
 2 files changed, 21 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index d0be6bc39..2269df0da 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -14,11 +14,13 @@ jobs:
       TEMP: "/tmp"
     defaults:
       run:
-        shell: bash.exe --noprofile --norc -exo pipefail -o igncr "{0}"
+        shell: C:\cygwin\bin\bash.exe --noprofile --norc -exo pipefail -o igncr "{0}"
 
     steps:
     - name: Force LF line endings
-      run: git config --global core.autocrlf input
+      run: |
+        git config --global core.autocrlf input
+      shell: bash
 
     - uses: actions/checkout@v4
       with:
@@ -29,9 +31,12 @@ jobs:
       with:
         packages: python39 python39-pip python39-virtualenv git
 
+    - name: Limit $PATH to Cygwin
+      run: echo 'C:\cygwin\bin' >"$GITHUB_PATH"
+
     - name: Tell git to trust this repo
       run: |
-        /usr/bin/git config --global --add safe.directory "$(pwd)"
+        git config --global --add safe.directory "$(pwd)"
 
     - name: Prepare this repo for tests
       run: |
@@ -39,30 +44,31 @@ jobs:
 
     - name: Further prepare git configuration for tests
       run: |
-        /usr/bin/git config --global user.email "travis@ci.com"
-        /usr/bin/git config --global user.name "Travis Runner"
+        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: |
-        /usr/bin/python -m pip install --upgrade pip setuptools wheel
+        python -m pip install --upgrade pip setuptools wheel
 
     - name: Install project and test dependencies
       run: |
-        /usr/bin/python -m pip install ".[test]"
+        python -m pip install ".[test]"
 
     - name: Show version and platform information
       run: |
-        /usr/bin/uname -a
-        /usr/bin/git version
-        /usr/bin/python --version
-        /usr/bin/python -c 'import sys; print(sys.platform)'
-        /usr/bin/python -c 'import os; print(os.name)'
-        /usr/bin/python -c 'import git; print(git.compat.is_win)'
+        uname -a
+        command -v git python
+        git version
+        python --version
+        python -c 'import sys; print(sys.platform)'
+        python -c 'import os; print(os.name)'
+        python -c 'import git; print(git.compat.is_win)'
 
     - name: Test with pytest
       run: |
         set +x
-        /usr/bin/python -m pytest --color=yes -p no:sugar --instafail -vv
+        python -m pytest --color=yes -p no:sugar --instafail -vv
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 0dc9d217a..78d3ddf86 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -64,6 +64,7 @@ jobs:
     - name: Show version and platform information
       run: |
         uname -a
+        command -v git python
         git version
         python --version
         python -c 'import sys; print(sys.platform)'

From 5d4097654c6498d56defcc98dc1611fbfba9b75a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 12:02:35 -0400
Subject: [PATCH 099/994] Try to work in all LF on Cygwin CI

+ Style tweak and comment to clarify the "Limit $PATH" step.
---
 .github/workflows/cygwin-test.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 2269df0da..b8f3efbba 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -9,7 +9,6 @@ jobs:
       fail-fast: false
     env:
       CHERE_INVOKING: 1
-      SHELLOPTS: igncr
       TMP: "/tmp"
       TEMP: "/tmp"
     defaults:
@@ -19,7 +18,7 @@ jobs:
     steps:
     - name: Force LF line endings
       run: |
-        git config --global core.autocrlf input
+        git config --global core.autocrlf false  # Affects the non-Cygwin git.
       shell: bash
 
     - uses: actions/checkout@v4
@@ -32,11 +31,13 @@ jobs:
         packages: python39 python39-pip python39-virtualenv git
 
     - name: Limit $PATH to Cygwin
-      run: echo 'C:\cygwin\bin' >"$GITHUB_PATH"
+      run: |
+        echo 'C:\cygwin\bin' > "$GITHUB_PATH"  # Overwrite it with just this.
 
-    - name: Tell git to trust this repo
+    - name: Special configuration for Cygwin's git
       run: |
         git config --global --add safe.directory "$(pwd)"
+        git config --global core.autocrlf false
 
     - name: Prepare this repo for tests
       run: |
@@ -70,5 +71,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        set +x
         python -m pytest --color=yes -p no:sugar --instafail -vv

From dda428640113d6a2c30225ea33f1387e784b7289 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Sep 2023 13:42:29 -0400
Subject: [PATCH 100/994] Consistent formatting style across all workflows

---
 .github/workflows/cygwin-test.yml   |  3 +++
 .github/workflows/lint.yml          | 12 +++++++-----
 .github/workflows/pythonpackage.yml |  3 ++-
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index b8f3efbba..3cca4dd5f 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -5,12 +5,15 @@ on: [push, pull_request, workflow_dispatch]
 jobs:
   build:
     runs-on: windows-latest
+
     strategy:
       fail-fast: false
+
     env:
       CHERE_INVOKING: 1
       TMP: "/tmp"
       TEMP: "/tmp"
+
     defaults:
       run:
         shell: C:\cygwin\bin\bash.exe --noprofile --norc -exo pipefail -o igncr "{0}"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 5e79664a8..2204bb792 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,8 +7,10 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
-        with:
-          python-version: "3.x"
-      - uses: pre-commit/action@v3.0.0
+    - uses: actions/checkout@v4
+
+    - uses: actions/setup-python@v4
+      with:
+        python-version: "3.x"
+
+    - uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 78d3ddf86..e9ccdd566 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -10,8 +10,8 @@ permissions:
 
 jobs:
   build:
-
     runs-on: ubuntu-latest
+
     strategy:
       fail-fast: false
       matrix:
@@ -20,6 +20,7 @@ jobs:
           - experimental: false
           - python-version: "3.12"
             experimental: true
+
     defaults:
       run:
         shell: /bin/bash --noprofile --norc -exo pipefail {0}

From 3007abc6d229bcfe6643963f648597b7e231ab3d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Sep 2023 00:01:32 -0400
Subject: [PATCH 101/994] Remove the recently added "Limit $PATH" step

I had put that step in the Cygwin workflow for purposes of
experimentation, and it seemed to make clearer what is going on,
but really it does the opposite: it's deceptive because Cygwin uses
other logic to set its PATH. So this step is unnecessary and
ineffective at doing what it appears to do.
---
 .github/workflows/cygwin-test.yml | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 3cca4dd5f..35e9fde72 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -33,10 +33,6 @@ jobs:
       with:
         packages: python39 python39-pip python39-virtualenv git
 
-    - name: Limit $PATH to Cygwin
-      run: |
-        echo 'C:\cygwin\bin' > "$GITHUB_PATH"  # Overwrite it with just this.
-
     - name: Special configuration for Cygwin's git
       run: |
         git config --global --add safe.directory "$(pwd)"

From 4860f701b96dc07ac7c489c74c06cae069ae3cd1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Sep 2023 00:14:29 -0400
Subject: [PATCH 102/994] Further reduce differences between test workflows

This makes the two CI test workflows more similar in a couple of
the remaining ways they differ unnecessarily.

This could be extended, and otherwise improved upon, in the future.
---
 .github/workflows/cygwin-test.yml   |  5 +++--
 .github/workflows/pythonpackage.yml | 10 +++-------
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 35e9fde72..e818803f1 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -42,7 +42,7 @@ jobs:
       run: |
         TRAVIS=yes ./init-tests-after-clone.sh
 
-    - name: Further prepare git configuration for tests
+    - name: Set git user identity and command aliases for the tests
       run: |
         git config --global user.email "travis@ci.com"
         git config --global user.name "Travis Runner"
@@ -52,7 +52,8 @@ jobs:
 
     - name: Update PyPA packages
       run: |
-        python -m pip install --upgrade pip setuptools wheel
+        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
+        python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index e9ccdd566..1b049ba02 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -41,7 +41,7 @@ jobs:
       run: |
         TRAVIS=yes ./init-tests-after-clone.sh
 
-    - name: Prepare git configuration for tests
+    - name: Set git user identity and command aliases for the tests
       run: |
         git config --global user.email "travis@ci.com"
         git config --global user.name "Travis Runner"
@@ -51,12 +51,8 @@ jobs:
 
     - name: Update PyPA packages
       run: |
-        python -m pip install --upgrade pip wheel
-
-        # Python prior to 3.12 ships setuptools. Upgrade it if present.
-        if pip freeze --all | grep --quiet '^setuptools=='; then
-            python -m pip install --upgrade setuptools
-        fi
+        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
+        python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel
 
     - name: Install project and test dependencies
       run: |

From 13b859745de3f5a610e9e6390d55da40c582e663 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Sep 2023 23:41:02 -0400
Subject: [PATCH 103/994] Fix new link to license in readme

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index dbec36024..c7c2992e3 100644
--- a/README.md
+++ b/README.md
@@ -267,7 +267,7 @@ gpg --edit-key 4C08421980C9
 
 ### LICENSE
 
-[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file](https://github.com/gitpython-developers/GitPython/blob/main/license).
+[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file][license].
 
 [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
-[license]: https://github.com/gitpython-developers/GitPython/blob/main/license
+[license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE

From 388c7d1d70c2dabe1fe5e5eb1d453edd8f40a49d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 00:05:08 -0400
Subject: [PATCH 104/994] Clearer YAML style for flake8 extra plugin list

---
 .pre-commit-config.yaml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5a34b8af0..67aefb342 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,11 +4,9 @@ repos:
     hooks:
       - id: flake8
         additional_dependencies:
-          [
-            flake8-bugbear==23.9.16,
-            flake8-comprehensions==3.14.0,
-            flake8-typing-imports==1.14.0,
-          ]
+          - flake8-bugbear==23.9.16
+          - flake8-comprehensions==3.14.0
+          - flake8-typing-imports==1.14.0
         exclude: ^doc|^git/ext/
 
   - repo: https://github.com/pre-commit/pre-commit-hooks

From 1a8f210a42c3f9c46ce3cfa9c25ea39b0a8ca6c6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 02:40:11 -0400
Subject: [PATCH 105/994] Drop flake8 suppressions that are no longer needed

+ Remove the comments that documented those old suppressions
+ Format the .flake8 file more readably
---
 .flake8 | 26 +++++++-------------------
 1 file changed, 7 insertions(+), 19 deletions(-)

diff --git a/.flake8 b/.flake8
index ed5d036bf..1cc049a69 100644
--- a/.flake8
+++ b/.flake8
@@ -1,38 +1,26 @@
 [flake8]
+
 show-source = True
-count= True
+count = True
 statistics = True
-# E265 = comment blocks like @{ section, which it can't handle
+
 # E266 = too many leading '#' for block comment
 # E731 = do not assign a lambda expression, use a def
-# W293 = Blank line contains whitespace
-# W504 = Line break after operator
-# E704 = multiple statements in one line  - used for @override
 # TC002 = move third party import to TYPE_CHECKING
-# ANN = flake8-annotations
 # TC, TC2 = flake8-type-checking
-# D = flake8-docstrings
 
 # select = C,E,F,W ANN, TC, TC2  # to enable code. Disabled if not listed, including builtin codes
 enable-extensions = TC, TC2  # only needed for extensions not enabled by default
 
-ignore = E265,E266,E731,E704,
-         W293, W504,
-         ANN0 ANN1 ANN2,
-         TC002,
-         TC0, TC1, TC2
-         # B,
-         A,
-         D,
-         RST, RST3
+ignore = E266, E731
 
-exclude = .tox,.venv,build,dist,doc,git/ext/
+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
+    attr, class, func, meth, mod, obj, ref, term, var  # used by sphinx
 
 min-python-version = 3.7.0
 
 # for `black` compatibility
 max-line-length = 120
-extend-ignore = E203,W503
+extend-ignore = E203, W503

From e07d91a6f67c1c4adb897096cb13d6cd1b98cf42 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 1 Oct 2023 15:31:00 -0400
Subject: [PATCH 106/994] Drop claim about Cygwin not having git-daemon

On a current Cygwin system with git 2.39.0 (the latest version
offered by the Cygwin package manager), git-daemon is present, with
the Cygwin path /usr/libexec/git-core/git-daemon.exe.

In addition, the cygwin-test.yml workflow does not take any special
steps to allow git-daemon to work, but all tests pass in it even
without skipping or xfailing tests that seem related to git-daemon.

The git_daemon_launched function in test/lib/helper.py only invokes
git-daemon directly (rather than through "git daemon") when is_win
evaluates true, which only happens on native Windows systems, not
Cygwin, which is treated the same as (other) Unix-like systems and
still works. So maybe Cygwin git-daemon was never a special case.

Whether or not it was, the message about git-daemon needing to be
findable in a PATH search is also under an is_win check, and thus
is never shown on Cygwin. So I've removed the Cygwin part of that
message. (Because the path shown is a MinGW-style path, I have kept
the wording about that being for MinGW-git, even though it is no
longer needed to disambiguate the Cygwin case.)
---
 README.md          |  3 +--
 test/lib/helper.py | 15 ++++++---------
 2 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index c7c2992e3..65c1e7bae 100644
--- a/README.md
+++ b/README.md
@@ -119,8 +119,7 @@ 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
-with MINGW's.
+exists in `Git\mingw64\libexec\git-core\`.
 
 #### Install test dependencies
 
diff --git a/test/lib/helper.py b/test/lib/helper.py
index e8464b7d4..9656345de 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -177,12 +177,10 @@ def git_daemon_launched(base_path, ip, port):
     gd = None
     try:
         if is_win:
-            ## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
-            #  but if invoked as 'git daemon', it detaches from parent `git` cmd,
-            #  and then CANNOT DIE!
-            #  So, invoke it as a single command.
-            ## Cygwin-git has no daemon.  But it can use MINGW's.
-            #
+            # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\,
+            # but if invoked as 'git daemon', it detaches from parent `git` cmd,
+            # and then CANNOT DIE!
+            # So, invoke it as a single command.
             daemon_cmd = [
                 "git-daemon",
                 "--enable=receive-pack",
@@ -217,12 +215,11 @@ def git_daemon_launched(base_path, ip, port):
         )
         if is_win:
             msg += textwrap.dedent(
-                r"""
+                R"""
 
             On Windows,
               the `git-daemon.exe` must be in PATH.
-              For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear.
-              CYGWIN has no daemon, but if one exists, it gets along fine (but has also paths problems)."""
+              For MINGW, look into \Git\mingw64\libexec\git-core\, but problems with paths might appear."""
             )
         log.warning(msg, ex, ip, port, base_path, base_path, exc_info=1)
 

From 35e3875cb71913771885646f294cd25cd19c35f3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 1 Oct 2023 17:06:47 -0400
Subject: [PATCH 107/994] Allow base_daemon_path to be normalized for Cygwin

Since the Cygwin git-daemon can be used.
---
 test/lib/helper.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 9656345de..1de904610 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -302,7 +302,7 @@ def remote_repo_creator(self):
                     cw.set("url", remote_repo_url)
 
                 with git_daemon_launched(
-                    Git.polish_url(base_daemon_path, is_cygwin=False),  # No daemon in Cygwin.
+                    Git.polish_url(base_daemon_path),
                     "127.0.0.1",
                     GIT_DAEMON_PORT,
                 ):

From 5e71c270b2ea0adfc5c0a103fce33ab6acf275b1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 1 Oct 2023 22:43:36 -0400
Subject: [PATCH 108/994] Fix the name of the "executes git" test

That test is not testing whether or not a shell is used (nor does
it need to test that), but just whether the library actually runs
git, passes an argument to it, and returns text from its stdout.
---
 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 481309538..a21a3c78e 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -73,7 +73,7 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         res = self.git.transform_kwargs(**{"s": True, "t": True})
         self.assertEqual({"-s", "-t"}, set(res))
 
-    def test_it_executes_git_to_shell_and_returns_result(self):
+    def test_it_executes_git_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):

From 59440607406873a28788ca38526871749c5549f9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 00:17:05 -0400
Subject: [PATCH 109/994] Test whether a shell is used

In the Popen calls in Git.execute, for all combinations of allowed
values for Git.USE_SHELL and the shell= keyword argument.
---
 test/test_git.py | 35 +++++++++++++++++++++++++++++------
 1 file changed, 29 insertions(+), 6 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index a21a3c78e..6fd2c8268 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -4,23 +4,24 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+import contextlib
 import os
+import os.path as osp
 import shutil
 import subprocess
 import sys
 from tempfile import TemporaryDirectory, TemporaryFile
 from unittest import mock, skipUnless
 
-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 cwd, finalize_process
-
-import os.path as osp
+import ddt
 
+from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
 from git.compat import is_win
+from git.util import cwd, finalize_process
+from test.lib import TestBase, fixture_path, with_rw_directory
 
 
+@ddt.ddt
 class TestGit(TestBase):
     @classmethod
     def setUpClass(cls):
@@ -73,6 +74,28 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         res = self.git.transform_kwargs(**{"s": True, "t": True})
         self.assertEqual({"-s", "-t"}, set(res))
 
+    @ddt.data(
+        (None, False, False),
+        (None, True, True),
+        (False, True, False),
+        (False, False, False),
+        (True, False, True),
+        (True, True, True),
+    )
+    @mock.patch.object(cmd, "Popen", wraps=cmd.Popen)  # Since it is gotten via a "from" import.
+    def test_it_uses_shell_or_not_as_specified(self, case, mock_popen):
+        """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`."""
+        value_in_call, value_from_class, expected_popen_arg = case
+        # FIXME: Check what gets logged too!
+        with mock.patch.object(Git, "USE_SHELL", value_from_class):
+            with contextlib.suppress(GitCommandError):
+                self.git.execute(
+                    "git",  # No args, so it runs with or without a shell, on all OSes.
+                    shell=value_in_call,
+                )
+        mock_popen.assert_called_once()
+        self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg)
+
     def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
 

From aa5e2f6b24e36d2d38e84e7f2241b104318396c3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 01:45:17 -0400
Subject: [PATCH 110/994] Test if whether a shell is used is logged

The log message shows "Popen(...)", not "execute(...)". So it
should faithfully report what is about to be passed to Popen in
cases where a user reading the log would otherwise be misled into
thinking this is what has happened.

Reporting the actual "shell=" argument passed to Popen is also more
useful to log than the argument passed to Git.execute (at least if
only one of them is to be logged).
---
 test/test_git.py | 29 +++++++++++++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 6fd2c8268..389e80552 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -5,8 +5,10 @@
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 import contextlib
+import logging
 import os
 import os.path as osp
+import re
 import shutil
 import subprocess
 import sys
@@ -74,7 +76,8 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         res = self.git.transform_kwargs(**{"s": True, "t": True})
         self.assertEqual({"-s", "-t"}, set(res))
 
-    @ddt.data(
+    _shell_cases = (
+        # value_in_call, value_from_class, expected_popen_arg
         (None, False, False),
         (None, True, True),
         (False, True, False),
@@ -82,20 +85,42 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         (True, False, True),
         (True, True, True),
     )
+
+    @ddt.idata(_shell_cases)
     @mock.patch.object(cmd, "Popen", wraps=cmd.Popen)  # Since it is gotten via a "from" import.
     def test_it_uses_shell_or_not_as_specified(self, case, mock_popen):
         """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`."""
         value_in_call, value_from_class, expected_popen_arg = case
-        # FIXME: Check what gets logged too!
+
         with mock.patch.object(Git, "USE_SHELL", value_from_class):
             with contextlib.suppress(GitCommandError):
                 self.git.execute(
                     "git",  # No args, so it runs with or without a shell, on all OSes.
                     shell=value_in_call,
                 )
+
         mock_popen.assert_called_once()
         self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg)
 
+    @ddt.idata(full_case[:2] for full_case in _shell_cases)
+    @mock.patch.object(cmd, "Popen", wraps=cmd.Popen)  # Since it is gotten via a "from" import.
+    def test_it_logs_if_it_uses_a_shell(self, case, mock_popen):
+        """``shell=`` in the log message agrees with what is passed to `Popen`."""
+        value_in_call, value_from_class = case
+
+        with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
+            with mock.patch.object(Git, "USE_SHELL", value_from_class):
+                with contextlib.suppress(GitCommandError):
+                    self.git.execute(
+                        "git",  # No args, so it runs with or without a shell, on all OSes.
+                        shell=value_in_call,
+                    )
+
+        popen_shell_arg = mock_popen.call_args.kwargs["shell"]
+        expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)")
+        match_attempts = [expected_message.fullmatch(message) for message in log_watcher.output]
+        self.assertTrue(any(match_attempts), repr(log_watcher.output))
+
     def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
 

From 0f19fb0be1bd3ccd3ff8f35dba9e924c9d379e41 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 02:21:10 -0400
Subject: [PATCH 111/994] Fix tests so they don't try to run "g"

Both new shell-related tests were causing the code under test to
split "git" into ["g", "i", "t"] and thus causing the code under
test to attempt to execute a "g" command. This passes the command
as a one-element list of strings rather than as a string, which
avoids this on all operating systems regardless of whether the code
under test has a bug being tested for.

This would not occur on Windows, which does not iterate commands of
type str character-by-character even when the command is run
without a shell. But it did happen on other systems.

Most of the time, the benefit of using a command that actually runs
"git" rather than "g" is the avoidance of confusion in the error
message. But this is also important because it is possible for the
user who runs the tests to have a "g" command, in which case it
could be very inconvenient, or even unsafe, to run "g". This should
be avoided even when the code under test has a bug that causes a
shell to be used when it shouldn't or not used when it should, so
having separate commands (list and str) per test case parameters
would not be a sufficient solution: it would only guard against
running "g" when a bug in the code under test were absent.
---
 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 389e80552..7e8e7c805 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -95,7 +95,7 @@ def test_it_uses_shell_or_not_as_specified(self, case, mock_popen):
         with mock.patch.object(Git, "USE_SHELL", value_from_class):
             with contextlib.suppress(GitCommandError):
                 self.git.execute(
-                    "git",  # No args, so it runs with or without a shell, on all OSes.
+                    ["git"],  # No args, so it runs with or without a shell, on all OSes.
                     shell=value_in_call,
                 )
 
@@ -112,7 +112,7 @@ def test_it_logs_if_it_uses_a_shell(self, case, mock_popen):
             with mock.patch.object(Git, "USE_SHELL", value_from_class):
                 with contextlib.suppress(GitCommandError):
                     self.git.execute(
-                        "git",  # No args, so it runs with or without a shell, on all OSes.
+                        ["git"],  # No args, so it runs with or without a shell, on all OSes.
                         shell=value_in_call,
                     )
 

From da3460c6cc3a7c6981dfd1d4675d167a7a5f2b0c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 02:56:37 -0400
Subject: [PATCH 112/994] Extract shared test logic to a helper

This also helps mock Popen over a smaller scope, which may be
beneficial (especially if it is mocked in the subprocess module,
rather than the git.cmd module, in the future).
---
 test/test_git.py | 34 ++++++++++++++++------------------
 1 file changed, 16 insertions(+), 18 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 7e8e7c805..343bf7a19 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -86,35 +86,33 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         (True, True, True),
     )
 
+    def _do_shell_combo(self, value_in_call, value_from_class):
+        with mock.patch.object(Git, "USE_SHELL", value_from_class):
+            # git.cmd gets Popen via a "from" import, so patch it there.
+            with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen:
+                # Use a command with no arguments (besides the program name), so it runs
+                # with or without a shell, on all OSes, with the same effect. Since git
+                # errors out when run with no arguments, we swallow that error.
+                with contextlib.suppress(GitCommandError):
+                    self.git.execute(["git"], shell=value_in_call)
+
+        return mock_popen
+
     @ddt.idata(_shell_cases)
-    @mock.patch.object(cmd, "Popen", wraps=cmd.Popen)  # Since it is gotten via a "from" import.
-    def test_it_uses_shell_or_not_as_specified(self, case, mock_popen):
+    def test_it_uses_shell_or_not_as_specified(self, case):
         """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`."""
         value_in_call, value_from_class, expected_popen_arg = case
-
-        with mock.patch.object(Git, "USE_SHELL", value_from_class):
-            with contextlib.suppress(GitCommandError):
-                self.git.execute(
-                    ["git"],  # No args, so it runs with or without a shell, on all OSes.
-                    shell=value_in_call,
-                )
-
+        mock_popen = self._do_shell_combo(value_in_call, value_from_class)
         mock_popen.assert_called_once()
         self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg)
 
     @ddt.idata(full_case[:2] for full_case in _shell_cases)
-    @mock.patch.object(cmd, "Popen", wraps=cmd.Popen)  # Since it is gotten via a "from" import.
-    def test_it_logs_if_it_uses_a_shell(self, case, mock_popen):
+    def test_it_logs_if_it_uses_a_shell(self, case):
         """``shell=`` in the log message agrees with what is passed to `Popen`."""
         value_in_call, value_from_class = case
 
         with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
-            with mock.patch.object(Git, "USE_SHELL", value_from_class):
-                with contextlib.suppress(GitCommandError):
-                    self.git.execute(
-                        ["git"],  # No args, so it runs with or without a shell, on all OSes.
-                        shell=value_in_call,
-                    )
+            mock_popen = self._do_shell_combo(value_in_call, value_from_class)
 
         popen_shell_arg = mock_popen.call_args.kwargs["shell"]
         expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)")

From 41294d578471f7f63c02bf59e8abc3459f9d6390 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 03:26:02 -0400
Subject: [PATCH 113/994] Use the mock backport on Python 3.7

Because mock.call.kwargs, i.e. the ability to examine
m.call_args.kwargs where m is a Mock or MagicMock, was introduced
in Python 3.8.

Currently it is only in test/test_git.py that any use of mocks
requires this, so I've put the conditional import logic to import
mock (the top-level package) rather than unittest.mock only there.

The mock library is added as a development (testing) dependency
only when the Python version is lower than 3.8, so it is not
installed when not needed.

This fixes a problem in the new tests of whether a shell is used,
and reported as used, in the Popen call in Git.execute. Those
just-introduced tests need this, to be able to use
mock_popen.call_args.kwargs on Python 3.7.
---
 test-requirements.txt | 3 ++-
 test/test_git.py      | 7 ++++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index 1c08c736f..9414da09c 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,7 @@
 black
 coverage[toml]
-ddt>=1.1.1, !=1.4.3
+ddt >= 1.1.1, != 1.4.3
+mock ; python_version < "3.8"
 mypy
 pre-commit
 pytest
diff --git a/test/test_git.py b/test/test_git.py
index 343bf7a19..583c74fa3 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -13,7 +13,12 @@
 import subprocess
 import sys
 from tempfile import TemporaryDirectory, TemporaryFile
-from unittest import mock, skipUnless
+from unittest import skipUnless
+
+if sys.version_info >= (3, 8):
+    from unittest import mock
+else:
+    import mock  # To be able to examine call_args.kwargs on a mock.
 
 import ddt
 

From baf3457ec8f92c64d2481b812107e6acc4059ddd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 2 Oct 2023 04:11:18 -0400
Subject: [PATCH 114/994] Fix Git.execute shell use and reporting bugs

This updates the shell variable itself, only when it is None, from
self.USE_SHELL.

(That attribute is usually Git.USE_SHELL rather than an instance
attribute. But although people probably shouldn't set it on
instances, people will expect that this is permitted.)

Now:

- USE_SHELL is used as a fallback only. When shell=False is passed,
  USE_SHELL is no longer consuled. Thus shell=False always keeps a
  shell from being used, even in the non-default case where the
  USE_SHELL attribue has been set to True.

- The debug message printed to the log shows the actual value that
  is being passed to Popen, because the updated shell variable is
  used both to produce that message and in the Popen call.
---
 git/cmd.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9921dd6c9..53b8b9050 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -974,6 +974,8 @@ def execute(
         istream_ok = "None"
         if istream:
             istream_ok = "<valid stream>"
+        if shell is None:
+            shell = self.USE_SHELL
         log.debug(
             "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)",
             redacted_command,
@@ -992,7 +994,7 @@ def execute(
                     stdin=istream or DEVNULL,
                     stderr=PIPE,
                     stdout=stdout_sink,
-                    shell=shell is not None and shell or self.USE_SHELL,
+                    shell=shell,
                     close_fds=is_posix,  # unsupported on windows
                     universal_newlines=universal_newlines,
                     creationflags=PROC_CREATIONFLAGS,

From b79198a880982e6768fec4d0ef244338420efbdc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 04:58:13 -0400
Subject: [PATCH 115/994] Document Git.execute parameters in definition order

- Reorder the items in the git.cmd.Git.execute docstring that
  document its parameters, to be in the same order the parameters
  are actually defined in.

- Use consistent spacing, by having a blank line between successive
  items that document parameters. Before, most of them were
  separated this way, but some of them were not.

- Reorder the elements of execute_kwargs (which list all those
  parameters except the first parameter, command) to be listed in
  that order as well. These were mostly in order, but a couple were
  out of order. This is just about the order they appear in the
  definition, since sets in Python (unlike dicts) have no key order
  guarantees.
---
 git/cmd.py | 46 ++++++++++++++++++++++++++--------------------
 1 file changed, 26 insertions(+), 20 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 53b8b9050..4d772e909 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -66,10 +66,10 @@
     "with_extended_output",
     "with_exceptions",
     "as_process",
-    "stdout_as_string",
     "output_stream",
-    "with_stdout",
+    "stdout_as_string",
     "kill_after_timeout",
+    "with_stdout",
     "universal_newlines",
     "shell",
     "env",
@@ -883,6 +883,27 @@ def execute(
             decoded into a string using the default encoding (usually utf-8).
             The latter can fail, if the output contains binary data.
 
+        :param kill_after_timeout:
+            To specify a timeout in seconds for the git command, after which the process
+            should be killed. This will have no effect if as_process is set to True. It is
+            set to None by default and will let the process run until the timeout is
+            explicitly specified. This feature is not supported on Windows. It's also worth
+            noting that kill_after_timeout uses SIGKILL, which can have negative side
+            effects on a repository. For example, stale locks in case of git gc could
+            render the repository incapable of accepting changes until the lock is manually
+            removed.
+
+        :param with_stdout:
+            If True, default True, we open stdout on the created process
+
+        :param universal_newlines:
+            if True, pipes will be opened as text, and lines are split at
+            all known line endings.
+
+        :param shell:
+            Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
+            It overrides :attr:`USE_SHELL` if it is not `None`.
+
         :param env:
             A dictionary of environment variables to be passed to `subprocess.Popen`.
 
@@ -891,29 +912,14 @@ def execute(
             one invocation of write() method. If the given number is not positive then
             the default value is used.
 
+        :param strip_newline_in_stdout:
+            Whether to strip the trailing ``\\n`` of the command stdout.
+
         :param subprocess_kwargs:
             Keyword arguments to be passed to subprocess.Popen. Please note that
             some of the valid kwargs are already set by this method, the ones you
             specify may not be the same ones.
 
-        :param with_stdout: If True, default True, we open stdout on the created process
-        :param universal_newlines:
-            if True, pipes will be opened as text, and lines are split at
-            all known line endings.
-        :param shell:
-            Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
-            It overrides :attr:`USE_SHELL` if it is not `None`.
-        :param kill_after_timeout:
-            To specify a timeout in seconds for the git command, after which the process
-            should be killed. This will have no effect if as_process is set to True. It is
-            set to None by default and will let the process run until the timeout is
-            explicitly specified. This feature is not supported on Windows. It's also worth
-            noting that kill_after_timeout uses SIGKILL, which can have negative side
-            effects on a repository. For example, stale locks in case of git gc could
-            render the repository incapable of accepting changes until the lock is manually
-            removed.
-        :param strip_newline_in_stdout:
-            Whether to strip the trailing ``\\n`` of the command stdout.
         :return:
             * str(output) if extended_output = False (Default)
             * tuple(int(status), str(stdout), str(stderr)) if extended_output = True

From 13e1593fc6a3c218451e29dd6b4a58b3a44afee3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 05:30:22 -0400
Subject: [PATCH 116/994] Don't say Git.execute uses a shell, in its summary

The top line of the Git.execute docstring said that it used a
shell, which is not necessarily the case (and is not usually the
case, since the default is not to use one). This removes that
claim while keeping the top-line wording otherwise the same.

It also rephrases the description of the command parameter, in a
way that does not change its meaning but reflects the more common
practice of passing a sequence of arguments (since portable calls
that do not use a shell must do that).
---
 git/cmd.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 4d772e909..e324db7e2 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -842,12 +842,12 @@ def execute(
         strip_newline_in_stdout: bool = True,
         **subprocess_kwargs: Any,
     ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
-        """Handles executing the command on the shell and consumes and returns
-        the returned information (stdout)
+        """Handles executing the command and consumes and returns the returned
+        information (stdout)
 
         :param command:
             The command argument list to execute.
-            It should be a string, or a sequence of program arguments. The
+            It should be a sequence of program arguments, or a string. The
             program to execute is the first item in the args sequence or string.
 
         :param istream:

From 74b68e9b621a729a3407b2020b0a48d7921fb1e9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 05:55:54 -0400
Subject: [PATCH 117/994] Copyedit Git.execute docstring

These are some small clarity and consistency revisions to the
docstring of git.cmd.Git.execute that didn't specifically fit in
the narrow topics of either of the preceding two commits.
---
 git/cmd.py | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index e324db7e2..f20cd77af 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -851,7 +851,7 @@ def execute(
             program to execute is the first item in the args sequence or string.
 
         :param istream:
-            Standard input filehandle passed to subprocess.Popen.
+            Standard input filehandle passed to `subprocess.Popen`.
 
         :param with_extended_output:
             Whether to return a (status, stdout, stderr) tuple.
@@ -862,8 +862,7 @@ def execute(
         :param as_process:
             Whether to return the created process instance directly from which
             streams can be read on demand. This will render with_extended_output and
-            with_exceptions ineffective - the caller will have
-            to deal with the details himself.
+            with_exceptions ineffective - the caller will have to deal with the details.
             It is important to note that the process will be placed into an AutoInterrupt
             wrapper that will interrupt the process once it goes out of scope. If you
             use the command in iterators, you should pass the whole process instance
@@ -876,25 +875,25 @@ def execute(
             always be created with a pipe due to issues with subprocess.
             This merely is a workaround as data will be copied from the
             output pipe to the given output stream directly.
-            Judging from the implementation, you shouldn't use this flag !
+            Judging from the implementation, you shouldn't use this parameter!
 
         :param stdout_as_string:
-            if False, the commands standard output will be bytes. Otherwise, it will be
-            decoded into a string using the default encoding (usually utf-8).
+            If False, the command's standard output will be bytes. Otherwise, it will be
+            decoded into a string using the default encoding (usually UTF-8).
             The latter can fail, if the output contains binary data.
 
         :param kill_after_timeout:
-            To specify a timeout in seconds for the git command, after which the process
+            Specifies a timeout in seconds for the git command, after which the process
             should be killed. This will have no effect if as_process is set to True. It is
             set to None by default and will let the process run until the timeout is
             explicitly specified. This feature is not supported on Windows. It's also worth
             noting that kill_after_timeout uses SIGKILL, which can have negative side
-            effects on a repository. For example, stale locks in case of git gc could
+            effects on a repository. For example, stale locks in case of ``git gc`` could
             render the repository incapable of accepting changes until the lock is manually
             removed.
 
         :param with_stdout:
-            If True, default True, we open stdout on the created process
+            If True, default True, we open stdout on the created process.
 
         :param universal_newlines:
             if True, pipes will be opened as text, and lines are split at
@@ -916,19 +915,19 @@ def execute(
             Whether to strip the trailing ``\\n`` of the command stdout.
 
         :param subprocess_kwargs:
-            Keyword arguments to be passed to subprocess.Popen. Please note that
-            some of the valid kwargs are already set by this method, the ones you
+            Keyword arguments to be passed to `subprocess.Popen`. Please note that
+            some of the valid kwargs are already set by this method; the ones you
             specify may not be the same ones.
 
         :return:
             * str(output) if extended_output = False (Default)
             * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
 
-            if output_stream is True, the stdout value will be your output stream:
+            If output_stream is True, the stdout value will be your output stream:
             * output_stream if extended_output = False
             * tuple(int(status), output_stream, str(stderr)) if extended_output = True
 
-            Note git is executed with LC_MESSAGES="C" to ensure consistent
+            Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
             output regardless of system language.
 
         :raise GitCommandError:

From 133271bb3871baff3ed772fbdea8bc359f115fd6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 06:34:05 -0400
Subject: [PATCH 118/994] Other copyediting in the git.cmd module

(Not specific to git.cmd.Git.execute.)
---
 git/cmd.py | 43 ++++++++++++++++++++-----------------------
 1 file changed, 20 insertions(+), 23 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index f20cd77af..a6d287986 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -105,7 +105,7 @@ def handle_process_output(
 ) -> None:
     """Registers for notifications to learn that process output is ready to read, and dispatches lines to
     the respective line handlers.
-    This function returns once the finalizer returns
+    This function returns once the finalizer returns.
 
     :return: result of finalizer
     :param process: subprocess.Popen instance
@@ -294,9 +294,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
-        """This gets called by the refresh function (see the top level
-        __init__).
-        """
+        """This gets called by the refresh function (see the top level __init__)."""
         # discern which path to refresh with
         if path is not None:
             new_git = os.path.expanduser(path)
@@ -446,9 +444,9 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
         if is_cygwin:
             url = cygpath(url)
         else:
-            """Remove any backslahes from urls to be written in config files.
+            """Remove any backslashes from urls to be written in config files.
 
-            Windows might create config-files containing paths with backslashed,
+            Windows might create config files containing paths with backslashes,
             but git stops liking them as it will escape the backslashes.
             Hence we undo the escaping just to be sure.
             """
@@ -464,8 +462,8 @@ def check_unsafe_protocols(cls, url: str) -> None:
         Check for unsafe protocols.
 
         Apart from the usual protocols (http, git, ssh),
-        Git allows "remote helpers" that have the form `<transport>::<address>`,
-        one of these helpers (`ext::`) can be used to invoke any arbitrary command.
+        Git allows "remote helpers" that have the form ``<transport>::<address>``,
+        one of these helpers (``ext::``) can be used to invoke any arbitrary command.
 
         See:
 
@@ -517,7 +515,7 @@ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
             self.status: Union[int, None] = None
 
         def _terminate(self) -> None:
-            """Terminate the underlying process"""
+            """Terminate the underlying process."""
             if self.proc is None:
                 return
 
@@ -572,7 +570,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
             """Wait for the process and return its status code.
 
             :param stderr: Previously read value of stderr, in case stderr is already closed.
-            :warn: may deadlock if output or error pipes are used and not handled separately.
+            :warn: May deadlock if output or error pipes are used and not handled separately.
             :raise GitCommandError: if the return status is not 0"""
             if stderr is None:
                 stderr_b = b""
@@ -605,13 +603,12 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte
     # END auto interrupt
 
     class CatFileContentStream(object):
-
         """Object representing a sized read-only stream returning the contents of
         an object.
         It behaves like a stream, but counts the data read and simulates an empty
         stream once our sized content region is empty.
-        If not all data is read to the end of the objects's lifetime, we read the
-        rest to assure the underlying stream continues to work"""
+        If not all data is read to the end of the object's lifetime, we read the
+        rest to assure the underlying stream continues to work."""
 
         __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size")
 
@@ -740,11 +737,11 @@ def __getattr__(self, name: str) -> Any:
 
     def set_persistent_git_options(self, **kwargs: Any) -> None:
         """Specify command line options to the git executable
-        for subsequent subcommand calls
+        for subsequent subcommand calls.
 
         :param kwargs:
             is a dict of keyword arguments.
-            these arguments are passed as in _call_process
+            These arguments are passed as in _call_process
             but will be passed to the git command rather than
             the subcommand.
         """
@@ -775,7 +772,7 @@ def version_info(self) -> Tuple[int, int, int, int]:
         """
         :return: tuple(int, int, int, int) tuple with integers representing the major, minor
             and additional version numbers as parsed from git version.
-            This value is generated on demand and is cached"""
+            This value is generated on demand and is cached."""
         return self._version_info
 
     @overload
@@ -843,7 +840,7 @@ def execute(
         **subprocess_kwargs: Any,
     ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
         """Handles executing the command and consumes and returns the returned
-        information (stdout)
+        information (stdout).
 
         :param command:
             The command argument list to execute.
@@ -1213,7 +1210,7 @@ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
 
     def __call__(self, **kwargs: Any) -> "Git":
         """Specify command line options to the git executable
-        for a subcommand call
+        for a subcommand call.
 
         :param kwargs:
             is a dict of keyword arguments.
@@ -1251,7 +1248,7 @@ def _call_process(
         self, method: str, *args: Any, **kwargs: Any
     ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
         """Run the given git command with the specified arguments and return
-        the result as a String
+        the result as a string.
 
         :param method:
             is the command. Contained "_" characters will be converted to dashes,
@@ -1260,7 +1257,7 @@ def _call_process(
         :param args:
             is the list of arguments. If None is included, it will be pruned.
             This allows your commands to call git more conveniently as None
-            is realized as non-existent
+            is realized as non-existent.
 
         :param kwargs:
             It contains key-values for the following:
@@ -1390,7 +1387,7 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]:
         return self.__get_object_header(cmd, ref)
 
     def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
-        """As get_object_header, but returns object data as well
+        """As get_object_header, but returns object data as well.
 
         :return: (hexsha, type_string, size_as_int, data_string)
         :note: not threadsafe"""
@@ -1400,10 +1397,10 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
         return (hexsha, typename, size, data)
 
     def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
-        """As get_object_header, but returns the data as a stream
+        """As get_object_header, but returns the data as a stream.
 
         :return: (hexsha, type_string, size_as_int, stream)
-        :note: This method is not threadsafe, you need one independent Command instance per thread to be safe !"""
+        :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!"""
         cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
         hexsha, typename, size = self.__get_object_header(cmd, ref)
         cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()

From fc755dae6866b9c9e0aa347297b693fec2c5b415 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 06:38:54 -0400
Subject: [PATCH 119/994] Avoid having a local function seem to be a method

The kill_process local function defined in the Git.execute method
is a local function and not a method, but it was easy to misread as
a method for two reasons:

- Its docstring described it as a method.

- It was named with a leading underscore, as though it were a
  nonpublic method. But this name is a local variable, and local
  variables are always nonpublic (except when they are function
  parameters, in which case they are in a sense public). A leading
  underscore in a local variable name usually means the variable is
  unused in the function.

This fixes the docstring and drops the leading underscore from the
name. If this is ever extracted from the Git.execute method and
placed at class or module scope, then the name can be changed back.
---
 git/cmd.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index a6d287986..1d8c70f32 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1012,8 +1012,8 @@ def execute(
         if as_process:
             return self.AutoInterrupt(proc, command)
 
-        def _kill_process(pid: int) -> None:
-            """Callback method to kill a process."""
+        def kill_process(pid: int) -> None:
+            """Callback to kill a process."""
             p = Popen(
                 ["ps", "--ppid", str(pid)],
                 stdout=PIPE,
@@ -1046,7 +1046,7 @@ def _kill_process(pid: int) -> None:
 
         if kill_after_timeout is not None:
             kill_check = threading.Event()
-            watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,))
+            watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
 
         # Wait for the process to return
         status = 0

From 2d1efdca84e266a422f4298ee94ee9b8dae6c32e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 07:55:35 -0400
Subject: [PATCH 120/994] Test that git.cmd.execute_kwargs is correct

---
 test/test_git.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/test/test_git.py b/test/test_git.py
index 583c74fa3..723bf624b 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -5,6 +5,7 @@
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 import contextlib
+import inspect
 import logging
 import os
 import os.path as osp
@@ -364,3 +365,11 @@ def counter_stderr(line):
 
         self.assertEqual(count[1], line_count)
         self.assertEqual(count[2], line_count)
+
+    def test_execute_kwargs_set_agrees_with_method(self):
+        parameter_names = inspect.signature(cmd.Git.execute).parameters.keys()
+        self_param, command_param, *most_params, extra_kwargs_param = parameter_names
+        self.assertEqual(self_param, "self")
+        self.assertEqual(command_param, "command")
+        self.assertEqual(set(most_params), cmd.execute_kwargs)  # Most important.
+        self.assertEqual(extra_kwargs_param, "subprocess_kwargs")

From a8a43fe6f8d6f0a7f9067149859634d624406bb1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 08:50:17 -0400
Subject: [PATCH 121/994] Simplify shell test helper with with_exceptions=False

Instead of swallowing GitCommandError exceptions in the helper used
by test_it_uses_shell_or_not_as_specified and
test_it_logs_if_it_uses_a_shell, this modifies the helper so it
prevents Git.execute from raising the exception in the first place.
---
 test/test_git.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 723bf624b..8d269f38a 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: https://opensource.org/license/bsd-3-clause/
-import contextlib
 import inspect
 import logging
 import os
@@ -97,10 +96,8 @@ def _do_shell_combo(self, value_in_call, value_from_class):
             # git.cmd gets Popen via a "from" import, so patch it there.
             with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen:
                 # Use a command with no arguments (besides the program name), so it runs
-                # with or without a shell, on all OSes, with the same effect. Since git
-                # errors out when run with no arguments, we swallow that error.
-                with contextlib.suppress(GitCommandError):
-                    self.git.execute(["git"], shell=value_in_call)
+                # with or without a shell, on all OSes, with the same effect.
+                self.git.execute(["git"], with_exceptions=False, shell=value_in_call)
 
         return mock_popen
 

From 9fa1ceef2c47c9f58e10d8925cc166fdfd6b5183 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 09:45:45 -0400
Subject: [PATCH 122/994] Extract a _assert_logged_for_popen method

This extracts the logic of searching log messages, and asserting
that (at least) one matches a pattern for the report of a Popen
call with a given argument, from test_it_logs_if_it_uses_a_shell
into a new nonpublic test helper method _assert_logged_for_popen.

The extracted version is modified to make it slightly more general,
and slightly more robust. This is still not extremely robust: the
notation used to log Popen calls is informal, so it wouldn't make
sense to really parse it as code. But this no longer assumes that
the representation of a value ends at a word boundary, nor that the
value is free of regular expression metacharacters.
---
 test/test_git.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 8d269f38a..10b346e4d 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -40,6 +40,13 @@ def tearDown(self):
 
         gc.collect()
 
+    def _assert_logged_for_popen(self, log_watcher, name, value):
+        re_name = re.escape(name)
+        re_value = re.escape(str(value))
+        re_line = re.compile(fr"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]")
+        match_attempts = [re_line.match(message) for message in log_watcher.output]
+        self.assertTrue(any(match_attempts), repr(log_watcher.output))
+
     @mock.patch.object(Git, "execute")
     def test_call_process_calls_execute(self, git):
         git.return_value = ""
@@ -113,14 +120,9 @@ def test_it_uses_shell_or_not_as_specified(self, case):
     def test_it_logs_if_it_uses_a_shell(self, case):
         """``shell=`` in the log message agrees with what is passed to `Popen`."""
         value_in_call, value_from_class = case
-
         with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
             mock_popen = self._do_shell_combo(value_in_call, value_from_class)
-
-        popen_shell_arg = mock_popen.call_args.kwargs["shell"]
-        expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)")
-        match_attempts = [expected_message.fullmatch(message) for message in log_watcher.output]
-        self.assertTrue(any(match_attempts), repr(log_watcher.output))
+        self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"])
 
     def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")

From 790a790f49a2548c620532ee2b9705b09fb33855 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 09:59:28 -0400
Subject: [PATCH 123/994] Log stdin arg as such, and test that this is done

This changes how the Popen call debug logging line shows the
informal summary of what kind of thing is being passed as the stdin
argument to Popen, showing it with stdin= rather than istream=.

The new test, with "istream" in place of "stdin", passed before the
code change in the git.cmd module, failed once "istream" was
changed to "stdin" in the test, and then, as expected, passed again
once "istream=" was changed to "stdin=" in the log.debug call in
git.cmd.Git.execute.
---
 git/cmd.py       |  2 +-
 test/test_git.py | 10 ++++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 1d8c70f32..e545ba160 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -979,7 +979,7 @@ def execute(
         if shell is None:
             shell = self.USE_SHELL
         log.debug(
-            "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)",
+            "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, stdin=%s)",
             redacted_command,
             cwd,
             universal_newlines,
diff --git a/test/test_git.py b/test/test_git.py
index 10b346e4d..1ee7b3642 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -124,6 +124,16 @@ def test_it_logs_if_it_uses_a_shell(self, case):
             mock_popen = self._do_shell_combo(value_in_call, value_from_class)
         self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"])
 
+    @ddt.data(
+        ("None", None),
+        ("<valid stream>", subprocess.PIPE),
+    )
+    def test_it_logs_istream_summary_for_stdin(self, case):
+        expected_summary, istream_argument = case
+        with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
+            self.git.execute(["git", "version"], istream=istream_argument)
+        self._assert_logged_for_popen(log_watcher, "stdin", expected_summary)
+
     def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
 

From c3fde7fb8dcd48d17ee24b78db7b0dd25d2348ab Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 10:16:10 -0400
Subject: [PATCH 124/994] Log args in the order they are passed to Popen

This is still not including all or even most of the arguments, nor
are all the logged arguments literal (nor should either of those
things likely be changed). It is just to facilitate easier
comparison of what is logged to the Popen call in the code.
---
 git/cmd.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index e545ba160..c35e56703 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -979,12 +979,12 @@ def execute(
         if shell is None:
             shell = self.USE_SHELL
         log.debug(
-            "Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, stdin=%s)",
+            "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,
             cwd,
-            universal_newlines,
-            shell,
             istream_ok,
+            shell,
+            universal_newlines,
         )
         try:
             with maybe_patch_caller_env:

From ab958865d89b3146bb953f826f30da11dc59139a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 10:30:58 -0400
Subject: [PATCH 125/994] Eliminate istream_ok variable

In Git.execute, the stdin argument to Popen is the only one where a
compound expression (rather than a single term) is currently
passed. So having that be the same in the log message makes it
easier to understand what is going on, as well as to see how the
information shown in the log corresponds to what Popen receives.
---
 git/cmd.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index c35e56703..7c448e3f2 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -973,16 +973,13 @@ def execute(
         # end handle
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
-        istream_ok = "None"
-        if istream:
-            istream_ok = "<valid stream>"
         if shell is None:
             shell = self.USE_SHELL
         log.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,
             cwd,
-            istream_ok,
+            "<valid stream>" if istream else "None",
             shell,
             universal_newlines,
         )

From c3e926fbfda3bf5a1723258e990dfcfa45f2ef86 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 12:16:07 -0400
Subject: [PATCH 126/994] Fix a small YAML formatting style inconsistency

---
 .github/workflows/pythonpackage.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 1b049ba02..5564a526b 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -17,9 +17,9 @@ jobs:
       matrix:
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
-          - experimental: false
-          - python-version: "3.12"
-            experimental: true
+        - experimental: false
+        - python-version: "3.12"
+          experimental: true
 
     defaults:
       run:

From b54a28a50b3e81e04b5b9ef61297fd12c62d09b3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 12:17:58 -0400
Subject: [PATCH 127/994] No longer allow CI to select a prerelease for 3.12

Since 3.12.0 stable is out, and available via setup-python, per:
https://github.com/actions/python-versions/blob/main/versions-manifest.json
---
 .github/workflows/pythonpackage.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 5564a526b..e43317807 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -18,8 +18,6 @@ jobs:
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false
-        - python-version: "3.12"
-          experimental: true
 
     defaults:
       run:

From 8c4df3cfdca1eebd2f07c7be24ab5e2805ec2708 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 03:12:48 -0400
Subject: [PATCH 128/994] Add pre-commit hook to run shellcheck

This also reorders the hooks from pre-commit/pre-commit-hooks so
that the overall order of all hooks from all repositories is: lint
Python, lint non-Python, non-lint.
---
 .pre-commit-config.yaml | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 67aefb342..c5973c9ea 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,9 +9,14 @@ repos:
           - flake8-typing-imports==1.14.0
         exclude: ^doc|^git/ext/
 
+  - repo: https://github.com/shellcheck-py/shellcheck-py
+    rev: v0.9.0.5
+    hooks:
+      - id: shellcheck
+
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.4.0
     hooks:
-      - id: check-merge-conflict
       - id: check-toml
       - id: check-yaml
+      - id: check-merge-conflict

From f3be76f474636f9805756bc9f05b22fb4aa8809d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 04:49:28 -0400
Subject: [PATCH 129/994] Force color when running shellcheck in pre-commit

Its output is colorized normally, but on CI it is not colorized
without this (pre-commit's own output is, but not shellcheck's).
---
 .pre-commit-config.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c5973c9ea..bacc90913 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,6 +13,7 @@ repos:
     rev: v0.9.0.5
     hooks:
       - id: shellcheck
+        args: [--color]
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.4.0

From 7dd8added2b1695b1740f0d1d7d7b2858a49a88c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 04:17:04 -0400
Subject: [PATCH 130/994] Suppress SC2086 where word splitting is intended

This suppresses ShellCheck SC2016, "Double quote to prevent
globbing and word splitting," on the command in the version check
script that expands $config_opts to build the "-c ..." arguments.

It also moves the code repsonsible for getting the latest tag,
which this is part of, into a function for that purpose, so it's
clear that building config_opts is specifically for that, and so
that the code is not made harder to read by adding the ShellCheck
suppression comment.

(The suppression applies only to the immediate next command.)
---
 check-version.sh | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/check-version.sh b/check-version.sh
index c50bf498b..46aa56173 100755
--- a/check-version.sh
+++ b/check-version.sh
@@ -10,6 +10,13 @@ trap 'echo "$0: Check failed. Stopping." >&2' ERR
 readonly version_path='VERSION'
 readonly changes_path='doc/source/changes.rst'
 
+function get_latest_tag() {
+    local config_opts
+    config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)"
+    # shellcheck disable=SC2086  # Deliberately word-splitting the arguments.
+    git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1
+}
+
 echo 'Checking current directory.'
 test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)"  # Ugly, but portable.
 
@@ -26,8 +33,7 @@ test -z "$(git status -s --ignore-submodules)"
 
 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)"
+latest_tag="$(get_latest_tag)"
 head_sha="$(git rev-parse HEAD)"
 latest_tag_sha="$(git rev-parse "${latest_tag}^{commit}")"
 

From 21875b5a84c899a5b38c627e895a1bb58344b2a1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 04:31:18 -0400
Subject: [PATCH 131/994] Don't split and glob the interpreter name

In the release building script, this changes $1 to "$1", because
$1 without quotes means to perform word splitting and globbing
(pathname expansion) on the parameter (unless otherwise disabled by
the value of $IFS or "set -f", respectively) and use the result of
doing so, which isn't the intent of the code.

This function is only used from within the script, where it is
not given values that would be changed by these additional
expansions. So this is mainly about communicating intent.

(If in the future it is intended that multiple arguments be usable,
then they should be passed as separate arguments to release_with,
which should be modified by replacing "$1" with "$@". I have not
used "$@" at this time because it is not intuitively obvious that
the arguments should be to the interpreter, rather than to the
build module, so I don't think this should be supported unless or
until a need for it determines that.)
---
 build-release.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build-release.sh b/build-release.sh
index 5840e4472..4eb760ddd 100755
--- a/build-release.sh
+++ b/build-release.sh
@@ -6,7 +6,7 @@
 set -eEu
 
 function release_with() {
-    $1 -m build --sdist --wheel
+    "$1" -m build --sdist --wheel
 }
 
 if test -n "${VIRTUAL_ENV:-}"; then

From 0920371905561d7a242a8be158b79d1a8408a7c4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 04:40:08 -0400
Subject: [PATCH 132/994] Extract suggest_venv out of the else block

I think this is easier to read, but this is admittedly subjective.

This commit also makes the separate change of adjusting comment
spacing for consistency within the script. (Two spaces before "#"
is not widely regarded as better than one in shell scripting, so
unlike Python where PEP-8 recommends that, it would be equally good
to have changed all the other places in the shell scrips to have
just one.)
---
 build-release.sh | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/build-release.sh b/build-release.sh
index 4eb760ddd..4fd4a2251 100755
--- a/build-release.sh
+++ b/build-release.sh
@@ -9,6 +9,11 @@ function release_with() {
     "$1" -m build --sdist --wheel
 }
 
+function suggest_venv() {
+    local venv_cmd='python -m venv env && source env/bin/activate'
+    printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd"
+}
+
 if test -n "${VIRTUAL_ENV:-}"; then
     deps=(build twine)  # Install twine along with build, as we need it later.
     echo "Virtual environment detected. Adding packages: ${deps[*]}"
@@ -16,11 +21,7 @@ if test -n "${VIRTUAL_ENV:-}"; then
     echo 'Starting the build.'
     release_with python
 else
-    function suggest_venv() {
-        venv_cmd='python -m venv env && source env/bin/activate'
-        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.
     echo 'Starting the build.'
-    release_with python3 # Outside a venv, use python3.
+    release_with python3  # Outside a venv, use python3.
 fi

From e973f527f92a932e833167c57cb4d4eeb3103429 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 05:06:59 -0400
Subject: [PATCH 133/994] Use some handy bash-isms in version check script

The script is nonportable to other shells already because of its
use of trap (and other features, such as implicit assumptions made
about echo, and the function keyword), which its hashbang already
clearly conveys.

This uses:

- $(<X) in place of $(cat X), to have the shell read the file
  directly rather than using cat.

- printf -v in one place to set a variable rather than printing.
  This eliminates a command substitution that was harder to read.
---
 check-version.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/check-version.sh b/check-version.sh
index 46aa56173..d4200bd20 100755
--- a/check-version.sh
+++ b/check-version.sh
@@ -12,7 +12,7 @@ readonly changes_path='doc/source/changes.rst'
 
 function get_latest_tag() {
     local config_opts
-    config_opts="$(printf ' -c versionsort.suffix=-%s' alpha beta pre rc RC)"
+    printf -v config_opts ' -c versionsort.suffix=-%s' alpha beta pre rc RC
     # shellcheck disable=SC2086  # Deliberately word-splitting the arguments.
     git $config_opts tag -l '[0-9]*' --sort=-v:refname | head -n1
 }
@@ -31,7 +31,7 @@ echo 'Checking that ALL changes are committed.'
 git status -s --ignore-submodules
 test -z "$(git status -s --ignore-submodules)"
 
-version_version="$(cat "$version_path")"
+version_version="$(<"$version_path")"
 changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")"
 latest_tag="$(get_latest_tag)"
 head_sha="$(git rev-parse HEAD)"

From be5382365b2e5fda1344fb1d606e8f7036fb53e2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 05:23:27 -0400
Subject: [PATCH 134/994] Have init script treat master unambiguously as a
 branch

Because users may have an old version of git without "git switch",
init-tests-after-clone.sh should continue to use "git checkout" to
attempt to switch to master. But without "--", this suffers from
the problem that it's ambiguous if master is a branch (so checkout
behaves like switch) or a path (so checkout behaves like restore).

There are two cases where this ambiguity can be a problem. The most
common is on a fork with no master branch but also, fortunately, no
file or directory named "master". Then the problem is just the
error message (printed just before the script proceeds to redo
the checkout with -b):

    error: pathspec 'master' did not match any file(s) known to git

The real cause of the error is the branch being absent, as happens
when a fork copies only the main branch and the upstream remote is
not also set up. Adding the "--" improves the error message:

    fatal: invalid reference: master

However, it is possible, though unlikely, for a file or directory
called "master" to exist. In that case, if there is also no master
branch, git discards unstaged changes made to the file or (much
worse!) everywhere in that directory, potentially losing work.

This commit adds "--" to the right of "master" so git never
regards it as a path. This is not needed with -b, which is always
followed by a symbolic name, so I have not added it there.

(Note that the command is still imperfect because, for example, in
rare cases there could be a master *tag*--and no master branch--in
which case, as before, HEAD would be detached there and the script
would attempt to continue.)
---
 init-tests-after-clone.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 95ced98b7..52c0c06aa 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -10,7 +10,7 @@ if [[ -z "$TRAVIS" ]]; then
 fi
 
 git tag __testing_point__
-git checkout master || git checkout -b master
+git checkout master -- || git checkout -b master
 git reset --hard HEAD~1
 git reset --hard HEAD~1
 git reset --hard HEAD~1

From e604b469a1bdfb83f03d4c2ef1f79b45b8a5c3fa Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 05:44:02 -0400
Subject: [PATCH 135/994] Use 4-space indentation in all shell scripts

This had been done everywhere except in init-tests-after-clone.sh.
---
 init-tests-after-clone.sh | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 52c0c06aa..9d65570da 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -3,10 +3,10 @@
 set -e
 
 if [[ -z "$TRAVIS" ]]; then
-  read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer
-  if [[ ! $answer =~ [yY] ]]; then
-    exit 2
-  fi
+    read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer
+    if [[ ! $answer =~ [yY] ]]; then
+        exit 2
+    fi
 fi
 
 git tag __testing_point__

From 19dfbd8ce4df9bde9b36eda12304ec4db71b084a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 07:33:56 -0400
Subject: [PATCH 136/994] Make the init script a portable POSIX shell script

This translates init-tests-after-clone.sh from bash to POSIX sh,
changing the hashbang and replacing all bash-specific features, so
that it can be run on any POSIX-compatible ("Bourne style") shell,
including but not limited to bash. This allows it to be used even
on systems that don't have any version of bash installed anywhere.

Only that script is modified. The other shell scripts make greater
use of (and gain greater benefit from) bash features, and are also
only used for building and releasing. In contrast, this script is
meant to be used by most users who clone the repository.
---
 init-tests-after-clone.sh | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 9d65570da..19ff0fd28 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -1,12 +1,16 @@
-#!/usr/bin/env bash
+#!/bin/sh
 
 set -e
 
-if [[ -z "$TRAVIS" ]]; then
-    read -rp "This operation will destroy locally modified files. Continue ? [N/y]: " answer
-    if [[ ! $answer =~ [yY] ]]; then
-        exit 2
-    fi
+if test -z "$TRAVIS"; then
+    printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2
+    read -r answer
+    case "$answer" in
+    [yY])
+        ;;
+    *)
+        exit 2 ;;
+    esac
 fi
 
 git tag __testing_point__

From 7110bf8e04f96ffb20518ebf48803a50d1e3bf22 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 14:56:20 -0400
Subject: [PATCH 137/994] Move extra tag-fetching step into init script

This makes three related changes:

- Removes "git fetch --tags" from the instructions in the readme,
  because the goal of this command can be achieved in the
  init-tests-after-clone.sh script, and because this fetch command,
  as written, is probably only achieving that goal in narrow cases.
  In clones and fetches, tags on branches are fetched by default,
  and the tags needed by the tests are on branches. So the
  situations where "git fetch --tags" was helping were (a) when the
  remote recently gained the tags, and (b) when the original remote
  was cloned in an unusual way, not fetching all tags. In both
  cases, the "--tags" option is not what makes that fetch get the
  needed tags.

- Adds "git fetch --all --tags" to init-tests-after-clone.sh. The
  "--all" option causes it to fetch from all remotes, and this is
  more significant than "--tags", since the tags needed for testing
  are on fetched branches. This achieves what "git fetch --tags"
  was achieving, and it also has the benefit of getting tags from
  remotes that have been added but not fetched from, as happens
  with an upstream remote that was manually added with no further
  action. (It also gets branches from those remotes, but if master
  is on multiple remotes but at different commits then "git
  checkout master" may not be able to pick one. So do this *after*
  rather than before that.)

- Skips this extra fetch, and also the submodule cloning/updating
  step, when running on CI. CI jobs will already have taken care of
  cloning the repo with submodules recursively, and fetching all
  available tags. In forks without tags, the necessary tags for the
  test are not fetched--but the upstream remote is not set up on
  CI, so they wouldn't be obtained anyway, not even by refetching
  with "--all".
---
 README.md                 | 7 +++----
 init-tests-after-clone.sh | 4 ++++
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 65c1e7bae..c17340f63 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,6 @@ To clone the [the GitHub repository](https://github.com/gitpython-developers/Git
 ```bash
 git clone https://github.com/gitpython-developers/GitPython
 cd GitPython
-git fetch --tags
 ./init-tests-after-clone.sh
 ```
 
@@ -114,9 +113,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 `git fetch --tags` followed by 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
+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\`.
diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 19ff0fd28..efa30a2dc 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -19,4 +19,8 @@ git reset --hard HEAD~1
 git reset --hard HEAD~1
 git reset --hard HEAD~1
 git reset --hard __testing_point__
+
+test -z "$TRAVIS" || exit 0  # CI jobs will already have taken care of the rest.
+
+git fetch --all --tags
 git submodule update --init --recursive

From c7cdaf48865f4483bfd34e1f7527ab3e1d205dad Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 18:27:44 -0400
Subject: [PATCH 138/994] Reduce code duplication in version check script

This extracts duplicated code to functions in check-version.sh.

One effect is unfortunately that the specific commands being run
are less explicitly clear when reading the script. However, small
future changes, if accidentally made to one but not the other in
either pair of "git status" commands, would create a much more
confusing situation. So I think this change is justified overall.
---
 check-version.sh | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/check-version.sh b/check-version.sh
index d4200bd20..b47482d7e 100755
--- a/check-version.sh
+++ b/check-version.sh
@@ -10,6 +10,11 @@ trap 'echo "$0: Check failed. Stopping." >&2' ERR
 readonly version_path='VERSION'
 readonly changes_path='doc/source/changes.rst'
 
+function check_status() {
+    git status -s "$@"
+    test -z "$(git status -s "$@")"
+}
+
 function get_latest_tag() {
     local config_opts
     printf -v config_opts ' -c versionsort.suffix=-%s' alpha beta pre rc RC
@@ -23,13 +28,11 @@ test "$(cd -- "$(dirname -- "$0")" && pwd)" = "$(pwd)"  # Ugly, but portable.
 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")"
+check_status -- "$version_path" "$changes_path"
 
 # This section can be commented out, if absolutely necessary.
 echo 'Checking that ALL changes are committed.'
-git status -s --ignore-submodules
-test -z "$(git status -s --ignore-submodules)"
+check_status --ignore-submodules
 
 version_version="$(<"$version_path")"
 changes_version="$(awk '/^[0-9]/ {print $0; exit}' "$changes_path")"

From f6dbba2ae4ad9eb3c470b30acce280a4fb6d0445 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 18:37:26 -0400
Subject: [PATCH 139/994] A couple more script tweaks for clarity

- Because the substitition string after the hyphen is empty,
  "${VIRTUAL_ENV:-}" and "${VIRTUAL_ENV-}" have the same effect.
  However, the latter, which this changes it to, expresses the
  correct idea that the special case being handled is when the
  variable is unset: in this case, we expand an empty field rather
  than triggering an error due to set -u. When the variable is set
  but empty, it already expands to the substitution value, and
  including that in the special case with ":" is thus misleading.

- Continuing in the vein of d18d90a (and 1e0b3f9), this removes
  another explicit newline by adding another echo command to print
  the leading blank line before the table.
---
 build-release.sh | 2 +-
 check-version.sh | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/build-release.sh b/build-release.sh
index 4fd4a2251..49c13b93a 100755
--- a/build-release.sh
+++ b/build-release.sh
@@ -14,7 +14,7 @@ function suggest_venv() {
     printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd"
 }
 
-if test -n "${VIRTUAL_ENV:-}"; then
+if test -n "${VIRTUAL_ENV-}"; then
     deps=(build twine)  # Install twine along with build, as we need it later.
     echo "Virtual environment detected. Adding packages: ${deps[*]}"
     pip install --quiet --upgrade "${deps[@]}"
diff --git a/check-version.sh b/check-version.sh
index b47482d7e..dac386e46 100755
--- a/check-version.sh
+++ b/check-version.sh
@@ -41,7 +41,8 @@ head_sha="$(git rev-parse HEAD)"
 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'
+echo
+echo 'The 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 5060c9dc42677c04af1b696e77228d17dac645a4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Sep 2023 19:05:25 -0400
Subject: [PATCH 140/994] Explain what each step in the init script achieves

This adds comments to init-tests-after-clone.sh to explain what
each of the steps is doing that is needed by some of the tests.

It also refactors some recently added logic, in a way that lends
itself to greater clarity when combined with these comments.
---
 init-tests-after-clone.sh | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index efa30a2dc..a53acbbef 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -13,14 +13,26 @@ if test -z "$TRAVIS"; then
     esac
 fi
 
+# Stop if we have run this. (You can delete __testing_point__ to let it rerun.)
+# This also keeps track of where we are, so we can get back here.
 git tag __testing_point__
+
+# The tests need a branch called master.
 git checkout master -- || git checkout -b master
+
+# The tests need a reflog history on the master branch.
 git reset --hard HEAD~1
 git reset --hard HEAD~1
 git reset --hard HEAD~1
+
+# Point the master branch where we started, so we test the correct code.
 git reset --hard __testing_point__
 
-test -z "$TRAVIS" || exit 0  # CI jobs will already have taken care of the rest.
+# Do some setup that CI takes care of but that may not have been done locally.
+if test -z "$TRAVIS"; then
+    # The tests needs some version tags. Try to get them even in forks.
+    git fetch --all --tags
 
-git fetch --all --tags
-git submodule update --init --recursive
+    # The tests need submodules, including a submodule with a submodule.
+    git submodule update --init --recursive
+fi

From d5479b206fd3a5815bad618d269ecb5e1577feb8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 01:40:33 -0400
Subject: [PATCH 141/994] Use set -u in init script

This is already done in the other shell scripts. Although
init-tests-after-clone.sh does not have as many places where a bug
could slip through by an inadvertently nonexistent parameter, it
does have $answer (and it may have more expansions in the future).
---
 init-tests-after-clone.sh | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index a53acbbef..5ca767654 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -1,8 +1,8 @@
 #!/bin/sh
 
-set -e
+set -eu
 
-if test -z "$TRAVIS"; then
+if test -z "${TRAVIS-}"; then
     printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2
     read -r answer
     case "$answer" in
@@ -29,7 +29,7 @@ git reset --hard HEAD~1
 git reset --hard __testing_point__
 
 # Do some setup that CI takes care of but that may not have been done locally.
-if test -z "$TRAVIS"; then
+if test -z "${TRAVIS-}"; then
     # The tests needs some version tags. Try to get them even in forks.
     git fetch --all --tags
 

From 52f9a68d04233c3be9653a4a8b56a58afb9d7093 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 03:01:43 -0400
Subject: [PATCH 142/994] Make the "all" Makefile target more robust

This is a minor improvement to the robustness of the command for
"make all", in the face of plausible future target names.

- Use [[:alpha:]] instead of [a-z] because, in POSIX BRE and ERE,
  whether [a-z] includes capital letters depends on the current
  collation order. (The goal here is greater consistency across
  systems, and for this it would also be okay to use [[:lower:]].)

- Pass -x to the command that filters out the all target itself,
  so that the entire name must be "all", because a future target
  may contain the substring "all" as part of a larger word.
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 38090244c..fe9d04a18 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 .PHONY: all clean release force_release
 
 all:
-	@grep -Ee '^[a-z].*:' Makefile | cut -d: -f1 | grep -vF all
+	@grep -E '^[[:alpha:]].*:' Makefile | cut -d: -f1 | grep -vxF all
 
 clean:
 	rm -rf build/ dist/ .eggs/ .tox/

From b88d07e47667194e0668431e2a871926eb54d948 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 03:16:33 -0400
Subject: [PATCH 143/994] Use a single awk instead of two greps and a cut

This seems simpler to me, but I admit it is subjective.
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index fe9d04a18..d4f9acf87 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 .PHONY: all clean release force_release
 
 all:
-	@grep -E '^[[:alpha:]].*:' Makefile | cut -d: -f1 | grep -vxF all
+	@awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile
 
 clean:
 	rm -rf build/ dist/ .eggs/ .tox/

From d36818cf59d398e50cc6568f2239d69cd904883d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 03:33:59 -0400
Subject: [PATCH 144/994] Add a black check to pre-commit

- Add the psf/black-pre-commit-mirror pre-commit hook but have it
  just check and report violations rather than fixing them
  automatically (to avoid inconsistency with all the other hooks,
  and also so that the linting instructions and CI lint workflow
  can remain the same and automatically do the black check).

- Remove the "black" environment from tox.ini, since it is now part
  of the linting done in the "lint" environment. (But README.md
  remains the same, as it documented actually auto-formatting with
  black, which is still done the same way.)

- Add missing "exclude" keys for the shellcheck and black
  pre-commit hooks to explicitly avoid traversing into the
  submodules.
---
 .pre-commit-config.yaml | 8 ++++++++
 tox.ini                 | 7 +------
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bacc90913..d6b496a31 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,11 @@
 repos:
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.9.1
+    hooks:
+      - id: black
+        args: [--check, --diff]
+        exclude: ^git/ext/
+
   - repo: https://github.com/PyCQA/flake8
     rev: 6.1.0
     hooks:
@@ -14,6 +21,7 @@ repos:
     hooks:
       - id: shellcheck
         args: [--color]
+        exclude: ^git/ext/
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.4.0
diff --git a/tox.ini b/tox.ini
index 82a41e22c..ed7896b59 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 requires = tox>=4
-env_list = py{37,38,39,310,311,312}, lint, mypy, black
+env_list = py{37,38,39,310,311,312}, lint, mypy
 
 [testenv]
 description = Run unit tests
@@ -20,11 +20,6 @@ base_python = py39
 commands = mypy -p git
 ignore_outcome = true
 
-[testenv:black]
-description = Check style with black
-base_python = py39
-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.
 [testenv:html]

From 4ba5ad107c4e91f62dacee9bfef277f2674fd90f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 04:27:07 -0400
Subject: [PATCH 145/994] Fix typo in comment

---
 init-tests-after-clone.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 5ca767654..5d1c16f0a 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -30,7 +30,7 @@ git reset --hard __testing_point__
 
 # Do some setup that CI takes care of but that may not have been done locally.
 if test -z "${TRAVIS-}"; then
-    # The tests needs some version tags. Try to get them even in forks.
+    # The tests need some version tags. Try to get them even in forks.
     git fetch --all --tags
 
     # The tests need submodules, including a submodule with a submodule.

From 5d8ddd9009ec38ecafddb55acfe7ad9919b1bb0d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 05:12:24 -0400
Subject: [PATCH 146/994] Use two hooks for black: to check, and format

This splits the pre-commit hook for black into two hooks, both
using the same repo and id but separately aliased:

- black-check, which checks but does not modify files. This only
  runs when the manual stage is specified, and it is used by tox
  and on CI, with tox.ini and lint.yml modified accordingly.

- black-format, which autoformats code. This provides the behavior
  most users will expect from a pre-commit hook for black. It runs
  automatically along with other hooks. But tox.ini and lint.yml,
  in addition to enabling the black-check hook, also explicitly
  skip the black-format hook.

The effect is that in ordinary development the pre-commit hook for
black does auto-formatting, but that pre-commit continues to be
used effectively for running checks in which code should not be
changed.
---
 .github/workflows/lint.yml |  4 ++++
 .pre-commit-config.yaml    |  8 ++++++++
 README.md                  | 12 ++++++------
 tox.ini                    |  4 +++-
 4 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2204bb792..71e0a8853 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,3 +14,7 @@ jobs:
         python-version: "3.x"
 
     - uses: pre-commit/action@v3.0.0
+      with:
+        extra_args: --hook-stage manual
+      env:
+        SKIP: black-format
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d6b496a31..5664fe980 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,8 +3,16 @@ repos:
     rev: 23.9.1
     hooks:
       - id: black
+        alias: black-check
+        name: black (check)
         args: [--check, --diff]
         exclude: ^git/ext/
+        stages: [manual]
+
+      - id: black
+        alias: black-format
+        name: black (format)
+        exclude: ^git/ext/
 
   - repo: https://github.com/PyCQA/flake8
     rev: 6.1.0
diff --git a/README.md b/README.md
index c17340f63..8cb6c88c2 100644
--- a/README.md
+++ b/README.md
@@ -142,22 +142,22 @@ To test, run:
 pytest
 ```
 
-To lint, run:
+To lint, and apply automatic code formatting, run:
 
 ```bash
 pre-commit run --all-files
 ```
 
-To typecheck, run:
+Code formatting can also be done by itself by running:
 
-```bash
-mypy -p git
+```
+black .
 ```
 
-For automatic code formatting, run:
+To typecheck, run:
 
 ```bash
-black .
+mypy -p git
 ```
 
 Configuration for flake8 is in the `./.flake8` file.
diff --git a/tox.ini b/tox.ini
index ed7896b59..518577183 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,9 @@ commands = pytest --color=yes {posargs}
 [testenv:lint]
 description = Lint via pre-commit
 base_python = py39
-commands = pre-commit run --all-files
+set_env =
+    SKIP = black-format
+commands = pre-commit run --all-files --hook-stage manual
 
 [testenv:mypy]
 description = Typecheck with mypy

From a872d9c9c90b2a99c0b1a29e10aaecbe2fa387ed Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 15:29:10 -0400
Subject: [PATCH 147/994] Pass --all-files explicitly so it is retained

In the lint workflow, passing extra-args replaced --all-files,
rather than keeping it but adding the extra arguments after it.

(But --show-diff-on-failure and --color=always were still passed.)
---
 .github/workflows/lint.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 71e0a8853..91dd919e0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -15,6 +15,6 @@ jobs:
 
     - uses: pre-commit/action@v3.0.0
       with:
-        extra_args: --hook-stage manual
+        extra_args: --all-files --hook-stage manual
       env:
         SKIP: black-format

From 9b9de1133b85fd308c8795a4f312d3cfbf40b75f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 3 Oct 2023 15:34:28 -0400
Subject: [PATCH 148/994] Fix the formatting

Now that the changes to lint.yml are fixed up, tested, and shown
to be capable of revealing formatting errors, the formatting error
I was using for testing (which is from 9fa1cee where I had
introduced it inadvertently) can be fixed.
---
 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 1ee7b3642..cf82d9ac7 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -43,7 +43,7 @@ def tearDown(self):
     def _assert_logged_for_popen(self, log_watcher, name, value):
         re_name = re.escape(name)
         re_value = re.escape(str(value))
-        re_line = re.compile(fr"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]")
+        re_line = re.compile(rf"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]")
         match_attempts = [re_line.match(message) for message in log_watcher.output]
         self.assertTrue(any(match_attempts), repr(log_watcher.output))
 

From 5d1506352cdff5f3207c5942d82cdc628cb03f3c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 05:29:50 -0400
Subject: [PATCH 149/994] Add "make lint" to lint without auto-formatting

As on CI and with tox (but not having to build and create and use a
tox environment).

This may not be the best way to do it, since currently the
project's makefiles are otherwise used only for things directly
related to building and publishing. However, this seemed like a
readily available way to run the moderately complex command that
produces the same behavior as on CI or with tox, but outside a tox
environment. It may be possible to improve on this in the future.
---
 Makefile  | 5 ++++-
 README.md | 7 ++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/Makefile b/Makefile
index d4f9acf87..839dc9f78 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,11 @@
-.PHONY: all clean release force_release
+.PHONY: all lint clean release force_release
 
 all:
 	@awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile
 
+lint:
+	SKIP=black-format pre-commit run --all-files --hook-stage manual
+
 clean:
 	rm -rf build/ dist/ .eggs/ .tox/
 
diff --git a/README.md b/README.md
index 8cb6c88c2..5ae43dc46 100644
--- a/README.md
+++ b/README.md
@@ -148,11 +148,8 @@ To lint, and apply automatic code formatting, run:
 pre-commit run --all-files
 ```
 
-Code formatting can also be done by itself by running:
-
-```
-black .
-```
+- Linting without modifying code can be done with: `make lint`
+- Auto-formatting without other lint checks can be done with: `black .`
 
 To typecheck, run:
 

From 6de86a85e1d17e32cab8996aa66f10783e6beced Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 05:50:35 -0400
Subject: [PATCH 150/994] Update readme about most of the test/lint tools

Including tox.
---
 README.md | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 5ae43dc46..aac69e0df 100644
--- a/README.md
+++ b/README.md
@@ -157,12 +157,26 @@ To typecheck, run:
 mypy -p git
 ```
 
-Configuration for flake8 is in the `./.flake8` file.
+#### CI (and tox)
 
-Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`.
+The same linting, and running tests on all the different supported Python versions, will be performed:
 
-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).
+- Upon submitting a pull request.
+- On each push, *if* you have a fork with a "main" branch and GitHub Actions enabled.
+- Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
+
+#### Configuration files
+
+Specific tools:
+
+- Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`.
+- Configuration for `flake8` is in the `./.flake8` file.
+
+Orchestration tools:
+
+- Configuration for `pre-commit` is in the `./.pre-commit-config.yaml` file.
+- Configuration for `tox` is in `./tox.ini`.
+- Configuration for GitHub Actions (CI) is in files inside `./.github/workflows/`.
 
 ### Contributions
 

From f0949096f4a2dec466bce48d46a4bf2dd598d36f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 06:31:05 -0400
Subject: [PATCH 151/994] Add BUILDDIR var to doc/Makefile; have tox use it

This adds BUILDDIR as a variable in the documentation generation
makefile that, along SPHINXOPTS, SPHINXBUILD, and PAPER, defaults
to the usual best value but can be set when invoking make.

The main use for choosing a different build output directory is to
test building without overwriting or otherwise interfering with any
files from a build one may really want to use. tox.ini is thus
modified to pass a path pointing inside its temporary directory as
BUILDDIR, so the "html" tox environment now makes no changes
outside the .tox directory. This is thus suitable for being run
automatically, so the "html" environment is now in the envlist.
---
 doc/Makefile | 43 ++++++++++++++++++++++---------------------
 tox.ini      |  8 ++++----
 2 files changed, 26 insertions(+), 25 deletions(-)

diff --git a/doc/Makefile b/doc/Makefile
index ef2d60e5f..ddeadbd7e 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -2,6 +2,7 @@
 #
 
 # You can set these variables from the command line.
+BUILDDIR      = build
 SPHINXOPTS    = -W
 SPHINXBUILD   = sphinx-build
 PAPER         =
@@ -9,7 +10,7 @@ PAPER         =
 # Internal variables.
 PAPEROPT_a4     = -D latex_paper_size=a4
 PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
 
 .PHONY: help clean html web pickle htmlhelp latex changes linkcheck
 
@@ -24,52 +25,52 @@ help:
 	@echo "  linkcheck to check all external links for integrity"
 
 clean:
-	-rm -rf build/*
+	-rm -rf $(BUILDDIR)/*
 
 html:
-	mkdir -p build/html build/doctrees
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html
+	mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 	@echo
-	@echo "Build finished. The HTML pages are in build/html."
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
 
 pickle:
-	mkdir -p build/pickle build/doctrees
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle
+	mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
 	@echo
 	@echo "Build finished; now you can process the pickle files."
 
 web: pickle
 
 json:
-	mkdir -p build/json build/doctrees
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json
+	mkdir -p $(BUILDDIR)/json $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 	@echo
 	@echo "Build finished; now you can process the JSON files."
 
 htmlhelp:
-	mkdir -p build/htmlhelp build/doctrees
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp
+	mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
 	@echo
 	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in build/htmlhelp."
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 
 latex:
-	mkdir -p build/latex build/doctrees
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex
+	mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
 	@echo
-	@echo "Build finished; the LaTeX files are in build/latex."
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
 	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
 	      "run these through (pdf)latex."
 
 changes:
-	mkdir -p build/changes build/doctrees
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes
+	mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
 	@echo
-	@echo "The overview file is in build/changes."
+	@echo "The overview file is in $(BUILDDIR)/changes."
 
 linkcheck:
-	mkdir -p build/linkcheck build/doctrees
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck
+	mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
 	@echo
 	@echo "Link check complete; look for any errors in the above output " \
-	      "or in build/linkcheck/output.txt."
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
diff --git a/tox.ini b/tox.ini
index 518577183..47a7a6209 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 requires = tox>=4
-env_list = py{37,38,39,310,311,312}, lint, mypy
+env_list = py{37,38,39,310,311,312}, lint, mypy, html
 
 [testenv]
 description = Run unit tests
@@ -22,11 +22,11 @@ base_python = py39
 commands = mypy -p git
 ignore_outcome = true
 
-# 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
+commands =
+    make BUILDDIR={env_tmp_dir}/doc/build -C doc clean
+    make BUILDDIR={env_tmp_dir}/doc/build -C doc html

From fc969807086d4483c4c32b80d2c2b67a6c6813e7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 15:27:11 -0400
Subject: [PATCH 152/994] Have init script check for GitHub Actions

As well as still checking for Travis, for backward compatibility
and because experience shows that this is safe.

The check can be much broader, and would be more accurate, with
fewer false negatives. But a false positive can result in local
data loss because the script does hard resets on CI without
prompting for confirmation. So for now, this just checks $TRAVIS
and $GITHUB_ACTIONS.

Now that GHA is included, the CI workflows no longer need to set
$TRAVIS when running the script, so that is removed.
---
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 init-tests-after-clone.sh           | 9 +++++++--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index e818803f1..cd913385f 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -40,7 +40,7 @@ jobs:
 
     - name: Prepare this repo for tests
       run: |
-        TRAVIS=yes ./init-tests-after-clone.sh
+        ./init-tests-after-clone.sh
 
     - name: Set git user identity and command aliases for the tests
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index e43317807..2a82e0e03 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -37,7 +37,7 @@ jobs:
 
     - name: Prepare this repo for tests
       run: |
-        TRAVIS=yes ./init-tests-after-clone.sh
+        ./init-tests-after-clone.sh
 
     - name: Set git user identity and command aliases for the tests
       run: |
diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 5d1c16f0a..4697c2ecc 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -2,7 +2,12 @@
 
 set -eu
 
-if test -z "${TRAVIS-}"; then
+ci() {
+    # For now, check just these, as a false positive could lead to data loss.
+    test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}"
+}
+
+if ! ci; then
     printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2
     read -r answer
     case "$answer" in
@@ -29,7 +34,7 @@ git reset --hard HEAD~1
 git reset --hard __testing_point__
 
 # Do some setup that CI takes care of but that may not have been done locally.
-if test -z "${TRAVIS-}"; then
+if ! ci; then
     # The tests need some version tags. Try to get them even in forks.
     git fetch --all --tags
 

From b98f15e46a7d5f343b1303b55bc4dae2d18bd621 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Sep 2023 23:59:14 -0400
Subject: [PATCH 153/994] Get tags for tests from original repo as fallback

This extends the init script so it tries harder to get version
tags:

- As before, locally, "git fetch --all --tags" is always run. (This
  also fetches other objects, and the developer experience would be
  undesirably inconsistent if that were only sometimes done.)

- On CI, run it if version tags are absent. The criterion followed
  here and in subsequent steps is that if any existing tag starts
  with a digit, or with the letter v followed by a digit, we regard
  version tags to be present. This is to balance the benefit of
  getting the tags (to make the tests work) against the risk of
  creating a very confusing situation in clones of forks that
  publish packages or otherwise use their own separate versioning
  scheme (especially if those tags later ended up being pushed).

- Both locally and on CI, after that, if version tags are absent,
  try to copy them from the original gitpython-developers/GitPython
  repo, without copying other tags or adding that repo as a remote.
  Copy only tags that start with a digit, since v+digit tags aren't
  currently used in this project (though forks may use them). This
  is a fallback option and it always displays a warning. If it
  fails, another message is issued for that. Unexpected failure to
  access the repo terminates the script with a failing exit status,
  but the absence of version tags in the fallback remote does not,
  so CI jobs can continue and reveal which tests fail as a result.

On GitHub Actions CI specifically, the Actions syntax for creating
a warning annotation on the workflow is used, but the warning is
still also written to stderr (as otherwise). GHA workflow
annotations don't support multi-line warnings, so the message is
adjusted into a single line where needed.
---
 init-tests-after-clone.sh | 38 ++++++++++++++++++++++++++++++++++----
 1 file changed, 34 insertions(+), 4 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 4697c2ecc..5e31e3315 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -2,11 +2,25 @@
 
 set -eu
 
+fallback_repo_for_tags='https://github.com/gitpython-developers/GitPython.git'
+
 ci() {
     # For now, check just these, as a false positive could lead to data loss.
     test -n "${TRAVIS-}" || test -n "${GITHUB_ACTIONS-}"
 }
 
+no_version_tags() {
+    test -z "$(git tag -l '[0-9]*' 'v[0-9]*')"
+}
+
+warn() {
+    printf '%s\n' "$@" >&2  # Warn in step output.
+
+    if test -n "${GITHUB_ACTIONS-}"; then
+        printf '::warning ::%s\n' "$*" >&2  # Annotate workflow.
+    fi
+}
+
 if ! ci; then
     printf 'This operation will destroy locally modified files. Continue ? [N/y]: ' >&2
     read -r answer
@@ -33,11 +47,27 @@ git reset --hard HEAD~1
 # Point the master branch where we started, so we test the correct code.
 git reset --hard __testing_point__
 
-# Do some setup that CI takes care of but that may not have been done locally.
+# The tests need submodules. (On CI, they would already have been checked out.)
 if ! ci; then
-    # The tests need some version tags. Try to get them even in forks.
+    git submodule update --init --recursive
+fi
+
+# The tests need some version tags. Try to get them even in forks. This fetch
+# gets other objects too, so for a consistent experience, always do it locally.
+if ! ci || no_version_tags; then
     git fetch --all --tags
+fi
 
-    # The tests need submodules, including a submodule with a submodule.
-    git submodule update --init --recursive
+# If we still have no version tags, try to get them from the original repo.
+if no_version_tags; then
+    warn 'No local or remote version tags found. Trying fallback remote:' \
+         "$fallback_repo_for_tags"
+
+    # git fetch supports * but not [], and --no-tags means no *other* tags, so...
+    printf 'refs/tags/%d*:refs/tags/%d*\n' 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 |
+        xargs git fetch --no-tags "$fallback_repo_for_tags"
+
+    if no_version_tags; then
+        warn 'No version tags found anywhere. Some tests will fail.'
+    fi
 fi

From 7cca7d2245a504047188943623cc58c4c2e0c35f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Sep 2023 10:28:17 -0400
Subject: [PATCH 154/994] Don't print the exact same warning twice

In the step output, the warning that produces a workflow annotation
is fully readable (and even made to stand out), so it doesn't need
to be printed in the plain way as well, which can be reserved for
when GitHub Actions is not detected.
---
 init-tests-after-clone.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 5e31e3315..49df49daa 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -14,10 +14,10 @@ no_version_tags() {
 }
 
 warn() {
-    printf '%s\n' "$@" >&2  # Warn in step output.
-
     if test -n "${GITHUB_ACTIONS-}"; then
         printf '::warning ::%s\n' "$*" >&2  # Annotate workflow.
+    else
+        printf '%s\n' "$@" >&2
     fi
 }
 

From e4e009d03b890d456b4c1ff2411591d3a50dcdd0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Sep 2023 16:59:58 -0400
Subject: [PATCH 155/994] Reword comment to fix ambiguity

---
 init-tests-after-clone.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 49df49daa..21d1f86d8 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -52,8 +52,8 @@ if ! ci; then
     git submodule update --init --recursive
 fi
 
-# The tests need some version tags. Try to get them even in forks. This fetch
-# gets other objects too, so for a consistent experience, always do it locally.
+# The tests need some version tags. Try to get them even in forks. This fetches
+# other objects too. So, locally, we always do it, for a consistent experience.
 if ! ci || no_version_tags; then
     git fetch --all --tags
 fi

From e16e4c0099cd0197b5c80ce6ec9a6b4bca41845e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Sep 2023 18:58:08 -0400
Subject: [PATCH 156/994] Format all YAML files in the same style

---
 .github/dependabot.yml  |  2 +-
 .pre-commit-config.yaml | 68 ++++++++++++++++++++---------------------
 2 files changed, 35 insertions(+), 35 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 203f3c889..8c139c7be 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,4 +3,4 @@ updates:
 - package-ecosystem: "github-actions"
   directory: "/"
   schedule:
-      interval: "weekly"
+    interval: "weekly"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5664fe980..be97d5f9b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,39 +1,39 @@
 repos:
-  - repo: https://github.com/psf/black-pre-commit-mirror
-    rev: 23.9.1
-    hooks:
-      - id: black
-        alias: black-check
-        name: black (check)
-        args: [--check, --diff]
-        exclude: ^git/ext/
-        stages: [manual]
+- repo: https://github.com/psf/black-pre-commit-mirror
+  rev: 23.9.1
+  hooks:
+  - id: black
+    alias: black-check
+    name: black (check)
+    args: [--check, --diff]
+    exclude: ^git/ext/
+    stages: [manual]
 
-      - id: black
-        alias: black-format
-        name: black (format)
-        exclude: ^git/ext/
+  - id: black
+    alias: black-format
+    name: black (format)
+    exclude: ^git/ext/
 
-  - repo: https://github.com/PyCQA/flake8
-    rev: 6.1.0
-    hooks:
-      - id: flake8
-        additional_dependencies:
-          - flake8-bugbear==23.9.16
-          - flake8-comprehensions==3.14.0
-          - flake8-typing-imports==1.14.0
-        exclude: ^doc|^git/ext/
+- repo: https://github.com/PyCQA/flake8
+  rev: 6.1.0
+  hooks:
+  - id: flake8
+    additional_dependencies:
+    - flake8-bugbear==23.9.16
+    - flake8-comprehensions==3.14.0
+    - flake8-typing-imports==1.14.0
+    exclude: ^doc|^git/ext/
 
-  - repo: https://github.com/shellcheck-py/shellcheck-py
-    rev: v0.9.0.5
-    hooks:
-      - id: shellcheck
-        args: [--color]
-        exclude: ^git/ext/
+- repo: https://github.com/shellcheck-py/shellcheck-py
+  rev: v0.9.0.5
+  hooks:
+  - id: shellcheck
+    args: [--color]
+    exclude: ^git/ext/
 
-  - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0
-    hooks:
-      - id: check-toml
-      - id: check-yaml
-      - id: check-merge-conflict
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v4.4.0
+  hooks:
+  - id: check-toml
+  - id: check-yaml
+  - id: check-merge-conflict

From 62c024e277820b2fd3764a0499a71f03d4aa432d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Sep 2023 19:33:07 -0400
Subject: [PATCH 157/994] Let tox run lint, mypy, and html envs without 3.9

The tox environments that are not duplicated per Python version
were set to run on Python 3.9, for consistency with Cygwin, where
3.9 is the latest available (through official Cygwin packages), so
output can be compared between Cygwin and other platforms while
troubleshooting problems.

However, this prevented them from running altogether on systems
where Python 3.9 was not installed. So I've added all the other
Python versions the project supports as fallback versions for those
tox environments to use, in one of the reasonable precedence
orders, while keeping 3.9 as the first choice for the same reasons
as before.
---
 tox.ini | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tox.ini b/tox.ini
index 47a7a6209..f9ac25b78 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,20 +11,20 @@ commands = pytest --color=yes {posargs}
 
 [testenv:lint]
 description = Lint via pre-commit
-base_python = py39
+base_python = py{39,310,311,312,38,37}
 set_env =
     SKIP = black-format
 commands = pre-commit run --all-files --hook-stage manual
 
 [testenv:mypy]
 description = Typecheck with mypy
-base_python = py39
+base_python = py{39,310,311,312,38,37}
 commands = mypy -p git
 ignore_outcome = true
 
 [testenv:html]
 description = Build HTML documentation
-base_python = py39
+base_python = py{39,310,311,312,38,37}
 deps = -r doc/requirements.txt
 allowlist_externals = make
 commands =

From 9e245d0561ca2f6249e9435a04761da2c64977f1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 1 Oct 2023 00:03:16 -0400
Subject: [PATCH 158/994] Update readme: CI jobs not just for "main" branch

This changed a while ago but README.md still listed having a main
branch as a condition for automatic CI linting and testing in a
fork.
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index aac69e0df..021aab15f 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ mypy -p git
 The same linting, and running tests on all the different supported Python versions, will be performed:
 
 - Upon submitting a pull request.
-- On each push, *if* you have a fork with a "main" branch and GitHub Actions enabled.
+- On each push, *if* you have a fork with GitHub Actions enabled.
 - Locally, if you run [`tox`](https://tox.wiki/) (this skips any Python versions you don't have installed).
 
 #### Configuration files

From c2472e9b57afde98bb18ada55ae978721a27713d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 1 Oct 2023 00:11:37 -0400
Subject: [PATCH 159/994] Note that the init script can be run from Git Bash

This is probably the *only* way anyone should run that script on
Windows, but I don't know of specific bad things that happen if it
is run in some other way, such as with WSL bash, aside from messing
up line endings, which users are likely to notice anyway.

This commit also clarifies the instructions by breaking up another
paragraph that really represented two separate steps.
---
 README.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 021aab15f..69fb54c9f 100644
--- a/README.md
+++ b/README.md
@@ -79,13 +79,17 @@ cd GitPython
 ./init-tests-after-clone.sh
 ```
 
+On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
+
 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):
+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 ".[test]"

From 8edc53b7c9be8beebef240b9a2a8fff19b79d21e Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Fri, 6 Oct 2023 12:18:30 -0600
Subject: [PATCH 160/994] Clean up __all__ in main, and explicit imports in exc

- explicit imports in exc added to avoid linting errors in __init__
---
 git/__init__.py | 88 ++++++++++++++++++++++++++++++++++++++++++-------
 git/exc.py      | 13 ++++++--
 2 files changed, 88 insertions(+), 13 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index cc0ca2136..00e7b8dd3 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -63,17 +63,83 @@ def _init_externals() -> None:
 
 # __all__ must be statically defined by py.typed support
 # __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
-__all__ = ['BadName', 'safe_decode',
-    'remove_password_if_present', 'List', 'Sequence', 'Tuple', 'Union', 'TYPE_CHECKING',
-    'PathLike', 'GitError', 'InvalidGitRepositoryError', 'WorkTreeRepositoryUnsupported',
-    'NoSuchPathError', 'UnsafeProtocolError', 'UnsafeOptionError', 'CommandError', 'GitCommandNotFound',
-    'GitCommandError', 'CheckoutError', 'CacheError', 'UnmergedEntriesError', 'HookExecutionError',
-    'RepositoryDirtyError', 'Optional', 'GitConfigParser', 'Object', 'IndexObject', 'Blob', 'Commit',
-    'Submodule', 'UpdateProgress', 'RootModule', 'RootUpdateProgress', 'TagObject', 'TreeModifier',
-    'Tree', 'SymbolicReference', 'Reference', 'HEAD', 'Head', 'TagReference', 'Tag', 'RemoteReference',
-    'RefLog', 'RefLogEntry', 'Diffable', 'DiffIndex', 'Diff', 'NULL_TREE', 'GitCmdObjectDB', 'GitDB',
-    'Git', 'Repo', 'RemoteProgress', 'PushInfo', 'FetchInfo', 'Remote', 'IndexFile', 'StageType',
-    'BlobFilter', 'BaseIndexEntry', 'IndexEntry', 'LockFile', 'BlockingLockFile', 'Stats', 'Actor', 'rmtree']
+__all__ = [
+    'Actor',
+    'AmbiguousObjectName',
+    'BadName',
+    'BadObject',
+    'BadObjectType',
+    'BaseIndexEntry',
+    'Blob',
+    'BlobFilter',
+    'BlockingLockFile',
+    'CacheError',
+    'CheckoutError',
+    'CommandError',
+    'Commit',
+    'Diff',
+    'DiffIndex',
+    'Diffable',
+    'FetchInfo',
+    'Git',
+    'GitCmdObjectDB',
+    'GitCommandError',
+    'GitCommandNotFound',
+    'GitConfigParser',
+    'GitDB',
+    'GitError',
+    'HEAD',
+    'Head',
+    'HookExecutionError',
+    'IndexEntry',
+    'IndexFile',
+    'IndexObject',
+    'InvalidDBRoot',
+    'InvalidGitRepositoryError',
+    'List',
+    'LockFile',
+    'NULL_TREE',
+    'NoSuchPathError',
+    'ODBError',
+    'Object',
+    'Optional',
+    'ParseError',
+    'PathLike',
+    'PushInfo',
+    'RefLog',
+    'RefLogEntry',
+    'Reference',
+    'Remote',
+    'RemoteProgress',
+    'RemoteReference',
+    'Repo',
+    'RepositoryDirtyError',
+    'RootModule',
+    'RootUpdateProgress',
+    'Sequence',
+    'StageType',
+    'Stats',
+    'Submodule',
+    'SymbolicReference',
+    'TYPE_CHECKING',
+    'Tag',
+    'TagObject',
+    'TagReference',
+    'Tree',
+    'TreeModifier',
+    'Tuple',
+    'Union',
+    'UnmergedEntriesError',
+    'UnsafeOptionError',
+    'UnsafeProtocolError',
+    'UnsupportedOperation',
+    'UpdateProgress',
+    'WorkTreeRepositoryUnsupported',
+    'remove_password_if_present',
+    'rmtree',
+    'safe_decode',
+    'to_hex_sha',
+]
 
 # { Initialize git executable path
 GIT_OK = None
diff --git a/git/exc.py b/git/exc.py
index 775528bf6..0c939f929 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -5,8 +5,17 @@
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
 """ Module containing all exceptions thrown throughout the git package, """
 
-from gitdb.exc import BadName  # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614
-from gitdb.exc import *  # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614
+from gitdb.exc import (
+    AmbiguousObjectName,
+    BadName,
+    BadObject,
+    BadObjectType,
+    InvalidDBRoot,
+    ODBError,
+    ParseError,
+    UnsupportedOperation,
+    to_hex_sha,
+) # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614
 from git.compat import safe_decode
 from git.util import remove_password_if_present
 

From 9ac6bb0b58c88490a8195a62b9b9662bdd538bd6 Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Fri, 6 Oct 2023 12:53:18 -0600
Subject: [PATCH 161/994] Add @UnusedImport to recent changes

---
 git/exc.py | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/git/exc.py b/git/exc.py
index 0c939f929..936381fd8 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -6,16 +6,16 @@
 """ Module containing all exceptions thrown throughout the git package, """
 
 from gitdb.exc import (
-    AmbiguousObjectName,
-    BadName,
-    BadObject,
-    BadObjectType,
-    InvalidDBRoot,
-    ODBError,
-    ParseError,
-    UnsupportedOperation,
-    to_hex_sha,
-) # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614
+    AmbiguousObjectName,  # @UnusedImport
+    BadName,  # @UnusedImport
+    BadObject,  # @UnusedImport
+    BadObjectType,  # @UnusedImport
+    InvalidDBRoot,  # @UnusedImport
+    ODBError,  # @UnusedImport
+    ParseError,  # @UnusedImport
+    UnsupportedOperation,  # @UnusedImport
+    to_hex_sha,  # @UnusedImport
+)
 from git.compat import safe_decode
 from git.util import remove_password_if_present
 

From 04f3200723d67aaf5d46106c5c5602a0d5caae94 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 11:40:01 -0400
Subject: [PATCH 162/994] Ask git where its daemon is and use that

This changes the test helpers on Windows to use "git --exec-path"
(with whatever "git" GitPython is using) to find the directory that
contains "git-daemon.exe", instead of finding it in a PATH search.
---
 README.md          | 3 ---
 test/lib/helper.py | 2 +-
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 69fb54c9f..b9e61912a 100644
--- a/README.md
+++ b/README.md
@@ -121,9 +121,6 @@ _Important_: Right after cloning this repository, please be sure to have execute
 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\`.
-
 #### Install test dependencies
 
 Ensure testing libraries are installed. This is taken care of already if you installed with:
diff --git a/test/lib/helper.py b/test/lib/helper.py
index 1de904610..d415ba2e7 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -182,7 +182,7 @@ def git_daemon_launched(base_path, ip, port):
             # and then CANNOT DIE!
             # So, invoke it as a single command.
             daemon_cmd = [
-                "git-daemon",
+                osp.join(Git()._call_process("--exec-path"), "git-daemon"),
                 "--enable=receive-pack",
                 "--listen=%s" % ip,
                 "--port=%s" % port,

From 28142630f6c3bfd83dcd654c102235c5c00075e5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 01:47:38 -0400
Subject: [PATCH 163/994] Add a missing PermissionError xfail on Windows

One of the tests that was commented as being skipped as a result of
SkipTest rasied in git.util.rmtree or one of the functions that
calls it, test_git_submodule_compatibility, was not skipped in that
way and was actually failing on Windows with PermissionError. It
appears that the cause of failure changed over time, so that it
once involved rmtree but no longer does.

This removes the outdated comment and adds an xfail mark instead,
specific to PermissionError and with a message identifying where in
the test case's logic the PermissionError is currently triggered.
---
 test/test_submodule.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 79ff2c5f2..0ebd8c51d 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -819,9 +819,11 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):
         assert commit_sm.binsha == sm_too.binsha
         assert sm_too.binsha != sm.binsha
 
-    # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,  ## ACTUALLY skipped by `git.submodule.base#L869`.
-    #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
-    #         "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx")  # noqa E501
+    @pytest.mark.xfail(
+        HIDE_WINDOWS_KNOWN_ERRORS,
+        reason='"The process cannot access the file because it is being used by another process" on call to sm.move',
+        raises=PermissionError,
+    )
     @with_rw_directory
     def test_git_submodule_compatibility(self, rwdir):
         parent = git.Repo.init(osp.join(rwdir, "parent"))

From fba59aa32119e22b1b300fea8959c0abd3c9f863 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 04:39:56 -0400
Subject: [PATCH 164/994] Update "ACTUALLY skipped by" comments

The remaining "ACTUALLY skipped by" comments in the test suite were
for tests that are actually skipped by SkipTest exceptions raised
from the code under test. But the information provided about where
in the code they were skipped was out of date, and also not
detailed enough because references to line numbers become stale
when code is added or removed in the referenced module before the
referenced code.

This updates them and also provides more detailed information about
the referenced code doing the skipping.

The error messages are the same as before, and the paths are the
same in relevant details, so this doesn't modify those parts of the
comments.
---
 test/test_docs.py      | 5 ++++-
 test/test_submodule.py | 5 ++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/test/test_docs.py b/test/test_docs.py
index 79e1f1be4..b38ac31b0 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -21,7 +21,10 @@ def tearDown(self):
 
         gc.collect()
 
-    # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,  ## ACTUALLY skipped by `git.submodule.base#L869`.
+    # ACTUALLY skipped by git.objects.submodule.base.Submodule.remove, at the last
+    # rmtree call (in "handle separate bare repository"), line 1082.
+    #
+    # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
     #         "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx")  # noqa E501
     @with_rw_directory
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 0ebd8c51d..1c105c816 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -457,7 +457,10 @@ def _do_base_tests(self, rwrepo):
             True,
         )
 
-    # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,  ## ACTUALLY skipped by `git.submodule.base#L869`.
+    # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via
+    # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1018.
+    #
+    # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"
     #         "it is being used by another process: "
     #         "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'")  # noqa E501

From 5039df3560d321af1746bbecbeb1b2838daf7f91 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 06:31:27 -0400
Subject: [PATCH 165/994] Eliminate duplicate rmtree try-except logic

In git.util.rmtree, exceptions are caught and conditionally
(depending on the value of HIDE_WINDOWS_KNOWN_ERRORS) reraised
wrapped in a unittest.SkipTest exception. Although this logic is
part of git.util.rmtree itself, two of the calls to that rmtree
function contain this same logic.

This is not quite a refactoring: because SkipTest derives from
Exception, and Exception rather than PermissionError is being
caught including in the duplicated logic, duplicated logic where
git.util.rmtree was called added another layer of indirection in
the chained exceptions leading back to the original that was raised
in an unsuccessful attempt to delete a file or directory in rmtree.
However, that appeared unintended and may be considered a minor
bug. The new behavior, differing only subtly, is preferable.
---
 git/objects/submodule/base.py | 20 ++------------------
 test/test_docs.py             |  4 ++--
 test/test_submodule.py        |  2 +-
 3 files changed, 5 insertions(+), 21 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index c7e7856f0..6fe946084 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -29,7 +29,6 @@
     unbare_repo,
     IterableList,
 )
-from git.util import HIDE_WINDOWS_KNOWN_ERRORS
 
 import os.path as osp
 
@@ -1060,28 +1059,13 @@ def remove(
                     import gc
 
                     gc.collect()
-                    try:
-                        rmtree(str(wtd))
-                    except Exception as ex:
-                        if HIDE_WINDOWS_KNOWN_ERRORS:
-                            from unittest import SkipTest
-
-                            raise SkipTest("FIXME: fails with: PermissionError\n  {}".format(ex)) from ex
-                        raise
+                    rmtree(str(wtd))
                 # END delete tree if possible
             # END handle force
 
             if not dry_run and osp.isdir(git_dir):
                 self._clear_cache()
-                try:
-                    rmtree(git_dir)
-                except Exception as ex:
-                    if HIDE_WINDOWS_KNOWN_ERRORS:
-                        from unittest import SkipTest
-
-                        raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
-                    else:
-                        raise
+                rmtree(git_dir)
             # end handle separate bare repository
         # END handle module deletion
 
diff --git a/test/test_docs.py b/test/test_docs.py
index b38ac31b0..f17538aeb 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -21,8 +21,8 @@ def tearDown(self):
 
         gc.collect()
 
-    # ACTUALLY skipped by git.objects.submodule.base.Submodule.remove, at the last
-    # rmtree call (in "handle separate bare repository"), line 1082.
+    # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via
+    # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1068.
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 1c105c816..318a5afde 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -458,7 +458,7 @@ def _do_base_tests(self, rwrepo):
         )
 
     # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via
-    # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1018.
+    # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1017.
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"

From 683a3eeba838bb786bb1f334c963deb8e13eed0f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 13:18:09 -0400
Subject: [PATCH 166/994] Clean up git.objects.submodule.base imports

This reorders them lexicographically within each group, makes
spacing/formatting more consistent, and removes the old comment
about needing a dict to set .name, which had originally been on
what later became the BytesIO import but had become separate from
it. (In Python 2, there was a cStringIO type, which could provide a
speed advantage over StringIO, but its instances, not having
instance dictionaries, didn't support the dynamic creation of new
attributes. This was changed to StringIO in 00ce31a to allow .name
to be added. It was changed to BytesIO in bc8c912 to work with
bytes on both Python 2 and Python 3. The comment about needing a
dict later ended up on the preceding line in 0210e39, at which
point its meaning was unclear. Because Python 2 is no longer
supported and Python 3 has no cStringIO type, the comment is no
longer needed, and this commit removes it.)
---
 git/objects/submodule/base.py | 24 +++++++++---------------
 test/test_docs.py             |  2 +-
 test/test_submodule.py        |  2 +-
 3 files changed, 11 insertions(+), 17 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 6fe946084..13d897263 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1,43 +1,37 @@
-# need a dict to set bloody .name field
 from io import BytesIO
 import logging
 import os
+import os.path as osp
 import stat
 import uuid
 
 import git
 from git.cmd import Git
-from git.compat import (
-    defenc,
-    is_win,
-)
-from git.config import SectionConstraint, GitConfigParser, cp
+from git.compat import defenc, is_win
+from git.config import GitConfigParser, SectionConstraint, cp
 from git.exc import (
+    BadName,
     InvalidGitRepositoryError,
     NoSuchPathError,
     RepositoryDirtyError,
-    BadName,
 )
 from git.objects.base import IndexObject, Object
 from git.objects.util import TraversableIterableObj
-
 from git.util import (
-    join_path_native,
-    to_native_path_linux,
+    IterableList,
     RemoteProgress,
+    join_path_native,
     rmtree,
+    to_native_path_linux,
     unbare_repo,
-    IterableList,
 )
 
-import os.path as osp
-
 from .util import (
+    SubmoduleConfigParser,
+    find_first_remote_branch,
     mkhead,
     sm_name,
     sm_section,
-    SubmoduleConfigParser,
-    find_first_remote_branch,
 )
 
 
diff --git a/test/test_docs.py b/test/test_docs.py
index f17538aeb..d1ed46926 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -22,7 +22,7 @@ def tearDown(self):
         gc.collect()
 
     # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via
-    # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1068.
+    # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1062.
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 318a5afde..31a555ce2 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -458,7 +458,7 @@ def _do_base_tests(self, rwrepo):
         )
 
     # ACTUALLY skipped by git.util.rmtree (in local onerror function), called via
-    # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1017.
+    # git.objects.submodule.base.Submodule.remove at "method(mp)", line 1011.
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"

From 2fe7f3c4a6f9870bb332761740c883a2c2ff2487 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 22:46:43 -0400
Subject: [PATCH 167/994] Test current expected behavior of git.util.rmtree

---
 test/test_util.py | 70 ++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 63 insertions(+), 7 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 2b1e518ed..552700c98 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -5,11 +5,13 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
 import os
+import pathlib
 import pickle
+import stat
 import sys
 import tempfile
 import time
-from unittest import mock, skipUnless
+from unittest import SkipTest, mock, skipIf, skipUnless
 from datetime import datetime
 
 import ddt
@@ -19,25 +21,26 @@
 from git.compat import is_win
 from git.objects.util import (
     altz_to_utctz_str,
-    utctz_to_altz,
-    verify_utctz,
+    from_timestamp,
     parse_date,
     tzoffset,
-    from_timestamp,
+    utctz_to_altz,
+    verify_utctz,
 )
 from test.lib import (
     TestBase,
     with_rw_repo,
 )
 from git.util import (
-    LockFile,
-    BlockingLockFile,
-    get_user_id,
     Actor,
+    BlockingLockFile,
     IterableList,
+    LockFile,
     cygpath,
     decygpath,
+    get_user_id,
     remove_password_if_present,
+    rmtree,
 )
 
 
@@ -85,6 +88,59 @@ def setup(self):
             "array": [42],
         }
 
+    def test_rmtree_deletes_nested_dir_with_files(self):
+        with tempfile.TemporaryDirectory() as parent:
+            td = pathlib.Path(parent, "testdir")
+            for d in td, td / "q", td / "s":
+                d.mkdir()
+            for f in td / "p", td / "q" / "w", td / "q" / "x", td / "r", td / "s" / "y", td / "s" / "z":
+                f.write_bytes(b"")
+
+            try:
+                rmtree(td)
+            except SkipTest as ex:
+                self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
+            self.assertFalse(td.exists())
+
+    @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
+    def test_rmtree_deletes_dir_with_readonly_files(self):
+        # Automatically works on Unix, but requires special handling on Windows.
+        with tempfile.TemporaryDirectory() as parent:
+            td = pathlib.Path(parent, "testdir")
+            for d in td, td / "sub":
+                d.mkdir()
+            for f in td / "x", td / "sub" / "y":
+                f.write_bytes(b"")
+                f.chmod(0)
+
+            try:
+                rmtree(td)
+            except SkipTest as ex:
+                self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
+            self.assertFalse(td.exists())
+
+    @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
+    @skipIf(sys.version_info < (3, 8), "In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
+    def test_rmtree_can_wrap_exceptions(self):
+        with tempfile.TemporaryDirectory() as parent:
+            td = pathlib.Path(parent, "testdir")
+            td.mkdir()
+            (td / "x").write_bytes(b"")
+            (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
+            td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
+
+            # Access the module through sys.modules so it is unambiguous which module's
+            # attribute we patch: the original git.util, not git.index.util even though
+            # git.index.util "replaces" git.util and is what "import git.util" gives us.
+            with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True):
+                # Disable common chmod functions so the callback can't fix the problem.
+                with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"):
+                    # Now we can see how an intractable PermissionError is treated.
+                    with self.assertRaises(SkipTest):
+                        rmtree(td)
+
     # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them).
     @pytest.mark.xfail(
         reason="Many return paths prefixed /proc/cygdrive instead.",

From d42cd721112d748c35d0abd11ba8dfc71052e864 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 9 Oct 2023 01:45:57 -0400
Subject: [PATCH 168/994] Test situations git.util.rmtree shouldn't wrap

One of the new test cases fails, showing the bug where
git.util.rmtree wraps any exception in SkipTest when
HIDE_WINDOWS_KNOWN_ERRORS is true, even though the message it uses
(and its purpose) is specific to PermissionError.

The other new cases pass, because wrapping exceptions in SkipTest
rightly does not occur when HIDE_WINDOWS_KNOWN_ERRORS is false.
---
 test/test_util.py | 41 ++++++++++++++++++++++++++++++++++++++---
 1 file changed, 38 insertions(+), 3 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 552700c98..a852eb975 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -4,6 +4,7 @@
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
+import contextlib
 import os
 import pathlib
 import pickle
@@ -121,16 +122,31 @@ def test_rmtree_deletes_dir_with_readonly_files(self):
 
             self.assertFalse(td.exists())
 
-    @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
-    @skipIf(sys.version_info < (3, 8), "In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
-    def test_rmtree_can_wrap_exceptions(self):
+    @staticmethod
+    @contextlib.contextmanager
+    def _tmpdir_to_force_permission_error():
+        if sys.platform == "cygwin":
+            raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
+        if sys.version_info < (3, 8):
+            raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
+
         with tempfile.TemporaryDirectory() as parent:
             td = pathlib.Path(parent, "testdir")
             td.mkdir()
             (td / "x").write_bytes(b"")
             (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
             td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
+            yield td
 
+    @staticmethod
+    @contextlib.contextmanager
+    def _tmpdir_for_file_not_found():
+        with tempfile.TemporaryDirectory() as parent:
+            yield pathlib.Path(parent, "testdir")  # It is deliberately never created.
+
+    def test_rmtree_can_wrap_exceptions(self):
+        """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        with self._tmpdir_to_force_permission_error() as td:
             # Access the module through sys.modules so it is unambiguous which module's
             # attribute we patch: the original git.util, not git.index.util even though
             # git.index.util "replaces" git.util and is what "import git.util" gives us.
@@ -141,6 +157,25 @@ def test_rmtree_can_wrap_exceptions(self):
                     with self.assertRaises(SkipTest):
                         rmtree(td)
 
+    @ddt.data(
+        (False, PermissionError, _tmpdir_to_force_permission_error),
+        (False, FileNotFoundError, _tmpdir_for_file_not_found),
+        (True, FileNotFoundError, _tmpdir_for_file_not_found),
+    )
+    def test_rmtree_does_not_wrap_unless_called_for(self, case):
+        """Our rmtree doesn't wrap non-PermissionError, nor when HIDE_WINDOWS_KNOWN_ERRORS is false."""
+        hide_windows_known_errors, exception_type, tmpdir_context_factory = case
+
+        with tmpdir_context_factory() as td:
+            # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here.
+            with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors):
+                with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"):
+                    with self.assertRaises(exception_type):
+                        try:
+                            rmtree(td)
+                        except SkipTest as ex:
+                            self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
     # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them).
     @pytest.mark.xfail(
         reason="Many return paths prefixed /proc/cygdrive instead.",

From 2a32e25bbd1cb42878928ef57a57100a17366202 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 9 Oct 2023 02:30:22 -0400
Subject: [PATCH 169/994] Fix test bug that assumed staticmethod callability

staticmethod objects are descriptors that cause the same-named
attribute on a class or its instances to be callable (and the first
argument, representing a class or instance, not to be passed). But
the actual staticmethod objects themselves are only callable
starting in Python 3.10. This reorgnizes the just-added test code
so it no longer wrongly relies on being able to call such objects.
---
 test/test_util.py | 49 +++++++++++++++++++++++++----------------------
 1 file changed, 26 insertions(+), 23 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index a852eb975..b108dc146 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -80,6 +80,30 @@ def __repr__(self):
         return "TestIterableMember(%r)" % self.name
 
 
+@contextlib.contextmanager
+def _tmpdir_to_force_permission_error():
+    """Context manager to test permission errors in situations where we do not fix them."""
+    if sys.platform == "cygwin":
+        raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
+    if sys.version_info < (3, 8):
+        raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
+
+    with tempfile.TemporaryDirectory() as parent:
+        td = pathlib.Path(parent, "testdir")
+        td.mkdir()
+        (td / "x").write_bytes(b"")
+        (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
+        td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
+        yield td
+
+
+@contextlib.contextmanager
+def _tmpdir_for_file_not_found():
+    """Context manager to test errors deleting a directory that are not due to permissions."""
+    with tempfile.TemporaryDirectory() as parent:
+        yield pathlib.Path(parent, "testdir")  # It is deliberately never created.
+
+
 @ddt.ddt
 class TestUtils(TestBase):
     def setup(self):
@@ -107,6 +131,7 @@ def test_rmtree_deletes_nested_dir_with_files(self):
     @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
     def test_rmtree_deletes_dir_with_readonly_files(self):
         # Automatically works on Unix, but requires special handling on Windows.
+        # Not to be confused with _tmpdir_to_force_permission_error (which is used below).
         with tempfile.TemporaryDirectory() as parent:
             td = pathlib.Path(parent, "testdir")
             for d in td, td / "sub":
@@ -122,31 +147,9 @@ def test_rmtree_deletes_dir_with_readonly_files(self):
 
             self.assertFalse(td.exists())
 
-    @staticmethod
-    @contextlib.contextmanager
-    def _tmpdir_to_force_permission_error():
-        if sys.platform == "cygwin":
-            raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
-        if sys.version_info < (3, 8):
-            raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
-
-        with tempfile.TemporaryDirectory() as parent:
-            td = pathlib.Path(parent, "testdir")
-            td.mkdir()
-            (td / "x").write_bytes(b"")
-            (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
-            td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
-            yield td
-
-    @staticmethod
-    @contextlib.contextmanager
-    def _tmpdir_for_file_not_found():
-        with tempfile.TemporaryDirectory() as parent:
-            yield pathlib.Path(parent, "testdir")  # It is deliberately never created.
-
     def test_rmtree_can_wrap_exceptions(self):
         """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
-        with self._tmpdir_to_force_permission_error() as td:
+        with _tmpdir_to_force_permission_error() as td:
             # Access the module through sys.modules so it is unambiguous which module's
             # attribute we patch: the original git.util, not git.index.util even though
             # git.index.util "replaces" git.util and is what "import git.util" gives us.

From b8e009e8d31e32a2c0e247e0a7dc41ccdd3556e7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 13:59:03 -0400
Subject: [PATCH 170/994] In rmtree, have onerror catch only PermissionError

The onerror function is still called on, and tries to resolve, any
exception. But now, when it re-calls the file deletion function
passed as func, the only exception it catches to conditionally
convert to SkipTest is PermissionError (or derived exceptions).

The old behavior of catching Exception was overly broad, and
inconsistent with the hard-coded prefix of "FIXME: fails with:
PermissionError" used to build the SkipTest exception messages.

This commit also changes the message to use an f-string (which was
one of the styles in the equivalent but differently coded duplicate
logic eliminated in 5039df3, and seems clearer in this case). That
change is a pure refactoring, not affecting generated messages.
---
 git/util.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/util.py b/git/util.py
index 48901ba0c..c0e40d7e6 100644
--- a/git/util.py
+++ b/git/util.py
@@ -188,11 +188,11 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None:
 
         try:
             func(path)  # Will scream if still not possible to delete.
-        except Exception as ex:
+        except PermissionError as ex:
             if HIDE_WINDOWS_KNOWN_ERRORS:
                 from unittest import SkipTest
 
-                raise SkipTest("FIXME: fails with: PermissionError\n  {}".format(ex)) from ex
+                raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
     return shutil.rmtree(path, False, onerror)

From ccbb2732efcfa265568f1a535a8b746ed07ed82a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 18:14:19 -0400
Subject: [PATCH 171/994] Fix onerror callback type hinting, improve style

The onerror parameter of shutil.rmtree receives a 3-tuple like what
sys.exc_info() gives, not a string. Since we are not using that
parameter anyway, I've fixed it in the onerror function defined
in git.util.rmtree by changing it to Any rather than hinting it
narrowly.

I've also renamed the parameters so the names are based on those
that are documented in the shutil.rmtree documentation. The names
are not part of the function's interface (this follows both from
the documentation and the typeshed hints) but using those names may
make it easier to understand the function.

- func is renamed to function.

- path remains path.

- exc_info is renamed to _excinfo. This parameter is documented as
  excinfo, but I've prepended an underscore to signifity that it is
  unused.

These changes are to a local function and non-breaking.

Finally, although not all type checkers will flag this as an error
automatically, the git.util.rmtree function, like the shutil.rmtree
function it calls, is conceptually void, so it should not have any
return statements with operands. Because the return statement
appeared at the end, I've removed "return".
---
 git/util.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/util.py b/git/util.py
index c0e40d7e6..6e1e95e49 100644
--- a/git/util.py
+++ b/git/util.py
@@ -182,12 +182,12 @@ def rmtree(path: PathLike) -> None:
     :note: we use shutil rmtree but adjust its behaviour to see whether files that
         couldn't be deleted are read-only. Windows will not remove them in that case"""
 
-    def onerror(func: Callable, path: PathLike, exc_info: str) -> None:
+    def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None:
         # Is the error an access error ?
         os.chmod(path, stat.S_IWUSR)
 
         try:
-            func(path)  # Will scream if still not possible to delete.
+            function(path)  # Will scream if still not possible to delete.
         except PermissionError as ex:
             if HIDE_WINDOWS_KNOWN_ERRORS:
                 from unittest import SkipTest
@@ -195,7 +195,7 @@ def onerror(func: Callable, path: PathLike, exc_info: str) -> None:
                 raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
-    return shutil.rmtree(path, False, onerror)
+    shutil.rmtree(path, False, onerror)
 
 
 def rmfile(path: PathLike) -> None:

From 0b88012471d8021fffe61beb9d058840c0235f5d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 20:16:38 -0400
Subject: [PATCH 172/994] Use onexc callback where supported

The shutil.rmtree callback defined as a local function in
git.util.rmtree was already capable of being used as both the old
onerror parameter and the new onexc parameter--introduced in Python
3.12, which also deprecates onerror--because they differ only in
the meaning of their third parameter (excinfo), which the callback
defined in git.util.rmtree does not use.

This modifies git.util.rmtree to pass it as onexc on 3.12 and
later, while still passing it as onerror on 3.11 and earlier.

Because the default value of ignore_errors is False, this makes the
varying logic clearer by omitting that argument and using a keyword
argument both when passing onexc (which is keyword-only) and when
passing onerror (which is not keyword-only but can only be passed
positionally if ignore_errors is passed explicitly).
---
 git/util.py | 28 ++++++++++++----------------
 1 file changed, 12 insertions(+), 16 deletions(-)

diff --git a/git/util.py b/git/util.py
index 6e1e95e49..8a4a20253 100644
--- a/git/util.py
+++ b/git/util.py
@@ -5,24 +5,24 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
 from abc import abstractmethod
-import os.path as osp
-from .compat import is_win
 import contextlib
 from functools import wraps
 import getpass
 import logging
 import os
+import os.path as osp
+import pathlib
 import platform
-import subprocess
 import re
 import shutil
 import stat
-from sys import maxsize
+import subprocess
+import sys
 import time
 from urllib.parse import urlsplit, urlunsplit
 import warnings
 
-# from git.objects.util import Traversable
+from .compat import is_win
 
 # typing ---------------------------------------------------------
 
@@ -42,22 +42,17 @@
     Tuple,
     TypeVar,
     Union,
-    cast,
     TYPE_CHECKING,
+    cast,
     overload,
 )
 
-import pathlib
-
 if TYPE_CHECKING:
     from git.remote import Remote
     from git.repo.base import Repo
     from git.config import GitConfigParser, SectionConstraint
     from git import Git
 
-    # from git.objects.base import IndexObject
-
-
 from .types import (
     Literal,
     SupportsIndex,
@@ -75,7 +70,6 @@
 
 # ---------------------------------------------------------------------
 
-
 from gitdb.util import (  # NOQA @IgnorePep8
     make_sha,
     LockedFD,  # @UnusedImport
@@ -88,7 +82,6 @@
     hex_to_bin,  # @UnusedImport
 )
 
-
 # NOTE:  Some of the unused imports might be used/imported by others.
 # Handle once test-cases are back up and running.
 # Most of these are unused here, but are for use by git-python modules so these
@@ -182,7 +175,7 @@ def rmtree(path: PathLike) -> None:
     :note: we use shutil rmtree but adjust its behaviour to see whether files that
         couldn't be deleted are read-only. Windows will not remove them in that case"""
 
-    def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None:
+    def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
         # Is the error an access error ?
         os.chmod(path, stat.S_IWUSR)
 
@@ -195,7 +188,10 @@ def onerror(function: Callable, path: PathLike, _excinfo: Any) -> None:
                 raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
-    shutil.rmtree(path, False, onerror)
+    if sys.version_info >= (3, 12):
+        shutil.rmtree(path, onexc=handler)
+    else:
+        shutil.rmtree(path, onerror=handler)
 
 
 def rmfile(path: PathLike) -> None:
@@ -995,7 +991,7 @@ def __init__(
         self,
         file_path: PathLike,
         check_interval_s: float = 0.3,
-        max_block_time_s: int = maxsize,
+        max_block_time_s: int = sys.maxsize,
     ) -> None:
         """Configure the instance
 

From 7dd59043b44d0f2169304f90da74b1b2f7f2b02e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Oct 2023 20:27:08 -0400
Subject: [PATCH 173/994] Revise and update rmtree docstrings and comments

---
 git/util.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/git/util.py b/git/util.py
index 8a4a20253..d7f348d48 100644
--- a/git/util.py
+++ b/git/util.py
@@ -170,17 +170,18 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]:
 
 
 def rmtree(path: PathLike) -> None:
-    """Remove the given recursively.
+    """Remove the given directory tree recursively.
 
-    :note: we use shutil rmtree but adjust its behaviour to see whether files that
-        couldn't be deleted are read-only. Windows will not remove them in that case"""
+    :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that
+        couldn't be deleted are read-only. Windows will not remove them in that case."""
 
     def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
-        # Is the error an access error ?
+        """Callback for :func:`shutil.rmtree`. Works either as ``onexc`` or ``onerror``."""
+        # Is the error an access error?
         os.chmod(path, stat.S_IWUSR)
 
         try:
-            function(path)  # Will scream if still not possible to delete.
+            function(path)
         except PermissionError as ex:
             if HIDE_WINDOWS_KNOWN_ERRORS:
                 from unittest import SkipTest

From 196cfbeefb5ab0844d37da0b1e010b6ee7cf9041 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 9 Oct 2023 03:27:58 -0400
Subject: [PATCH 174/994] Clean up test_util, reorganizing for readability

- Slightly improve import sorting, grouping, and formatting.

- Move the cygpath pairs parameters into the test class, so they
  can be immediately above the tests that use them. This was almost
  the case in the past, but stopped being the case when helpers for
  some new tests above those were introduced (and those helpers
  can't be moved inside the class without extra complexity).

- Rename TestIterableMember to _Member, so it is no longer named as
  a test class. The unittest framework wouldn't consider it one,
  since it doesn't derive from unittest.TestCase, but the pytest
  runner, which we're actually using, does. More importanly (since
  it has no test methods anyway), this makes clear to humans that
  it is a helper class for tests, rather than a class of tests.

- Improve the style of _Member, and have its __repr__ show the
  actual class of the instance, so if future tests ever use a
  derived class of it--or if its name ever changes again--the
  type name in the repr will be correct.

- Remove the setup method (of TestUtils). It looks like this may
  at one time have been intended as a setUp method (note the case
  difference), but it is unused and there doesn't seem to be any
  attempt to use the instance attribute it was setting.

- Use R"" instead of r"" for raw strings representing Windows
  paths, so that some editors (at least VS Code) refrain from
  highlighting their contents as regular expressions.

- Other very minor reformatting and slight comment rewording.
---
 test/test_util.py | 101 +++++++++++++++++++++-------------------------
 1 file changed, 46 insertions(+), 55 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index b108dc146..7b37918d1 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -5,6 +5,7 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
 import contextlib
+from datetime import datetime
 import os
 import pathlib
 import pickle
@@ -13,7 +14,6 @@
 import tempfile
 import time
 from unittest import SkipTest, mock, skipIf, skipUnless
-from datetime import datetime
 
 import ddt
 import pytest
@@ -28,10 +28,6 @@
     utctz_to_altz,
     verify_utctz,
 )
-from test.lib import (
-    TestBase,
-    with_rw_repo,
-)
 from git.util import (
     Actor,
     BlockingLockFile,
@@ -43,41 +39,19 @@
     remove_password_if_present,
     rmtree,
 )
+from test.lib import TestBase, with_rw_repo
 
 
-_norm_cygpath_pairs = (
-    (r"foo\bar", "foo/bar"),
-    (r"foo/bar", "foo/bar"),
-    (r"C:\Users", "/cygdrive/c/Users"),
-    (r"C:\d/e", "/cygdrive/c/d/e"),
-    ("C:\\", "/cygdrive/c/"),
-    (r"\\server\C$\Users", "//server/C$/Users"),
-    (r"\\server\C$", "//server/C$"),
-    ("\\\\server\\c$\\", "//server/c$/"),
-    (r"\\server\BAR/", "//server/BAR/"),
-    (r"D:/Apps", "/cygdrive/d/Apps"),
-    (r"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"),
-    (r"D:\Apps/123", "/cygdrive/d/Apps/123"),
-)
-
-_unc_cygpath_pairs = (
-    (r"\\?\a:\com", "/cygdrive/a/com"),
-    (r"\\?\a:/com", "/cygdrive/a/com"),
-    (r"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
-)
-
-
-class TestIterableMember(object):
-
-    """A member of an iterable list"""
+class _Member:
+    """A member of an IterableList."""
 
-    __slots__ = "name"
+    __slots__ = ("name",)
 
     def __init__(self, name):
         self.name = name
 
     def __repr__(self):
-        return "TestIterableMember(%r)" % self.name
+        return f"{type(self).__name__}({self.name!r})"
 
 
 @contextlib.contextmanager
@@ -106,13 +80,6 @@ def _tmpdir_for_file_not_found():
 
 @ddt.ddt
 class TestUtils(TestBase):
-    def setup(self):
-        self.testdict = {
-            "string": "42",
-            "int": 42,
-            "array": [42],
-        }
-
     def test_rmtree_deletes_nested_dir_with_files(self):
         with tempfile.TemporaryDirectory() as parent:
             td = pathlib.Path(parent, "testdir")
@@ -131,7 +98,7 @@ def test_rmtree_deletes_nested_dir_with_files(self):
     @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
     def test_rmtree_deletes_dir_with_readonly_files(self):
         # Automatically works on Unix, but requires special handling on Windows.
-        # Not to be confused with _tmpdir_to_force_permission_error (which is used below).
+        # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below).
         with tempfile.TemporaryDirectory() as parent:
             td = pathlib.Path(parent, "testdir")
             for d in td, td / "sub":
@@ -179,6 +146,27 @@ def test_rmtree_does_not_wrap_unless_called_for(self, case):
                         except SkipTest as ex:
                             self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
+    _norm_cygpath_pairs = (
+        (R"foo\bar", "foo/bar"),
+        (R"foo/bar", "foo/bar"),
+        (R"C:\Users", "/cygdrive/c/Users"),
+        (R"C:\d/e", "/cygdrive/c/d/e"),
+        ("C:\\", "/cygdrive/c/"),
+        (R"\\server\C$\Users", "//server/C$/Users"),
+        (R"\\server\C$", "//server/C$"),
+        ("\\\\server\\c$\\", "//server/c$/"),
+        (R"\\server\BAR/", "//server/BAR/"),
+        (R"D:/Apps", "/cygdrive/d/Apps"),
+        (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"),
+        (R"D:\Apps/123", "/cygdrive/d/Apps/123"),
+    )
+
+    _unc_cygpath_pairs = (
+        (R"\\?\a:\com", "/cygdrive/a/com"),
+        (R"\\?\a:/com", "/cygdrive/a/com"),
+        (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
+    )
+
     # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them).
     @pytest.mark.xfail(
         reason="Many return paths prefixed /proc/cygdrive instead.",
@@ -192,16 +180,16 @@ def test_cygpath_ok(self, case):
         self.assertEqual(cwpath, cpath, wpath)
 
     @pytest.mark.xfail(
-        reason=r'2nd example r".\bar" -> "bar" fails, returns "./bar"',
+        reason=R'2nd example r".\bar" -> "bar" fails, returns "./bar"',
         raises=AssertionError,
     )
     @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
-        (r"./bar", "bar"),
-        (r".\bar", "bar"),  # FIXME: Mark only this one xfail, somehow (or fix it).
-        (r"../bar", "../bar"),
-        (r"..\bar", "../bar"),
-        (r"../bar/.\foo/../chu", "../bar/chu"),
+        (R"./bar", "bar"),
+        (R".\bar", "bar"),  # FIXME: Mark only this one xfail, somehow (or fix it).
+        (R"../bar", "../bar"),
+        (R"..\bar", "../bar"),
+        (R"../bar/.\foo/../chu", "../bar/chu"),
     )
     def test_cygpath_norm_ok(self, case):
         wpath, cpath = case
@@ -210,12 +198,12 @@ def test_cygpath_norm_ok(self, case):
 
     @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
     @ddt.data(
-        r"C:",
-        r"C:Relative",
-        r"D:Apps\123",
-        r"D:Apps/123",
-        r"\\?\a:rel",
-        r"\\share\a:rel",
+        R"C:",
+        R"C:Relative",
+        R"D:Apps\123",
+        R"D:Apps/123",
+        R"\\?\a:rel",
+        R"\\share\a:rel",
     )
     def test_cygpath_invalids(self, wpath):
         cwpath = cygpath(wpath)
@@ -380,15 +368,18 @@ def test_actor_from_string(self):
             Actor("name last another", "some-very-long-email@example.com"),
         )
 
-    @ddt.data(("name", ""), ("name", "prefix_"))
+    @ddt.data(
+        ("name", ""),
+        ("name", "prefix_"),
+    )
     def test_iterable_list(self, case):
         name, prefix = case
         ilist = IterableList(name, prefix)
 
         name1 = "one"
         name2 = "two"
-        m1 = TestIterableMember(prefix + name1)
-        m2 = TestIterableMember(prefix + name2)
+        m1 = _Member(prefix + name1)
+        m2 = _Member(prefix + name2)
 
         ilist.extend((m1, m2))
 

From 100ab989fcba0b1d1bd89b5b4b41ea5014da3d82 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 15:31:25 -0400
Subject: [PATCH 175/994] Add initial test_env_vars_for_windows_tests

The new test method just verifies the current behavior of the
HIDE_WINDOWS_KNOWN_ERRORS and HIDE_WINDOWS_FREEZE_ERRORS
environment variables. This is so there is a test to modify when
changing that behavior. The purpose of these tests is *not* to
claim that the behavior of either variable is something code that
uses GitPython can (or has ever been able to) rely on.

This also adds pytest-subtests as a dependency, so multiple
failures from the subtests can be seen in the same test run.
---
 test-requirements.txt |  1 +
 test/test_util.py     | 45 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)

diff --git a/test-requirements.txt b/test-requirements.txt
index 9414da09c..a69181be1 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,4 +7,5 @@ pre-commit
 pytest
 pytest-cov
 pytest-instafail
+pytest-subtests
 pytest-sugar
diff --git a/test/test_util.py b/test/test_util.py
index 7b37918d1..d65b25d49 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -4,12 +4,14 @@
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
+import ast
 import contextlib
 from datetime import datetime
 import os
 import pathlib
 import pickle
 import stat
+import subprocess
 import sys
 import tempfile
 import time
@@ -502,3 +504,46 @@ def test_remove_password_from_command_line(self):
 
         assert cmd_4 == remove_password_if_present(cmd_4)
         assert cmd_5 == remove_password_if_present(cmd_5)
+
+    @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS")
+    def test_env_vars_for_windows_tests(self, name):
+        def run_parse(value):
+            command = [
+                sys.executable,
+                "-c",
+                f"from git.util import {name}; print(repr({name}))",
+            ]
+            output = subprocess.check_output(
+                command,
+                env=None if value is None else dict(os.environ, **{name: value}),
+                text=True,
+            )
+            return ast.literal_eval(output)
+
+        assert_true_iff_win = self.assertTrue if os.name == "nt" else self.assertFalse
+
+        truthy_cases = [
+            ("unset", None),
+            ("true-seeming", "1"),
+            ("true-seeming", "true"),
+            ("true-seeming", "True"),
+            ("true-seeming", "yes"),
+            ("true-seeming", "YES"),
+            ("false-seeming", "0"),
+            ("false-seeming", "false"),
+            ("false-seeming", "False"),
+            ("false-seeming", "no"),
+            ("false-seeming", "NO"),
+            ("whitespace", " "),
+        ]
+        falsy_cases = [
+            ("empty", ""),
+        ]
+
+        for msg, env_var_value in truthy_cases:
+            with self.subTest(msg, env_var_value=env_var_value):
+                assert_true_iff_win(run_parse(env_var_value))
+
+        for msg, env_var_value in falsy_cases:
+            with self.subTest(msg, env_var_value=env_var_value):
+                self.assertFalse(run_parse(env_var_value))

From 7604da185ce12b9ef540aff3255580db02c88268 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 16:06:49 -0400
Subject: [PATCH 176/994] Warn if HIDE_WINDOWS_*_ERRORS set in environment

This warns if the HIDE_WINDOWS_KNOWN_ERRORS or
HIDE_WINDOWS_FREEZE_ERRORS environment variables are set. These
behave unexpectedly, including (and especially) in their effect on
the same-named git.util module attributes, and neither their
effects nor those of those attributes are documented in a way that
would have supported code outside the project relying on their
specific semantics.

The new warning message characterizes their status as deprecated.

- This is now the case for HIDE_WINDOWS_KNOWN_ERRORS, and
  almost so for the same-named attribute, whose existence (though
  not its meaning) can technically be relied on due to inclusion in
  `__all__` (which this does *not* change).

- But the HIDE_WINDOWS_FREEZE_ERRORS attribute was never guaranteed
  even to exist, so technically neither it nor the same-named
  environment variable are not *even* deprecated. The attribute's
  presence has never been reflected in the public interface of any
  GitPython component in any way.

However, these attributes are still used by the tests. Furthermore,
in the case of HIDE_WINDOWS_KNOWN_ERRORS, setting it is the only
way to disable the behavior of converting errors from some file
deletion operations into SkipTest exceptions on Windows. Since that
behavior has not yet changed, but is unlikely to be desired outside
of testing, no *attributes* are deprecated at this time, and no
effort to warn from accessing or using attributes is attempted.
---
 git/util.py | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/git/util.py b/git/util.py
index d7f348d48..5ffe93e70 100644
--- a/git/util.py
+++ b/git/util.py
@@ -109,14 +109,28 @@
 
 log = logging.getLogger(__name__)
 
-# types############################################################
+
+def _read_env_flag(name: str, default: bool) -> Union[bool, str]:
+    try:
+        value = os.environ[name]
+    except KeyError:
+        return default
+
+    log.warning(
+        "The %s environment variable is deprecated. Its effect has never been documented and changes without warning.",
+        name,
+    )
+
+    # FIXME: This should always return bool, as that is how it is used.
+    # FIXME: This should treat some values besides "" as expressing falsehood.
+    return value
 
 
 #: We need an easy way to see if Appveyor TCs start failing,
 #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
 #: till then, we wish to hide them.
-HIDE_WINDOWS_KNOWN_ERRORS = is_win and os.environ.get("HIDE_WINDOWS_KNOWN_ERRORS", True)
-HIDE_WINDOWS_FREEZE_ERRORS = is_win and os.environ.get("HIDE_WINDOWS_FREEZE_ERRORS", True)
+HIDE_WINDOWS_KNOWN_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_KNOWN_ERRORS", True)
+HIDE_WINDOWS_FREEZE_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_FREEZE_ERRORS", True)
 
 # { Utility Methods
 

From eb51277e73ca274e3948a3f009168d210dd587ca Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 17:01:46 -0400
Subject: [PATCH 177/994] Make HIDE_* attributes always bool

For now, this doesn't change how the correspondng environment
variables are interpreted, in terms of truth and falsehood.
But it does *convert* them to bool, so that the values of the
HIDE_WINDOWS_KNOWN_ERRORS and HIDE_WINDOWS_FREEZE_ERRORS
attributes are always bools.

It also updates the tests accordingly, to validate this behavior.
---
 git/util.py       | 5 ++---
 test/test_util.py | 6 +++---
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/git/util.py b/git/util.py
index 5ffe93e70..a6153f1f3 100644
--- a/git/util.py
+++ b/git/util.py
@@ -110,7 +110,7 @@
 log = logging.getLogger(__name__)
 
 
-def _read_env_flag(name: str, default: bool) -> Union[bool, str]:
+def _read_env_flag(name: str, default: bool) -> bool:
     try:
         value = os.environ[name]
     except KeyError:
@@ -121,9 +121,8 @@ def _read_env_flag(name: str, default: bool) -> Union[bool, str]:
         name,
     )
 
-    # FIXME: This should always return bool, as that is how it is used.
     # FIXME: This should treat some values besides "" as expressing falsehood.
-    return value
+    return bool(value)
 
 
 #: We need an easy way to see if Appveyor TCs start failing,
diff --git a/test/test_util.py b/test/test_util.py
index d65b25d49..5912ea4a0 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -520,7 +520,7 @@ def run_parse(value):
             )
             return ast.literal_eval(output)
 
-        assert_true_iff_win = self.assertTrue if os.name == "nt" else self.assertFalse
+        true_iff_win = os.name == "nt"  # Same as is_win, but don't depend on that here.
 
         truthy_cases = [
             ("unset", None),
@@ -542,8 +542,8 @@ def run_parse(value):
 
         for msg, env_var_value in truthy_cases:
             with self.subTest(msg, env_var_value=env_var_value):
-                assert_true_iff_win(run_parse(env_var_value))
+                self.assertIs(run_parse(env_var_value), true_iff_win)
 
         for msg, env_var_value in falsy_cases:
             with self.subTest(msg, env_var_value=env_var_value):
-                self.assertFalse(run_parse(env_var_value))
+                self.assertIs(run_parse(env_var_value), False)

From 333896b4447c56093fa4ae402a3d22491928ce29 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 17:29:04 -0400
Subject: [PATCH 178/994] Treat false-seeming HIDE_* env var values as false

This changes how HIDE_WINDOWS_KNOWN_ERRORS and
HIDE_WINDOWS_FREEZE_ERRORS environment variables, if present, are
interpreted, so that values that strongly seem intuitivley to
represent falsehood now do.

Before, only the empty string was treated as false. Now:

- "0", "false", "no", and their case variants, as well as the empty
  string, are treated as false.

- The presence of leading and trailing whitespace in the value now
  longer changes the truth value it represents. For example,
  all-whitespace (e.g., a space) is treated as false.

- Values other than the above false values, and the recognized true
  values "1", "true", "yes", and their variants, are still treated
  as true, but issue a warning about how they are unrecognied.
---
 git/util.py       | 10 ++++++++--
 test/test_util.py |  8 ++++----
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/git/util.py b/git/util.py
index a6153f1f3..97f461a83 100644
--- a/git/util.py
+++ b/git/util.py
@@ -121,8 +121,14 @@ def _read_env_flag(name: str, default: bool) -> bool:
         name,
     )
 
-    # FIXME: This should treat some values besides "" as expressing falsehood.
-    return bool(value)
+    adjusted_value = value.strip().lower()
+
+    if adjusted_value in {"", "0", "false", "no"}:
+        return False
+    if adjusted_value in {"1", "true", "yes"}:
+        return True
+    log.warning("%s has unrecognized value %r, treating as %r.", name, value, default)
+    return default
 
 
 #: We need an easy way to see if Appveyor TCs start failing,
diff --git a/test/test_util.py b/test/test_util.py
index 5912ea4a0..647a02833 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -529,15 +529,15 @@ def run_parse(value):
             ("true-seeming", "True"),
             ("true-seeming", "yes"),
             ("true-seeming", "YES"),
+        ]
+        falsy_cases = [
+            ("empty", ""),
+            ("whitespace", " "),
             ("false-seeming", "0"),
             ("false-seeming", "false"),
             ("false-seeming", "False"),
             ("false-seeming", "no"),
             ("false-seeming", "NO"),
-            ("whitespace", " "),
-        ]
-        falsy_cases = [
-            ("empty", ""),
         ]
 
         for msg, env_var_value in truthy_cases:

From c11b36660382c713709b36bbca1da8a1acb3a4ec Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Oct 2023 17:52:27 -0400
Subject: [PATCH 179/994] Simplify HIDE_* env var test; add missing cases

Now that the expected truth values are intuitive, it is no longer
necessary to group them by result and include messages that
acknowldge the unintuitive cases. This reorders them so that pairs
(like "yes" and "no") appear together, removes the messages that
are no longer necessary, and reduces test code duplication.

This also adds cases to test leading/trailing whitespace in
otherwise nonempty strings, so it is clearer what the test is
asserting overall, and so a bug where lstrip or rstrip (or
equivalent with a regex) were used instead strip would be caught.
---
 test/test_util.py | 46 +++++++++++++++++++---------------------------
 1 file changed, 19 insertions(+), 27 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 647a02833..a4c06b80e 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -520,30 +520,22 @@ def run_parse(value):
             )
             return ast.literal_eval(output)
 
-        true_iff_win = os.name == "nt"  # Same as is_win, but don't depend on that here.
-
-        truthy_cases = [
-            ("unset", None),
-            ("true-seeming", "1"),
-            ("true-seeming", "true"),
-            ("true-seeming", "True"),
-            ("true-seeming", "yes"),
-            ("true-seeming", "YES"),
-        ]
-        falsy_cases = [
-            ("empty", ""),
-            ("whitespace", " "),
-            ("false-seeming", "0"),
-            ("false-seeming", "false"),
-            ("false-seeming", "False"),
-            ("false-seeming", "no"),
-            ("false-seeming", "NO"),
-        ]
-
-        for msg, env_var_value in truthy_cases:
-            with self.subTest(msg, env_var_value=env_var_value):
-                self.assertIs(run_parse(env_var_value), true_iff_win)
-
-        for msg, env_var_value in falsy_cases:
-            with self.subTest(msg, env_var_value=env_var_value):
-                self.assertIs(run_parse(env_var_value), False)
+        for env_var_value, expected_truth_value in (
+            (None, os.name == "nt"),  # True on Windows when the environment variable is unset.
+            ("", False),
+            (" ", False),
+            ("0", False),
+            ("1", os.name == "nt"),
+            ("false", False),
+            ("true", os.name == "nt"),
+            ("False", False),
+            ("True", os.name == "nt"),
+            ("no", False),
+            ("yes", os.name == "nt"),
+            ("NO", False),
+            ("YES", os.name == "nt"),
+            (" no  ", False),
+            (" yes  ", os.name == "nt"),
+        ):
+            with self.subTest(env_var_value=env_var_value):
+                self.assertIs(run_parse(env_var_value), expected_truth_value)

From f0e15e8935580a4e4dc4ed6490c9e1b229493e19 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 9 Oct 2023 04:20:09 -0400
Subject: [PATCH 180/994] Further cleanup in test_util (on new tests)

The main change here is to move the tests of how the HIDE_*
environment variables are treated up near the rmtree tests, since
they although the behaviors being tested are separate, they are
conceptually related, and also not entirely independent in that
they both involve the HIDE_WINDOWS_KNOWN_ERRORS attribute.

Other changes are mostly to formatting.
---
 test/test_util.py | 94 +++++++++++++++++++++++++++--------------------
 1 file changed, 54 insertions(+), 40 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index a4c06b80e..b20657fb1 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -87,7 +87,14 @@ def test_rmtree_deletes_nested_dir_with_files(self):
             td = pathlib.Path(parent, "testdir")
             for d in td, td / "q", td / "s":
                 d.mkdir()
-            for f in td / "p", td / "q" / "w", td / "q" / "x", td / "r", td / "s" / "y", td / "s" / "z":
+            for f in (
+                td / "p",
+                td / "q" / "w",
+                td / "q" / "x",
+                td / "r",
+                td / "s" / "y",
+                td / "s" / "z",
+            ):
                 f.write_bytes(b"")
 
             try:
@@ -97,7 +104,10 @@ def test_rmtree_deletes_nested_dir_with_files(self):
 
             self.assertFalse(td.exists())
 
-    @skipIf(sys.platform == "cygwin", "Cygwin can't set the permissions that make the test meaningful.")
+    @skipIf(
+        sys.platform == "cygwin",
+        "Cygwin can't set the permissions that make the test meaningful.",
+    )
     def test_rmtree_deletes_dir_with_readonly_files(self):
         # Automatically works on Unix, but requires special handling on Windows.
         # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below).
@@ -117,7 +127,7 @@ def test_rmtree_deletes_dir_with_readonly_files(self):
             self.assertFalse(td.exists())
 
     def test_rmtree_can_wrap_exceptions(self):
-        """Our rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
         with _tmpdir_to_force_permission_error() as td:
             # Access the module through sys.modules so it is unambiguous which module's
             # attribute we patch: the original git.util, not git.index.util even though
@@ -135,12 +145,16 @@ def test_rmtree_can_wrap_exceptions(self):
         (True, FileNotFoundError, _tmpdir_for_file_not_found),
     )
     def test_rmtree_does_not_wrap_unless_called_for(self, case):
-        """Our rmtree doesn't wrap non-PermissionError, nor when HIDE_WINDOWS_KNOWN_ERRORS is false."""
+        """rmtree doesn't wrap non-PermissionError, nor if HIDE_WINDOWS_KNOWN_ERRORS is false."""
         hide_windows_known_errors, exception_type, tmpdir_context_factory = case
 
         with tmpdir_context_factory() as td:
             # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here.
-            with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors):
+            with mock.patch.object(
+                sys.modules["git.util"],
+                "HIDE_WINDOWS_KNOWN_ERRORS",
+                hide_windows_known_errors,
+            ):
                 with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"):
                     with self.assertRaises(exception_type):
                         try:
@@ -148,6 +162,41 @@ def test_rmtree_does_not_wrap_unless_called_for(self, case):
                         except SkipTest as ex:
                             self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
+    @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS")
+    def test_env_vars_for_windows_tests(self, name):
+        def run_parse(value):
+            command = [
+                sys.executable,
+                "-c",
+                f"from git.util import {name}; print(repr({name}))",
+            ]
+            output = subprocess.check_output(
+                command,
+                env=None if value is None else dict(os.environ, **{name: value}),
+                text=True,
+            )
+            return ast.literal_eval(output)
+
+        for env_var_value, expected_truth_value in (
+            (None, os.name == "nt"),  # True on Windows when the environment variable is unset.
+            ("", False),
+            (" ", False),
+            ("0", False),
+            ("1", os.name == "nt"),
+            ("false", False),
+            ("true", os.name == "nt"),
+            ("False", False),
+            ("True", os.name == "nt"),
+            ("no", False),
+            ("yes", os.name == "nt"),
+            ("NO", False),
+            ("YES", os.name == "nt"),
+            (" no  ", False),
+            (" yes  ", os.name == "nt"),
+        ):
+            with self.subTest(env_var_value=env_var_value):
+                self.assertIs(run_parse(env_var_value), expected_truth_value)
+
     _norm_cygpath_pairs = (
         (R"foo\bar", "foo/bar"),
         (R"foo/bar", "foo/bar"),
@@ -504,38 +553,3 @@ def test_remove_password_from_command_line(self):
 
         assert cmd_4 == remove_password_if_present(cmd_4)
         assert cmd_5 == remove_password_if_present(cmd_5)
-
-    @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS")
-    def test_env_vars_for_windows_tests(self, name):
-        def run_parse(value):
-            command = [
-                sys.executable,
-                "-c",
-                f"from git.util import {name}; print(repr({name}))",
-            ]
-            output = subprocess.check_output(
-                command,
-                env=None if value is None else dict(os.environ, **{name: value}),
-                text=True,
-            )
-            return ast.literal_eval(output)
-
-        for env_var_value, expected_truth_value in (
-            (None, os.name == "nt"),  # True on Windows when the environment variable is unset.
-            ("", False),
-            (" ", False),
-            ("0", False),
-            ("1", os.name == "nt"),
-            ("false", False),
-            ("true", os.name == "nt"),
-            ("False", False),
-            ("True", os.name == "nt"),
-            ("no", False),
-            ("yes", os.name == "nt"),
-            ("NO", False),
-            ("YES", os.name == "nt"),
-            (" no  ", False),
-            (" yes  ", os.name == "nt"),
-        ):
-            with self.subTest(env_var_value=env_var_value):
-                self.assertIs(run_parse(env_var_value), expected_truth_value)

From a9b05ece674578d3417f8969ade17b06ab287ffe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 9 Oct 2023 07:59:32 -0400
Subject: [PATCH 181/994] Clarify a test helper docstring

The wording was ambiguous before, because fixing a PermissionError
could mean addressing it successfully at runtime (which is the
intended meaning in that docstring) but it could also mean fixing
a bug or test case related to such an error (not intended).
---
 test/test_util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_util.py b/test/test_util.py
index b20657fb1..f75231c98 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -58,7 +58,7 @@ def __repr__(self):
 
 @contextlib.contextmanager
 def _tmpdir_to_force_permission_error():
-    """Context manager to test permission errors in situations where we do not fix them."""
+    """Context manager to test permission errors in situations where they are not overcome."""
     if sys.platform == "cygwin":
         raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
     if sys.version_info < (3, 8):

From ebd9ce50ebd702b0655384db0d509113c70ad94c Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Tue, 10 Oct 2023 15:03:43 -0600
Subject: [PATCH 182/994] Fix unused import linting

---
 git/exc.py | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/git/exc.py b/git/exc.py
index 936381fd8..66cf9ad06 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -5,16 +5,16 @@
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
 """ Module containing all exceptions thrown throughout the git package, """
 
-from gitdb.exc import (
-    AmbiguousObjectName,  # @UnusedImport
-    BadName,  # @UnusedImport
-    BadObject,  # @UnusedImport
-    BadObjectType,  # @UnusedImport
-    InvalidDBRoot,  # @UnusedImport
-    ODBError,  # @UnusedImport
-    ParseError,  # @UnusedImport
-    UnsupportedOperation,  # @UnusedImport
-    to_hex_sha,  # @UnusedImport
+from gitdb.exc import (  # noqa: @UnusedImport
+    AmbiguousObjectName,
+    BadName,
+    BadObject,
+    BadObjectType,
+    InvalidDBRoot,
+    ODBError,
+    ParseError,
+    UnsupportedOperation,
+    to_hex_sha,
 )
 from git.compat import safe_decode
 from git.util import remove_password_if_present

From b1478bb1091e24dc001798da6d28bb366baf733f Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Tue, 10 Oct 2023 15:19:01 -0600
Subject: [PATCH 183/994] Typo

---
 git/exc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/exc.py b/git/exc.py
index 66cf9ad06..5110d502d 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -3,7 +3,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
-""" Module containing all exceptions thrown throughout the git package, """
+""" Module containing all exceptions thrown throughout the git package """
 
 from gitdb.exc import (  # noqa: @UnusedImport
     AmbiguousObjectName,

From a4eb2b3ea7680ac4da01d72746d9e82aa0cb0422 Mon Sep 17 00:00:00 2001
From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Date: Tue, 10 Oct 2023 15:24:15 -0600
Subject: [PATCH 184/994] black lint

---
 git/__init__.py | 150 ++++++++++++++++++++++++------------------------
 1 file changed, 75 insertions(+), 75 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 00e7b8dd3..ef7493079 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -64,81 +64,81 @@ def _init_externals() -> None:
 # __all__ must be statically defined by py.typed support
 # __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
 __all__ = [
-    'Actor',
-    'AmbiguousObjectName',
-    'BadName',
-    'BadObject',
-    'BadObjectType',
-    'BaseIndexEntry',
-    'Blob',
-    'BlobFilter',
-    'BlockingLockFile',
-    'CacheError',
-    'CheckoutError',
-    'CommandError',
-    'Commit',
-    'Diff',
-    'DiffIndex',
-    'Diffable',
-    'FetchInfo',
-    'Git',
-    'GitCmdObjectDB',
-    'GitCommandError',
-    'GitCommandNotFound',
-    'GitConfigParser',
-    'GitDB',
-    'GitError',
-    'HEAD',
-    'Head',
-    'HookExecutionError',
-    'IndexEntry',
-    'IndexFile',
-    'IndexObject',
-    'InvalidDBRoot',
-    'InvalidGitRepositoryError',
-    'List',
-    'LockFile',
-    'NULL_TREE',
-    'NoSuchPathError',
-    'ODBError',
-    'Object',
-    'Optional',
-    'ParseError',
-    'PathLike',
-    'PushInfo',
-    'RefLog',
-    'RefLogEntry',
-    'Reference',
-    'Remote',
-    'RemoteProgress',
-    'RemoteReference',
-    'Repo',
-    'RepositoryDirtyError',
-    'RootModule',
-    'RootUpdateProgress',
-    'Sequence',
-    'StageType',
-    'Stats',
-    'Submodule',
-    'SymbolicReference',
-    'TYPE_CHECKING',
-    'Tag',
-    'TagObject',
-    'TagReference',
-    'Tree',
-    'TreeModifier',
-    'Tuple',
-    'Union',
-    'UnmergedEntriesError',
-    'UnsafeOptionError',
-    'UnsafeProtocolError',
-    'UnsupportedOperation',
-    'UpdateProgress',
-    'WorkTreeRepositoryUnsupported',
-    'remove_password_if_present',
-    'rmtree',
-    'safe_decode',
-    'to_hex_sha',
+    "Actor",
+    "AmbiguousObjectName",
+    "BadName",
+    "BadObject",
+    "BadObjectType",
+    "BaseIndexEntry",
+    "Blob",
+    "BlobFilter",
+    "BlockingLockFile",
+    "CacheError",
+    "CheckoutError",
+    "CommandError",
+    "Commit",
+    "Diff",
+    "DiffIndex",
+    "Diffable",
+    "FetchInfo",
+    "Git",
+    "GitCmdObjectDB",
+    "GitCommandError",
+    "GitCommandNotFound",
+    "GitConfigParser",
+    "GitDB",
+    "GitError",
+    "HEAD",
+    "Head",
+    "HookExecutionError",
+    "IndexEntry",
+    "IndexFile",
+    "IndexObject",
+    "InvalidDBRoot",
+    "InvalidGitRepositoryError",
+    "List",
+    "LockFile",
+    "NULL_TREE",
+    "NoSuchPathError",
+    "ODBError",
+    "Object",
+    "Optional",
+    "ParseError",
+    "PathLike",
+    "PushInfo",
+    "RefLog",
+    "RefLogEntry",
+    "Reference",
+    "Remote",
+    "RemoteProgress",
+    "RemoteReference",
+    "Repo",
+    "RepositoryDirtyError",
+    "RootModule",
+    "RootUpdateProgress",
+    "Sequence",
+    "StageType",
+    "Stats",
+    "Submodule",
+    "SymbolicReference",
+    "TYPE_CHECKING",
+    "Tag",
+    "TagObject",
+    "TagReference",
+    "Tree",
+    "TreeModifier",
+    "Tuple",
+    "Union",
+    "UnmergedEntriesError",
+    "UnsafeOptionError",
+    "UnsafeProtocolError",
+    "UnsupportedOperation",
+    "UpdateProgress",
+    "WorkTreeRepositoryUnsupported",
+    "remove_password_if_present",
+    "rmtree",
+    "safe_decode",
+    "to_hex_sha",
 ]
 
 # { Initialize git executable path

From cc848d256369b01ffa9c6e1cb1c0001e6d828dcf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 11 Oct 2023 04:27:52 -0400
Subject: [PATCH 185/994] Fix small #1662 regression due to #1659

When #1659 was updated to pick up linting configuration changes, it
inadvertently undid one of the URL changes made in #1662, putting
the URL in the git.exc module back to the one that redirects to a
different BSD license from the one this project uses.

Since only that one module was affected, the fix is simple. This
only changes the URL back; it doesn't undo any other #1659 changes.
---
 git/exc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/exc.py b/git/exc.py
index 5110d502d..b4ffe7568 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 (  # noqa: @UnusedImport

From 7b1c04681a7dfcd2461be0b57f0914d15d81e782 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 01:45:06 -0400
Subject: [PATCH 186/994] Have Dependabot offer submodule updates

This extends the current Dependabot configuration so that, in
addition to offering updates for GitHub Actions, it also offers
them for git submodules (for the gitdb direct submodule).
---
 .github/dependabot.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8c139c7be..5acde1a9a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,3 +4,8 @@ updates:
   directory: "/"
   schedule:
     interval: "weekly"
+
+- package-ecosystem: "gitsubmodule"
+  directory: "/"
+  schedule:
+    interval: "monthly"

From a29a8750c914ac1d13fc784994132a077d47e686 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 02:23:07 -0400
Subject: [PATCH 187/994] Drop obsolete info on yanking from security policy

Versions may still be yanked for security reasons under specific
circumstances, but this is not the usual or most common practice in
GitPython, at least currently. Recent security updates have not
been accompanied by yanking older versions, and allowing these
versions to be selected automatically even when not called for
specifically can be good, such as to prevent an even older version
with even more vulnerabilities from being selected in situations
where for some reason the latest version cannot yet be used.

In general, users shouldn't (and don't) assume all non-yanked
versions to be free of security fixes that later versions have
received. This change updates SECURITY.md to avoid giving that
impression, but of course some versions of GitPython may still be
yanked in the future if circumstances warrant it.
---
 SECURITY.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index cf25c09ea..cbfaafdde 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,8 +2,7 @@
 
 ## Supported Versions
 
-Only the latest version of GitPython can receive security updates. If a vulnerability is discovered, a fix can be issued in a new release, while older releases
-are likely to be yanked.
+Only the latest version of GitPython can receive security updates. If a vulnerability is discovered, a fix can be issued in a new release.
 
 | Version | Supported          |
 | ------- | ------------------ |

From 05e4ca31d33733ddb3b34272bbd04b6b9cc784f1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Oct 2023 07:51:18 +0000
Subject: [PATCH 188/994] Bump git/ext/gitdb from `49c3178` to `8ec2390`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `49c3178` to `8ec2390`.
- [Commits](https://github.com/gitpython-developers/gitdb/compare/49c3178711ddb3510f0e96297187f823cc019871...8ec23904f44fbbb9bb560b83bd891a9b48be7645)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 49c317871..8ec23904f 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 49c3178711ddb3510f0e96297187f823cc019871
+Subproject commit 8ec23904f44fbbb9bb560b83bd891a9b48be7645

From 24a45f21e0a93a4bf94b45e46d8b91b03632019a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Oct 2023 08:27:51 +0000
Subject: [PATCH 189/994] Bump git/ext/gitdb from `8ec2390` to `6a22706`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `8ec2390` to `6a22706`.
- [Commits](https://github.com/gitpython-developers/gitdb/compare/8ec23904f44fbbb9bb560b83bd891a9b48be7645...6a2270635842c4287ae14d94fdc3f9e1ddd0527a)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 8ec23904f..6a2270635 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 8ec23904f44fbbb9bb560b83bd891a9b48be7645
+Subproject commit 6a2270635842c4287ae14d94fdc3f9e1ddd0527a

From ba750deec95df77b7f0819e18fa02acbb52cecdf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 17:15:16 -0400
Subject: [PATCH 190/994] Update readme for milestone-less releasing

---
 README.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/README.md b/README.md
index b9e61912a..b567d49a2 100644
--- a/README.md
+++ b/README.md
@@ -199,13 +199,12 @@ 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.
+- Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
 - Commit everything.
 - Run `git tag -s <version>` 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_.
 - 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)

From a151ea0f66bee8f810b2f93c883a2a0dbad67616 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 17:53:29 -0400
Subject: [PATCH 191/994] Make the release instructions a numbered list

Since they are sequential steps.
---
 README.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index b567d49a2..25d2a910e 100644
--- a/README.md
+++ b/README.md
@@ -198,14 +198,14 @@ 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. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
-- Commit everything.
-- Run `git tag -s <version>` 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`.\
+1. Update/verify the **version** in the `VERSION` file.
+2. Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
+3. Commit everything.
+4. Run `git tag -s <version>` to tag the version in Git.
+5. _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`.
-- Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog.
+6. Run `make release`.
+7. 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)
 

From feb4414d955cc76df496f406ae914793028a804e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 18:01:50 -0400
Subject: [PATCH 192/994] Shorten another step

---
 README.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 25d2a910e..a7942fd2f 100644
--- a/README.md
+++ b/README.md
@@ -202,8 +202,7 @@ Please have a look at the [contributions file][contributing].
 2. Update/verify that the `doc/source/changes.rst` changelog file was updated. It should include a link to the forthcoming release page: `https://github.com/gitpython-developers/GitPython/releases/tag/<version>`
 3. Commit everything.
 4. Run `git tag -s <version>` to tag the version in Git.
-5. _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.)
+5. _Optionally_ create and activate a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment). (Then the next step can install `build` and `twine`.)
 6. Run `make release`.
 7. Go to [GitHub Releases](https://github.com/gitpython-developers/GitPython/releases) and publish a new one with the recently pushed tag. Generate the changelog.
 

From e8956e53e75d59d1bda1c69307e19ae79921d813 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 13 Oct 2023 19:48:44 -0400
Subject: [PATCH 193/994] Run Cygwin CI workflow commands in login shells

This passes --login to the bash shell used to run commands in the
Cygwin environment on CI. This eliminates the need to work around a
partly broken environment, and the extra code what was used to do
that is accordingly removed. There are two benefits of this change:

- The PATH is correct: Cygwin's /usr/local/bin and /usr/bin are
  present at the beginning of PATH. Otherwise, it is easy to get
  /usr/bin at the front, but rather involved to get /usr/local/bin
  to precede it. Because Python on Cygwin puts scripts/executables
  such as the upgraded "pip" and the "pytest" command in
  /usr/local/bin, it is valuable to have that directory in the PATH
  and best to have it before /usr/bin. (I have set CYGWIN_NOWINPATH
  to omit other directories, since finding any of the commands to
  be run in the Cygwin environment outside that environment is
  unintended.)

- Every step automatically has correct temporary directories: When
  Cygwin commands were not being run in login shells, they didn't
  automatically get correct values for TMP and TEMP for their
  environment. To work around this, those environment variables
  were set globally, for every step. But that caused them to refer
  to nonexistent locations for steps such as actions/checkout. Most
  likely this would not cause any errors, but it did cause copious
  warnings about a nonexistent temporary directory, which risked
  obscuring other potentially important output. Now that Cygwin
  commands run in login shells, both the few non-Cygwin steps, and
  the steps run in the Cygwin enviroment, all get correct temporary
  directories (with TMP and TEMP set in the prewritten startup
  script the login shell uses).

A theoretical disadvantage of this is that login shells take
slightly longer to start up, but that delay is insigificant in
this application. A more significant disadvantage is that setting
the -x shell option the way it was done before would produce a lot
of noise at the beginning of the output for every command-running
step. To work around that, -x is omitted from the value of "shell"
and "set -x" is added at the end of the startup script for login
shells, so it runs before each step's "payload" command, but
without applying to the commands run in the startup script itself.
---
 .github/workflows/cygwin-test.yml | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index cd913385f..d190f1132 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -10,30 +10,36 @@ jobs:
       fail-fast: false
 
     env:
-      CHERE_INVOKING: 1
-      TMP: "/tmp"
-      TEMP: "/tmp"
+      CHERE_INVOKING: "1"
+      CYGWIN_NOWINPATH: "1"
 
     defaults:
       run:
-        shell: C:\cygwin\bin\bash.exe --noprofile --norc -exo pipefail -o igncr "{0}"
+        shell: C:\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}"
 
     steps:
     - name: Force LF line endings
       run: |
         git config --global core.autocrlf false  # Affects the non-Cygwin git.
-      shell: bash
+      shell: bash  # Use Git Bash instead of Cygwin Bash for this step.
 
     - uses: actions/checkout@v4
       with:
         fetch-depth: 0
         submodules: recursive
 
-    - uses: cygwin/cygwin-install-action@v4
+    - name: Install Cygwin
+      uses: cygwin/cygwin-install-action@v4
       with:
         packages: python39 python39-pip python39-virtualenv git
+        add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
-    - name: Special configuration for Cygwin's git
+    - name: Arrange for verbose output
+      run: |
+        # Arrange for verbose output but without shell environment setup details.
+        echo 'set -x' >~/.bash_profile
+
+    - name: Special configuration for Cygwin git
       run: |
         git config --global --add safe.directory "$(pwd)"
         git config --global core.autocrlf false
@@ -57,7 +63,7 @@ jobs:
 
     - name: Install project and test dependencies
       run: |
-        python -m pip install ".[test]"
+        pip install ".[test]"
 
     - name: Show version and platform information
       run: |
@@ -71,4 +77,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        python -m pytest --color=yes -p no:sugar --instafail -vv
+        pytest --color=yes -p no:sugar --instafail -vv

From 6f765a2028e45a14853bf06801104cc6d22e053e Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Tue, 17 Oct 2023 08:08:14 +0200
Subject: [PATCH 194/994] prepare next release

---
 VERSION                | 2 +-
 doc/source/changes.rst | 6 ++++++
 git/ext/gitdb          | 2 +-
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/VERSION b/VERSION
index 1f1a39706..3fef00337 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.37
+3.1.38
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index a789b068d..431ad03e2 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,12 @@
 Changelog
 =========
 
+3.1.38
+======
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.38
+
 3.1.37
 ======
 
diff --git a/git/ext/gitdb b/git/ext/gitdb
index 6a2270635..8ec23904f 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 6a2270635842c4287ae14d94fdc3f9e1ddd0527a
+Subproject commit 8ec23904f44fbbb9bb560b83bd891a9b48be7645

From 427c177047b526b5d15859e36b2c86fea477bfff Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 04:51:01 -0400
Subject: [PATCH 195/994] Add missing info in Submodule.remove docstring

This rewords and adds some missing information to the docstring of
Submodule.remove, for the "method" parameter, discussed in #1712.
It uses the second suggestion presented in that issue, formatted
in the style of the surrounding docstring (an 88-column wrap).

It also does some other rewording in that docstring, for clarity,
at the end of that same section (on the "method" parameter), and a
small punctuation fix in the section about the "force" parameter.
---
 git/objects/submodule/base.py | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 13d897263..473ebde76 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -948,17 +948,16 @@ def remove(
         """Remove this submodule from the repository. This will remove our entry
         from the .gitmodules file and the entry in the .git / config file.
 
-        :param module: If True, the module checkout we point to will be deleted
-            as well. If the module is currently on a commit which is not part
-            of any branch in the remote, if the currently checked out branch
-            working tree, or untracked files,
-            is ahead of its tracking branch, if you have modifications in the
+        :param module: If True, the checked out module we point to will be deleted as
+            well.If that module is currently on a commit outside any branch in the
+            remote, or if it is ahead of its tracking branch, or if there are modified
+            or untracked files in its working tree, then the removal will fail.
             In case the removal of the repository fails for these reasons, the
             submodule status will not have been altered.
-            If this submodule has child - modules on its own, these will be deleted
-            prior to touching the own module.
+            If this submodule has child modules of its own, these will be deleted prior
+            to touching the direct submodule.
         :param force: Enforces the deletion of the module even though it contains
-            modifications. This basically enforces a brute - force file system based
+            modifications. This basically enforces a brute-force file system based
             deletion.
         :param configuration: if True, the submodule is deleted from the configuration,
             otherwise it isn't. Although this should be enabled most of the times,

From bb48c8775a5415f2c86663e356067b53f414a0cc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 05:59:57 -0400
Subject: [PATCH 196/994] Don't pre-clone submodules on CI, at least for now

This has actions/checkout no longer automatically clone submodules
in the CI test workflows. This change is for the purpose of
reproducing #1713, to allow the forthcoming fix for it to be
tested.

However, continuing to rely on init-tests-after-clone.sh to get the
submodules would serve as a kind of regression testing for #1713.
So it is unclear at this time if and when this change should be
undone.
---
 .github/workflows/cygwin-test.yml   | 1 -
 .github/workflows/pythonpackage.yml | 1 -
 2 files changed, 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index d190f1132..89c03a394 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -26,7 +26,6 @@ jobs:
     - uses: actions/checkout@v4
       with:
         fetch-depth: 0
-        submodules: recursive
 
     - name: Install Cygwin
       uses: cygwin/cygwin-install-action@v4
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 2a82e0e03..2dd97183b 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -27,7 +27,6 @@ jobs:
     - uses: actions/checkout@v4
       with:
         fetch-depth: 0
-        submodules: recursive
 
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v4

From 8ea3133bb15a4498cfa7ab256a0d79287ac564c9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 06:40:40 -0400
Subject: [PATCH 197/994] Have init script clone submodules unconditionally

Since 7110bf8 (in #1693), "git submodule update --init --recursive"
was not run on CI, on the mistaken grounds that the CI test
workflows would already have taken care of cloning all submodules
(ever since 4eef3ec when the "submodules: recursive" option was
added to the actions/checkout step).

This changes the init-tests-after-clone.sh script to again run that
command unconditionally, including on CI. The assumption that it
wasn't needed on CI was based on the specific content of
GitPython's own GitHub Actions workflows. But this disregarded that
the test suite is run on CI for *other* projects: specifically, for
downstream projects that package GitPython (#1713).

This also brings back the comment from fc96980 that says more about
how the tests rely on submodules being present (specifically, that
they need a submodule with a submodule). However, that is not
specifically related to the bug being fixed.
---
 init-tests-after-clone.sh | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 21d1f86d8..118e1de22 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -47,10 +47,8 @@ git reset --hard HEAD~1
 # Point the master branch where we started, so we test the correct code.
 git reset --hard __testing_point__
 
-# The tests need submodules. (On CI, they would already have been checked out.)
-if ! ci; then
-    git submodule update --init --recursive
-fi
+# The tests need submodules, including a submodule with a submodule.
+git submodule update --init --recursive
 
 # The tests need some version tags. Try to get them even in forks. This fetches
 # other objects too. So, locally, we always do it, for a consistent experience.

From 44102f30eaadcd122899f5f801f28b83bd9a5111 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Wed, 18 Oct 2023 17:26:05 +0200
Subject: [PATCH 198/994] prepare next release

---
 VERSION                | 2 +-
 doc/source/changes.rst | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/VERSION b/VERSION
index 3fef00337..efb1eb44f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.38
+3.1.40
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 431ad03e2..71f0bd74c 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,12 @@
 Changelog
 =========
 
+3.1.40
+======
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.40
+
 3.1.38
 ======
 

From 8197e9043f26a88168dc3227c4fbb0ec64ef42f9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 19 Oct 2023 21:17:19 -0400
Subject: [PATCH 199/994] Remove now-unused import in top-level __init__.py

The inspect module was only used to dynamically generate __all__,
which is statically written since c862845. Although it includes
everything it originally had for compatibility (8edc53b), this did
not include modules, since the comprehension used to generate it
omitted those (which is what it needed the inspect module for).
So it was not, and is not, listed in __all__, and can be removed.
---
 git/__init__.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/git/__init__.py b/git/__init__.py
index be8338ddc..38a59e517 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -6,7 +6,6 @@
 # flake8: noqa
 # @PydevCodeAnalysisIgnore
 from git.exc import *  # @NoMove @IgnorePep8
-import inspect
 import os
 import sys
 import os.path as osp

From 7545b802d811128ac367a47b794917f2f678836b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 19 Oct 2023 21:41:39 -0400
Subject: [PATCH 200/994] Add __all__ in git.exc, adjust __init__.py imports

The git.exc module imports exceptions from gitdb.exc to republish
them, as well as defining its own (also for use from outside). But
because it did not define __all__, the intent for the exceptions it
imported was unclear, since names that are introduced by imports
and not present in __all__ are not generally considered public,
even when __all__ is absent and a "*" import would reimport them.

This rectifies that by adding __all__ and listing both imported and
newly introduced exceptions explicitly in it. Although this
strictly expands which names are public under typical conventions,
it strictly contracts which names are imported by a "*" import,
because the presence of __all__ suppresses names not listed in it
from being imported that way. However, because under typical
conventions those other names are not considered public, and they
were not even weakly documented as public, this should be okay.

(Even though this is not a breaking change, in that code it would
break would already technically be broken... if it turns out that
it is common to wrongly rely on the availabiliy of those names,
then this may need to be revisited and slightly modified.)

This brings the readily identified public interface of git.exc in
line with what is weakly implied (and intended) by its docstring.

This also modifies __init__.py accordingly: The top-level git
module has for some time used a "*" import on git.exc, causing
the extra names originally meant as implementation details to be
included. Because its own __all__ was dynamically generated until
c862845, #1659 also added 8edc53b to retain the formerly present
names in __all__. So the change here imports those names from the
modules that deliberately provide them, to preserve compatibility.
---
 git/__init__.py |  8 ++++++--
 git/exc.py      | 30 ++++++++++++++++++++++++++++--
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 38a59e517..05a02a7ff 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -7,10 +7,10 @@
 # @PydevCodeAnalysisIgnore
 from git.exc import *  # @NoMove @IgnorePep8
 import os
-import sys
 import os.path as osp
+import sys
 
-from typing import Optional
+from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
 from git.types import PathLike
 
 __version__ = "git"
@@ -38,7 +38,10 @@ def _init_externals() -> None:
 
 # { Imports
 
+from gitdb.util import to_hex_sha
+
 try:
+    from git.compat import safe_decode  # @NoMove @IgnorePep8
     from git.config import GitConfigParser  # @NoMove @IgnorePep8
     from git.objects import *  # @NoMove @IgnorePep8
     from git.refs import *  # @NoMove @IgnorePep8
@@ -53,6 +56,7 @@ def _init_externals() -> None:
         BlockingLockFile,
         Stats,
         Actor,
+        remove_password_if_present,
         rmtree,
     )
 except GitError as _exc:
diff --git a/git/exc.py b/git/exc.py
index b4ffe7568..32c371d0b 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -5,7 +5,34 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 """ Module containing all exceptions thrown throughout the git package """
 
-from gitdb.exc import (  # noqa: @UnusedImport
+__all__ = [
+    # Defined in gitdb.exc:
+    "AmbiguousObjectName",
+    "BadName",
+    "BadObject",
+    "BadObjectType",
+    "InvalidDBRoot",
+    "ODBError",
+    "ParseError",
+    "UnsupportedOperation",
+    # Introduced in this module:
+    "GitError",
+    "InvalidGitRepositoryError",
+    "WorkTreeRepositoryUnsupported",
+    "NoSuchPathError",
+    "UnsafeProtocolError",
+    "UnsafeOptionError",
+    "CommandError",
+    "GitCommandNotFound",
+    "GitCommandError",
+    "CheckoutError",
+    "CacheError",
+    "UnmergedEntriesError",
+    "HookExecutionError",
+    "RepositoryDirtyError",
+]
+
+from gitdb.exc import (
     AmbiguousObjectName,
     BadName,
     BadObject,
@@ -14,7 +41,6 @@
     ODBError,
     ParseError,
     UnsupportedOperation,
-    to_hex_sha,
 )
 from git.compat import safe_decode
 from git.util import remove_password_if_present

From 2af36792db870c6336b52d3c8aa3d19c0b2fe9c4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 20 Oct 2023 02:56:16 -0400
Subject: [PATCH 201/994] Remove `@UnusedImport` from an import that is used

Inclusion in __all__ is considered a use.
---
 git/db.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/db.py b/git/db.py
index bff43347b..b1a0d108a 100644
--- a/git/db.py
+++ b/git/db.py
@@ -1,7 +1,7 @@
 """Module with our own gitdb implementation - it uses the git command"""
 from git.util import bin_to_hex, hex_to_bin
 from gitdb.base import OInfo, OStream
-from gitdb.db import GitDB  # @UnusedImport
+from gitdb.db import GitDB
 from gitdb.db import LooseObjectDB
 
 from gitdb.exc import BadObject

From a9dab913ae6371b645e7509fb21257ee56124063 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 20 Oct 2023 07:53:39 -0400
Subject: [PATCH 202/994] Set submodule update cadence to weekly

This sets the Dependabot submodule update cadence from montly to
weekly, as requested in:
https://github.com/gitpython-developers/GitPython/pull/1702#issuecomment-1761182333

(This change in GitPython corresponds directly to
https://github.com/gitpython-developers/gitdb/pull/104 in gitdb.)
---
 .github/dependabot.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5acde1a9a..2fe73ca77 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,4 +8,4 @@ updates:
 - package-ecosystem: "gitsubmodule"
   directory: "/"
   schedule:
-    interval: "monthly"
+    interval: "weekly"

From 00aee49d2027bdf9c0f1c8c2065aa1b67b5ecb2e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 20 Oct 2023 05:32:01 -0400
Subject: [PATCH 203/994] Never modify sys.path

This removes the logic that, under some (most) circumstances when
not using a version from PyPI, would insert the location of the
gitdb git-submodule near the front of sys.path.

(As noted in #1717, the specific way this was being done was not
causing the git-submodule's version of gitdb to actually be used.
But it was still modifying sys.path, which this now prevents.)

The installation test, which had verified the insertion into
sys.path, is modified accordingly, except that for now the check
that the very first sys.path entry is undisturbed is kept in place.
---
 git/__init__.py           | 29 -----------------------------
 test/test_installation.py | 24 ++++++++++++++++++++++--
 2 files changed, 22 insertions(+), 31 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 05a02a7ff..5b55e59eb 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -6,38 +6,11 @@
 # flake8: noqa
 # @PydevCodeAnalysisIgnore
 from git.exc import *  # @NoMove @IgnorePep8
-import os
-import os.path as osp
-import sys
-
 from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
 from git.types import PathLike
 
 __version__ = "git"
 
-
-# { Initialization
-def _init_externals() -> None:
-    """Initialize external projects by putting them into the path"""
-    if __version__ == "git" and "PYOXIDIZER" not in os.environ:
-        sys.path.insert(1, osp.join(osp.dirname(__file__), "ext", "gitdb"))
-
-    try:
-        import gitdb
-    except ImportError as e:
-        raise ImportError("'gitdb' could not be found in your PYTHONPATH") from e
-    # END verify import
-
-
-# } END initialization
-
-
-#################
-_init_externals()
-#################
-
-# { Imports
-
 from gitdb.util import to_hex_sha
 
 try:
@@ -62,8 +35,6 @@ def _init_externals() -> None:
 except GitError as _exc:
     raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
 
-# } END imports
-
 # __all__ must be statically defined by py.typed support
 # __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
 __all__ = [
diff --git a/test/test_installation.py b/test/test_installation.py
index 0cb0c71fa..3a0e43973 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -25,6 +25,7 @@ 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", "."],
             stdout=subprocess.PIPE,
@@ -35,12 +36,32 @@ def test_installation(self, rw_dir):
             result.returncode,
             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)
+
+        result = subprocess.run(
+            [self.python, "-c", "import git"],
+            stdout=subprocess.PIPE,
+            cwd=self.sources,
+        )
         self.assertEqual(
             0,
             result.returncode,
             msg=result.stderr or result.stdout or "Selftest failed",
         )
+
+        result = subprocess.run(
+            [self.python, "-c", "import gitdb; import smmap"],
+            stdout=subprocess.PIPE,
+            cwd=self.sources,
+        )
+        self.assertEqual(
+            0,
+            result.returncode,
+            msg=result.stderr or result.stdout or "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 = subprocess.run(
             [self.python, "-c", "import sys;import git; print(sys.path)"],
             stdout=subprocess.PIPE,
@@ -53,4 +74,3 @@ def test_installation(self, rw_dir):
             syspath[0],
             msg="Failed to follow the conventions for https://docs.python.org/3/library/sys.html#sys.path",
         )
-        self.assertTrue(syspath[1].endswith("gitdb"), msg="Failed to add gitdb to sys.path")

From 610c46ddf8c535d8a7bd75921fa8a215648f94ec Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 20 Oct 2023 05:36:06 -0400
Subject: [PATCH 204/994] Document how to use vendored dependencies

This adds a subsection to the end of the installation instructions
in the readme to explain how to cause the versions of the gitdb
and/or smmap dependencies that are vendored as submodules of the
GitPython repository to be used, instead of the PyPI versions, in
the infrequent case that this is desired.

This goes along with he removal of the logic that conditionally
modified sys.path, since that logic was intended to facilitate this
(and at one time had). The approach now documented in the readme
uses editable installs and does not involve modifying sys.path.

Because the need for this is uncommon, it may end up being moved
entirely into documentation in the doc/ directory in the future.
---
 README.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/README.md b/README.md
index a7942fd2f..889ea635f 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,20 @@ 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.
 
+#### With editable *dependencies* (not preferred, and rarely needed)
+
+In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediatley reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
+
+If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
+
+```bash
+pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
+```
+
+The submodules must have been cloned for that to work, but that will already be the case if you have run `./init-tests-after-clone.sh`. You can use `pip list` to check which packages are installed editably and which are installed normally.
+
+To reiterate, this approach should only rarely be used. For most development it is preferable to allow the gitdb and smmap dependencices to be retrieved automatically from PyPI in their latest stable packaged versions.
+
 ### Limitations
 
 #### Leakage of System Resources

From 88e06d715dd88074155e35f1761d6a554c4edd9c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 20 Oct 2023 15:29:25 +0000
Subject: [PATCH 205/994] Bump git/ext/gitdb from `8ec2390` to `ec58b7e`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `8ec2390` to `ec58b7e`.
- [Release notes](https://github.com/gitpython-developers/gitdb/releases)
- [Commits](https://github.com/gitpython-developers/gitdb/compare/8ec23904f44fbbb9bb560b83bd891a9b48be7645...ec58b7e55c8fec2479290a22faa293799f6805fc)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 8ec23904f..ec58b7e55 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 8ec23904f44fbbb9bb560b83bd891a9b48be7645
+Subproject commit ec58b7e55c8fec2479290a22faa293799f6805fc

From 7dd209593d08067e005c92b6856a07dc5a2b618a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 10 Oct 2023 01:08:54 -0400
Subject: [PATCH 206/994] Fix docstrings that intend '\' literally

This intended '\' literally, but it was actually '' because the \'
became just ' (backslash-apostrophe becomes just aspostrophe).
---
 git/objects/base.py | 2 +-
 git/util.py         | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 1d07fd0f6..34279c639 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -212,7 +212,7 @@ def name(self) -> str:
 
     @property
     def abspath(self) -> PathLike:
-        """
+        R"""
         :return:
             Absolute path to this index object in the file system ( as opposed to the
             .path field which is a path relative to the git repository ).
diff --git a/git/util.py b/git/util.py
index 97f461a83..ccadb5881 100644
--- a/git/util.py
+++ b/git/util.py
@@ -239,7 +239,7 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
 
 
 def join_path(a: PathLike, *p: PathLike) -> PathLike:
-    """Join path tokens together similar to osp.join, but always use
+    R"""Join path tokens together similar to osp.join, but always use
     '/' instead of possibly '\' on windows."""
     path = str(a)
     for b in p:
@@ -277,7 +277,7 @@ def to_native_path_linux(path: PathLike) -> str:
 
 
 def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
-    """
+    R"""
     As join path, but makes sure an OS native path is returned. This is only
         needed to play it safe on my dear windows and to assure nice paths that only
         use '\'"""

From ffcbf07e7c879bfeb031b41728fdf2ecfa63f66e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 10 Oct 2023 01:29:34 -0400
Subject: [PATCH 207/994] Put all regex patterns in r-strings

This improves consistency, because most were already. For a few it
allows backslashes to removed, improving readability. Even for the
others, some editors will highlight them as regular expressions now
that they are raw string literals.
---
 git/cmd.py       | 2 +-
 git/diff.py      | 2 +-
 git/refs/log.py  | 2 +-
 git/repo/base.py | 4 ++--
 test/test_exc.py | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7c448e3f2..e18bff8c1 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -264,7 +264,7 @@ class Git(LazyMixin):
 
     _excluded_ = ("cat_file_all", "cat_file_header", "_version_info")
 
-    re_unsafe_protocol = re.compile("(.+)::.+")
+    re_unsafe_protocol = re.compile(r"(.+)::.+")
 
     def __getstate__(self) -> Dict[str, Any]:
         return slots_to_dict(self, exclude=self._excluded_)
diff --git a/git/diff.py b/git/diff.py
index 3e3de7bc1..843d96152 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -53,7 +53,7 @@
 # Special object to compare against the empty tree in diffs
 NULL_TREE = object()
 
-_octal_byte_re = re.compile(b"\\\\([0-9]{3})")
+_octal_byte_re = re.compile(rb"\\([0-9]{3})")
 
 
 def _octal_repl(matchobj: Match) -> bytes:
diff --git a/git/refs/log.py b/git/refs/log.py
index ef3f86b8b..9b02051d3 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -41,7 +41,7 @@ class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
 
     """Named tuple allowing easy access to the revlog data fields"""
 
-    _re_hexsha_only = re.compile("^[0-9A-Fa-f]{40}$")
+    _re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
     __slots__ = ()
 
     def __repr__(self) -> str:
diff --git a/git/repo/base.py b/git/repo/base.py
index bc1b8876d..23136a1d1 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -131,8 +131,8 @@ class Repo(object):
 
     # precompiled regex
     re_whitespace = re.compile(r"\s+")
-    re_hexsha_only = re.compile("^[0-9A-Fa-f]{40}$")
-    re_hexsha_shortened = re.compile("^[0-9A-Fa-f]{4,40}$")
+    re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
+    re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$")
     re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
     re_author_committer_start = re.compile(r"^(author|committer)")
     re_tab_full_line = re.compile(r"^\t(.*)$")
diff --git a/test/test_exc.py b/test/test_exc.py
index 9e125d246..0c3ff35a8 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -102,7 +102,7 @@ def test_CommandError_unicode(self, case):
 
             if subs is not None:
                 # Substrings (must) already contain opening `'`.
-                subs = "(?<!')%s(?!')" % re.escape(subs)
+                subs = r"(?<!')%s(?!')" % re.escape(subs)
                 self.assertRegex(s, subs)
 
         if not stream:

From 2c94b6aee17393ffdf8785bafb31e54e854e6882 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 10 Oct 2023 01:44:25 -0400
Subject: [PATCH 208/994] Remove r prefix from strings that need not be raw

These are a couple strings that, in addition to not having any
escape sequences, don't represent regular expressions, Windows
paths, or anything else that would be clarified by raw literals.
---
 doc/source/conf.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/source/conf.py b/doc/source/conf.py
index 54f1f4723..d19b0c0ec 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -170,7 +170,7 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, document class [howto/manual]).
 latex_documents = [
-    ("index", "GitPython.tex", r"GitPython Documentation", r"Michael Trier", "manual"),
+    ("index", "GitPython.tex", "GitPython Documentation", "Michael Trier", "manual"),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of

From 35fd65bda145eb946ba1f69ff7ed371424f1a0a6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 15 Oct 2023 16:37:12 -0400
Subject: [PATCH 209/994] Fix _index_from_*_format docstrings (proc reading)

In the git.diff.Diff class, the _index_from_patch_format and
_index_from_raw_format methods' docstrings had still described them
as reading from streams, even though they have instead read from
processes (and taken "proc", not "stream", arguments) since #519
when the change was made in a5db3d3 to fix a freezing bug.

This updates the docstrings to reflect that they read from
processes.
---
 git/diff.py | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 843d96152..8ee2527d4 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -486,10 +486,12 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m
 
     @classmethod
     def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex:
-        """Create a new DiffIndex from the given text which must be in patch format
-        :param repo: is the repository we are operating on - it is required
-        :param stream: result of 'git diff' as a stream (supporting file protocol)
-        :return: git.DiffIndex"""
+        """Create a new DiffIndex from the given process output which must be in patch format.
+
+        :param repo: The repository we are operating on
+        :param proc: ``git diff`` process to read from (supports :class:`Git.AutoInterrupt` wrapper)
+        :return: git.DiffIndex
+        """
 
         ## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
         text_list: List[bytes] = []
@@ -644,8 +646,12 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
 
     @classmethod
     def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex":
-        """Create a new DiffIndex from the given stream which must be in raw format.
-        :return: git.DiffIndex"""
+        """Create a new DiffIndex from the given process output which must be in raw format.
+
+        :param repo: The repository we are operating on
+        :param proc: Process to read output from
+        :return: git.DiffIndex
+        """
         # handles
         # :100644 100644 687099101... 37c5e30c8... M    .gitignore
 

From 5af74467ba536dcd0e22c49345eb791eb48cbf45 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 15 Oct 2023 23:24:03 -0400
Subject: [PATCH 210/994] Fix case in IndexObject.abspath exception message

The message refers to the (public) working_tree_dir attribute by
name, so that should be uncapitalized to reflect the case by which
it must be accessed, even when it appears at the beginning of a
sentence.
---
 git/objects/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 34279c639..0dab5ccdb 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -221,4 +221,4 @@ def abspath(self) -> PathLike:
         if self.repo.working_tree_dir is not None:
             return join_path_native(self.repo.working_tree_dir, self.path)
         else:
-            raise WorkTreeRepositoryUnsupported("Working_tree_dir was None or empty")
+            raise WorkTreeRepositoryUnsupported("working_tree_dir was None or empty")

From 692e59e9060f6cfc4cd49d8e83860522666fe966 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 16 Oct 2023 01:53:02 -0400
Subject: [PATCH 211/994] Remove Commit._deserialize doc for
 param_from_rev_list

The git.objects.commit.Commit._deserialize method stopped accepting
a param_from_rev_list argument in ae5a69f, but the documentation
for that parameter was never removed.

Because that was the only part of the method's docstring, and it is
a nonpublic method, and the associated _serialize method does not
have a docstring, this change simply removes the _deserialize
method's docstring without adding anything.
---
 git/objects/commit.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/git/objects/commit.py b/git/objects/commit.py
index 88c485d09..fd65fa1e4 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -688,10 +688,6 @@ def _serialize(self, stream: BytesIO) -> "Commit":
         return self
 
     def _deserialize(self, stream: BytesIO) -> "Commit":
-        """
-        :param from_rev_list: if true, the stream format is coming from the rev-list command
-            Otherwise it is assumed to be a plain data stream from our object
-        """
         readline = stream.readline
         self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "")
 

From ab46192e88feebd6ff8e9ffbec5142e47ff03207 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 17 Oct 2023 16:28:20 -0400
Subject: [PATCH 212/994] Add missing space in Submodule.update debug message

---
 git/objects/submodule/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 473ebde76..2a340399d 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -755,7 +755,7 @@ def update(
                     if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha):
                         if force:
                             msg = "Will force checkout or reset on local branch that is possibly in the future of"
-                            msg += "the commit it will be checked out to, effectively 'forgetting' new commits"
+                            msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
                             log.debug(msg)
                         else:
                             msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"

From 1114828de8e7cedaa62776522e0284ae2fe8b5d0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 17 Oct 2023 20:28:31 -0400
Subject: [PATCH 213/994] Remove obsolete comment in Submodule.module

The local (i.e. late) import was removed in 7b3ef45, but the
comment about it on (what was) the preceding line has persisted
until now.
---
 git/objects/submodule/base.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 2a340399d..e1946c27a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1217,7 +1217,6 @@ def module(self) -> "Repo":
         """:return: Repo instance initialized from the repository at our submodule path
         :raise InvalidGitRepositoryError: if a repository was not available. This could
             also mean that it was not yet initialized"""
-        # late import to workaround circular dependencies
         module_checkout_abspath = self.abspath
         try:
             repo = git.Repo(module_checkout_abspath)

From f95d4fd2e4d7bc5802633ef0c6f6abb818e1559c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 17 Oct 2023 21:31:43 -0400
Subject: [PATCH 214/994] Clarify "master repository" in RootModule docs

This changes

    master repository

to
    superproject (master repository)

in the RootModule class docstring and in the tutorial, to make even
clearer what this is referring to. This way, users who are less
familiar with submodules will be less likely to confuse this with
a "master" branch (since "master" is one of the popular default
branch names), while users who are more familiar with submodules
and may search for the official term "superproject" will find the
docs when doing so.

This retains "master repository" parenthesized rather than
completely replacing it because although "superproject" is the
official term for this, it is a bit obscure and unintuitive.
---
 doc/source/tutorial.rst       | 3 +--
 git/objects/submodule/root.py | 6 ++++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst
index fcbc18bff..fd3b14c57 100644
--- a/doc/source/tutorial.rst
+++ b/doc/source/tutorial.rst
@@ -413,7 +413,7 @@ If you obtained your submodule object by traversing a tree object which is not r
 you have to inform the submodule about its actual commit to retrieve the data from
 by using the ``set_parent_commit(...)`` method.
 
-The special :class:`RootModule <git.objects.submodule.root.RootModule>` type allows you to treat your master repository as root of a hierarchy of submodules, which allows very convenient submodule handling. Its ``update(...)`` method is reimplemented to provide an advanced way of updating submodules as they change their values over time. The update method will track changes and make sure your working tree and submodule checkouts stay consistent, which is very useful in case submodules get deleted or added to name just two of the handled cases.
+The special :class:`RootModule <git.objects.submodule.root.RootModule>` type allows you to treat your superproject (master repository) as root of a hierarchy of submodules, which allows very convenient submodule handling. Its ``update(...)`` method is reimplemented to provide an advanced way of updating submodules as they change their values over time. The update method will track changes and make sure your working tree and submodule checkouts stay consistent, which is very useful in case submodules get deleted or added to name just two of the handled cases.
 
 Additionally, GitPython adds functionality to track a specific branch, instead of just a commit. Supported by customized update methods, you are able to automatically update submodules to the latest revision available in the remote repository, as well as to keep track of changes and movements of these submodules. To use it, set the name of the branch you want to track to the ``submodule.$name.branch`` option of the *.gitmodules*  file, and use GitPython update methods on the resulting repository with the ``to_latest_revision`` parameter turned on. In the latter case, the sha of your submodule will be ignored, instead a local tracking branch will be updated to the respective remote branch automatically, provided there are no local changes. The resulting behaviour is much like the one of svn::externals, which can be useful in times.
 
@@ -545,4 +545,3 @@ And even more ...
 There is more functionality in there, like the ability to archive repositories, get stats and logs, blame, and probably a few other things that were not mentioned here.
 
 Check the unit tests for an in-depth introduction on how each function is supposed to be used.
-
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 0cbc262ca..4f4906c51 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -43,9 +43,11 @@ class RootUpdateProgress(UpdateProgress):
 
 
 class RootModule(Submodule):
+    """A (virtual) root of all submodules in the given repository.
 
-    """A (virtual) Root of all submodules in the given repository. It can be used
-    to more easily traverse all submodules of the master repository"""
+    This can be used to more easily traverse all submodules of the
+    superproject (master repository).
+    """
 
     __slots__ = ()
 

From 9fea4881791a116a7f1edd49d7c16b4218dc991f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 17 Oct 2023 21:52:13 -0400
Subject: [PATCH 215/994] Change spelling from "commit'ish" to "commit-ish"

Since the hyphen spelling is what the official Git docs use.

This change affects only docstrings.
---
 git/index/base.py             | 2 +-
 git/objects/submodule/base.py | 2 +-
 git/objects/submodule/root.py | 2 +-
 git/refs/symbolic.py          | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 0cdeb1ce5..5da904b4e 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1300,7 +1300,7 @@ def reset(
             but if True, this method behaves like HEAD.reset.
 
         :param paths: if given as an iterable of absolute or repository-relative paths,
-            only these will be reset to their state at the given commit'ish.
+            only these will be reset to their state at the given commit-ish.
             The paths need to exist at the commit, otherwise an exception will be
             raised.
 
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index e1946c27a..61c300652 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1095,7 +1095,7 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
         contain the .gitmodules blob.
 
         :param commit:
-            Commit'ish reference pointing at the root_tree, or None to always point to the
+            Commit-ish reference pointing at the root_tree, or None to always point to the
             most recent commit
         :param check:
             if True, relatively expensive checks will be performed to verify
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 4f4906c51..d338441ef 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -91,7 +91,7 @@ def update(
         Additionally it will detect addition and removal of submodules, which will be handled
         gracefully.
 
-        :param previous_commit: If set to a commit'ish, the commit we should use
+        :param previous_commit: If set to a commit-ish, the commit we should use
             as the previous commit the HEAD pointed to before it was set to the commit it points to now.
             If None, it defaults to HEAD@{1} otherwise
         :param recursive: if True, the children of submodules will be updated as well
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 549160444..7cc304136 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -641,7 +641,7 @@ def create(
 
         :param reference:
             The reference to which the new symbolic reference should point to.
-            If it is a commit'ish, the symbolic ref will be detached.
+            If it is a commit-ish, the symbolic ref will be detached.
 
         :param force:
             if True, force creation even if a symbolic reference with that name already exists.

From 4536b634b7e5d74867d56ca413d27ed81fca0e1b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 17 Oct 2023 23:17:48 -0400
Subject: [PATCH 216/994] Update git-source citation in Reference.set_object

Reference.set_object explains its handling of oldbinsha by quoting
a comment from refs.c in the Git source code. However, that comment
now appears in refs/files-backend.c in that codebase. This updates
the reference so readers can look it up and find the comment in its
surrounding context.

The commit to the git project's source code that moved the code
that includes that comment is:

https://github.com/git/git/commit/7bd9bcf372d4c03bb7034346d72ae1318e2d0742
---
 git/refs/reference.py | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/git/refs/reference.py b/git/refs/reference.py
index 4f9e3a0a7..dea1af68c 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -86,18 +86,19 @@ def set_object(
         super(Reference, self).set_object(object, logmsg)
 
         if oldbinsha is not None:
-            # /* from refs.c in git-source
-            # * Special hack: If a branch is updated directly and HEAD
-            # * points to it (may happen on the remote side of a push
-            # * for example) then logically the HEAD reflog should be
-            # * updated too.
-            # * A generic solution implies reverse symref information,
-            # * but finding all symrefs pointing to the given branch
-            # * would be rather costly for this rare event (the direct
-            # * update of a branch) to be worth it.  So let's cheat and
-            # * check with HEAD only which should cover 99% of all usage
-            # * scenarios (even 100% of the default ones).
-            # */
+            # From refs/files-backend.c in git-source:
+            # /*
+            #  * Special hack: If a branch is updated directly and HEAD
+            #  * points to it (may happen on the remote side of a push
+            #  * for example) then logically the HEAD reflog should be
+            #  * updated too.
+            #  * A generic solution implies reverse symref information,
+            #  * but finding all symrefs pointing to the given branch
+            #  * would be rather costly for this rare event (the direct
+            #  * update of a branch) to be worth it.  So let's cheat and
+            #  * check with HEAD only which should cover 99% of all usage
+            #  * scenarios (even 100% of the default ones).
+            #  */
             self.repo.head.log_append(oldbinsha, logmsg)
         # END check if the head
 

From 59d208c0e9566b02a835215bbbeddcce37da0afd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 00:43:55 -0400
Subject: [PATCH 217/994] Fix message in SymbolicReference.from_path

This rarely-seen ValueError message had said SymbolicRef, and is
now changed to SymbolicReference.
---
 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 7cc304136..45496c9d9 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -798,7 +798,7 @@ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_Refere
                 instance: T_References
                 instance = ref_type(repo, path)
                 if instance.__class__ == SymbolicReference and instance.is_detached:
-                    raise ValueError("SymbolRef was detached, we drop it")
+                    raise ValueError("SymbolicRef was detached, we drop it")
                 else:
                     return instance
 

From add46d9daa7664628ba3512297a2401981694c98 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 00:45:32 -0400
Subject: [PATCH 218/994] Use "is" to compare __class__

Since the value of __class__ is a type, comparing it to another
type object should use "is" rather than "==".

Some of these, involving type(), were fixed in bf7af69, but flake8
did not catch the .__class__ variation addressed here.
---
 git/refs/symbolic.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 45496c9d9..5ff84367d 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -769,7 +769,7 @@ def iter_items(
 
             List is lexicographically sorted
             The returned objects represent actual subclasses, such as Head or TagReference"""
-        return (r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached)
+        return (r for r in cls._iter_items(repo, common_path) if r.__class__ is SymbolicReference or not r.is_detached)
 
     @classmethod
     def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_References:
@@ -797,7 +797,7 @@ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_Refere
             try:
                 instance: T_References
                 instance = ref_type(repo, path)
-                if instance.__class__ == SymbolicReference and instance.is_detached:
+                if instance.__class__ is SymbolicReference and instance.is_detached:
                     raise ValueError("SymbolicRef was detached, we drop it")
                 else:
                     return instance

From cd16a3534eedb5a0990293e16a6371ca16d8776a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 04:21:41 -0400
Subject: [PATCH 219/994] Revise docstrings and comments for clarity and
 formatting

In the git module (including the modules it contains).

This also makes one small change in doc/ to synchronize with a
change made in a docstring.
---
 git/__init__.py                   |   2 +
 git/cmd.py                        | 357 +++++++++++----------
 git/compat.py                     |  12 +-
 git/config.py                     | 225 +++++++------
 git/db.py                         |  23 +-
 git/diff.py                       | 158 ++++-----
 git/exc.py                        |  36 ++-
 git/index/__init__.py             |   4 +-
 git/index/base.py                 | 389 +++++++++++-----------
 git/index/fun.py                  | 146 +++++----
 git/index/typ.py                  |  24 +-
 git/index/util.py                 |  16 +-
 git/objects/__init__.py           |  11 +-
 git/objects/base.py               |  75 +++--
 git/objects/blob.py               |  10 +-
 git/objects/commit.py             | 171 +++++-----
 git/objects/fun.py                |  95 +++---
 git/objects/submodule/__init__.py |   4 +-
 git/objects/submodule/base.py     | 513 ++++++++++++++++--------------
 git/objects/submodule/root.py     | 142 +++++----
 git/objects/submodule/util.py     |  23 +-
 git/objects/tag.py                |  32 +-
 git/objects/tree.py               |  89 +++---
 git/objects/util.py               | 157 +++++----
 git/refs/__init__.py              |   2 +-
 git/refs/head.py                  |  75 +++--
 git/refs/log.py                   | 111 ++++---
 git/refs/reference.py             |  52 +--
 git/refs/remote.py                |  16 +-
 git/refs/symbolic.py              | 294 ++++++++++-------
 git/refs/tag.py                   |  48 +--
 git/remote.py                     | 300 +++++++++--------
 git/repo/__init__.py              |   4 +-
 git/repo/base.py                  | 426 +++++++++++++++----------
 git/repo/fun.py                   |   4 +-
 git/types.py                      |   3 +
 git/util.py                       | 213 +++++++------
 37 files changed, 2370 insertions(+), 1892 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 5b55e59eb..46d54a960 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -3,8 +3,10 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 # flake8: noqa
 # @PydevCodeAnalysisIgnore
+
 from git.exc import *  # @NoMove @IgnorePep8
 from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
 from git.types import PathLike
diff --git a/git/cmd.py b/git/cmd.py
index e18bff8c1..f8b6c9e78 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -3,7 +3,9 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 from __future__ import annotations
+
 import re
 import contextlib
 import io
@@ -103,20 +105,21 @@ def handle_process_output(
     decode_streams: bool = True,
     kill_after_timeout: Union[None, float] = None,
 ) -> None:
-    """Registers for notifications to learn that process output is ready to read, and dispatches lines to
-    the respective line handlers.
+    """Register for notifications to learn that process output is ready to read, and
+    dispatch lines to the respective line handlers.
+
     This function returns once the finalizer returns.
 
-    :return: result of finalizer
-    :param process: subprocess.Popen instance
+    :return: Result of finalizer
+    :param process: :class:`subprocess.Popen` instance
     :param stdout_handler: f(stdout_line_string), or None
     :param stderr_handler: f(stderr_line_string), or None
     :param finalizer: f(proc) - wait for proc to finish
     :param decode_streams:
-        Assume stdout/stderr streams are binary and decode them before pushing \
+        Assume stdout/stderr streams are binary and decode them before pushing
         their contents to handlers.
-        Set it to False if `universal_newline == True` (then streams are in text-mode)
-        or if decoding must happen later (i.e. for Diffs).
+        Set it to False if ``universal_newlines == True`` (then streams are in
+        text mode) or if decoding must happen later (i.e. for Diffs).
     :param kill_after_timeout:
         float or None, Default = None
         To specify a timeout in seconds for the git command, after which the process
@@ -232,13 +235,11 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
 PROC_CREATIONFLAGS = (
     CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP if is_win else 0  # type: ignore[attr-defined]
-)  # mypy error if not windows
+)  # mypy error if not Windows.
 
 
 class Git(LazyMixin):
-
-    """
-    The Git class manages communication with the Git binary.
+    """The Git class manages communication with the Git binary.
 
     It provides a convenient interface to calling the Git binary, such as in::
 
@@ -274,44 +275,41 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     # CONFIGURATION
 
-    git_exec_name = "git"  # default that should work on linux and windows
+    git_exec_name = "git"  # Default that should work on Linux and Windows.
 
-    # Enables debugging of GitPython's git commands
+    # Enables debugging of GitPython's git commands.
     GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
 
     # If True, a shell will be used when executing git commands.
     # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126
     # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required.
-    # Override this value using `Git.USE_SHELL = True`
+    # Override this value using `Git.USE_SHELL = True`.
     USE_SHELL = False
 
-    # Provide the full path to the git executable. Otherwise it assumes git is in the path
+    # Provide the full path to the git executable. Otherwise it assumes git is in the path.
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
     _refresh_env_var = "GIT_PYTHON_REFRESH"
     GIT_PYTHON_GIT_EXECUTABLE = None
-    # note that the git executable is actually found during the refresh step in
-    # the top level __init__
+    # Note that the git executable is actually found during the refresh step in
+    # the top level __init__.
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         """This gets called by the refresh function (see the top level __init__)."""
-        # discern which path to refresh with
+        # Discern which path to refresh with.
         if path is not None:
             new_git = os.path.expanduser(path)
             new_git = os.path.abspath(new_git)
         else:
             new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name)
 
-        # keep track of the old and new git executable path
+        # Keep track of the old and new git executable path.
         old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
         cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
 
-        # test if the new git executable path is valid
-
-        # - a GitCommandNotFound error is spawned by ourselves
-        # - a PermissionError is spawned if the git executable provided
-        #   cannot be executed for whatever reason
-
+        # Test if the new git executable path is valid. A GitCommandNotFound error is
+        # spawned by us. A PermissionError is spawned if the git executable cannot be
+        # executed for whatever reason.
         has_git = False
         try:
             cls().version()
@@ -319,7 +317,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         except (GitCommandNotFound, PermissionError):
             pass
 
-        # warn or raise exception if test failed
+        # Warn or raise exception if test failed.
         if not has_git:
             err = (
                 dedent(
@@ -334,18 +332,18 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 % cls._git_exec_env_var
             )
 
-            # revert to whatever the old_git was
+            # Revert to whatever the old_git was.
             cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
 
             if old_git is None:
-                # on the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is
-                # None) we only are quiet, warn, or error depending on the
-                # GIT_PYTHON_REFRESH value
-
-                # determine what the user wants to happen during the initial
-                # refresh we expect GIT_PYTHON_REFRESH to either be unset or
-                # be one of the following values:
-                #   0|q|quiet|s|silence
+                # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
+                # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
+
+                # Determine what the user wants to happen during the initial refresh we
+                # expect GIT_PYTHON_REFRESH to either be unset or be one of the
+                # following values:
+                #
+                #   0|q|quiet|s|silence|n|none
                 #   1|w|warn|warning
                 #   2|r|raise|e|error
 
@@ -410,14 +408,13 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                     )
                     raise ImportError(err)
 
-                # we get here if this was the init refresh and the refresh mode
-                # was not error, go ahead and set the GIT_PYTHON_GIT_EXECUTABLE
-                # such that we discern the difference between a first import
-                # and a second import
+                # We get here if this was the init refresh and the refresh mode was not
+                # error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
+                # discern the difference between a first import and a second import.
                 cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
             else:
-                # after the first refresh (when GIT_PYTHON_GIT_EXECUTABLE
-                # is no longer None) we raise an exception
+                # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
+                # None) we raise an exception.
                 raise GitCommandNotFound("git", err)
 
         return has_git
@@ -438,18 +435,18 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str:
 
     @classmethod
     def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
+        """Remove any backslashes from urls to be written in config files.
+
+        Windows might create config files containing paths with backslashes,
+        but git stops liking them as it will escape the backslashes. Hence we
+        undo the escaping just to be sure.
+        """
         if is_cygwin is None:
             is_cygwin = cls.is_cygwin()
 
         if is_cygwin:
             url = cygpath(url)
         else:
-            """Remove any backslashes from urls to be written in config files.
-
-            Windows might create config files containing paths with backslashes,
-            but git stops liking them as it will escape the backslashes.
-            Hence we undo the escaping just to be sure.
-            """
             url = os.path.expandvars(url)
             if url.startswith("~"):
                 url = os.path.expanduser(url)
@@ -458,12 +455,11 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
 
     @classmethod
     def check_unsafe_protocols(cls, url: str) -> None:
-        """
-        Check for unsafe protocols.
+        """Check for unsafe protocols.
 
         Apart from the usual protocols (http, git, ssh),
-        Git allows "remote helpers" that have the form ``<transport>::<address>``,
-        one of these helpers (``ext::``) can be used to invoke any arbitrary command.
+        Git allows "remote helpers" that have the form ``<transport>::<address>``.
+        One of these helpers (``ext::``) can be used to invoke any arbitrary command.
 
         See:
 
@@ -479,8 +475,7 @@ def check_unsafe_protocols(cls, url: str) -> None:
 
     @classmethod
     def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
-        """
-        Check for unsafe options.
+        """Check for unsafe options.
 
         Some options that are passed to `git <command>` can be used to execute
         arbitrary commands, this are blocked by default.
@@ -496,17 +491,21 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
                     )
 
     class AutoInterrupt(object):
-        """Kill/Interrupt the stored process instance once this instance goes out of scope. It is
-        used to prevent processes piling up in case iterators stop reading.
-        Besides all attributes are wired through to the contained process object.
+        """Process wrapper that terminates the wrapped process on finalization.
+
+        This kills/interrupts the stored process instance once this instance goes out of
+        scope. It is used to prevent processes piling up in case iterators stop reading.
 
-        The wait method was overridden to perform automatic status code checking
-        and possibly raise."""
+        All attributes are wired through to the contained process object.
+
+        The wait method is overridden to perform automatic status code checking and
+        possibly raise.
+        """
 
         __slots__ = ("proc", "args", "status")
 
         # If this is non-zero it will override any status code during
-        # _terminate, used to prevent race conditions in testing
+        # _terminate, used to prevent race conditions in testing.
         _status_code_if_terminate: int = 0
 
         def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
@@ -527,7 +526,7 @@ def _terminate(self) -> None:
                 proc.stdout.close()
             if proc.stderr:
                 proc.stderr.close()
-            # did the process finish already so we have a return code ?
+            # Did the process finish already so we have a return code?
             try:
                 if proc.poll() is not None:
                     self.status = self._status_code_if_terminate or proc.poll()
@@ -535,23 +534,23 @@ def _terminate(self) -> None:
             except OSError as ex:
                 log.info("Ignored error after process had died: %r", ex)
 
-            # can be that nothing really exists anymore ...
+            # It can be that nothing really exists anymore...
             if os is None or getattr(os, "kill", None) is None:
                 return None
 
-            # try to kill it
+            # Try to kill it.
             try:
                 proc.terminate()
-                status = proc.wait()  # ensure process goes away
+                status = proc.wait()  # Ensure the process goes away.
 
                 self.status = self._status_code_if_terminate or status
             except OSError as ex:
                 log.info("Ignored error after process had died: %r", ex)
             except AttributeError:
-                # try windows
-                # for some reason, providing None for stdout/stderr still prints something. This is why
-                # we simply use the shell and redirect to nul. Its slower than CreateProcess, question
-                # is whether we really want to see all these messages. Its annoying no matter what.
+                # Try Windows.
+                # For some reason, providing None for stdout/stderr still prints something. This is why
+                # we simply use the shell and redirect to nul. Slower than CreateProcess. The question
+                # is whether we really want to see all these messages. It's annoying no matter what.
                 if is_win:
                     call(
                         ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)),
@@ -571,7 +570,8 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
 
             :param stderr: Previously read value of stderr, in case stderr is already closed.
             :warn: May deadlock if output or error pipes are used and not handled separately.
-            :raise GitCommandError: if the return status is not 0"""
+            :raise GitCommandError: If the return status is not 0.
+            """
             if stderr is None:
                 stderr_b = b""
             stderr_b = force_bytes(data=stderr, encoding="utf-8")
@@ -579,7 +579,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
             if self.proc is not None:
                 status = self.proc.wait()
                 p_stderr = self.proc.stderr
-            else:  # Assume the underlying proc was killed earlier or never existed
+            else:  # Assume the underlying proc was killed earlier or never existed.
                 status = self.status
                 p_stderr = None
 
@@ -605,19 +605,22 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte
     class CatFileContentStream(object):
         """Object representing a sized read-only stream returning the contents of
         an object.
-        It behaves like a stream, but counts the data read and simulates an empty
+
+        This behaves like a stream, but counts the data read and simulates an empty
         stream once our sized content region is empty.
-        If not all data is read to the end of the object's lifetime, we read the
-        rest to assure the underlying stream continues to work."""
+
+        If not all data are read to the end of the object's lifetime, we read the
+        rest to assure the underlying stream continues to work.
+        """
 
         __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size")
 
         def __init__(self, size: int, stream: IO[bytes]) -> None:
             self._stream = stream
             self._size = size
-            self._nbr = 0  # num bytes read
+            self._nbr = 0  # Number of bytes read.
 
-            # special case: if the object is empty, has null bytes, get the
+            # Special case: If the object is empty, has null bytes, get the
             # final newline right away.
             if size == 0:
                 stream.read(1)
@@ -628,16 +631,16 @@ def read(self, size: int = -1) -> bytes:
             if bytes_left == 0:
                 return b""
             if size > -1:
-                # assure we don't try to read past our limit
+                # Ensure we don't try to read past our limit.
                 size = min(bytes_left, size)
             else:
-                # they try to read all, make sure its not more than what remains
+                # They try to read all, make sure it's not more than what remains.
                 size = bytes_left
             # END check early depletion
             data = self._stream.read(size)
             self._nbr += len(data)
 
-            # check for depletion, read our final byte to make the stream usable by others
+            # Check for depletion, read our final byte to make the stream usable by others.
             if self._size - self._nbr == 0:
                 self._stream.read(1)  # final newline
             # END finish reading
@@ -647,7 +650,7 @@ def readline(self, size: int = -1) -> bytes:
             if self._nbr == self._size:
                 return b""
 
-            # clamp size to lowest allowed value
+            # Clamp size to lowest allowed value.
             bytes_left = self._size - self._nbr
             if size > -1:
                 size = min(bytes_left, size)
@@ -658,7 +661,7 @@ def readline(self, size: int = -1) -> bytes:
             data = self._stream.readline(size)
             self._nbr += len(data)
 
-            # handle final byte
+            # Handle final byte.
             if self._size - self._nbr == 0:
                 self._stream.read(1)
             # END finish reading
@@ -669,7 +672,7 @@ def readlines(self, size: int = -1) -> List[bytes]:
             if self._nbr == self._size:
                 return []
 
-            # leave all additional logic to our readline method, we just check the size
+            # Leave all additional logic to our readline method, we just check the size.
             out = []
             nbr = 0
             while True:
@@ -701,8 +704,8 @@ def __next__(self) -> bytes:
         def __del__(self) -> None:
             bytes_left = self._size - self._nbr
             if bytes_left:
-                # read and discard - seeking is impossible within a stream
-                # includes terminating newline
+                # Read and discard - seeking is impossible within a stream.
+                # This includes any terminating newline.
                 self._stream.read(bytes_left + 1)
             # END handle incomplete read
 
@@ -711,9 +714,10 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
 
         :param working_dir:
            Git directory we should work in. If None, we always work in the current
-           directory as returned by os.getcwd().
+           directory as returned by :func:`os.getcwd`.
            It is meant to be the working tree directory if available, or the
-           .git directory in case of bare repositories."""
+           ``.git`` directory in case of bare repositories.
+        """
         super(Git, self).__init__()
         self._working_dir = expand_path(working_dir)
         self._git_options: Union[List[str], Tuple[str, ...]] = ()
@@ -722,7 +726,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         # Extra environment variables to pass to git commands
         self._environment: Dict[str, str] = {}
 
-        # cached command slots
+        # Cached command slots
         self.cat_file_header: Union[None, TBD] = None
         self.cat_file_all: Union[None, TBD] = None
 
@@ -730,28 +734,30 @@ def __getattr__(self, name: str) -> Any:
         """A convenience method as it allows to call the command as if it was
         an object.
 
-        :return: Callable object that will execute call _call_process with your arguments."""
+        :return:
+            Callable object that will execute call :meth:`_call_process` with
+            your arguments.
+        """
         if name[0] == "_":
             return LazyMixin.__getattr__(self, name)
         return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
 
     def set_persistent_git_options(self, **kwargs: Any) -> None:
-        """Specify command line options to the git executable
-        for subsequent subcommand calls.
+        """Specify command line options to the git executable for subsequent
+        subcommand calls.
 
         :param kwargs:
-            is a dict of keyword arguments.
-            These arguments are passed as in _call_process
-            but will be passed to the git command rather than
-            the subcommand.
+            A dict of keyword arguments.
+            These arguments are passed as in :meth:`_call_process`, but will be
+            passed to the git command rather than the subcommand.
         """
 
         self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
 
     def _set_cache_(self, attr: str) -> None:
         if attr == "_version_info":
-            # We only use the first 4 numbers, as everything else could be strings in fact (on windows)
-            process_version = self._call_process("version")  # should be as default *args and **kwargs used
+            # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
+            process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
             version_numbers = process_version.split(" ")[2]
 
             self._version_info = cast(
@@ -772,7 +778,9 @@ def version_info(self) -> Tuple[int, int, int, int]:
         """
         :return: tuple(int, int, int, int) tuple with integers representing the major, minor
             and additional version numbers as parsed from git version.
-            This value is generated on demand and is cached."""
+
+            This value is generated on demand and is cached.
+        """
         return self._version_info
 
     @overload
@@ -839,7 +847,7 @@ def execute(
         strip_newline_in_stdout: bool = True,
         **subprocess_kwargs: Any,
     ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]:
-        """Handles executing the command and consumes and returns the returned
+        R"""Handle executing the command, and consume and return the returned
         information (stdout).
 
         :param command:
@@ -848,7 +856,7 @@ def execute(
             program to execute is the first item in the args sequence or string.
 
         :param istream:
-            Standard input filehandle passed to `subprocess.Popen`.
+            Standard input filehandle passed to :class:`subprocess.Popen`.
 
         :param with_extended_output:
             Whether to return a (status, stdout, stderr) tuple.
@@ -858,17 +866,17 @@ def execute(
 
         :param as_process:
             Whether to return the created process instance directly from which
-            streams can be read on demand. This will render with_extended_output and
-            with_exceptions ineffective - the caller will have to deal with the details.
-            It is important to note that the process will be placed into an AutoInterrupt
-            wrapper that will interrupt the process once it goes out of scope. If you
-            use the command in iterators, you should pass the whole process instance
-            instead of a single stream.
+            streams can be read on demand. This will render `with_extended_output`
+            and `with_exceptions` ineffective - the caller will have to deal with
+            the details. It is important to note that the process will be placed
+            into an :class:`AutoInterrupt` wrapper that will interrupt the process
+            once it goes out of scope. If you use the command in iterators, you
+            should pass the whole process instance instead of a single stream.
 
         :param output_stream:
             If set to a file-like object, data produced by the git command will be
             output to the given stream directly.
-            This feature only has any effect if as_process is False. Processes will
+            This feature only has any effect if `as_process` is False. Processes will
             always be created with a pipe due to issues with subprocess.
             This merely is a workaround as data will be copied from the
             output pipe to the given output stream directly.
@@ -881,13 +889,13 @@ def execute(
 
         :param kill_after_timeout:
             Specifies a timeout in seconds for the git command, after which the process
-            should be killed. This will have no effect if as_process is set to True. It is
-            set to None by default and will let the process run until the timeout is
-            explicitly specified. This feature is not supported on Windows. It's also worth
-            noting that kill_after_timeout uses SIGKILL, which can have negative side
-            effects on a repository. For example, stale locks in case of ``git gc`` could
-            render the repository incapable of accepting changes until the lock is manually
-            removed.
+            should be killed. This will have no effect if `as_process` is set to True.
+            It is set to None by default and will let the process run until the timeout
+            is explicitly specified. This feature is not supported on Windows. It's also
+            worth noting that `kill_after_timeout` uses SIGKILL, which can have negative
+            side effects on a repository. For example, stale locks in case of ``git gc``
+            could render the repository incapable of accepting changes until the lock is
+            manually removed.
 
         :param with_stdout:
             If True, default True, we open stdout on the created process.
@@ -901,7 +909,7 @@ def execute(
             It overrides :attr:`USE_SHELL` if it is not `None`.
 
         :param env:
-            A dictionary of environment variables to be passed to `subprocess.Popen`.
+            A dictionary of environment variables to be passed to :class:`subprocess.Popen`.
 
         :param max_chunk_size:
             Maximum number of bytes in one chunk of data passed to the output_stream in
@@ -909,11 +917,11 @@ def execute(
             the default value is used.
 
         :param strip_newline_in_stdout:
-            Whether to strip the trailing ``\\n`` of the command stdout.
+            Whether to strip the trailing ``\n`` of the command stdout.
 
         :param subprocess_kwargs:
-            Keyword arguments to be passed to `subprocess.Popen`. Please note that
-            some of the valid kwargs are already set by this method; the ones you
+            Keyword arguments to be passed to :class:`subprocess.Popen`. Please note
+            that some of the valid kwargs are already set by this method; the ones you
             specify may not be the same ones.
 
         :return:
@@ -931,8 +939,9 @@ def execute(
 
         :note:
            If you add additional keyword arguments to the signature of this method,
-           you must update the execute_kwargs tuple housed in this module."""
-        # Remove password for the command if present
+           you must update the execute_kwargs tuple housed in this module.
+        """
+        # Remove password for the command if present.
         redacted_command = remove_password_if_present(command)
         if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
             log.info(" ".join(redacted_command))
@@ -945,12 +954,12 @@ def execute(
         except FileNotFoundError:
             cwd = None
 
-        # Start the process
+        # Start the process.
         inline_env = env
         env = os.environ.copy()
-        # Attempt to force all output to plain ascii english, which is what some parsing code
-        # may expect.
-        # According to stackoverflow (http://goo.gl/l74GC8), we are setting LANGUAGE as well
+        # Attempt to force all output to plain ASCII English, which is what some parsing
+        # code may expect.
+        # According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well
         # just to be sure.
         env["LANGUAGE"] = "C"
         env["LC_ALL"] = "C"
@@ -994,7 +1003,7 @@ def execute(
                     stderr=PIPE,
                     stdout=stdout_sink,
                     shell=shell,
-                    close_fds=is_posix,  # unsupported on windows
+                    close_fds=is_posix,  # Unsupported on Windows.
                     universal_newlines=universal_newlines,
                     creationflags=PROC_CREATIONFLAGS,
                     **subprocess_kwargs,
@@ -1002,7 +1011,7 @@ def execute(
         except cmd_not_found_exception as err:
             raise GitCommandNotFound(redacted_command, err) from err
         else:
-            # replace with a typeguard for Popen[bytes]?
+            # Replace with a typeguard for Popen[bytes]?
             proc.stdout = cast(BinaryIO, proc.stdout)
             proc.stderr = cast(BinaryIO, proc.stderr)
 
@@ -1024,7 +1033,7 @@ def kill_process(pid: int) -> None:
                         if local_pid.isdigit():
                             child_pids.append(int(local_pid))
             try:
-                # Windows does not have SIGKILL, so use SIGTERM instead
+                # Windows does not have SIGKILL, so use SIGTERM instead.
                 sig = getattr(signal, "SIGKILL", signal.SIGTERM)
                 os.kill(pid, sig)
                 for child_pid in child_pids:
@@ -1032,7 +1041,7 @@ def kill_process(pid: int) -> None:
                         os.kill(child_pid, sig)
                     except OSError:
                         pass
-                kill_check.set()  # tell the main routine that the process was killed
+                kill_check.set()  # Tell the main routine that the process was killed.
             except OSError:
                 # It is possible that the process gets completed in the duration after timeout
                 # happens and before we try to kill the process.
@@ -1045,7 +1054,7 @@ def kill_process(pid: int) -> None:
             kill_check = threading.Event()
             watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
 
-        # Wait for the process to return
+        # Wait for the process to return.
         status = 0
         stdout_value: Union[str, bytes] = b""
         stderr_value: Union[str, bytes] = b""
@@ -1076,7 +1085,7 @@ def kill_process(pid: int) -> None:
                 stream_copy(proc.stdout, output_stream, max_chunk_size)
                 stdout_value = proc.stdout.read()
                 stderr_value = proc.stderr.read()
-                # strip trailing "\n"
+                # Strip trailing "\n".
                 if stderr_value.endswith(newline):  # type: ignore
                     stderr_value = stderr_value[:-1]
                 status = proc.wait()
@@ -1110,10 +1119,10 @@ def as_text(stdout_value: Union[bytes, str]) -> str:
         if with_exceptions and status != 0:
             raise GitCommandError(redacted_command, status, stderr_value, stdout_value)
 
-        if isinstance(stdout_value, bytes) and stdout_as_string:  # could also be output_stream
+        if isinstance(stdout_value, bytes) and stdout_as_string:  # Could also be output_stream.
             stdout_value = safe_decode(stdout_value)
 
-        # Allow access to the command's status code
+        # Allow access to the command's status code.
         if with_extended_output:
             return (status, stdout_value, safe_decode(stderr_value))
         else:
@@ -1123,18 +1132,18 @@ def environment(self) -> Dict[str, str]:
         return self._environment
 
     def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
-        """
-        Set environment variables for future git invocations. Return all changed
-        values in a format that can be passed back into this function to revert
-        the changes:
+        """Set environment variables for future git invocations. Return all changed
+        values in a format that can be passed back into this function to revert the
+        changes.
 
         ``Examples``::
 
             old_env = self.update_environment(PWD='/tmp')
             self.update_environment(**old_env)
 
-        :param kwargs: environment variables to use for git processes
-        :return: dict that maps environment variables to their old values
+        :param kwargs: Environment variables to use for git processes
+
+        :return: Dict that maps environment variables to their old values
         """
         old_env = {}
         for key, value in kwargs.items():
@@ -1150,16 +1159,15 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
 
     @contextlib.contextmanager
     def custom_environment(self, **kwargs: Any) -> Iterator[None]:
-        """
-        A context manager around the above ``update_environment`` method to restore the
-        environment back to its previous state after operation.
+        """A context manager around the above :meth:`update_environment` method to
+        restore the environment back to its previous state after operation.
 
         ``Examples``::
 
             with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
                 repo.remotes.origin.fetch()
 
-        :param kwargs: see update_environment
+        :param kwargs: See :meth:`update_environment`
         """
         old_env = self.update_environment(**kwargs)
         try:
@@ -1184,7 +1192,7 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool
         return []
 
     def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
-        """Transforms Python style kwargs into git command line options."""
+        """Transform Python style kwargs into git command line options."""
         args = []
         for k, v in kwargs.items():
             if isinstance(v, (list, tuple)):
@@ -1206,23 +1214,22 @@ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
         return outlist
 
     def __call__(self, **kwargs: Any) -> "Git":
-        """Specify command line options to the git executable
-        for a subcommand call.
+        """Specify command line options to the git executable for a subcommand call.
 
         :param kwargs:
-            is a dict of keyword arguments.
-            these arguments are passed as in _call_process
-            but will be passed to the git command rather than
-            the subcommand.
+            A dict of keyword arguments.
+            These arguments are passed as in :meth:`_call_process`, but will be
+            passed to the git command rather than the subcommand.
 
         ``Examples``::
-            git(work_tree='/tmp').difftool()"""
+            git(work_tree='/tmp').difftool()
+        """
         self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
         return self
 
     @overload
     def _call_process(self, method: str, *args: None, **kwargs: None) -> str:
-        ...  # if no args given, execute called with all defaults
+        ...  # If no args were given, execute the call with all defaults.
 
     @overload
     def _call_process(
@@ -1248,20 +1255,20 @@ def _call_process(
         the result as a string.
 
         :param method:
-            is the command. Contained "_" characters will be converted to dashes,
-            such as in 'ls_files' to call 'ls-files'.
+            The command. Contained ``_`` characters will be converted to dashes,
+            such as in ``ls_files`` to call ``ls-files``.
 
         :param args:
-            is the list of arguments. If None is included, it will be pruned.
+            The list of arguments. If None is included, it will be pruned.
             This allows your commands to call git more conveniently as None
             is realized as non-existent.
 
         :param kwargs:
-            It contains key-values for the following:
-            - the :meth:`execute()` kwds, as listed in :var:`execute_kwargs`;
-            - "command options" to be converted by :meth:`transform_kwargs()`;
-            - the `'insert_kwargs_after'` key which its value must match one of ``*args``
-            and any cmd-options will be appended after the matched arg.
+            Contains key-values for the following:
+            - The :meth:`execute()` kwds, as listed in :var:`execute_kwargs`.
+            - "Command options" to be converted by :meth:`transform_kwargs`.
+            - The `'insert_kwargs_after'` key which its value must match one of ``*args``.
+            It also contains any command options, to be appended after the matched arg.
 
         Examples::
 
@@ -1271,17 +1278,18 @@ def _call_process(
 
            git rev-list max-count 10 --header master
 
-        :return: Same as ``execute``
-                 if no args given used execute default (esp. as_process = False, stdout_as_string = True)
-                 and return str"""
-        # Handle optional arguments prior to calling transform_kwargs
-        # otherwise these'll end up in args, which is bad.
+        :return: Same as :meth:`execute`.
+            If no args are given, used :meth:`execute`'s default (especially
+            ``as_process = False``, ``stdout_as_string = True``) and return str.
+        """
+        # Handle optional arguments prior to calling transform_kwargs.
+        # Otherwise these'll end up in args, which is bad.
         exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
         opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
 
         insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None)
 
-        # Prepare the argument list
+        # Prepare the argument list.
 
         opt_args = self.transform_kwargs(**opts_kwargs)
         ext_args = self._unpack_args([a for a in args if a is not None])
@@ -1302,11 +1310,10 @@ def _call_process(
 
         call = [self.GIT_PYTHON_GIT_EXECUTABLE]
 
-        # add persistent git options
+        # Add persistent git options.
         call.extend(self._persistent_git_options)
 
-        # add the git options, then reset to empty
-        # to avoid side_effects
+        # Add the git options, then reset to empty to avoid side effects.
         call.extend(self._git_options)
         self._git_options = ()
 
@@ -1322,7 +1329,7 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
 
         :return: (hex_sha, type_string, size_as_int)
 
-        :raise ValueError: if the header contains indication for an error due to
+        :raise ValueError: If the header contains indication for an error due to
             incorrect input sha"""
         tokens = header_line.split()
         if len(tokens) != 3:
@@ -1338,12 +1345,12 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
         return (tokens[0], tokens[1], int(tokens[2]))
 
     def _prepare_ref(self, ref: AnyStr) -> bytes:
-        # required for command to separate refs on stdin, as bytes
+        # Required for command to separate refs on stdin, as bytes.
         if isinstance(ref, bytes):
-            # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text
+            # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text.
             refstr: str = ref.decode("ascii")
         elif not isinstance(ref, str):
-            refstr = str(ref)  # could be ref-object
+            refstr = str(ref)  # Could be ref-object.
         else:
             refstr = ref
 
@@ -1379,7 +1386,8 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]:
         :note: The method will only suffer from the costs of command invocation
             once and reuses the command in subsequent calls.
 
-        :return: (hexsha, type_string, size_as_int)"""
+        :return: (hexsha, type_string, size_as_int)
+        """
         cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
         return self.__get_object_header(cmd, ref)
 
@@ -1387,7 +1395,8 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
         """As get_object_header, but returns object data as well.
 
         :return: (hexsha, type_string, size_as_int, data_string)
-        :note: not threadsafe"""
+        :note: Not threadsafe.
+        """
         hexsha, typename, size, stream = self.stream_object_data(ref)
         data = stream.read(size)
         del stream
@@ -1397,7 +1406,8 @@ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileConte
         """As get_object_header, but returns the data as a stream.
 
         :return: (hexsha, type_string, size_as_int, stream)
-        :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!"""
+        :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!
+        """
         cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
         hexsha, typename, size = self.__get_object_header(cmd, ref)
         cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()
@@ -1408,7 +1418,8 @@ def clear_cache(self) -> "Git":
 
         Currently persistent commands will be interrupted.
 
-        :return: self"""
+        :return: self
+        """
         for cmd in (self.cat_file_all, self.cat_file_header):
             if cmd:
                 cmd.__del__()
diff --git a/git/compat.py b/git/compat.py
index 624f26116..93e06d2ec 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -1,10 +1,12 @@
 # -*- coding: utf-8 -*-
-# config.py
+# compat.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
-"""utilities to help provide compatibility with python 3"""
+
+"""Utilities to help provide compatibility with Python 3."""
+
 # flake8: noqa
 
 import locale
@@ -50,7 +52,7 @@ def safe_decode(s: AnyStr) -> str:
 
 
 def safe_decode(s: Union[AnyStr, None]) -> Optional[str]:
-    """Safely decodes a binary string to unicode"""
+    """Safely decode a binary string to Unicode."""
     if isinstance(s, str):
         return s
     elif isinstance(s, bytes):
@@ -72,7 +74,7 @@ def safe_encode(s: AnyStr) -> bytes:
 
 
 def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]:
-    """Safely encodes a binary string to unicode"""
+    """Safely encode a binary string to Unicode."""
     if isinstance(s, str):
         return s.encode(defenc)
     elif isinstance(s, bytes):
@@ -94,7 +96,7 @@ def win_encode(s: AnyStr) -> bytes:
 
 
 def win_encode(s: Optional[AnyStr]) -> Optional[bytes]:
-    """Encode unicodes for process arguments on Windows."""
+    """Encode Unicode strings for process arguments on Windows."""
     if isinstance(s, str):
         return s.encode(locale.getpreferredencoding(False))
     elif isinstance(s, bytes):
diff --git a/git/config.py b/git/config.py
index 76b149179..31b8a665d 100644
--- a/git/config.py
+++ b/git/config.py
@@ -3,8 +3,9 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module containing module parser implementation able to properly read and write
-configuration files"""
+configuration files."""
 
 import sys
 import abc
@@ -85,12 +86,12 @@
 
 
 class MetaParserBuilder(abc.ABCMeta):  # noqa: B024
-    """Utility class wrapping base-class methods into decorators that assure read-only properties"""
+    """Utility class wrapping base-class methods into decorators that assure read-only properties."""
 
     def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder":
+        """Equip all base-class methods with a needs_values decorator, and all non-const
+        methods with a set_dirty_and_flush_changes decorator in addition to that.
         """
-        Equip all base-class methods with a needs_values decorator, and all non-const methods
-        with a set_dirty_and_flush_changes decorator in addition to that."""
         kmm = "_mutating_methods_"
         if kmm in clsdict:
             mutating_methods = clsdict[kmm]
@@ -114,7 +115,7 @@ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParse
 
 
 def needs_values(func: Callable[..., _T]) -> Callable[..., _T]:
-    """Returns method assuring we read values (on demand) before we try to access them"""
+    """Return a method for ensuring we read values (on demand) before we try to access them."""
 
     @wraps(func)
     def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
@@ -126,9 +127,10 @@ def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _
 
 
 def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]:
-    """Return method that checks whether given non constant function may be called.
-    If so, the instance will be set dirty.
-    Additionally, we flush the changes right to disk"""
+    """Return a method that checks whether given non constant function may be called.
+
+    If so, the instance will be set dirty. Additionally, we flush the changes right to disk.
+    """
 
     def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
         rval = non_const_func(self, *args, **kwargs)
@@ -142,14 +144,13 @@ def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
 
 
 class SectionConstraint(Generic[T_ConfigParser]):
-
     """Constrains a ConfigParser to only option commands which are constrained to
     always use the section we have been initialized with.
 
     It supports all ConfigParser methods that operate on an option.
 
-    :note:
-        If used as a context manager, will release the wrapped ConfigParser."""
+    :note: If used as a context manager, will release the wrapped ConfigParser.
+    """
 
     __slots__ = ("_config", "_section_name")
     _valid_attrs_ = (
@@ -183,16 +184,17 @@ def __getattr__(self, attr: str) -> Any:
 
     def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any:
         """Call the configuration at the given method which must take a section name
-        as first argument"""
+        as first argument."""
         return getattr(self._config, method)(self._section_name, *args, **kwargs)
 
     @property
     def config(self) -> T_ConfigParser:
-        """return: Configparser instance we constrain"""
+        """return: ConfigParser instance we constrain"""
         return self._config
 
     def release(self) -> None:
-        """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance"""
+        """Equivalent to GitConfigParser.release(), which is called on our underlying
+        parser instance."""
         return self._config.release()
 
     def __enter__(self) -> "SectionConstraint[T_ConfigParser]":
@@ -248,8 +250,8 @@ 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
+    # We do not support an absolute path of the gitconfig on Windows.
+    # Use the global config instead.
     if is_win and config_level == "system":
         config_level = "global"
 
@@ -271,7 +273,6 @@ def get_config_path(config_level: Lit_config_levels) -> str:
 
 
 class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
-
     """Implements specifics required to read git style configuration files.
 
     This variation behaves much like the git.config command such that the configuration
@@ -286,7 +287,10 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     :note:
         The config is case-sensitive even when queried, hence section and option names
         must match perfectly.
-        If used as a context manager, will release the locked file."""
+
+    :note:
+        If used as a context manager, this will release the locked file.
+    """
 
     # { Configuration
     # The lock type determines the type of lock to use in new configuration readers.
@@ -317,29 +321,34 @@ def __init__(
         repo: Union["Repo", None] = None,
     ) -> None:
         """Initialize a configuration reader to read the given file_or_files and to
-        possibly allow changes to it by setting read_only False
+        possibly allow changes to it by setting read_only False.
 
         :param file_or_files:
-            A single file path or file objects or multiple of these
+            A file path or file object, or a sequence of possibly more than one of them.
 
         :param read_only:
-            If True, the ConfigParser may only read the data , but not change it.
-            If False, only a single file path or file object may be given. We will write back the changes
-            when they happen, or when the ConfigParser is released. This will not happen if other
-            configuration files have been included
-        :param merge_includes: if True, we will read files mentioned in [include] sections and merge their
-            contents into ours. This makes it impossible to write back an individual configuration file.
-            Thus, if you want to modify a single configuration file, turn this off to leave the original
-            dataset unaltered when reading it.
-        :param repo: Reference to repository to use if [includeIf] sections are found in configuration files.
-
+            If True, the ConfigParser may only read the data, but not change it.
+            If False, only a single file path or file object may be given. We will write
+            back the changes when they happen, or when the ConfigParser is released.
+            This will not happen if other configuration files have been included.
+
+        :param merge_includes:
+            If True, we will read files mentioned in ``[include]`` sections and merge
+            their contents into ours. This makes it impossible to write back an
+            individual configuration file. Thus, if you want to modify a single
+            configuration file, turn this off to leave the original dataset unaltered
+            when reading it.
+
+        :param repo:
+            Reference to repository to use if ``[includeIf]`` sections are found in
+            configuration files.
         """
         cp.RawConfigParser.__init__(self, dict_type=_OMD)
         self._dict: Callable[..., _OMD]  # type: ignore   # mypy/typeshed bug?
         self._defaults: _OMD
         self._sections: _OMD  # type: ignore  # mypy/typeshed bug?
 
-        # Used in python 3, needs to stay in sync with sections for underlying implementation to work
+        # Used in Python 3. Needs to stay in sync with sections for underlying implementation to work.
         if not hasattr(self, "_proxies"):
             self._proxies = self._dict()
 
@@ -377,7 +386,7 @@ def _acquire_lock(self) -> None:
                     file_or_files = self._file_or_files.name
 
                 # END get filename from handle/stream
-                # initialize lock base - we want to write
+                # Initialize lock base - we want to write.
                 self._lock = self.t_lock(file_or_files)
             # END lock check
 
@@ -386,7 +395,7 @@ def _acquire_lock(self) -> None:
 
     def __del__(self) -> None:
         """Write pending changes if required and release locks"""
-        # NOTE: only consistent in PY2
+        # NOTE: Only consistent in Python 2.
         self.release()
 
     def __enter__(self) -> "GitConfigParser":
@@ -398,10 +407,11 @@ def __exit__(self, *args: Any) -> None:
 
     def release(self) -> None:
         """Flush changes and release the configuration write lock. This instance must not be used anymore afterwards.
+
         In Python 3, it's required to explicitly release locks and flush changes, as __del__ is not called
         deterministically anymore."""
-        # checking for the lock here makes sure we do not raise during write()
-        # in case an invalid parser was created who could not get a lock
+        # Checking for the lock here makes sure we do not raise during write()
+        # in case an invalid parser was created who could not get a lock.
         if self.read_only or (self._lock and not self._lock._has_lock()):
             return
 
@@ -410,33 +420,31 @@ def release(self) -> None:
         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
+            # This happens in Python 3... and usually means that some state cannot be
+            # written as the sections dict cannot be iterated. This usually happens when
+            # the interpreter is shutting down. Can it be fixed?
             pass
         finally:
             if self._lock is not None:
                 self._lock._release_lock()
 
     def optionxform(self, optionstr: str) -> str:
-        """Do not transform options in any way when writing"""
+        """Do not transform options in any way when writing."""
         return optionstr
 
     def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
-        """A direct copy of the py2.4 version of the super class's _read method
-        to assure it uses ordered dicts. Had to change one line to make it work.
-
-        Future versions have this fixed, but in fact its quite embarrassing for the
-        guys not to have done it right in the first place !
-
-        Removed big comments to make it more compact.
+        """Originally a direct copy of the Python 2.4 version of RawConfigParser._read,
+        to ensure it uses ordered dicts.
 
-        Made sure it ignores initial whitespace as git uses tabs"""
-        cursect = None  # None, or a dictionary
+        The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since
+        Python 3.7. This has some other changes, especially that it ignores initial
+        whitespace, since git uses tabs. (Big comments are removed to be more compact.)
+        """
+        cursect = None  # None, or a dictionary.
         optname = None
         lineno = 0
         is_multi_line = False
-        e = None  # None, or an exception
+        e = None  # None, or an exception.
 
         def string_decode(v: str) -> str:
             if v[-1] == "\\":
@@ -449,19 +457,19 @@ def string_decode(v: str) -> str:
         # end
 
         while True:
-            # we assume to read binary !
+            # We assume to read binary!
             line = fp.readline().decode(defenc)
             if not line:
                 break
             lineno = lineno + 1
-            # comment or blank line?
+            # Comment or blank line?
             if line.strip() == "" or self.re_comment.match(line):
                 continue
             if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR":
-                # no leading whitespace
+                # No leading whitespace.
                 continue
 
-            # is it a section header?
+            # Is it a section header?
             mo = self.SECTCRE.match(line.strip())
             if not is_multi_line and mo:
                 sectname: str = mo.group("header").strip()
@@ -473,16 +481,16 @@ def string_decode(v: str) -> str:
                     cursect = self._dict((("__name__", sectname),))
                     self._sections[sectname] = cursect
                     self._proxies[sectname] = None
-                # So sections can't start with a continuation line
+                # So sections can't start with a continuation line.
                 optname = None
-            # no section header in the file?
+            # No section header in the file?
             elif cursect is None:
                 raise cp.MissingSectionHeaderError(fpname, lineno, line)
-            # an option line?
+            # An option line?
             elif not is_multi_line:
                 mo = self.OPTCRE.match(line)
                 if mo:
-                    # We might just have handled the last line, which could contain a quotation we want to remove
+                    # We might just have handled the last line, which could contain a quotation we want to remove.
                     optname, vi, optval = mo.group("option", "vi", "value")
                     if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
                         pos = optval.find(";")
@@ -497,10 +505,10 @@ def string_decode(v: str) -> str:
                         is_multi_line = True
                         optval = string_decode(optval[1:])
                     # end handle multi-line
-                    # preserves multiple values for duplicate optnames
+                    # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)
                 else:
-                    # check if it's an option with no value - it's just ignored by git
+                    # Check if it's an option with no value - it's just ignored by git.
                     if not self.OPTVALUEONLY.match(line):
                         if not e:
                             e = cp.ParsingError(fpname)
@@ -517,7 +525,7 @@ def string_decode(v: str) -> str:
             # END parse section or option
         # END while reading
 
-        # if any parsing errors occurred, raise an exception
+        # If any parsing errors occurred, raise an exception.
         if e:
             raise e
 
@@ -525,8 +533,9 @@ def _has_includes(self) -> Union[bool, int]:
         return self._merge_includes and len(self._included_paths())
 
     def _included_paths(self) -> List[Tuple[str, str]]:
-        """Return List all paths that must be included to configuration
-        as Tuples of (option, value).
+        """List all paths that must be included to configuration.
+
+        :return: The list of paths, where each path is a tuple of ``(option, value)``.
         """
         paths = []
 
@@ -573,21 +582,24 @@ def _included_paths(self) -> List[Tuple[str, str]]:
         return paths
 
     def read(self) -> None:  # type: ignore[override]
-        """Reads the data stored in the files we have been initialized with. It will
-        ignore files that cannot be read, possibly leaving an empty configuration
+        """Read the data stored in the files we have been initialized with.
+
+        This will ignore files that cannot be read, possibly leaving an empty
+        configuration.
 
         :return: Nothing
-        :raise IOError: if a file cannot be handled"""
+        :raise IOError: If a file cannot be handled
+        """
         if self._is_initialized:
             return None
         self._is_initialized = True
 
         files_to_read: List[Union[PathLike, IO]] = [""]
         if isinstance(self._file_or_files, (str, os.PathLike)):
-            # for str or Path, as str is a type of Sequence
+            # For str or Path, as str is a type of Sequence.
             files_to_read = [self._file_or_files]
         elif not isinstance(self._file_or_files, (tuple, list, Sequence)):
-            # could merge with above isinstance once runtime type known
+            # Could merge with above isinstance once runtime type known.
             files_to_read = [self._file_or_files]
         else:  # for lists or tuples
             files_to_read = list(self._file_or_files)
@@ -600,11 +612,11 @@ def read(self) -> None:  # type: ignore[override]
             file_ok = False
 
             if hasattr(file_path, "seek"):
-                # must be a file objectfile-object
-                file_path = cast(IO[bytes], file_path)  # replace with assert to narrow type, once sure
+                # Must be a file objectfile-object.
+                file_path = cast(IO[bytes], file_path)  # TODO: Replace with assert to narrow type, once sure.
                 self._read(file_path, file_path.name)
             else:
-                # assume a path if it is not a file-object
+                # Assume a path if it is not a file-object.
                 file_path = cast(PathLike, file_path)
                 try:
                     with open(file_path, "rb") as fp:
@@ -613,8 +625,8 @@ def read(self) -> None:  # type: ignore[override]
                 except IOError:
                     continue
 
-            # Read includes and append those that we didn't handle yet
-            # We expect all paths to be normalized and absolute (and will assure that is the case)
+            # Read includes and append those that we didn't handle yet.
+            # We expect all paths to be normalized and absolute (and will assure that is the case).
             if self._has_includes():
                 for _, include_path in self._included_paths():
                     if include_path.startswith("~"):
@@ -631,27 +643,27 @@ def read(self) -> None:  # type: ignore[override]
                     if include_path in seen or not os.access(include_path, os.R_OK):
                         continue
                     seen.add(include_path)
-                    # insert included file to the top to be considered first
+                    # Insert included file to the top to be considered first.
                     files_to_read.insert(0, include_path)
                     num_read_include_files += 1
                 # each include path in configuration file
             # end handle includes
         # END for each file object to read
 
-        # If there was no file included, we can safely write back (potentially) the configuration file
-        # without altering it's meaning
+        # If there was no file included, we can safely write back (potentially) the
+        # configuration file without altering its meaning.
         if num_read_include_files == 0:
             self._merge_includes = False
         # end
 
     def _write(self, fp: IO) -> None:
         """Write an .ini-format representation of the configuration state in
-        git compatible format"""
+        git compatible format."""
 
         def write_section(name: str, section_dict: _OMD) -> None:
             fp.write(("[%s]\n" % name).encode(defenc))
 
-            values: Sequence[str]  # runtime only gets str in tests, but should be whatever _OMD stores
+            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():
                 if key == "__name__":
@@ -692,9 +704,9 @@ def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
 
     @needs_values
     def write(self) -> None:
-        """Write changes to our file, if there are changes at all
+        """Write changes to our file, if there are changes at all.
 
-        :raise IOError: if this is a read-only writer instance or if we could not obtain
+        :raise IOError: If this is a read-only writer instance or if we could not obtain
             a file lock"""
         self._assure_writable("write")
         if not self._dirty:
@@ -717,9 +729,9 @@ def write(self) -> None:
 
         fp = self._file_or_files
 
-        # we have a physical file on disk, so get a lock
-        is_file_lock = isinstance(fp, (str, os.PathLike, IOBase))  # can't use Pathlike until 3.5 dropped
-        if is_file_lock and self._lock is not None:  # else raise Error?
+        # We have a physical file on disk, so get a lock.
+        is_file_lock = isinstance(fp, (str, os.PathLike, IOBase))  # TODO: Use PathLike (having dropped 3.5).
+        if is_file_lock and self._lock is not None:  # Else raise error?
             self._lock._obtain_lock()
 
         if not hasattr(fp, "seek"):
@@ -729,7 +741,7 @@ def write(self) -> None:
         else:
             fp = cast("BytesIO", fp)
             fp.seek(0)
-            # make sure we do not overwrite into an existing file
+            # Make sure we do not overwrite into an existing file.
             if hasattr(fp, "truncate"):
                 fp.truncate()
             self._write(fp)
@@ -747,13 +759,13 @@ def read_only(self) -> bool:
         """:return: True if this instance may change the configuration file"""
         return self._read_only
 
+    # FIXME: Figure out if default or return type can really include bool.
     def get_value(
         self,
         section: str,
         option: str,
         default: Union[int, float, str, bool, None] = None,
     ) -> Union[int, float, str, bool]:
-        # can default or return type include bool?
         """Get an option's value.
 
         If multiple values are specified for this option in the section, the
@@ -762,10 +774,12 @@ def get_value(
         :param default:
             If not None, the given default value will be returned in case
             the option did not exist
+
         :return: a properly typed value, either int, float or string
 
         :raise TypeError: in case the value could not be understood
-            Otherwise the exceptions known to the ConfigParser will be raised."""
+            Otherwise the exceptions known to the ConfigParser will be raised.
+        """
         try:
             valuestr = self.get(section, option)
         except Exception:
@@ -789,10 +803,12 @@ def get_values(
         :param default:
             If not None, a list containing the given default value will be
             returned in case the option did not exist
+
         :return: a list of properly typed values, either int, float or string
 
         :raise TypeError: in case the value could not be understood
-            Otherwise the exceptions known to the ConfigParser will be raised."""
+            Otherwise the exceptions known to the ConfigParser will be raised.
+        """
         try:
             self.sections()
             lst = self._sections[section].getall(option)
@@ -816,7 +832,7 @@ def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]:
                 continue
         # END for each numeric type
 
-        # try boolean values as git uses them
+        # Try boolean values as git uses them.
         vl = valuestr.lower()
         if vl == "false":
             return False
@@ -839,16 +855,17 @@ def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str:
     @needs_values
     @set_dirty_and_flush_changes
     def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
-        """Sets the given option in section to the given value.
-        It will create the section if required, and will not throw as opposed to the default
-        ConfigParser 'set' method.
+        """Set the given option in section to the given value.
+
+        This will create the section if required, and will not throw as opposed to the
+        default ConfigParser 'set' method.
 
         :param section: Name of the section in which the option resides or should reside
         :param option: Name of the options whose value to set
-
-        :param value: Value to set the option to. It must be a string or convertible
-            to a string
-        :return: this instance"""
+        :param value: Value to set the option to. It must be a string or convertible to
+            a string.
+        :return: This instance
+        """
         if not self.has_section(section):
             self.add_section(section)
         self.set(section, option, self._value_to_string(value))
@@ -857,27 +874,29 @@ def set_value(self, section: str, option: str, value: Union[str, bytes, int, flo
     @needs_values
     @set_dirty_and_flush_changes
     def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
-        """Adds a value for the given option in section.
-        It will create the section if required, and will not throw as opposed to the default
+        """Add a value for the given option in section.
+
+        This will create the section if required, and will not throw as opposed to the default
         ConfigParser 'set' method. The value becomes the new value of the option as returned
         by 'get_value', and appends to the list of values returned by 'get_values`'.
 
         :param section: Name of the section in which the option resides or should reside
         :param option: Name of the option
-
         :param value: Value to add to option. It must be a string or convertible
             to a string
-        :return: this instance"""
+        :return: This instance
+        """
         if not self.has_section(section):
             self.add_section(section)
         self._sections[section].add(option, self._value_to_string(value))
         return self
 
     def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
-        """rename the given section to new_name
-        :raise ValueError: if section doesn't exit
-        :raise ValueError: if a section with new_name does already exist
-        :return: this instance
+        """Rename the given section to new_name.
+
+        :raise ValueError: If ``section`` doesn't exist
+        :raise ValueError: If a section with ``new_name`` does already exist
+        :return: This instance
         """
         if not self.has_section(section):
             raise ValueError("Source section '%s' doesn't exist" % section)
@@ -890,6 +909,6 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
             new_section.setall(k, vs)
         # end for each value to copy
 
-        # This call writes back the changes, which is why we don't have the respective decorator
+        # This call writes back the changes, which is why we don't have the respective decorator.
         self.remove_section(section)
         return self
diff --git a/git/db.py b/git/db.py
index b1a0d108a..1aacd0c84 100644
--- a/git/db.py
+++ b/git/db.py
@@ -1,4 +1,5 @@
-"""Module with our own gitdb implementation - it uses the git command"""
+"""Module with our own gitdb implementation - it uses the git command."""
+
 from git.util import bin_to_hex, hex_to_bin
 from gitdb.base import OInfo, OStream
 from gitdb.db import GitDB
@@ -22,17 +23,17 @@
 
 
 class GitCmdObjectDB(LooseObjectDB):
-
     """A database representing the default git object store, which includes loose
-    objects, pack files and an alternates file
+    objects, pack files and an alternates file.
 
     It will create objects only in the loose object database.
-    :note: for now, we use the git command to do all the lookup, just until he
-        have packs and the other implementations
+
+    :note: For now, we use the git command to do all the lookup, just until we
+        have packs and the other implementations.
     """
 
     def __init__(self, root_path: PathLike, git: "Git") -> None:
-        """Initialize this instance with the root and a git command"""
+        """Initialize this instance with the root and a git command."""
         super(GitCmdObjectDB, self).__init__(root_path)
         self._git = git
 
@@ -48,11 +49,15 @@ def stream(self, binsha: bytes) -> OStream:
     # { Interface
 
     def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
-        """:return: Full binary 20 byte sha from the given partial hexsha
+        """
+        :return: Full binary 20 byte sha from the given partial hexsha
+
         :raise AmbiguousObjectName:
         :raise BadObject:
-        :note: currently we only raise BadObject as git does not communicate
-            AmbiguousObjects separately"""
+
+        :note: Currently we only raise :class:`BadObject` as git does not communicate
+            AmbiguousObjects separately.
+        """
         try:
             hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)
             return hex_to_bin(hexsha)
diff --git a/git/diff.py b/git/diff.py
index 8ee2527d4..1fde4d676 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -50,7 +50,7 @@
 
 __all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE")
 
-# Special object to compare against the empty tree in diffs
+# Special object to compare against the empty tree in diffs.
 NULL_TREE = object()
 
 _octal_byte_re = re.compile(rb"\\([0-9]{3})")
@@ -80,27 +80,28 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
 
 
 class Diffable(object):
-
-    """Common interface for all object that can be diffed against another object of compatible type.
+    """Common interface for all objects that can be diffed against another object of
+    compatible type.
 
     :note:
-        Subclasses require a repo member as it is the case for Object instances, for practical
-        reasons we do not derive from Object."""
+        Subclasses require a repo member as it is the case for Object instances, for
+        practical reasons we do not derive from Object.
+    """
 
     __slots__ = ()
 
-    # standin indicating you want to diff against the index
     class Index(object):
-        pass
+        """Stand-in indicating you want to diff against the index."""
 
     def _process_diff_args(
         self, args: List[Union[str, "Diffable", Type["Diffable.Index"], object]]
     ) -> List[Union[str, "Diffable", Type["Diffable.Index"], object]]:
         """
         :return:
-            possibly altered version of the given args list.
-            Method is called right before git command execution.
-            Subclasses can use it to alter the behaviour of the superclass"""
+            Possibly altered version of the given args list.
+            This method is called right before git command execution.
+            Subclasses can use it to alter the behaviour of the superclass.
+        """
         return args
 
     def diff(
@@ -110,41 +111,47 @@ def diff(
         create_patch: bool = False,
         **kwargs: Any,
     ) -> "DiffIndex":
-        """Creates diffs between two items being trees, trees and index or an
-        index and the working tree. It will detect renames automatically.
+        """Create diffs between two items being trees, trees and index or an
+        index and the working tree. Detects renames automatically.
 
         :param other:
-            Is the item to compare us with.
-            If None, we will be compared to the working tree.
-            If Treeish, it will be compared against the respective tree
-            If Index ( type ), it will be compared against the index.
-            If git.NULL_TREE, it will compare against the empty tree.
-            It defaults to Index to assure the method will not by-default fail
-            on bare repositories.
+            This the item to compare us with.
+
+            * If None, we will be compared to the working tree.
+            * If :class:`Treeish <git.index.base.Treeish>`, it will be compared against
+              the respective tree.
+            * If :class:`Index <Diffable.Index>`, it will be compared against the index.
+            * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
+            * It defaults to :class:`Index <Diffable.Index>` to assure the method will
+              not by-default fail on bare repositories.
 
         :param paths:
-            is a list of paths or a single path to limit the diff to.
-            It will only include at least one of the given path or paths.
+            This a list of paths or a single path to limit the diff to. It will only
+            include at least one of the given path or paths.
 
         :param create_patch:
-            If True, the returned Diff contains a detailed patch that if applied
-            makes the self to other. Patches are somewhat costly as blobs have to be read
-            and diffed.
+            If True, the returned :class:`Diff` contains a detailed patch that if
+            applied makes the self to other. Patches are somewhat costly as blobs have
+            to be read and diffed.
 
         :param kwargs:
-            Additional arguments passed to git-diff, such as
-            R=True to swap both sides of the diff.
+            Additional arguments passed to git-diff, such as ``R=True`` to swap both
+            sides of the diff.
 
         :return: git.DiffIndex
 
         :note:
-            On a bare repository, 'other' needs to be provided as Index or as
-            as Tree/Commit, or a git command error will occur"""
+            On a bare repository, 'other' needs to be provided
+            as :class:`Index <Diffable.Index>`,
+            or as :class:`Tree <git.objects.tree.Tree>`
+            or :class:`Commit <git.objects.commit.Commit>`, or a git command error will
+            occur.
+        """
         args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = []
-        args.append("--abbrev=40")  # we need full shas
-        args.append("--full-index")  # get full index paths, not only filenames
+        args.append("--abbrev=40")  # We need full shas.
+        args.append("--full-index")  # Get full index paths, not only filenames.
 
-        # remove default '-M' arg (check for renames) if user is overriding it
+        # 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")):
             args.append("-M")
 
@@ -154,8 +161,8 @@ def diff(
             args.append("--raw")
             args.append("-z")
 
-        # in any way, assure we don't see colored output,
-        # fixes https://github.com/gitpython-developers/GitPython/issues/172
+        # Ensure we never see colored output.
+        # Fixes: https://github.com/gitpython-developers/GitPython/issues/172
         args.append("--no-color")
 
         if paths is not None and not isinstance(paths, (tuple, list)):
@@ -168,17 +175,17 @@ def diff(
         if other is self.Index:
             args.insert(0, "--cached")
         elif other is NULL_TREE:
-            args.insert(0, "-r")  # recursive diff-tree
+            args.insert(0, "-r")  # Recursive diff-tree.
             args.insert(0, "--root")
             diff_cmd = self.repo.git.diff_tree
         elif other is not None:
-            args.insert(0, "-r")  # recursive diff-tree
+            args.insert(0, "-r")  # Recursive diff-tree.
             args.insert(0, other)
             diff_cmd = self.repo.git.diff_tree
 
         args.insert(0, self)
 
-        # paths is list here or None
+        # paths is list here, or None.
         if paths:
             args.append("--")
             args.extend(paths)
@@ -198,13 +205,13 @@ def diff(
 
 
 class DiffIndex(List[T_Diff]):
+    """An Index for diffs, allowing a list of Diffs to be queried by the diff
+    properties.
 
-    """Implements an Index for diffs, allowing a list of Diffs to be queried by
-    the diff properties.
-
-    The class improves the diff handling convenience"""
+    The class improves the diff handling convenience.
+    """
 
-    # change type invariant identifying possible ways a blob can have changed
+    # Change type invariant identifying possible ways a blob can have changed:
     # A = Added
     # D = Deleted
     # R = Renamed
@@ -215,10 +222,10 @@ class DiffIndex(List[T_Diff]):
     def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
         """
         :return:
-            iterator yielding Diff instances that match the given change_type
+            iterator yielding :class:`Diff` instances that match the given `change_type`
 
         :param change_type:
-            Member of DiffIndex.change_type, namely:
+            Member of :attr:`DiffIndex.change_type`, namely:
 
             * 'A' for added paths
             * 'D' for deleted paths
@@ -246,11 +253,10 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
 
 
 class Diff(object):
-
     """A Diff contains diff information between two Trees.
 
     It contains two sides a and b of the diff, members are prefixed with
-    "a" and "b" respectively to inidcate that.
+    "a" and "b" respectively to indicate that.
 
     Diffs keep information about the changed blob objects, the file mode, renames,
     deletions and new files.
@@ -273,11 +279,13 @@ class Diff(object):
 
         When comparing to working trees, the working tree blob will have a null hexsha
         as a corresponding object does not yet exist. The mode will be null as well.
-        But the path will be available though.
-        If it is listed in a diff the working tree version of the file must
-        be different to the version in the index or tree, and hence has been modified."""
+        The path will be available, though.
+
+        If it is listed in a diff, the working tree version of the file must
+        differ from the version in the index or tree, and hence has been modified.
+    """
 
-    # precompiled regex
+    # Precompiled regex.
     re_header = re.compile(
         rb"""
                                 ^diff[ ]--git
@@ -299,7 +307,8 @@ class Diff(object):
                             """,
         re.VERBOSE | re.MULTILINE,
     )
-    # can be used for comparisons
+
+    # These can be used for comparisons.
     NULL_HEX_SHA = "0" * 40
     NULL_BIN_SHA = b"\0" * 20
 
@@ -346,8 +355,8 @@ def __init__(
         self.a_mode = mode_str_to_int(a_mode) if a_mode else None
         self.b_mode = mode_str_to_int(b_mode) if b_mode else None
 
-        # Determine whether this diff references a submodule, if it does then
-        # we need to overwrite "repo" to the corresponding submodule's repo instead
+        # Determine whether this diff references a submodule. If it does then
+        # we need to overwrite "repo" to the corresponding submodule's repo instead.
         if repo and a_rawpath:
             for submodule in repo.submodules:
                 if submodule.path == a_rawpath.decode(defenc, "replace"):
@@ -371,7 +380,7 @@ def __init__(
         self.deleted_file: bool = deleted_file
         self.copied_file: bool = copied_file
 
-        # be clear and use None instead of empty strings
+        # Be clear and use None instead of empty strings.
         assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
         assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
         self.raw_rename_from = raw_rename_from or None
@@ -395,15 +404,15 @@ def __hash__(self) -> int:
         return hash(tuple(getattr(self, n) for n in self.__slots__))
 
     def __str__(self) -> str:
-        h: str = "%s"
+        h = "%s"
         if self.a_blob:
             h %= self.a_blob.path
         elif self.b_blob:
             h %= self.b_blob.path
 
-        msg: str = ""
-        line = None  # temp line
-        line_length = 0  # line length
+        msg = ""
+        line = None
+        line_length = 0
         for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")):
             if b:
                 line = "\n%s: %o | %s" % (n, b.mode, b.hexsha)
@@ -414,7 +423,7 @@ def __str__(self) -> str:
             msg += line
         # END for each blob
 
-        # add headline
+        # Add headline.
         h += "\n" + "=" * line_length
 
         if self.deleted_file:
@@ -437,11 +446,7 @@ def __str__(self) -> str:
             msg += "\n---"
         # END diff info
 
-        # Python2 silliness: have to assure we convert our likely to be unicode object to a string with the
-        # right encoding. Otherwise it tries to convert it using ascii, which may fail ungracefully
-        res = h + msg
-        # end
-        return res
+        return h + msg
 
     @property
     def a_path(self) -> Optional[str]:
@@ -461,8 +466,11 @@ def rename_to(self) -> Optional[str]:
 
     @property
     def renamed(self) -> bool:
-        """:returns: True if the blob of our diff has been renamed
-        :note: This property is deprecated, please use ``renamed_file`` instead.
+        """
+        :returns: True if the blob of our diff has been renamed
+
+        :note: This property is deprecated.
+            Please use the :attr:`renamed_file` property instead.
         """
         return self.renamed_file
 
@@ -493,17 +501,17 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
         :return: git.DiffIndex
         """
 
-        ## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
+        # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
         text_list: List[bytes] = []
         handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)
 
-        # for now, we have to bake the stream
+        # For now, we have to bake the stream.
         text = b"".join(text_list)
         index: "DiffIndex" = DiffIndex()
         previous_header: Union[Match[bytes], None] = None
         header: Union[Match[bytes], None] = None
-        a_path, b_path = None, None  # for mypy
-        a_mode, b_mode = None, None  # for mypy
+        a_path, b_path = None, None  # For mypy.
+        a_mode, b_mode = None, None  # For mypy.
         for _header in cls.re_header.finditer(text):
             (
                 a_path_fallback,
@@ -532,13 +540,13 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
             b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
 
             # Our only means to find the actual text is to see what has not been matched by our regex,
-            # and then retro-actively assign it to our index
+            # and then retro-actively assign it to our index.
             if previous_header is not None:
                 index[-1].diff = text[previous_header.end() : _header.start()]
             # end assign actual diff
 
-            # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid
-            # We just use the one mode we should have parsed
+            # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid.
+            # We just use the one mode we should have parsed.
             a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
             b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
             index.append(
@@ -579,7 +587,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
 
         for line in lines.split("\x00:"):
             if not line:
-                # The line data is empty, skip
+                # The line data is empty, skip.
                 continue
             meta, _, path = line.partition("\x00")
             path = path.rstrip("\x00")
@@ -603,7 +611,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
             rename_to = None
 
             # NOTE: We cannot conclude from the existence of a blob to change type
-            # as diffs with the working do not have blobs yet
+            # as diffs with the working do not have blobs yet.
             if change_type == "D":
                 b_blob_id = None  # Optional[str]
                 deleted_file = True
@@ -621,7 +629,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
                 b_path = b_path_str.encode(defenc)
                 rename_from, rename_to = a_path, b_path
             elif change_type == "T":
-                # Nothing to do
+                # Nothing to do.
                 pass
             # END add/remove handling
 
diff --git a/git/exc.py b/git/exc.py
index 32c371d0b..bfb023fa5 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -3,7 +3,8 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
-""" Module containing all exceptions thrown throughout the git package """
+
+"""Module containing all exceptions thrown throughout the git package."""
 
 __all__ = [
     # Defined in gitdb.exc:
@@ -57,7 +58,7 @@
 
 
 class GitError(Exception):
-    """Base class for all package exceptions"""
+    """Base class for all package exceptions."""
 
 
 class InvalidGitRepositoryError(GitError):
@@ -65,7 +66,7 @@ class InvalidGitRepositoryError(GitError):
 
 
 class WorkTreeRepositoryUnsupported(InvalidGitRepositoryError):
-    """Thrown to indicate we can't handle work tree repositories"""
+    """Thrown to indicate we can't handle work tree repositories."""
 
 
 class NoSuchPathError(GitError, OSError):
@@ -133,7 +134,7 @@ def __str__(self) -> str:
 
 class GitCommandNotFound(CommandError):
     """Thrown if we cannot find the `git` executable in the PATH or at the path given by
-    the GIT_PYTHON_GIT_EXECUTABLE environment variable"""
+    the GIT_PYTHON_GIT_EXECUTABLE environment variable."""
 
     def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
         super(GitCommandNotFound, self).__init__(command, cause)
@@ -157,15 +158,15 @@ class CheckoutError(GitError):
     """Thrown if a file could not be checked out from the index as it contained
     changes.
 
-    The .failed_files attribute contains a list of relative paths that failed
-    to be checked out as they contained changes that did not exist in the index.
+    The :attr:`failed_files` attribute contains a list of relative paths that failed to
+    be checked out as they contained changes that did not exist in the index.
 
-    The .failed_reasons attribute contains a string informing about the actual
+    The :attr:`failed_reasons` attribute contains a string informing about the actual
     cause of the issue.
 
-    The .valid_files attribute contains a list of relative paths to files that
-    were checked out successfully and hence match the version stored in the
-    index"""
+    The :attr:`valid_files` attribute contains a list of relative paths to files that
+    were checked out successfully and hence match the version stored in the index.
+    """
 
     def __init__(
         self,
@@ -184,18 +185,20 @@ def __str__(self) -> str:
 
 
 class CacheError(GitError):
-
-    """Base for all errors related to the git index, which is called cache internally"""
+    """Base for all errors related to the git index, which is called cache
+    internally."""
 
 
 class UnmergedEntriesError(CacheError):
     """Thrown if an operation cannot proceed as there are still unmerged
-    entries in the cache"""
+    entries in the cache."""
 
 
 class HookExecutionError(CommandError):
-    """Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
-    via standard output"""
+    """Thrown if a hook exits with a non-zero exit code.
+
+    This provides access to the exit code and the string returned via standard output.
+    """
 
     def __init__(
         self,
@@ -209,7 +212,8 @@ def __init__(
 
 
 class RepositoryDirtyError(GitError):
-    """Thrown whenever an operation on a repository fails as it has uncommitted changes that would be overwritten"""
+    """Thrown whenever an operation on a repository fails as it has uncommitted changes
+    that would be overwritten."""
 
     def __init__(self, repo: "Repo", message: str) -> None:
         self.repo = repo
diff --git a/git/index/__init__.py b/git/index/__init__.py
index 96b721f07..f9a534ee7 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -1,4 +1,6 @@
-"""Initialize the index package"""
+"""Initialize the index package."""
+
 # flake8: noqa
+
 from .base import *
 from .typ import *
diff --git a/git/index/base.py b/git/index/base.py
index 5da904b4e..4f1b6c469 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -95,12 +95,11 @@
 
 
 class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
-
     """
-    Implements an Index that can be manipulated using a native implementation in
-    order to save git command function calls wherever possible.
+    An Index that can be manipulated using a native implementation in order to save git
+    command function calls wherever possible.
 
-    It provides custom merging facilities allowing to merge without actually changing
+    This provides custom merging facilities allowing to merge without actually changing
     your index or your working tree. This way you can perform own test-merges based
     on the index only without having to deal with the working copy. This is useful
     in case of partial working trees.
@@ -115,18 +114,23 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
         index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
 
     Make sure you use index.write() once you are done manipulating the index directly
-    before operating on it using the git command"""
+    before operating on it using the git command.
+    """
 
     __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
-    _VERSION = 2  # latest version we support
-    S_IFGITLINK = S_IFGITLINK  # a submodule
+
+    _VERSION = 2  # Latest version we support.
+
+    S_IFGITLINK = S_IFGITLINK  # A submodule.
 
     def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
         """Initialize this Index instance, optionally from the given ``file_path``.
+
         If no file_path is given, we will be created from the current index file.
 
         If a stream is not given, the stream will be initialized from the current
-        repository's index on demand."""
+        repository's index on demand.
+        """
         self.repo = repo
         self.version = self._VERSION
         self._extension_data = b""
@@ -137,7 +141,7 @@ def _set_cache_(self, attr: str) -> None:
             try:
                 fd = os.open(self._file_path, os.O_RDONLY)
             except OSError:
-                # in new repositories, there may be no index, which means we are empty
+                # In new repositories, there may be no index, which means we are empty.
                 self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
                 return None
             # END exception handling
@@ -163,18 +167,19 @@ def path(self) -> PathLike:
         return self._file_path
 
     def _delete_entries_cache(self) -> None:
-        """Safely clear the entries cache so it can be recreated"""
+        """Safely clear the entries cache so it can be recreated."""
         try:
             del self.entries
         except AttributeError:
-            # fails in python 2.6.5 with this exception
+            # It failed in Python 2.6.5 with AttributeError.
+            # FIXME: Look into whether we can just remove this except clause now.
             pass
         # END exception handling
 
     # { Serializable Interface
 
     def _deserialize(self, stream: IO) -> "IndexFile":
-        """Initialize this instance with index values read from the given stream"""
+        """Initialize this instance with index values read from the given stream."""
         self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
         return self
 
@@ -197,7 +202,7 @@ def write(
         file_path: Union[None, PathLike] = None,
         ignore_extension_data: bool = False,
     ) -> None:
-        """Write the current state to our file path or to the given one
+        """Write the current state to our file path or to the given one.
 
         :param file_path:
             If None, we will write to our stored file path from which we have
@@ -214,12 +219,11 @@ def write(
             If this data is present in the written index, git-write-tree
             will instead write the stored/cached tree.
             Alternatively, use IndexFile.write_tree() to handle this case
-            automatically
-
-        :return: self  # does it? or returns None?"""
-        # make sure we have our entries read before getting a write lock
-        # else it would be done when streaming. This can happen
-        # if one doesn't change the index, but writes it right away
+            automatically.
+        """
+        # Make sure we have our entries read before getting a write lock.
+        # Otherwise it would be done when streaming. This can happen if one
+        # doesn't change the index, but writes it right away.
         self.entries
         lfd = LockedFD(file_path or self._file_path)
         stream = lfd.open(write=True, stream=True)
@@ -232,7 +236,7 @@ def write(
 
         lfd.commit()
 
-        # make sure we represent what we have written
+        # Make sure we represent what we have written.
         if file_path is not None:
             self._file_path = file_path
 
@@ -242,26 +246,26 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF
         """Merge the given rhs treeish into the current index, possibly taking
         a common base treeish into account.
 
-        As opposed to the :func:`IndexFile.from_tree` method, this allows you to use an already
-        existing tree as the left side of the merge
+        As opposed to the :func:`IndexFile.from_tree` method, this allows you to use an
+        already existing tree as the left side of the merge.
 
         :param rhs:
-            treeish reference pointing to the 'other' side of the merge.
+            Treeish reference pointing to the 'other' side of the merge.
 
         :param base:
-            optional treeish reference pointing to the common base of 'rhs' and
-            this index which equals lhs
+            Optional treeish reference pointing to the common base of 'rhs' and this
+            index which equals lhs.
 
         :return:
-            self ( containing the merge and possibly unmerged entries in case of
-            conflicts )
+            self (containing the merge and possibly unmerged entries in case of
+            conflicts)
 
         :raise GitCommandError:
-            If there is a merge conflict. The error will
-            be raised at the first conflicting path. If you want to have proper
-            merge resolution to be done by yourself, you have to commit the changed
-            index ( or make a valid tree from it ) and retry with a three-way
-            index.from_tree call."""
+            If there is a merge conflict. The error will be raised at the first
+            conflicting path. If you want to have proper merge resolution to be done by
+            yourself, you have to commit the changed index (or make a valid tree from
+            it) and retry with a three-way index.from_tree call.
+        """
         # -i : ignore working tree status
         # --aggressive : handle more merge cases
         # -m : do an actual merge
@@ -276,22 +280,24 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF
     @classmethod
     def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
         """Merge the given treeish revisions into a new index which is returned.
-        This method behaves like git-read-tree --aggressive when doing the merge.
+
+        This method behaves like ``git-read-tree --aggressive`` when doing the merge.
 
         :param repo: The repository treeish are located in.
 
         :param tree_sha:
-            20 byte or 40 byte tree sha or tree objects
+            20 byte or 40 byte tree sha or tree objects.
 
         :return:
             New IndexFile instance. Its path will be undefined.
             If you intend to write such a merged Index, supply an alternate file_path
-            to its 'write' method."""
+            to its 'write' method.
+        """
         tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
         base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
 
         inst = cls(repo)
-        # convert to entries dict
+        # Convert to entries dict.
         entries: Dict[Tuple[PathLike, int], IndexEntry] = dict(
             zip(
                 ((e.path, e.stage) for e in base_entries),
@@ -305,7 +311,7 @@ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
     @classmethod
     def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
         """Merge the given treeish revisions into a new index which is returned.
-        The original index will remain unaltered
+        The original index will remain unaltered.
 
         :param repo:
             The repository treeish are located in.
@@ -319,10 +325,10 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             one. It behaves like a fast-forward.
             If 3 Trees are given, a 3-way merge will be performed with the first tree
             being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree,
-            tree 3 is the 'other' one
+            tree 3 is the 'other' one.
 
         :param kwargs:
-            Additional arguments passed to git-read-tree
+            Additional arguments passed to git-read-tree.
 
         :return:
             New IndexFile instance. It will point to a temporary index location which
@@ -336,46 +342,49 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
 
             As the underlying git-read-tree command takes into account the current index,
             it will be temporarily moved out of the way to assure there are no unsuspected
-            interferences."""
+            interferences.
+        """
         if len(treeish) == 0 or len(treeish) > 3:
             raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
 
         arg_list: List[Union[Treeish, str]] = []
-        # ignore that working tree and index possibly are out of date
+        # Ignore that the working tree and index possibly are out of date.
         if len(treeish) > 1:
-            # drop unmerged entries when reading our index and merging
+            # Drop unmerged entries when reading our index and merging.
             arg_list.append("--reset")
-            # handle non-trivial cases the way a real merge does
+            # Handle non-trivial cases the way a real merge does.
             arg_list.append("--aggressive")
         # END merge handling
 
         # tmp file created in git home directory to be sure renaming
-        # works - /tmp/ dirs could be on another device
+        # works - /tmp/ dirs could be on another device.
         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.
+            # 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
+            # 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
+            index.entries  # Force it to read the file as we will delete the temp-file.
             return index
             # END index merge handling
 
     # UTILITIES
     @unbare_repo
     def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
-        """Expand the directories in list of paths to the corresponding paths accordingly,
+        """Expand the directories in list of paths to the corresponding paths accordingly.
 
-        Note: git will add items multiple times even if a glob overlapped
-        with manually specified paths or if paths where specified multiple
-        times - we respect that and do not prune"""
+        :note:
+            git will add items multiple times even if a glob overlapped
+            with manually specified paths or if paths where specified multiple
+            times - we respect that and do not prune.
+        """
 
         def raise_exc(e: Exception) -> NoReturn:
             raise e
@@ -389,9 +398,9 @@ def raise_exc(e: Exception) -> NoReturn:
             # END make absolute path
 
             try:
-                st = os.lstat(abs_path)  # handles non-symlinks as well
+                st = os.lstat(abs_path)  # Handles non-symlinks as well.
             except OSError:
-                # the lstat call may fail as the path may contain globs as well
+                # The lstat call may fail as the path may contain globs as well.
                 pass
             else:
                 if S_ISLNK(st.st_mode):
@@ -399,7 +408,7 @@ def raise_exc(e: Exception) -> NoReturn:
                     continue
             # end check symlink
 
-            # if the path is not already pointing to an existing file, resolve globs if possible
+            # If the path is not already pointing to an existing file, resolve globs if possible.
             if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
                 resolved_paths = glob.glob(abs_path)
                 # not abs_path in resolved_paths:
@@ -416,12 +425,12 @@ def raise_exc(e: Exception) -> NoReturn:
             try:
                 for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
                     for rela_file in files:
-                        # add relative paths only
+                        # Add relative paths only.
                         yield osp.join(root.replace(rs, ""), rela_file)
                     # END for each file in subdir
                 # END for each subdirectory
             except OSError:
-                # was a file or something that could not be iterated
+                # It was a file or something that could not be iterated.
                 yield abs_path.replace(rs, "")
             # END path exception handling
         # END for each path
@@ -438,17 +447,20 @@ def _write_path_to_stdin(
         """Write path to proc.stdin and make sure it processes the item, including progress.
 
         :return: stdout string
+
         :param read_from_stdout: if True, proc.stdout will be read after the item
-            was sent to stdin. In that case, it will return None
+            was sent to stdin. In that case, it will return None.
+
         :note: There is a bug in git-update-index that prevents it from sending
             reports just in time. This is why we have a version that tries to
             read stdout and one which doesn't. In fact, the stdout is not
-            important as the piped-in files are processed anyway and just in time
+            important as the piped-in files are processed anyway and just in time.
+
         :note: Newlines are essential here, gits behaviour is somewhat inconsistent
             on this depending on the version, hence we try our best to deal with
             newlines carefully. Usually the last newline will not be sent, instead
-            we will close stdin to break the pipe."""
-
+            we will close stdin to break the pipe.
+        """
         fprogress(filepath, False, item)
         rval: Union[None, str] = None
 
@@ -456,7 +468,7 @@ def _write_path_to_stdin(
             try:
                 proc.stdin.write(("%s\n" % filepath).encode(defenc))
             except IOError as e:
-                # pipe broke, usually because some error happened
+                # Pipe broke, usually because some error happened.
                 raise fmakeexc() from e
             # END write exception handling
             proc.stdin.flush()
@@ -475,7 +487,8 @@ def iter_blobs(
         :param predicate:
             Function(t) returning True if tuple(stage, Blob) should be yielded by the
             iterator. A default filter, the BlobFilter, allows you to yield blobs
-            only if they match a given list of paths."""
+            only if they match a given list of paths.
+        """
         for entry in self.entries.values():
             blob = entry.to_blob(self.repo)
             blob.size = entry.size
@@ -489,8 +502,7 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
         :return:
             Dict(path : list( tuple( stage, Blob, ...))), being
             a dictionary associating a path in the index with a list containing
-            sorted stage/blob pairs
-
+            sorted stage/blob pairs.
 
         :note:
             Blobs that have been removed in one side simply do not exist in the
@@ -512,19 +524,21 @@ def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[
         return entry_key(*entry)
 
     def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
-        """Resolve the blobs given in blob iterator. This will effectively remove the
-        index entries of the respective path at all non-null stages and add the given
-        blob as new stage null blob.
+        """Resolve the blobs given in blob iterator.
+
+        This will effectively remove the index entries of the respective path at all
+        non-null stages and add the given blob as new stage null blob.
 
         For each path there may only be one blob, otherwise a ValueError will be raised
         claiming the path is already at stage 0.
 
         :raise ValueError: if one of the blobs already existed at stage 0
+
         :return: self
 
         :note:
             You will have to write the index manually once you are done, i.e.
-            index.resolve_blobs(blobs).write()
+            ``index.resolve_blobs(blobs).write()``.
         """
         for blob in iter_blobs:
             stage_null_key = (blob.path, 0)
@@ -532,7 +546,7 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
                 raise ValueError("Path %r already exists at stage 0" % str(blob.path))
             # END assert blob is not stage 0 already
 
-            # delete all possible stages
+            # Delete all possible stages.
             for stage in (1, 2, 3):
                 try:
                     del self.entries[(blob.path, stage)]
@@ -550,34 +564,40 @@ def update(self) -> "IndexFile":
         """Reread the contents of our index file, discarding all cached information
         we might have.
 
-        :note: This is a possibly dangerious operations as it will discard your changes
-            to index.entries
-        :return: self"""
+        :note: This is a possibly dangerous operations as it will discard your changes
+            to index.entries.
+
+        :return: self
+        """
         self._delete_entries_cache()
-        # allows to lazily reread on demand
+        # Allows to lazily reread on demand.
         return self
 
     def write_tree(self) -> Tree:
-        """Writes this index to a corresponding Tree object into the repository's
+        """Write this index to a corresponding Tree object into the repository's
         object database and return it.
 
-        :return: Tree object representing this index
+        :return: Tree object representing this index.
+
         :note: The tree will be written even if one or more objects the tree refers to
             does not yet exist in the object database. This could happen if you added
             Entries to the index directly.
+
         :raise ValueError: if there are no entries in the cache
-        :raise UnmergedEntriesError:"""
-        # we obtain no lock as we just flush our contents to disk as tree
-        # If we are a new index, the entries access will load our data accordingly
+
+        :raise UnmergedEntriesError:
+        """
+        # We obtain no lock as we just flush our contents to disk as tree.
+        # If we are a new index, the entries access will load our data accordingly.
         mdb = MemoryDB()
         entries = self._entries_sorted()
         binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
 
-        # copy changed trees only
+        # Copy changed trees only.
         mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
 
-        # note: additional deserialization could be saved if write_tree_from_cache
-        # would return sorted tree entries
+        # Note: Additional deserialization could be saved if write_tree_from_cache
+        # would return sorted tree entries.
         root_tree = Tree(self.repo, binsha, path="")
         root_tree._cache = tree_items
         return root_tree
@@ -630,9 +650,10 @@ def _preprocess_add_items(
     def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
         """Store file at filepath in the database and return the base index entry
         Needs the git_working_dir decorator active ! This must be assured in the calling code"""
-        st = os.lstat(filepath)  # handles non-symlinks as well
+        st = os.lstat(filepath)  # Handles non-symlinks as well.
         if S_ISLNK(st.st_mode):
-            # in PY3, readlink is string, but we need bytes. In PY2, it's just OS encoded bytes, we assume UTF-8
+            # In PY3, readlink is a string, but we need bytes.
+            # In PY2, it was just OS encoded bytes, we assumed UTF-8.
             open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
         else:
             open_stream = lambda: open(filepath, "rb")
@@ -708,51 +729,54 @@ def add(
             relative or absolute.
 
             - path string
-                strings denote a relative or absolute path into the repository pointing to
-                an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'.
+                Strings denote a relative or absolute path into the repository pointing
+                to an existing file, e.g., CHANGES, lib/myfile.ext,
+                '/home/gitrepo/lib/myfile.ext'.
 
-                Absolute paths must start with working tree directory of this index's repository
-                to be considered valid. For example, if it was initialized with a non-normalized path, like
-                `/root/repo/../repo`, absolute paths to be added must start with `/root/repo/../repo`.
+                Absolute paths must start with working tree directory of this index's
+                repository to be considered valid. For example, if it was initialized
+                with a non-normalized path, like ``/root/repo/../repo``, absolute paths
+                to be added must start with ``/root/repo/../repo``.
 
                 Paths provided like this must exist. When added, they will be written
                 into the object database.
 
-                PathStrings may contain globs, such as 'lib/__init__*' or can be directories
-                like 'lib', the latter ones will add all the files within the directory and
-                subdirectories.
+                PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
+                directories like ``lib``, which will add all the files within the
+                directory and subdirectories.
 
                 This equals a straight git-add.
 
-                They are added at stage 0
+                They are added at stage 0.
 
             - Blob or Submodule object
                 Blobs are added as they are assuming a valid mode is set.
-                The file they refer to may or may not exist in the file system, but
-                must be a path relative to our repository.
-
-                If their sha is null ( 40*0 ), their path must exist in the file system
-                relative to the git repository as an object will be created from
-                the data at the path.
-                The handling now very much equals the way string paths are processed, except that
-                the mode you have set will be kept. This allows you to create symlinks
-                by settings the mode respectively and writing the target of the symlink
-                directly into the file. This equals a default Linux-Symlink which
-                is not dereferenced automatically, except that it can be created on
-                filesystems not supporting it as well.
+
+                The file they refer to may or may not exist in the file system, but must
+                be a path relative to our repository.
+
+                If their sha is null (40*0), their path must exist in the file system
+                relative to the git repository as an object will be created from the
+                data at the path.
+
+                The handling now very much equals the way string paths are processed,
+                except that the mode you have set will be kept. This allows you to
+                create symlinks by settings the mode respectively and writing the target
+                of the symlink directly into the file. This equals a default
+                Linux-Symlink which is not dereferenced automatically, except that it
+                can be created on filesystems not supporting it as well.
 
                 Please note that globs or directories are not allowed in Blob objects.
 
-                They are added at stage 0
+                They are added at stage 0.
 
             - BaseIndexEntry or type
-                Handling equals the one of Blob objects, but the stage may be
-                explicitly set. Please note that Index Entries require binary sha's.
+                Handling equals the one of Blob objects, but the stage may be explicitly
+                set. Please note that Index Entries require binary sha's.
 
         :param force:
             **CURRENTLY INEFFECTIVE**
-            If True, otherwise ignored or excluded files will be
-            added anyway.
+            If True, otherwise ignored or excluded files will be added anyway.
             As opposed to the git-add command, we enable this flag by default
             as the API user usually wants the item to be added even though
             they might be excluded.
@@ -766,26 +790,31 @@ def add(
             in the index already as the index is currently being processed.
 
         :param path_rewriter:
-            Function with signature (string) func(BaseIndexEntry) function returning a path
-            for each passed entry which is the path to be actually recorded for the
-            object created from entry.path. This allows you to write an index which
-            is not identical to the layout of the actual files on your hard-disk.
-            If not None and ``items`` contain plain paths, these paths will be
-            converted to Entries beforehand and passed to the path_rewriter.
-            Please note that entry.path is relative to the git repository.
+            Function with signature (string) func(BaseIndexEntry) function returning a
+            path for each passed entry which is the path to be actually recorded for the
+            object created from entry.path. This allows you to write an index which is
+            not identical to the layout of the actual files on your hard-disk. If not
+            None and ``items`` contain plain paths, these paths will be converted to
+            Entries beforehand and passed to the path_rewriter. Please note that
+            entry.path is relative to the git repository.
 
         :param write:
             If True, the index will be written once it was altered. Otherwise
             the changes only exist in memory and are not available to git commands.
 
         :param write_extension_data:
-            If True, extension data will be written back to the index. This can lead to issues in case
-            it is containing the 'TREE' extension, which will cause the `git commit` command to write an
-            old tree, instead of a new one representing the now changed index.
-            This doesn't matter if you use `IndexFile.commit()`, which ignores the `TREE` extension altogether.
-            You should set it to True if you intend to use `IndexFile.commit()` exclusively while maintaining
-            support for third-party extensions. Besides that, you can usually safely ignore the built-in
-            extensions when using GitPython on repositories that are not handled manually at all.
+            If True, extension data will be written back to the index. This can lead to
+            issues in case it is containing the 'TREE' extension, which will cause the
+            `git commit` command to write an old tree, instead of a new one representing
+            the now changed index.
+
+            This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
+            `TREE` extension altogether. You should set it to True if you intend to use
+            :meth:`IndexFile.commit` exclusively while maintaining support for
+            third-party extensions. Besides that, you can usually safely ignore the
+            built-in extensions when using GitPython on repositories that are not
+            handled manually at all.
+
             All current built-in extensions are listed here:
             http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/technical/index-format.txt
 
@@ -793,18 +822,17 @@ def add(
             List(BaseIndexEntries) representing the entries just actually added.
 
         :raise OSError:
-            if a supplied Path did not exist. Please note that BaseIndexEntry
+            If a supplied Path did not exist. Please note that BaseIndexEntry
             Objects that do not have a null sha will be added even if their paths
             do not exist.
         """
-        # sort the entries into strings and Entries, Blobs are converted to entries
-        # automatically
-        # paths can be git-added, for everything else we use git-update-index
+        # Sort the entries into strings and Entries. Blobs are converted to entries automatically.
+        # Paths can be git-added. For everything else we use git-update-index.
         paths, entries = self._preprocess_add_items(items)
         entries_added: List[BaseIndexEntry] = []
         # This code needs a working tree, therefore we try not to run it unless required.
         # That way, we are OK on a bare repository as well.
-        # If there are no paths, the rewriter has nothing to do either
+        # If there are no paths, the rewriter has nothing to do either.
         if paths:
             entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
 
@@ -818,7 +846,7 @@ def add(
             # END null mode should be remove
 
             # HANDLE ENTRY OBJECT CREATION
-            # create objects if required, otherwise go with the existing shas
+            # Create objects if required, otherwise go with the existing shas.
             null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
             if null_entries_indices:
 
@@ -828,7 +856,7 @@ def handle_null_entries(self: "IndexFile") -> None:
                         null_entry = entries[ei]
                         new_entry = self._store_path(null_entry.path, fprogress)
 
-                        # update null entry
+                        # Update null entry.
                         entries[ei] = BaseIndexEntry(
                             (
                                 null_entry.mode,
@@ -844,15 +872,14 @@ def handle_null_entries(self: "IndexFile") -> None:
             # END null_entry handling
 
             # REWRITE PATHS
-            # If we have to rewrite the entries, do so now, after we have generated
-            # all object sha's
+            # If we have to rewrite the entries, do so now, after we have generated all object sha's.
             if path_rewriter:
                 for i, e in enumerate(entries):
                     entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
                 # END for each entry
             # END handle path rewriting
 
-            # just go through the remaining entries and provide progress info
+            # Just go through the remaining entries and provide progress info.
             for i, entry in enumerate(entries):
                 progress_sent = i in null_entries_indices
                 if not progress_sent:
@@ -864,7 +891,7 @@ def handle_null_entries(self: "IndexFile") -> None:
         # END if there are base entries
 
         # FINALIZE
-        # add the new entries to this instance
+        # Add the new entries to this instance.
         for entry in entries_added:
             self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
 
@@ -879,9 +906,9 @@ def _items_to_rela_paths(
         items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
     ) -> List[PathLike]:
         """Returns a list of repo-relative paths from the given items which
-        may be absolute or relative paths, entries or blobs"""
+        may be absolute or relative paths, entries or blobs."""
         paths = []
-        # if string put in list
+        # If string, put in list.
         if isinstance(items, (str, os.PathLike)):
             items = [items]
 
@@ -937,17 +964,18 @@ def remove(
             List(path_string, ...) list of repository relative paths that have
             been removed effectively.
             This is interesting to know in case you have provided a directory or
-            globs. Paths are relative to the repository."""
+            globs. Paths are relative to the repository.
+        """
         args = []
         if not working_tree:
             args.append("--cached")
         args.append("--")
 
-        # preprocess paths
+        # Preprocess paths.
         paths = self._items_to_rela_paths(items)
         removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
 
-        # process output to gain proper paths
+        # Process output to gain proper paths.
         # rm 'path'
         return [p[4:-1] for p in removed_paths]
 
@@ -960,14 +988,14 @@ def move(
         **kwargs: Any,
     ) -> List[Tuple[str, str]]:
         """Rename/move the items, whereas the last item is considered the destination of
-        the move operation. If the destination is a file, the first item ( of two )
+        the move operation. If the destination is a file, the first item (of two)
         must be a file as well. If the destination is a directory, it may be preceded
         by one or more directories or files.
 
         The working tree will be affected in non-bare repositories.
 
         :parma items:
-            Multiple types of items are supported, please see the 'remove' method
+            Multiple types of items are supported, please see the :meth:`remove` method
             for reference.
         :param skip_errors:
             If True, errors such as ones resulting from missing source files will
@@ -981,7 +1009,8 @@ def move(
             actual destination. Relative to the repository root.
 
         :raise ValueError: If only one item was given
-        :raise GitCommandError: If git could not handle your request"""
+        :raise GitCommandError: If git could not handle your request
+        """
         args = []
         if skip_errors:
             args.append("-k")
@@ -993,13 +1022,13 @@ def move(
         was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
         kwargs["dry_run"] = True
 
-        # first execute rename in dryrun so the command tells us what it actually does
-        # ( for later output )
+        # First execute rename in dryrun so the command tells us what it actually does.
+        # (for later output)
         out = []
         mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
 
-        # parse result - first 0:n/2 lines are 'checking ', the remaining ones
-        # are the 'renaming' ones which we parse
+        # Parse result - first 0:n/2 lines are 'checking ', the remaining ones
+        # are the 'renaming' ones which we parse.
         for ln in range(int(len(mvlines) / 2), len(mvlines)):
             tokens = mvlines[ln].split(" to ")
             assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
@@ -1009,12 +1038,12 @@ def move(
             out.append((tokens[0][9:], tokens[1]))
         # END for each line to parse
 
-        # either prepare for the real run, or output the dry-run result
+        # Either prepare for the real run, or output the dry-run result.
         if was_dry_run:
             return out
         # END handle dryrun
 
-        # now apply the actual operation
+        # Now apply the actual operation.
         kwargs.pop("dry_run")
         self.repo.git.mv(args, paths, **kwargs)
 
@@ -1031,14 +1060,18 @@ def commit(
         commit_date: Union[datetime.datetime, str, None] = None,
         skip_hooks: bool = False,
     ) -> Commit:
-        """Commit the current default index file, creating a commit object.
-        For more information on the arguments, see Commit.create_from_tree().
-
-        :note: If you have manually altered the .entries member of this instance,
-               don't forget to write() your changes to disk beforehand.
-               Passing skip_hooks=True is the equivalent of using `-n`
-               or `--no-verify` on the command line.
-        :return: Commit object representing the new commit"""
+        """Commit the current default index file, creating a Commit object.
+
+        For more information on the arguments, see
+        :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
+
+        :note: If you have manually altered the :attr:`entries` member of this instance,
+               don't forget to :meth:`write` your changes to disk beforehand.
+               Passing ``skip_hooks=True`` is the equivalent of using ``-n``
+               or ``--no-verify`` on the command line.
+
+        :return: :class:`Commit` object representing the new commit
+        """
         if not skip_hooks:
             run_commit_hook("pre-commit", self)
 
@@ -1099,11 +1132,11 @@ def checkout(
         fprogress: Callable = lambda *args: None,
         **kwargs: Any,
     ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
-        """Checkout the given paths or all files from the version known to the index into
-        the working tree.
+        """Check out the given paths or all files from the version known to the index
+        into the working tree.
 
-        :note: Be sure you have written pending changes using the ``write`` method
-            in case you have altered the enties dictionary directly
+        :note: Be sure you have written pending changes using the :meth:`write` method
+            in case you have altered the entries dictionary directly.
 
         :param paths:
             If None, all paths in the index will be checked out. Otherwise an iterable
@@ -1112,20 +1145,20 @@ def checkout(
 
         :param force:
             If True, existing files will be overwritten even if they contain local modifications.
-            If False, these will trigger a CheckoutError.
+            If False, these will trigger a :class:`CheckoutError`.
 
         :param fprogress:
             see :func:`IndexFile.add` for signature and explanation.
             The provided progress information will contain None as path and item if no
             explicit paths are given. Otherwise progress information will be send
-            prior and after a file has been checked out
+            prior and after a file has been checked out.
 
         :param kwargs:
-            Additional arguments to be passed to git-checkout-index
+            Additional arguments to be passed to git-checkout-index.
 
         :return:
             iterable yielding paths to files which have been checked out and are
-            guaranteed to match the version stored in the index
+            guaranteed to match the version stored in the index.
 
         :raise exc.CheckoutError:
             If at least one file failed to be checked out. This is a summary,
@@ -1133,7 +1166,7 @@ def checkout(
             If one of files or directories do not exist in the index
             ( as opposed to the  original git command who ignores them ).
             Raise GitCommandError if error lines could not be parsed - this truly is
-            an exceptional state
+            an exceptional state.
 
         .. note:: The checkout is limited to checking out the files in the
             index. Files which are not in the index anymore and exist in
@@ -1307,9 +1340,9 @@ def reset(
         :param kwargs:
             Additional keyword arguments passed to git-reset
 
-        .. note:: IndexFile.reset, as opposed to HEAD.reset, will not delete anyfiles
+        .. note:: IndexFile.reset, as opposed to HEAD.reset, will not delete any files
             in order to maintain a consistent working tree. Instead, it will just
-            checkout the files according to their state in the index.
+            check out the files according to their state in the index.
             If you want git-reset like behaviour, use *HEAD.reset* instead.
 
         :return: self"""
@@ -1355,40 +1388,40 @@ def diff(
         create_patch: bool = False,
         **kwargs: Any,
     ) -> git_diff.DiffIndex:
-        """Diff this index against the working copy or a Tree or Commit object
+        """Diff this index against the working copy or a Tree or Commit object.
 
-        For a documentation of the parameters and return values, see,
-        Diffable.diff
+        For a documentation of the parameters and return values, see
+        :meth:`Diffable.diff <git.diff.Diffable.diff>`.
 
         :note:
             Will only work with indices that represent the default git index as
             they have not been initialized with a stream.
         """
 
-        # only run if we are the default repository index
+        # Only run if we are the default repository index.
         if self._file_path != self._index_path():
             raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
-        # index against index is always empty
+        # Index against index is always empty.
         if other is self.Index:
             return git_diff.DiffIndex()
 
-        # index against anything but None is a reverse diff with the respective
+        # Index against anything but None is a reverse diff with the respective
         # item. Handle existing -R flags properly. Transform strings to the object
-        # so that we can call diff on it
+        # so that we can call diff on it.
         if isinstance(other, str):
             other = self.repo.rev_parse(other)
         # END object conversion
 
         if isinstance(other, Object):  # for Tree or Commit
-            # invert the existing R flag
+            # Invert the existing R flag.
             cur_val = kwargs.get("R", False)
             kwargs["R"] = not cur_val
             return other.diff(self.Index, paths, create_patch, **kwargs)
         # END diff against other item handling
 
-        # if other is not None here, something is wrong
+        # If other is not None here, something is wrong.
         if other is not None:
             raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other)
 
-        # diff against working copy - can be handled by superclass natively
+        # Diff against working copy - can be handled by superclass natively.
         return super(IndexFile, self).diff(other, paths, create_patch, **kwargs)
diff --git a/git/index/fun.py b/git/index/fun.py
index b50f1f465..ace0866f4 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -1,6 +1,5 @@
-# Contains standalone functions to accompany the index implementation and make it
-# more versatile
-# NOTE: Autodoc hates it if this is a docstring
+# Standalone functions to accompany the index implementation and make it more versatile.
+# NOTE: Autodoc hates it if this is a docstring.
 
 from io import BytesIO
 from pathlib import Path
@@ -56,7 +55,7 @@
 # ------------------------------------------------------------------------------------
 
 
-S_IFGITLINK = S_IFLNK | S_IFDIR  # a submodule
+S_IFGITLINK = S_IFLNK | S_IFDIR  # A submodule.
 CE_NAMEMASK_INV = ~CE_NAMEMASK
 
 __all__ = (
@@ -81,12 +80,13 @@ def _has_file_extension(path: str) -> str:
 
 
 def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
-    """Run the commit hook of the given name. Silently ignores hooks that do not exist.
+    """Run the commit hook of the given name. Silently ignore hooks that do not exist.
 
     :param name: name of hook, like 'pre-commit'
     :param index: IndexFile instance
-    :param args: arguments passed to hook file
-    :raises HookExecutionError:"""
+    :param args: Arguments passed to hook file
+    :raises HookExecutionError:
+    """
     hp = hook_path(name, index.repo.git_dir)
     if not os.access(hp, os.X_OK):
         return None
@@ -128,7 +128,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
 
 def stat_mode_to_index_mode(mode: int) -> int:
     """Convert the given mode from a stat call to the corresponding index mode
-    and return it"""
+    and return it."""
     if S_ISLNK(mode):  # symlinks
         return S_IFLNK
     if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK:  # submodules
@@ -142,9 +142,10 @@ def write_cache(
     extension_data: Union[None, bytes] = None,
     ShaStreamCls: Type[IndexFileSHA1Writer] = IndexFileSHA1Writer,
 ) -> None:
-    """Write the cache represented by entries to a stream
+    """Write the cache represented by entries to a stream.
 
     :param entries: **sorted** list of entries
+
     :param stream: stream to wrap into the AdapterStreamCls - it is used for
         final output.
 
@@ -152,28 +153,29 @@ def write_cache(
         while writing to it, before the data is passed on to the wrapped stream
 
     :param extension_data: any kind of data to write as a trailer, it must begin
-        a 4 byte identifier, followed by its size ( 4 bytes )"""
-    # wrap the stream into a compatible writer
+        a 4 byte identifier, followed by its size (4 bytes).
+    """
+    # Wrap the stream into a compatible writer.
     stream_sha = ShaStreamCls(stream)
 
     tell = stream_sha.tell
     write = stream_sha.write
 
-    # header
+    # Header
     version = 2
     write(b"DIRC")
     write(pack(">LL", version, len(entries)))
 
-    # body
+    # Body
     for entry in entries:
         beginoffset = tell()
         write(entry.ctime_bytes)  # ctime
         write(entry.mtime_bytes)  # mtime
         path_str = str(entry.path)
         path: bytes = force_bytes(path_str, encoding=defenc)
-        plen = len(path) & CE_NAMEMASK  # path length
+        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
+        flags = plen | (entry.flags & CE_NAMEMASK_INV)  # Clear possible previous values.
         write(
             pack(
                 ">LLLLLL20sH",
@@ -192,11 +194,11 @@ def write_cache(
         write(b"\0" * ((beginoffset + real_size) - tell()))
     # END for each entry
 
-    # write previously cached extensions data
+    # Write previously cached extensions data.
     if extension_data is not None:
         stream_sha.write(extension_data)
 
-    # write the sha over the content
+    # Write the sha over the content.
     stream_sha.write_sha()
 
 
@@ -208,14 +210,16 @@ 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
+    # TODO: Handle version 3: extended data, see read-cache.c.
     assert version in (1, 2)
     return version, num_entries
 
 
 def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]:
     """:return: Key suitable to be used for the index.entries dictionary
-    :param entry: One instance of type BaseIndexEntry or the path and the stage"""
+
+    :param entry: One instance of type BaseIndexEntry or the path and the stage
+    """
 
     # def is_entry_key_tup(entry_key: Tuple) -> TypeGuard[Tuple[PathLike, int]]:
     #     return isinstance(entry_key, tuple) and len(entry_key) == 2
@@ -234,14 +238,15 @@ def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, i
 def read_cache(
     stream: IO[bytes],
 ) -> Tuple[int, Dict[Tuple[PathLike, int], "IndexEntry"], bytes, bytes]:
-    """Read a cache file from the given stream
+    """Read a cache file from the given stream.
 
     :return: tuple(version, entries_dict, extension_data, content_sha)
 
-      * version is the integer version number
-      * entries dict is a dictionary which maps IndexEntry instances to a path at a stage
-      * extension_data is '' or 4 bytes of type + 4 bytes of size + size bytes
-      * content_sha is a 20 byte sha on all cache file contents"""
+      * version is the integer version number.
+      * entries dict is a dictionary which maps IndexEntry instances to a path at a stage.
+      * extension_data is '' or 4 bytes of type + 4 bytes of size + size bytes.
+      * content_sha is a 20 byte sha on all cache file contents.
+    """
     version, num_entries = read_header(stream)
     count = 0
     entries: Dict[Tuple[PathLike, int], "IndexEntry"] = {}
@@ -259,17 +264,17 @@ def read_cache(
         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_key would be the method to use, but we safe the effort
+        # entry_key would be the method to use, but we save the effort.
         entries[(path, entry.stage)] = entry
         count += 1
     # END for each entry
 
-    # the footer contains extension data and a sha on the content so far
-    # Keep the extension footer,and verify we have a sha in the end
+    # The footer contains extension data and a sha on the content so far.
+    # Keep the extension footer,and verify we have a sha in the end.
     # Extension data format is:
-    # 4 bytes ID
-    # 4 bytes length of chunk
-    # repeated 0 - N times
+    #   4 bytes ID
+    #   4 bytes length of chunk
+    #   Repeated 0 - N times
     extension_data = stream.read(~0)
     assert (
         len(extension_data) > 19
@@ -277,7 +282,7 @@ def read_cache(
 
     content_sha = extension_data[-20:]
 
-    # truncate the sha in the end as we will dynamically create it anyway
+    # Truncate the sha in the end as we will dynamically create it anyway.
     extension_data = extension_data[:-20]
 
     return (version, entries, extension_data, content_sha)
@@ -287,14 +292,15 @@ def write_tree_from_cache(
     entries: List[IndexEntry], odb: "GitCmdObjectDB", sl: slice, si: int = 0
 ) -> Tuple[bytes, List["TreeCacheTup"]]:
     """Create a tree from the given sorted list of entries and put the respective
-    trees into the given object database
+    trees into the given object database.
 
-    :param entries: **sorted** list of IndexEntries
-    :param odb: object database to store the trees in
-    :param si: start index at which we should start creating subtrees
-    :param sl: slice indicating the range we should process on the entries list
+    :param entries: **Sorted** list of IndexEntries
+    :param odb: Object database to store the trees in
+    :param si: Start index at which we should start creating subtrees
+    :param sl: Slice indicating the range we should process on the entries list
     :return: tuple(binsha, list(tree_entry, ...)) a tuple of a sha and a list of
-        tree entries being a tuple of hexsha, mode, name"""
+        tree entries being a tuple of hexsha, mode, name
+    """
     tree_items: List["TreeCacheTup"] = []
 
     ci = sl.start
@@ -307,10 +313,10 @@ def write_tree_from_cache(
         ci += 1
         rbound = entry.path.find("/", si)
         if rbound == -1:
-            # its not a tree
+            # It's not a tree.
             tree_items.append((entry.binsha, entry.mode, entry.path[si:]))
         else:
-            # find common base range
+            # Find common base range.
             base = entry.path[si:rbound]
             xi = ci
             while xi < end:
@@ -322,19 +328,19 @@ def write_tree_from_cache(
                 xi += 1
             # END find common base
 
-            # enter recursion
-            # ci - 1 as we want to count our current item as well
+            # Enter recursion.
+            # ci - 1 as we want to count our current item as well.
             sha, _tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)
             tree_items.append((sha, S_IFDIR, base))
 
-            # skip ahead
+            # Skip ahead.
             ci = xi
         # END handle bounds
     # END for each entry
 
-    # finally create the tree
+    # Finally create the tree.
     sio = BytesIO()
-    tree_to_stream(tree_items, sio.write)  # writes to stream as bytes, but doesn't change tree_items
+    tree_to_stream(tree_items, sio.write)  # Writes to stream as bytes, but doesn't change tree_items.
     sio.seek(0)
 
     istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio))
@@ -347,16 +353,18 @@ def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> Bas
 
 def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]:
     """
-    :return: list of BaseIndexEntries representing the aggressive merge of the given
+    :return: List of BaseIndexEntries representing the aggressive merge of the given
         trees. All valid entries are on stage 0, whereas the conflicting ones are left
         on stage 1, 2 or 3, whereas stage 1 corresponds to the common ancestor tree,
         2 to our tree and 3 to 'their' tree.
-    :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas
-        If 1 or two, the entries will effectively correspond to the last given tree
-        If 3 are given, a 3 way merge is performed"""
+
+    :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas.
+        If 1 or two, the entries will effectively correspond to the last given tree.
+        If 3 are given, a 3 way merge is performed.
+    """
     out: List[BaseIndexEntry] = []
 
-    # one and two way is the same for us, as we don't have to handle an existing
+    # One and two way is the same for us, as we don't have to handle an existing
     # index, instrea
     if len(tree_shas) in (1, 2):
         for entry in traverse_tree_recursive(odb, tree_shas[-1], ""):
@@ -368,72 +376,72 @@ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) ->
     if len(tree_shas) > 3:
         raise ValueError("Cannot handle %i trees at once" % len(tree_shas))
 
-    # three trees
+    # Three trees.
     for base, ours, theirs in traverse_trees_recursive(odb, tree_shas, ""):
         if base is not None:
-            # base version exists
+            # Base version exists.
             if ours is not None:
-                # ours exists
+                # Ours exists.
                 if theirs is not None:
-                    # it exists in all branches, if it was changed in both
-                    # its a conflict, otherwise we take the changed version
-                    # This should be the most common branch, so it comes first
+                    # It exists in all branches. Ff it was changed in both
+                    # its a conflict. Otherwise, we take the changed version.
+                    # This should be the most common branch, so it comes first.
                     if (base[0] != ours[0] and base[0] != theirs[0] and ours[0] != theirs[0]) or (
                         base[1] != ours[1] and base[1] != theirs[1] and ours[1] != theirs[1]
                     ):
-                        # changed by both
+                        # Changed by both.
                         out.append(_tree_entry_to_baseindexentry(base, 1))
                         out.append(_tree_entry_to_baseindexentry(ours, 2))
                         out.append(_tree_entry_to_baseindexentry(theirs, 3))
                     elif base[0] != ours[0] or base[1] != ours[1]:
-                        # only we changed it
+                        # Only we changed it.
                         out.append(_tree_entry_to_baseindexentry(ours, 0))
                     else:
-                        # either nobody changed it, or they did. In either
-                        # case, use theirs
+                        # Either nobody changed it, or they did. In either
+                        # case, use theirs.
                         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
+                        # They deleted it, we changed it, conflict.
                         out.append(_tree_entry_to_baseindexentry(base, 1))
                         out.append(_tree_entry_to_baseindexentry(ours, 2))
                     # else:
-                    #   we didn't change it, ignore
+                    #   # We didn't change it, ignore.
                     #   pass
                     # END handle our change
                 # END handle theirs
             else:
                 if theirs is None:
-                    # deleted in both, its fine - its out
+                    # Deleted in both, its fine - it's out.
                     pass
                 else:
                     if theirs[0] != base[0] or theirs[1] != base[1]:
-                        # deleted in ours, changed theirs, conflict
+                        # Deleted in ours, changed theirs, conflict.
                         out.append(_tree_entry_to_baseindexentry(base, 1))
                         out.append(_tree_entry_to_baseindexentry(theirs, 3))
                     # END theirs changed
                     # else:
-                    #   theirs didn't change
+                    #   # Theirs didn't change.
                     #   pass
                 # END handle theirs
             # END handle ours
         else:
-            # all three can't be None
+            # All three can't be None.
             if ours is None:
-                # added in their branch
+                # Added in their branch.
                 assert theirs is not None
                 out.append(_tree_entry_to_baseindexentry(theirs, 0))
             elif theirs is None:
-                # added in our branch
+                # Added in our branch.
                 out.append(_tree_entry_to_baseindexentry(ours, 0))
             else:
-                # both have it, except for the base, see whether it changed
+                # Both have it, except for the base, see whether it changed.
                 if ours[0] != theirs[0] or ours[1] != theirs[1]:
                     out.append(_tree_entry_to_baseindexentry(ours, 2))
                     out.append(_tree_entry_to_baseindexentry(theirs, 3))
                 else:
-                    # it was added the same in both
+                    # It was added the same in both.
                     out.append(_tree_entry_to_baseindexentry(ours, 0))
                 # END handle two items
             # END handle heads
diff --git a/git/index/typ.py b/git/index/typ.py
index b2c6c371b..046df6e83 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -1,4 +1,4 @@
-"""Module with additional types used by the index"""
+"""Module with additional types used by the index."""
 
 from binascii import b2a_hex
 from pathlib import Path
@@ -33,7 +33,6 @@
 
 
 class BlobFilter(object):
-
     """
     Predicate to be used by iter_blobs allowing to filter only return blobs which
     match the given list of directories or files.
@@ -46,7 +45,7 @@ class BlobFilter(object):
     def __init__(self, paths: Sequence[PathLike]) -> None:
         """
         :param paths:
-            tuple or list of paths which are either pointing to directories or
+            Tuple or list of paths which are either pointing to directories or
             to files relative to the current repository
         """
         self.paths = paths
@@ -84,8 +83,7 @@ class BaseIndexEntryHelper(NamedTuple):
 
 
 class BaseIndexEntry(BaseIndexEntryHelper):
-
-    """Small Brother of an index entry which can be created to describe changes
+    """Small brother of an index entry which can be created to describe changes
     done to the index in which case plenty of additional information is not required.
 
     As the first 4 data members match exactly to the IndexEntry type, methods
@@ -138,25 +136,26 @@ def to_blob(self, repo: "Repo") -> Blob:
 
 
 class IndexEntry(BaseIndexEntry):
-
     """Allows convenient access to IndexEntry data without completely unpacking it.
 
-    Attributes usully accessed often are cached in the tuple whereas others are
+    Attributes usually accessed often are cached in the tuple whereas others are
     unpacked on demand.
 
-    See the properties for a mapping between names and tuple indices."""
+    See the properties for a mapping between names and tuple indices.
+    """
 
     @property
     def ctime(self) -> Tuple[int, int]:
         """
         :return:
             Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
-            file's creation time"""
+            file's creation time
+        """
         return cast(Tuple[int, int], unpack(">LL", self.ctime_bytes))
 
     @property
     def mtime(self) -> Tuple[int, int]:
-        """See ctime property, but returns modification time"""
+        """See ctime property, but returns modification time."""
         return cast(Tuple[int, int], unpack(">LL", self.mtime_bytes))
 
     @classmethod
@@ -164,9 +163,10 @@ def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry":
         """
         :return:
             Minimal entry as created from the given BaseIndexEntry instance.
-            Missing values will be set to null-like values
+            Missing values will be set to null-like values.
 
-        :param base: Instance of type BaseIndexEntry"""
+        :param base: 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))
 
diff --git a/git/index/util.py b/git/index/util.py
index 6cf838f3b..f52b61b4a 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -1,4 +1,5 @@
-"""Module containing index utilities"""
+"""Module containing index utilities."""
+
 from functools import wraps
 import os
 import struct
@@ -33,7 +34,6 @@
 
 
 class TemporaryFileSwap(object):
-
     """Utility class moving a file to a temporary location within the same directory
     and moving it back on to where on object deletion."""
 
@@ -42,7 +42,7 @@ class TemporaryFileSwap(object):
     def __init__(self, file_path: PathLike) -> None:
         self.file_path = file_path
         self.tmp_file_path = str(self.file_path) + tempfile.mktemp("", "", "")
-        # it may be that the source does not exist
+        # It may be that the source does not exist.
         try:
             os.rename(self.file_path, self.tmp_file_path)
         except OSError:
@@ -90,9 +90,11 @@ def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any)
 
 
 def default_index(func: Callable[..., _T]) -> Callable[..., _T]:
-    """Decorator assuring the wrapped method may only run if we are the default
-    repository index. This is as we rely on git commands that operate
-    on that index only."""
+    """Decorator ensuring the wrapped method may only run if we are the default
+    repository index.
+
+    This is as we rely on git commands that operate on that index only.
+    """
 
     @wraps(func)
     def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
@@ -109,7 +111,7 @@ def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
 
 def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
     """Decorator which changes the current working dir to the one of the git
-    repository in order to assure relative paths are handled correctly"""
+    repository in order to ensure relative paths are handled correctly."""
 
     @wraps(func)
     def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 5910ac58a..2a4a114c7 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -1,7 +1,7 @@
-"""
-Import all submodules main classes into the package space
-"""
+"""Import all submodules' main classes into the package space."""
+
 # flake8: noqa
+
 import inspect
 
 from .base import *
@@ -14,11 +14,10 @@
 from .tree import *
 
 # Fix import dependency - add IndexObject to the util module, so that it can be
-# imported by the submodule.base
+# imported by the submodule.base.
 smutil.IndexObject = IndexObject  # type: ignore[attr-defined]
 smutil.Object = Object  # type: ignore[attr-defined]
 del smutil
 
-# must come after submodule was made available
-
+# Must come after submodule was made available.
 __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
diff --git a/git/objects/base.py b/git/objects/base.py
index 0dab5ccdb..0d88aa185 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -38,8 +38,7 @@
 
 
 class Object(LazyMixin):
-
-    """Implements an Object which may be Blobs, Trees, Commits and Tags"""
+    """An Object which may be Blobs, Trees, Commits and Tags."""
 
     NULL_HEX_SHA = "0" * 40
     NULL_BIN_SHA = b"\0" * 20
@@ -50,7 +49,9 @@ class Object(LazyMixin):
         dbtyp.str_commit_type,
         dbtyp.str_tag_type,
     )
+
     __slots__ = ("repo", "binsha", "size")
+
     type: Union[Lit_commit_ish, None] = None
 
     def __init__(self, repo: "Repo", binsha: bytes):
@@ -59,7 +60,8 @@ def __init__(self, repo: "Repo", binsha: bytes):
 
         :param repo: repository this object is located in
 
-        :param binsha: 20 byte SHA1"""
+        :param binsha: 20 byte SHA1
+        """
         super(Object, self).__init__()
         self.repo = repo
         self.binsha = binsha
@@ -71,14 +73,15 @@ def __init__(self, repo: "Repo", binsha: bytes):
     @classmethod
     def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
         """
-        :return: New Object instance of a type appropriate to the object type behind
-            id. The id of the newly created object will be a binsha even though
-            the input id may have been a Reference or Rev-Spec
+        :return: New :class:`Object`` instance of a type appropriate to the object type
+            behind `id`. The id of the newly created object will be a binsha even though
+            the input id may have been a Reference or Rev-Spec.
 
         :param id: reference, rev-spec, or hexsha
 
-        :note: This cannot be a __new__ method as it would always call __init__
-            with the input id which is not necessarily a binsha."""
+        :note: This cannot be a ``__new__`` method as it would always call
+            :meth:`__init__` with the input id which is not necessarily a binsha.
+        """
         return repo.rev_parse(str(id))
 
     @classmethod
@@ -86,9 +89,11 @@ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
         """
         :return: new object instance of a type appropriate to represent the given
             binary sha1
-        :param sha1: 20 byte binary sha1"""
+
+        :param sha1: 20 byte binary sha1
+        """
         if sha1 == cls.NULL_BIN_SHA:
-            # the NULL binsha is always the root commit
+            # The NULL binsha is always the root commit.
             return get_object_type_by_name(b"commit")(repo, sha1)
         # END handle special case
         oinfo = repo.odb.info(sha1)
@@ -97,7 +102,7 @@ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
         return inst
 
     def _set_cache_(self, attr: str) -> None:
-        """Retrieve object information"""
+        """Retrieve object information."""
         if attr == "size":
             oinfo = self.repo.odb.info(self.binsha)
             self.size = oinfo.size  # type:  int
@@ -137,28 +142,31 @@ def hexsha(self) -> str:
 
     @property
     def data_stream(self) -> "OStream":
-        """:return:  File Object compatible stream to the uncompressed raw data of the object
-        :note: returned streams must be read in order"""
+        """
+        :return: File Object compatible stream to the uncompressed raw data of the object
+
+        :note: Returned streams must be read in order.
+        """
         return self.repo.odb.stream(self.binsha)
 
     def stream_data(self, ostream: "OStream") -> "Object":
-        """Writes our data directly to the given output stream
+        """Write our data directly to the given output stream.
 
         :param ostream: File object compatible stream object.
-        :return: self"""
+        :return: self
+        """
         istream = self.repo.odb.stream(self.binsha)
         stream_copy(istream, ostream)
         return self
 
 
 class IndexObject(Object):
-
-    """Base for all objects that can be part of the index file , namely Tree, Blob and
-    SubModule objects"""
+    """Base for all objects that can be part of the index file, namely Tree, Blob and
+    SubModule objects."""
 
     __slots__ = ("path", "mode")
 
-    # for compatibility with iterable lists
+    # For compatibility with iterable lists.
     _id_attribute_ = "path"
 
     def __init__(
@@ -168,19 +176,20 @@ def __init__(
         mode: Union[None, int] = None,
         path: Union[None, PathLike] = None,
     ) -> None:
-        """Initialize a newly instanced IndexObject
+        """Initialize a newly instanced IndexObject.
 
-        :param repo: is the Repo we are located in
-        :param binsha: 20 byte sha1
+        :param repo: The :class:`Repo <git.repo.base.Repo>` we are located in.
+        :param binsha: 20 byte sha1.
         :param mode:
-            is the stat compatible file mode as int, use the stat module
-            to evaluate the information
+            The stat compatible file mode as int, use the :mod:`stat` module to evaluate
+            the information.
         :param path:
-            is the path to the file in the file system, relative to the git repository root, i.e.
-            file.ext or folder/other.ext
+            The path to the file in the file system, relative to the git repository
+            root, like ``file.ext`` or ``folder/other.ext``.
         :note:
-            Path may not be set of the index object has been created directly as it cannot
-            be retrieved without knowing the parent tree."""
+            Path may not be set if the index object has been created directly, as it
+            cannot be retrieved without knowing the parent tree.
+        """
         super(IndexObject, self).__init__(repo, binsha)
         if mode is not None:
             self.mode = mode
@@ -191,7 +200,8 @@ def __hash__(self) -> int:
         """
         :return:
             Hash of our path as index items are uniquely identifiable by path, not
-            by their data !"""
+            by their data!
+        """
         return hash(self.path)
 
     def _set_cache_(self, attr: str) -> None:
@@ -214,10 +224,11 @@ def name(self) -> str:
     def abspath(self) -> PathLike:
         R"""
         :return:
-            Absolute path to this index object in the file system ( as opposed to the
-            .path field which is a path relative to the git repository ).
+            Absolute path to this index object in the file system (as opposed to the
+            :attr:`path` field which is a path relative to the git repository).
 
-            The returned path will be native to the system and contains '\' on windows."""
+            The returned path will be native to the system and contains '\' on Windows.
+        """
         if self.repo.working_tree_dir is not None:
             return join_path_native(self.repo.working_tree_dir, self.path)
         else:
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 96ce486f5..f0d3181c2 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -3,6 +3,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 from mimetypes import guess_type
 from . import base
 
@@ -12,13 +13,12 @@
 
 
 class Blob(base.IndexObject):
-
-    """A Blob encapsulates a git blob object"""
+    """A Blob encapsulates a git blob object."""
 
     DEFAULT_MIME_TYPE = "text/plain"
     type: Literal["blob"] = "blob"
 
-    # valid blob modes
+    # Valid blob modes
     executable_mode = 0o100755
     file_mode = 0o100644
     link_mode = 0o120000
@@ -29,7 +29,9 @@ class Blob(base.IndexObject):
     def mime_type(self) -> str:
         """
         :return: String describing the mime type of this file (based on the filename)
-        :note: Defaults to 'text/plain' in case the actual file type is unknown."""
+
+        :note: Defaults to 'text/plain' in case the actual file type is unknown.
+        """
         guesses = None
         if self.path:
             guesses = guess_type(str(self.path))
diff --git a/git/objects/commit.py b/git/objects/commit.py
index fd65fa1e4..29123a44f 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -3,6 +3,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import datetime
 import re
 from subprocess import Popen, PIPE
@@ -66,7 +67,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     value on demand only if it involves calling the git binary."""
 
     # ENVIRONMENT VARIABLES
-    # read when creating new commits
+    # Read when creating new commits.
     env_author_date = "GIT_AUTHOR_DATE"
     env_committer_date = "GIT_COMMITTER_DATE"
 
@@ -113,36 +114,38 @@ def __init__(
         be implicitly set on first query.
 
         :param binsha: 20 byte sha1
-        :param parents: tuple( Commit, ... )
-            is a tuple of commit ids or actual Commits
+        :param parents: tuple(Commit, ...)
+            A tuple of commit ids or actual Commits
         :param tree: Tree object
         :param author: Actor
-            is the author Actor object
+            The author Actor object
         :param authored_date: int_seconds_since_epoch
-            is the authored DateTime - use time.gmtime() to convert it into a
+            The authored DateTime - use time.gmtime() to convert it into a
             different format
         :param author_tz_offset: int_seconds_west_of_utc
-            is the timezone that the authored_date is in
+            The timezone that the authored_date is in
         :param committer: Actor
-            is the committer string
+            The committer string
         :param committed_date: int_seconds_since_epoch
-            is the committed DateTime - use time.gmtime() to convert it into a
+            The committed DateTime - use time.gmtime() to convert it into a
             different format
         :param committer_tz_offset: int_seconds_west_of_utc
-            is the timezone that the committed_date is in
+            The timezone that the committed_date is in
         :param message: string
-            is the commit message
+            The commit message
         :param encoding: string
-            encoding of the message, defaults to UTF-8
+            Encoding of the message, defaults to UTF-8
         :param parents:
             List or tuple of Commit objects which are our parent(s) in the commit
             dependency graph
+
         :return: git.Commit
 
         :note:
             Timezone information is in the same format and in the same sign
             as what time.altzone returns. The sign is inverted compared to git's
-            UTC timezone."""
+            UTC timezone.
+        """
         super(Commit, self).__init__(repo, binsha)
         self.binsha = binsha
         if tree is not None:
@@ -211,7 +214,7 @@ def replace(self, **kwargs: Any) -> "Commit":
 
     def _set_cache_(self, attr: str) -> None:
         if attr in Commit.__slots__:
-            # read the data in a chunk, its faster - then provide a file wrapper
+            # Read the data in a chunk, its faster - then provide a file wrapper.
             _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha)
             self._deserialize(BytesIO(stream.read()))
         else:
@@ -235,17 +238,19 @@ def summary(self) -> Union[str, bytes]:
             return self.message.split(b"\n", 1)[0]
 
     def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> int:
-        """Count the number of commits reachable from this commit
+        """Count the number of commits reachable from this commit.
 
         :param paths:
-            is an optional path or a list of paths restricting the return value
-            to commits actually containing the paths
+            An optional path or a list of paths restricting the return value
+            to commits actually containing the paths.
 
         :param kwargs:
             Additional options to be passed to git-rev-list. They must not alter
-            the output style of the command, or parsing will yield incorrect results
-        :return: int defining the number of reachable commits"""
-        # yes, it makes a difference whether empty paths are given or not in our case
+            the output style of the command, or parsing will yield incorrect results.
+
+        :return: An int defining the number of reachable commits
+        """
+        # Yes, it makes a difference whether empty paths are given or not in our case
         # as the empty paths version will ignore merge commits for some reason.
         if paths:
             return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines())
@@ -256,7 +261,8 @@ def name_rev(self) -> str:
         """
         :return:
             String describing the commits hex sha based on the closest Reference.
-            Mostly useful for UI purposes"""
+            Mostly useful for UI purposes
+        """
         return self.repo.git.name_rev(self)
 
     @classmethod
@@ -269,23 +275,29 @@ def iter_items(
     ) -> Iterator["Commit"]:
         """Find all commits matching the given criteria.
 
-        :param repo: is the Repo
-        :param rev: revision specifier, see git-rev-parse for viable options
+        :param repo: The Repo
+
+        :param rev: Revision specifier, see git-rev-parse for viable options.
+
         :param paths:
-            is an optional path or list of paths, if set only Commits that include the path
-            or paths will be considered
+            An optional path or list of paths, if set only Commits that include the path
+            or paths will be considered.
+
         :param kwargs:
-            optional keyword arguments to git rev-list where
-            ``max_count`` is the maximum number of commits to fetch
-            ``skip`` is the number of commits to skip
-            ``since`` all commits since i.e. '1970-01-01'
-        :return: iterator yielding Commit items"""
+            Optional keyword arguments to ``git rev-list`` where:
+
+            * ``max_count`` is the maximum number of commits to fetch
+            * ``skip`` is the number of commits to skip
+            * ``since`` all commits since e.g. '1970-01-01'
+
+        :return: Iterator yielding :class:`Commit` items.
+        """
         if "pretty" in kwargs:
             raise ValueError("--pretty cannot be used as parsing expects single sha's only")
         # END handle pretty
 
-        # use -- in any case, to prevent possibility of ambiguous arguments
-        # see https://github.com/gitpython-developers/GitPython/issues/264
+        # Use -- in all cases, to prevent possibility of ambiguous arguments.
+        # See https://github.com/gitpython-developers/GitPython/issues/264.
 
         args_list: List[PathLike] = ["--"]
 
@@ -309,7 +321,8 @@ def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs
             Optional path or list of paths limiting the Commits to those that
             contain at least one of the paths
         :param kwargs: All arguments allowed by git-rev-list
-        :return: Iterator yielding Commit objects which are parents of self"""
+        :return: Iterator yielding Commit objects which are parents of self
+        """
         # skip ourselves
         skip = kwargs.get("skip", 1)
         if skip == 0:  # skip ourselves
@@ -323,7 +336,8 @@ def stats(self) -> Stats:
         """Create a git stat from changes between this commit and its first parent
         or from all changes done if this is the very first commit.
 
-        :return: git.Stats"""
+        :return: git.Stats
+        """
         if not self.parents:
             text = self.repo.git.diff_tree(self.hexsha, "--", numstat=True, no_renames=True, root=True)
             text2 = ""
@@ -339,17 +353,18 @@ def stats(self) -> Stats:
     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``.
+        :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.
+            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
+        """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).
@@ -399,7 +414,7 @@ def trailers_list(self) -> List[Tuple[str, str]]:
 
     @property
     def trailers_dict(self) -> Dict[str, List[str]]:
-        """Get the trailers of the message as a dictionary
+        """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).
@@ -440,12 +455,14 @@ def trailers_dict(self) -> Dict[str, List[str]]:
 
     @classmethod
     def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:
-        """Parse out commit information into a list of Commit objects
+        """Parse out commit information into a list of Commit objects.
+
         We expect one-line per commit, and parse the actual commit information directly
-        from our lighting fast object database
+        from our lighting fast object database.
 
         :param proc: git-rev-list process instance - one sha per line
-        :return: iterator returning Commit objects"""
+        :return: iterator supplying :class:`Commit` objects
+        """
 
         # def is_proc(inp) -> TypeGuard[Popen]:
         #     return hasattr(proc_or_stream, 'wait') and not hasattr(proc_or_stream, 'readline')
@@ -468,15 +485,16 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen,
                 break
             hexsha = line.strip()
             if len(hexsha) > 40:
-                # split additional information, as returned by bisect for instance
+                # Split additional information, as returned by bisect for instance.
                 hexsha, _ = line.split(None, 1)
             # END handle extra info
 
             assert len(hexsha) == 40, "Invalid line: %s" % hexsha
             yield cls(repo, hex_to_bin(hexsha))
         # END for each line in stream
+
         # TODO: Review this - it seems process handling got a bit out of control
-        # due to many developers trying to fix the open file handles issue
+        # due to many developers trying to fix the open file handles issue.
         if hasattr(proc_or_stream, "wait"):
             proc_or_stream = cast(Popen, proc_or_stream)
             finalize_process(proc_or_stream)
@@ -497,38 +515,38 @@ def create_from_tree(
         """Commit the given tree, creating a commit object.
 
         :param repo: Repo object the commit should be part of
-        :param tree: Tree object or hex or bin sha
-            the tree of the new commit
-        :param message: Commit message. It may be an empty string if no message is provided.
-            It will be converted to a string , in any case.
+        :param tree: Tree object or hex or bin sha. The tree of the new commit.
+        :param message: Commit message. It may be an empty string if no message is
+            provided. It will be converted to a string, in any case.
         :param parent_commits:
-            Optional Commit objects to use as parents for the new commit.
+            Optional :class:`Commit` objects to use as parents for the new commit.
             If empty list, the commit will have no parents at all and become
             a root commit.
-            If None , the current head commit will be the parent of the
-            new commit object
+            If None, the current head commit will be the parent of the
+            new commit object.
         :param head:
             If True, the HEAD will be advanced to the new commit automatically.
-            Else the HEAD will remain pointing on the previous commit. This could
+            Otherwise the HEAD will remain pointing on the previous commit. This could
             lead to undesired results when diffing files.
         :param author: The name of the author, optional. If unset, the repository
             configuration is used to obtain this value.
         :param committer: The name of the committer, optional. If unset, the
             repository configuration is used to obtain this value.
-        :param author_date: The timestamp for the author field
-        :param commit_date: The timestamp for the committer field
+        :param author_date: The timestamp for the author field.
+        :param commit_date: The timestamp for the committer field.
 
-        :return: Commit object representing the new commit
+        :return: Commit object representing the new commit.
 
         :note:
             Additional information about the committer and Author are taken from the
             environment or from the git configuration, see git-commit-tree for
-            more information"""
+            more information.
+        """
         if parent_commits is None:
             try:
                 parent_commits = [repo.head.commit]
             except ValueError:
-                # empty repositories have no head commit
+                # Empty repositories have no head commit.
                 parent_commits = []
             # END handle parent commits
         else:
@@ -538,11 +556,10 @@ def create_from_tree(
             # end check parent commit types
         # END if parent commits are unset
 
-        # retrieve all additional information, create a commit object, and
-        # serialize it
+        # Retrieve all additional information, create a commit object, and serialize it.
         # Generally:
-        # * Environment variables override configuration values
-        # * Sensible defaults are set according to the git documentation
+        # * Environment variables override configuration values.
+        # * Sensible defaults are set according to the git documentation.
 
         # COMMITTER AND AUTHOR INFO
         cr = repo.config_reader()
@@ -574,14 +591,14 @@ def create_from_tree(
             committer_time, committer_offset = unix_time, offset
         # END set committer time
 
-        # assume utf8 encoding
+        # Assume UTF-8 encoding.
         enc_section, enc_option = cls.conf_encoding.split(".")
         conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding)
         if not isinstance(conf_encoding, str):
             raise TypeError("conf_encoding could not be coerced to str")
 
-        # if the tree is no object, make sure we create one - otherwise
-        # the created commit object is invalid
+        # If the tree is no object, make sure we create one - otherwise
+        # the created commit object is invalid.
         if isinstance(tree, str):
             tree = repo.tree(tree)
         # END tree conversion
@@ -605,15 +622,15 @@ def create_from_tree(
         new_commit.binsha = cls._calculate_sha_(repo, new_commit)
 
         if head:
-            # need late import here, importing git at the very beginning throws
-            # as well ...
+            # Need late import here, importing git at the very beginning throws
+            # as well...
             import git.refs
 
             try:
                 repo.head.set_commit(new_commit, logmsg=message)
             except ValueError:
-                # head is not yet set to the ref our HEAD points to
-                # Happens on first commit
+                # head is not yet set to the ref our HEAD points to.
+                # Happens on first commit.
                 master = git.refs.Head.create(
                     repo,
                     repo.head.ref,
@@ -651,7 +668,7 @@ def _serialize(self, stream: BytesIO) -> "Commit":
             ).encode(self.encoding)
         )
 
-        # encode committer
+        # Encode committer.
         aname = c.name
         write(
             (
@@ -679,7 +696,7 @@ def _serialize(self, stream: BytesIO) -> "Commit":
 
         write(b"\n")
 
-        # write plain bytes, be sure its encoded according to our encoding
+        # Write plain bytes, be sure its encoded according to our encoding.
         if isinstance(self.message, str):
             write(self.message.encode(self.encoding))
         else:
@@ -703,11 +720,11 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
         # END for each parent line
         self.parents = tuple(self.parents)
 
-        # we don't know actual author encoding before we have parsed it, so keep the lines around
+        # We don't know actual author encoding before we have parsed it, so keep the lines around.
         author_line = next_line
         committer_line = readline()
 
-        # we might run into one or more mergetag blocks, skip those for now
+        # We might run into one or more mergetag blocks, skip those for now.
         next_line = readline()
         while next_line.startswith(b"mergetag "):
             next_line = readline()
@@ -715,12 +732,11 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                 next_line = readline()
         # end skip mergetags
 
-        # now we can have the encoding line, or an empty line followed by the optional
-        # message.
+        # Now we can have the encoding line, or an empty line followed by the optional message.
         self.encoding = self.default_encoding
         self.gpgsig = ""
 
-        # read headers
+        # Read headers.
         enc = next_line
         buf = enc.strip()
         while buf:
@@ -743,8 +759,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                 if is_next_header:
                     continue
             buf = readline().strip()
-        # decode the authors name
 
+        # Decode the author's name.
         try:
             (
                 self.author,
@@ -774,8 +790,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
             )
         # END handle author's encoding
 
-        # a stream from our data simply gives us the plain message
-        # The end of our message stream is marked with a newline that we strip
+        # A stream from our data simply gives us the plain message.
+        # The end of our message stream is marked with a newline that we strip.
         self.message = stream.read()
         try:
             self.message = self.message.decode(self.encoding, "replace")
@@ -796,6 +812,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
     def co_authors(self) -> List[Actor]:
         """
         Search the commit message for any co-authors of this commit.
+
         Details on co-authors: https://github.blog/2018-01-29-commit-together-with-co-authors/
 
         :return: List of co-authors for this commit (as Actor objects).
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 043eec721..7756154be 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -1,4 +1,5 @@
-"""Module with functions which are supposed to be as fast as possible"""
+"""Module with functions which are supposed to be as fast as possible."""
+
 from stat import S_ISDIR
 
 
@@ -36,12 +37,13 @@
 
 
 def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
-    """Write the give list of entries into a stream using its write method
+    """Write the given list of entries into a stream using its write method.
 
     :param entries: **sorted** list of tuples with (binsha, mode, name)
-    :param write: write method which takes a data string"""
+    :param write: write method which takes a data string
+    """
     ord_zero = ord("0")
-    bit_mask = 7  # 3 bits set
+    bit_mask = 7  # 3 bits set.
 
     for binsha, mode, name in entries:
         mode_str = b""
@@ -49,16 +51,16 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer
             mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str
         # END for each 8 octal value
 
-        # git slices away the first octal if its zero
+        # git slices away the first octal if it's zero.
         if mode_str[0] == ord_zero:
             mode_str = mode_str[1:]
         # END save a byte
 
-        # here it comes:  if the name is actually unicode, the replacement below
+        # Here it comes: If the name is actually unicode, the replacement below
         # will not work as the binsha is not part of the ascii unicode encoding -
-        # hence we must convert to an utf8 string for it to work properly.
+        # hence we must convert to an UTF-8 string for it to work properly.
         # According to my tests, this is exactly what git does, that is it just
-        # takes the input literally, which appears to be utf8 on linux.
+        # takes the input literally, which appears to be UTF-8 on linux.
         if isinstance(name, str):
             name_bytes = name.encode(defenc)
         else:
@@ -80,32 +82,32 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
     while i < len_data:
         mode = 0
 
-        # read mode
-        # Some git versions truncate the leading 0, some don't
-        # The type will be extracted from the mode later
+        # Read Mode
+        # Some git versions truncate the leading 0, some don't.
+        # The type will be extracted from the mode later.
         while data[i] != space_ord:
-            # move existing mode integer up one level being 3 bits
-            # and add the actual ordinal value of the character
+            # Move existing mode integer up one level being 3 bits
+            # and add the actual ordinal value of the character.
             mode = (mode << 3) + (data[i] - ord_zero)
             i += 1
         # END while reading mode
 
-        # byte is space now, skip it
+        # Byte is space now, skip it.
         i += 1
 
-        # parse name, it is NULL separated
+        # Parse name, it is NULL separated.
 
         ns = i
         while data[i] != 0:
             i += 1
         # END while not reached NULL
 
-        # default encoding for strings in git is utf8
-        # Only use the respective unicode object if the byte stream was encoded
+        # Default encoding for strings in git is UTF-8.
+        # Only use the respective unicode object if the byte stream was encoded.
         name_bytes = data[ns:i]
         name = safe_decode(name_bytes)
 
-        # byte is NULL, get next 20
+        # Byte is NULL, get next 20.
         i += 1
         sha = data[i : i + 20]
         i = i + 20
@@ -115,10 +117,11 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
 
 
 def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone:
-    """return data entry matching the given name and tree mode
-    or None.
-    Before the item is returned, the respective data item is set
-    None in the tree_data list to mark it done"""
+    """Return data entry matching the given name and tree mode or None.
+
+    Before the item is returned, the respective data item is set None in the
+    tree_data list to mark it done.
+    """
 
     try:
         item = tree_data[start_at]
@@ -148,7 +151,7 @@ def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup:
 
 
 def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone:
-    """Rebuild entry with given path prefix"""
+    """Rebuild entry with given path prefix."""
     if not item:
         return item
     return (item[0], item[1], path_prefix + item[2])
@@ -160,17 +163,23 @@ def traverse_trees_recursive(
     """
     :return: list of list with entries according to the given binary tree-shas.
         The result is encoded in a list
-        of n tuple|None per blob/commit, (n == len(tree_shas)), where
+        of n tuple|None per blob/commit, (n == len(tree_shas)), where:
+
         * [0] == 20 byte sha
         * [1] == mode as int
         * [2] == path relative to working tree root
+
         The entry tuple is None if the respective blob/commit did not
         exist in the given tree.
+
     :param tree_shas: iterable of shas pointing to trees. All trees must
-        be on the same level. A tree-sha may be None in which case None
+        be on the same level. A tree-sha may be None in which case None.
+
     :param path_prefix: a prefix to be added to the returned paths on this level,
-        set it '' for the first iteration
-    :note: The ordering of the returned items will be partially lost"""
+        set it '' for the first iteration.
+
+    :note: The ordering of the returned items will be partially lost.
+    """
     trees_data: List[List[EntryTupOrNone]] = []
 
     nt = len(tree_shas)
@@ -178,7 +187,7 @@ def traverse_trees_recursive(
         if tree_sha is None:
             data: List[EntryTupOrNone] = []
         else:
-            # make new list for typing as list invariant
+            # Make new list for typing as list invariant.
             data = list(tree_entries_from_data(odb.stream(tree_sha).read()))
         # END handle muted trees
         trees_data.append(data)
@@ -186,9 +195,9 @@ def traverse_trees_recursive(
 
     out: List[Tuple[EntryTupOrNone, ...]] = []
 
-    # find all matching entries and recursively process them together if the match
+    # Find all matching entries and recursively process them together if the match
     # is a tree. If the match is a non-tree item, put it into the result.
-    # Processed items will be set None
+    # Processed items will be set None.
     for ti, tree_data in enumerate(trees_data):
         for ii, item in enumerate(tree_data):
             if not item:
@@ -198,17 +207,17 @@ def traverse_trees_recursive(
             entries = [None for _ in range(nt)]
             entries[ti] = item
             _sha, mode, name = item
-            is_dir = S_ISDIR(mode)  # type mode bits
+            is_dir = S_ISDIR(mode)  # Type mode bits
 
-            # find this item in all other tree data items
-            # wrap around, but stop one before our current index, hence
-            # ti+nt, not ti+1+nt
+            # Find this item in all other tree data items.
+            # Wrap around, but stop one before our current index, hence
+            # ti+nt, not ti+1+nt.
             for tio in range(ti + 1, ti + nt):
                 tio = tio % nt
                 entries[tio] = _find_by_name(trees_data[tio], name, is_dir, ii)
 
             # END for each other item data
-            # if we are a directory, enter recursion
+            # If we are a directory, enter recursion.
             if is_dir:
                 out.extend(
                     traverse_trees_recursive(
@@ -221,11 +230,11 @@ def traverse_trees_recursive(
                 out.append(tuple(_to_full_path(e, path_prefix) for e in entries))
 
             # END handle recursion
-            # finally mark it done
+            # Finally mark it done.
             tree_data[ii] = None
         # END for each item
 
-        # we are done with one tree, set all its data empty
+        # We are done with one tree, set all its data empty.
         del tree_data[:]
     # END for each tree_data chunk
     return out
@@ -233,16 +242,20 @@ def traverse_trees_recursive(
 
 def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]:
     """
-    :return: list of entries of the tree pointed to by the binary tree_sha. An entry
-        has the following format:
+    :return: list of entries of the tree pointed to by the binary tree_sha.
+
+        An entry has the following format:
+
         * [0] 20 byte sha
         * [1] mode as int
         * [2] path relative to the repository
-    :param path_prefix: prefix to prepend to the front of all returned paths"""
+
+    :param path_prefix: Prefix to prepend to the front of all returned paths.
+    """
     entries = []
     data = tree_entries_from_data(odb.stream(tree_sha).read())
 
-    # unpacking/packing is faster than accessing individual items
+    # Unpacking/packing is faster than accessing individual items.
     for sha, mode, name in data:
         if S_ISDIR(mode):
             entries.extend(traverse_tree_recursive(odb, sha, path_prefix + name + "/"))
diff --git a/git/objects/submodule/__init__.py b/git/objects/submodule/__init__.py
index 82df59b0d..8edc13be4 100644
--- a/git/objects/submodule/__init__.py
+++ b/git/objects/submodule/__init__.py
@@ -1,2 +1,2 @@
-# NOTE: Cannot import anything here as the top-level _init_ has to handle
-# our dependencies
+# NOTE: Cannot import anything here as the top-level __init__ has to handle
+# our dependencies.
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 61c300652..24ea5569c 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -57,9 +57,8 @@
 
 
 class UpdateProgress(RemoteProgress):
-
     """Class providing detailed progress information to the caller who should
-    derive from it and implement the ``update(...)`` message"""
+    derive from it and implement the ``update(...)`` message."""
 
     CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
     _num_op_codes: int = RemoteProgress._num_op_codes + 3
@@ -78,25 +77,27 @@ class UpdateProgress(RemoteProgress):
 # mechanism which cause plenty of trouble of the only reason for packages and
 # modules is refactoring - subpackages shouldn't depend on parent packages
 class Submodule(IndexObject, TraversableIterableObj):
-
     """Implements access to a git submodule. They are special in that their sha
     represents a commit in the submodule's repository which is to be checked out
     at the path of this instance.
+
     The submodule type does not have a string type associated with it, as it exists
     solely as a marker in the tree and index.
 
-    All methods work in bare and non-bare repositories."""
+    All methods work in bare and non-bare repositories.
+    """
 
     _id_attribute_ = "name"
     k_modules_file = ".gitmodules"
     k_head_option = "branch"
     k_head_default = "master"
-    k_default_mode = stat.S_IFDIR | stat.S_IFLNK  # submodules are directories with link-status
+    k_default_mode = stat.S_IFDIR | stat.S_IFLNK  # Submodules are directories with link-status.
 
-    # this is a bogus type for base class compatibility
+    # This is a bogus type for base class compatibility.
     type: Literal["submodule"] = "submodule"  # type: ignore
 
     __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
+
     _cache_attrs = ("path", "_url", "_branch_path")
 
     def __init__(
@@ -110,14 +111,17 @@ def __init__(
         url: Union[str, None] = None,
         branch_path: Union[PathLike, None] = None,
     ) -> None:
-        """Initialize this instance with its attributes. We only document the ones
-        that differ from ``IndexObject``
+        """Initialize this instance with its attributes.
+
+        We only document the parameters that differ
+        from :class:`IndexObject <git.objects.base.IndexObject>`.
 
         :param repo: Our parent repository
         :param binsha: binary sha referring to a commit in the remote repository, see url parameter
         :param parent_commit: see set_parent_commit()
         :param url: The url to the remote repository which is the submodule
-        :param branch_path: full (relative) path to ref to checkout when cloning the remote repository"""
+        :param branch_path: full (relative) path to ref to checkout when cloning the remote repository
+        """
         super(Submodule, self).__init__(repo, binsha, mode, path)
         self.size = 0
         self._parent_commit = parent_commit
@@ -165,18 +169,18 @@ def _need_gitfile_submodules(cls, git: Git) -> bool:
         return git.version_info[:3] >= (1, 7, 5)
 
     def __eq__(self, other: Any) -> bool:
-        """Compare with another submodule"""
-        # we may only compare by name as this should be the ID they are hashed with
-        # Otherwise this type wouldn't be hashable
+        """Compare with another submodule."""
+        # We may only compare by name as this should be the ID they are hashed with.
+        # Otherwise this type wouldn't be hashable.
         # return self.path == other.path and self.url == other.url and super(Submodule, self).__eq__(other)
         return self._name == other._name
 
     def __ne__(self, other: object) -> bool:
-        """Compare with another submodule for inequality"""
+        """Compare with another submodule for inequality."""
         return not (self == other)
 
     def __hash__(self) -> int:
-        """Hash this instance using its logical id, not the sha"""
+        """Hash this instance using its logical id, not the sha."""
         return hash(self._name)
 
     def __str__(self) -> str:
@@ -195,16 +199,19 @@ def __repr__(self) -> str:
     def _config_parser(
         cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool
     ) -> SubmoduleConfigParser:
-        """:return: Config Parser constrained to our submodule in read or write mode
-        :raise IOError: If the .gitmodules file cannot be found, either locally or in the repository
-            at the given parent commit. Otherwise the exception would be delayed until the first
-            access of the config parser"""
+        """
+        :return: Config Parser constrained to our submodule in read or write mode
+
+        :raise IOError: If the .gitmodules file cannot be found, either locally or in
+            the repository at the given parent commit. Otherwise the exception would be
+            delayed until the first access of the config parser.
+        """
         parent_matches_head = True
         if parent_commit is not None:
             try:
                 parent_matches_head = repo.head.commit == parent_commit
             except ValueError:
-                # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref
+                # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref.
                 pass
         # end handle parent_commit
         fp_module: Union[str, BytesIO]
@@ -228,7 +235,7 @@ def _config_parser(
         return SubmoduleConfigParser(fp_module, read_only=read_only)
 
     def _clear_cache(self) -> None:
-        # clear the possibly changed values
+        """Clear the possibly changed values."""
         for name in self._cache_attrs:
             try:
                 delattr(self, name)
@@ -275,14 +282,16 @@ def _clone_repo(
         allow_unsafe_protocols: bool = False,
         **kwargs: Any,
     ) -> "Repo":
-        """:return: Repo instance of newly cloned repository
-        :param repo: our parent repository
-        :param url: url to clone from
-        :param path: repository - relative path to the submodule checkout location
-        :param name: canonical of the submodule
+        """
+        :return: Repo instance of newly cloned repository
+        :param repo: Our parent repository
+        :param url: URL to clone from
+        :param path: Repository - relative path to the submodule checkout location
+        :param name: Canonical name of the submodule
         :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
         :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :param kwargs: additional arguments given to git.clone"""
+        :param kwargs: Additional arguments given to git.clone
+        """
         module_abspath = cls._module_abspath(repo, path, name)
         module_checkout_path = module_abspath
         if cls._need_gitfile_submodules(repo.git):
@@ -331,14 +340,16 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
 
     @classmethod
     def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
-        """Writes a .git file containing a(preferably) relative path to the actual git module repository.
+        """Write a .git file containing a(preferably) relative path to the actual git module repository.
+
         It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir
-        :note: will overwrite existing files !
+
+        :note: This will overwrite existing files!
         :note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration
             and will not roll back changes done to the git file. This should be a non - issue, but may easily be fixed
-            if it becomes one
-        :param working_tree_dir: directory to write the .git file into
-        :param module_abspath: absolute path to the bare repository
+            if it becomes one.
+        :param working_tree_dir: Directory to write the .git file into
+        :param module_abspath: Absolute path to the bare repository
         """
         git_file = osp.join(working_tree_dir, ".git")
         rela_path = osp.relpath(module_abspath, start=working_tree_dir)
@@ -377,15 +388,15 @@ def add(
         If the submodule already exists, no matter if the configuration differs
         from the one provided, the existing submodule will be returned.
 
-        :param repo: Repository instance which should receive the submodule
-        :param name: The name/identifier for the submodule
-        :param path: repository-relative or absolute path at which the submodule
-            should be located
+        :param repo: Repository instance which should receive the submodule.
+        :param name: The name/identifier for the submodule.
+        :param path: Repository-relative or absolute path at which the submodule
+            should be located.
             It will be created as required during the repository initialization.
-        :param url: git-clone compatible URL, see git-clone reference for more information
+        :param url: git-clone compatible URL, see git-clone reference for more information.
             If None, the repository is assumed to exist, and the url of the first
             remote is taken instead. This is useful if you want to make an existing
-            repository a submodule of anotherone.
+            repository a submodule of another one.
         :param branch: name of branch at which the submodule should (later) be checked out.
             The given branch must exist in the remote repository, and will be checked
             out locally as a tracking branch.
@@ -393,24 +404,25 @@ def add(
             when the checked out branch will be the one the remote HEAD pointed to.
             The result you get in these situation is somewhat fuzzy, and it is recommended
             to specify at least 'master' here.
-            Examples are 'master' or 'feature/new'
-        :param no_checkout: if True, and if the repository has to be cloned manually,
-            no checkout will be performed
+            Examples are 'master' or 'feature/new'.
+        :param no_checkout: If True, and if the repository has to be cloned manually,
+            no checkout will be performed.
         :param depth: Create a shallow clone with a history truncated to the
             specified number of commits.
         :param env: Optional dictionary containing the desired environment variables.
-            Note: Provided variables will be used to update the execution
-            environment for `git`. If some variable is not specified in `env`
-            and is defined in `os.environ`, value from `os.environ` will be used.
-            If you want to unset some variable, consider providing empty string
-            as its value.
-        :param clone_multi_options: A list of Clone options. Please see ``git.repo.base.Repo.clone``
-            for details.
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
+            Note: Provided variables will be used to update the execution environment
+            for ``git``. If some variable is not specified in `env` and is defined in
+            attr:`os.environ`, the value from attr:`os.environ` will be used. If you
+            want to unset some variable, consider providing an empty string as its
+            value.
+        :param clone_multi_options: A list of Clone options. Please see
+            :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
+        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
         :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :return: The newly created submodule instance
-        :note: works atomically, such that no change will be done if the repository
-            update fails for instance"""
+        :return: The newly created submodule instance.
+        :note: Works atomically, such that no change will be done if the repository
+            update fails for instance.
+        """
 
         if repo.bare:
             raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
@@ -418,11 +430,11 @@ def add(
 
         path = cls._to_relative_path(repo, path)
 
-        # assure we never put backslashes into the url, as some operating systems
-        # like it ...
+        # Ensure we never put backslashes into the URL, as some operating systems
+        # like it...
         if url is not None:
             url = to_native_path_linux(url)
-        # END assure url correctness
+        # END ensure URL correctness
 
         # INSTANTIATE INTERMEDIATE SM
         sm = cls(
@@ -434,13 +446,13 @@ def add(
             url="invalid-temporary",
         )
         if sm.exists():
-            # reretrieve submodule from tree
+            # Reretrieve submodule from tree.
             try:
                 sm = repo.head.commit.tree[str(path)]
                 sm._name = name
                 return sm
             except KeyError:
-                # could only be in index
+                # Could only be in index.
                 index = repo.index
                 entry = index.entries[index.entry_key(path, 0)]
                 sm.binsha = entry.binsha
@@ -448,7 +460,7 @@ def add(
             # END handle exceptions
         # END handle existing
 
-        # fake-repo - we only need the functionality on the branch instance
+        # fake-repo - we only need the functionality on the branch instance.
         br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
         has_module = sm.module_exists()
         branch_is_default = branch is None
@@ -501,11 +513,11 @@ def add(
             )
         # END verify url
 
-        ## See #525 for ensuring git urls in config-files valid under Windows.
+        ## See #525 for ensuring git URLs in config-files are valid under Windows.
         url = Git.polish_url(url)
 
         # It's important to add the URL to the parent config, to let `git submodule` know.
-        # otherwise there is a '-' character in front of the submodule listing
+        # Otherwise there is a '-' character in front of the submodule listing:
         #  a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
         # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
         writer: Union[GitConfigParser, SectionConstraint]
@@ -513,7 +525,7 @@ def add(
         with sm.repo.config_writer() as writer:
             writer.set_value(sm_section(name), "url", url)
 
-        # update configuration and index
+        # Update configuration and index.
         index = sm.repo.index
         with sm.config_writer(index=index, write=False) as writer:
             writer.set_value("url", url)
@@ -525,7 +537,7 @@ def add(
                 writer.set_value(cls.k_head_option, br.path)
                 sm._branch_path = br.path
 
-        # we deliberately assume that our head matches our index !
+        # We deliberately assume that our head matches our index!
         if mrepo:
             sm.binsha = mrepo.head.commit.binsha
         index.add([sm], write=True)
@@ -549,40 +561,54 @@ def update(
         """Update the repository of this submodule to point to the checkout
         we point at with the binsha of this instance.
 
-        :param recursive: if True, we will operate recursively and update child-
-            modules as well.
-        :param init: if True, the module repository will be cloned into place if necessary
-        :param to_latest_revision: if True, the submodule's sha will be ignored during checkout.
-            Instead, the remote will be fetched, and the local tracking branch updated.
-            This only works if we have a local tracking branch, which is the case
-            if the remote repository had a master branch, or of the 'branch' option
-            was specified for this submodule and the branch existed remotely
-        :param progress: UpdateProgress instance or None if no progress should be shown
-        :param dry_run: if True, the operation will only be simulated, but not performed.
-            All performed operations are read - only
+        :param recursive:
+            If True, we will operate recursively and update child modules as well.
+        :param init:
+            If True, the module repository will be cloned into place if necessary.
+        :param to_latest_revision:
+            If True, the submodule's sha will be ignored during checkout. Instead, the
+            remote will be fetched, and the local tracking branch updated. This only
+            works if we have a local tracking branch, which is the case if the remote
+            repository had a master branch, or of the 'branch' option was specified for
+            this submodule and the branch existed remotely.
+        :param progress:
+            UpdateProgress instance or None if no progress should be shown.
+        :param dry_run:
+            If True, the operation will only be simulated, but not performed.
+            All performed operations are read-only.
         :param force:
-            If True, we may reset heads even if the repository in question is dirty. Additinoally we will be allowed
-            to set a tracking branch which is ahead of its remote branch back into the past or the location of the
-            remote branch. This will essentially 'forget' commits.
-            If False, local tracking branches that are in the future of their respective remote branches will simply
-            not be moved.
-        :param keep_going: if True, we will ignore but log all errors, and keep going recursively.
-            Unless dry_run is set as well, keep_going could cause subsequent / inherited errors you wouldn't see
-            otherwise.
-            In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules
+            If True, we may reset heads even if the repository in question is dirty.
+            Additionally we will be allowed to set a tracking branch which is ahead of
+            its remote branch back into the past or the location of the remote branch.
+            This will essentially 'forget' commits.
+            If False, local tracking branches that are in the future of their respective
+            remote branches will simply not be moved.
+        :param keep_going:
+            If True, we will ignore but log all errors, and keep going recursively.
+            Unless dry_run is set as well, keep_going could cause subsequent / inherited
+            errors you wouldn't see otherwise.
+            In conjunction with dry_run, it can be useful to anticipate all errors when
+            updating submodules.
         :param env: Optional dictionary containing the desired environment variables.
-            Note: Provided variables will be used to update the execution
-            environment for `git`. If some variable is not specified in `env`
-            and is defined in `os.environ`, value from `os.environ` will be used.
-            If you want to unset some variable, consider providing empty string
-            as its value.
-        :param clone_multi_options:  list of Clone options. Please see ``git.repo.base.Repo.clone``
-            for details. Only take effect with `init` option.
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :note: does nothing in bare repositories
-        :note: method is definitely not atomic if recurisve is True
-        :return: self"""
+            Note: Provided variables will be used to update the execution environment
+            for ``git``. If some variable is not specified in `env` and is defined in
+            attr:`os.environ`, value from attr:`os.environ` will be used.
+            If you want to unset some variable, consider providing the empty string as
+            its value.
+        :param clone_multi_options:
+            List of Clone options.
+            Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
+            They only take effect with the `init` option.
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ext.
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like --upload-pack.
+
+        :note: Does nothing in bare repositories.
+        :note: This method is definitely not atomic if `recursive` is True.
+
+        :return: self
+        """
         if self.repo.bare:
             return self
         # END pass in bare mode
@@ -595,14 +621,14 @@ def update(
             prefix = "DRY-RUN: "
         # END handle prefix
 
-        # to keep things plausible in dry-run mode
+        # To keep things plausible in dry-run mode.
         if dry_run:
             mrepo = None
         # END init mrepo
 
         try:
-            # ASSURE REPO IS PRESENT AND UPTODATE
-            #####################################
+            # ENSURE REPO IS PRESENT AND UP-TO-DATE
+            #######################################
             try:
                 mrepo = self.module()
                 rmts = mrepo.remotes
@@ -640,7 +666,7 @@ def update(
                     return self
                 # END early abort if init is not allowed
 
-                # there is no git-repository yet - but delete empty paths
+                # There is no git-repository yet - but delete empty paths.
                 checkout_module_abspath = self.abspath
                 if not dry_run and osp.isdir(checkout_module_abspath):
                     try:
@@ -652,8 +678,8 @@ def update(
                     # END handle OSError
                 # END handle directory removal
 
-                # don't check it out at first - nonetheless it will create a local
-                # branch according to the remote-HEAD if possible
+                # Don't check it out at first - nonetheless it will create a local
+                # branch according to the remote-HEAD if possible.
                 progress.update(
                     BEGIN | CLONE,
                     0,
@@ -682,19 +708,19 @@ def update(
                 )
 
                 if not dry_run:
-                    # see whether we have a valid branch to checkout
+                    # See whether we have a valid branch to check out.
                     try:
                         mrepo = cast("Repo", mrepo)
-                        # find  a remote which has our branch - we try to be flexible
+                        # Find a remote which has our branch - we try to be flexible.
                         remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
                         local_branch = mkhead(mrepo, self.branch_path)
 
-                        # have a valid branch, but no checkout - make sure we can figure
-                        # that out by marking the commit with a null_sha
+                        # Have a valid branch, but no checkout - make sure we can figure
+                        # that out by marking the commit with a null_sha.
                         local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
                         # END initial checkout + branch creation
 
-                        # make sure HEAD is not detached
+                        # Make sure HEAD is not detached.
                         mrepo.head.set_reference(
                             local_branch,
                             logmsg="submodule: attaching head to %s" % local_branch,
@@ -704,21 +730,21 @@ def update(
                         log.warning("Failed to checkout tracking branch %s", self.branch_path)
                     # END handle tracking branch
 
-                    # NOTE: Have to write the repo config file as well, otherwise
-                    # the default implementation will be offended and not update the repository
-                    # Maybe this is a good way to assure it doesn't get into our way, but
-                    # we want to stay backwards compatible too ... . Its so redundant !
+                    # NOTE: Have to write the repo config file as well, otherwise the
+                    # default implementation will be offended and not update the repository.
+                    # Maybe this is a good way to ensure it doesn't get into our way, but
+                    # we want to stay backwards compatible too... It's so redundant!
                     with self.repo.config_writer() as writer:
                         writer.set_value(sm_section(self.name), "url", self.url)
                 # END handle dry_run
             # END handle initialization
 
-            # DETERMINE SHAS TO CHECKOUT
-            ############################
+            # DETERMINE SHAS TO CHECK OUT
+            #############################
             binsha = self.binsha
             hexsha = self.hexsha
             if mrepo is not None:
-                # mrepo is only set if we are not in dry-run mode or if the module existed
+                # mrepo is only set if we are not in dry-run mode or if the module existed.
                 is_detached = mrepo.head.is_detached
             # END handle dry_run
 
@@ -742,13 +768,13 @@ def update(
                 # END handle detached head
             # END handle to_latest_revision option
 
-            # update the working tree
-            # handles dry_run
+            # Update the working tree.
+            # Handles dry_run.
             if mrepo is not None and mrepo.head.commit.binsha != binsha:
-                # We must assure that our destination sha (the one to point to) is in the future of our current head.
-                # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed
+                # We must ensure that our destination sha (the one to point to) is in the future of our current head.
+                # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed.
                 # We also handle the case that history has been rewritten, leaving no merge-base. In that case
-                # we behave conservatively, protecting possible changes the user had done
+                # we behave conservatively, protecting possible changes the user had done.
                 may_reset = True
                 if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
                     base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
@@ -785,7 +811,7 @@ def update(
 
                 if not dry_run and may_reset:
                     if is_detached:
-                        # NOTE: for now we force, the user is no supposed to change detached
+                        # NOTE: For now we force. The user is not supposed to change detached
                         # submodules anyway. Maybe at some point this becomes an option, to
                         # properly handle user modifications - see below for future options
                         # regarding rebase and merge.
@@ -793,7 +819,7 @@ def update(
                     else:
                         mrepo.head.reset(hexsha, index=True, working_tree=True)
                     # END handle checkout
-                # if we may reset/checkout
+                # If we may reset/checkout.
                 progress.update(
                     END | UPDWKTREE,
                     0,
@@ -810,7 +836,7 @@ def update(
         # HANDLE RECURSION
         ##################
         if recursive:
-            # in dry_run mode, the module might not exist
+            # In dry_run mode, the module might not exist.
             if mrepo is not None:
                 for submodule in self.iter_items(self.module()):
                     submodule.update(
@@ -834,19 +860,19 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
         the repository at our current path, changing the configuration, as well as
         adjusting our index entry accordingly.
 
-        :param module_path: the path to which to move our module in the parent repostory's working tree,
-            given as repository - relative or absolute path. Intermediate directories will be created
-            accordingly. If the path already exists, it must be empty.
-            Trailing(back)slashes are removed automatically
-        :param configuration: if True, the configuration will be adjusted to let
+        :param module_path: The path to which to move our module in the parent
+            repository's working tree, given as repository - relative or absolute path.
+            Intermediate directories will be created accordingly. If the path already
+            exists, it must be empty. Trailing (back)slashes are removed automatically.
+        :param configuration: If True, the configuration will be adjusted to let
             the submodule point to the given path.
-        :param module: if True, the repository managed by this submodule
-            will be moved as well. If False, we don't move the submodule's checkout, which may leave
-            the parent repository in an inconsistent state.
+        :param module: If True, the repository managed by this submodule
+            will be moved as well. If False, we don't move the submodule's checkout,
+            which may leave the parent repository in an inconsistent state.
         :return: self
-        :raise ValueError: if the module path existed and was not empty, or was a file
+        :raise ValueError: If the module path existed and was not empty, or was a file.
         :note: Currently the method is not atomic, and it could leave the repository
-            in an inconsistent state if a sub - step fails for some reason
+            in an inconsistent state if a sub-step fails for some reason.
         """
         if module + configuration < 1:
             raise ValueError("You must specify to move at least the module or the configuration of the submodule")
@@ -871,7 +897,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             raise ValueError("Index entry for target path did already exist")
         # END handle index key already there
 
-        # remove existing destination
+        # Remove existing destination.
         if module:
             if osp.exists(module_checkout_abspath):
                 if len(os.listdir(module_checkout_abspath)):
@@ -884,13 +910,13 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
                     os.rmdir(module_checkout_abspath)
                 # END handle link
             else:
-                # recreate parent directories
-                # NOTE: renames() does that now
+                # Recreate parent directories.
+                # NOTE: renames() does that now.
                 pass
             # END handle existence
         # END handle module
 
-        # move the module into place if possible
+        # Move the module into place if possible.
         cur_path = self.abspath
         renamed_module = False
         if module and osp.exists(cur_path):
@@ -903,8 +929,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             # end handle git file rewrite
         # END move physical module
 
-        # rename the index entry - have to manipulate the index directly as
-        # git-mv cannot be used on submodules ... yeah
+        # Rename the index entry - we have to manipulate the index directly as
+        # git-mv cannot be used on submodules... yeah.
         previous_sm_path = self.path
         try:
             if configuration:
@@ -918,8 +944,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
                     raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e
                 # END handle submodule doesn't exist
 
-                # update configuration
-                with self.config_writer(index=index) as writer:  # auto-write
+                # Update configuration.
+                with self.config_writer(index=index) as writer:  # Auto-write.
                     writer.set_value("path", module_checkout_path)
                     self.path = module_checkout_path
             # END handle configuration flag
@@ -930,7 +956,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             raise
         # END handle undo rename
 
-        # Auto-rename submodule if it's name was 'default', that is, the checkout directory
+        # Auto-rename submodule if it's name was 'default', that is, the checkout directory.
         if previous_sm_path == self.name:
             self.rename(module_checkout_path)
         # end
@@ -946,7 +972,7 @@ def remove(
         dry_run: bool = False,
     ) -> "Submodule":
         """Remove this submodule from the repository. This will remove our entry
-        from the .gitmodules file and the entry in the .git / config file.
+        from the .gitmodules file and the entry in the .git/config file.
 
         :param module: If True, the checked out module we point to will be deleted as
             well.If that module is currently on a commit outside any branch in the
@@ -959,22 +985,23 @@ def remove(
         :param force: Enforces the deletion of the module even though it contains
             modifications. This basically enforces a brute-force file system based
             deletion.
-        :param configuration: if True, the submodule is deleted from the configuration,
-            otherwise it isn't. Although this should be enabled most of the times,
+        :param configuration: If True, the submodule is deleted from the configuration,
+            otherwise it isn't. Although this should be enabled most of the time,
             this flag enables you to safely delete the repository of your submodule.
-        :param dry_run: if True, we will not actually do anything, but throw the errors
-            we would usually throw
+        :param dry_run: If True, we will not actually do anything, but throw the errors
+            we would usually throw.
         :return: self
-        :note: doesn't work in bare repositories
-        :note: doesn't work atomically, as failure to remove any part of the submodule will leave
-            an inconsistent state
-        :raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
-        :raise OSError: if directories or files could not be removed"""
+        :note: Doesn't work in bare repositories.
+        :note: Doesn't work atomically, as failure to remove any part of the submodule
+            will leave an inconsistent state.
+        :raise InvalidGitRepositoryError: Thrown if the repository cannot be deleted.
+        :raise OSError: If directories or files could not be removed.
+        """
         if not (module or configuration):
             raise ValueError("Need to specify to delete at least the module, or the configuration")
         # END handle parameters
 
-        # Recursively remove children of this submodule
+        # Recursively remove children of this submodule.
         nc = 0
         for csm in self.children():
             nc += 1
@@ -982,8 +1009,8 @@ def remove(
             del csm
         # end
         if configuration and not dry_run and nc > 0:
-            # Assure we don't leave the parent repository in a dirty state, and commit our changes
-            # It's important for recursive, unforced, deletions to work as expected
+            # Ensure we don't leave the parent repository in a dirty state, and commit our changes.
+            # It's important for recursive, unforced, deletions to work as expected.
             self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
         # end handle recursion
 
@@ -993,9 +1020,9 @@ def remove(
             mod = self.module()
             git_dir = mod.git_dir
             if force:
-                # take the fast lane and just delete everything in our module path
+                # Take the fast lane and just delete everything in our module path.
                 # TODO: If we run into permission problems, we have a highly inconsistent
-                # state. Delete the .git folders last, start with the submodules first
+                # state. Delete the .git folders last, start with the submodules first.
                 mp = self.abspath
                 method: Union[None, Callable[[PathLike], None]] = None
                 if osp.islink(mp):
@@ -1010,7 +1037,7 @@ def remove(
                     method(mp)
                 # END apply deletion method
             else:
-                # verify we may delete our module
+                # Verify we may delete our module.
                 if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
                     raise InvalidGitRepositoryError(
                         "Cannot delete module at %s with any modifications, unless force is specified"
@@ -1018,25 +1045,27 @@ def remove(
                     )
                 # END check for dirt
 
-                # figure out whether we have new commits compared to the remotes
-                # NOTE: If the user pulled all the time, the remote heads might
-                # not have been updated, so commits coming from the remote look
-                # as if they come from us. But we stay strictly read-only and
-                # don't fetch beforehand.
+                # Figure out whether we have new commits compared to the remotes.
+                # NOTE: If the user pulled all the time, the remote heads might not have
+                # been updated, so commits coming from the remote look as if they come
+                # from us. But we stay strictly read-only and don't fetch beforehand.
                 for remote in mod.remotes:
                     num_branches_with_new_commits = 0
                     rrefs = remote.refs
                     for rref in rrefs:
                         num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
                     # END for each remote ref
-                    # not a single remote branch contained all our commits
+                    # Not a single remote branch contained all our commits.
                     if len(rrefs) and num_branches_with_new_commits == len(rrefs):
                         raise InvalidGitRepositoryError(
                             "Cannot delete module at %s as there are new commits" % mod.working_tree_dir
                         )
                     # END handle new commits
-                    # have to manually delete references as python's scoping is
-                    # not existing, they could keep handles open ( on windows this is a problem )
+                    # We have to manually delete some references to allow resources to
+                    # be cleaned up immediately when we are done with them, because
+                    # Python's scoping is no more granular than the whole function (loop
+                    # bodies are not scopes). When the objects stay alive longer, they
+                    # can keep handles open. On Windows, this is a problem.
                     if len(rrefs):
                         del rref  # skipcq: PYL-W0631
                     # END handle remotes
@@ -1044,11 +1073,11 @@ def remove(
                     del remote
                 # END for each remote
 
-                # finally delete our own submodule
+                # Finally delete our own submodule.
                 if not dry_run:
                     self._clear_cache()
                     wtd = mod.working_tree_dir
-                    del mod  # release file-handles (windows)
+                    del mod  # Release file-handles (Windows).
                     import gc
 
                     gc.collect()
@@ -1062,14 +1091,14 @@ def remove(
             # end handle separate bare repository
         # END handle module deletion
 
-        # void our data not to delay invalid access
+        # Void our data so as not to delay invalid access.
         if not dry_run:
             self._clear_cache()
 
         # DELETE CONFIGURATION
         ######################
         if configuration and not dry_run:
-            # first the index-entry
+            # First the index-entry.
             parent_index = self.repo.index
             try:
                 del parent_index.entries[parent_index.entry_key(self.path, 0)]
@@ -1078,8 +1107,8 @@ def remove(
             # END delete entry
             parent_index.write()
 
-            # now git config - need the config intact, otherwise we can't query
-            # information anymore
+            # Now git config - we need the config intact, otherwise we can't query
+            # information anymore.
 
             with self.repo.config_writer() as gcp_writer:
                 gcp_writer.remove_section(sm_section(self.name))
@@ -1095,15 +1124,16 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
         contain the .gitmodules blob.
 
         :param commit:
-            Commit-ish reference pointing at the root_tree, or None to always point to the
-            most recent commit
+            Commit-ish reference pointing at the root_tree, or None to always point to
+            the most recent commit
         :param check:
-            if True, relatively expensive checks will be performed to verify
+            If True, relatively expensive checks will be performed to verify
             validity of the submodule.
-        :raise ValueError: if the commit's tree didn't contain the .gitmodules blob.
+        :raise ValueError: If the commit's tree didn't contain the .gitmodules blob.
         :raise ValueError:
-            if the parent commit didn't store this submodule under the current path
-        :return: self"""
+            If the parent commit didn't store this submodule under the current path.
+        :return: self
+        """
         if commit is None:
             self._parent_commit = None
             return self
@@ -1125,9 +1155,9 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
             # END handle submodule did not exist
         # END handle checking mode
 
-        # update our sha, it could have changed
-        # If check is False, we might see a parent-commit that doesn't even contain the submodule anymore.
-        # in that case, mark our sha as being NULL
+        # Update our sha, it could have changed.
+        # If check is False, we might see a parent-commit that doesn't even contain the
+        # submodule anymore. in that case, mark our sha as being NULL.
         try:
             self.binsha = pctree[str(self.path)].binsha
         except KeyError:
@@ -1141,19 +1171,23 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
     def config_writer(
         self, index: Union["IndexFile", None] = None, write: bool = True
     ) -> SectionConstraint["SubmoduleConfigParser"]:
-        """:return: a config writer instance allowing you to read and write the data
+        """
+        :return: A config writer instance allowing you to read and write the data
             belonging to this submodule into the .gitmodules file.
 
-        :param index: if not None, an IndexFile instance which should be written.
-            defaults to the index of the Submodule's parent repository.
-        :param write: if True, the index will be written each time a configuration
+        :param index: If not None, an IndexFile instance which should be written.
+            Defaults to the index of the Submodule's parent repository.
+        :param write: If True, the index will be written each time a configuration
             value changes.
-        :note: the parameters allow for a more efficient writing of the index,
+
+        :note: The parameters allow for a more efficient writing of the index,
             as you can pass in a modified index on your own, prevent automatic writing,
-            and write yourself once the whole operation is complete
-        :raise ValueError: if trying to get a writer on a parent_commit which does not
-            match the current head commit
-        :raise IOError: If the .gitmodules file/blob could not be read"""
+            and write yourself once the whole operation is complete.
+
+        :raise ValueError: If trying to get a writer on a parent_commit which does not
+            match the current head commit.
+        :raise IOError: If the .gitmodules file/blob could not be read
+        """
         writer = self._config_parser_constrained(read_only=False)
         if index is not None:
             writer.config._index = index
@@ -1162,17 +1196,19 @@ def config_writer(
 
     @unbare_repo
     def rename(self, new_name: str) -> "Submodule":
-        """Rename this submodule
-        :note: This method takes care of renaming the submodule in various places, such as
+        """Rename this submodule.
+
+        :note:
+            This method takes care of renaming the submodule in various places, such as:
 
             * $parent_git_dir / config
             * $working_tree_dir / .gitmodules
             * (git >= v1.8.0: move submodule repository to new name)
 
-        As .gitmodules will be changed, you would need to make a commit afterwards. The changed .gitmodules file
-        will already be added to the index
+        As .gitmodules will be changed, you would need to make a commit afterwards. The
+        changed .gitmodules file will already be added to the index.
 
-        :return: this submodule instance
+        :return: This submodule instance
         """
         if self.name == new_name:
             return self
@@ -1195,7 +1231,7 @@ def rename(self, new_name: str) -> "Submodule":
         if mod.has_separate_working_tree():
             destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
             source_dir = mod.git_dir
-            # Let's be sure the submodule name is not so obviously tied to a directory
+            # Let's be sure the submodule name is not so obviously tied to a directory.
             if str(destination_module_abspath).startswith(str(mod.git_dir)):
                 tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
                 os.renames(source_dir, tmp_dir)
@@ -1214,9 +1250,12 @@ def rename(self, new_name: str) -> "Submodule":
 
     @unbare_repo
     def module(self) -> "Repo":
-        """:return: Repo instance initialized from the repository at our submodule path
-        :raise InvalidGitRepositoryError: if a repository was not available. This could
-            also mean that it was not yet initialized"""
+        """
+        :return: Repo instance initialized from the repository at our submodule path
+
+        :raise InvalidGitRepositoryError: If a repository was not available. This could
+            also mean that it was not yet initialized.
+        """
         module_checkout_abspath = self.abspath
         try:
             repo = git.Repo(module_checkout_abspath)
@@ -1230,7 +1269,7 @@ def module(self) -> "Repo":
         # END handle exceptions
 
     def module_exists(self) -> bool:
-        """:return: True if our module exists and is a valid git repository. See module() method"""
+        """:return: True if our module exists and is a valid git repository. See module() method."""
         try:
             self.module()
             return True
@@ -1241,10 +1280,11 @@ def module_exists(self) -> bool:
     def exists(self) -> bool:
         """
         :return: True if the submodule exists, False otherwise. Please note that
-            a submodule may exist ( in the .gitmodules file) even though its module
-            doesn't exist on disk"""
-        # keep attributes for later, and restore them if we have no valid data
-        # this way we do not actually alter the state of the object
+            a submodule may exist (in the .gitmodules file) even though its module
+            doesn't exist on disk.
+        """
+        # Keep attributes for later, and restore them if we have no valid data.
+        # This way we do not actually alter the state of the object.
         loc = locals()
         for attr in self._cache_attrs:
             try:
@@ -1252,7 +1292,7 @@ def exists(self) -> bool:
                     loc[attr] = getattr(self, attr)
                 # END if we have the attribute cache
             except (cp.NoSectionError, ValueError):
-                # on PY3, this can happen apparently ... don't know why this doesn't happen on PY2
+                # On PY3, this can happen apparently... don't know why this doesn't happen on PY2.
                 pass
         # END for each attr
         self._clear_cache()
@@ -1274,22 +1314,26 @@ def exists(self) -> bool:
 
     @property
     def branch(self) -> "Head":
-        """:return: The branch instance that we are to checkout
-        :raise InvalidGitRepositoryError: if our module is not yet checked out"""
+        """
+        :return: The branch instance that we are to checkout
+
+        :raise InvalidGitRepositoryError: If our module is not yet checked out
+        """
         return mkhead(self.module(), self._branch_path)
 
     @property
     def branch_path(self) -> PathLike:
         """
-        :return: full(relative) path as string to the branch we would checkout
-            from the remote and track"""
+        :return: Full (relative) path as string to the branch we would checkout
+            from the remote and track
+        """
         return self._branch_path
 
     @property
     def branch_name(self) -> str:
-        """:return: the name of the branch, which is the shortest possible branch name"""
-        # use an instance method, for this we create a temporary Head instance
-        # which uses a repository that is available at least ( it makes no difference )
+        """:return: The name of the branch, which is the shortest possible branch name"""
+        # Use an instance method, for this we create a temporary Head instance
+        # which uses a repository that is available at least (it makes no difference).
         return git.Head(self.repo, self._branch_path).name
 
     @property
@@ -1299,37 +1343,46 @@ def url(self) -> str:
 
     @property
     def parent_commit(self) -> "Commit_ish":
-        """:return: Commit instance with the tree containing the .gitmodules file
-        :note: will always point to the current head's commit if it was not set explicitly"""
+        """
+        :return: Commit instance with the tree containing the .gitmodules file
+
+        :note: Will always point to the current head's commit if it was not set explicitly.
+        """
         if self._parent_commit is None:
             return self.repo.commit()
         return self._parent_commit
 
     @property
     def name(self) -> str:
-        """:return: The name of this submodule. It is used to identify it within the
+        """
+        :return: The name of this submodule. It is used to identify it within the
             .gitmodules file.
-        :note: by default, the name is the path at which to find the submodule, but
-            in git - python it should be a unique identifier similar to the identifiers
-            used for remotes, which allows to change the path of the submodule
-            easily
+
+        :note: By default, this is the name is the path at which to find the submodule,
+            but in GitPython it should be a unique identifier similar to the identifiers
+            used for remotes, which allows to change the path of the submodule easily.
         """
         return self._name
 
     def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
         """
-        :return: ConfigReader instance which allows you to qurey the configuration values
-            of this submodule, as provided by the .gitmodules file
-        :note: The config reader will actually read the data directly from the repository
-            and thus does not need nor care about your working tree.
-        :note: Should be cached by the caller and only kept as long as needed
-        :raise IOError: If the .gitmodules file/blob could not be read"""
+        :return: ConfigReader instance which allows you to query the configuration
+            values of this submodule, as provided by the .gitmodules file.
+
+        :note: The config reader will actually read the data directly from the
+            repository and thus does not need nor care about your working tree.
+
+        :note: Should be cached by the caller and only kept as long as needed.
+
+        :raise IOError: If the .gitmodules file/blob could not be read.
+        """
         return self._config_parser_constrained(read_only=True)
 
     def children(self) -> IterableList["Submodule"]:
         """
         :return: IterableList(Submodule, ...) an iterable list of submodules instances
-            which are children of this submodule or 0 if the submodule is not checked out"""
+            which are children of this submodule or 0 if the submodule is not checked out.
+        """
         return self._get_intermediate_items(self)
 
     # } END query interface
@@ -1344,9 +1397,9 @@ def iter_items(
         *Args: Any,
         **kwargs: Any,
     ) -> Iterator["Submodule"]:
-        """:return: iterator yielding Submodule instances available in the given repository"""
+        """:return: Iterator yielding Submodule instances available in the given repository"""
         try:
-            pc = repo.commit(parent_commit)  # parent commit instance
+            pc = repo.commit(parent_commit)  # Parent commit instance
             parser = cls._config_parser(repo, pc, read_only=True)
         except (IOError, BadName):
             return iter([])
@@ -1361,13 +1414,13 @@ def iter_items(
                 b = str(parser.get(sms, cls.k_head_option))
             # END handle optional information
 
-            # get the binsha
+            # Get the binsha.
             index = repo.index
             try:
-                rt = pc.tree  # root tree
+                rt = pc.tree  # Root tree
                 sm = rt[p]
             except KeyError:
-                # try the index, maybe it was just added
+                # Try the index, maybe it was just added.
                 try:
                     entry = index.entries[index.entry_key(p, 0)]
                     sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
@@ -1378,15 +1431,15 @@ def iter_items(
                 # END handle keyerror
             # END handle critical error
 
-            # Make sure we are looking at a submodule object
+            # Make sure we are looking at a submodule object.
             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
+            # Fill in remaining info - saves time as it doesn't have to be parsed again.
             sm._name = n
             if pc != repo.commit():
                 sm._parent_commit = pc
-            # end set only if not most recent !
+            # end set only if not most recent!
             sm._branch_path = git.Head.to_full_path(b)
             sm._url = u
 
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index d338441ef..b3c06ce9a 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -24,7 +24,7 @@
 
 
 class RootUpdateProgress(UpdateProgress):
-    """Utility class which adds more opcodes to the UpdateProgress"""
+    """Utility class which adds more opcodes to the UpdateProgress."""
 
     REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
         1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
@@ -67,7 +67,7 @@ def __init__(self, repo: "Repo"):
         )
 
     def _clear_cache(self) -> None:
-        """May not do anything"""
+        """May not do anything."""
         pass
 
     # { Interface
@@ -85,37 +85,48 @@ def update(
         keep_going: bool = False,
     ) -> "RootModule":
         """Update the submodules of this repository to the current HEAD commit.
+
         This method behaves smartly by determining changes of the path of a submodules
         repository, next to changes to the to-be-checked-out commit or the branch to be
         checked out. This works if the submodules ID does not change.
-        Additionally it will detect addition and removal of submodules, which will be handled
-        gracefully.
+        Additionally it will detect addition and removal of submodules, which will be
+        handled gracefully.
 
-        :param previous_commit: If set to a commit-ish, the commit we should use
-            as the previous commit the HEAD pointed to before it was set to the commit it points to now.
-            If None, it defaults to HEAD@{1} otherwise
+        :param previous_commit: If set to a commit-ish, the commit we should use as the
+            previous commit the HEAD pointed to before it was set to the commit it
+            points to now.
+            If None, it defaults to ``HEAD@{1}`` otherwise
         :param recursive: if True, the children of submodules will be updated as well
-            using the same technique
-        :param force_remove: If submodules have been deleted, they will be forcibly removed.
-            Otherwise the update may fail if a submodule's repository cannot be deleted as
-            changes have been made to it (see Submodule.update() for more information)
-        :param init: If we encounter a new module which would need to be initialized, then do it.
-        :param to_latest_revision: If True, instead of checking out the revision pointed to
-            by this submodule's sha, the checked out tracking branch will be merged with the
-            latest remote branch fetched from the repository's origin.
-            Unless force_reset is specified, a local tracking branch will never be reset into its past, therefore
-            the remote branch must be in the future for this to have an effect.
-        :param force_reset: if True, submodules may checkout or reset their branch even if the repository has
-            pending changes that would be overwritten, or if the local tracking branch is in the future of the
-            remote tracking branch and would be reset into its past.
-        :param progress: RootUpdateProgress instance or None if no progress should be sent
-        :param dry_run: if True, operations will not actually be performed. Progress messages
-            will change accordingly to indicate the WOULD DO state of the operation.
-        :param keep_going: if True, we will ignore but log all errors, and keep going recursively.
-            Unless dry_run is set as well, keep_going could cause subsequent/inherited errors you wouldn't see
-            otherwise.
-            In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules
-        :return: self"""
+            using the same technique.
+        :param force_remove: If submodules have been deleted, they will be forcibly
+            removed. Otherwise the update may fail if a submodule's repository cannot be
+            deleted as changes have been made to it.
+            (See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
+            for more information.)
+        :param init: If we encounter a new module which would need to be initialized,
+            then do it.
+        :param to_latest_revision: If True, instead of checking out the revision pointed
+            to by this submodule's sha, the checked out tracking branch will be merged
+            with the latest remote branch fetched from the repository's origin.
+            Unless `force_reset` is specified, a local tracking branch will never be
+            reset into its past, therefore the remote branch must be in the future for
+            this to have an effect.
+        :param force_reset: If True, submodules may checkout or reset their branch even
+            if the repository has pending changes that would be overwritten, or if the
+            local tracking branch is in the future of the remote tracking branch and
+            would be reset into its past.
+        :param progress: :class:`RootUpdateProgress` instance or None if no progress
+            should be sent.
+        :param dry_run: If True, operations will not actually be performed. Progress
+            messages will change accordingly to indicate the WOULD DO state of the
+            operation.
+        :param keep_going: If True, we will ignore but log all errors, and keep going
+            recursively. Unless `dry_run` is set as well, `keep_going` could cause
+            subsequent/inherited errors you wouldn't see otherwise.
+            In conjunction with `dry_run`, this can be useful to anticipate all errors
+            when updating submodules.
+        :return: self
+        """
         if self.repo.bare:
             raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
         # END handle bare
@@ -141,11 +152,11 @@ def update(
                         raise IndexError
                     # END handle initial commit
                 except IndexError:
-                    # in new repositories, there is no previous commit
+                    # In new repositories, there is no previous commit.
                     previous_commit = cur_commit
                 # END exception handling
             else:
-                previous_commit = repo.commit(previous_commit)  # obtain commit object
+                previous_commit = repo.commit(previous_commit)  # Obtain commit object.
             # END handle previous commit
 
             psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
@@ -164,8 +175,8 @@ def update(
                     op |= BEGIN
                 # END handle begin
 
-                # fake it into thinking its at the current commit to allow deletion
-                # of previous module. Trigger the cache to be updated before that
+                # Fake it into thinking its at the current commit to allow deletion
+                # of previous module. Trigger the cache to be updated before that.
                 progress.update(
                     op,
                     i,
@@ -188,7 +199,7 @@ def update(
 
             # HANDLE PATH RENAMES
             #####################
-            # url changes + branch changes
+            # URL changes + branch changes.
             csms = spsms & ssms
             len_csms = len(csms)
             for i, csm in enumerate(csms):
@@ -204,7 +215,7 @@ def update(
                         len_csms,
                         prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath),
                     )
-                    # move the module to the new path
+                    # Move the module to the new path.
                     if not dry_run:
                         psm.move(sm.path, module=True, configuration=False)
                     # END handle dry_run
@@ -220,14 +231,14 @@ def update(
                     # HANDLE URL CHANGE
                     ###################
                     if sm.url != psm.url:
-                        # Add the new remote, remove the old one
+                        # Add the new remote, remove the old one.
                         # This way, if the url just changes, the commits will not
-                        # have to be re-retrieved
+                        # have to be re-retrieved.
                         nn = "__new_origin__"
                         smm = sm.module()
                         rmts = smm.remotes
 
-                        # don't do anything if we already have the url we search in place
+                        # Don't do anything if we already have the url we search in place.
                         if len([r for r in rmts if r.url == sm.url]) == 0:
                             progress.update(
                                 BEGIN | URLCHANGE,
@@ -250,7 +261,7 @@ def update(
                                     )
                                 # END head is not detached
 
-                                # now delete the changed one
+                                # Now delete the changed one.
                                 rmt_for_deletion = None
                                 for remote in rmts:
                                     if remote.url == psm.url:
@@ -259,17 +270,17 @@ def update(
                                     # END if urls match
                                 # END for each remote
 
-                                # if we didn't find a matching remote, but have exactly one,
-                                # we can safely use this one
+                                # If we didn't find a matching remote, but have exactly one,
+                                # we can safely use this one.
                                 if rmt_for_deletion is None:
                                     if len(rmts) == 1:
                                         rmt_for_deletion = rmts[0]
                                     else:
-                                        # if we have not found any remote with the original url
+                                        # If we have not found any remote with the original URL
                                         # we may not have a name. This is a special case,
-                                        # and its okay to fail here
-                                        # Alternatively we could just generate a unique name and leave all
-                                        # existing ones in place
+                                        # and its okay to fail here.
+                                        # Alternatively we could just generate a unique name and
+                                        # leave all existing ones in place.
                                         raise InvalidGitRepositoryError(
                                             "Couldn't find original remote-repo at url %r" % psm.url
                                         )
@@ -280,15 +291,15 @@ def update(
                                 smm.delete_remote(rmt_for_deletion)
                                 # NOTE: Currently we leave tags from the deleted remotes
                                 # as well as separate tracking branches in the possibly totally
-                                # changed repository ( someone could have changed the url to
-                                # another project ). At some point, one might want to clean
+                                # changed repository (someone could have changed the url to
+                                # another project). At some point, one might want to clean
                                 # it up, but the danger is high to remove stuff the user
-                                # has added explicitly
+                                # has added explicitly.
 
-                                # rename the new remote back to what it was
+                                # Rename the new remote back to what it was.
                                 smr.rename(orig_name)
 
-                                # early on, we verified that the our current tracking branch
+                                # Early on, we verified that the our current tracking branch
                                 # exists in the remote. Now we have to assure that the
                                 # sha we point to is still contained in the new remote
                                 # tracking branch.
@@ -303,10 +314,10 @@ def update(
                                 # END for each commit
 
                                 if not found:
-                                    # adjust our internal binsha to use the one of the remote
-                                    # this way, it will be checked out in the next step
+                                    # Adjust our internal binsha to use the one of the remote
+                                    # this way, it will be checked out in the next step.
                                     # This will change the submodule relative to us, so
-                                    # the user will be able to commit the change easily
+                                    # the user will be able to commit the change easily.
                                     log.warning(
                                         "Current sha %s was not contained in the tracking\
              branch at the new remote, setting it the the remote's tracking branch",
@@ -315,7 +326,7 @@ def update(
                                     sm.binsha = rref.commit.binsha
                                 # END reset binsha
 
-                                # NOTE: All checkout is performed by the base implementation of update
+                                # NOTE: All checkout is performed by the base implementation of update.
                             # END handle dry_run
                             progress.update(
                                 END | URLCHANGE,
@@ -329,8 +340,7 @@ def update(
                     # HANDLE PATH CHANGES
                     #####################
                     if sm.branch_path != psm.branch_path:
-                        # finally, create a new tracking branch which tracks the
-                        # new remote branch
+                        # Finally, create a new tracking branch which tracks the new remote branch.
                         progress.update(
                             BEGIN | BRANCHCHANGE,
                             i,
@@ -342,7 +352,8 @@ def update(
                         if not dry_run:
                             smm = sm.module()
                             smmr = smm.remotes
-                            # As the branch might not exist yet, we will have to fetch all remotes to be sure ... .
+                            # As the branch might not exist yet, we will have to fetch all remotes
+                            # to be sure...
                             for remote in smmr:
                                 remote.fetch(progress=progress)
                             # end for each remote
@@ -354,15 +365,16 @@ def update(
                                     logmsg="branch: Created from HEAD",
                                 )
                             except OSError:
-                                # ... or reuse the existing one
+                                # ...or reuse the existing one.
                                 tbr = git.Head(smm, sm.branch_path)
                             # END assure tracking branch exists
 
                             tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
                             # NOTE: All head-resetting is done in the base implementation of update
-                            # but we will have to checkout the new branch here. As it still points to the currently
-                            # checkout out commit, we don't do any harm.
-                            # As we don't want to update working-tree or index, changing the ref is all there is to do
+                            # but we will have to checkout the new branch here. As it still points
+                            # to the currently checked out commit, we don't do any harm.
+                            # As we don't want to update working-tree or index, changing the ref is
+                            # all there is to do.
                             smm.head.reference = tbr
                         # END handle dry_run
 
@@ -384,7 +396,7 @@ def update(
         # FINALLY UPDATE ALL ACTUAL SUBMODULES
         ######################################
         for sm in sms:
-            # update the submodule using the default method
+            # Update the submodule using the default method.
             sm.update(
                 recursive=False,
                 init=init,
@@ -395,12 +407,12 @@ def update(
                 keep_going=keep_going,
             )
 
-            # update recursively depth first - question is which inconsistent
+            # Update recursively depth first - question is which inconsistent
             # state will be better in case it fails somewhere. Defective branch
             # or defective depth. The RootSubmodule type will never process itself,
-            # which was done in the previous expression
+            # which was done in the previous expression.
             if recursive:
-                # the module would exist by now if we are not in dry_run mode
+                # The module would exist by now if we are not in dry_run mode.
                 if sm.module_exists():
                     type(self)(sm.module()).update(
                         recursive=True,
@@ -419,7 +431,7 @@ def update(
         return self
 
     def module(self) -> "Repo":
-        """:return: the actual repository containing the submodules"""
+        """:return: The actual repository containing the submodules"""
         return self.repo
 
     # } END interface
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index 56ce1489a..e13528a8f 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -32,12 +32,12 @@
 
 
 def sm_section(name: str) -> str:
-    """:return: section title used in .gitmodules configuration file"""
+    """:return: Section title used in .gitmodules configuration file"""
     return f'submodule "{name}"'
 
 
 def sm_name(section: str) -> str:
-    """:return: name of the submodule as parsed from the section name"""
+    """:return: Name of the submodule as parsed from the section name"""
     section = section.strip()
     return section[11:-1]
 
@@ -48,7 +48,7 @@ def mkhead(repo: "Repo", path: PathLike) -> "Head":
 
 
 def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
-    """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError"""
+    """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError."""
     for remote in remotes:
         try:
             return remote.refs[branch_name]
@@ -66,14 +66,13 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "
 
 
 class SubmoduleConfigParser(GitConfigParser):
+    """Catches calls to _write, and updates the .gitmodules blob in the index
+    with the new data, if we have written into a stream.
 
-    """
-    Catches calls to _write, and updates the .gitmodules blob in the index
-    with the new data, if we have written into a stream. Otherwise it will
-    add the local file to the index to make it correspond with the working tree.
-    Additionally, the cache must be cleared
+    Otherwise it would add the local file to the index to make it correspond
+    with the working tree. Additionally, the cache must be cleared.
 
-    Please note that no mutating method will work in bare mode
+    Please note that no mutating method will work in bare mode.
     """
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -85,13 +84,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
     # { Interface
     def set_submodule(self, submodule: "Submodule") -> None:
         """Set this instance's submodule. It must be called before
-        the first write operation begins"""
+        the first write operation begins."""
         self._smref = weakref.ref(submodule)
 
     def flush_to_index(self) -> None:
-        """Flush changes in our configuration file to the index"""
+        """Flush changes in our configuration file to the index."""
         assert self._smref is not None
-        # should always have a file here
+        # Should always have a file here.
         assert not isinstance(self._file_or_files, BytesIO)
 
         sm = self._smref()
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 56fd05d1a..55f7e19da 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -3,7 +3,9 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
-""" Module containing all object based types. """
+
+"""Module containing all Object-based types."""
+
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
 from ..util import hex_to_bin
@@ -24,10 +26,10 @@
 
 
 class TagObject(base.Object):
-
-    """Non-Lightweight tag carrying additional information about an object we are pointing to."""
+    """Non-lightweight tag carrying additional information about an object we are pointing to."""
 
     type: Literal["tag"] = "tag"
+
     __slots__ = (
         "object",
         "tag",
@@ -48,18 +50,20 @@ def __init__(
         tagger_tz_offset: Union[int, None] = None,
         message: Union[str, None] = None,
     ) -> None:  # @ReservedAssignment
-        """Initialize a tag object with additional data
+        """Initialize a tag object with additional data.
 
-        :param repo: repository this object is located in
+        :param repo: Repository this object is located in
         :param binsha: 20 byte SHA1
         :param object: Object instance of object we are pointing to
-        :param tag: name of this tag
+        :param tag: Name of this tag
         :param tagger: Actor identifying the tagger
         :param tagged_date: int_seconds_since_epoch
-            is the DateTime of the tag creation - use time.gmtime to convert
-            it into a different format
-        :param tagged_tz_offset: int_seconds_west_of_utc is the timezone that the
-            authored_date is in, in a format similar to time.altzone"""
+            The :class:`DateTime` of the tag creation.
+            Use :func:`time.gmtime` to convert it into a different format.
+        :param tagged_tz_offset: int_seconds_west_of_utc
+            The timezone that the authored_date is in, in a format similar
+            to :attr:`time.altzone`.
+        """
         super(TagObject, self).__init__(repo, binsha)
         if object is not None:
             self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object
@@ -75,7 +79,7 @@ def __init__(
             self.message = message
 
     def _set_cache_(self, attr: str) -> None:
-        """Cache all our attributes at once"""
+        """Cache all our attributes at once."""
         if attr in TagObject.__slots__:
             ostream = self.repo.odb.stream(self.binsha)
             lines: List[str] = ostream.read().decode(defenc, "replace").splitlines()
@@ -95,9 +99,9 @@ def _set_cache_(self, attr: str) -> None:
                     self.tagger_tz_offset,
                 ) = parse_actor_and_date(tagger_info)
 
-            # line 4 empty - it could mark the beginning of the next header
-            # in case there really is no message, it would not exist. Otherwise
-            # a newline separates header from message
+            # Line 4 empty - it could mark the beginning of the next header.
+            # In case there really is no message, it would not exist.
+            # Otherwise a newline separates header from message.
             if len(lines) > 5:
                 self.message = "\n".join(lines[5:])
             else:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 4f490af54..fec98d6e8 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -103,11 +103,11 @@ def merge_sort(a: List[TreeCacheTup], cmp: Callable[[TreeCacheTup, TreeCacheTup]
 
 
 class TreeModifier(object):
-
     """A utility class providing methods to alter the underlying cache in a list-like fashion.
 
     Once all adjustments are complete, the _cache, which really is a reference to
-    the cache of a tree, will be sorted. Assuring it will be in a serializable state"""
+    the cache of a tree, will be sorted. This ensures it will be in a serializable state.
+    """
 
     __slots__ = "_cache"
 
@@ -126,10 +126,12 @@ def _index_by_name(self, name: str) -> int:
     # { Interface
     def set_done(self) -> "TreeModifier":
         """Call this method once you are done modifying the tree information.
-        It may be called several times, but be aware that each call will cause
-        a sort operation
 
-        :return self:"""
+        This may be called several times, but be aware that each call will cause
+        a sort operation.
+
+        :return self:
+        """
         merge_sort(self._cache, git_cmp)
         return self
 
@@ -137,16 +139,21 @@ def set_done(self) -> "TreeModifier":
 
     # { Mutators
     def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeModifier":
-        """Add the given item to the tree. If an item with the given name already
-        exists, nothing will be done, but a ValueError will be raised if the
-        sha and mode of the existing item do not match the one you add, unless
-        force is True
+        """Add the given item to the tree.
+
+        If an item with the given name already exists, nothing will be done, but a
+        ValueError will be raised if the sha and mode of the existing item do not match
+        the one you add, unless force is True
 
         :param sha: The 20 or 40 byte sha of the item to add
+
         :param mode: int representing the stat compatible mode of the item
-        :param force: If True, an item with your name and information will overwrite
-            any existing item with the same name, no matter which information it has
-        :return: self"""
+
+        :param force: If True, an item with your name and information will overwrite any
+            existing item with the same name, no matter which information it has
+
+        :return: self
+        """
         if "/" in name:
             raise ValueError("Name must not contain '/' characters")
         if (mode >> 12) not in Tree._map_id_to_type:
@@ -173,18 +180,20 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
         return self
 
     def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
-        """Add the given item to the tree, its correctness is assumed, which
+        """Add the given item to the tree. Its correctness is assumed, which
         puts the caller into responsibility to assure the input is correct.
-        For more information on the parameters, see ``add``
 
-        :param binsha: 20 byte binary sha"""
+        For more information on the parameters, see :meth:`add`.
+
+        :param binsha: 20 byte binary sha
+        """
         assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str)
         tree_cache = (binsha, mode, name)
 
         self._cache.append(tree_cache)
 
     def __delitem__(self, name: str) -> None:
-        """Deletes an item with the given name if it exists"""
+        """Delete an item with the given name if it exists."""
         index = self._index_by_name(name)
         if index > -1:
             del self._cache[index]
@@ -193,7 +202,6 @@ def __delitem__(self, name: str) -> None:
 
 
 class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
-
     """Tree objects represent an ordered list of Blobs and other Trees.
 
     ``Tree as a list``::
@@ -208,8 +216,8 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     type: Literal["tree"] = "tree"
     __slots__ = "_cache"
 
-    # actual integer ids for comparison
-    commit_id = 0o16  # equals stat.S_IFDIR | stat.S_IFLNK - a directory link
+    # Actual integer IDs for comparison.
+    commit_id = 0o16  # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link.
     blob_id = 0o10
     symlink_id = 0o12
     tree_id = 0o04
@@ -218,7 +226,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
         commit_id: Submodule,
         blob_id: Blob,
         symlink_id: Blob
-        # tree id added once Tree is defined
+        # Tree ID added once Tree is defined.
     }
 
     def __init__(
@@ -241,7 +249,7 @@ def _get_intermediate_items(
 
     def _set_cache_(self, attr: str) -> None:
         if attr == "_cache":
-            # Set the data when we need it
+            # Set the data when we need it.
             ostream = self.repo.odb.stream(self.binsha)
             self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read())
         else:
@@ -250,7 +258,8 @@ def _set_cache_(self, attr: str) -> None:
 
     def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
         """Iterable yields tuples of (binsha, mode, name), which will be converted
-        to the respective object representation"""
+        to the respective object representation.
+        """
         for binsha, mode, name in iterable:
             path = join_path(self.path, name)
             try:
@@ -260,10 +269,11 @@ def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[
         # END for each item
 
     def join(self, file: str) -> IndexObjUnion:
-        """Find the named object in this tree's contents
+        """Find the named object in this tree's contents.
 
         :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule``
-        :raise KeyError: if given file or tree does not exist in tree"""
+        :raise KeyError: if given file or tree does not exist in tree
+        """
         msg = "Blob or Tree named %r not found"
         if "/" in file:
             tree = self
@@ -274,7 +284,7 @@ def join(self, file: str) -> IndexObjUnion:
                 if item.type == "tree":
                     tree = item
                 else:
-                    # safety assertion - blobs are at the end of the path
+                    # Safety assertion - blobs are at the end of the path.
                     if i != len(tokens) - 1:
                         raise KeyError(msg % file)
                     return item
@@ -294,7 +304,10 @@ def join(self, file: str) -> IndexObjUnion:
         # END handle long paths
 
     def __truediv__(self, file: str) -> IndexObjUnion:
-        """For PY3 only"""
+        """The ``/`` operator is another syntax for joining.
+
+        See :meth:`join` for details.
+        """
         return self.join(file)
 
     @property
@@ -313,7 +326,8 @@ def cache(self) -> TreeModifier:
         :return: An object allowing to modify the internal cache. This can be used
             to change the tree's contents. When done, make sure you call ``set_done``
             on the tree modifier, or serialization behaviour will be incorrect.
-            See the ``TreeModifier`` for more information on how to alter the cache"""
+            See :class:`TreeModifier` for more information on how to alter the cache.
+        """
         return TreeModifier(self._cache)
 
     def traverse(
@@ -326,8 +340,10 @@ def traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]:
-        """For documentation, see util.Traversable._traverse()
-        Trees are set to visit_once = False to gain more performance in the traversal"""
+        """For documentation, see util.Traversable._traverse().
+
+        Trees are set to ``visit_once = False`` to gain more performance in the traversal.
+        """
 
         # """
         # # To typecheck instead of using cast.
@@ -392,7 +408,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
         # END handle item is index object
         # compatibility
 
-        # treat item as repo-relative path
+        # Treat item as repo-relative path.
         else:
             path = self.path
             for info in self._cache:
@@ -405,10 +421,12 @@ def __reversed__(self) -> Iterator[IndexObjUnion]:
         return reversed(self._iter_convert_to_object(self._cache))  # type: ignore
 
     def _serialize(self, stream: "BytesIO") -> "Tree":
-        """Serialize this tree into the stream. Please note that we will assume
-        our tree data to be in a sorted state. If this is not the case, serialization
-        will not generate a correct tree representation as these are assumed to be sorted
-        by algorithms"""
+        """Serialize this tree into the stream. Assumes sorted tree data.
+
+        .. note:: We will assume our tree data to be in a sorted state. If this is not
+            the case, serialization will not generate a correct tree representation as
+            these are assumed to be sorted by algorithms.
+        """
         tree_to_stream(self._cache, stream.write)
         return self
 
@@ -419,6 +437,5 @@ def _deserialize(self, stream: "BytesIO") -> "Tree":
 
 # END tree
 
-# finalize map definition
+# Finalize map definition.
 Tree._map_id_to_type[Tree.tree_id] = Tree
-#
diff --git a/git/objects/util.py b/git/objects/util.py
index 992a53d9c..d2c1c0158 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -3,7 +3,9 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
-"""Module for general utility functions"""
+
+"""Module for general utility functions."""
+
 # flake8: noqa F401
 
 
@@ -62,10 +64,10 @@ class TraverseNT(NamedTuple):
     src: Union["Traversable", None]
 
 
-T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj")  # for TraversableIterableObj.traverse()
+T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj")  # For TraversableIterableObj.traverse()
 
 TraversedTup = Union[
-    Tuple[Union["Traversable", None], "Traversable"],  # for commit, submodule
+    Tuple[Union["Traversable", None], "Traversable"],  # For commit, submodule
     "TraversedTreeTup",
 ]  # for tree.traverse()
 
@@ -92,12 +94,14 @@ class TraverseNT(NamedTuple):
 
 def mode_str_to_int(modestr: Union[bytes, str]) -> int:
     """
-    :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used
+    :param modestr:
+        String like 755 or 644 or 100644 - only the last 6 chars will be used.
+
     :return:
         String identifying a mode compatible to the mode methods ids of the
         stat module regarding the rwx permissions for user, group and other,
-        special flags and file system flags, i.e. whether it is a symlink
-        for example."""
+        special flags and file system flags, such as whether it is a symlink.
+    """
     mode = 0
     for iteration, char in enumerate(reversed(modestr[-6:])):
         char = cast(Union[str, int], char)
@@ -110,12 +114,13 @@ def get_object_type_by_name(
     object_type_name: bytes,
 ) -> Union[Type["Commit"], Type["TagObject"], Type["Tree"], Type["Blob"]]:
     """
-    :return: type suitable to handle the given object type name.
+    :return: A type suitable to handle the given object type name.
         Use the type to create new instances.
 
     :param object_type_name: Member of TYPES
 
-    :raise ValueError: In case object_type_name is unknown"""
+    :raise ValueError: If object_type_name is unknown
+    """
     if object_type_name == b"commit":
         from . import commit
 
@@ -138,9 +143,9 @@ def get_object_type_by_name(
 
 def utctz_to_altz(utctz: str) -> int:
     """Convert a git timezone offset into a timezone offset west of
-    UTC in seconds (compatible with time.altzone).
+    UTC in seconds (compatible with :attr:`time.altzone`).
 
-    :param utctz: git utc timezone string, i.e. +0200
+    :param utctz: git utc timezone string, e.g. +0200
     """
     int_utctz = int(utctz)
     seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60
@@ -148,9 +153,9 @@ def utctz_to_altz(utctz: str) -> int:
 
 
 def altz_to_utctz_str(altz: float) -> str:
-    """Convert a timezone offset west of UTC in seconds into a git timezone offset string
+    """Convert a timezone offset west of UTC in seconds into a Git timezone offset string.
 
-    :param altz: timezone offset in seconds west of UTC
+    :param altz: Timezone offset in seconds west of UTC
     """
     hours = abs(altz) // 3600
     minutes = (abs(altz) % 3600) // 60
@@ -159,8 +164,11 @@ def altz_to_utctz_str(altz: float) -> str:
 
 
 def verify_utctz(offset: str) -> str:
-    """:raise ValueError: if offset is incorrect
-    :return: offset"""
+    """
+    :raise ValueError: If offset is incorrect
+
+    :return: offset
+    """
     fmt_exc = ValueError("Invalid timezone offset format: %s" % offset)
     if len(offset) != 5:
         raise fmt_exc
@@ -194,7 +202,7 @@ def dst(self, dt: Union[datetime, None]) -> timedelta:
 
 
 def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
-    """Converts a timestamp + tz_offset into an aware datetime instance."""
+    """Convert a timestamp + tz_offset into an aware datetime instance."""
     utc_dt = datetime.fromtimestamp(timestamp, utc)
     try:
         local_dt = utc_dt.astimezone(tzoffset(tz_offset))
@@ -205,16 +213,18 @@ def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
 
 def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
     """
-    Parse the given date as one of the following
+    Parse the given date as one of the following:
 
-        * aware datetime instance
+        * Aware datetime instance
         * Git internal format: timestamp offset
         * RFC 2822: Thu, 07 Apr 2005 22:13:13 +0200.
         * ISO 8601 2005-04-07T22:13:13
-            The T can be a space as well
+            The T can be a space as well.
 
     :return: Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch
+
     :raise ValueError: If the format could not be understood
+
     :note: Date can also be YYYY.MM.DD, MM/DD/YYYY and DD.MM.YYYY.
     """
     if isinstance(string_date, datetime):
@@ -225,7 +235,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
         else:
             raise ValueError(f"string_date datetime object without tzinfo, {string_date}")
 
-    # git time
+    # Git time
     try:
         if string_date.count(" ") == 1 and string_date.rfind(":") == -1:
             timestamp, offset_str = string_date.split()
@@ -234,21 +244,21 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
             timestamp_int = int(timestamp)
             return timestamp_int, utctz_to_altz(verify_utctz(offset_str))
         else:
-            offset_str = "+0000"  # local time by default
+            offset_str = "+0000"  # Local time by default.
             if string_date[-5] in "-+":
                 offset_str = verify_utctz(string_date[-5:])
                 string_date = string_date[:-6]  # skip space as well
             # END split timezone info
             offset = utctz_to_altz(offset_str)
 
-            # now figure out the date and time portion - split time
+            # Now figure out the date and time portion - split time.
             date_formats = []
             splitter = -1
             if "," in string_date:
                 date_formats.append("%a, %d %b %Y")
                 splitter = string_date.rfind(" ")
             else:
-                # iso plus additional
+                # ISO plus additional
                 date_formats.append("%Y-%m-%d")
                 date_formats.append("%Y.%m.%d")
                 date_formats.append("%m/%d/%Y")
@@ -258,15 +268,15 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
                 if splitter == -1:
                     splitter = string_date.rfind(" ")
                 # END handle 'T' and ' '
-            # END handle rfc or iso
+            # END handle RFC or ISO
 
             assert splitter > -1
 
-            # split date and time
-            time_part = string_date[splitter + 1 :]  # skip space
+            # Split date and time.
+            time_part = string_date[splitter + 1 :]  # Skip space.
             date_part = string_date[:splitter]
 
-            # parse time
+            # Parse time.
             tstruct = time.strptime(time_part, "%H:%M:%S")
 
             for fmt in date_formats:
@@ -291,7 +301,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
                 # END exception handling
             # END for each fmt
 
-            # still here ? fail
+            # Still here ? fail.
             raise ValueError("no format matched")
         # END handle format
     except Exception as e:
@@ -299,7 +309,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
     # END handle exceptions
 
 
-# precompiled regex
+# Precompiled regexes
 _re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")
 _re_only_actor = re.compile(r"^.+? (.*)$")
 
@@ -309,7 +319,8 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
 
         author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
 
-    :return: [Actor, int_seconds_since_epoch, int_timezone_offset]"""
+    :return: [Actor, int_seconds_since_epoch, int_timezone_offset]
+    """
     actor, epoch, offset = "", "0", "0"
     m = _re_actor_epoch.search(line)
     if m:
@@ -327,12 +338,12 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
 
 
 class ProcessStreamAdapter(object):
-
-    """Class wireing all calls to the contained Process instance.
+    """Class wiring all calls to the contained Process instance.
 
     Use this type to hide the underlying process to provide access only to a specified
     stream. The process is usually wrapped into an AutoInterrupt class to kill
-    it if the instance goes out of scope."""
+    it if the instance goes out of scope.
+    """
 
     __slots__ = ("_proc", "_stream")
 
@@ -346,11 +357,12 @@ def __getattr__(self, attr: str) -> Any:
 
 @runtime_checkable
 class Traversable(Protocol):
-
     """Simple interface to perform depth-first or breadth-first traversals
-    into one direction.
+    in one direction.
+
     Subclasses only need to implement one function.
-    Instances of the Subclass must be hashable
+
+    Instances of the Subclass must be hashable.
 
     Defined subclasses = [Commit, Tree, SubModule]
     """
@@ -363,7 +375,7 @@ def _get_intermediate_items(cls, item: Any) -> Sequence["Traversable"]:
         """
         Returns:
             Tuple of items connected to the given item.
-            Must be implemented in subclass
+            Must be implemented in subclass.
 
         class Commit::     (cls, Commit) -> Tuple[Commit, ...]
         class Submodule::  (cls, Submodule) -> Iterablelist[Submodule]
@@ -393,22 +405,22 @@ def _list_traverse(
             Submodule ->  IterableList['Submodule']
             Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
         """
-        # Commit and Submodule have id.__attribute__ as IterableObj
-        # Tree has id.__attribute__ inherited from IndexObject
+        # Commit and Submodule have id.__attribute__ as IterableObj.
+        # Tree has id.__attribute__ inherited from IndexObject.
         if isinstance(self, Has_id_attribute):
             id = self._id_attribute_
         else:
-            id = ""  # shouldn't reach here, unless Traversable subclass created with no _id_attribute_
-            # could add _id_attribute_ to Traversable, or make all Traversable also Iterable?
+            id = ""  # Shouldn't reach here, unless Traversable subclass created with no _id_attribute_.
+            # Could add _id_attribute_ to Traversable, or make all Traversable also Iterable?
 
         if not as_edge:
             out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
             out.extend(self.traverse(as_edge=as_edge, *args, **kwargs))
             return out
-            # overloads in subclasses (mypy doesn't allow typing self: subclass)
+            # Overloads in subclasses (mypy doesn't allow typing self: subclass).
             # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]]
         else:
-            # Raise deprecationwarning, doesn't make sense to use this
+            # Raise DeprecationWarning, it doesn't make sense to use this.
             out_list: IterableList = IterableList(self.traverse(*args, **kwargs))
             return out_list
 
@@ -434,35 +446,37 @@ def _traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
-        """:return: iterator yielding of items found when traversing self
-        :param predicate: f(i,d) returns False if item i at depth d should not be included in the result
+        """:return: Iterator yielding items found when traversing self
+
+        :param predicate: f(i,d) returns False if item i at depth d should not be
+            included in the result.
 
         :param prune:
-            f(i,d) return True if the search should stop at item i at depth d.
-            Item i will not be returned.
+            f(i,d) return True if the search should stop at item i at depth d. Item i
+            will not be returned.
 
         :param depth:
-            define at which level the iteration should not go deeper
-            if -1, there is no limit
-            if 0, you would effectively only get self, the root of the iteration
+            Defines at which level the iteration should not go deeper if -1, there is no
+            limit if 0, you would effectively only get self, the root of the iteration
             i.e. if 1, you would only get the first level of predecessors/successors
 
         :param branch_first:
             if True, items will be returned branch first, otherwise depth first
 
         :param visit_once:
-            if True, items will only be returned once, although they might be encountered
-            several times. Loops are prevented that way.
+            if True, items will only be returned once, although they might be
+            encountered several times. Loops are prevented that way.
 
         :param ignore_self:
-            if True, self will be ignored and automatically pruned from
-            the result. Otherwise it will be the first item to be returned.
-            If as_edge is True, the source of the first edge is None
+            if True, self will be ignored and automatically pruned from the result.
+            Otherwise it will be the first item to be returned. If as_edge is True, the
+            source of the first edge is None
 
         :param as_edge:
             if True, return a pair of items, first being the source, second the
-            destination, i.e. tuple(src, dest) with the edge spanning from
-            source to destination"""
+            destination, i.e. tuple(src, dest) with the edge spanning from source to
+            destination
+        """
 
         """
             Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]]
@@ -473,11 +487,12 @@ def _traverse(
            ignore_self=True is_edge=True -> Iterator[item]
            ignore_self=True is_edge=False --> Iterator[item]
            ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]]
-           ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]"""
+           ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]
+        """
 
         visited = set()
         stack: Deque[TraverseNT] = deque()
-        stack.append(TraverseNT(0, self, None))  # self is always depth level 0
+        stack.append(TraverseNT(0, self, None))  # self is always depth level 0.
 
         def addToStack(
             stack: Deque[TraverseNT],
@@ -497,7 +512,7 @@ def addToStack(
         # END addToStack local method
 
         while stack:
-            d, item, src = stack.pop()  # depth of item, item, item_source
+            d, item, src = stack.pop()  # Depth of item, item, item_source
 
             if visit_once and item in visited:
                 continue
@@ -506,7 +521,7 @@ def addToStack(
                 visited.add(item)
 
             rval: Union[TraversedTup, "Traversable", "Blob"]
-            if as_edge:  # if as_edge return (src, item) unless rrc is None (e.g. for first item)
+            if as_edge:  # If as_edge return (src, item) unless rrc is None (e.g. for first item).
                 rval = (src, item)
             else:
                 rval = item
@@ -518,7 +533,7 @@ def addToStack(
             if not skipStartItem and predicate(rval, d):
                 yield rval
 
-            # only continue to next level if this is appropriate !
+            # Only continue to next level if this is appropriate!
             nd = d + 1
             if depth > -1 and nd > depth:
                 continue
@@ -529,24 +544,30 @@ def addToStack(
 
 @runtime_checkable
 class Serializable(Protocol):
-
-    """Defines methods to serialize and deserialize objects from and into a data stream"""
+    """Defines methods to serialize and deserialize objects from and into a data stream."""
 
     __slots__ = ()
 
     # @abstractmethod
     def _serialize(self, stream: "BytesIO") -> "Serializable":
-        """Serialize the data of this object into the given data stream
-        :note: a serialized object would ``_deserialize`` into the same object
+        """Serialize the data of this object into the given data stream.
+
+        :note: A serialized object would ``_deserialize`` into the same object.
+
         :param stream: a file-like object
-        :return: self"""
+
+        :return: self
+        """
         raise NotImplementedError("To be implemented in subclass")
 
     # @abstractmethod
     def _deserialize(self, stream: "BytesIO") -> "Serializable":
-        """Deserialize all information regarding this object from the stream
+        """Deserialize all information regarding this object from the stream.
+
         :param stream: a file-like object
-        :return: self"""
+
+        :return: self
+        """
         raise NotImplementedError("To be implemented in subclass")
 
 
diff --git a/git/refs/__init__.py b/git/refs/__init__.py
index 1486dffe6..18ea2013c 100644
--- a/git/refs/__init__.py
+++ b/git/refs/__init__.py
@@ -1,5 +1,5 @@
 # flake8: noqa
-# import all modules in order, fix the names they require
+# Import all modules in order, fix the names they require.
 from .symbolic import *
 from .reference import *
 from .head import *
diff --git a/git/refs/head.py b/git/refs/head.py
index 26efc6cb9..194f51e78 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -5,7 +5,7 @@
 from .symbolic import SymbolicReference
 from .reference import Reference
 
-# typinng ---------------------------------------------------
+# typing ---------------------------------------------------
 
 from typing import Any, Sequence, Union, TYPE_CHECKING
 
@@ -28,12 +28,12 @@ def strip_quotes(string: str) -> str:
 
 
 class HEAD(SymbolicReference):
-
-    """Special case of a Symbolic Reference as it represents the repository's
+    """Special case of a SymbolicReference representing the repository's
     HEAD reference."""
 
     _HEAD_NAME = "HEAD"
     _ORIG_HEAD_NAME = "ORIG_HEAD"
+
     __slots__ = ()
 
     def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME):
@@ -45,7 +45,8 @@ def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME):
     def orig_head(self) -> SymbolicReference:
         """
         :return: SymbolicReference pointing at the ORIG_HEAD, which is maintained
-            to contain the previous value of HEAD"""
+            to contain the previous value of HEAD.
+        """
         return SymbolicReference(self.repo, self._ORIG_HEAD_NAME)
 
     def reset(
@@ -71,7 +72,7 @@ def reset(
         :param working_tree:
             If True, the working tree will be forcefully adjusted to match the given
             commit, possibly overwriting uncommitted changes without warning.
-            If working_tree is True, index must be true as well
+            If `working_tree` is True, `index` must be True as well.
 
         :param paths:
             Single path or list of paths relative to the git root directory
@@ -80,14 +81,15 @@ def reset(
         :param kwargs:
             Additional arguments passed to git-reset.
 
-        :return: self"""
+        :return: self
+        """
         mode: Union[str, None]
         mode = "--soft"
         if index:
             mode = "--mixed"
 
-            # it appears, some git-versions declare mixed and paths deprecated
-            # see http://github.com/Byron/GitPython/issues#issue/2
+            # Tt appears some git versions declare mixed and paths deprecated.
+            # See http://github.com/Byron/GitPython/issues#issue/2.
             if paths:
                 mode = None
             # END special case
@@ -104,7 +106,7 @@ def reset(
             self.repo.git.reset(mode, commit, "--", paths, **kwargs)
         except GitCommandError as e:
             # git nowadays may use 1 as status to indicate there are still unstaged
-            # modifications after the reset
+            # modifications after the reset.
             if e.status != 1:
                 raise
         # END handle exception
@@ -113,7 +115,6 @@ def reset(
 
 
 class Head(Reference):
-
     """A Head is a named reference to a Commit. Every Head instance contains a name
     and a Commit object.
 
@@ -129,33 +130,35 @@ class Head(Reference):
         <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
 
         >>> head.commit.hexsha
-        '1c09f116cbc2cb4100fb6935bb162daa4723f455'"""
+        '1c09f116cbc2cb4100fb6935bb162daa4723f455'
+    """
 
     _common_path_default = "refs/heads"
     k_config_remote = "remote"
-    k_config_remote_ref = "merge"  # branch to merge from remote
+    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:
-        """Delete the given heads
+        """Delete the given heads.
 
         :param force:
             If True, the heads will be deleted even if they are not yet merged into
             the main development stream.
-            Default False"""
+            Default False
+        """
         flag = "-d"
         if force:
             flag = "-D"
         repo.git.branch(flag, *heads)
 
     def set_tracking_branch(self, remote_reference: Union["RemoteReference", None]) -> "Head":
-        """
-        Configure this branch to track the given remote reference. This will alter
-            this branch's configuration accordingly.
+        """Configure this branch to track the given remote reference. This will
+        alter this branch's configuration accordingly.
 
         :param remote_reference: The remote reference to track or None to untrack
-            any references
-        :return: self"""
+            any references.
+        :return: self
+        """
         from .remote import RemoteReference
 
         if remote_reference is not None and not isinstance(remote_reference, RemoteReference):
@@ -180,7 +183,7 @@ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None])
     def tracking_branch(self) -> Union["RemoteReference", None]:
         """
         :return: The remote_reference we are tracking, or None if we are
-            not a tracking branch"""
+            not a tracking branch."""
         from .remote import RemoteReference
 
         reader = self.config_reader()
@@ -193,22 +196,24 @@ def tracking_branch(self) -> Union["RemoteReference", None]:
             return RemoteReference(self.repo, remote_refpath)
         # END handle have tracking branch
 
-        # we are not a tracking branch
+        # We are not a tracking branch.
         return None
 
     def rename(self, new_path: PathLike, force: bool = False) -> "Head":
-        """Rename self to a new path
+        """Rename self to a new path.
 
         :param new_path:
             Either a simple name or a path, i.e. new_name or features/new_name.
-            The prefix refs/heads is implied
+            The prefix refs/heads is implied.
 
         :param force:
             If True, the rename will succeed even if a head with the target name
             already exists.
 
         :return: self
-        :note: respects the ref log as git commands are used"""
+
+        :note: Respects the ref log as git commands are used.
+        """
         flag = "-m"
         if force:
             flag = "-M"
@@ -218,19 +223,20 @@ def rename(self, new_path: PathLike, force: bool = False) -> "Head":
         return self
 
     def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
-        """Checkout this head by setting the HEAD to this reference, by updating the index
-        to reflect the tree we point to and by updating the working tree to reflect
-        the latest index.
+        """Check out this head by setting the HEAD to this reference, by updating the
+        index to reflect the tree we point to and by updating the working tree to
+        reflect the latest index.
 
         The command will fail if changed working tree files would be overwritten.
 
         :param force:
             If True, changes to the index and the working tree will be discarded.
-            If False, GitCommandError will be raised in that situation.
+            If False, :class:`GitCommandError <git.exc.GitCommandError>` will be
+            raised in that situation.
 
         :param kwargs:
-            Additional keyword arguments to be passed to git checkout, i.e.
-            b='new_branch' to create a new branch at the given spot.
+            Additional keyword arguments to be passed to git checkout, e.g.
+            ``b="new_branch"`` to create a new branch at the given spot.
 
         :return:
             The active branch after the checkout operation, usually self unless
@@ -241,7 +247,8 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
         :note:
             By default it is only allowed to checkout heads - everything else
             will leave the HEAD detached which is allowed and possible, but remains
-            a special state that some tools might not be able to handle."""
+            a special state that some tools might not be able to handle.
+        """
         kwargs["f"] = force
         if kwargs["f"] is False:
             kwargs.pop("f")
@@ -265,13 +272,15 @@ def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]:
     def config_reader(self) -> SectionConstraint[GitConfigParser]:
         """
         :return: A configuration parser instance constrained to only read
-            this instance's values"""
+            this instance's values.
+        """
         return self._config_parser(read_only=True)
 
     def config_writer(self) -> SectionConstraint[GitConfigParser]:
         """
         :return: A configuration writer instance with read-and write access
-            to options of this head"""
+            to options of this head.
+        """
         return self._config_parser(read_only=False)
 
     # } END configuration
diff --git a/git/refs/log.py b/git/refs/log.py
index 9b02051d3..21c757ccd 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -38,18 +38,17 @@
 
 
 class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
-
-    """Named tuple allowing easy access to the revlog data fields"""
+    """Named tuple allowing easy access to the revlog data fields."""
 
     _re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
     __slots__ = ()
 
     def __repr__(self) -> str:
-        """Representation of ourselves in git reflog format"""
+        """Representation of ourselves in git reflog format."""
         return self.format()
 
     def format(self) -> str:
-        """:return: a string suitable to be placed in a reflog file"""
+        """:return: A string suitable to be placed in a reflog file."""
         act = self.actor
         time = self.time
         return "{} {} {} <{}> {!s} {}\t{}\n".format(
@@ -64,30 +63,31 @@ def format(self) -> str:
 
     @property
     def oldhexsha(self) -> str:
-        """The hexsha to the commit the ref pointed to before the change"""
+        """The hexsha to the commit the ref pointed to before the change."""
         return self[0]
 
     @property
     def newhexsha(self) -> str:
-        """The hexsha to the commit the ref now points to, after the change"""
+        """The hexsha to the commit the ref now points to, after the change."""
         return self[1]
 
     @property
     def actor(self) -> Actor:
-        """Actor instance, providing access"""
+        """Actor instance, providing access."""
         return self[2]
 
     @property
     def time(self) -> Tuple[int, int]:
         """time as tuple:
 
-        * [0] = int(time)
-        * [1] = int(timezone_offset) in time.altzone format"""
+        * [0] = ``int(time)``
+        * [1] = ``int(timezone_offset)`` in :attr:`time.altzone` format
+        """
         return self[3]
 
     @property
     def message(self) -> str:
-        """Message describing the operation that acted on the reference"""
+        """Message describing the operation that acted on the reference."""
         return self[4]
 
     @classmethod
@@ -109,8 +109,11 @@ def new(
     @classmethod
     def from_line(cls, line: bytes) -> "RefLogEntry":
         """:return: New RefLogEntry instance from the given revlog line.
-        :param line: line bytes without trailing newline
-        :raise ValueError: If line could not be parsed"""
+
+        :param line: Line bytes without trailing newline
+
+        :raise ValueError: If `line` could not be parsed
+        """
         line_str = line.decode(defenc)
         fields = line_str.split("\t", 1)
         if len(fields) == 1:
@@ -141,13 +144,13 @@ def from_line(cls, line: bytes) -> "RefLogEntry":
 
 
 class RefLog(List[RefLogEntry], Serializable):
-
     """A reflog contains RefLogEntrys, each of which defines a certain state
     of the head in question. Custom query methods allow to retrieve log entries
     by date or by other criteria.
 
-    Reflog entries are ordered, the first added entry is first in the list, the last
-    entry, i.e. the last change of the head or reference, is last in the list."""
+    Reflog entries are ordered. The first added entry is first in the list. The last
+    entry, i.e. the last change of the head or reference, is last in the list.
+    """
 
     __slots__ = ("_path",)
 
@@ -158,7 +161,7 @@ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog":
     def __init__(self, filepath: Union[PathLike, None] = None):
         """Initialize this instance with an optional filepath, from which we will
         initialize our data. The path is also used to write changes back using
-        the write() method"""
+        the write() method."""
         self._path = filepath
         if filepath is not None:
             self._read_from_file()
@@ -168,7 +171,7 @@ def _read_from_file(self) -> None:
         try:
             fmap = file_contents_ro_filepath(self._path, stream=True, allow_mmap=True)
         except OSError:
-            # it is possible and allowed that the file doesn't exist !
+            # It is possible and allowed that the file doesn't exist!
             return
         # END handle invalid log
 
@@ -183,31 +186,35 @@ def _read_from_file(self) -> None:
     @classmethod
     def from_file(cls, filepath: PathLike) -> "RefLog":
         """
-        :return: a new RefLog instance containing all entries from the reflog
+        :return: A new RefLog instance containing all entries from the reflog
             at the given filepath
-        :param filepath: path to reflog
-        :raise ValueError: If the file could not be read or was corrupted in some way"""
+        :param filepath: Path to reflog
+        :raise ValueError: If the file could not be read or was corrupted in some way
+        """
         return cls(filepath)
 
     @classmethod
     def path(cls, ref: "SymbolicReference") -> str:
         """
-        :return: string to absolute path at which the reflog of the given ref
+        :return: String to absolute path at which the reflog of the given ref
             instance would be found. The path is not guaranteed to point to a valid
             file though.
-        :param ref: SymbolicReference instance"""
+        :param ref: SymbolicReference instance
+        """
         return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path))
 
     @classmethod
     def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEntry]:
         """
         :return: Iterator yielding RefLogEntry instances, one for each line read
-            sfrom the given stream.
-        :param stream: file-like object containing the revlog in its native format
-            or string instance pointing to a file to read"""
+            from the given stream.
+
+        :param stream: File-like object containing the revlog in its native format
+            or string instance pointing to a file to read.
+        """
         new_entry = RefLogEntry.from_line
         if isinstance(stream, str):
-            # default args return mmap on py>3
+            # Default args return mmap since Python 3.
             _stream = file_contents_ro_filepath(stream)
             assert isinstance(_stream, mmap)
         else:
@@ -223,23 +230,23 @@ def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEnt
     @classmethod
     def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry":
         """
-        :return: RefLogEntry at the given index
+        :return: RefLogEntry at the given index.
 
-        :param filepath: full path to the index file from which to read the entry
+        :param filepath: Full path to the index file from which to read the entry.
 
-        :param index: python list compatible index, i.e. it may be negative to
-            specify an entry counted from the end of the list
+        :param index: Python list compatible index, i.e. it may be negative to
+            specify an entry counted from the end of the list.
 
-        :raise IndexError: If the entry didn't exist
+        :raise IndexError: If the entry didn't exist.
 
         .. note:: This method is faster as it only parses the entry at index, skipping
             all other lines. Nonetheless, the whole file has to be read if
-            the index is negative
+            the index is negative.
         """
         with open(filepath, "rb") as fp:
             if index < 0:
                 return RefLogEntry.from_line(fp.readlines()[index].strip())
-            # read until index is reached
+            # Read until index is reached.
 
             for i in range(index + 1):
                 line = fp.readline()
@@ -254,7 +261,8 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry":
     def to_file(self, filepath: PathLike) -> None:
         """Write the contents of the reflog instance to a file at the given filepath.
 
-        :param filepath: path to file, parent directories are assumed to exist"""
+        :param filepath: Path to file, parent directories are assumed to exist.
+        """
         lfd = LockedFD(filepath)
         assure_directory_exists(filepath, is_file=True)
 
@@ -279,19 +287,21 @@ def append_entry(
     ) -> "RefLogEntry":
         """Append a new log entry to the revlog at filepath.
 
-        :param config_reader: configuration reader of the repository - used to obtain
-            user information. May also be an Actor instance identifying the committer directly or None.
-        :param filepath: full path to the log file
-        :param oldbinsha: binary sha of the previous commit
-        :param newbinsha: binary sha of the current commit
-        :param message: message describing the change to the reference
-        :param write: If True, the changes will be written right away. Otherwise
-            the change will not be written
+        :param config_reader: Configuration reader of the repository - used to obtain
+            user information. May also be an Actor instance identifying the committer
+            directly or None.
+        :param filepath: Full path to the log file.
+        :param oldbinsha: Binary sha of the previous commit.
+        :param newbinsha: Binary sha of the current commit.
+        :param message: Message describing the change to the reference.
+        :param write: If True, the changes will be written right away. Otherwise the
+            change will not be written.
 
-        :return: RefLogEntry objects which was appended to the log
+        :return: RefLogEntry objects which was appended to the log.
 
-        :note: As we are append-only, concurrent access is not a problem as we
-            do not interfere with readers."""
+        :note: As we are append-only, concurrent access is not a problem as we do not
+            interfere with readers.
+        """
 
         if len(oldbinsha) != 20 or len(newbinsha) != 20:
             raise ValueError("Shas need to be given in binary format")
@@ -325,9 +335,10 @@ def append_entry(
         return entry
 
     def write(self) -> "RefLog":
-        """Write this instance's data to the file we are originating from
+        """Write this instance's data to the file we are originating from.
 
-        :return: self"""
+        :return: self
+        """
         if self._path is None:
             raise ValueError("Instance was not initialized with a path, use to_file(...) instead")
         # END assert path
@@ -337,10 +348,11 @@ def write(self) -> "RefLog":
     # } END interface
 
     # { Serializable Interface
+
     def _serialize(self, stream: "BytesIO") -> "RefLog":
         write = stream.write
 
-        # write all entries
+        # Write all entries.
         for e in self:
             write(e.format().encode(defenc))
         # END for each entry
@@ -348,5 +360,6 @@ def _serialize(self, stream: "BytesIO") -> "RefLog":
 
     def _deserialize(self, stream: "BytesIO") -> "RefLog":
         self.extend(self.iter_entries(stream))
-        # } END serializable interface
         return self
+
+    # } END serializable interface
diff --git a/git/refs/reference.py b/git/refs/reference.py
index dea1af68c..ca265c0e4 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -22,7 +22,7 @@
 
 
 def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
-    """A decorator raising a TypeError if we are not a valid remote, based on the path"""
+    """A decorator raising a TypeError if we are not a valid remote, based on the path."""
 
     def wrapper(self: T_References, *args: Any) -> _T:
         if not self.is_remote():
@@ -38,27 +38,30 @@ def wrapper(self: T_References, *args: Any) -> _T:
 
 
 class Reference(SymbolicReference, LazyMixin, IterableObj):
+    """A named reference to any object.
 
-    """Represents a named reference to any object. Subclasses may apply restrictions though,
-    i.e. Heads can only point to commits."""
+    Subclasses may apply restrictions though, e.g., a :class:`Head <git.refs.head.Head>`
+    can only point to commits.
+    """
 
     __slots__ = ()
+
     _points_to_commits_only = False
     _resolve_ref_on_create = True
     _common_path_default = "refs"
 
     def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> None:
-        """Initialize this instance
-
-        :param repo: Our parent repository
-        :param path:
-            Path relative to the .git/ directory pointing to the ref in question, i.e.
-            refs/heads/master
-        :param check_path: if False, you can provide any path. Otherwise the path must start with the
-            default path prefix of this type."""
+        """Initialize this instance.
+
+        :param repo: Our parent repository.
+        :param path: Path relative to the .git/ directory pointing to the ref in
+            question, e.g. ``refs/heads/master``.
+        :param check_path: If False, you can provide any path. Otherwise the path must
+            start with the default path prefix of this type.
+        """
         if check_path and not str(path).startswith(self._common_path_default + "/"):
             raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
-        self.path: str  # SymbolicReference converts to string atm
+        self.path: str  # SymbolicReference converts to string at the moment.
         super(Reference, self).__init__(repo, path)
 
     def __str__(self) -> str:
@@ -72,9 +75,10 @@ def set_object(
         object: Union[Commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "Reference":
-        """Special version which checks if the head-log needs an update as well
+        """Special version which checks if the head-log needs an update as well.
 
-        :return: self"""
+        :return: self
+        """
         oldbinsha = None
         if logmsg is not None:
             head = self.repo.head
@@ -104,13 +108,13 @@ def set_object(
 
         return self
 
-    # NOTE: Don't have to overwrite properties as the will only work without a the log
+    # NOTE: No need to overwrite properties, as the will only work without a the log.
 
     @property
     def name(self) -> str:
         """:return: (shortest) Name of this reference - it may contain path components"""
-        # first two path tokens are can be removed as they are
-        # refs/heads or refs/tags or refs/remotes
+        # The first two path tokens can be removed as they are
+        # refs/heads or refs/tags or refs/remotes.
         tokens = self.path.split("/")
         if len(tokens) < 3:
             return self.path  # could be refs/HEAD
@@ -132,23 +136,27 @@ def iter_items(
 
     # { Remote Interface
 
-    @property  # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21)
+    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
     @require_remote_ref_path
     def remote_name(self) -> str:
         """
         :return:
             Name of the remote we are a reference of, such as 'origin' for a reference
-            named 'origin/master'"""
+            named 'origin/master'.
+        """
         tokens = self.path.split("/")
         # /refs/remotes/<remote name>/<branch_name>
         return tokens[2]
 
-    @property  # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21)
+    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
     @require_remote_ref_path
     def remote_head(self) -> str:
-        """:return: Name of the remote head itself, i.e. master.
+        """
+        :return: Name of the remote head itself, e.g. master.
+
         :note: The returned name is usually not qualified enough to uniquely identify
-            a branch"""
+            a branch.
+        """
         tokens = self.path.split("/")
         return "/".join(tokens[3:])
 
diff --git a/git/refs/remote.py b/git/refs/remote.py
index ec10c5a1b..e4b1f4392 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -21,8 +21,7 @@
 
 
 class RemoteReference(Head):
-
-    """Represents a reference pointing to a remote head."""
+    """A reference pointing to a remote head."""
 
     _common_path_default = Head._remote_common_path_default
 
@@ -35,7 +34,7 @@ def iter_items(
         *args: Any,
         **kwargs: Any,
     ) -> Iterator["RemoteReference"]:
-        """Iterate remote references, and if given, constrain them to the given remote"""
+        """Iterate remote references, and if given, constrain them to the given remote."""
         common_path = common_path or cls._common_path_default
         if remote is not None:
             common_path = join_path(common_path, str(remote))
@@ -49,15 +48,16 @@ def iter_items(
     # "type: ignore".  (See https://github.com/python/typing/issues/241)
     @classmethod
     def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore
-        """Delete the given remote references
+        """Delete the given remote references.
 
         :note:
             kwargs are given for comparability with the base class method as we
-            should not narrow the signature."""
+            should not narrow the signature.
+        """
         repo.git.branch("-d", "-r", *refs)
-        # the official deletion method will ignore remote symbolic refs - these
+        # The official deletion method will ignore remote symbolic refs - these
         # are generally ignored in the refs/ folder. We don't though
-        # and delete remainders manually
+        # and delete remainders manually.
         for ref in refs:
             try:
                 os.remove(os.path.join(repo.common_dir, ref.path))
@@ -71,5 +71,5 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:
 
     @classmethod
     def create(cls, *args: Any, **kwargs: Any) -> NoReturn:
-        """Used to disable this method"""
+        """Raises TypeError. Defined so the create method is disabled."""
         raise TypeError("Cannot explicitly create remote references")
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 5ff84367d..7da957e2f 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -48,7 +48,7 @@
 
 
 def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
-    """Find the git dir that's appropriate for the path"""
+    """Find the git dir that is appropriate for the path."""
     name = f"{path}"
     if name in ["HEAD", "ORIG_HEAD", "FETCH_HEAD", "index", "logs"]:
         return repo.git_dir
@@ -56,14 +56,16 @@ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
 
 
 class SymbolicReference(object):
+    """Special case of a reference that is symbolic.
 
-    """Represents a special case of a reference such that this reference is symbolic.
-    It does not point to a specific commit, but to another Head, which itself
-    specifies a commit.
+    This does not point to a specific commit, but to another
+    :class:`Head <git.refs.head.Head>`, which itself specifies a commit.
 
-    A typical example for a symbolic reference is HEAD."""
+    A typical example for a symbolic reference is ``HEAD``.
+    """
 
     __slots__ = ("repo", "path")
+
     _resolve_ref_on_create = False
     _points_to_commits_only = True
     _common_path_default = ""
@@ -97,7 +99,8 @@ def name(self) -> str:
         """
         :return:
             In case of symbolic references, the shortest assumable name
-            is the path itself."""
+            is the path itself.
+        """
         return str(self.path)
 
     @property
@@ -110,8 +113,11 @@ def _get_packed_refs_path(cls, repo: "Repo") -> str:
 
     @classmethod
     def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
-        """Returns an iterator yielding pairs of sha1/path pairs (as strings) for the corresponding refs.
-        :note: The packed refs file will be kept open as long as we iterate"""
+        """Return an iterator yielding pairs of sha1/path pairs (as strings)
+        for the corresponding refs.
+
+        :note: The packed refs file will be kept open as long as we iterate.
+        """
         try:
             with open(cls._get_packed_refs_path(repo), "rt", encoding="UTF-8") as fp:
                 for line in fp:
@@ -133,8 +139,8 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
                         continue
                     # END parse comment
 
-                    # skip dereferenced tag object entries - previous line was actual
-                    # tag reference for it
+                    # Skip dereferenced tag object entries - previous line was actual
+                    # tag reference for it.
                     if line[0] == "^":
                         continue
 
@@ -153,7 +159,9 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) ->
         """
         :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all
             intermediate references as required
-        :param repo: the repository containing the reference at ref_path"""
+
+        :param repo: The repository containing the reference at ref_path
+        """
 
         while True:
             hexsha, ref_path = cls._get_ref_info(repo, ref_path)
@@ -163,7 +171,11 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) ->
 
     @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
+        """Check a ref name for validity.
+
+        This is 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):
@@ -210,9 +222,12 @@ def _check_ref_name_valid(ref_path: PathLike) -> None:
     def _get_ref_info_helper(
         cls, repo: "Repo", ref_path: Union[PathLike, None]
     ) -> Union[Tuple[str, None], Tuple[None, str]]:
-        """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"""
+        """
+        :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 ref_path:
             cls._check_ref_name_valid(ref_path)
 
@@ -221,18 +236,18 @@ def _get_ref_info_helper(
         try:
             with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp:
                 value = fp.read().rstrip()
-            # Don't only split on spaces, but on whitespace, which allows to parse lines like
+            # Don't only split on spaces, but on whitespace, which allows to parse lines like:
             # 60b64ef992065e2600bfef6187a97f92398a9144                branch 'master' of git-server:/path/to/repo
             tokens = value.split()
             assert len(tokens) != 0
         except OSError:
-            # Probably we are just packed, find our entry in the packed refs file
+            # Probably we are just packed. Find our entry in the packed refs file.
             # NOTE: We are not a symbolic ref if we are in a packed file, as these
-            # are excluded explicitly
+            # are excluded explicitly.
             for sha, path in cls._iter_packed_refs(repo):
                 if path != ref_path:
                     continue
-                # sha will be used
+                # sha will be used.
                 tokens = sha, path
                 break
             # END for each packed ref
@@ -240,11 +255,11 @@ def _get_ref_info_helper(
         if tokens is None:
             raise ValueError("Reference at %r does not exist" % ref_path)
 
-        # is it a reference ?
+        # Is it a reference?
         if tokens[0] == "ref:":
             return (None, tokens[1])
 
-        # its a commit
+        # It's a commit.
         if repo.re_hexsha_only.match(tokens[0]):
             return (tokens[0], None)
 
@@ -252,25 +267,31 @@ def _get_ref_info_helper(
 
     @classmethod
     def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[Tuple[str, None], Tuple[None, str]]:
-        """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"""
+        """
+        :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.
+        """
         return cls._get_ref_info_helper(repo, ref_path)
 
     def _get_object(self) -> Commit_ish:
         """
         :return:
             The object our ref currently refers to. Refs can be cached, they will
-            always point to the actual object as it gets re-created on each query"""
-        # have to be dynamic here as we may be a tag which can point to anything
-        # Our path will be resolved to the hexsha which will be used accordingly
+            always point to the actual object as it gets re-created on each query.
+        """
+        # We have to be dynamic here as we may be a tag which can point to anything.
+        # Our path will be resolved to the hexsha which will be used accordingly.
         return Object.new_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path)))
 
     def _get_commit(self) -> "Commit":
         """
         :return:
-            Commit object we point to, works for detached and non-detached
-            SymbolicReferences. The symbolic reference will be dereferenced recursively."""
+            Commit object we point to. This works for detached and non-detached
+            :class:`SymbolicReference` instances. The symbolic reference will be
+            dereferenced recursively.
+        """
         obj = self._get_object()
         if obj.type == "tag":
             obj = obj.object
@@ -286,12 +307,13 @@ def set_commit(
         commit: Union[Commit, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
-        """As set_object, but restricts the type of object to be a Commit
+        """As set_object, but restricts the type of object to be a Commit.
 
         :raise ValueError: If commit is not a Commit object or doesn't point to
             a commit
-        :return: self"""
-        # check the type - assume the best if it is a base-string
+        :return: self
+        """
+        # Check the type - assume the best if it is a base-string.
         invalid_type = False
         if isinstance(commit, Object):
             invalid_type = commit.type != Commit.type
@@ -309,7 +331,7 @@ def set_commit(
             raise ValueError("Need commit, got %r" % commit)
         # END handle raise
 
-        # we leave strings to the rev-parse method below
+        # We leave strings to the rev-parse method below.
         self.set_object(commit, logmsg)
 
         return self
@@ -320,14 +342,18 @@ def set_object(
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set the object we point to, possibly dereference our symbolic reference first.
-        If the reference does not exist, it will be created
+        If the reference does not exist, it will be created.
 
-        :param object: a refspec, a SymbolicReference or an Object instance. SymbolicReferences
-            will be dereferenced beforehand to obtain the object they point to
+        :param object: A refspec, a :class:`SymbolicReference` or an
+            :class:`Object <git.objects.base.Object>` instance.
+            :class:`SymbolicReference` instances will be dereferenced beforehand to
+            obtain the object they point to.
         :param logmsg: If not None, the message will be used in the reflog entry to be
-            written. Otherwise the reflog is not altered
-        :note: plain SymbolicReferences may not actually point to objects by convention
-        :return: self"""
+            written. Otherwise the reflog is not altered.
+        :note: Plain :class:`SymbolicReference` instances may not actually point to
+            objects by convention.
+        :return: self
+        """
         if isinstance(object, SymbolicReference):
             object = object.object  # @ReservedAssignment
         # END resolve references
@@ -349,7 +375,9 @@ def set_object(
     object = property(_get_object, set_object, doc="Return the object our ref currently refers to")  # type: ignore
 
     def _get_reference(self) -> "SymbolicReference":
-        """:return: Reference Object we point to
+        """
+        :return: Reference Object we point to
+
         :raise TypeError: If this symbolic reference is detached, hence it doesn't point
             to a reference, but to a commit"""
         sha, target_ref_path = self._get_ref_info(self.repo, self.path)
@@ -367,18 +395,23 @@ def set_reference(
         will be set which effectively detaches the reference if it was a purely
         symbolic one.
 
-        :param ref: SymbolicReference instance, Object instance or refspec string
-            Only if the ref is a SymbolicRef instance, we will point to it. Everything
-            else is dereferenced to obtain the actual object.
+        :param ref:
+            A :class:`SymbolicReference` instance,
+            an :class:`Object <git.objects.base.Object>` instance, or a refspec string.
+            Only if the ref is a :class:`SymbolicReference` instance, we will point to
+            it. Everything else is dereferenced to obtain the actual object.
+
         :param logmsg: If set to a string, the message will be used in the reflog.
             Otherwise, a reflog entry is not written for the changed reference.
             The previous commit of the entry will be the commit we point to now.
 
-            See also: log_append()
+            See also: :meth:`log_append`
 
         :return: self
+
         :note: This symbolic reference will not be dereferenced. For that, see
-            ``set_object(...)``"""
+            :meth:`set_object`.
+        """
         write_value = None
         obj = None
         if isinstance(ref, SymbolicReference):
@@ -388,7 +421,7 @@ def set_reference(
             write_value = ref.hexsha
         elif isinstance(ref, str):
             try:
-                obj = self.repo.rev_parse(ref + "^{}")  # optionally deref tags
+                obj = self.repo.rev_parse(ref + "^{}")  # Optionally dereference tags.
                 write_value = obj.hexsha
             except (BadObject, BadName) as e:
                 raise ValueError("Could not extract object from %s" % ref) from e
@@ -428,7 +461,7 @@ def set_reference(
 
         return self
 
-    # aliased reference
+    # Aliased reference
     reference: Union["Head", "TagReference", "RemoteReference", "Reference"]
     reference = property(_get_reference, set_reference, doc="Returns the Reference we point to")  # type: ignore
     ref = reference
@@ -437,7 +470,8 @@ def is_valid(self) -> bool:
         """
         :return:
             True if the reference is valid, hence it can be read and points to
-            a valid object or reference."""
+            a valid object or reference.
+        """
         try:
             self.object
         except (OSError, ValueError):
@@ -450,7 +484,8 @@ def is_detached(self) -> bool:
         """
         :return:
             True if we are a detached reference, hence we point to a specific commit
-            instead to another reference"""
+            instead to another reference.
+        """
         try:
             self.ref
             return False
@@ -460,10 +495,11 @@ def is_detached(self) -> bool:
     def log(self) -> "RefLog":
         """
         :return: RefLog for this reference. Its last entry reflects the latest change
-            applied to this reference
+            applied to this reference.
 
         .. note:: As the log is parsed every time, its recommended to cache it for use
-            instead of calling this method repeatedly. It should be considered read-only."""
+            instead of calling this method repeatedly. It should be considered read-only.
+        """
         return RefLog.from_file(RefLog.path(self))
 
     def log_append(
@@ -472,16 +508,17 @@ def log_append(
         message: Union[str, None],
         newbinsha: Union[bytes, None] = None,
     ) -> "RefLogEntry":
-        """Append a logentry to the logfile of this ref
+        """Append a logentry to the logfile of this ref.
 
-        :param oldbinsha: binary sha this ref used to point to
-        :param message: A message describing the change
+        :param oldbinsha: Binary sha this ref used to point to.
+        :param message: A message describing the change.
         :param newbinsha: The sha the ref points to now. If None, our current commit sha
-            will be used
-        :return: added RefLogEntry instance"""
-        # NOTE: we use the committer of the currently active commit - this should be
+            will be used.
+        :return: The added :class:`RefLogEntry <git.refs.log.RefLogEntry>` instance.
+        """
+        # NOTE: We use the committer of the currently active commit - this should be
         # correct to allow overriding the committer on a per-commit level.
-        # See https://github.com/gitpython-developers/GitPython/pull/146
+        # See https://github.com/gitpython-developers/GitPython/pull/146.
         try:
             committer_or_reader: Union["Actor", "GitConfigParser"] = self.commit.committer
         except ValueError:
@@ -496,19 +533,24 @@ def log_append(
         return RefLog.append_entry(committer_or_reader, RefLog.path(self), oldbinsha, newbinsha, message)
 
     def log_entry(self, index: int) -> "RefLogEntry":
-        """:return: RefLogEntry at the given index
-        :param index: python list compatible positive or negative index
+        """
+        :return: RefLogEntry at the given index
+
+        :param index: Python list compatible positive or negative index
 
         .. note:: This method must read part of the reflog during execution, hence
-            it should be used sparringly, or only if you need just one index.
-            In that case, it will be faster than the ``log()`` method"""
+            it should be used sparingly, or only if you need just one index.
+            In that case, it will be faster than the ``log()`` method.
+        """
         return RefLog.entry_at(RefLog.path(self), index)
 
     @classmethod
     def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
         """
         :return: string with a full repository-relative path which can be used to initialize
-            a Reference instance, for instance by using ``Reference.from_path``"""
+            a Reference instance, for instance by using
+            :meth:`Reference.from_path <git.refs.reference.Reference.from_path>`.
+        """
         if isinstance(path, SymbolicReference):
             path = path.path
         full_ref_path = path
@@ -520,21 +562,22 @@ def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
 
     @classmethod
     def delete(cls, repo: "Repo", path: PathLike) -> None:
-        """Delete the reference at the given path
+        """Delete the reference at the given path.
 
         :param repo:
-            Repository to delete the reference from
+            Repository to delete the reference from.
 
         :param path:
-            Short or full path pointing to the reference, i.e. refs/myreference
-            or just "myreference", hence 'refs/' is implied.
-            Alternatively the symbolic reference to be deleted"""
+            Short or full path pointing to the reference, e.g. ``refs/myreference``
+            or just ``myreference``, hence ``refs/`` is implied.
+            Alternatively the symbolic reference to be deleted.
+        """
         full_ref_path = cls.to_full_path(path)
         abs_path = os.path.join(repo.common_dir, full_ref_path)
         if os.path.exists(abs_path):
             os.remove(abs_path)
         else:
-            # check packed refs
+            # Check packed refs.
             pack_file_path = cls._get_packed_refs_path(repo)
             try:
                 with open(pack_file_path, "rb") as reader:
@@ -545,10 +588,10 @@ def delete(cls, repo: "Repo", path: PathLike) -> None:
                         line = line_bytes.decode(defenc)
                         _, _, line_ref = line.partition(" ")
                         line_ref = line_ref.strip()
-                        # keep line if it is a comment or if the ref to delete is not
-                        # in the line
+                        # Keep line if it is a comment or if the ref to delete is not
+                        # in the line.
                         # If we deleted the last line and this one is a tag-reference object,
-                        # we drop it as well
+                        # we drop it as well.
                         if (line.startswith("#") or full_ref_path != line_ref) and (
                             not dropped_last_line or dropped_last_line and not line.startswith("^")
                         ):
@@ -557,21 +600,21 @@ def delete(cls, repo: "Repo", path: PathLike) -> None:
                             continue
                         # END skip comments and lines without our path
 
-                        # drop this line
+                        # Drop this line.
                         made_change = True
                         dropped_last_line = True
 
-                # write the new lines
+                # Write the new lines.
                 if made_change:
-                    # write-binary is required, otherwise windows will
-                    # open the file in text mode and change LF to CRLF !
+                    # Binary writing is required, otherwise Windows will
+                    # open the file in text mode and change LF to CRLF!
                     with open(pack_file_path, "wb") as fd:
                         fd.writelines(line.encode(defenc) for line in new_lines)
 
             except OSError:
-                pass  # it didn't exist at all
+                pass  # It didn't exist at all.
 
-        # delete the reflog
+        # Delete the reflog.
         reflog_path = RefLog.path(cls(repo, full_ref_path))
         if os.path.isfile(reflog_path):
             os.remove(reflog_path)
@@ -587,16 +630,18 @@ def _create(
         force: bool,
         logmsg: Union[str, None] = None,
     ) -> T_References:
-        """internal method used to create a new symbolic reference.
-        If resolve is False, the reference will be taken as is, creating
+        """Internal method used to create a new symbolic reference.
+
+        If `resolve` is False, the reference will be taken as is, creating
         a proper symbolic reference. Otherwise it will be resolved to the
         corresponding object and a detached symbolic reference will be created
-        instead"""
+        instead.
+        """
         git_dir = _git_dir(repo, path)
         full_ref_path = cls.to_full_path(path)
         abs_ref_path = os.path.join(git_dir, full_ref_path)
 
-        # figure out target data
+        # Figure out target data.
         target = reference
         if resolve:
             target = repo.rev_parse(str(reference))
@@ -630,22 +675,22 @@ def create(
         force: bool = False,
         **kwargs: Any,
     ) -> T_References:
-        """Create a new symbolic reference, hence a reference pointing , to another reference.
+        """Create a new symbolic reference: a reference pointing to another reference.
 
         :param repo:
-            Repository to create the reference in
+            Repository to create the reference in.
 
         :param path:
-            full path at which the new symbolic reference is supposed to be
-            created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref"
+            Full path at which the new symbolic reference is supposed to be
+            created at, e.g. ``NEW_HEAD`` or ``symrefs/my_new_symref``.
 
         :param reference:
-            The reference to which the new symbolic reference should point to.
+            The reference which the new symbolic reference should point to.
             If it is a commit-ish, the symbolic ref will be detached.
 
         :param force:
-            if True, force creation even if a symbolic reference with that name already exists.
-            Raise OSError otherwise
+            If True, force creation even if a symbolic reference with that name already exists.
+            Raise :class:`OSError` otherwise.
 
         :param logmsg:
             If not None, the message to append to the reflog. Otherwise no reflog
@@ -657,23 +702,26 @@ def create(
             If a (Symbolic)Reference with the same name but different contents
             already exists.
 
-        :note: This does not alter the current HEAD, index or Working Tree"""
+        :note: This does not alter the current HEAD, index or working tree.
+        """
         return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg)
 
     def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference":
-        """Rename self to a new path
+        """Rename self to a new path.
 
         :param new_path:
-            Either a simple name or a full path, i.e. new_name or features/new_name.
-            The prefix refs/ is implied for references and will be set as needed.
-            In case this is a symbolic ref, there is no implied prefix
+            Either a simple name or a full path, e.g. ``new_name`` or ``features/new_name``.
+            The prefix ``refs/`` is implied for references and will be set as needed.
+            In case this is a symbolic ref, there is no implied prefix.
 
         :param force:
             If True, the rename will succeed even if a head with the target name
-            already exists. It will be overwritten in that case
+            already exists. It will be overwritten in that case.
 
         :return: self
-        :raise OSError: In case a file at path but a different contents already exists"""
+
+        :raise OSError: If a file at path but with different contents already exists.
+        """
         new_path = self.to_full_path(new_path)
         if self.path == new_path:
             return self
@@ -682,15 +730,15 @@ def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference"
         cur_abs_path = os.path.join(_git_dir(self.repo, self.path), self.path)
         if os.path.isfile(new_abs_path):
             if not force:
-                # if they point to the same file, its not an error
+                # If they point to the same file, it's not an error.
                 with open(new_abs_path, "rb") as fd1:
                     f1 = fd1.read().strip()
                 with open(cur_abs_path, "rb") as fd2:
                     f2 = fd2.read().strip()
                 if f1 != f2:
                     raise OSError("File at path %r already exists" % new_abs_path)
-                # else: we could remove ourselves and use the otherone, but
-                # but clarity we just continue as usual
+                # else: We could remove ourselves and use the other one, but...
+                # ...for clarity, we just continue as usual.
             # END not force handling
             os.remove(new_abs_path)
         # END handle existing target file
@@ -713,10 +761,10 @@ def _iter_items(
             common_path = cls._common_path_default
         rela_paths = set()
 
-        # walk loose refs
-        # Currently we do not follow links
+        # Walk loose refs.
+        # Currently we do not follow links.
         for root, dirs, files in os.walk(join_path_native(repo.common_dir, common_path)):
-            if "refs" not in root.split(os.sep):  # skip non-refs subfolders
+            if "refs" not in root.split(os.sep):  # Skip non-refs subfolders.
                 refs_id = [d for d in dirs if d == "refs"]
                 if refs_id:
                     dirs[0:] = ["refs"]
@@ -730,14 +778,14 @@ def _iter_items(
             # END for each file in root directory
         # END for each directory to walk
 
-        # read packed refs
+        # Read packed refs.
         for _sha, rela_path in cls._iter_packed_refs(repo):
             if rela_path.startswith(str(common_path)):
                 rela_paths.add(rela_path)
             # END relative path matches common path
         # END packed refs reading
 
-        # return paths in sorted order
+        # Yield paths in sorted order.
         for path in sorted(rela_paths):
             try:
                 yield cls.from_path(repo, path)
@@ -753,37 +801,45 @@ def iter_items(
         *args: Any,
         **kwargs: Any,
     ) -> Iterator[T_References]:
-        """Find all refs in the repository
+        """Find all refs in the repository.
 
         :param repo: is the Repo
 
         :param common_path:
-            Optional keyword argument to the path which is to be shared by all
-            returned Ref objects.
-            Defaults to class specific portion if None assuring that only
-            refs suitable for the actual class are returned.
+            Optional keyword argument to the path which is to be shared by all returned
+            Ref objects.
+            Defaults to class specific portion if None, ensuring that only refs suitable
+            for the actual class are returned.
 
         :return:
-            git.SymbolicReference[], each of them is guaranteed to be a symbolic
-            ref which is not detached and pointing to a valid ref
+            A list of :class:`SymbolicReference`, each guaranteed to be a symbolic ref
+            which is not detached and pointing to a valid ref.
 
-            List is lexicographically sorted
-            The returned objects represent actual subclasses, such as Head or TagReference"""
+            The list is lexicographically sorted. The returned objects are instances of
+            concrete subclasses, such as :class:`Head <git.refs.head.Head>` or
+            :class:`TagReference <git.refs.tag.TagReference>`.
+        """
         return (r for r in cls._iter_items(repo, common_path) if r.__class__ is SymbolicReference or not r.is_detached)
 
     @classmethod
     def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_References:
         """
-        :param path: full .git-directory-relative path name to the Reference to instantiate
-        :note: use to_full_path() if you only have a partial path of a known Reference Type
+        Make a symbolic reference from a path.
+
+        :param path: Full ``.git``-directory-relative path name to the Reference to instantiate.
+
+        :note: Use :meth:`to_full_path` if you only have a partial path of a known Reference type.
+
         :return:
-            Instance of type Reference, Head, or Tag
-            depending on the given path"""
+            Instance of type :class:`Reference <git.refs.reference.Reference>`,
+            :class:`Head <git.refs.head.Head>`, or :class:`Tag <git.refs.tag.Tag>`,
+            depending on the given path.
+        """
         if not path:
             raise ValueError("Cannot create Reference from %r" % path)
 
-        # Names like HEAD are inserted after the refs module is imported - we have an import dependency
-        # cycle and don't want to import these names in-function
+        # Names like HEAD are inserted after the refs module is imported - we have an
+        # import dependency cycle and don't want to import these names in-function.
         from . import HEAD, Head, RemoteReference, TagReference, Reference
 
         for ref_type in (
diff --git a/git/refs/tag.py b/git/refs/tag.py
index d32d91bcf..3c269d9ba 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -18,10 +18,9 @@
 
 
 class TagReference(Reference):
-
-    """Class representing a lightweight tag reference which either points to a commit
-    ,a tag object or any other object. In the latter case additional information,
-    like the signature or the tag-creator, is available.
+    """A lightweight tag reference which either points to a commit, a tag object or any
+    other object. In the latter case additional information, like the signature or the
+    tag-creator, is available.
 
     This tag object will always point to a commit object, but may carry additional
     information in a tag object::
@@ -29,9 +28,11 @@ class TagReference(Reference):
      tagref = TagReference.list_items(repo)[0]
      print(tagref.commit.message)
      if tagref.tag is not None:
-        print(tagref.tag.message)"""
+        print(tagref.tag.message)
+    """
 
     __slots__ = ()
+
     _common_default = "tags"
     _common_path_default = Reference._common_path_default + "/" + _common_default
 
@@ -39,11 +40,12 @@ class TagReference(Reference):
     def commit(self) -> "Commit":  # type: ignore[override]  # LazyMixin has unrelated commit method
         """:return: Commit object the tag ref points to
 
-        :raise ValueError: if the tag points to a tree or blob"""
+        :raise ValueError: If the tag points to a tree or blob
+        """
         obj = self.object
         while obj.type != "commit":
             if obj.type == "tag":
-                # it is a tag object which carries the commit as an object - we can point to anything
+                # It is a tag object which carries the commit as an object - we can point to anything.
                 obj = obj.object
             else:
                 raise ValueError(
@@ -59,16 +61,13 @@ def commit(self) -> "Commit":  # type: ignore[override]  # LazyMixin has unrelat
     def tag(self) -> Union["TagObject", None]:
         """
         :return: Tag object this tag ref points to or None in case
-            we are a light weight tag"""
+            we are a lightweight tag"""
         obj = self.object
         if obj.type == "tag":
             return obj
         return None
 
-    # make object read-only
-    # It should be reasonably hard to adjust an existing tag
-
-    # object = property(Reference._get_object)
+    # Make object read-only. It should be reasonably hard to adjust an existing tag.
     @property
     def object(self) -> Commit_ish:  # type: ignore[override]
         return Reference._get_object(self)
@@ -86,30 +85,31 @@ def create(
         """Create a new tag reference.
 
         :param path:
-            The name of the tag, i.e. 1.0 or releases/1.0.
-            The prefix refs/tags is implied
+            The name of the tag, e.g. ``1.0`` or ``releases/1.0``.
+            The prefix ``refs/tags`` is implied.
 
         :param ref:
-            A reference to the Object you want to tag. The Object can be a commit, tree or
-            blob.
+            A reference to the :class:`Object <git.objects.base.Object>` you want to
+            tag. The Object can be a commit, tree or blob.
 
         :param logmsg:
             If not None, the message will be used in your tag object. This will also
-            create an additional tag object that allows to obtain that information, i.e.::
+            create an additional tag object that allows to obtain that information, e.g.::
 
                 tagref.tag.message
 
         :param message:
-            Synonym for :param logmsg:
-            Included for backwards compatibility. :param logmsg is used in preference if both given.
+            Synonym for the `logmsg` parameter.
+            Included for backwards compatibility. `logmsg` takes precedence if both are passed.
 
         :param force:
-            If True, to force creation of a tag even though that tag already exists.
+            If True, force creation of a tag even though that tag already exists.
 
         :param kwargs:
-            Additional keyword arguments to be passed to git-tag
+            Additional keyword arguments to be passed to git-tag.
 
-        :return: A new TagReference"""
+        :return: A new TagReference.
+        """
         if "ref" in kwargs and kwargs["ref"]:
             reference = kwargs["ref"]
 
@@ -130,9 +130,9 @@ def create(
 
     @classmethod
     def delete(cls, repo: "Repo", *tags: "TagReference") -> None:  # type: ignore[override]
-        """Delete the given existing tag or tags"""
+        """Delete the given existing tag or tags."""
         repo.git.tag("-d", *tags)
 
 
-# provide an alias
+# Provide an alias.
 Tag = TagReference
diff --git a/git/remote.py b/git/remote.py
index fc2b2ceba..76352087a 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -4,7 +4,8 @@
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
-# Module implementing a remote object allowing easy access to git remotes
+"""Module implementing a remote object allowing easy access to git remotes."""
+
 import logging
 import re
 
@@ -80,9 +81,13 @@ def add_progress(
     progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None],
 ) -> Any:
     """Add the --progress flag to the given kwargs dict if supported by the
-    git command. If the actual progress in the given progress instance is not
-    given, we do not request any progress
-    :return: possibly altered kwargs"""
+    git command.
+
+    :note: If the actual progress in the given progress instance is not
+        given, we do not request any progress.
+
+    :return: possibly altered kwargs
+    """
     if progress is not None:
         v = git.version_info[:2]
         if v >= (1, 7):
@@ -113,18 +118,16 @@ def to_progress_instance(progress: RemoteProgress) -> RemoteProgress:
 def to_progress_instance(
     progress: Union[Callable[..., Any], RemoteProgress, None]
 ) -> Union[RemoteProgress, CallableRemoteProgress]:
-    """Given the 'progress' return a suitable object derived from
-    RemoteProgress().
-    """
-    # new API only needs progress as a function
+    """Given the 'progress' return a suitable object derived from RemoteProgress."""
+    # New API only needs progress as a function.
     if callable(progress):
         return CallableRemoteProgress(progress)
 
-    # where None is passed create a parser that eats the progress
+    # Where None is passed create a parser that eats the progress.
     elif progress is None:
         return RemoteProgress()
 
-    # assume its the old API with an instance of RemoteProgress.
+    # Assume its the old API with an instance of RemoteProgress.
     return progress
 
 
@@ -152,6 +155,7 @@ class PushInfo(IterableObj, object):
         "_remote",
         "summary",
     )
+
     _id_attribute_ = "pushinfo"
 
     (
@@ -187,8 +191,10 @@ def __init__(
         old_commit: Optional[str] = None,
         summary: str = "",
     ) -> None:
-        """Initialize a new instance
-        local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None"""
+        """Initialize a new instance.
+
+        local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None
+        """
         self.flags = flags
         self.local_ref = local_ref
         self.remote_ref_string = remote_ref_string
@@ -204,9 +210,11 @@ def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]:
     def remote_ref(self) -> Union[RemoteReference, TagReference]:
         """
         :return:
-            Remote Reference or TagReference in the local repository corresponding
-            to the remote_ref_string kept in this instance."""
-        # translate heads to a local remote, tags stay as they are
+            Remote :class:`Reference <git.refs.reference.Reference>` or
+            :class:`TagReference <git.refs.tag.TagReference>` in the local repository
+            corresponding to the :attr:`remote_ref_string` kept in this instance.
+        """
+        # Translate heads to a local remote. Tags stay as they are.
         if self.remote_ref_string.startswith("refs/tags"):
             return TagReference(self._remote.repo, self.remote_ref_string)
         elif self.remote_ref_string.startswith("refs/heads"):
@@ -222,11 +230,11 @@ def remote_ref(self) -> Union[RemoteReference, TagReference]:
     @classmethod
     def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
         """Create a new PushInfo instance as parsed from line which is expected to be like
-        refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes"""
+        refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
         control_character, from_to, summary = line.split("\t", 3)
         flags = 0
 
-        # control character handling
+        # Control character handling
         try:
             flags |= cls._flag_map[control_character]
         except KeyError as e:
@@ -243,7 +251,7 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
             else:
                 from_ref = Reference.from_path(remote.repo, from_ref_string)
 
-        # commit handling, could be message or commit info
+        # Commit handling, could be message or commit info
         old_commit: Optional[str] = None
         if summary.startswith("["):
             if "[rejected]" in summary:
@@ -260,13 +268,13 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
                 flags |= cls.NEW_HEAD
             # uptodate encoded in control character
         else:
-            # fast-forward or forced update - was encoded in control character,
-            # but we parse the old and new commit
+            # Fast-forward or forced update - was encoded in control character,
+            # but we parse the old and new commit.
             split_token = "..."
             if control_character == " ":
                 split_token = ".."
             old_sha, _new_sha = summary.split(" ")[0].split(split_token)
-            # have to use constructor here as the sha usually is abbreviated
+            # Have to use constructor here as the sha usually is abbreviated.
             old_commit = old_sha
         # END message handling
 
@@ -278,9 +286,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn:  # ->
 
 
 class PushInfoList(IterableList[PushInfo]):
-    """
-    IterableList of PushInfo objects.
-    """
+    """IterableList of PushInfo objects."""
 
     def __new__(cls) -> "PushInfoList":
         return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
@@ -290,15 +296,12 @@ def __init__(self) -> None:
         self.error: Optional[Exception] = None
 
     def raise_if_error(self) -> None:
-        """
-        Raise an exception if any ref failed to push.
-        """
+        """Raise an exception if any ref failed to push."""
         if self.error:
             raise self.error
 
 
 class FetchInfo(IterableObj, object):
-
     """
     Carries information about the results of a fetch operation of a single head::
 
@@ -315,6 +318,7 @@ class FetchInfo(IterableObj, object):
     """
 
     __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path")
+
     _id_attribute_ = "fetchinfo"
 
     (
@@ -341,9 +345,7 @@ class FetchInfo(IterableObj, object):
 
     @classmethod
     def refresh(cls) -> Literal[True]:
-        """This gets called by the refresh function (see the top level
-        __init__).
-        """
+        """This gets called by the refresh function (see the top level __init__)."""
         # clear the old values in _flag_map
         try:
             del cls._flag_map["t"]
@@ -371,9 +373,7 @@ def __init__(
         old_commit: Union[Commit_ish, None] = None,
         remote_ref_path: Optional[PathLike] = None,
     ) -> None:
-        """
-        Initialize a new instance
-        """
+        """Initialize a new instance."""
         self.ref = ref
         self.flags = flags
         self.note = note
@@ -410,12 +410,13 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         ' ' means a fast-forward
 
         fetch line is the corresponding line from FETCH_HEAD, like
-        acb0fa8b94ef421ad60c8507b634759a472cd56c    not-for-merge   branch '0.1.7RC' of /tmp/tmpya0vairemote_repo"""
+        acb0fa8b94ef421ad60c8507b634759a472cd56c    not-for-merge   branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
+        """
         match = cls._re_fetch_result.match(line)
         if match is None:
             raise ValueError("Failed to parse line: %r" % line)
 
-        # parse lines
+        # Parse lines.
         remote_local_ref_str: str
         (
             control_character,
@@ -432,7 +433,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         except ValueError as e:  # unpack error
             raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e
 
-        # parse flags from control_character
+        # Parse flags from control_character.
         flags = 0
         try:
             flags |= cls._flag_map[control_character]
@@ -440,7 +441,8 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
             raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e
         # END control char exception handling
 
-        # parse operation string for more info - makes no sense for symbolic refs, but we parse it anyway
+        # Parse operation string for more info.
+        # This makes no sense for symbolic refs, but we parse it anyway.
         old_commit: Union[Commit_ish, None] = None
         is_tag_operation = False
         if "rejected" in operation:
@@ -460,45 +462,45 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
             old_commit = repo.rev_parse(operation.split(split_token)[0])
         # END handle refspec
 
-        # handle FETCH_HEAD and figure out ref type
+        # Handle FETCH_HEAD and figure out ref type.
         # If we do not specify a target branch like master:refs/remotes/origin/master,
         # the fetch result is stored in FETCH_HEAD which destroys the rule we usually
-        # have. In that case we use a symbolic reference which is detached
+        # have. In that case we use a symbolic reference which is detached.
         ref_type: Optional[Type[SymbolicReference]] = None
         if remote_local_ref_str == "FETCH_HEAD":
             ref_type = SymbolicReference
         elif ref_type_name == "tag" or is_tag_operation:
-            # the ref_type_name can be branch, whereas we are still seeing a tag operation. It happens during
-            # testing, which is based on actual git operations
+            # The ref_type_name can be branch, whereas we are still seeing a tag operation.
+            # It happens during testing, which is based on actual git operations.
             ref_type = TagReference
         elif ref_type_name in ("remote-tracking", "branch"):
-            # note: remote-tracking is just the first part of the 'remote-tracking branch' token.
-            # We don't parse it correctly, but its enough to know what to do, and its new in git 1.7something
+            # Note: remote-tracking is just the first part of the 'remote-tracking branch' token.
+            # We don't parse it correctly, but its enough to know what to do, and it's new in git 1.7something.
             ref_type = RemoteReference
         elif "/" in ref_type_name:
-            # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*', and is thus pretty
-            # much anything the user wants, we will have trouble to determine what's going on
-            # For now, we assume the local ref is a Head
+            # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*',
+            # and is thus pretty much anything the user wants, we will have trouble
+            # determining what's going on. For now, we assume the local ref is a Head.
             ref_type = Head
         else:
             raise TypeError("Cannot handle reference type: %r" % ref_type_name)
         # END handle ref type
 
-        # create ref instance
+        # Create ref instance.
         if ref_type is SymbolicReference:
             remote_local_ref = ref_type(repo, "FETCH_HEAD")
         else:
-            # determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories.
-            # It is not clear sometimes where exactly the item is, unless we have an absolute path as indicated
-            # by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the
-            # 'tags/' subdirectory in its path.
+            # Determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories.
+            # It is not clear sometimes where exactly the item is, unless we have an absolute path as
+            # indicated by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is
+            # when it will have the 'tags/' subdirectory in its path.
             # We don't want to test for actual existence, but try to figure everything out analytically.
             ref_path: Optional[PathLike] = None
             remote_local_ref_str = remote_local_ref_str.strip()
 
             if remote_local_ref_str.startswith(Reference._common_path_default + "/"):
-                # always use actual type if we get absolute paths
-                # Will always be the case if something is fetched outside of refs/remotes (if its not a tag)
+                # Always use actual type if we get absolute paths.
+                # Will always be the case if something is fetched outside of refs/remotes (if its not a tag).
                 ref_path = remote_local_ref_str
                 if ref_type is not TagReference and not remote_local_ref_str.startswith(
                     RemoteReference._common_path_default + "/"
@@ -506,14 +508,14 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
                     ref_type = Reference
                 # END downgrade remote reference
             elif ref_type is TagReference and "tags/" in remote_local_ref_str:
-                # even though its a tag, it is located in refs/remotes
+                # Even though it's a tag, it is located in refs/remotes.
                 ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str)
             else:
                 ref_path = join_path(ref_type._common_path_default, remote_local_ref_str)
             # END obtain refpath
 
-            # even though the path could be within the git conventions, we make
-            # sure we respect whatever the user wanted, and disabled path checking
+            # Even though the path could be within the git conventions, we make
+            # sure we respect whatever the user wanted, and disabled path checking.
             remote_local_ref = ref_type(repo, ref_path, check_path=False)
         # END create ref instance
 
@@ -527,16 +529,17 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn:  # ->
 
 
 class Remote(LazyMixin, IterableObj):
-
     """Provides easy read and write access to a git remote.
 
     Everything not part of this interface is considered an option for the current
     remote, allowing constructs like remote.pushurl to query the pushurl.
 
-    NOTE: When querying configuration, the configuration accessor will be cached
-    to speed up subsequent accesses."""
+    :note: When querying configuration, the configuration accessor will be cached
+        to speed up subsequent accesses.
+    """
 
     __slots__ = ("repo", "name", "_config_reader")
+
     _id_attribute_ = "name"
 
     unsafe_git_fetch_options = [
@@ -557,22 +560,23 @@ class Remote(LazyMixin, IterableObj):
     ]
 
     def __init__(self, repo: "Repo", name: str) -> None:
-        """Initialize a remote instance
+        """Initialize a remote instance.
 
         :param repo: The repository we are a remote of
-        :param name: the name of the remote, i.e. 'origin'"""
+        :param name: The name of the remote, e.g. 'origin'
+        """
         self.repo = repo
         self.name = name
         self.url: str
 
     def __getattr__(self, attr: str) -> Any:
         """Allows to call this instance like
-        remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name"""
+        remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name."""
         if attr == "_config_reader":
             return super(Remote, self).__getattr__(attr)
 
-        # sometimes, probably due to a bug in python itself, we are being called
-        # even though a slot of the same name exists
+        # Sometimes, probably due to a bug in Python itself, we are being called
+        # even though a slot of the same name exists.
         try:
             return self._config_reader.get(attr)
         except cp.NoOptionError:
@@ -584,8 +588,8 @@ def _config_section_name(self) -> str:
 
     def _set_cache_(self, attr: str) -> None:
         if attr == "_config_reader":
-            # NOTE: This is cached as __getattr__ is overridden to return remote config values implicitly, such as
-            # in print(r.pushurl)
+            # NOTE: This is cached as __getattr__ is overridden to return remote config
+            # values implicitly, such as in print(r.pushurl).
             self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name())
         else:
             super(Remote, self)._set_cache_(attr)
@@ -608,12 +612,13 @@ def __hash__(self) -> int:
     def exists(self) -> bool:
         """
         :return: True if this is a valid, existing remote.
-            Valid remotes have an entry in the repository's configuration"""
+            Valid remotes have an entry in the repository's configuration.
+        """
         try:
             self.config_reader.get("url")
             return True
         except cp.NoOptionError:
-            # we have the section at least ...
+            # We have the section at least...
             return True
         except cp.NoSectionError:
             return False
@@ -635,12 +640,12 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote
     def set_url(
         self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
     ) -> "Remote":
-        """Configure URLs on current remote (cf command git remote set_url)
+        """Configure URLs on current remote (cf command git remote set_url).
 
         This command manages URLs on the remote.
 
-        :param new_url: string being the URL to add as an extra remote URL
-        :param old_url: when set, replaces this URL with new_url for the remote
+        :param new_url: String being the URL to add as an extra remote URL
+        :param old_url: When set, replaces this URL with new_url for the remote
         :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
         :return: self
         """
@@ -655,12 +660,12 @@ def set_url(
         return self
 
     def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
-        """Adds a new url on current remote (special case of git remote set_url)
+        """Adds a new url on current remote (special case of git remote set_url).
 
         This command adds new URLs to a given remote, making it possible to have
         multiple URLs for a single remote.
 
-        :param url: string being the URL to add as an extra remote URL
+        :param url: String being the URL to add as an extra remote URL
         :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
         :return: self
         """
@@ -672,7 +677,7 @@ def delete_url(self, url: str, **kwargs: Any) -> "Remote":
         This command deletes new URLs to a given remote, making it possible to have
         multiple URLs for a single remote.
 
-        :param url: string being the URL to delete from the remote
+        :param url: String being the URL to delete from the remote
         :return: self
         """
         return self.set_url(url, delete=True)
@@ -700,7 +705,7 @@ def urls(self) -> Iterator[str]:
                             yield line.split(": ")[-1]
                 except GitCommandError as _ex:
                     if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]):
-                        # If ssh is not setup to access this repository, see issue 694
+                        # If ssh is not setup to access this repository, see issue 694.
                         remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name)
                         assert isinstance(remote_details, str)
                         for line in remote_details.split("\n"):
@@ -715,8 +720,9 @@ def refs(self) -> IterableList[RemoteReference]:
         """
         :return:
             IterableList of RemoteReference objects. It is prefixed, allowing
-            you to omit the remote path portion, i.e.::
-            remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')"""
+            you to omit the remote path portion, e.g.::
+            remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')
+        """
         out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
         out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name))
         return out_refs
@@ -745,7 +751,7 @@ def stale_refs(self) -> IterableList[Reference]:
             if not line.startswith(token):
                 continue
             ref_name = line.replace(token, "")
-            # sometimes, paths start with a full ref name, like refs/tags/foo, see #260
+            # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260.
             if ref_name.startswith(Reference._common_path_default + "/"):
                 out_refs.append(Reference.from_path(self.repo, ref_name))
             else:
@@ -757,7 +763,7 @@ def stale_refs(self) -> IterableList[Reference]:
 
     @classmethod
     def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
-        """Create a new remote to the given repository
+        """Create a new remote to the given repository.
 
         :param repo: Repository instance that is to receive the new remote
         :param name: Desired name of the remote
@@ -765,7 +771,8 @@ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool
         :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
         :param kwargs: Additional arguments to be passed to the git-remote add command
         :return: New Remote instance
-        :raise GitCommandError: in case an origin with that name already exists"""
+        :raise GitCommandError: in case an origin with that name already exists
+        """
         scmd = "add"
         kwargs["insert_kwargs_after"] = scmd
         url = Git.polish_url(url)
@@ -774,29 +781,30 @@ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool
         repo.git.remote(scmd, "--", name, url, **kwargs)
         return cls(repo, name)
 
-    # add is an alias
+    # `add` is an alias.
     @classmethod
     def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
         return cls.create(repo, name, url, **kwargs)
 
     @classmethod
     def remove(cls, repo: "Repo", name: str) -> str:
-        """Remove the remote with the given name
+        """Remove the remote with the given name.
 
-        :return: the passed remote name to remove
+        :return: The passed remote name to remove
         """
         repo.git.remote("rm", name)
         if isinstance(name, cls):
             name._clear_cache()
         return name
 
-    # alias
+    # `rm` is an alias.
     rm = remove
 
     def rename(self, new_name: str) -> "Remote":
-        """Rename self to the given new_name
+        """Rename self to the given new_name.
 
-        :return: self"""
+        :return: self
+        """
         if self.name == new_name:
             return self
 
@@ -808,13 +816,12 @@ def rename(self, new_name: str) -> "Remote":
 
     def update(self, **kwargs: Any) -> "Remote":
         """Fetch all changes for this remote, including new branches which will
-        be forced in ( in case your local remote branch is not part the new remote branches
-        ancestry anymore ).
-
-        :param kwargs:
-            Additional arguments passed to git-remote update
+        be forced in (in case your local remote branch is not part the new remote
+        branch's ancestry anymore).
 
-        :return: self"""
+        :param kwargs: Additional arguments passed to git-remote update
+        :return: self
+        """
         scmd = "update"
         kwargs["insert_kwargs_after"] = scmd
         self.repo.git.remote(scmd, self.name, **kwargs)
@@ -828,15 +835,15 @@ def _get_fetch_info_from_stderr(
     ) -> IterableList["FetchInfo"]:
         progress = to_progress_instance(progress)
 
-        # skip first line as it is some remote info we are not interested in
+        # Skip first line as it is some remote info we are not interested in.
         output: IterableList["FetchInfo"] = IterableList("name")
 
-        # lines which are no progress are fetch info lines
-        # this also waits for the command to finish
-        # Skip some progress lines that don't provide relevant information
+        # Lines which are no progress are fetch info lines.
+        # This also waits for the command to finish.
+        # Skip some progress lines that don't provide relevant information.
         fetch_info_lines = []
-        # Basically we want all fetch info lines which appear to be in regular form, and thus have a
-        # command character. Everything else we ignore,
+        # Basically we want all fetch info lines which appear to be in regular form, and
+        # thus have a command character. Everything else we ignore.
         cmds = set(FetchInfo._flag_map.keys())
 
         progress_handler = progress.new_message_handler()
@@ -861,7 +868,7 @@ def _get_fetch_info_from_stderr(
                     fetch_info_lines.append(line)
                     continue
 
-        # read head information
+        # Read head information.
         fetch_head = SymbolicReference(self.repo, "FETCH_HEAD")
         with open(fetch_head.abspath, "rb") as fp:
             fetch_head_info = [line.decode(defenc) for line in fp.readlines()]
@@ -899,10 +906,10 @@ def _get_push_info(
     ) -> PushInfoList:
         progress = to_progress_instance(progress)
 
-        # read progress information from stderr
-        # we hope stdout can hold all the data, it should ...
-        # read the lines manually as it will use carriage returns between the messages
-        # to override the previous one. This is why we read the bytes manually
+        # Read progress information from stderr.
+        # We hope stdout can hold all the data, it should...
+        # Read the lines manually as it will use carriage returns between the messages
+        # to override the previous one. This is why we read the bytes manually.
         progress_handler = progress.new_message_handler()
         output: PushInfoList = PushInfoList()
 
@@ -925,8 +932,8 @@ def stdout_handler(line: str) -> None:
         try:
             proc.wait(stderr=stderr_text)
         except Exception as e:
-            # This is different than fetch (which fails if there is any std_err
-            # even if there is an output)
+            # This is different than fetch (which fails if there is any stderr
+            # even if there is an output).
             if not output:
                 raise
             elif stderr_text:
@@ -936,7 +943,7 @@ def stdout_handler(line: str) -> None:
         return output
 
     def _assert_refspec(self) -> None:
-        """Turns out we can't deal with remotes if the refspec is missing"""
+        """Turns out we can't deal with remotes if the refspec is missing."""
         config = self.config_reader
         unset = "placeholder"
         try:
@@ -958,38 +965,46 @@ def fetch(
         allow_unsafe_options: bool = False,
         **kwargs: Any,
     ) -> IterableList[FetchInfo]:
-        """Fetch the latest changes for this remote
+        """Fetch the latest changes for this remote.
 
         :param refspec:
             A "refspec" is used by fetch and push to describe the mapping
             between remote ref and local ref. They are combined with a colon in
-            the format <src>:<dst>, preceded by an optional plus sign, +.
-            For example: git fetch $URL refs/heads/master:refs/heads/origin means
+            the format ``<src>:<dst>``, preceded by an optional plus sign, ``+``.
+            For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means
             "grab the master branch head from the $URL and store it as my origin
-            branch head". And git push $URL refs/heads/master:refs/heads/to-upstream
+            branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream``
             means "publish my master branch head as to-upstream branch at $URL".
             See also git-push(1).
 
-            Taken from the git manual
+            Taken from the git manual, gitglossary(7).
 
             Fetch supports multiple refspecs (as the
             underlying git-fetch does) - supplying a list rather than a string
             for 'refspec' will make use of this facility.
-        :param progress: See 'push' method
-        :param verbose: Boolean for verbose output
+
+        :param progress: See :meth:`push` method.
+
+        :param verbose: Boolean for verbose output.
+
         :param kill_after_timeout:
             To specify a timeout in seconds for the git command, after which the process
             should be killed. It is set to None by default.
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :param kwargs: Additional arguments to be passed to git-fetch
+
+        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+
+        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
+
+        :param kwargs: Additional arguments to be passed to git-fetch.
+
         :return:
             IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed
             information about the fetch results
 
         :note:
             As fetch does not provide progress information to non-ttys, we cannot make
-            it available here unfortunately as in the 'push' method."""
+            it available here unfortunately as in the :meth:`push` method.
+        """
         if refspec is None:
             # No argument refspec, then ensure the repo's config has a fetch refspec.
             self._assert_refspec()
@@ -1028,13 +1043,14 @@ def pull(
         """Pull changes from the given branch, being the same as a fetch followed
         by a merge of branch with your local branch.
 
-        :param refspec: see :meth:`fetch` method
-        :param progress: see :meth:`push` method
-        :param kill_after_timeout: see :meth:`fetch` method
+        :param refspec: See :meth:`fetch` method
+        :param progress: See :meth:`push` method
+        :param kill_after_timeout: See :meth:`fetch` method
         :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
         :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
         :param kwargs: Additional arguments to be passed to git-pull
-        :return: Please see :meth:`fetch` method"""
+        :return: Please see :meth:`fetch` method
+        """
         if refspec is None:
             # No argument refspec, then ensure the repo's config has a fetch refspec.
             self._assert_refspec()
@@ -1067,33 +1083,42 @@ def push(
     ) -> PushInfoList:
         """Push changes from source branch in refspec to target branch in refspec.
 
-        :param refspec: see 'fetch' method
+        :param refspec: See :meth:`fetch` method.
+
         :param progress:
             Can take one of many value types:
 
-            * None to discard progress information
+            * None to discard progress information.
             * A function (callable) that is called with the progress information.
               Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
               `Click here <http://goo.gl/NPa7st>`__ for a description of all arguments
               given to the function.
-            * An instance of a class derived from ``git.RemoteProgress`` that
-              overrides the ``update()`` function.
+            * An instance of a class derived from :class:`git.RemoteProgress` that
+              overrides the :meth:`update <git.RemoteProgress.update>` method.
 
         :note: No further progress information is returned after push returns.
+
         :param kill_after_timeout:
             To specify a timeout in seconds for the git command, after which the process
             should be killed. It is set to None by default.
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --receive-pack
-        :param kwargs: Additional arguments to be passed to git-push
+
+        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like --receive-pack.
+
+        :param kwargs: Additional arguments to be passed to git-push.
+
         :return:
-            A ``PushInfoList`` object, where each list member
+            A :class:`PushInfoList` object, where each list member
             represents an individual head which had been updated on the remote side.
-            If the push contains rejected heads, these will have the PushInfo.ERROR bit set
-            in their flags.
+            If the push contains rejected heads, these will have the
+            :attr:`PushInfo.ERROR` bit set in their flags.
             If the operation fails completely, the length of the returned PushInfoList will
             be 0.
-            Call ``.raise_if_error()`` on the returned object to raise on any failure."""
+            Call :meth:`raise_if_error <PushInfoList.raise_if_error>` on the returned
+            object to raise on any failure.
+        """
         kwargs = add_progress(kwargs, self.repo.git, progress)
 
         refspec = Git._unpack_args(refspec or [])
@@ -1121,7 +1146,8 @@ def config_reader(self) -> SectionConstraint[GitConfigParser]:
         """
         :return:
             GitConfigParser compatible object able to read options for only our remote.
-            Hence you may simple type config.get("pushurl") to obtain the information"""
+            Hence you may simple type config.get("pushurl") to obtain the information.
+        """
         return self._config_reader
 
     def _clear_cache(self) -> None:
@@ -1135,15 +1161,17 @@ def _clear_cache(self) -> None:
     def config_writer(self) -> SectionConstraint:
         """
         :return: GitConfigParser compatible object able to write options for this remote.
+
         :note:
             You can only own one writer at a time - delete it to release the
             configuration file and make it usable by others.
 
             To assure consistent results, you should only query options through the
             writer. Once you are done writing, you are free to use the config reader
-            once again."""
+            once again.
+        """
         writer = self.repo.config_writer()
 
-        # clear our cache to assure we re-read the possibly changed configuration
+        # Clear our cache to ensure we re-read the possibly changed configuration.
         self._clear_cache()
         return SectionConstraint(writer, self._config_section_name())
diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index 23c18db85..f1eac3311 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -1,3 +1,5 @@
-"""Initialize the Repo package"""
+"""Initialize the Repo package."""
+
 # flake8: noqa
+
 from .base import Repo as Repo
diff --git a/git/repo/base.py b/git/repo/base.py
index 23136a1d1..da81698d8 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -3,7 +3,9 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 from __future__ import annotations
+
 import logging
 import os
 import re
@@ -164,10 +166,10 @@ def __init__(
         search_parent_directories: bool = False,
         expand_vars: bool = True,
     ) -> None:
-        """Create a new Repo instance
+        """Create a new Repo instance.
 
         :param path:
-            the path to either the root git directory or the bare git repo::
+            The path to either the root git directory or the bare git repo::
 
                 repo = Repo("/Users/mtrier/Development/git-python")
                 repo = Repo("/Users/mtrier/Development/git-python.git")
@@ -175,21 +177,26 @@ def __init__(
                 repo = Repo("$REPOSITORIES/Development/git-python.git")
                 repo = Repo("C:\\Users\\mtrier\\Development\\git-python\\.git")
 
-            - In *Cygwin*, path may be a `'cygdrive/...'` prefixed path.
-            - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also evals to false,
-              the current-directory is used.
+            - In *Cygwin*, path may be a ``cygdrive/...`` prefixed path.
+            - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also
+              evals to false, the current-directory is used.
+
         :param odbt:
             Object DataBase type - a type which is constructed by providing
             the directory containing the database objects, i.e. .git/objects. It will
             be used to access all object data
+
         :param search_parent_directories:
-            if True, all parent directories will be searched for a valid repo as well.
+            If True, all parent directories will be searched for a valid repo as well.
+
+            Please note that this was the default behaviour in older versions of
+            GitPython, which is considered a bug though.
 
-            Please note that this was the default behaviour in older versions of GitPython,
-            which is considered a bug though.
         :raise InvalidGitRepositoryError:
         :raise NoSuchPathError:
-        :return: git.Repo"""
+
+        :return: git.Repo
+        """
 
         epath = path or os.getenv("GIT_DIR")
         if not epath:
@@ -271,7 +278,7 @@ def __init__(
         try:
             self._bare = self.config_reader("repository").getboolean("core", "bare")
         except Exception:
-            # lets not assume the option exists, although it should
+            # Let's not assume the option exists, although it should.
             pass
 
         try:
@@ -280,8 +287,8 @@ def __init__(
         except OSError:
             self._common_dir = ""
 
-        # adjust the wd in case we are actually bare - we didn't know that
-        # in the first place
+        # Adjust the working directory in case we are actually bare - we didn't know
+        # that in the first place.
         if self._bare:
             self._working_tree_dir = None
         # END working dir handling
@@ -289,7 +296,7 @@ def __init__(
         self.working_dir: PathLike = self._working_tree_dir or self.common_dir
         self.git = self.GitCommandWrapperType(self.working_dir)
 
-        # special handling, in special times
+        # Special handling, in special times.
         rootpath = osp.join(self.common_dir, "objects")
         if issubclass(odbt, GitCmdObjectDB):
             self.odb = odbt(rootpath, self.git)
@@ -311,12 +318,10 @@ def __del__(self) -> None:
     def close(self) -> None:
         if self.git:
             self.git.clear_cache()
-            # Tempfiles objects on Windows are holding references to
-            # open files until they are collected by the garbage
-            # collector, thus preventing deletion.
-            # TODO: Find these references and ensure they are closed
-            # and deleted synchronously rather than forcing a gc
-            # collection.
+            # Tempfiles objects on Windows are holding references to open files until
+            # they are collected by the garbage collector, thus preventing deletion.
+            # TODO: Find these references and ensure they are closed and deleted
+            # synchronously rather than forcing a gc collection.
             if is_win:
                 gc.collect()
             gitdb.util.mman.collect()
@@ -351,14 +356,18 @@ def _set_description(self, descr: str) -> None:
 
     @property
     def working_tree_dir(self) -> Optional[PathLike]:
-        """:return: The working tree directory of our git repository. If this is a bare repository, None is returned."""
+        """
+        :return: The working tree directory of our git repository.
+            If this is a bare repository, None is returned.
+        """
         return self._working_tree_dir
 
     @property
     def common_dir(self) -> PathLike:
         """
         :return: The git dir that holds everything except possibly HEAD,
-            FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/."""
+            FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
+        """
         return self._common_dir or self.git_dir
 
     @property
@@ -368,30 +377,36 @@ def bare(self) -> bool:
 
     @property
     def heads(self) -> "IterableList[Head]":
-        """A list of ``Head`` objects representing the branch heads in
-        this repo
+        """A list of ``Head`` objects representing the branch heads in this repo.
 
-        :return: ``git.IterableList(Head, ...)``"""
+        :return: ``git.IterableList(Head, ...)``
+        """
         return Head.list_items(self)
 
     @property
     def references(self) -> "IterableList[Reference]":
         """A list of Reference objects representing tags, heads and remote references.
 
-        :return: IterableList(Reference, ...)"""
+        :return: IterableList(Reference, ...)
+        """
         return Reference.list_items(self)
 
-    # alias for references
+    # Alias for references.
     refs = references
 
-    # alias for heads
+    # Alias for heads.
     branches = heads
 
     @property
     def index(self) -> "IndexFile":
-        """:return: IndexFile representing this repository's index.
-        :note: This property can be expensive, as the returned ``IndexFile`` will be
-         reinitialized. It's recommended to re-use the object."""
+        """
+        :return: :class:`IndexFile <git.index.base.IndexFile>` representing this
+            repository's index.
+
+        :note: This property can be expensive, as the returned
+            :class:`IndexFile <git.index.base.IndexFile>` will be reinitialized.
+            It is recommended to reuse the object.
+        """
         return IndexFile(self)
 
     @property
@@ -401,14 +416,17 @@ def head(self) -> "HEAD":
 
     @property
     def remotes(self) -> "IterableList[Remote]":
-        """A list of Remote objects allowing to access and manipulate remotes
+        """A list of Remote objects allowing to access and manipulate remotes.
 
-        :return: ``git.IterableList(Remote, ...)``"""
+        :return: ``git.IterableList(Remote, ...)``
+        """
         return Remote.list_items(self)
 
     def remote(self, name: str = "origin") -> "Remote":
         """:return: Remote with the specified name
-        :raise ValueError:  if no remote with such a name exists"""
+
+        :raise ValueError: If no remote with such a name exists
+        """
         r = Remote(self, name)
         if not r.exists():
             raise ValueError("Remote named '%s' didn't exist" % name)
@@ -420,12 +438,15 @@ def remote(self, name: str = "origin") -> "Remote":
     def submodules(self) -> "IterableList[Submodule]":
         """
         :return: git.IterableList(Submodule, ...) of direct submodules
-            available from the current head"""
+            available from the current head
+        """
         return Submodule.list_items(self)
 
     def submodule(self, name: str) -> "Submodule":
         """:return: Submodule with the given name
-        :raise ValueError: If no such submodule exists"""
+
+        :raise ValueError: If no such submodule exists
+        """
         try:
             return self.submodules[name]
         except IndexError as e:
@@ -433,38 +454,47 @@ def submodule(self, name: str) -> "Submodule":
         # END exception handling
 
     def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
-        """Create a new submodule
+        """Create a new submodule.
 
         :note: See the documentation of Submodule.add for a description of the
-            applicable parameters
-        :return: created submodules"""
+            applicable parameters.
+
+        :return: The created submodules.
+        """
         return Submodule.add(self, *args, **kwargs)
 
     def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
         """An iterator yielding Submodule instances, see Traversable interface
-        for a description of args and kwargs
+        for a description of args and kwargs.
 
-        :return: Iterator"""
+        :return: Iterator
+        """
         return RootModule(self).traverse(*args, **kwargs)
 
     def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
         """Update the submodules, keeping the repository consistent as it will
-        take the previous state into consideration. For more information, please
-        see the documentation of RootModule.update"""
+        take the previous state into consideration.
+
+        :note: For more information, please see the documentation of
+            :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
+        """
         return RootModule(self).update(*args, **kwargs)
 
     # }END submodules
 
     @property
     def tags(self) -> "IterableList[TagReference]":
-        """A list of ``Tag`` objects that are available in this repo
+        """A list of ``Tag`` objects that are available in this repo.
 
-        :return: ``git.IterableList(TagReference, ...)``"""
+        :return: ``git.IterableList(TagReference, ...)``
+        """
         return TagReference.list_items(self)
 
     def tag(self, path: PathLike) -> TagReference:
         """:return: TagReference Object, reference pointing to a Commit or Tag
-        :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5"""
+
+        :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5
+        """
         full_path = self._to_full_tag_path(path)
         return TagReference(self, full_path)
 
@@ -486,15 +516,19 @@ def create_head(
         logmsg: Optional[str] = None,
     ) -> "Head":
         """Create a new head within the repository.
-        For more documentation, please see the Head.create method.
 
-        :return: newly created Head Reference"""
+        :note: For more documentation, please see the
+            :meth:`Head.create <git.refs.head.Head.create>` method.
+
+        :return: Newly created :class:`Head <git.refs.head.Head>` Reference
+        """
         return Head.create(self, path, commit, logmsg, force)
 
     def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
-        """Delete the given heads
+        """Delete the given heads.
 
-        :param kwargs: Additional keyword arguments to be passed to git-branch"""
+        :param kwargs: Additional keyword arguments to be passed to git-branch
+        """
         return Head.delete(self, *heads, **kwargs)
 
     def create_tag(
@@ -506,9 +540,12 @@ def create_tag(
         **kwargs: Any,
     ) -> TagReference:
         """Create a new tag reference.
-        For more documentation, please see the TagReference.create method.
 
-        :return: TagReference object"""
+        :note: For more documentation, please see the
+            :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
+
+        :return: :class:`TagReference <git.refs.tag.TagReference>` object
+        """
         return TagReference.create(self, path, ref, message, force, **kwargs)
 
     def delete_tag(self, *tags: TagReference) -> None:
@@ -518,10 +555,11 @@ def delete_tag(self, *tags: TagReference) -> None:
     def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
         """Create a new remote.
 
-        For more information, please see the documentation of the Remote.create
-        methods
+        For more information, please see the documentation of the
+        :meth:`Remote.create <git.remote.Remote.create>` method.
 
-        :return: Remote reference"""
+        :return: :class:`Remote <git.remote.Remote>` reference
+        """
         return Remote.create(self, name, url, **kwargs)
 
     def delete_remote(self, remote: "Remote") -> str:
@@ -531,8 +569,8 @@ def delete_remote(self, remote: "Remote") -> str:
     def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str:
         if git_dir is None:
             git_dir = self.git_dir
-        # we do not support an absolute path of the gitconfig on windows ,
-        # use the global config instead
+        # We do not support an absolute path of the gitconfig on Windows.
+        # Use the global config instead.
         if is_win and config_level == "system":
             config_level = "global"
 
@@ -561,17 +599,20 @@ def config_reader(
     ) -> GitConfigParser:
         """
         :return:
-            GitConfigParser allowing to read the full git configuration, but not to write it
+            :class:`GitConfigParser <git.config.GitConfigParser>` allowing to read the
+            full git configuration, but not to write it.
 
             The configuration will include values from the system, user and repository
             configuration files.
 
         :param config_level:
-            For possible values, see config_writer method
-            If None, all applicable levels will be used. Specify a level in case
-            you know which file you wish to read to prevent reading multiple files.
-        :note: On windows, system configuration cannot currently be read as the path is
-            unknown, instead the global path will be used."""
+            For possible values, see the :meth:`config_writer` method. If None, all
+            applicable levels will be used. Specify a level in case you know which file
+            you wish to read to prevent reading multiple files.
+
+        :note: On Windows, system configuration cannot currently be read as the path is
+            unknown, instead the global path will be used.
+        """
         return self._config_reader(config_level=config_level)
 
     def _config_reader(
@@ -592,23 +633,26 @@ def _config_reader(
     def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
         """
         :return:
-            GitConfigParser allowing to write values of the specified configuration file level.
-            Config writers should be retrieved, used to change the configuration, and written
-            right away as they will lock the configuration file in question and prevent other's
-            to write it.
+            A :class:`GitConfigParser <git.config.GitConfigParser>` allowing to write
+            values of the specified configuration file level. Config writers should be
+            retrieved, used to change the configuration, and written right away as they
+            will lock the configuration file in question and prevent other's to write
+            it.
 
         :param config_level:
-            One of the following values
-            system = system wide configuration file
-            global = user level configuration file
-            repository = configuration file for this repository only"""
+            One of the following values:
+
+            * ``"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, merge_includes=False)
 
     def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
-        """The Commit object for the specified revision
+        """The Commit object for the specified revision.
 
         :param rev: revision specifier, see git-rev-parse for viable options.
-        :return: ``git.Commit``
+        :return: :class:`git.Commit <git.objects.commit.Commit>`
         """
         if rev is None:
             return self.head.commit
@@ -616,22 +660,27 @@ def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
 
     def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
         """:return: Iterator yielding Tree objects
-        :note: Takes all arguments known to iter_commits method"""
+
+        :note: Accepts all arguments known to the :meth:`iter_commits` method.
+        """
         return (c.tree for c in self.iter_commits(*args, **kwargs))
 
     def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
-        """The Tree object for the given treeish revision
+        """The Tree object for the given tree-ish revision.
+
         Examples::
 
               repo.tree(repo.heads[0])
 
-        :param rev: is a revision pointing to a Treeish ( being a commit or tree )
+        :param rev: is a revision pointing to a Treeish (being a commit or tree)
+
         :return: ``git.Tree``
 
         :note:
             If you need a non-root level tree, find it by iterating the root tree. Otherwise
             it cannot know about its path relative to the repository root and subsequent
-            operations might have unexpected results."""
+            operations might have unexpected results.
+        """
         if rev is None:
             return self.head.commit.tree
         return self.rev_parse(str(rev) + "^{tree}")
@@ -642,37 +691,40 @@ def iter_commits(
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator[Commit]:
-        """A list of Commit objects representing the history of a given ref/commit
+        """A list of Commit objects representing the history of a given ref/commit.
 
         :param rev:
-            revision specifier, see git-rev-parse for viable options.
+            Revision specifier, see git-rev-parse for viable options.
             If None, the active branch will be used.
 
         :param paths:
-            is an optional path or a list of paths; if set only commits that include the path
-            or paths will be returned
+            An optional path or a list of paths; if set only commits that include the
+            path or paths will be returned
 
         :param kwargs:
-            Arguments to be passed to git-rev-list - common ones are
-            max_count and skip
+            Arguments to be passed to git-rev-list - common ones are max_count and skip.
 
-        :note: to receive only commits between two named revisions, use the
-            "revA...revB" revision specifier
+        :note: To receive only commits between two named revisions, use the
+            ``"revA...revB"`` revision specifier.
 
-        :return: ``git.Commit[]``"""
+        :return: ``git.Commit[]``
+        """
         if rev is None:
             rev = self.head.commit
 
         return Commit.iter_items(self, rev, paths, **kwargs)
 
     def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
-        """Find the closest common ancestor for the given revision (e.g. Commits, Tags, References, etc)
+        """Find the closest common ancestor for the given revision (Commits, Tags, References, etc.).
 
         :param rev: At least two revs to find the common ancestor for.
-        :param kwargs: Additional arguments to be passed to the repo.git.merge_base() command which does all the work.
-        :return: A list of Commit objects. If --all was not specified as kwarg, the list will have at max one Commit,
-            or is empty if no common merge base exists.
-        :raises ValueError: If not at least two revs are provided
+        :param kwargs: Additional arguments to be passed to the
+            ``repo.git.merge_base()`` command which does all the work.
+        :return: A list of :class:`Commit <git.objects.commit.Commit>` objects. If
+            ``--all`` was not passed as a keyword argument, the list will have at max
+            one :class:`Commit <git.objects.commit.Commit>`, or is empty if no common
+            merge base exists.
+        :raises ValueError: If not at least two revs are provided.
         """
         if len(rev) < 2:
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
@@ -697,7 +749,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
         return res
 
     def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool:
-        """Check if a commit is an ancestor of another
+        """Check if a commit is an ancestor of another.
 
         :param ancestor_rev: Rev which should be an ancestor
         :param rev: Rev to test against ancestor_rev
@@ -754,9 +806,10 @@ def _set_daemon_export(self, value: object) -> None:
     del _set_daemon_export
 
     def _get_alternates(self) -> List[str]:
-        """The list of alternates for this repo from which objects can be retrieved
+        """The list of alternates for this repo from which objects can be retrieved.
 
-        :return: list of strings being pathnames of alternates"""
+        :return: List of strings being pathnames of alternates
+        """
         if self.git_dir:
             alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
 
@@ -767,16 +820,18 @@ def _get_alternates(self) -> List[str]:
         return []
 
     def _set_alternates(self, alts: List[str]) -> None:
-        """Sets the alternates
+        """Sets the alternates.
 
         :param alts:
             is the array of string paths representing the alternates at which
             git should look for objects, i.e. /home/user/repo/.git/objects
 
         :raise NoSuchPathError:
+
         :note:
             The method does not check for the existence of the paths in alts
-            as the caller is responsible."""
+            as the caller is responsible.
+        """
         alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
         if not alts:
             if osp.isfile(alternates_path):
@@ -801,27 +856,28 @@ def is_dirty(
     ) -> bool:
         """
         :return:
-            ``True``, the repository is considered dirty. By default it will react
+            ``True`` if the repository is considered dirty. By default it will react
             like a git-status without untracked files, hence it is dirty if the
-            index or the working copy have changes."""
+            index or the working copy have changes.
+        """
         if self._bare:
             # Bare repositories with no associated working directory are
             # always considered to be clean.
             return False
 
-        # start from the one which is fastest to evaluate
+        # Start from the one which is fastest to evaluate.
         default_args = ["--abbrev=40", "--full-index", "--raw"]
         if not submodules:
             default_args.append("--ignore-submodules")
         if path:
             default_args.extend(["--", str(path)])
         if index:
-            # diff index against HEAD
+            # diff index against HEAD.
             if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
                 return True
         # END index handling
         if working_tree:
-            # diff index against working tree
+            # diff index against working tree.
             if len(self.git.diff(*default_args)):
                 return True
         # END working tree handling
@@ -841,14 +897,15 @@ def untracked_files(self) -> List[str]:
             are relative to the current working directory of the git command.
 
         :note:
-            ignored files will not appear here, i.e. files mentioned in .gitignore
+            Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
         :note:
-            This property is expensive, as no cache is involved. To process the result, please
-            consider caching it yourself."""
+            This property is expensive, as no cache is involved. To process the result,
+            please consider caching it yourself.
+        """
         return self._get_untracked_files()
 
     def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
-        # make sure we get all files, not only untracked directories
+        # Make sure we get all files, not only untracked directories.
         proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs)
         # Untracked files prefix in porcelain mode
         prefix = "?? "
@@ -868,11 +925,13 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
         return untracked_files
 
     def ignored(self, *paths: PathLike) -> List[str]:
-        """Checks if paths are ignored via .gitignore
-        Doing so using the "git check-ignore" method.
+        """Checks if paths are ignored via .gitignore.
+
+        This does so using the ``git check-ignore`` method.
 
         :param paths: List of paths to check whether they are ignored or not
-        :return: subset of those paths which are ignored
+
+        :return: Subset of those paths which are ignored
         """
         try:
             proc: str = self.git.check_ignore(*paths)
@@ -892,23 +951,25 @@ def active_branch(self) -> Head:
         """The name of the currently active branch.
 
         :raises	TypeError: If HEAD is detached
-        :return: Head to the active branch"""
+        :return: Head to the active branch
+        """
         # reveal_type(self.head.reference)  # => Reference
         return self.head.reference
 
     def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
         """Iterator for blame information for the given file at the given revision.
 
-        Unlike .blame(), this does not return the actual file's contents, only
-        a stream of BlameEntry tuples.
+        Unlike :meth:`blame`, this does not return the actual file's contents, only a
+        stream of :class:`BlameEntry` tuples.
 
-        :param rev: revision specifier, see git-rev-parse for viable options.
-        :return: lazy iterator of BlameEntry tuples, where the commit
-                 indicates the commit to blame for the line, and range
-                 indicates a span of line numbers in the resulting file.
+        :param rev: Revision specifier, see git-rev-parse for viable options.
 
-        If you combine all line number ranges outputted by this command, you
-        should get a continuous range spanning all line numbers in the file.
+        :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates
+            the commit to blame for the line, and range indicates a span of line numbers
+            in the resulting file.
+
+        If you combine all line number ranges outputted by this command, you should get
+        a continuous range spanning all line numbers in the file.
         """
 
         data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
@@ -917,7 +978,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
         stream = (line for line in data.split(b"\n") if line)
         while True:
             try:
-                line = next(stream)  # when exhausted, causes a StopIteration, terminating this function
+                line = next(stream)  # When exhausted, causes a StopIteration, terminating this function.
             except StopIteration:
                 return
             split_line = line.split()
@@ -927,7 +988,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
             orig_lineno = int(orig_lineno_b)
             if hexsha not in commits:
                 # Now read the next few lines and build up a dict of properties
-                # for this commit
+                # for this commit.
                 props: Dict[bytes, bytes] = {}
                 while True:
                     try:
@@ -936,13 +997,13 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
                         return
                     if line == b"boundary":
                         # "boundary" indicates a root commit and occurs
-                        # instead of the "previous" tag
+                        # instead of the "previous" tag.
                         continue
 
                     tag, value = line.split(b" ", 1)
                     props[tag] = value
                     if tag == b"filename":
-                        # "filename" formally terminates the entry for --incremental
+                        # "filename" formally terminates the entry for --incremental.
                         orig_filename = value
                         break
 
@@ -963,10 +1024,10 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
                 commits[hexsha] = c
             else:
                 # Discard all lines until we find "filename" which is
-                # guaranteed to be the last line
+                # guaranteed to be the last line.
                 while True:
                     try:
-                        line = next(stream)  # will fail if we reach the EOF unexpectedly
+                        line = next(stream)  # Will fail if we reach the EOF unexpectedly.
                     except StopIteration:
                         return
                     tag, value = line.split(b" ", 1)
@@ -991,12 +1052,15 @@ def blame(
     ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
         """The blame information for the given file at the given revision.
 
-        :param rev: revision specifier, see git-rev-parse for viable options.
+        :param rev: Revision specifier, see git-rev-parse for viable options.
+
         :return:
             list: [git.Commit, list: [<line>]]
+
             A list of lists associating a Commit object with a list of lines that
             changed within the given commit. The Commit objects will be given in order
-            of appearance."""
+            of appearance.
+        """
         if incremental:
             return self.blame_incremental(rev, file, **kwargs)
         rev_opts = rev_opts or []
@@ -1028,9 +1092,9 @@ class InfoTD(TypedDict, total=False):
                 is_binary = True
             else:
                 # As we don't have an idea when the binary data ends, as it could contain multiple newlines
-                # in the process. So we rely on being able to decode to tell us what is is.
+                # in the process. So we rely on being able to decode to tell us what it is.
                 # This can absolutely fail even on text files, but even if it does, we should be fine treating it
-                # as binary instead
+                # as binary instead.
                 parts = self.re_whitespace.split(line_str, 1)
                 firstpart = parts[0]
                 is_binary = False
@@ -1133,32 +1197,32 @@ def init(
         expand_vars: bool = True,
         **kwargs: Any,
     ) -> "Repo":
-        """Initialize a git repository at the given path if specified
+        """Initialize a git repository at the given path if specified.
 
         :param path:
-            is the full path to the repo (traditionally ends with /<name>.git)
-            or None in which case the repository will be created in the current
-            working directory
+            The full path to the repo (traditionally ends with /<name>.git) or None in
+            which case the repository will be created in the current working directory
 
         :param mkdir:
-            if specified will create the repository directory if it doesn't
-            already exists. Creates the directory with a mode=0755.
-            Only effective if a path is explicitly given
+            If specified, will create the repository directory if it doesn't
+            already exist. Creates the directory with a mode=0755.
+            Only effective if a path is explicitly given.
 
         :param odbt:
             Object DataBase type - a type which is constructed by providing
             the directory containing the database objects, i.e. .git/objects.
-            It will be used to access all object data
+            It will be used to access all object data.
 
         :param expand_vars:
-            if specified, environment variables will not be escaped. This
+            If specified, environment variables will not be escaped. This
             can lead to information disclosure, allowing attackers to
-            access the contents of environment variables
+            access the contents of environment variables.
 
         :param kwargs:
-            keyword arguments serving as additional options to the git-init command
+            Keyword arguments serving as additional options to the git-init command.
 
-        :return: ``git.Repo`` (the newly created repo)"""
+        :return: ``git.Repo`` (the newly created repo)
+        """
         if path:
             path = expand_path(path, expand_vars)
         if mkdir and path and not osp.exists(path):
@@ -1184,7 +1248,7 @@ def _clone(
     ) -> "Repo":
         odbt = kwargs.pop("odbt", odb_default_type)
 
-        # when pathlib.Path or other classbased path is passed
+        # When pathlib.Path or other classbased path is passed
         if not isinstance(path, str):
             path = str(path)
 
@@ -1236,21 +1300,21 @@ def _clone(
             log.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
             finalize_process(proc, stderr=stderr)
 
-        # our git command could have a different working dir than our actual
-        # environment, hence we prepend its working dir if required
+        # Our git command could have a different working dir than our actual
+        # environment, hence we prepend its working dir if required.
         if not osp.isabs(path):
             path = osp.join(git._working_dir, path) if git._working_dir is not None else path
 
         repo = cls(path, odbt=odbt)
 
-        # retain env values that were passed to _clone()
+        # Retain env values that were passed to _clone().
         repo.git.update_environment(**git.environment())
 
-        # adjust remotes - there may be operating systems which use backslashes,
+        # Adjust remotes - there may be operating systems which use backslashes,
         # These might be given as initial paths, but when handling the config file
         # that contains the remote from which we were clones, git stops liking it
         # as it will escape the backslashes. Hence we undo the escaping just to be
-        # sure
+        # sure.
         if repo.remotes:
             with repo.remotes[0].config_writer as writer:
                 writer.set_value("url", Git.polish_url(repo.remotes[0].url))
@@ -1268,20 +1332,22 @@ def clone(
     ) -> "Repo":
         """Create a clone from this repository.
 
-        :param path: is the full path of the new repo (traditionally ends with ./<name>.git).
-        :param progress: See 'git.remote.Remote.push'.
-        :param multi_options: A list of Clone options that can be provided multiple times.  One
-            option per list item which is passed exactly as specified to clone.
-            For example ['--config core.filemode=false', '--config core.ignorecase',
+        :param path: The full path of the new repo (traditionally ends with
+            ``./<name>.git``).
+        :param progress: See :meth:`git.remote.Remote.push`.
+        :param multi_options: A list of Clone options that can be provided multiple times.
+            One option per list item which is passed exactly as specified to clone.
+            For example: ['--config core.filemode=false', '--config core.ignorecase',
             '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
+        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
         :param kwargs:
             * odbt = ObjectDatabase Type, allowing to determine the object database
-              implementation used by the returned Repo instance
-            * All remaining keyword arguments are given to the git-clone command
+              implementation used by the returned Repo instance.
+            * All remaining keyword arguments are given to the git-clone command.
 
-        :return: ``git.Repo`` (the newly cloned repo)"""
+        :return: :class:`Repo` (the newly cloned repo)
+        """
         return self._clone(
             self.git,
             self.common_dir,
@@ -1306,22 +1372,32 @@ def clone_from(
         allow_unsafe_options: bool = False,
         **kwargs: Any,
     ) -> "Repo":
-        """Create a clone from the given URL
+        """Create a clone from the given URL.
+
+        :param url: Valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
+
+        :param to_path: Path to which the repository should be cloned to.
+
+        :param progress: See :meth:`git.remote.Remote.push`.
 
-        :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
-        :param to_path: Path to which the repository should be cloned to
-        :param progress: See 'git.remote.Remote.push'.
         :param env: Optional dictionary containing the desired environment variables.
+
             Note: Provided variables will be used to update the execution
             environment for `git`. If some variable is not specified in `env`
             and is defined in `os.environ`, value from `os.environ` will be used.
             If you want to unset some variable, consider providing empty string
             as its value.
-        :param multi_options: See ``clone`` method
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :param kwargs: see the ``clone`` method
-        :return: Repo instance pointing to the cloned directory"""
+
+        :param multi_options: See :meth:`clone` method.
+
+        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+
+        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
+
+        :param kwargs: See the :meth:`clone` method.
+
+        :return: :class:`Repo` instance pointing to the cloned directory.
+        """
         git = cls.GitCommandWrapperType(os.getcwd())
         if env is not None:
             git.update_environment(**env)
@@ -1346,18 +1422,23 @@ def archive(
     ) -> Repo:
         """Archive the tree at the given revision.
 
-        :param ostream: file compatible stream object to which the archive will be written as bytes
-        :param treeish: is the treeish name/id, defaults to active branch
-        :param prefix: is the optional prefix to prepend to each filename in the archive
-        :param kwargs: Additional arguments passed to git-archive
+        :param ostream: file compatible stream object to which the archive will be written as bytes.
+
+        :param treeish: is the treeish name/id, defaults to active branch.
+
+        :param prefix: is the optional prefix to prepend to each filename in the archive.
+
+        :param kwargs: Additional arguments passed to git-archive:
 
             * Use the 'format' argument to define the kind of format. Use
               specialized ostreams to write any format supported by python.
             * You may specify the special **path** keyword, which may either be a repository-relative
               path to a directory or file to place into the archive, or a list or tuple of multiple paths.
 
-        :raise GitCommandError: in case something went wrong
-        :return: self"""
+        :raise GitCommandError: If something went wrong.
+
+        :return: self
+        """
         if treeish is None:
             treeish = self.head.commit
         if prefix and "prefix" not in kwargs:
@@ -1374,7 +1455,8 @@ def archive(
     def has_separate_working_tree(self) -> bool:
         """
         :return: True if our git_dir is not at the root of our working_tree_dir, but a .git file with a
-            platform agnositic symbolic link. Our git_dir will be wherever the .git file points to
+            platform agnositic symbolic link. Our git_dir will be wherever the .git file points to.
+
         :note: bare repositories will always return False here
         """
         if self.bare:
@@ -1382,7 +1464,7 @@ def has_separate_working_tree(self) -> bool:
         if self.working_tree_dir:
             return osp.isfile(osp.join(self.working_tree_dir, ".git"))
         else:
-            return False  # or raise Error?
+            return False  # Or raise Error?
 
     rev_parse = rev_parse
 
@@ -1394,7 +1476,7 @@ def currently_rebasing_on(self) -> Commit | None:
         """
         :return: The commit which is currently being replayed while rebasing.
 
-        None if we are not currently rebasing.
+            None if we are not currently rebasing.
         """
         if self.git_dir:
             rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
diff --git a/git/repo/fun.py b/git/repo/fun.py
index ae35aa81e..f0bb4cd1f 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -1,5 +1,7 @@
-"""Package with general repository related functions"""
+"""Module with general repository-related functions."""
+
 from __future__ import annotations
+
 import os
 import stat
 from pathlib import Path
diff --git a/git/types.py b/git/types.py
index 21276b5f1..22bb91c16 100644
--- a/git/types.py
+++ b/git/types.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 # flake8: noqa
 
 import os
@@ -75,9 +76,11 @@
 
 def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None:
     """For use in exhaustive checking of literal or Enum in if/else chain.
+
     Should only be reached if all members not handled OR attempt to pass non-members through chain.
 
     If all members handled, type is Empty. Otherwise, will cause mypy error.
+
     If non-members given, should cause mypy error at variable creation.
 
     If raise_error is True, will also raise AssertionError or the Exception passed to exc.
diff --git a/git/util.py b/git/util.py
index ccadb5881..d6a20b3e3 100644
--- a/git/util.py
+++ b/git/util.py
@@ -144,7 +144,7 @@ def _read_env_flag(name: str, default: bool) -> bool:
 
 def unbare_repo(func: Callable[..., T]) -> Callable[..., T]:
     """Methods with this decorator raise :class:`.exc.InvalidGitRepositoryError` if they
-    encounter a bare repository"""
+    encounter a bare repository."""
 
     from .exc import InvalidGitRepositoryError
 
@@ -164,8 +164,9 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T:
 def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
     """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."""
+    This is similar to :func:`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:
@@ -191,8 +192,10 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]:
 def rmtree(path: PathLike) -> None:
     """Remove the given directory tree recursively.
 
-    :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that
-        couldn't be deleted are read-only. Windows will not remove them in that case."""
+    :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files
+        that couldn't be deleted are read-only. Windows will not remove them in that
+        case.
+    """
 
     def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
         """Callback for :func:`shutil.rmtree`. Works either as ``onexc`` or ``onerror``."""
@@ -224,9 +227,10 @@ def rmfile(path: PathLike) -> None:
 
 def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * 1024) -> int:
     """Copy all data from the source stream into the destination stream in chunks
-    of size chunk_size
+    of size chunk_size.
 
-    :return: amount of bytes written"""
+    :return: Number of bytes written
+    """
     br = 0
     while True:
         chunk = source.read(chunk_size)
@@ -240,7 +244,7 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
 
 def join_path(a: PathLike, *p: PathLike) -> PathLike:
     R"""Join path tokens together similar to osp.join, but always use
-    '/' instead of possibly '\' on windows."""
+    '/' instead of possibly '\' on Windows."""
     path = str(a)
     for b in p:
         b = str(b)
@@ -269,7 +273,7 @@ def to_native_path_linux(path: PathLike) -> str:
     __all__.append("to_native_path_windows")
     to_native_path = to_native_path_windows
 else:
-    # no need for any work on linux
+    # No need for any work on Linux.
     def to_native_path_linux(path: PathLike) -> str:
         return str(path)
 
@@ -277,19 +281,22 @@ def to_native_path_linux(path: PathLike) -> str:
 
 
 def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
-    R"""
-    As join path, but makes sure an OS native path is returned. This is only
-        needed to play it safe on my dear windows and to assure nice paths that only
-        use '\'"""
+    R"""Like join_path, but makes sure an OS native path is returned.
+
+    This is only needed to play it safe on Windows and to assure nice paths that only
+    use '\'.
+    """
     return to_native_path(join_path(a, *p))
 
 
 def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
-    """Assure that the directory pointed to by path exists.
+    """Make sure that the directory pointed to by path exists.
+
+    :param is_file: If True, ``path`` is assumed to be a file and handled correctly.
+        Otherwise it must be a directory.
 
-    :param is_file: If True, path is assumed to be a file and handled correctly.
-        Otherwise it must be a directory
-    :return: True if the directory was created, False if it already existed"""
+    :return: True if the directory was created, False if it already existed.
+    """
     if is_file:
         path = osp.dirname(path)
     # END handle file
@@ -365,9 +372,9 @@ def _cygexpath(drive: Optional[str], path: str) -> str:
 
 
 def cygpath(path: str) -> str:
-    """Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment."""
-    path = str(path)  # ensure is str and not AnyPath.
-    # Fix to use Paths when 3.5 dropped. or to be just str if only for urls?
+    """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
+    path = str(path)  # Ensure is str and not AnyPath.
+    # Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs?
     if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")):
         for regex, parser, recurse in _cygpath_parsers:
             match = regex.match(path)
@@ -477,11 +484,10 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[
 
 
 def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
-    """
-    Parse any command line argument and if on of the element is an URL with a
+    """Parse any command line argument and if one of the elements is an URL with a
     username and/or password, replace them by stars (in-place).
 
-    If nothing found just returns the command line as-is.
+    If nothing is found, this just returns the command line as-is.
 
     This should be used for every log line that print a command line, as well as
     exception messages.
@@ -491,7 +497,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
         new_cmdline.append(to_parse)
         try:
             url = urlsplit(to_parse)
-            # Remove password from the URL if present
+            # Remove password from the URL if present.
             if url.password is None and url.username is None:
                 continue
 
@@ -501,7 +507,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
                 url = url._replace(netloc=url.netloc.replace(url.username, "*****"))
             new_cmdline[index] = urlunsplit(url)
         except ValueError:
-            # This is not a valid URL
+            # This is not a valid URL.
             continue
     return new_cmdline
 
@@ -555,14 +561,15 @@ def _parse_progress_line(self, line: AnyStr) -> None:
         or git-fetch.
 
         - Lines that do not contain progress info are stored in :attr:`other_lines`.
-        - Lines that seem to contain an error (i.e. start with error: or fatal:) are stored
-            in :attr:`error_lines`."""
+        - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``)
+          are stored in :attr:`error_lines`.
+        """
         # handle
         # Counting objects: 4, done.
         # Compressing objects:  50% (1/2)
         # Compressing objects: 100% (2/2)
         # Compressing objects: 100% (2/2), done.
-        if isinstance(line, bytes):  # mypy argues about ternary assignment
+        if isinstance(line, bytes):  # mypy argues about ternary assignment.
             line_str = line.decode("utf-8")
         else:
             line_str = line
@@ -572,14 +579,14 @@ def _parse_progress_line(self, line: AnyStr) -> None:
             self.error_lines.append(self._cur_line)
             return
 
-        # find escape characters and cut them away - regex will not work with
-        # them as they are non-ascii. As git might expect a tty, it will send them
+        # Find escape characters and cut them away - regex will not work with
+        # them as they are non-ASCII. As git might expect a tty, it will send them.
         last_valid_index = None
         for i, c in enumerate(reversed(line_str)):
             if ord(c) < 32:
                 # its a slice index
                 last_valid_index = -i - 1
-            # END character was non-ascii
+            # END character was non-ASCII
         # END for each character in line
         if last_valid_index is not None:
             line_str = line_str[:last_valid_index]
@@ -600,7 +607,7 @@ def _parse_progress_line(self, line: AnyStr) -> None:
         op_code = 0
         _remote, op_name, _percent, cur_count, max_count, message = match.groups()
 
-        # get operation id
+        # Get operation ID.
         if op_name == "Counting objects":
             op_code |= self.COUNTING
         elif op_name == "Compressing objects":
@@ -616,7 +623,7 @@ def _parse_progress_line(self, line: AnyStr) -> None:
         elif op_name == "Checking out files":
             op_code |= self.CHECKING_OUT
         else:
-            # Note: On windows it can happen that partial lines are sent
+            # Note: On Windows it can happen that partial lines are sent.
             # Hence we get something like "CompreReceiving objects", which is
             # a blend of "Compressing objects" and "Receiving objects".
             # This can't really be prevented, so we drop the line verbosely
@@ -624,11 +631,11 @@ def _parse_progress_line(self, line: AnyStr) -> None:
             # commands at some point.
             self.line_dropped(line_str)
             # Note: Don't add this line to the other lines, as we have to silently
-            # drop it
+            # drop it.
             return None
         # END handle op code
 
-        # figure out stage
+        # Figure out stage.
         if op_code not in self._seen_ops:
             self._seen_ops.append(op_code)
             op_code |= self.BEGIN
@@ -655,8 +662,9 @@ def _parse_progress_line(self, line: AnyStr) -> None:
     def new_message_handler(self) -> Callable[[str], None]:
         """
         :return:
-            a progress handler suitable for handle_process_output(), passing lines on to this Progress
-            handler in a suitable format"""
+            A progress handler suitable for handle_process_output(), passing lines on to
+            this Progress handler in a suitable format
+        """
 
         def handler(line: AnyStr) -> None:
             return self._parse_progress_line(line.rstrip())
@@ -675,7 +683,7 @@ def update(
         max_count: Union[str, float, None] = None,
         message: str = "",
     ) -> None:
-        """Called whenever the progress changes
+        """Called whenever the progress changes.
 
         :param op_code:
             Integer allowing to be compared against Operation IDs and stage IDs.
@@ -683,11 +691,12 @@ def update(
             Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation
             ID as well as END. It may be that BEGIN and END are set at once in case only
             one progress message was emitted due to the speed of the operation.
-            Between BEGIN and END, none of these flags will be set
+            Between BEGIN and END, none of these flags will be set.
 
             Operation IDs are all held within the OP_MASK. Only one Operation ID will
             be active per call.
-        :param cur_count: Current absolute count of items
+
+        :param cur_count: Current absolute count of items.
 
         :param max_count:
             The maximum count of items we expect. It may be None in case there is
@@ -697,12 +706,13 @@ def update(
             In case of the 'WRITING' operation, it contains the amount of bytes
             transferred. It may possibly be used for other purposes as well.
 
-        You may read the contents of the current line in self._cur_line"""
+        You may read the contents of the current line in ``self._cur_line``.
+        """
         pass
 
 
 class CallableRemoteProgress(RemoteProgress):
-    """An implementation forwarding updates to any callable"""
+    """An implementation forwarding updates to any callable."""
 
     __slots__ = "_callable"
 
@@ -724,7 +734,7 @@ class Actor(object):
     name_email_regex = re.compile(r"(.*) <(.*?)>")
 
     # ENVIRONMENT VARIABLES
-    # read when creating new commits
+    # These are read when creating new commits.
     env_author_name = "GIT_AUTHOR_NAME"
     env_author_email = "GIT_AUTHOR_EMAIL"
     env_committer_name = "GIT_COMMITTER_NAME"
@@ -758,11 +768,13 @@ def __repr__(self) -> str:
     @classmethod
     def _from_string(cls, string: str) -> "Actor":
         """Create an Actor from a string.
-        :param string: is the string, which is expected to be in regular git format
 
-                John Doe <jdoe@example.com>
+        :param string: The string, which is expected to be in regular git format::
 
-        :return: Actor"""
+            John Doe <jdoe@example.com>
+
+        :return: Actor
+        """
         m = cls.name_email_regex.search(string)
         if m:
             name, email = m.groups()
@@ -771,7 +783,7 @@ def _from_string(cls, string: str) -> "Actor":
             m = cls.name_only_regex.search(string)
             if m:
                 return Actor(m.group(1), None)
-            # assume best and use the whole string as name
+            # Assume the best and use the whole string as name.
             return Actor(string, None)
             # END special case name
         # END handle name/email matching
@@ -784,7 +796,7 @@ def _main_actor(
         config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None,
     ) -> "Actor":
         actor = Actor("", "")
-        user_id = None  # We use this to avoid multiple calls to getpass.getuser()
+        user_id = None  # We use this to avoid multiple calls to getpass.getuser().
 
         def default_email() -> str:
             nonlocal user_id
@@ -822,20 +834,21 @@ def committer(cls, config_reader: Union[None, "GitConfigParser", "SectionConstra
         :return: Actor instance corresponding to the configured committer. It behaves
             similar to the git implementation, such that the environment will override
             configuration values of config_reader. If no value is set at all, it will be
-            generated
+            generated.
+
         :param config_reader: ConfigReader to use to retrieve the values from in case
-            they are not set in the environment"""
+            they are not set in the environment.
+        """
         return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader)
 
     @classmethod
     def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor":
-        """Same as committer(), but defines the main author. It may be specified in the environment,
-        but defaults to the committer"""
+        """Same as committer(), but defines the main author. It may be specified in the
+        environment, but defaults to the committer."""
         return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
 
 
 class Stats(object):
-
     """
     Represents stat information as presented by git at the end of a merge. It is
     created from the output of a diff operation.
@@ -859,7 +872,8 @@ class Stats(object):
 
     In addition to the items in the stat-dict, it features additional information::
 
-     files = number of changed files as int"""
+     files = number of changed files as int
+    """
 
     __slots__ = ("total", "files")
 
@@ -871,7 +885,8 @@ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]):
     def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
         """Create a Stat object from output retrieved by git-diff.
 
-        :return: git.Stat"""
+        :return: git.Stat
+        """
 
         hsh: HSH_TD = {
             "total": {"insertions": 0, "deletions": 0, "lines": 0, "files": 0},
@@ -895,14 +910,14 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
 
 
 class IndexFileSHA1Writer(object):
-
     """Wrapper around a file-like object that remembers the SHA1 of
     the data written to it. It will write a sha when the stream is closed
     or if the asked for explicitly using write_sha.
 
-    Only useful to the indexfile
+    Only useful to the index file.
 
-    :note: Based on the dulwich project"""
+    :note: Based on the dulwich project.
+    """
 
     __slots__ = ("f", "sha1")
 
@@ -929,13 +944,13 @@ def tell(self) -> int:
 
 
 class LockFile(object):
-
     """Provides methods to obtain, check for, and release a file based lock which
     should be used to handle concurrent access to the same file.
 
     As we are a utility class to be derived from, we only use protected methods.
 
-    Locks will automatically be released on destruction"""
+    Locks will automatically be released on destruction.
+    """
 
     __slots__ = ("_file_path", "_owns_lock")
 
@@ -951,14 +966,18 @@ def _lock_file_path(self) -> str:
         return "%s.lock" % (self._file_path)
 
     def _has_lock(self) -> bool:
-        """:return: True if we have a lock and if the lockfile still exists
-        :raise AssertionError: if our lock-file does not exist"""
+        """
+        :return: True if we have a lock and if the lockfile still exists
+
+        :raise AssertionError: If our lock-file does not exist
+        """
         return self._owns_lock
 
     def _obtain_lock_or_raise(self) -> None:
-        """Create a lock file as flag for other instances, mark our instance as lock-holder
+        """Create a lock file as flag for other instances, mark our instance as lock-holder.
 
-        :raise IOError: if a lock was already present or a lock file could not be written"""
+        :raise IOError: If a lock was already present or a lock file could not be written
+        """
         if self._has_lock():
             return
         lock_file = self._lock_file_path()
@@ -978,15 +997,15 @@ def _obtain_lock_or_raise(self) -> None:
 
     def _obtain_lock(self) -> None:
         """The default implementation will raise if a lock cannot be obtained.
-        Subclasses may override this method to provide a different implementation"""
+        Subclasses may override this method to provide a different implementation."""
         return self._obtain_lock_or_raise()
 
     def _release_lock(self) -> None:
-        """Release our lock if we have one"""
+        """Release our lock if we have one."""
         if not self._has_lock():
             return
 
-        # if someone removed our file beforhand, lets just flag this issue
+        # If someone removed our file beforehand, lets just flag this issue
         # instead of failing, to make it more usable.
         lfp = self._lock_file_path()
         try:
@@ -997,13 +1016,13 @@ def _release_lock(self) -> None:
 
 
 class BlockingLockFile(LockFile):
-
     """The lock file will block until a lock could be obtained, or fail after
     a specified timeout.
 
     :note: If the directory containing the lock was removed, an exception will
         be raised during the blocking period, preventing hangs as the lock
-        can never be obtained."""
+        can never be obtained.
+    """
 
     __slots__ = ("_check_interval", "_max_block_time")
 
@@ -1013,13 +1032,14 @@ def __init__(
         check_interval_s: float = 0.3,
         max_block_time_s: int = sys.maxsize,
     ) -> None:
-        """Configure the instance
+        """Configure the instance.
 
         :param check_interval_s:
             Period of time to sleep until the lock is checked the next time.
-            By default, it waits a nearly unlimited time
+            By default, it waits a nearly unlimited time.
 
-        :param max_block_time_s: Maximum amount of seconds we may lock"""
+        :param max_block_time_s: Maximum amount of seconds we may lock.
+        """
         super(BlockingLockFile, self).__init__(file_path)
         self._check_interval = check_interval_s
         self._max_block_time = max_block_time_s
@@ -1027,7 +1047,9 @@ def __init__(
     def _obtain_lock(self) -> None:
         """This method blocks until it obtained the lock, or raises IOError if
         it ran out of time or if the parent directory was not available anymore.
-        If this method returns, you are guaranteed to own the lock"""
+
+        If this method returns, you are guaranteed to own the lock.
+        """
         starttime = time.time()
         maxtime = starttime + float(self._max_block_time)
         while True:
@@ -1059,7 +1081,6 @@ def _obtain_lock(self) -> None:
 
 
 class IterableList(List[T_IterableObj]):
-
     """
     List of iterable objects allowing to query an object by id or by named index::
 
@@ -1070,13 +1091,14 @@ class IterableList(List[T_IterableObj]):
 
     Iterable parent objects = [Commit, SubModule, Reference, FetchInfo, PushInfo]
     Iterable via inheritance = [Head, TagReference, RemoteReference]
-    ]
+
     It requires an id_attribute name to be set which will be queried from its
     contained items to have a means for comparison.
 
     A prefix can be specified which is to be used in case the id returned by the
     items always contains a prefix that does not matter to the user, so it
-    can be left out."""
+    can be left out.
+    """
 
     __slots__ = ("_id_attr", "_prefix")
 
@@ -1088,7 +1110,7 @@ def __init__(self, id_attr: str, prefix: str = "") -> None:
         self._prefix = prefix
 
     def __contains__(self, attr: object) -> bool:
-        # first try identity match for performance
+        # First try identity match for performance.
         try:
             rval = list.__contains__(self, attr)
             if rval:
@@ -1097,9 +1119,9 @@ def __contains__(self, attr: object) -> bool:
             pass
         # END handle match
 
-        # otherwise make a full name search
+        # Otherwise make a full name search.
         try:
-            getattr(self, cast(str, attr))  # use cast to silence mypy
+            getattr(self, cast(str, attr))  # Use cast to silence mypy.
             return True
         except (AttributeError, TypeError):
             return False
@@ -1148,7 +1170,7 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None:
 
 
 class IterableClassWatcher(type):
-    """Metaclass that watches"""
+    """Metaclass that watches."""
 
     def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
         for base in bases:
@@ -1164,24 +1186,26 @@ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
 
 
 class Iterable(metaclass=IterableClassWatcher):
-
     """Defines an interface for iterable items which is to assure a uniform
-    way to retrieve and iterate items within the git repository"""
+    way to retrieve and iterate items within the git repository."""
 
     __slots__ = ()
+
     _id_attribute_ = "attribute that most suitably identifies your instance"
 
     @classmethod
     def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
         """
         Deprecated, use IterableObj instead.
+
         Find all items of this type - subclasses can specify args and kwargs differently.
         If no args are given, subclasses are obliged to return all items if no additional
         arguments arg given.
 
         :note: Favor the iter_items method as it will
 
-        :return: list(Item,...) list of item instances"""
+        :return: list(Item,...) list of item instances
+        """
         out_list: Any = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
@@ -1189,19 +1213,23 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
     @classmethod
     def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
         # return typed to be compatible with subtypes e.g. Remote
-        """For more information about the arguments, see list_items
-        :return:  iterator yielding Items"""
+        """For more information about the arguments, see list_items.
+
+        :return: Iterator yielding Items
+        """
         raise NotImplementedError("To be implemented by Subclass")
 
 
 @runtime_checkable
 class IterableObj(Protocol):
     """Defines an interface for iterable items which is to assure a uniform
-    way to retrieve and iterate items within the git repository
+    way to retrieve and iterate items within the git repository.
 
-    Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]"""
+    Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]
+    """
 
     __slots__ = ()
+
     _id_attribute_: str
 
     @classmethod
@@ -1213,7 +1241,8 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
 
         :note: Favor the iter_items method as it will
 
-        :return: list(Item,...) list of item instances"""
+        :return: list(Item,...) list of item instances
+        """
         out_list: IterableList = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
@@ -1221,9 +1250,11 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
     @classmethod
     @abstractmethod
     def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:  # Iterator[T_IterableObj]:
-        # return typed to be compatible with subtypes e.g. Remote
-        """For more information about the arguments, see list_items
-        :return:  iterator yielding Items"""
+        # Return-typed to be compatible with subtypes e.g. Remote.
+        """For more information about the arguments, see list_items.
+
+        :return: Iterator yielding Items
+        """
         raise NotImplementedError("To be implemented by Subclass")
 
 

From e8343e26f0d03fcdc3492b1412af7156cf17b636 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 11:48:44 -0400
Subject: [PATCH 220/994] Firm up comment about is_win in util.is_cygwin_git

The is_cygwin_git function returns False when we have is_win,
because is_win is not True on Cygwin systems. The existing comment
explained why, but was tentative. The claims are accurate, so this
rewrites the comment to state it more definitively.
---
 git/util.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/git/util.py b/git/util.py
index d6a20b3e3..29cf12f7c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -419,8 +419,7 @@ def is_cygwin_git(git_executable: PathLike) -> bool:
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
     if is_win:
-        # is_win seems to be true only for Windows-native pythons
-        # cygwin has os.name = posix, I think
+        # is_win is only True on native Windows systems. On Cygwin, os.name == "posix".
         return False
 
     if git_executable is None:

From f78587fd9376d3578fa816e8c99f5d8dd657332c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 11:57:50 -0400
Subject: [PATCH 221/994] Remove explicit inheritance from object

Within the git module.

In Python 2, it was necessary to inherit from object to have a
new-style class. In Python 3, all classes are new-style classes,
and inheritance from object is implied.

Usually, removing explicit inheritance from object is only a small
clarity benefit while modernizing Python codebases. However, in
GitPython, the benefit is greater, because it is possible to
confuse object (the root of the Python class hierarchy) with
Object (the root of the GitPython subhierarchy of classes
representing things in the git object model).
---
 git/cmd.py           |  4 ++--
 git/diff.py          |  6 +++---
 git/index/typ.py     |  2 +-
 git/index/util.py    |  2 +-
 git/objects/tree.py  |  2 +-
 git/objects/util.py  |  2 +-
 git/refs/symbolic.py |  2 +-
 git/repo/base.py     |  2 +-
 git/util.py          | 10 +++++-----
 9 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index f8b6c9e78..384a39077 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -490,7 +490,7 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
                         f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
                     )
 
-    class AutoInterrupt(object):
+    class AutoInterrupt:
         """Process wrapper that terminates the wrapped process on finalization.
 
         This kills/interrupts the stored process instance once this instance goes out of
@@ -602,7 +602,7 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte
 
     # END auto interrupt
 
-    class CatFileContentStream(object):
+    class CatFileContentStream:
         """Object representing a sized read-only stream returning the contents of
         an object.
 
diff --git a/git/diff.py b/git/diff.py
index 1fde4d676..d5740a7fc 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -79,7 +79,7 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
     return path
 
 
-class Diffable(object):
+class Diffable:
     """Common interface for all objects that can be diffed against another object of
     compatible type.
 
@@ -90,7 +90,7 @@ class Diffable(object):
 
     __slots__ = ()
 
-    class Index(object):
+    class Index:
         """Stand-in indicating you want to diff against the index."""
 
     def _process_diff_args(
@@ -252,7 +252,7 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
         # END for each diff
 
 
-class Diff(object):
+class Diff:
     """A Diff contains diff information between two Trees.
 
     It contains two sides a and b of the diff, members are prefixed with
diff --git a/git/index/typ.py b/git/index/typ.py
index 046df6e83..7b86cae55 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -32,7 +32,7 @@
 # } END invariants
 
 
-class BlobFilter(object):
+class BlobFilter:
     """
     Predicate to be used by iter_blobs allowing to filter only return blobs which
     match the given list of directories or files.
diff --git a/git/index/util.py b/git/index/util.py
index f52b61b4a..08e49d860 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -33,7 +33,7 @@
 # } END aliases
 
 
-class TemporaryFileSwap(object):
+class TemporaryFileSwap:
     """Utility class moving a file to a temporary location within the same directory
     and moving it back on to where on object deletion."""
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index fec98d6e8..b33da3709 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -102,7 +102,7 @@ def merge_sort(a: List[TreeCacheTup], cmp: Callable[[TreeCacheTup, TreeCacheTup]
         k = k + 1
 
 
-class TreeModifier(object):
+class TreeModifier:
     """A utility class providing methods to alter the underlying cache in a list-like fashion.
 
     Once all adjustments are complete, the _cache, which really is a reference to
diff --git a/git/objects/util.py b/git/objects/util.py
index d2c1c0158..5ccb3ec37 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -337,7 +337,7 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
 # { Classes
 
 
-class ProcessStreamAdapter(object):
+class ProcessStreamAdapter:
     """Class wiring all calls to the contained Process instance.
 
     Use this type to hide the underlying process to provide access only to a specified
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 7da957e2f..3587a52d3 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -55,7 +55,7 @@ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
     return repo.common_dir
 
 
-class SymbolicReference(object):
+class SymbolicReference:
     """Special case of a reference that is symbolic.
 
     This does not point to a specific commit, but to another
diff --git a/git/repo/base.py b/git/repo/base.py
index da81698d8..2694282d0 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -108,7 +108,7 @@ class BlameEntry(NamedTuple):
     orig_linenos: range
 
 
-class Repo(object):
+class Repo:
     """Represents a git repository and allows you to query references,
     gather commit information, generate diffs, create and clone repositories query
     the log.
diff --git a/git/util.py b/git/util.py
index 29cf12f7c..e61e3c6f5 100644
--- a/git/util.py
+++ b/git/util.py
@@ -516,7 +516,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
 # { Classes
 
 
-class RemoteProgress(object):
+class RemoteProgress:
     """
     Handler providing an interface to parse progress information emitted by git-push
     and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.
@@ -723,7 +723,7 @@ def update(self, *args: Any, **kwargs: Any) -> None:
         self._callable(*args, **kwargs)
 
 
-class Actor(object):
+class Actor:
     """Actors hold information about a person acting on the repository. They
     can be committers and authors or anything with a name and an email as
     mentioned in the git log entries."""
@@ -847,7 +847,7 @@ def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint
         return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
 
 
-class Stats(object):
+class Stats:
     """
     Represents stat information as presented by git at the end of a merge. It is
     created from the output of a diff operation.
@@ -908,7 +908,7 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
         return Stats(hsh["total"], hsh["files"])
 
 
-class IndexFileSHA1Writer(object):
+class IndexFileSHA1Writer:
     """Wrapper around a file-like object that remembers the SHA1 of
     the data written to it. It will write a sha when the stream is closed
     or if the asked for explicitly using write_sha.
@@ -942,7 +942,7 @@ def tell(self) -> int:
         return self.f.tell()
 
 
-class LockFile(object):
+class LockFile:
     """Provides methods to obtain, check for, and release a file based lock which
     should be used to handle concurrent access to the same file.
 

From 0327f8f8cb07bc09070468fbf5b242fbc63a1589 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 18 Oct 2023 12:18:33 -0400
Subject: [PATCH 222/994] Make all one-element __slots__ be tuples

Most already were, but a few were strings.
---
 git/index/typ.py    | 2 +-
 git/objects/tree.py | 5 +++--
 git/util.py         | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/git/index/typ.py b/git/index/typ.py
index 7b86cae55..9f57f067b 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -40,7 +40,7 @@ class BlobFilter:
     The given paths are given relative to the repository.
     """
 
-    __slots__ = "paths"
+    __slots__ = ("paths",)
 
     def __init__(self, paths: Sequence[PathLike]) -> None:
         """
diff --git a/git/objects/tree.py b/git/objects/tree.py
index b33da3709..c91c01807 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -109,7 +109,7 @@ class TreeModifier:
     the cache of a tree, will be sorted. This ensures it will be in a serializable state.
     """
 
-    __slots__ = "_cache"
+    __slots__ = ("_cache",)
 
     def __init__(self, cache: List[TreeCacheTup]) -> None:
         self._cache = cache
@@ -214,7 +214,8 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     """
 
     type: Literal["tree"] = "tree"
-    __slots__ = "_cache"
+
+    __slots__ = ("_cache",)
 
     # Actual integer IDs for comparison.
     commit_id = 0o16  # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link.
diff --git a/git/util.py b/git/util.py
index e61e3c6f5..4e96f7394 100644
--- a/git/util.py
+++ b/git/util.py
@@ -713,7 +713,7 @@ def update(
 class CallableRemoteProgress(RemoteProgress):
     """An implementation forwarding updates to any callable."""
 
-    __slots__ = "_callable"
+    __slots__ = ("_callable",)
 
     def __init__(self, fn: Callable) -> None:
         self._callable = fn

From d4a87c1ad48faa23b0565111e1f0c14bbdfe10a6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 22 Oct 2023 02:55:35 -0400
Subject: [PATCH 223/994] Revise comments in tests used to generate tutorials

---
 test/test_docs.py      | 267 +++++++++++++++++++++--------------------
 test/test_quick_doc.py |  36 +++---
 2 files changed, 152 insertions(+), 151 deletions(-)

diff --git a/test/test_docs.py b/test/test_docs.py
index d1ed46926..1ca07f886 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import os
 import sys
 
@@ -34,7 +35,7 @@ def test_init_repo_object(self, rw_dir):
 
         # rorepo is a Repo instance pointing to the git-python repository.
         # For all you know, the first argument to Repo is a path to the repository
-        # you want to work with
+        # you want to work with.
         repo = Repo(self.rorepo.working_tree_dir)
         assert not repo.bare
         # ![1-test_init_repo_object]
@@ -45,20 +46,20 @@ def test_init_repo_object(self, rw_dir):
         # ![2-test_init_repo_object]
 
         # [3-test_init_repo_object]
-        repo.config_reader()  # get a config reader for read-only access
-        with repo.config_writer():  # get a config writer to change configuration
-            pass  # call release() to be sure changes are written and locks are released
+        repo.config_reader()  # Get a config reader for read-only access.
+        with repo.config_writer():  # Get a config writer to change configuration.
+            pass  # Call release() to be sure changes are written and locks are released.
         # ![3-test_init_repo_object]
 
         # [4-test_init_repo_object]
-        assert not bare_repo.is_dirty()  # check the dirty state
-        repo.untracked_files  # retrieve a list of untracked files
+        assert not bare_repo.is_dirty()  # Check the dirty state.
+        repo.untracked_files  # Retrieve a list of untracked files.
         # ['my_untracked_file']
         # ![4-test_init_repo_object]
 
         # [5-test_init_repo_object]
         cloned_repo = repo.clone(os.path.join(rw_dir, "to/this/path"))
-        assert cloned_repo.__class__ is Repo  # clone an existing repository
+        assert cloned_repo.__class__ is Repo  # Clone an existing repository.
         assert Repo.init(os.path.join(rw_dir, "path/for/new/repo")).__class__ is Repo
         # ![5-test_init_repo_object]
 
@@ -69,9 +70,9 @@ def test_init_repo_object(self, rw_dir):
 
         # repository paths
         # [7-test_init_repo_object]
-        assert os.path.isdir(cloned_repo.working_tree_dir)  # directory with your work files
-        assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir)  # directory containing the git repository
-        assert bare_repo.working_tree_dir is None  # bare repositories have no working tree
+        assert os.path.isdir(cloned_repo.working_tree_dir)  # Directory with your work files.
+        assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir)  # Directory containing the git repository.
+        assert bare_repo.working_tree_dir is None  # Bare repositories have no working tree.
         # ![7-test_init_repo_object]
 
         # heads, tags and references
@@ -79,59 +80,59 @@ def test_init_repo_object(self, rw_dir):
         # [8-test_init_repo_object]
         self.assertEqual(
             repo.head.ref,
-            repo.heads.master,  # head is a sym-ref pointing to master
+            repo.heads.master,  # head is a sym-ref pointing to master.
             "It's ok if TC not running from `master`.",
         )
-        self.assertEqual(repo.tags["0.3.5"], repo.tag("refs/tags/0.3.5"))  # you can access tags in various ways too
-        self.assertEqual(repo.refs.master, repo.heads["master"])  # .refs provides all refs, ie heads ...
+        self.assertEqual(repo.tags["0.3.5"], repo.tag("refs/tags/0.3.5"))  # You can access tags in various ways too.
+        self.assertEqual(repo.refs.master, repo.heads["master"])  # .refs provides all refs, i.e. heads...
 
         if "TRAVIS" not in os.environ:
             self.assertEqual(repo.refs["origin/master"], repo.remotes.origin.refs.master)  # ... remotes ...
-        self.assertEqual(repo.refs["0.3.5"], repo.tags["0.3.5"])  # ... and tags
+        self.assertEqual(repo.refs["0.3.5"], repo.tags["0.3.5"])  # ... and tags.
         # ![8-test_init_repo_object]
 
-        # create a new head/branch
+        # Create a new head/branch.
         # [9-test_init_repo_object]
-        new_branch = cloned_repo.create_head("feature")  # create a new branch ...
+        new_branch = cloned_repo.create_head("feature")  # Create a new branch ...
         assert cloned_repo.active_branch != new_branch  # which wasn't checked out yet ...
-        self.assertEqual(new_branch.commit, cloned_repo.active_branch.commit)  # pointing to the checked-out commit
-        # It's easy to let a branch point to the previous commit, without affecting anything else
-        # Each reference provides access to the git object it points to, usually commits
+        self.assertEqual(new_branch.commit, cloned_repo.active_branch.commit)  # pointing to the checked-out commit.
+        # It's easy to let a branch point to the previous commit, without affecting anything else.
+        # Each reference provides access to the git object it points to, usually commits.
         assert new_branch.set_commit("HEAD~1").commit == cloned_repo.active_branch.commit.parents[0]
         # ![9-test_init_repo_object]
 
-        # create a new tag reference
+        # Create a new tag reference.
         # [10-test_init_repo_object]
         past = cloned_repo.create_tag(
             "past",
             ref=new_branch,
             message="This is a tag-object pointing to %s" % new_branch.name,
         )
-        self.assertEqual(past.commit, new_branch.commit)  # the tag points to the specified commit
-        assert past.tag.message.startswith("This is")  # and its object carries the message provided
+        self.assertEqual(past.commit, new_branch.commit)  # The tag points to the specified commit
+        assert past.tag.message.startswith("This is")  # and its object carries the message provided.
 
-        now = cloned_repo.create_tag("now")  # This is a tag-reference. It may not carry meta-data
+        now = cloned_repo.create_tag("now")  # This is a tag-reference. It may not carry meta-data.
         assert now.tag is None
         # ![10-test_init_repo_object]
 
         # Object handling
         # [11-test_init_repo_object]
         assert now.commit.message != past.commit.message
-        # You can read objects directly through binary streams, no working tree required
+        # You can read objects directly through binary streams, no working tree required.
         assert (now.commit.tree / "VERSION").data_stream.read().decode("ascii").startswith("3")
 
-        # You can traverse trees as well to handle all contained files of a particular commit
+        # You can traverse trees as well to handle all contained files of a particular commit.
         file_count = 0
         tree_count = 0
         tree = past.commit.tree
         for item in tree.traverse():
             file_count += item.type == "blob"
             tree_count += item.type == "tree"
-        assert file_count and tree_count  # we have accumulated all directories and files
-        self.assertEqual(len(tree.blobs) + len(tree.trees), len(tree))  # a tree is iterable on its children
+        assert file_count and tree_count  # We have accumulated all directories and files.
+        self.assertEqual(len(tree.blobs) + len(tree.trees), len(tree))  # A tree is iterable on its children.
         # ![11-test_init_repo_object]
 
-        # remotes allow handling push, pull and fetch operations
+        # Remotes allow handling push, pull and fetch operations.
         # [12-test_init_repo_object]
         from git import RemoteProgress
 
@@ -147,67 +148,67 @@ def update(self, op_code, cur_count, max_count=None, message=""):
 
         # end
 
-        self.assertEqual(len(cloned_repo.remotes), 1)  # we have been cloned, so should be one remote
-        self.assertEqual(len(bare_repo.remotes), 0)  # this one was just initialized
+        self.assertEqual(len(cloned_repo.remotes), 1)  # We have been cloned, so should be one remote.
+        self.assertEqual(len(bare_repo.remotes), 0)  # This one was just initialized.
         origin = bare_repo.create_remote("origin", url=cloned_repo.working_tree_dir)
         assert origin.exists()
         for fetch_info in origin.fetch(progress=MyProgressPrinter()):
             print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit))
-        # create a local branch at the latest fetched master. We specify the name statically, but you have all
+        # Create a local branch at the latest fetched master. We specify the name statically, but you have all
         # information to do it programmatically as well.
         bare_master = bare_repo.create_head("master", origin.refs.master)
         bare_repo.head.set_reference(bare_master)
         assert not bare_repo.delete_remote(origin).exists()
-        # push and pull behave very similarly
+        # push and pull behave very similarly.
         # ![12-test_init_repo_object]
 
         # index
         # [13-test_init_repo_object]
-        self.assertEqual(new_branch.checkout(), cloned_repo.active_branch)  # checking out branch adjusts the wtree
-        self.assertEqual(new_branch.commit, past.commit)  # Now the past is checked out
+        self.assertEqual(new_branch.checkout(), cloned_repo.active_branch)  # Checking out branch adjusts the wtree.
+        self.assertEqual(new_branch.commit, past.commit)  # Now the past is checked out.
 
         new_file_path = os.path.join(cloned_repo.working_tree_dir, "my-new-file")
-        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
+        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 merge")
 
-        # prepare a merge
-        master = cloned_repo.heads.master  # right-hand side is ahead of us, in the future
-        merge_base = cloned_repo.merge_base(new_branch, master)  # allows for a three-way merge
-        cloned_repo.index.merge_tree(master, base=merge_base)  # write the merge result into index
+        # Prepare a merge.
+        master = cloned_repo.heads.master  # Right-hand side is ahead of us, in the future.
+        merge_base = cloned_repo.merge_base(new_branch, master)  # Allows for a three-way merge.
+        cloned_repo.index.merge_tree(master, base=merge_base)  # Write the merge result into index.
         cloned_repo.index.commit(
             "Merged past and now into future ;)",
             parent_commits=(new_branch.commit, master.commit),
         )
 
-        # now new_branch is ahead of master, which probably should be checked out and reset softly.
-        # note that all these operations didn't touch the working tree, as we managed it ourselves.
-        # This definitely requires you to know what you are doing :) !
-        assert os.path.basename(new_file_path) in new_branch.commit.tree  # new file is now in tree
-        master.commit = new_branch.commit  # let master point to most recent commit
-        cloned_repo.head.reference = master  # we adjusted just the reference, not the working tree or index
+        # Now new_branch is ahead of master, which probably should be checked out and reset softly.
+        # Note that all these operations didn't touch the working tree, as we managed it ourselves.
+        # This definitely requires you to know what you are doing! :)
+        assert os.path.basename(new_file_path) in new_branch.commit.tree  # New file is now in tree.
+        master.commit = new_branch.commit  # Let master point to most recent commit.
+        cloned_repo.head.reference = master  # We adjusted just the reference, not the working tree or index.
         # ![13-test_init_repo_object]
 
         # submodules
 
         # [14-test_init_repo_object]
-        # create a new submodule and check it out on the spot, setup to track master branch of `bare_repo`
+        # Create a new submodule and check it out on the spot, setup to track master branch of `bare_repo`.
         # As our GitPython repository has submodules already that point to GitHub, make sure we don't
-        # interact with them
+        # interact with them.
         for sm in cloned_repo.submodules:
             assert not sm.remove().exists()  # after removal, the sm doesn't exist anymore
         sm = cloned_repo.create_submodule("mysubrepo", "path/to/subrepo", url=bare_repo.git_dir, branch="master")
 
-        # .gitmodules was written and added to the index, which is now being committed
+        # .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 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
+        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.
 
-        # update all submodules, non-recursively to save time, this method is very powerful, go have a look
+        # Update all submodules, non-recursively to save time. This method is very powerful, go have a look.
         cloned_repo.submodule_update(recursive=False)
-        assert sm.module_exists()  # The submodules working tree was checked out by update
+        assert sm.module_exists()  # The submodule's working tree was checked out by update.
         # ![14-test_init_repo_object]
 
     @with_rw_directory
@@ -218,25 +219,25 @@ def test_references_and_objects(self, rw_dir):
         repo = git.Repo.clone_from(self._small_repo_url(), os.path.join(rw_dir, "repo"), branch="master")
 
         heads = repo.heads
-        master = heads.master  # lists can be accessed by name for convenience
-        master.commit  # the commit pointed to by head called master
-        master.rename("new_name")  # rename heads
+        master = heads.master  # Lists can be accessed by name for convenience.
+        master.commit  # the commit pointed to by head called master.
+        master.rename("new_name")  # Rename heads.
         master.rename("master")
         # ![1-test_references_and_objects]
 
         # [2-test_references_and_objects]
         tags = repo.tags
         tagref = tags[0]
-        tagref.tag  # tags may have tag objects carrying additional information
-        tagref.commit  # but they always point to commits
-        repo.delete_tag(tagref)  # delete or
-        repo.create_tag("my_tag")  # create tags using the repo for convenience
+        tagref.tag  # Tags may have tag objects carrying additional information
+        tagref.commit  # but they always point to commits.
+        repo.delete_tag(tagref)  # Delete or
+        repo.create_tag("my_tag")  # create tags using the repo for convenience.
         # ![2-test_references_and_objects]
 
         # [3-test_references_and_objects]
-        head = repo.head  # the head points to the active branch/ref
-        master = head.reference  # retrieve the reference the head points to
-        master.commit  # from here you use it as any other reference
+        head = repo.head  # The head points to the active branch/ref.
+        master = head.reference  # Retrieve the reference the head points to.
+        master.commit  # From here you use it as any other reference.
         # ![3-test_references_and_objects]
         #
         # [4-test_references_and_objects]
@@ -246,14 +247,14 @@ def test_references_and_objects(self, rw_dir):
         # ![4-test_references_and_objects]
 
         # [5-test_references_and_objects]
-        new_branch = repo.create_head("new")  # create a new one
-        new_branch.commit = "HEAD~10"  # set branch to another commit without changing index or working trees
-        repo.delete_head(new_branch)  # delete an existing head - only works if it is not checked out
+        new_branch = repo.create_head("new")  # Create a new one.
+        new_branch.commit = "HEAD~10"  # Set branch to another commit without changing index or working trees.
+        repo.delete_head(new_branch)  # Delete an existing head - only works if it is not checked out.
         # ![5-test_references_and_objects]
 
         # [6-test_references_and_objects]
         new_tag = repo.create_tag("my_new_tag", message="my message")
-        # You cannot change the commit a tag points to. Tags need to be re-created
+        # You cannot change the commit a tag points to. Tags need to be re-created.
         self.assertRaises(AttributeError, setattr, new_tag, "commit", repo.commit("HEAD~1"))
         repo.delete_tag(new_tag)
         # ![6-test_references_and_objects]
@@ -272,22 +273,22 @@ def test_references_and_objects(self, rw_dir):
         # ![8-test_references_and_objects]
 
         # [9-test_references_and_objects]
-        self.assertEqual(hct.type, "tree")  # preset string type, being a class attribute
+        self.assertEqual(hct.type, "tree")  # Preset string type, being a class attribute.
         assert hct.size > 0  # size in bytes
         assert len(hct.hexsha) == 40
         assert len(hct.binsha) == 20
         # ![9-test_references_and_objects]
 
         # [10-test_references_and_objects]
-        self.assertEqual(hct.path, "")  # root tree has no path
-        assert hct.trees[0].path != ""  # the first contained item has one though
-        self.assertEqual(hct.mode, 0o40000)  # trees have the mode of a linux directory
-        self.assertEqual(hct.blobs[0].mode, 0o100644)  # blobs have specific mode, comparable to a standard linux fs
+        self.assertEqual(hct.path, "")  # Root tree has no path.
+        assert hct.trees[0].path != ""  # The first contained item has one though.
+        self.assertEqual(hct.mode, 0o40000)  # Trees have the mode of a Linux directory.
+        self.assertEqual(hct.blobs[0].mode, 0o100644)  # Blobs have specific mode, comparable to a standard Linux fs.
         # ![10-test_references_and_objects]
 
         # [11-test_references_and_objects]
-        hct.blobs[0].data_stream.read()  # stream object to read data from
-        hct.blobs[0].stream_data(open(os.path.join(rw_dir, "blob_data"), "wb"))  # write data to given stream
+        hct.blobs[0].data_stream.read()  # Stream object to read data from.
+        hct.blobs[0].stream_data(open(os.path.join(rw_dir, "blob_data"), "wb"))  # Write data to a given stream.
         # ![11-test_references_and_objects]
 
         # [12-test_references_and_objects]
@@ -299,7 +300,7 @@ def test_references_and_objects(self, rw_dir):
         # [13-test_references_and_objects]
         fifty_first_commits = list(repo.iter_commits("master", max_count=50))
         assert len(fifty_first_commits) == 50
-        # this will return commits 21-30 from the commit list as traversed backwards master
+        # This will return commits 21-30 from the commit list as traversed backwards master.
         ten_commits_past_twenty = list(repo.iter_commits("master", max_count=10, skip=20))
         assert len(ten_commits_past_twenty) == 10
         assert fifty_first_commits[20:30] == ten_commits_past_twenty
@@ -334,20 +335,20 @@ def test_references_and_objects(self, rw_dir):
         # ![17-test_references_and_objects]
 
         # [18-test_references_and_objects]
-        assert len(tree.trees) > 0  # trees are subdirectories
-        assert len(tree.blobs) > 0  # blobs are files
+        assert len(tree.trees) > 0  # Trees are subdirectories.
+        assert len(tree.blobs) > 0  # Blobs are files.
         assert len(tree.blobs) + len(tree.trees) == len(tree)
         # ![18-test_references_and_objects]
 
         # [19-test_references_and_objects]
-        self.assertEqual(tree["smmap"], tree / "smmap")  # access by index and by sub-path
-        for entry in tree:  # intuitive iteration of tree members
+        self.assertEqual(tree["smmap"], tree / "smmap")  # Access by index and by sub-path.
+        for entry in tree:  # Intuitive iteration of tree members.
             print(entry)
-        blob = tree.trees[1].blobs[0]  # let's get a blob in a sub-tree
+        blob = tree.trees[1].blobs[0]  # Let's get a blob in a sub-tree.
         assert blob.name
         assert len(blob.path) < len(blob.abspath)
-        self.assertEqual(tree.trees[1].name + "/" + blob.name, blob.path)  # this is how relative blob path generated
-        self.assertEqual(tree[blob.path], blob)  # you can use paths like 'dir/file' in tree
+        self.assertEqual(tree.trees[1].name + "/" + blob.name, blob.path)  # This is how relative blob path generated.
+        self.assertEqual(tree[blob.path], blob)  # You can use paths like 'dir/file' in tree,
         # ![19-test_references_and_objects]
 
         # [20-test_references_and_objects]
@@ -356,11 +357,11 @@ def test_references_and_objects(self, rw_dir):
         # ![20-test_references_and_objects]
 
         # [21-test_references_and_objects]
-        # This example shows the various types of allowed ref-specs
+        # This example shows the various types of allowed ref-specs.
         assert repo.tree() == repo.head.commit.tree
         past = repo.commit("HEAD~5")
         assert repo.tree(past) == repo.tree(past.hexsha)
-        self.assertEqual(repo.tree("v0.8.1").type, "tree")  # yes, you can provide any refspec - works everywhere
+        self.assertEqual(repo.tree("v0.8.1").type, "tree")  # Yes, you can provide any refspec - works everywhere.
         # ![21-test_references_and_objects]
 
         # [22-test_references_and_objects]
@@ -369,36 +370,36 @@ def test_references_and_objects(self, rw_dir):
 
         # [23-test_references_and_objects]
         index = repo.index
-        # The index contains all blobs in a flat list
+        # 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
+        # Access blob objects.
         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()
-        index.add([new_file_path])  # add a new file to the index
-        index.remove(["LICENSE"])  # remove an existing one
-        assert os.path.isfile(os.path.join(repo.working_tree_dir, "LICENSE"))  # working tree is untouched
+        index.add([new_file_path])  # Add a new file to the index.
+        index.remove(["LICENSE"])  # Remove an existing one.
+        assert os.path.isfile(os.path.join(repo.working_tree_dir, "LICENSE"))  # Working tree is untouched.
 
-        self.assertEqual(index.commit("my commit message").type, "commit")  # commit changed index
-        repo.active_branch.commit = repo.commit("HEAD~1")  # forget last commit
+        self.assertEqual(index.commit("my commit message").type, "commit")  # Commit changed index.
+        repo.active_branch.commit = repo.commit("HEAD~1")  # Forget last commit.
 
         from git import Actor
 
         author = Actor("An author", "author@example.com")
         committer = Actor("A committer", "committer@example.com")
-        # commit by commit message and author and committer
+        # Commit with a commit message, author, and committer.
         index.commit("my commit message", author=author, committer=committer)
         # ![23-test_references_and_objects]
 
         # [24-test_references_and_objects]
         from git import IndexFile
 
-        # loads a tree into a temporary index, which exists just in memory
+        # Load a tree into a temporary index, which exists just in memory.
         IndexFile.from_tree(repo, "HEAD~1")
-        # merge two trees three-way into memory
+        # Merge two trees three-way into memory...
         merge_index = IndexFile.from_tree(repo, "HEAD~10", "HEAD", repo.merge_base("HEAD~10", "HEAD"))
-        # and persist it
+        # ...and persist it.
         merge_index.write(os.path.join(rw_dir, "merged_index"))
         # ![24-test_references_and_objects]
 
@@ -407,20 +408,20 @@ def test_references_and_objects(self, rw_dir):
         origin = empty_repo.create_remote("origin", repo.remotes.origin.url)
         assert origin.exists()
         assert origin == empty_repo.remotes.origin == empty_repo.remotes["origin"]
-        origin.fetch()  # assure we actually have data. fetch() returns useful information
-        # Setup a local tracking branch of a remote branch
-        empty_repo.create_head("master", origin.refs.master)  # create local branch "master" from remote "master"
-        empty_repo.heads.master.set_tracking_branch(origin.refs.master)  # set local "master" to track remote "master
-        empty_repo.heads.master.checkout()  # checkout local "master" to working tree
+        origin.fetch()  # Ensure we actually have data. fetch() returns useful information.
+        # Set up a local tracking branch of a remote branch.
+        empty_repo.create_head("master", origin.refs.master)  # Create local branch "master" from remote "master".
+        empty_repo.heads.master.set_tracking_branch(origin.refs.master)  # Set local "master" to track remote "master.
+        empty_repo.heads.master.checkout()  # Check out local "master" to working tree.
         # Three above commands in one:
         empty_repo.create_head("master", origin.refs.master).set_tracking_branch(origin.refs.master).checkout()
-        # rename remotes
+        # Rename remotes.
         origin.rename("new_origin")
-        # push and pull behaves similarly to `git push|pull`
+        # Push and pull behaves similarly to `git push|pull`.
         origin.pull()
-        origin.push()  # attempt push, ignore errors
-        origin.push().raise_if_error()  # push and raise error if it fails
-        # assert not empty_repo.delete_remote(origin).exists()     # create and delete remotes
+        origin.push()  # Attempt push, ignore errors.
+        origin.push().raise_if_error()  # Push and raise error if it fails.
+        # assert not empty_repo.delete_remote(origin).exists()     # Create and delete remotes.
         # ![25-test_references_and_objects]
 
         # [26-test_references_and_objects]
@@ -428,20 +429,20 @@ def test_references_and_objects(self, rw_dir):
         with origin.config_writer as cw:
             cw.set("pushurl", "other_url")
 
-        # Please note that in python 2, writing origin.config_writer.set(...) is totally safe.
+        # Please note that in Python 2, writing origin.config_writer.set(...) is totally safe.
         # In py3 __del__ calls can be delayed, thus not writing changes in time.
         # ![26-test_references_and_objects]
 
         # [27-test_references_and_objects]
         hcommit = repo.head.commit
-        hcommit.diff()  # diff tree against index
-        hcommit.diff("HEAD~1")  # diff tree against previous tree
-        hcommit.diff(None)  # diff tree against working tree
+        hcommit.diff()  # diff tree against index.
+        hcommit.diff("HEAD~1")  # diff tree against previous tree.
+        hcommit.diff(None)  # diff tree against working tree.
 
         index = repo.index
-        index.diff()  # diff index against itself yielding empty diff
-        index.diff(None)  # diff index against working copy
-        index.diff("HEAD")  # diff index against current HEAD tree
+        index.diff()  # diff index against itself yielding empty diff.
+        index.diff(None)  # diff index against working copy.
+        index.diff("HEAD")  # diff index against current HEAD tree.
         # ![27-test_references_and_objects]
 
         # [28-test_references_and_objects]
@@ -451,32 +452,32 @@ def test_references_and_objects(self, rw_dir):
         # ![28-test_references_and_objects]
 
         # [29-test_references_and_objects]
-        # Reset our working tree 10 commits into the past
+        # Reset our working tree 10 commits into the past.
         past_branch = repo.create_head("past_branch", "HEAD~10")
         repo.head.reference = past_branch
         assert not repo.head.is_detached
-        # reset the index and working tree to match the pointed-to commit
+        # Reset the index and working tree to match the pointed-to commit.
         repo.head.reset(index=True, working_tree=True)
 
-        # To detach your head, you have to point to a commit directly
+        # To detach your head, you have to point to a commit directly.
         repo.head.reference = repo.commit("HEAD~5")
         assert repo.head.is_detached
-        # now our head points 15 commits into the past, whereas the working tree
-        # and index are 10 commits in the past
+        # Now our head points 15 commits into the past, whereas the working tree
+        # and index are 10 commits in the past.
         # ![29-test_references_and_objects]
 
         # [30-test_references_and_objects]
-        # checkout the branch using git-checkout. It will fail as the working tree appears dirty
+        # Check out the branch using git-checkout. It will fail as the working tree appears dirty.
         self.assertRaises(git.GitCommandError, repo.heads.master.checkout)
         repo.heads.past_branch.checkout()
         # ![30-test_references_and_objects]
 
         # [31-test_references_and_objects]
         git = repo.git
-        git.checkout("HEAD", b="my_new_branch")  # create a new branch
+        git.checkout("HEAD", b="my_new_branch")  # Create a new branch.
         git.branch("another-new-one")
-        git.branch("-D", "another-new-one")  # pass strings for full control over argument order
-        git.for_each_ref()  # '-' becomes '_' when calling it
+        git.branch("-D", "another-new-one")  # Pass strings for full control over argument order.
+        git.for_each_ref()  # '-' becomes '_' when calling it.
         # ![31-test_references_and_objects]
 
         repo.git.clear_cache()
@@ -493,19 +494,19 @@ def test_submodules(self):
 
         assert len(sms) == 1
         sm = sms[0]
-        self.assertEqual(sm.name, "gitdb")  # git-python has gitdb as single submodule ...
-        self.assertEqual(sm.children()[0].name, "smmap")  # ... which has smmap as single submodule
+        self.assertEqual(sm.name, "gitdb")  # GitPython has gitdb as its one and only (direct) submodule...
+        self.assertEqual(sm.children()[0].name, "smmap")  # ...which has smmap as its one and only submodule.
 
-        # The module is the repository referenced by the submodule
-        assert sm.module_exists()  # the module is available, which doesn't have to be the case.
+        # The module is the repository referenced by the submodule.
+        assert sm.module_exists()  # The module is available, which doesn't have to be the case.
         assert sm.module().working_tree_dir.endswith("gitdb")
-        # the submodule's absolute path is the module's path
+        # The submodule's absolute path is the module's path.
         assert sm.abspath == sm.module().working_tree_dir
-        self.assertEqual(len(sm.hexsha), 40)  # Its sha defines the commit to checkout
-        assert sm.exists()  # yes, this submodule is valid and exists
-        # read its configuration conveniently
+        self.assertEqual(len(sm.hexsha), 40)  # Its sha defines the commit to check out.
+        assert sm.exists()  # Yes, this submodule is valid and exists.
+        # Read its configuration conveniently.
         assert sm.config_reader().get_value("path") == sm.path
-        self.assertEqual(len(sm.children()), 1)  # query the submodule hierarchy
+        self.assertEqual(len(sm.children()), 1)  # Query the submodule hierarchy.
         # ![1-test_submodules]
 
     @with_rw_directory
@@ -516,7 +517,7 @@ def test_add_file_and_commit(self, rw_dir):
         file_name = os.path.join(repo_dir, "new-file")
 
         r = git.Repo.init(repo_dir)
-        # This function just creates an empty file ...
+        # This function just creates an empty file.
         open(file_name, "wb").close()
         r.index.add([file_name])
         r.index.commit("initial commit")
diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py
index 342a7f293..13b587bd5 100644
--- a/test/test_quick_doc.py
+++ b/test/test_quick_doc.py
@@ -28,7 +28,7 @@ def test_init_repo_object(self, path_to_dir):
     def test_cloned_repo_object(self, local_dir):
         from git import Repo
 
-        # code to clone from url
+        # Code to clone from url
         # [1-test_cloned_repo_object]
         # $ git clone <url> <local_dir>
 
@@ -37,7 +37,7 @@ def test_cloned_repo_object(self, local_dir):
         repo = Repo.clone_from(repo_url, local_dir)
         # ![1-test_cloned_repo_object]
 
-        # code to add files
+        # 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
 
@@ -52,7 +52,7 @@ def test_cloned_repo_object(self, local_dir):
         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
+        # Code to commit - not sure how to test this
         # [4-test_cloned_repo_object]
         # $ git commit -m <message>
         repo.index.commit("Update to file2")
@@ -61,8 +61,8 @@ def test_cloned_repo_object(self, local_dir):
         # [5-test_cloned_repo_object]
         # $ git log <file>
 
-        # relative path from git root
-        repo.iter_commits(all=True, max_count=10, paths=update_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: <generator object Commit._iter_from_process_or_stream at 0x7fb66c186cf0>
 
@@ -79,7 +79,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]
 
@@ -90,14 +90,14 @@ def test_cloned_repo_object(self, local_dir):
 
         # Modified files
         # [9-test_cloned_repo_object]
-        # Let's 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
+            f.write("file3 version 2")  # Overwrite file 3.
         # ![9-test_cloned_repo_object]
 
         # [10-test_cloned_repo_object]
-        repo.index.diff(None)  # compares staging area to working directory
+        repo.index.diff(None)  # Compares staging area to working directory.
 
         # Output: [<git.diff.Diff object at 0x7fb66c076e50>,
         # <git.diff.Diff object at 0x7fb66c076ca0>]
@@ -112,7 +112,7 @@ def test_cloned_repo_object(self, local_dir):
         # Downloads/file3.txt
         # ![11-test_cloned_repo_object]
 
-        # compares staging area to head commit
+        # Compares staging area to head commit
         # [11.1-test_cloned_repo_object]
         diffs = repo.index.diff(repo.head.commit)
         for d in diffs:
@@ -122,7 +122,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
+        # Let's add untracked.txt.
         repo.index.add(["untracked.txt"])
         diffs = repo.index.diff(repo.head.commit)
         for d in diffs:
@@ -152,7 +152,7 @@ def test_cloned_repo_object(self, local_dir):
 
         # Previous commit tree
         # [13-test_cloned_repo_object]
-        prev_commits = list(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]
 
@@ -191,29 +191,29 @@ def print_files_from_git(root, level=0):
         # Printing text files
         # [17-test_cloned_repo_object]
         print_file = "dir1/file2.txt"
-        tree[print_file]  # the head commit tree
+        tree[print_file]  # The head commit tree.
 
         # Output <git.Blob "SHA1-HEX-HASH">
         # ![17-test_cloned_repo_object]
 
-        # print latest file
+        # Print latest file
         # [18-test_cloned_repo_object]
         blob = tree[print_file]
         print(blob.data_stream.read().decode())
 
         # Output
-        # file 2 version 1
+        # File 2 version 1
         # Update version 2
         # ![18-test_cloned_repo_object]
 
-        # print previous tree
+        # Print previous tree
         # [18.1-test_cloned_repo_object]
         commits_for_file = list(repo.iter_commits(all=True, paths=print_file))
-        tree = commits_for_file[-1].tree  # gets the first commit tree
+        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
+        # File 2 version 1
         # ![18.1-test_cloned_repo_object]

From 11fce8a3a7ccd82fbfde5aa51dd62dcb9e76c2f3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 20 Oct 2023 06:04:55 -0400
Subject: [PATCH 224/994] Revise docstrings/comments in most other test modules

Intentionally untouched test modules in this commit:

- Modules where there are no such changes to be made.

- Modules in test/performance/ -- will be done separately.

- test/lib/ modules (these are test *helpers* and will be done
  separately).

- test/test_util.py, because although it has a few comments that
  might be improved, it may make more sense to do that together
  with some further refactoring changes.
---
 test/test_actor.py       |   2 +-
 test/test_base.py        |  21 +--
 test/test_blob_filter.py |   1 +
 test/test_clone.py       |   2 +-
 test/test_commit.py      |  86 ++++++-----
 test/test_config.py      |  44 +++---
 test/test_db.py          |   5 +-
 test/test_diff.py        |  57 +++----
 test/test_fun.py         |  68 ++++-----
 test/test_git.py         |  35 ++---
 test/test_index.py       | 210 +++++++++++++------------
 test/test_reflog.py      |  30 ++--
 test/test_refs.py        | 158 ++++++++++---------
 test/test_remote.py      | 191 +++++++++++------------
 test/test_repo.py        | 178 ++++++++++-----------
 test/test_submodule.py   | 323 ++++++++++++++++++++-------------------
 test/test_tree.py        |  20 +--
 17 files changed, 719 insertions(+), 712 deletions(-)

diff --git a/test/test_actor.py b/test/test_actor.py
index f495ac084..80b93d7bc 100644
--- a/test/test_actor.py
+++ b/test/test_actor.py
@@ -14,7 +14,7 @@ def test_from_string_should_separate_name_and_email(self):
         self.assertEqual("Michael Trier", a.name)
         self.assertEqual("mtrier@example.com", a.email)
 
-        # base type capabilities
+        # Base type capabilities
         assert a == a
         assert not (a != a)
         m = set()
diff --git a/test/test_base.py b/test/test_base.py
index 90e701c4b..94a268ecd 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import os
 import sys
 import tempfile
@@ -34,7 +35,7 @@ def tearDown(self):
     )
 
     def test_base_object(self):
-        # test interface of base object classes
+        # Test interface of base object classes.
         types = (Blob, Tree, Commit, TagObject)
         self.assertEqual(len(types), len(self.type_tuples))
 
@@ -61,12 +62,12 @@ def test_base_object(self):
 
             if isinstance(item, base.IndexObject):
                 num_index_objs += 1
-                if hasattr(item, "path"):  # never runs here
-                    assert not item.path.startswith("/")  # must be relative
+                if hasattr(item, "path"):  # Never runs here.
+                    assert not item.path.startswith("/")  # Must be relative.
                     assert isinstance(item.mode, int)
             # END index object check
 
-            # read from stream
+            # Read from stream.
             data_stream = item.data_stream
             data = data_stream.read()
             assert data
@@ -79,7 +80,7 @@ def test_base_object(self):
             os.remove(tmpfilename)
         # END for each object type to create
 
-        # each has a unique sha
+        # Each has a unique sha.
         self.assertEqual(len(s), num_objs)
         self.assertEqual(len(s | s), num_objs)
         self.assertEqual(num_index_objs, 2)
@@ -92,7 +93,7 @@ def test_get_object_type_by_name(self):
         self.assertRaises(ValueError, get_object_type_by_name, b"doesntexist")
 
     def test_object_resolution(self):
-        # objects must be resolved to shas so they compare equal
+        # Objects must be resolved to shas so they compare equal.
         self.assertEqual(self.rorepo.head.reference.object, self.rorepo.active_branch.object)
 
     @with_rw_repo("HEAD", bare=True)
@@ -122,7 +123,7 @@ def test_add_unicode(self, rw_repo):
 
         file_path = osp.join(rw_repo.working_dir, filename)
 
-        # verify first that we could encode file name in this environment
+        # Verify first that we could encode file name in this environment.
         try:
             file_path.encode(sys.getfilesystemencoding())
         except UnicodeEncodeError as e:
@@ -132,14 +133,14 @@ def test_add_unicode(self, rw_repo):
             fp.write(b"something")
 
         if is_win:
-            # on windows, there is no way this works, see images on
+            # On Windows, there is no way this works, see images on:
             # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
-            # Therefore, it must be added using the python implementation
+            # Therefore, it must be added using the Python implementation.
             rw_repo.index.add([file_path])
             # However, when the test winds down, rmtree fails to delete this file, which is recognized
             # as ??? only.
         else:
-            # on posix, we can just add unicode files without problems
+            # On POSIX, we can just add Unicode files without problems.
             rw_repo.git.add(rw_repo.working_dir)
         # end
         rw_repo.index.commit("message")
diff --git a/test/test_blob_filter.py b/test/test_blob_filter.py
index cbaa30b8b..ad4f0e7ff 100644
--- a/test/test_blob_filter.py
+++ b/test/test_blob_filter.py
@@ -1,4 +1,5 @@
 """Test the blob filter."""
+
 from pathlib import Path
 from typing import Sequence, Tuple
 from unittest.mock import MagicMock
diff --git a/test/test_clone.py b/test/test_clone.py
index 1b4a6c332..f66130cf0 100644
--- a/test/test_clone.py
+++ b/test/test_clone.py
@@ -21,7 +21,7 @@ def test_checkout_in_non_empty_dir(self, rw_dir):
         garbage_file.write_text("Garbage!")
 
         # Verify that cloning into the non-empty dir fails while complaining about
-        # the target directory not being empty/non-existent
+        # the target directory not being empty/non-existent.
         try:
             self.rorepo.clone(non_empty_dir)
         except git.GitCommandError as exc:
diff --git a/test/test_commit.py b/test/test_commit.py
index 527aea334..3f3c52f1b 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import copy
 from datetime import datetime
 from io import BytesIO
@@ -28,18 +29,20 @@
 
 class TestCommitSerialization(TestBase):
     def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info=False):
-        """traverse all commits in the history of commit identified by commit_id and check
+        """Traverse all commits in the history of commit identified by commit_id and check
         if the serialization works.
-        :param print_performance_info: if True, we will show how fast we are"""
-        ns = 0  # num serializations
-        nds = 0  # num deserializations
+
+        :param print_performance_info: If True, we will show how fast we are.
+        """
+        ns = 0  # Number of serializations.
+        nds = 0  # Number of deserializations.
 
         st = time.time()
         for cm in rwrepo.commit(commit_id).traverse():
             nds += 1
 
-            # assert that we deserialize commits correctly, hence we get the same
-            # sha on serialization
+            # Assert that we deserialize commits correctly, hence we get the same
+            # sha on serialization.
             stream = BytesIO()
             cm._serialize(stream)
             ns += 1
@@ -71,13 +74,13 @@ def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info=
             streamlen = stream.tell()
             stream.seek(0)
 
-            # reuse istream
+            # Reuse istream.
             istream.size = streamlen
             istream.stream = stream
             istream.binsha = None
             nc.binsha = rwrepo.odb.store(istream).binsha
 
-            # if it worked, we have exactly the same contents !
+            # If it worked, we have exactly the same contents!
             self.assertEqual(nc.hexsha, cm.hexsha)
         # END check commits
         elapsed = time.time() - st
@@ -94,7 +97,7 @@ 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
+        # Commits have no dict.
         self.assertRaises(AttributeError, setattr, commit, "someattr", 1)
         commit.author  # bake
 
@@ -148,7 +151,7 @@ def check_entries(d):
             check_entries(d)
         # END for each stated file
 
-        # assure data is parsed properly
+        # Check that data is parsed properly.
         michael = Actor._from_string("Michael Trier <mtrier@gmail.com>")
         self.assertEqual(commit.author, michael)
         self.assertEqual(commit.committer, michael)
@@ -162,9 +165,9 @@ def test_renames(self):
         commit = self.rorepo.commit("185d847ec7647fd2642a82d9205fb3d07ea71715")
         files = commit.stats.files
 
-        # when a file is renamed, the output of git diff is like "dir/{old => new}"
-        # unless we disable rename with --no-renames, which produces two lines
-        # one with the old path deletes and another with the new added
+        # When a file is renamed, the output of git diff is like "dir/{old => new}"
+        # unless we disable rename with --no-renames, which produces two lines,
+        # one with the old path deletes and another with the new added.
         self.assertEqual(len(files), 2)
 
         def check_entries(path, changes):
@@ -190,7 +193,7 @@ def check_entries(path, changes):
         # END for each stated file
 
     def test_unicode_actor(self):
-        # assure we can parse unicode actors correctly
+        # Check that we can parse Unicode actors correctly.
         name = "Üäöß ÄußÉ"
         self.assertEqual(len(name), 9)
         special = Actor._from_string("%s <something@this.com>" % name)
@@ -205,7 +208,7 @@ def test_traversal(self):
         p00 = p0.parents[0]
         p10 = p1.parents[0]
 
-        # basic branch first, depth first
+        # Basic branch first, depth first.
         dfirst = start.traverse(branch_first=False)
         bfirst = start.traverse(branch_first=True)
         self.assertEqual(next(dfirst), p0)
@@ -216,7 +219,7 @@ def test_traversal(self):
         self.assertEqual(next(bfirst), p00)
         self.assertEqual(next(bfirst), p10)
 
-        # at some point, both iterations should stop
+        # At some point, both iterations should stop.
         self.assertEqual(list(bfirst)[-1], first)
 
         stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(
@@ -235,40 +238,39 @@ def test_traversal(self):
         stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(as_edge=True)
         self.assertEqual(len(next(stoptraverse)), 2)
 
-        # ignore self
+        # Ignore self
         self.assertEqual(next(start.traverse(ignore_self=False)), start)
 
-        # depth
+        # Depth
         self.assertEqual(len(list(start.traverse(ignore_self=False, depth=0))), 1)
 
-        # prune
+        # Prune
         self.assertEqual(next(start.traverse(branch_first=1, prune=lambda i, d: i == p0)), p1)
 
-        # predicate
+        # Predicate
         self.assertEqual(next(start.traverse(branch_first=1, predicate=lambda i, d: i == p1)), p1)
 
-        # traversal should stop when the beginning is reached
+        # Traversal should stop when the beginning is reached.
         self.assertRaises(StopIteration, next, first.traverse())
 
-        # parents of the first commit should be empty ( as the only parent has a null
-        # sha )
+        # Parents of the first commit should be empty (as the only parent has a null sha)
         self.assertEqual(len(first.parents), 0)
 
     def test_iteration(self):
-        # we can iterate commits
+        # We can iterate commits.
         all_commits = Commit.list_items(self.rorepo, self.rorepo.head)
         assert all_commits
         self.assertEqual(all_commits, list(self.rorepo.iter_commits()))
 
-        # this includes merge commits
+        # This includes merge commits.
         mcomit = self.rorepo.commit("d884adc80c80300b4cc05321494713904ef1df2d")
         assert mcomit in all_commits
 
-        # we can limit the result to paths
+        # We can limit the result to paths.
         ltd_commits = list(self.rorepo.iter_commits(paths="CHANGES"))
         assert ltd_commits and len(ltd_commits) < len(all_commits)
 
-        # show commits of multiple paths, resulting in a union of commits
+        # Show commits of multiple paths, resulting in a union of commits.
         less_ltd_commits = list(Commit.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS")))
         assert len(ltd_commits) < len(less_ltd_commits)
 
@@ -280,7 +282,7 @@ def __init__(self, *args, **kwargs):
         assert type(child_commits[0]) is Child
 
     def test_iter_items(self):
-        # pretty not allowed
+        # pretty not allowed.
         self.assertRaises(ValueError, Commit.iter_items, self.rorepo, "master", pretty="raw")
 
     def test_rev_list_bisect_all(self):
@@ -311,14 +313,14 @@ def test_ambiguous_arg_iteration(self, rw_dir):
         touch(path)
         rw_repo.index.add([path])
         rw_repo.index.commit("initial commit")
-        list(rw_repo.iter_commits(rw_repo.head.ref))  # should fail unless bug is fixed
+        list(rw_repo.iter_commits(rw_repo.head.ref))  # Should fail unless bug is fixed.
 
     def test_count(self):
         self.assertEqual(self.rorepo.tag("refs/tags/0.1.5").commit.count(), 143)
 
     def test_list(self):
         # This doesn't work anymore, as we will either attempt getattr with bytes, or compare 20 byte string
-        # with actual 20 byte bytes. This usage makes no sense anyway
+        # with actual 20 byte bytes. This usage makes no sense anyway.
         assert isinstance(
             Commit.list_items(self.rorepo, "0.1.5", max_count=5)["5117c9c8a4d3af19a9958677e45cda9269de1541"],
             Commit,
@@ -340,7 +342,7 @@ def test_equality(self):
         self.assertNotEqual(commit2, commit3)
 
     def test_iter_parents(self):
-        # should return all but ourselves, even if skip is defined
+        # Should return all but ourselves, even if skip is defined.
         c = self.rorepo.commit("0.1.5")
         for skip in (0, 1):
             piter = c.iter_parents(skip=skip)
@@ -355,17 +357,17 @@ def test_name_rev(self):
 
     @with_rw_repo("HEAD", bare=True)
     def test_serialization(self, rwrepo):
-        # create all commits of our repo
+        # Create all commits of our repo.
         self.assert_commit_serialization(rwrepo, "0.1.6")
 
     def test_serialization_unicode_support(self):
         self.assertEqual(Commit.default_encoding.lower(), "utf-8")
 
-        # create a commit with unicode in the message, and the author's name
-        # Verify its serialization and deserialization
+        # Create a commit with Unicode in the message, and the author's name.
+        # Verify its serialization and deserialization.
         cmt = self.rorepo.commit("0.1.6")
-        assert isinstance(cmt.message, str)  # it automatically decodes it as such
-        assert isinstance(cmt.author.name, str)  # same here
+        assert isinstance(cmt.message, str)  # It automatically decodes it as such.
+        assert isinstance(cmt.author.name, str)  # Same here.
 
         cmt.message = "üäêèß"
         self.assertEqual(len(cmt.message), 5)
@@ -383,8 +385,8 @@ def test_serialization_unicode_support(self):
 
         self.assertEqual(cmt.author.name, ncmt.author.name)
         self.assertEqual(cmt.message, ncmt.message)
-        # actually, it can't be printed in a shell as repr wants to have ascii only
-        # it appears
+        # Actually, it can't be printed in a shell as repr wants to have ascii only
+        # it appears.
         cmt.author.__repr__()
 
     def test_invalid_commit(self):
@@ -498,14 +500,14 @@ def test_trailers(self):
         KEY_2 = "Key"
         VALUE_2 = "Value with inner spaces"
 
-        # Check the following trailer example is extracted from multiple msg variations
+        # Check that 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
+                # 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"
             ),
@@ -523,7 +525,7 @@ def test_trailers(self):
                 KEY_2: [VALUE_2],
             }
 
-        # check that trailer stays empty for multiple msg combinations
+        # Check that the trailer stays empty for multiple msg combinations.
         msgs = [
             "Subject\n",
             "Subject\n\nBody with some\nText\n",
@@ -539,7 +541,7 @@ def test_trailers(self):
             assert commit.trailers_list == []
             assert commit.trailers_dict == {}
 
-        # check that only the last key value paragraph is evaluated
+        # 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 == [(KEY_2, VALUE_2)]
diff --git a/test/test_config.py b/test/test_config.py
index f805570d5..2e278bec2 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -50,25 +50,25 @@ def test_read_write(self):
         for filename in ("git_config", "git_config_global"):
             file_obj = self._to_memcache(fixture_path(filename))
             with GitConfigParser(file_obj, read_only=False) as w_config:
-                w_config.read()  # enforce reading
+                w_config.read()  # Enforce reading.
                 assert w_config._sections
-                w_config.write()  # enforce writing
+                w_config.write()  # Enforce writing.
 
-                # we stripped lines when reading, so the results differ
+                # We stripped lines when reading, so the results differ.
                 assert file_obj.getvalue()
                 self.assertEqual(
                     file_obj.getvalue(),
                     self._to_memcache(fixture_path(filename)).getvalue(),
                 )
 
-                # creating an additional config writer must fail due to exclusive access
+                # Creating an additional config writer must fail due to exclusive access.
                 with self.assertRaises(IOError):
                     GitConfigParser(file_obj, read_only=False)
 
-                # should still have a lock and be able to make changes
+                # Should still have a lock and be able to make changes.
                 assert w_config._lock._has_lock()
 
-                # changes should be written right away
+                # Changes should be written right away.
                 sname = "my_section"
                 oname = "mykey"
                 val = "myvalue"
@@ -93,11 +93,11 @@ def test_read_write(self):
 
     def test_includes_order(self):
         with GitConfigParser(list(map(fixture_path, ("git_config", "git_config_global")))) as r_config:
-            r_config.read()  # enforce reading
-            # Simple inclusions, again checking them taking precedence
+            r_config.read()  # Enforce reading.
+            # Simple inclusions, again checking them taking precedence.
             assert r_config.get_value("sec", "var0") == "value0_included"
             # This one should take the git_config_global value since included
-            # values must be considered as soon as they get them
+            # values must be considered as soon as they get them.
             assert r_config.get_value("diff", "tool") == "meld"
             try:
                 # FIXME: Split this assertion out somehow and mark it xfail (or fix it).
@@ -111,16 +111,16 @@ def test_lock_reentry(self, rw_dir):
         gcp = GitConfigParser(fpl, read_only=False)
         with gcp as cw:
             cw.set_value("include", "some_value", "a")
-        # entering again locks the file again...
+        # Entering again locks the file again...
         with gcp as cw:
             cw.set_value("include", "some_other_value", "b")
-            # ...so creating an additional config writer must fail due to exclusive access
+            # ...so creating an additional config writer must fail due to exclusive access.
             with self.assertRaises(IOError):
                 GitConfigParser(fpl, read_only=False)
         # but work when the lock is removed
         with GitConfigParser(fpl, read_only=False):
             assert osp.exists(fpl)
-            # reentering with an existing lock must fail due to exclusive access
+            # Reentering with an existing lock must fail due to exclusive access.
             with self.assertRaises(IOError):
                 gcp.__enter__()
 
@@ -152,7 +152,7 @@ def test_base(self):
         num_sections = 0
         num_options = 0
 
-        # test reader methods
+        # Test reader methods.
         assert r_config._is_initialized is False
         for section in r_config.sections():
             num_sections += 1
@@ -165,7 +165,7 @@ def test_base(self):
                 assert "\n" not in option
                 assert "\n" not in val
 
-                # writing must fail
+                # Writing must fail.
                 with self.assertRaises(IOError):
                     r_config.set(section, option, None)
                 with self.assertRaises(IOError):
@@ -177,11 +177,11 @@ def test_base(self):
         assert num_sections and num_options
         assert r_config._is_initialized is True
 
-        # get value which doesn't exist, with default
+        # Get value which doesn't exist, with default.
         default = "my default value"
         assert r_config.get_value("doesnt", "exist", default) == default
 
-        # it raises if there is no default though
+        # It raises if there is no default though.
         with self.assertRaises(cp.NoSectionError):
             r_config.get_value("doesnt", "exist")
 
@@ -228,7 +228,7 @@ def check_test_value(cr, value):
             # end for each test to verify
             assert len(cr.items("include")) == 8, "Expected all include sections to be merged"
 
-        # test writable config writers - assure write-back doesn't involve includes
+        # Test writable config writers - assure write-back doesn't involve includes.
         with GitConfigParser(fpa, read_only=False, merge_includes=True) as cw:
             tv = "x"
             write_test_value(cw, tv)
@@ -237,7 +237,7 @@ def check_test_value(cr, value):
             with self.assertRaises(cp.NoSectionError):
                 check_test_value(cr, tv)
 
-        # But can make it skip includes altogether, and thus allow write-backs
+        # But can make it skip includes altogether, and thus allow write-backs.
         with GitConfigParser(fpa, read_only=False, merge_includes=False) as cw:
             write_test_value(cw, tv)
 
@@ -246,11 +246,11 @@ def check_test_value(cr, value):
 
     @with_rw_directory
     def test_conditional_includes_from_git_dir(self, rw_dir):
-        # Initiate repository path
+        # Initiate repository path.
         git_dir = osp.join(rw_dir, "target1", "repo1")
         os.makedirs(git_dir)
 
-        # Initiate mocked repository
+        # Initiate mocked repository.
         repo = mock.Mock(git_dir=git_dir)
 
         # Initiate config files.
@@ -313,11 +313,11 @@ def test_conditional_includes_from_git_dir(self, rw_dir):
 
     @with_rw_directory
     def test_conditional_includes_from_branch_name(self, rw_dir):
-        # Initiate mocked branch
+        # Initiate mocked branch.
         branch = mock.Mock()
         type(branch).name = mock.PropertyMock(return_value="/foo/branch")
 
-        # Initiate mocked repository
+        # Initiate mocked repository.
         repo = mock.Mock(active_branch=branch)
 
         # Initiate config files.
diff --git a/test/test_db.py b/test/test_db.py
index ebf73b535..acf8379db 100644
--- a/test/test_db.py
+++ b/test/test_db.py
@@ -3,6 +3,7 @@
 #
 # This module is part of GitPython and is released under
 # 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
@@ -15,12 +16,12 @@ class TestDB(TestBase):
     def test_base(self):
         gdb = GitCmdObjectDB(osp.join(self.rorepo.git_dir, "objects"), self.rorepo.git)
 
-        # partial to complete - works with everything
+        # Partial to complete - works with everything.
         hexsha = bin_to_hex(gdb.partial_to_complete_sha_hex("0.1.6"))
         assert len(hexsha) == 40
 
         assert bin_to_hex(gdb.partial_to_complete_sha_hex(hexsha[:20])) == hexsha
 
-        # fails with BadObject
+        # Fails with BadObject.
         for invalid_rev in ("0000", "bad/ref", "super bad"):
             self.assertRaises(BadObject, gdb.partial_to_complete_sha_hex, invalid_rev)
diff --git a/test/test_diff.py b/test/test_diff.py
index 5aa4408bf..1d9c398c2 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import ddt
 import shutil
 import tempfile
@@ -44,7 +45,7 @@ def tearDown(self):
         shutil.rmtree(self.submodule_dir)
 
     def _assert_diff_format(self, diffs):
-        # verify that the format of the diff is sane
+        # Verify that the format of the diff is sane.
         for diff in diffs:
             if diff.a_mode:
                 assert isinstance(diff.a_mode, int)
@@ -60,7 +61,7 @@ def _assert_diff_format(self, diffs):
 
     @with_rw_directory
     def test_diff_with_staged_file(self, rw_dir):
-        # SETUP INDEX WITH MULTIPLE STAGES
+        # SET UP INDEX WITH MULTIPLE STAGES
         r = Repo.init(rw_dir)
         fp = osp.join(rw_dir, "hello.txt")
         with open(fp, "w") as fs:
@@ -88,11 +89,11 @@ def test_diff_with_staged_file(self, rw_dir):
             fs.write("Hallo Welt")
         r.git.commit(all=True, message="change on topic branch")
 
-        # there must be a merge-conflict
+        # There must be a merge conflict.
         with self.assertRaises(GitCommandError):
             r.git.cherry_pick("master")
 
-        # Now do the actual testing - this should just work
+        # Now do the actual testing - this should just work.
         self.assertEqual(len(r.index.diff(None)), 2)
 
         self.assertEqual(
@@ -255,7 +256,7 @@ def test_diff_initial_commit(self):
         self.assertIsNotNone(diff_index[0].new_file)
         self.assertEqual(diff_index[0].diff, "")
 
-        # ...and with creating a patch
+        # ...and with creating a patch.
         diff_index = initial_commit.diff(NULL_TREE, create_patch=True)
         self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
         self.assertEqual(diff_index[0].b_path, "CHANGES", repr(diff_index[0].b_path))
@@ -292,8 +293,8 @@ def test_diff_unsafe_paths(self):
         self.assertEqual(res[13].b_path, 'b/"with even more quotes"')
 
     def test_diff_patch_format(self):
-        # test all of the 'old' format diffs for completeness - it should at least
-        # be able to deal with it
+        # Test all of the 'old' format diffs for completeness - it should at least
+        # be able to deal with it.
         fixtures = (
             "diff_2",
             "diff_2f",
@@ -321,14 +322,14 @@ def test_diff_with_spaces(self):
 
     def test_diff_submodule(self):
         """Test that diff is able to correctly diff commits that cover submodule changes"""
-        # Init a temp git repo that will be referenced as a submodule
+        # Init a temp git repo that will be referenced as a submodule.
         sub = Repo.init(self.submodule_dir)
         with open(self.submodule_dir + "/subfile", "w") as sub_subfile:
             sub_subfile.write("")
         sub.index.add(["subfile"])
         sub.index.commit("first commit")
 
-        # Init a temp git repo that will incorporate the submodule
+        # Init a temp git repo that will incorporate the submodule.
         repo = Repo.init(self.repo_dir)
         with open(self.repo_dir + "/test", "w") as foo_test:
             foo_test.write("")
@@ -337,7 +338,7 @@ def test_diff_submodule(self):
         repo.index.commit("first commit")
         repo.create_tag("1")
 
-        # Add a commit to the submodule
+        # Add a commit to the submodule.
         submodule = repo.submodule("subtest")
         with open(self.repo_dir + "/sub/subfile", "w") as foo_sub_subfile:
             foo_sub_subfile.write("blub")
@@ -345,19 +346,19 @@ def test_diff_submodule(self):
         submodule.module().index.commit("changed subfile")
         submodule.binsha = submodule.module().head.commit.binsha
 
-        # Commit submodule updates in parent repo
+        # Commit submodule updates in parent repo.
         repo.index.add([submodule])
         repo.index.commit("submodule changed")
         repo.create_tag("2")
 
         diff = repo.commit("1").diff(repo.commit("2"))[0]
         # If diff is unable to find the commit hashes (looks in wrong repo) the *_blob.size
-        # property will be a string containing exception text, an int indicates success
+        # property will be a string containing exception text, an int indicates success.
         self.assertIsInstance(diff.a_blob.size, int)
         self.assertIsInstance(diff.b_blob.size, int)
 
     def test_diff_interface(self):
-        # test a few variations of the main diff routine
+        # Test a few variations of the main diff routine.
         assertion_map = {}
         for i, commit in enumerate(self.rorepo.iter_commits("0.1.6", max_count=2)):
             diff_item = commit
@@ -379,7 +380,7 @@ def test_diff_interface(self):
                                 assertion_map[key] = assertion_map[key] + len(list(diff_index.iter_change_type(ct)))
                             # END for each changetype
 
-                            # check entries
+                            # Check entries.
                             diff_set = set()
                             diff_set.add(diff_index[0])
                             diff_set.add(diff_index[0])
@@ -398,14 +399,14 @@ def test_diff_interface(self):
             # END for each other side
         # END for each commit
 
-        # assert we could always find at least one instance of the members we
+        # Assert that we could always find at least one instance of the members we
         # can iterate in the diff index - if not this indicates its not working correctly
-        # or our test does not span the whole range of possibilities
+        # or our test does not span the whole range of possibilities.
         for key, value in assertion_map.items():
             self.assertIsNotNone(value, "Did not find diff for %s" % key)
         # END for each iteration type
 
-        # test path not existing in the index - should be ignored
+        # Test path not existing in the index - should be ignored.
         c = self.rorepo.head.commit
         cp = c.parents[0]
         diff_index = c.diff(cp, ["does/not/exist"])
@@ -415,7 +416,7 @@ def test_diff_interface(self):
     def test_rename_override(self, rw_dir):
         """Test disabling of diff rename detection"""
 
-        # create and commit file_a.txt
+        # 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:
@@ -423,10 +424,10 @@ def test_rename_override(self, rw_dir):
         repo.git.add(Git.polish_url(file_a))
         repo.git.commit(message="Added file_a.txt")
 
-        # remove file_a.txt
+        # Remove file_a.txt.
         repo.git.rm(Git.polish_url(file_a))
 
-        # create and commit file_b.txt with similarity index of 52
+        # 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:
             outfile.write("hello world\nhello world")
@@ -436,7 +437,7 @@ def test_rename_override(self, rw_dir):
         commit_a = repo.commit("HEAD")
         commit_b = repo.commit("HEAD~1")
 
-        # check default diff command with renamed files enabled
+        # Check default diff command with renamed files enabled.
         diffs = commit_b.diff(commit_a)
         self.assertEqual(1, len(diffs))
         diff = diffs[0]
@@ -444,35 +445,35 @@ def test_rename_override(self, rw_dir):
         self.assertEqual("file_a.txt", diff.rename_from)
         self.assertEqual("file_b.txt", diff.rename_to)
 
-        # check diff with rename files disabled
+        # Check diff with rename files disabled.
         diffs = commit_b.diff(commit_a, no_renames=True)
         self.assertEqual(2, len(diffs))
 
-        # check fileA.txt deleted
+        # Check fileA.txt deleted.
         diff = diffs[0]
         self.assertEqual(True, diff.deleted_file)
         self.assertEqual("file_a.txt", diff.a_path)
 
-        # check fileB.txt added
+        # Check fileB.txt added.
         diff = diffs[1]
         self.assertEqual(True, diff.new_file)
         self.assertEqual("file_b.txt", diff.a_path)
 
-        # check diff with high similarity index
+        # Check diff with high similarity index.
         diffs = commit_b.diff(commit_a, split_single_char_options=False, M="75%")
         self.assertEqual(2, len(diffs))
 
-        # check fileA.txt deleted
+        # Check fileA.txt deleted.
         diff = diffs[0]
         self.assertEqual(True, diff.deleted_file)
         self.assertEqual("file_a.txt", diff.a_path)
 
-        # check fileB.txt added
+        # Check fileB.txt added.
         diff = diffs[1]
         self.assertEqual(True, diff.new_file)
         self.assertEqual("file_b.txt", diff.a_path)
 
-        # check diff with low similarity index
+        # Check diff with low similarity index.
         diffs = commit_b.diff(commit_a, split_single_char_options=False, M="40%")
         self.assertEqual(1, len(diffs))
         diff = diffs[0]
diff --git a/test/test_fun.py b/test/test_fun.py
index f39955aa0..0015b30c6 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -32,21 +32,21 @@ def _assert_index_entries(self, entries, trees):
         # END assert entry matches fully
 
     def test_aggressive_tree_merge(self):
-        # head tree with additions, removals and modification compared to its predecessor
+        # Head tree with additions, removals and modification compared to its predecessor.
         odb = self.rorepo.odb
         HC = self.rorepo.commit("6c1faef799095f3990e9970bc2cb10aa0221cf9c")
         H = HC.tree
         B = HC.parents[0].tree
 
-        # entries from single tree
+        # Entries from single tree.
         trees = [H.binsha]
         self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
 
-        # from multiple trees
+        # From multiple trees.
         trees = [B.binsha, H.binsha]
         self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
 
-        # three way, no conflict
+        # Three way, no conflict.
         tree = self.rorepo.tree
         B = tree("35a09c0534e89b2d43ec4101a5fb54576b577905")
         H = tree("4fe5cfa0e063a8d51a1eb6f014e2aaa994e5e7d4")
@@ -54,18 +54,18 @@ def test_aggressive_tree_merge(self):
         trees = [B.binsha, H.binsha, M.binsha]
         self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
 
-        # three-way, conflict in at least one file, both modified
+        # Three-way, conflict in at least one file, both modified.
         B = tree("a7a4388eeaa4b6b94192dce67257a34c4a6cbd26")
         H = tree("f9cec00938d9059882bb8eabdaf2f775943e00e5")
         M = tree("44a601a068f4f543f73fd9c49e264c931b1e1652")
         trees = [B.binsha, H.binsha, M.binsha]
         self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
 
-        # too many trees
+        # Too many trees.
         self.assertRaises(ValueError, aggressive_tree_merge, odb, trees * 2)
 
     def mktree(self, odb, entries):
-        """create a tree from the given tree entries and safe it to the database"""
+        """Create a tree from the given tree entries and safe it to the database."""
         sio = BytesIO()
         tree_to_stream(entries, sio.write)
         sio.seek(0)
@@ -92,84 +92,84 @@ def assert_entries(entries, num_entries, has_conflict=False):
 
         odb = rwrepo.odb
 
-        # base tree
+        # Base tree.
         bfn = "basefile"
         fbase = mkfile(bfn, shaa)
         tb = mktree(odb, [fbase])
 
-        # non-conflicting new files, same data
+        # Non-conflicting new files, same data.
         fa = mkfile("1", shab)
         th = mktree(odb, [fbase, fa])
         fb = mkfile("2", shac)
         tm = mktree(odb, [fbase, fb])
 
-        # two new files, same base file
+        # Two new files, same base file.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 3)
 
-        # both delete same file, add own one
+        # Both delete same file, add own one.
         fa = mkfile("1", shab)
         th = mktree(odb, [fa])
         fb = mkfile("2", shac)
         tm = mktree(odb, [fb])
 
-        # two new files
+        # Two new files.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 2)
 
-        # same file added in both, differently
+        # Same file added in both, differently.
         fa = mkfile("1", shab)
         th = mktree(odb, [fa])
         fb = mkfile("1", shac)
         tm = mktree(odb, [fb])
 
-        # expect conflict
+        # Expect conflict.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 2, True)
 
-        # same file added, different mode
+        # Same file added, different mode.
         fa = mkfile("1", shab)
         th = mktree(odb, [fa])
         fb = mkcommit("1", shab)
         tm = mktree(odb, [fb])
 
-        # expect conflict
+        # Expect conflict.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 2, True)
 
-        # same file added in both
+        # Same file added in both.
         fa = mkfile("1", shab)
         th = mktree(odb, [fa])
         fb = mkfile("1", shab)
         tm = mktree(odb, [fb])
 
-        # expect conflict
+        # Expect conflict.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 1)
 
-        # modify same base file, differently
+        # Modify same base file, differently.
         fa = mkfile(bfn, shab)
         th = mktree(odb, [fa])
         fb = mkfile(bfn, shac)
         tm = mktree(odb, [fb])
 
-        # conflict, 3 versions on 3 stages
+        # Conflict, 3 versions on 3 stages.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 3, True)
 
-        # change mode on same base file, by making one a commit, the other executable
-        # no content change ( this is totally unlikely to happen in the real world )
+        # Change mode on same base file, by making one a commit, the other executable,
+        # no content change (this is totally unlikely to happen in the real world).
         fa = mkcommit(bfn, shaa)
         th = mktree(odb, [fa])
         fb = mkfile(bfn, shaa, executable=1)
         tm = mktree(odb, [fb])
 
-        # conflict, 3 versions on 3 stages, because of different mode
+        # Conflict, 3 versions on 3 stages, because of different mode.
         trees = [tb, th, tm]
         assert_entries(aggressive_tree_merge(odb, trees), 3, True)
 
         for is_them in range(2):
-            # only we/they change contents
+            # Only we/they change contents.
             fa = mkfile(bfn, shab)
             th = mktree(odb, [fa])
 
@@ -179,7 +179,7 @@ def assert_entries(entries, num_entries, has_conflict=False):
             entries = aggressive_tree_merge(odb, trees)
             assert len(entries) == 1 and entries[0].binsha == shab
 
-            # only we/they change the mode
+            # Only we/they change the mode.
             fa = mkcommit(bfn, shaa)
             th = mktree(odb, [fa])
 
@@ -189,14 +189,14 @@ def assert_entries(entries, num_entries, has_conflict=False):
             entries = aggressive_tree_merge(odb, trees)
             assert len(entries) == 1 and entries[0].binsha == shaa and entries[0].mode == fa[1]
 
-            # one side deletes, the other changes = conflict
+            # One side deletes, the other changes = conflict.
             fa = mkfile(bfn, shab)
             th = mktree(odb, [fa])
             tm = mktree(odb, [])
             trees = [tb, th, tm]
             if is_them:
                 trees = [tb, tm, th]
-            # as one is deleted, there are only 2 entries
+            # As one is deleted, there are only 2 entries.
             assert_entries(aggressive_tree_merge(odb, trees), 2, True)
         # END handle ours, theirs
 
@@ -227,19 +227,19 @@ def _assert_tree_entries(self, entries, num_trees):
             assert len(entry) == num_trees
             paths = {e[2] for e in entry if e}
 
-            # only one path per set of entries
+            # Only one path per set of entries.
             assert len(paths) == 1
         # END verify entry
 
     def test_tree_traversal(self):
-        # low level tree tarversal
+        # Low level tree traversal.
         odb = self.rorepo.odb
         H = self.rorepo.tree("29eb123beb1c55e5db4aa652d843adccbd09ae18")  # head tree
         M = self.rorepo.tree("e14e3f143e7260de9581aee27e5a9b2645db72de")  # merge tree
         B = self.rorepo.tree("f606937a7a21237c866efafcad33675e6539c103")  # base tree
         B_old = self.rorepo.tree("1f66cfbbce58b4b552b041707a12d437cc5f400a")  # old base tree
 
-        # two very different trees
+        # Two very different trees.
         entries = traverse_trees_recursive(odb, [B_old.binsha, H.binsha], "")
         self._assert_tree_entries(entries, 2)
 
@@ -247,17 +247,17 @@ def test_tree_traversal(self):
         assert len(oentries) == len(entries)
         self._assert_tree_entries(oentries, 2)
 
-        # single tree
+        # Single tree.
         is_no_tree = lambda i, d: i.type != "tree"
         entries = traverse_trees_recursive(odb, [B.binsha], "")
         assert len(entries) == len(list(B.traverse(predicate=is_no_tree)))
         self._assert_tree_entries(entries, 1)
 
-        # two trees
+        # Two trees.
         entries = traverse_trees_recursive(odb, [B.binsha, H.binsha], "")
         self._assert_tree_entries(entries, 2)
 
-        # tree trees
+        # Three trees.
         entries = traverse_trees_recursive(odb, [B.binsha, H.binsha, M.binsha], "")
         self._assert_tree_entries(entries, 3)
 
@@ -275,7 +275,7 @@ def test_tree_traversal_single(self):
 
     @with_rw_directory
     def test_linked_worktree_traversal(self, rw_dir):
-        """Check that we can identify a linked worktree based on a .git file"""
+        """Check that we can identify a linked worktree based on a .git file."""
         git = Git(rw_dir)
         if git.version_info[:3] < (2, 5, 1):
             raise RuntimeError("worktree feature unsupported (test needs git 2.5.1 or later)")
diff --git a/test/test_git.py b/test/test_git.py
index cf82d9ac7..aa7a415e5 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import inspect
 import logging
 import os
@@ -77,14 +78,14 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
         self.assertEqual(["--max-count=0"], self.git.transform_kwargs(**{"max_count": 0}))
         self.assertEqual([], self.git.transform_kwargs(**{"max_count": None}))
 
-        # Multiple args are supported by using lists/tuples
+        # Multiple args are supported by using lists/tuples.
         self.assertEqual(
             ["-L", "1-3", "-L", "12-18"],
             self.git.transform_kwargs(**{"L": ("1-3", "12-18")}),
         )
         self.assertEqual(["-C", "-C"], self.git.transform_kwargs(**{"C": [True, True, None, False]}))
 
-        # order is undefined
+        # Order is undefined.
         res = self.git.transform_kwargs(**{"s": True, "t": True})
         self.assertEqual({"-s", "-t"}, set(res))
 
@@ -189,7 +190,7 @@ def test_it_accepts_stdin(self):
 
     @mock.patch.object(Git, "execute")
     def test_it_ignores_false_kwargs(self, git):
-        # this_should_not_be_ignored=False implies it *should* be ignored
+        # this_should_not_be_ignored=False implies it *should* be ignored.
         self.git.version(pass_this_kwarg=False)
         self.assertTrue("pass_this_kwarg" not in git.call_args[1])
 
@@ -218,31 +219,31 @@ def test_it_accepts_environment_variables(self):
             self.assertEqual(commit, "4cfd6b0314682d5a58f80be39850bad1640e9241")
 
     def test_persistent_cat_file_command(self):
-        # read header only
+        # Read header only.
         hexsha = "b2339455342180c7cc1e9bba3e9f181f7baa5167"
         g = self.git.cat_file(batch_check=True, istream=subprocess.PIPE, as_process=True)
         g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n")
         g.stdin.flush()
         obj_info = g.stdout.readline()
 
-        # read header + data
+        # Read header + data.
         g = self.git.cat_file(batch=True, istream=subprocess.PIPE, as_process=True)
         g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n")
         g.stdin.flush()
         obj_info_two = g.stdout.readline()
         self.assertEqual(obj_info, obj_info_two)
 
-        # read data - have to read it in one large chunk
+        # Read data - have to read it in one large chunk.
         size = int(obj_info.split()[2])
         g.stdout.read(size)
         g.stdout.read(1)
 
-        # now we should be able to read a new object
+        # Now we should be able to read a new object.
         g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n")
         g.stdin.flush()
         self.assertEqual(g.stdout.readline(), obj_info)
 
-        # same can be achieved using the respective command functions
+        # Same can be achieved using the respective command functions.
         hexsha, typename, size = self.git.get_object_header(hexsha)
         hexsha, typename_two, size_two, _ = self.git.get_object_data(hexsha)
         self.assertEqual(typename, typename_two)
@@ -264,31 +265,31 @@ def test_cmd_override(self):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
     def test_refresh(self):
-        # test a bad git path refresh
+        # Test a bad git path refresh.
         self.assertRaises(GitCommandNotFound, refresh, "yada")
 
-        # test a good path refresh
+        # Test a good path refresh.
         which_cmd = "where" if is_win else "command -v"
         path = os.popen("{0} git".format(which_cmd)).read().strip().split("\n")[0]
         refresh(path)
 
     def test_options_are_passed_to_git(self):
-        # This work because any command after git --version is ignored
+        # This works because any command after git --version is ignored.
         git_version = self.git(version=True).NoOp()
         git_command_version = self.git.version()
         self.assertEqual(git_version, git_command_version)
 
     def test_persistent_options(self):
         git_command_version = self.git.version()
-        # analog to test_options_are_passed_to_git
+        # Analog to test_options_are_passed_to_git.
         self.git.set_persistent_git_options(version=True)
         git_version = self.git.NoOp()
         self.assertEqual(git_version, git_command_version)
-        # subsequent calls keep this option:
+        # Subsequent calls keep this option:
         git_version_2 = self.git.NoOp()
         self.assertEqual(git_version_2, git_command_version)
 
-        # reset to empty:
+        # Reset to empty:
         self.git.set_persistent_git_options()
         self.assertRaises(GitCommandError, self.git.NoOp)
 
@@ -301,7 +302,7 @@ def test_change_to_transform_kwargs_does_not_break_command_options(self):
         self.git.log(n=1)
 
     def test_insert_after_kwarg_raises(self):
-        # This isn't a complete add command, which doesn't matter here
+        # This isn't a complete add command, which doesn't matter here.
         self.assertRaises(ValueError, self.git.remote, "add", insert_kwargs_after="foo")
 
     def test_env_vars_passed_to_git(self):
@@ -311,10 +312,10 @@ def test_env_vars_passed_to_git(self):
 
     @with_rw_directory
     def test_environment(self, rw_dir):
-        # sanity check
+        # Sanity check.
         self.assertEqual(self.git.environment(), {})
 
-        # make sure the context manager works and cleans up after itself
+        # Make sure the context manager works and cleans up after itself.
         with self.git.custom_environment(PWD="/tmp"):
             self.assertEqual(self.git.environment(), {"PWD": "/tmp"})
 
diff --git a/test/test_index.py b/test/test_index.py
index 06db3aedd..c73d25543 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -93,7 +93,7 @@ def _fprogress_add(self, path, done, item):
         self._fprogress(path, done, item)
 
     def _reset_progress(self):
-        # maps paths to the count of calls
+        # Maps paths to the count of calls.
         self._fprogress_map = {}
 
     def _assert_entries(self, entries):
@@ -104,12 +104,12 @@ def _assert_entries(self, entries):
         # END for each entry
 
     def test_index_file_base(self):
-        # read from file
+        # Read from file.
         index = IndexFile(self.rorepo, fixture_path("index"))
         assert index.entries
         assert index.version > 0
 
-        # test entry
+        # Test entry.
         entry = next(iter(index.entries.values()))
         for attr in (
             "path",
@@ -128,17 +128,17 @@ def test_index_file_base(self):
             getattr(entry, attr)
         # END for each method
 
-        # test update
+        # Test update.
         entries = index.entries
         assert isinstance(index.update(), IndexFile)
         assert entries is not index.entries
 
-        # test stage
+        # Test stage.
         index_merge = IndexFile(self.rorepo, fixture_path("index_merge"))
         self.assertEqual(len(index_merge.entries), 106)
         assert len([e for e in index_merge.entries.values() if e.stage != 0])
 
-        # write the data - it must match the original
+        # Write the data - it must match the original.
         tmpfile = tempfile.mktemp()
         index_merge.write(tmpfile)
         with open(tmpfile, "rb") as fp:
@@ -146,7 +146,7 @@ def test_index_file_base(self):
         os.remove(tmpfile)
 
     def _cmp_tree_index(self, tree, index):
-        # fail unless both objects contain the same paths and blobs
+        # Fail unless both objects contain the same paths and blobs.
         if isinstance(tree, str):
             tree = self.rorepo.commit(tree).tree
 
@@ -169,14 +169,14 @@ def add_bad_blob():
             rw_repo.index.add([Blob(rw_repo, b"f" * 20, "bad-permissions", "foo")])
 
         try:
-            ## 1st fail on purpose adding into index.
+            ## First, fail on purpose adding into index.
             add_bad_blob()
         except Exception as ex:
             msg_py3 = "required argument is not an integer"
             msg_py2 = "cannot convert argument to integer"
             assert msg_py2 in str(ex) or msg_py3 in str(ex)
 
-        ## 2nd time should not fail due to stray lock file
+        ## The second time should not fail due to stray lock file.
         try:
             add_bad_blob()
         except Exception as ex:
@@ -188,17 +188,17 @@ def test_index_file_from_tree(self, rw_repo):
         cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573"
         other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9"
 
-        # simple index from tree
+        # Simple index from tree.
         base_index = IndexFile.from_tree(rw_repo, common_ancestor_sha)
         assert base_index.entries
         self._cmp_tree_index(common_ancestor_sha, base_index)
 
-        # merge two trees - its like a fast-forward
+        # Merge two trees - it's like a fast-forward.
         two_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha)
         assert two_way_index.entries
         self._cmp_tree_index(cur_sha, two_way_index)
 
-        # merge three trees - here we have a merge conflict
+        # Merge three trees - here we have a merge conflict.
         three_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha, other_sha)
         assert len([e for e in three_way_index.entries.values() if e.stage != 0])
 
@@ -209,19 +209,19 @@ def test_index_file_from_tree(self, rw_repo):
         assert merge_blobs[0][0] in (1, 2, 3)
         assert isinstance(merge_blobs[0][1], Blob)
 
-        # test BlobFilter
+        # Test BlobFilter.
         prefix = "lib/git"
         for _stage, blob in base_index.iter_blobs(BlobFilter([prefix])):
             assert blob.path.startswith(prefix)
 
-        # writing a tree should fail with an unmerged index
+        # Writing a tree should fail with an unmerged index.
         self.assertRaises(UnmergedEntriesError, three_way_index.write_tree)
 
-        # removed unmerged entries
+        # Removed unmerged entries.
         unmerged_blob_map = three_way_index.unmerged_blobs()
         assert unmerged_blob_map
 
-        # pick the first blob at the first stage we find and use it as resolved version
+        # Pick the first blob at the first stage we find and use it as resolved version.
         three_way_index.resolve_blobs(line[0][1] for line in unmerged_blob_map.values())
         tree = three_way_index.write_tree()
         assert isinstance(tree, Tree)
@@ -239,13 +239,13 @@ def test_index_merge_tree(self, rw_repo):
         self.assertEqual(len({self.rorepo, self.rorepo, rw_repo, rw_repo}), 2)
 
         # SINGLE TREE MERGE
-        # current index is at the (virtual) cur_commit
+        # Current index is at the (virtual) cur_commit.
         next_commit = "4c39f9da792792d4e73fc3a5effde66576ae128c"
         parent_commit = rw_repo.head.commit.parents[0]
         manifest_key = IndexFile.entry_key("MANIFEST.in", 0)
         manifest_entry = rw_repo.index.entries[manifest_key]
         rw_repo.index.merge_tree(next_commit)
-        # only one change should be recorded
+        # Only one change should be recorded.
         assert manifest_entry.binsha != rw_repo.index.entries[manifest_key].binsha
 
         rw_repo.index.reset(rw_repo.head)
@@ -255,40 +255,40 @@ def test_index_merge_tree(self, rw_repo):
         #############
         # Add a change with a NULL sha that should conflict with next_commit. We
         # pretend there was a change, but we do not even bother adding a proper
-        # sha for it ( which makes things faster of course )
+        # sha for it (which makes things faster of course).
         manifest_fake_entry = BaseIndexEntry((manifest_entry[0], b"\0" * 20, 0, manifest_entry[3]))
-        # try write flag
+        # Try write flag.
         self._assert_entries(rw_repo.index.add([manifest_fake_entry], write=False))
-        # add actually resolves the null-hex-sha for us as a feature, but we can
-        # edit the index manually
+        # Add actually resolves the null-hex-sha for us as a feature, but we can
+        # edit the index manually.
         assert rw_repo.index.entries[manifest_key].binsha != Object.NULL_BIN_SHA
-        # must operate on the same index for this ! Its a bit problematic as
-        # it might confuse people
+        # We must operate on the same index for this! It's a bit problematic as
+        # it might confuse people.
         index = rw_repo.index
         index.entries[manifest_key] = IndexEntry.from_base(manifest_fake_entry)
         index.write()
         self.assertEqual(rw_repo.index.entries[manifest_key].hexsha, Diff.NULL_HEX_SHA)
 
-        # write an unchanged index ( just for the fun of it )
+        # Write an unchanged index (just for the fun of it).
         rw_repo.index.write()
 
-        # a three way merge would result in a conflict and fails as the command will
+        # A three way merge would result in a conflict and fails as the command will
         # not overwrite any entries in our index and hence leave them unmerged. This is
-        # mainly a protection feature as the current index is not yet in a tree
+        # mainly a protection feature as the current index is not yet in a tree.
         self.assertRaises(GitCommandError, index.merge_tree, next_commit, base=parent_commit)
 
-        # the only way to get the merged entries is to safe the current index away into a tree,
-        # which is like a temporary commit for us. This fails as well as the NULL sha deos not
-        # have a corresponding object
-        # NOTE: missing_ok is not a kwarg anymore, missing_ok is always true
+        # The only way to get the merged entries is to safe the current index away into a tree,
+        # which is like a temporary commit for us. This fails as well as the NULL sha does not
+        # have a corresponding object.
+        # NOTE: missing_ok is not a kwarg anymore, missing_ok is always true.
         # self.assertRaises(GitCommandError, index.write_tree)
 
-        # if missing objects are okay, this would work though ( they are always okay now )
-        # As we can't read back the tree with NULL_SHA, we rather set it to something else
+        # If missing objects are okay, this would work though (they are always okay now).
+        # As we can't read back the tree with NULL_SHA, we rather set it to something else.
         index.entries[manifest_key] = IndexEntry(manifest_entry[:1] + (hex_to_bin("f" * 40),) + manifest_entry[2:])
         tree = index.write_tree()
 
-        # now make a proper three way merge with unmerged entries
+        # Now make a proper three way merge with unmerged entries.
         unmerged_tree = IndexFile.from_tree(rw_repo, parent_commit, tree, next_commit)
         unmerged_blobs = unmerged_tree.unmerged_blobs()
         self.assertEqual(len(unmerged_blobs), 1)
@@ -296,49 +296,49 @@ def test_index_merge_tree(self, rw_repo):
 
     @with_rw_repo("0.1.6")
     def test_index_file_diffing(self, rw_repo):
-        # default Index instance points to our index
+        # Default Index instance points to our index.
         index = IndexFile(rw_repo)
         assert index.path is not None
         assert len(index.entries)
 
-        # write the file back
+        # Write the file back.
         index.write()
 
-        # could sha it, or check stats
+        # Could sha it, or check stats.
 
-        # test diff
-        # resetting the head will leave the index in a different state, and the
-        # diff will yield a few changes
+        # Test diff.
+        # Resetting the head will leave the index in a different state, and the
+        # diff will yield a few changes.
         cur_head_commit = rw_repo.head.reference.commit
         rw_repo.head.reset("HEAD~6", index=True, working_tree=False)
 
-        # diff against same index is 0
+        # Diff against same index is 0.
         diff = index.diff()
         self.assertEqual(len(diff), 0)
 
-        # against HEAD as string, must be the same as it matches index
+        # Against HEAD as string, must be the same as it matches index.
         diff = index.diff("HEAD")
         self.assertEqual(len(diff), 0)
 
-        # against previous head, there must be a difference
+        # Against previous head, there must be a difference.
         diff = index.diff(cur_head_commit)
         assert len(diff)
 
-        # we reverse the result
+        # We reverse the result.
         adiff = index.diff(str(cur_head_commit), R=True)
-        odiff = index.diff(cur_head_commit, R=False)  # now its not reversed anymore
+        odiff = index.diff(cur_head_commit, R=False)  # Now its not reversed anymore.
         assert adiff != odiff
-        self.assertEqual(odiff, diff)  # both unreversed diffs against HEAD
+        self.assertEqual(odiff, diff)  # Both unreversed diffs against HEAD.
 
-        # against working copy - its still at cur_commit
+        # Against working copy - it's still at cur_commit.
         wdiff = index.diff(None)
         assert wdiff != adiff
         assert wdiff != odiff
 
-        # against something unusual
+        # Against something unusual.
         self.assertRaises(ValueError, index.diff, int)
 
-        # adjust the index to match an old revision
+        # Adjust the index to match an old revision.
         cur_branch = rw_repo.active_branch
         cur_commit = cur_branch.commit
         rev_head_parent = "HEAD~1"
@@ -347,10 +347,10 @@ def test_index_file_diffing(self, rw_repo):
         self.assertEqual(cur_branch, rw_repo.active_branch)
         self.assertEqual(cur_commit, rw_repo.head.commit)
 
-        # there must be differences towards the working tree which is in the 'future'
+        # There must be differences towards the working tree which is in the 'future'.
         assert index.diff(None)
 
-        # reset the working copy as well to current head,to pull 'back' as well
+        # Reset the working copy as well to current head, to pull 'back' as well.
         new_data = b"will be reverted"
         file_path = osp.join(rw_repo.working_tree_dir, "CHANGES")
         with open(file_path, "wb") as fp:
@@ -362,7 +362,7 @@ def test_index_file_diffing(self, rw_repo):
         with open(file_path, "rb") as fp:
             assert fp.read() != new_data
 
-        # test full checkout
+        # Test full checkout.
         test_file = osp.join(rw_repo.working_tree_dir, "CHANGES")
         with open(test_file, "ab") as fd:
             fd.write(b"some data")
@@ -377,25 +377,25 @@ def test_index_file_diffing(self, rw_repo):
         self._assert_fprogress([None])
         assert osp.isfile(test_file)
 
-        # individual file
+        # Individual file.
         os.remove(test_file)
         rval = index.checkout(test_file, fprogress=self._fprogress)
         self.assertEqual(list(rval)[0], "CHANGES")
         self._assert_fprogress([test_file])
         assert osp.exists(test_file)
 
-        # checking out non-existing file throws
+        # Checking out non-existing file throws.
         self.assertRaises(CheckoutError, index.checkout, "doesnt_exist_ever.txt.that")
         self.assertRaises(CheckoutError, index.checkout, paths=["doesnt/exist"])
 
-        # checkout file with modifications
+        # Check out file with modifications.
         append_data = b"hello"
         with open(test_file, "ab") as fp:
             fp.write(append_data)
         try:
             index.checkout(test_file)
         except CheckoutError as e:
-            # detailed exceptions are only possible in older git versions
+            # Detailed exceptions are only possible in older git versions.
             if rw_repo.git._version_info < (2, 29):
                 self.assertEqual(len(e.failed_files), 1)
                 self.assertEqual(e.failed_files[0], osp.basename(test_file))
@@ -408,19 +408,17 @@ def test_index_file_diffing(self, rw_repo):
         else:
             raise AssertionError("Exception CheckoutError not thrown")
 
-        # if we force it it should work
+        # If we force it, it should work.
         index.checkout(test_file, force=True)
         assert not open(test_file, "rb").read().endswith(append_data)
 
-        # checkout directory
+        # Check out directory.
         rmtree(osp.join(rw_repo.working_tree_dir, "lib"))
         rval = index.checkout("lib")
         assert len(list(rval)) > 1
 
     def _count_existing(self, repo, files):
-        """
-        Returns count of files that actually exist in the repository directory.
-        """
+        """Return count of files that actually exist in the repository directory."""
         existing = 0
         basedir = repo.working_tree_dir
         for f in files:
@@ -443,8 +441,8 @@ def test_index_mutation(self, rw_repo):
             writer.set_value("user", "email", umail)
         self.assertEqual(writer.get_value("user", "name"), uname)
 
-        # remove all of the files, provide a wild mix of paths, BaseIndexEntries,
-        # IndexEntries
+        # Remove all of the files, provide a wild mix of paths, BaseIndexEntries,
+        # IndexEntries.
         def mixed_iterator():
             count = 0
             for entry in index.entries.values():
@@ -468,29 +466,29 @@ def mixed_iterator():
         self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
         self.assertEqual(len(index.entries), 0)
 
-        # reset the index to undo our changes
+        # Reset the index to undo our changes.
         index.reset()
         self.assertEqual(len(index.entries), num_entries)
 
-        # remove with working copy
+        # Remove with working copy.
         deleted_files = index.remove(mixed_iterator(), working_tree=True)
         assert deleted_files
         self.assertEqual(self._count_existing(rw_repo, deleted_files), 0)
 
-        # reset everything
+        # Reset everything.
         index.reset(working_tree=True)
         self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
 
-        # invalid type
+        # Invalid type.
         self.assertRaises(TypeError, index.remove, [1])
 
-        # absolute path
+        # Absolute path.
         deleted_files = index.remove([osp.join(rw_repo.working_tree_dir, "lib")], r=True)
         assert len(deleted_files) > 1
         self.assertRaises(ValueError, index.remove, ["/doesnt/exists"])
 
         # TEST COMMITTING
-        # commit changed index
+        # Commit changed index.
         cur_commit = cur_head.commit
         commit_message = "commit default head by Frèderic Çaufl€"
 
@@ -505,7 +503,7 @@ def mixed_iterator():
         self.assertEqual(len(new_commit.parents), 1)
         self.assertEqual(cur_head.commit, cur_commit)
 
-        # commit with other actor
+        # Commit with other actor.
         cur_commit = cur_head.commit
 
         my_author = Actor("Frèderic Çaufl€", "author@example.com")
@@ -522,7 +520,7 @@ def mixed_iterator():
         self.assertEqual(cur_head.commit, commit_actor)
         self.assertEqual(cur_head.log()[-1].actor, my_committer)
 
-        # commit with author_date and commit_date
+        # Commit with author_date and commit_date.
         cur_commit = cur_head.commit
         commit_message = "commit with dates by Avinash Sajjanshetty"
 
@@ -537,14 +535,14 @@ def mixed_iterator():
         self.assertEqual(new_commit.authored_date, 1144447993)
         self.assertEqual(new_commit.committed_date, 1112911993)
 
-        # same index, no parents
+        # Same index, no parents.
         commit_message = "index without parents"
         commit_no_parents = index.commit(commit_message, parent_commits=[], head=True)
         self.assertEqual(commit_no_parents.message, commit_message)
         self.assertEqual(len(commit_no_parents.parents), 0)
         self.assertEqual(cur_head.commit, commit_no_parents)
 
-        # same index, multiple parents
+        # same index, multiple parents.
         commit_message = "Index with multiple parents\n    commit with another line"
         commit_multi_parent = index.commit(commit_message, parent_commits=(commit_no_parents, new_commit))
         self.assertEqual(commit_multi_parent.message, commit_message)
@@ -553,41 +551,41 @@ def mixed_iterator():
         self.assertEqual(commit_multi_parent.parents[1], new_commit)
         self.assertEqual(cur_head.commit, commit_multi_parent)
 
-        # re-add all files in lib
-        # get the lib folder back on disk, but get an index without it
+        # Re-add all files in lib.
+        # Get the lib folder back on disk, but get an index without it.
         index.reset(new_commit.parents[0], working_tree=True).reset(new_commit, working_tree=False)
         lib_file_path = osp.join("lib", "git", "__init__.py")
         assert (lib_file_path, 0) not in index.entries
         assert osp.isfile(osp.join(rw_repo.working_tree_dir, lib_file_path))
 
-        # directory
+        # Directory.
         entries = index.add(["lib"], fprogress=self._fprogress_add)
         self._assert_entries(entries)
         self._assert_fprogress(entries)
         assert len(entries) > 1
 
-        # glob
+        # Glob.
         entries = index.reset(new_commit).add([osp.join("lib", "git", "*.py")], fprogress=self._fprogress_add)
         self._assert_entries(entries)
         self._assert_fprogress(entries)
         self.assertEqual(len(entries), 14)
 
-        # same file
+        # Same file.
         entries = index.reset(new_commit).add(
             [osp.join(rw_repo.working_tree_dir, "lib", "git", "head.py")] * 2,
             fprogress=self._fprogress_add,
         )
         self._assert_entries(entries)
         self.assertEqual(entries[0].mode & 0o644, 0o644)
-        # would fail, test is too primitive to handle this case
+        # Would fail, test is too primitive to handle this case.
         # self._assert_fprogress(entries)
         self._reset_progress()
         self.assertEqual(len(entries), 2)
 
-        # missing path
+        # Missing path.
         self.assertRaises(OSError, index.reset(new_commit).add, ["doesnt/exist/must/raise"])
 
-        # blob from older revision overrides current index revision
+        # Blob from older revision overrides current index revision.
         old_blob = new_commit.parents[0].tree.blobs[0]
         entries = index.reset(new_commit).add([old_blob], fprogress=self._fprogress_add)
         self._assert_entries(entries)
@@ -595,7 +593,7 @@ def mixed_iterator():
         self.assertEqual(index.entries[(old_blob.path, 0)].hexsha, old_blob.hexsha)
         self.assertEqual(len(entries), 1)
 
-        # mode 0 not allowed
+        # Mode 0 not allowed.
         null_hex_sha = Diff.NULL_HEX_SHA
         null_bin_sha = b"\0" * 20
         self.assertRaises(
@@ -604,7 +602,7 @@ def mixed_iterator():
             [BaseIndexEntry((0, null_bin_sha, 0, "doesntmatter"))],
         )
 
-        # add new file
+        # Add new file.
         new_file_relapath = "my_new_file"
         self._make_file(new_file_relapath, "hello world", rw_repo)
         entries = index.reset(new_commit).add(
@@ -616,7 +614,7 @@ def mixed_iterator():
         self.assertEqual(len(entries), 1)
         self.assertNotEqual(entries[0].hexsha, null_hex_sha)
 
-        # add symlink
+        # Add symlink.
         if not is_win:
             for target in ("/etc/nonexisting", "/etc/passwd", "/etc"):
                 basename = "my_real_symlink"
@@ -630,7 +628,7 @@ def mixed_iterator():
                 self.assertTrue(S_ISLNK(entries[0].mode))
                 self.assertTrue(S_ISLNK(index.entries[index.entry_key("my_real_symlink", 0)].mode))
 
-                # we expect only the target to be written
+                # We expect only the target to be written.
                 self.assertEqual(
                     index.repo.odb.stream(entries[0].binsha).read().decode("ascii"),
                     target,
@@ -640,7 +638,7 @@ def mixed_iterator():
             # end for each target
         # END real symlink test
 
-        # add fake symlink and assure it checks-our as symlink
+        # Add fake symlink and assure it checks-our as symlink.
         fake_symlink_relapath = "my_fake_symlink"
         link_target = "/etc/that"
         fake_symlink_path = self._make_file(fake_symlink_relapath, link_target, rw_repo)
@@ -652,7 +650,7 @@ def mixed_iterator():
         self.assertEqual(len(entries), 1)
         self.assertTrue(S_ISLNK(entries[0].mode))
 
-        # assure this also works with an alternate method
+        # Check that this also works with an alternate method.
         full_index_entry = IndexEntry.from_base(BaseIndexEntry((0o120000, entries[0].binsha, 0, entries[0].path)))
         entry_key = index.entry_key(full_index_entry)
         index.reset(new_commit)
@@ -660,24 +658,24 @@ def mixed_iterator():
         assert entry_key not in index.entries
         index.entries[entry_key] = full_index_entry
         index.write()
-        index.update()  # force reread of entries
+        index.update()  # Force reread of entries.
         new_entry = index.entries[entry_key]
         assert S_ISLNK(new_entry.mode)
 
-        # a tree created from this should contain the symlink
+        # A tree created from this should contain the symlink.
         tree = index.write_tree()
         assert fake_symlink_relapath in tree
-        index.write()  # flush our changes for the checkout
+        index.write()  # Flush our changes for the checkout.
 
-        # checkout the fakelink, should be a link then
+        # Check out the fake link, should be a link then.
         assert not S_ISLNK(os.stat(fake_symlink_path)[ST_MODE])
         os.remove(fake_symlink_path)
         index.checkout(fake_symlink_path)
 
-        # on windows we will never get symlinks
+        # On Windows, we will never get symlinks.
         if is_win:
-            # simlinks should contain the link as text ( which is what a
-            # symlink actually is )
+            # Symlinks should contain the link as text (which is what a
+            # symlink actually is).
             with open(fake_symlink_path, "rt") as fd:
                 self.assertEqual(fd.read(), link_target)
         else:
@@ -692,24 +690,24 @@ def assert_mv_rval(rval):
         # END move assertion utility
 
         self.assertRaises(ValueError, index.move, ["just_one_path"])
-        # file onto existing file
+        # Try to move a file onto an existing file.
         files = ["AUTHORS", "LICENSE"]
         self.assertRaises(GitCommandError, index.move, files)
 
-        # again, with force
+        # Again, with force.
         assert_mv_rval(index.move(files, f=True))
 
-        # files into directory - dry run
+        # Move files into a directory - dry run.
         paths = ["LICENSE", "VERSION", "doc"]
         rval = index.move(paths, dry_run=True)
         self.assertEqual(len(rval), 2)
         assert osp.exists(paths[0])
 
-        # again, no dry run
+        # Again, no dry run.
         rval = index.move(paths)
         assert_mv_rval(rval)
 
-        # dir into dir
+        # Move dir into dir.
         rval = index.move(["doc", "test"])
         assert_mv_rval(rval)
 
@@ -725,7 +723,7 @@ def rewriter(entry):
         # END rewriter
 
         def make_paths():
-            # two existing ones, one new one
+            """Help out the test by yielding two existing paths and one new path."""
             yield "CHANGES"
             yield "ez_setup.py"
             yield index.entries[index.entry_key("README", 0)]
@@ -766,12 +764,12 @@ def make_paths():
         for fkey in keys:
             assert fkey in index.entries
 
-        # just the index
+        # Just the index.
         index.reset(paths=(arela, afile))
         assert akey not in index.entries
         assert bkey in index.entries
 
-        # now with working tree - files on disk as well as entries must be recreated
+        # Now with working tree - files on disk as well as entries must be recreated.
         rw_repo.head.commit = nc
         for absfile in absfiles:
             os.remove(absfile)
@@ -785,8 +783,8 @@ def make_paths():
 
     @with_rw_repo("HEAD")
     def test_compare_write_tree(self, rw_repo):
-        # write all trees and compare them
-        # its important to have a few submodules in there too
+        """Test writing all trees, comparing them for equality."""
+        # It's important to have a few submodules in there too.
         max_count = 25
         count = 0
         for commit in rw_repo.head.commit.traverse():
@@ -847,7 +845,7 @@ def test_index_bare_add(self, rw_bare_repo):
 
     @with_rw_directory
     def test_add_utf8P_path(self, rw_dir):
-        # NOTE: fp is not a Unicode object in python 2 (which is the source of the problem)
+        # NOTE: fp is not a Unicode object in Python 2 (which is the source of the problem).
         fp = osp.join(rw_dir, "ø.txt")
         with open(fp, "wb") as fs:
             fs.write("content of ø".encode("utf-8"))
@@ -858,7 +856,7 @@ def test_add_utf8P_path(self, rw_dir):
 
     @with_rw_directory
     def test_add_a_file_with_wildcard_chars(self, rw_dir):
-        # see issue #407
+        # See issue #407.
         fp = osp.join(rw_dir, "[.exe")
         with open(fp, "wb") as f:
             f.write(b"something")
diff --git a/test/test_reflog.py b/test/test_reflog.py
index e899ac48c..d5173d2f4 100644
--- a/test/test_reflog.py
+++ b/test/test_reflog.py
@@ -26,7 +26,7 @@ def test_reflogentry(self):
         assert e.time[1] == 1
         assert e.message == msg
 
-        # check representation (roughly)
+        # Check representation (roughly).
         assert repr(e).startswith(nullhexsha)
 
     def test_base(self):
@@ -38,27 +38,27 @@ def test_base(self):
         rlp_master_ro = RefLog.path(self.rorepo.head)
         assert osp.isfile(rlp_master_ro)
 
-        # simple read
+        # Simple read.
         reflog = RefLog.from_file(rlp_master_ro)
         assert reflog._path is not None
         assert isinstance(reflog, RefLog)
         assert len(reflog)
 
-        # iter_entries works with path and with stream
+        # iter_entries works with path and with stream.
         assert len(list(RefLog.iter_entries(open(rlp_master, "rb"))))
         assert len(list(RefLog.iter_entries(rlp_master)))
 
-        # raise on invalid revlog
-        # TODO: Try multiple corrupted ones !
+        # Raise on invalid revlog.
+        # TODO: Try multiple corrupted ones!
         pp = "reflog_invalid_"
         for suffix in ("oldsha", "newsha", "email", "date", "sep"):
             self.assertRaises(ValueError, RefLog.from_file, fixture_path(pp + suffix))
         # END for each invalid file
 
-        # cannot write an uninitialized reflog
+        # Cannot write an uninitialized reflog.
         self.assertRaises(ValueError, RefLog().write)
 
-        # test serialize and deserialize - results must match exactly
+        # Test serialize and deserialize - results must match exactly.
         binsha = hex_to_bin(("f" * 40).encode("ascii"))
         msg = "my reflog message"
         cr = self.rorepo.config_reader()
@@ -68,33 +68,33 @@ def test_base(self):
             reflog.to_file(tfile)
             assert reflog.write() is reflog
 
-            # parsed result must match ...
+            # Parsed result must match...
             treflog = RefLog.from_file(tfile)
             assert treflog == reflog
 
-            # ... as well as each bytes of the written stream
+            # ...as well as each bytes of the written stream.
             assert open(tfile).read() == open(rlp).read()
 
-            # append an entry
+            # Append an entry.
             entry = RefLog.append_entry(cr, tfile, IndexObject.NULL_BIN_SHA, binsha, msg)
             assert entry.oldhexsha == IndexObject.NULL_HEX_SHA
             assert entry.newhexsha == "f" * 40
             assert entry.message == msg
             assert RefLog.from_file(tfile)[-1] == entry
 
-            # index entry
-            # raises on invalid index
+            # Index entry.
+            # Raises on invalid index.
             self.assertRaises(IndexError, RefLog.entry_at, rlp, 10000)
 
-            # indices can be positive ...
+            # Indices can be positive...
             assert isinstance(RefLog.entry_at(rlp, 0), RefLogEntry)
             RefLog.entry_at(rlp, 23)
 
-            # ... and negative
+            # ...and negative.
             for idx in (-1, -24):
                 RefLog.entry_at(rlp, idx)
             # END for each index to read
         # END for each reflog
 
-        # finally remove our temporary data
+        # Finally remove our temporary data.
         rmtree(tdir)
diff --git a/test/test_refs.py b/test/test_refs.py
index f9fc8b0ad..ae07ce421 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -30,7 +30,7 @@
 
 class TestRefs(TestBase):
     def test_from_path(self):
-        # should be able to create any reference directly
+        # Should be able to create any reference directly.
         for ref_type in (Reference, Head, TagReference, RemoteReference):
             for name in ("rela_name", "path/rela_name"):
                 full_path = ref_type.to_full_path(name)
@@ -39,9 +39,9 @@ def test_from_path(self):
             # END for each name
         # END for each type
 
-        # invalid path
+        # Invalid path.
         self.assertRaises(ValueError, TagReference, self.rorepo, "refs/invalid/tag")
-        # works without path check
+        # Works without path check.
         TagReference(self.rorepo, "refs/invalid/tag", check_path=False)
 
     def test_tag_base(self):
@@ -53,7 +53,7 @@ def test_tag_base(self):
             if tag.tag is not None:
                 tag_object_refs.append(tag)
                 tagobj = tag.tag
-                # have no dict
+                # Have no dict.
                 self.assertRaises(AttributeError, setattr, tagobj, "someattr", 1)
                 assert isinstance(tagobj, TagObject)
                 assert tagobj.tag == tag.name
@@ -62,7 +62,7 @@ def test_tag_base(self):
                 assert isinstance(tagobj.tagger_tz_offset, int)
                 assert tagobj.message
                 assert tag.object == tagobj
-                # can't assign the object
+                # Can't assign the object.
                 self.assertRaises(AttributeError, setattr, tag, "object", tagobj)
             # END if we have a tag object
         # END for tag in repo-tags
@@ -77,7 +77,7 @@ def test_tags_author(self):
         assert tagger_name == "Michael Trier"
 
     def test_tags(self):
-        # tag refs can point to tag objects or to commits
+        # Tag refs can point to tag objects or to commits.
         s = set()
         ref_count = 0
         for ref in chain(self.rorepo.tags, self.rorepo.heads):
@@ -100,8 +100,8 @@ def test_heads(self, rwrepo):
             assert "refs/heads" in head.path
             prev_object = head.object
             cur_object = head.object
-            assert prev_object == cur_object  # represent the same git object
-            assert prev_object is not cur_object  # but are different instances
+            assert prev_object == cur_object  # Represent the same git object...
+            assert prev_object is not cur_object  # ...but are different instances.
 
             with head.config_writer() as writer:
                 tv = "testopt"
@@ -111,7 +111,7 @@ def test_heads(self, rwrepo):
             with head.config_writer() as writer:
                 writer.remove_option(tv)
 
-            # after the clone, we might still have a tracking branch setup
+            # After the clone, we might still have a tracking branch setup.
             head.set_tracking_branch(None)
             assert head.tracking_branch() is None
             remote_ref = rwrepo.remotes[0].refs[0]
@@ -123,7 +123,7 @@ def test_heads(self, rwrepo):
             special_name = "feature#123"
             special_name_remote_ref = SymbolicReference.create(rwrepo, "refs/remotes/origin/%s" % special_name)
             gp_tracking_branch = rwrepo.create_head("gp_tracking#123")
-            special_name_remote_ref = rwrepo.remotes[0].refs[special_name]  # get correct type
+            special_name_remote_ref = rwrepo.remotes[0].refs[special_name]  # Get correct type.
             gp_tracking_branch.set_tracking_branch(special_name_remote_ref)
             TBranch = gp_tracking_branch.tracking_branch()
             if TBranch is not None:
@@ -136,7 +136,7 @@ def test_heads(self, rwrepo):
                 assert TBranch.name == special_name_remote_ref.name
         # END for each head
 
-        # verify REFLOG gets altered
+        # Verify REFLOG gets altered.
         head = rwrepo.head
         cur_head = head.ref
         cur_commit = cur_head.commit
@@ -144,32 +144,31 @@ def test_heads(self, rwrepo):
         hlog_len = len(head.log())
         blog_len = len(cur_head.log())
         assert head.set_reference(pcommit, "detached head") is head
-        # one new log-entry
+        # One new log-entry.
         thlog = head.log()
         assert len(thlog) == hlog_len + 1
         assert thlog[-1].oldhexsha == cur_commit.hexsha
         assert thlog[-1].newhexsha == pcommit.hexsha
 
-        # the ref didn't change though
+        # The ref didn't change though.
         assert len(cur_head.log()) == blog_len
 
-        # head changes once again, cur_head doesn't change
+        # head changes once again, cur_head doesn't change.
         head.set_reference(cur_head, "reattach head")
         assert len(head.log()) == hlog_len + 2
         assert len(cur_head.log()) == blog_len
 
-        # adjusting the head-ref also adjust the head, so both reflogs are
-        # altered
+        # Adjusting the head-ref also adjust the head, so both reflogs are altered.
         cur_head.set_commit(pcommit, "changing commit")
         assert len(cur_head.log()) == blog_len + 1
         assert len(head.log()) == hlog_len + 3
 
-        # with automatic dereferencing
+        # With automatic dereferencing.
         assert head.set_commit(cur_commit, "change commit once again") is head
         assert len(head.log()) == hlog_len + 4
         assert len(cur_head.log()) == blog_len + 2
 
-        # a new branch has just a single entry
+        # A new branch has just a single entry.
         other_head = Head.create(rwrepo, "mynewhead", pcommit, logmsg="new head created")
         log = other_head.log()
         assert len(log) == 1
@@ -178,7 +177,7 @@ def test_heads(self, rwrepo):
 
     @with_rw_repo("HEAD", bare=False)
     def test_set_tracking_branch_with_import(self, rwrepo):
-        # prepare included config file
+        # 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")
@@ -230,11 +229,11 @@ def test_head_reset(self, rw_repo):
         cur_head.reset(new_head_commit, index=True, working_tree=True)  # index + wt
         assert cur_head.reference.commit == new_head_commit
 
-        # paths - make sure we have something to do
+        # Paths - make sure we have something to do.
         rw_repo.index.reset(old_head_commit.parents[0])
         cur_head.reset(cur_head, paths="test")
         cur_head.reset(new_head_commit, paths="lib")
-        # hard resets with paths don't work, its all or nothing
+        # Hard resets with paths don't work; it's all or nothing.
         self.assertRaises(
             GitCommandError,
             cur_head.reset,
@@ -243,12 +242,12 @@ def test_head_reset(self, rw_repo):
             paths="lib",
         )
 
-        # we can do a mixed reset, and then checkout from the index though
+        # We can do a mixed reset, and then checkout from the index though.
         cur_head.reset(new_head_commit)
         rw_repo.index.checkout(["lib"], force=True)
 
-        # now that we have a write write repo, change the HEAD reference - its
-        # like git-reset --soft
+        # Now that we have a write write repo, change the HEAD reference - it's
+        # like "git-reset --soft".
         heads = rw_repo.heads
         assert heads
         for head in heads:
@@ -259,7 +258,7 @@ def test_head_reset(self, rw_repo):
             assert not cur_head.is_detached
         # END for each head
 
-        # detach
+        # Detach.
         active_head = heads[0]
         curhead_commit = active_head.commit
         cur_head.reference = curhead_commit
@@ -267,20 +266,20 @@ def test_head_reset(self, rw_repo):
         assert cur_head.is_detached
         self.assertRaises(TypeError, getattr, cur_head, "reference")
 
-        # tags are references, hence we can point to them
+        # Tags are references, hence we can point to them.
         some_tag = rw_repo.tags[0]
         cur_head.reference = some_tag
         assert not cur_head.is_detached
         assert cur_head.commit == some_tag.commit
         assert isinstance(cur_head.reference, TagReference)
 
-        # put HEAD back to a real head, otherwise everything else fails
+        # Put HEAD back to a real head, otherwise everything else fails.
         cur_head.reference = active_head
 
-        # type check
+        # Type check.
         self.assertRaises(ValueError, setattr, cur_head, "reference", "that")
 
-        # head handling
+        # Head handling.
         commit = "HEAD"
         prev_head_commit = cur_head.commit
         for count, new_name in enumerate(("my_new_head", "feature/feature1")):
@@ -289,13 +288,13 @@ def test_head_reset(self, rw_repo):
             assert new_head.is_detached
             assert cur_head.commit == prev_head_commit
             assert isinstance(new_head, Head)
-            # already exists, but has the same value, so its fine
+            # Already exists, but has the same value, so it's fine.
             Head.create(rw_repo, new_name, new_head.commit)
 
-            # its not fine with a different value
+            # It's not fine with a different value.
             self.assertRaises(OSError, Head.create, rw_repo, new_name, new_head.commit.parents[0])
 
-            # force it
+            # Force it.
             new_head = Head.create(rw_repo, new_name, actual_commit, force=True)
             old_path = new_head.path
             old_name = new_head.name
@@ -304,7 +303,7 @@ def test_head_reset(self, rw_repo):
             assert new_head.rename("hello/world").name == "hello/world"
             assert new_head.rename(old_name).name == old_name and new_head.path == old_path
 
-            # rename with force
+            # Rename with force.
             tmp_head = Head.create(rw_repo, "tmphead")
             self.assertRaises(GitCommandError, tmp_head.rename, new_head)
             tmp_head.rename(new_head, force=True)
@@ -313,15 +312,15 @@ def test_head_reset(self, rw_repo):
             logfile = RefLog.path(tmp_head)
             assert osp.isfile(logfile)
             Head.delete(rw_repo, tmp_head)
-            # deletion removes the log as well
+            # Deletion removes the log as well.
             assert not osp.isfile(logfile)
             heads = rw_repo.heads
             assert tmp_head not in heads and new_head not in heads
-            # force on deletion testing would be missing here, code looks okay though ;)
+            # Force on deletion testing would be missing here, code looks okay though. ;)
         # END for each new head name
         self.assertRaises(TypeError, RemoteReference.create, rw_repo, "some_name")
 
-        # tag ref
+        # Tag ref.
         tag_name = "5.0.2"
         TagReference.create(rw_repo, tag_name)
         self.assertRaises(GitCommandError, TagReference.create, rw_repo, tag_name)
@@ -331,7 +330,7 @@ def test_head_reset(self, rw_repo):
         assert light_tag.commit == cur_head.commit.parents[0]
         assert light_tag.tag is None
 
-        # tag with tag object
+        # Tag with tag object.
         other_tag_name = "releases/1.0.2RC"
         msg = "my mighty tag\nsecond line"
         obj_tag = TagReference.create(rw_repo, other_tag_name, message=msg)
@@ -344,7 +343,7 @@ def test_head_reset(self, rw_repo):
         tags = rw_repo.tags
         assert light_tag not in tags and obj_tag not in tags
 
-        # remote deletion
+        # Remote deletion.
         remote_refs_so_far = 0
         remotes = rw_repo.remotes
         assert remotes
@@ -367,11 +366,11 @@ def test_head_reset(self, rw_repo):
         assert remote_refs_so_far
 
         for remote in remotes:
-            # remotes without references should produce an empty list
+            # Remotes without references should produce an empty list.
             self.assertEqual(remote.refs, [])
         # END for each remote
 
-        # change where the active head points to
+        # Change where the active head points to.
         if cur_head.is_detached:
             cur_head.reference = rw_repo.heads[0]
 
@@ -382,17 +381,17 @@ def test_head_reset(self, rw_repo):
         assert head.commit == cur_head.commit
         head.commit = old_commit
 
-        # setting a non-commit as commit fails, but succeeds as object
+        # Setting a non-commit as commit fails, but succeeds as object.
         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 heads 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
+        # Cannot query tree as commit.
         self.assertRaises(TypeError, getattr, head, "commit")
 
-        # set the commit directly using the head. This would never detach the head
+        # Set the commit directly using the head. This would never detach the head.
         assert not cur_head.is_detached
         head.object = old_commit
         cur_head.reference = head.commit
@@ -408,30 +407,30 @@ def test_head_reset(self, rw_repo):
         assert not cur_head.is_detached
         assert head.commit == parent_commit
 
-        # test checkout
+        # Test checkout.
         active_branch = rw_repo.active_branch
         for head in rw_repo.heads:
             checked_out_head = head.checkout()
             assert checked_out_head == head
         # END for each head to checkout
 
-        # checkout with branch creation
+        # Check out with branch creation.
         new_head = active_branch.checkout(b="new_head")
         assert active_branch != rw_repo.active_branch
         assert new_head == rw_repo.active_branch
 
-        # checkout  with force as we have a changed a file
-        # clear file
+        # Checkout with force as we have a changed a file.
+        # Clear file.
         open(new_head.commit.tree.blobs[-1].abspath, "w").close()
         assert len(new_head.commit.diff(None))
 
-        # create a new branch that is likely to touch the file we changed
+        # Create a new branch that is likely to touch the file we changed.
         far_away_head = rw_repo.create_head("far_head", "HEAD~100")
         self.assertRaises(GitCommandError, far_away_head.checkout)
         assert active_branch == active_branch.checkout(force=True)
         assert rw_repo.head.reference != far_away_head
 
-        # test reference creation
+        # Test reference creation.
         partial_ref = "sub/ref"
         full_ref = "refs/%s" % partial_ref
         ref = Reference.create(rw_repo, partial_ref)
@@ -439,21 +438,21 @@ def test_head_reset(self, rw_repo):
         assert ref.object == rw_repo.head.commit
 
         self.assertRaises(OSError, Reference.create, rw_repo, full_ref, "HEAD~20")
-        # it works if it is at the same spot though and points to the same reference
+        # It works if it is at the same spot though and points to the same reference.
         assert Reference.create(rw_repo, full_ref, "HEAD").path == full_ref
         Reference.delete(rw_repo, full_ref)
 
-        # recreate the reference using a full_ref
+        # Recreate the reference using a full_ref.
         ref = Reference.create(rw_repo, full_ref)
         assert ref.path == full_ref
         assert ref.object == rw_repo.head.commit
 
-        # recreate using force
+        # Recreate using force.
         ref = Reference.create(rw_repo, partial_ref, "HEAD~1", force=True)
         assert ref.path == full_ref
         assert ref.object == rw_repo.head.commit.parents[0]
 
-        # rename it
+        # Rename it.
         orig_obj = ref.object
         for name in ("refs/absname", "rela_name", "feature/rela_name"):
             ref_new_name = ref.rename(name)
@@ -463,19 +462,19 @@ def test_head_reset(self, rw_repo):
             assert ref_new_name == ref
         # END for each name type
 
-        # References that don't exist trigger an error if we want to access them
+        # References that don't exist trigger an error if we want to access them.
         self.assertRaises(ValueError, getattr, Reference(rw_repo, "refs/doesntexist"), "commit")
 
-        # exists, fail unless we force
+        # Exists, fail unless we force.
         ex_ref_path = far_away_head.path
         self.assertRaises(OSError, ref.rename, ex_ref_path)
-        # if it points to the same commit it works
+        # If it points to the same commit it works.
         far_away_head.commit = ref.commit
         ref.rename(ex_ref_path)
         assert ref.path == ex_ref_path and ref.object == orig_obj
         assert ref.rename(ref.path).path == ex_ref_path  # rename to same name
 
-        # create symbolic refs
+        # Create symbolic refs.
         symref_path = "symrefs/sym"
         symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference)
         assert symref.path == symref_path
@@ -488,20 +487,20 @@ def test_head_reset(self, rw_repo):
             symref_path,
             cur_head.reference.commit,
         )
-        # it works if the new ref points to the same reference
+        # It works if the new ref points to the same reference.
         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
+        # Would raise if the symref wouldn't have been deleted (probably).
         symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference)
 
-        # test symbolic references which are not at default locations like HEAD
-        # or FETCH_HEAD - they may also be at spots in refs of course
+        # Test symbolic references which are not at default locations like HEAD
+        # or FETCH_HEAD - they may also be at spots in refs of course.
         symbol_ref_path = "refs/symbol_ref"
         symref = SymbolicReference(rw_repo, symbol_ref_path)
         assert symref.path == symbol_ref_path
         symbol_ref_abspath = osp.join(rw_repo.git_dir, symref.path)
 
-        # set it
+        # Set it.
         symref.reference = new_head
         assert symref.reference == new_head
         assert osp.isfile(symbol_ref_abspath)
@@ -516,10 +515,10 @@ def test_head_reset(self, rw_repo):
             assert not symref.is_detached
         # END for each ref
 
-        # create a new non-head ref just to be sure we handle it even if packed
+        # Create a new non-head ref just to be sure we handle it even if packed.
         Reference.create(rw_repo, full_ref)
 
-        # test ref listing - assure we have packed refs
+        # Test ref listing - make sure we have packed refs.
         rw_repo.git.pack_refs(all=True, prune=True)
         heads = rw_repo.heads
         assert heads
@@ -527,15 +526,15 @@ def test_head_reset(self, rw_repo):
         assert active_branch in heads
         assert rw_repo.tags
 
-        # we should be able to iterate all symbolic refs as well - in that case
-        # we should expect only symbolic references to be returned
+        # We should be able to iterate all symbolic refs as well - in that case
+        # we should expect only symbolic references to be returned.
         for symref in SymbolicReference.iter_items(rw_repo):
             assert not symref.is_detached
 
-        # when iterating references, we can get references and symrefs
-        # when deleting all refs, I'd expect them to be gone ! Even from
-        # the packed ones
-        # For this to work, we must not be on any branch
+        # When iterating references, we can get references and symrefs
+        # when deleting all refs, I'd expect them to be gone! Even from
+        # the packed ones.
+        # For this to work, we must not be on any branch.
         rw_repo.head.reference = rw_repo.head.commit
         deleted_refs = set()
         for ref in Reference.iter_items(rw_repo):
@@ -551,16 +550,15 @@ def test_head_reset(self, rw_repo):
                 assert ref not in deleted_refs
         # END for each ref
 
-        # reattach head - head will not be returned if it is not a symbolic
-        # ref
+        # Reattach head - head will not be returned if it is not a symbolic ref.
         rw_repo.head.reference = Head.create(rw_repo, "master")
 
-        # At least the head should still exist
+        # At least the head should still exist.
         assert osp.isfile(osp.join(rw_repo.git_dir, "HEAD"))
         refs = list(SymbolicReference.iter_items(rw_repo))
         assert len(refs) == 1
 
-        # test creation of new refs from scratch
+        # Test creation of new refs from scratch.
         for path in ("basename", "dir/somename", "dir2/subdir/basename"):
             # REFERENCES
             ############
@@ -570,11 +568,11 @@ def test_head_reset(self, rw_repo):
             ref = Reference(rw_repo, fpath)
             assert ref == ref_fp
 
-            # can be created by assigning a commit
+            # Can be created by assigning a commit.
             ref.commit = rw_repo.head.commit
             assert ref.is_valid()
 
-            # if the assignment raises, the ref doesn't exist
+            # If the assignment raises, the ref doesn't exist.
             Reference.delete(ref.repo, ref.path)
             assert not ref.is_valid()
             self.assertRaises(ValueError, setattr, ref, "commit", "nonsense")
@@ -614,7 +612,7 @@ def test_tag_message(self, rw_repo):
         assert tag_ref.tag.message == "test2"
 
     def test_dereference_recursive(self):
-        # for now, just test the HEAD
+        # For now, just test the HEAD.
         assert SymbolicReference.dereference_recursive(self.rorepo, "HEAD")
 
     def test_reflog(self):
@@ -634,7 +632,7 @@ def test_refs_outside_repo(self):
 
     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
+        # 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")
@@ -665,5 +663,5 @@ def test_validity_ref_names(self):
         self.assertRaises(ValueError, check_ref, "@")
         # Rule 10
         self.assertRaises(ValueError, check_ref, "ref/contain\\s/backslash")
-        # Valid reference name should not raise
+        # Valid reference name should not raise.
         check_ref("valid/ref/name")
diff --git a/test/test_remote.py b/test/test_remote.py
index 7144b2791..b102730ac 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -36,7 +36,7 @@
 import os.path as osp
 
 
-# assure we have repeatable results
+# Make sure we have repeatable results.
 random.seed(0)
 
 
@@ -50,8 +50,8 @@ def __init__(self):
         self._num_progress_messages = 0
 
     def _parse_progress_line(self, line):
-        # we may remove the line later if it is dropped
-        # Keep it for debugging
+        # We may remove the line later if it is dropped.
+        # Keep it for debugging.
         self._seen_lines.append(line)
         rval = super(TestRemoteProgress, self)._parse_progress_line(line)
         return rval
@@ -63,7 +63,7 @@ def line_dropped(self, line):
             pass
 
     def update(self, op_code, cur_count, max_count=None, message=""):
-        # check each stage only comes once
+        # Check each stage only comes once.
         op_id = op_code & self.OP_MASK
         assert op_id in (self.COUNTING, self.COMPRESSING, self.WRITING)
 
@@ -85,15 +85,15 @@ def update(self, op_code, cur_count, max_count=None, message=""):
         self._num_progress_messages += 1
 
     def make_assertion(self):
-        # we don't always receive messages
+        # We don't always receive messages.
         if not self._seen_lines:
             return
 
-        # sometimes objects are not compressed which is okay
+        # Sometimes objects are not compressed which is okay.
         assert len(self._seen_ops) in (2, 3), len(self._seen_ops)
         assert self._stages_per_op
 
-        # must have seen all stages
+        # Must have seen all stages.
         for _op, stages in self._stages_per_op.items():
             assert stages & self.STAGE_MASK == self.STAGE_MASK
         # END for each op/stage
@@ -151,7 +151,7 @@ def _do_test_push_result(self, results, remote):
                 # END for each bitflag
                 self.assertTrue(has_one)
             else:
-                # there must be a remote commit
+                # There must be a remote commit.
                 if info.flags & info.DELETED == 0:
                     self.assertIsInstance(info.local_ref, Reference)
                 else:
@@ -163,7 +163,7 @@ def _do_test_push_result(self, results, remote):
         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
+            # No errors, so this should do nothing.
             results.raise_if_error()
 
     def _do_test_fetch_info(self, repo):
@@ -177,8 +177,10 @@ def _do_test_fetch_info(self, repo):
         )
 
     def _commit_random_file(self, repo):
-        # Create a file with a random name and random data and commit it to  repo.
-        # Return the committed absolute file path
+        """Create a file with a random name and random data and commit it to a repo.
+
+        :return: The committed absolute file path.
+        """
         index = repo.index
         new_file = self._make_file(osp.basename(tempfile.mktemp()), str(random.random()), repo)
         index.add([new_file])
@@ -186,7 +188,7 @@ def _commit_random_file(self, repo):
         return new_file
 
     def _do_test_fetch(self, remote, rw_repo, remote_repo, **kwargs):
-        # specialized fetch testing to de-clutter the main test
+        """Specialized fetch testing to de-clutter the main test."""
         self._do_test_fetch_info(rw_repo)
 
         def fetch_and_test(remote, **kwargs):
@@ -202,16 +204,16 @@ def fetch_and_test(remote, **kwargs):
         def get_info(res, remote, name):
             return res["%s/%s" % (remote, name)]
 
-        # put remote head to master as it is guaranteed to exist
+        # Put remote head to master as it is guaranteed to exist.
         remote_repo.head.reference = remote_repo.heads.master
 
         res = fetch_and_test(remote, **kwargs)
-        # all up to date
+        # All up to date.
         for info in res:
             self.assertTrue(info.flags & info.HEAD_UPTODATE)
 
-        # rewind remote head to trigger rejection
-        # index must be false as remote is a bare repo
+        # Rewind remote head to trigger rejection.
+        # index must be false as remote is a bare repo.
         rhead = remote_repo.head
         remote_commit = rhead.commit
         rhead.reset("HEAD~2", index=False)
@@ -221,50 +223,50 @@ def get_info(res, remote, name):
         self.assertTrue(master_info.flags & FetchInfo.FORCED_UPDATE)
         self.assertIsNotNone(master_info.note)
 
-        # normal fast forward - set head back to previous one
+        # Normal fast forward - set head back to previous one.
         rhead.commit = remote_commit
         res = fetch_and_test(remote)
         self.assertTrue(res[mkey].flags & FetchInfo.FAST_FORWARD)
 
-        # new remote branch
+        # New remote branch.
         new_remote_branch = Head.create(remote_repo, "new_branch")
         res = fetch_and_test(remote)
         new_branch_info = get_info(res, remote, new_remote_branch)
         self.assertTrue(new_branch_info.flags & FetchInfo.NEW_HEAD)
 
-        # remote branch rename ( causes creation of a new one locally )
+        # Remote branch rename (causes creation of a new one locally).
         new_remote_branch.rename("other_branch_name")
         res = fetch_and_test(remote)
         other_branch_info = get_info(res, remote, new_remote_branch)
         self.assertEqual(other_branch_info.ref.commit, new_branch_info.ref.commit)
 
-        # remove new branch
+        # Remove new branch.
         Head.delete(new_remote_branch.repo, new_remote_branch)
         res = fetch_and_test(remote)
-        # deleted remote will not be fetched
+        # Deleted remote will not be fetched.
         self.assertRaises(IndexError, get_info, res, remote, new_remote_branch)
 
-        # prune stale tracking branches
+        # Prune stale tracking branches.
         stale_refs = remote.stale_refs
         self.assertEqual(len(stale_refs), 2)
         self.assertIsInstance(stale_refs[0], RemoteReference)
         RemoteReference.delete(rw_repo, *stale_refs)
 
-        # test single branch fetch with refspec including target remote
+        # Test single branch fetch with refspec including target remote.
         res = fetch_and_test(remote, refspec="master:refs/remotes/%s/master" % remote)
         self.assertEqual(len(res), 1)
         self.assertTrue(get_info(res, remote, "master"))
 
-        # ... with respec and no target
+        # ...with respec and no target.
         res = fetch_and_test(remote, refspec="master")
         self.assertEqual(len(res), 1)
 
-        # ... multiple refspecs ... works, but git command returns with error if one ref is wrong without
-        # doing anything. This is new in  later binaries
+        # ...multiple refspecs...works, but git command returns with error if one ref is
+        # wrong without doing anything. This is new in later binaries.
         # res = fetch_and_test(remote, refspec=['master', 'fred'])
         # self.assertEqual(len(res), 1)
 
-        # add new tag reference
+        # Add new tag reference.
         rtag = TagReference.create(remote_repo, "1.0-RV_hello.there")
         res = fetch_and_test(remote, tags=True)
         tinfo = res[str(rtag)]
@@ -272,10 +274,10 @@ def get_info(res, remote, name):
         self.assertEqual(tinfo.ref.commit, rtag.commit)
         self.assertTrue(tinfo.flags & tinfo.NEW_TAG)
 
-        # adjust the local tag commit
+        # Adjust the local tag commit.
         Reference.set_object(rtag, rhead.commit.parents[0].parents[0])
 
-        # as of git 2.20 one cannot clobber local tags that have changed without
+        # As of git 2.20 one cannot clobber local tags that have changed without
         # specifying --force, and the test assumes you can clobber, so...
         force = None
         if rw_repo.git.version_info[:2] >= (2, 20):
@@ -285,63 +287,63 @@ def get_info(res, remote, name):
         self.assertEqual(tinfo.commit, rtag.commit)
         self.assertTrue(tinfo.flags & tinfo.TAG_UPDATE)
 
-        # delete remote tag - local one will stay
+        # Delete remote tag - local one will stay.
         TagReference.delete(remote_repo, rtag)
         res = fetch_and_test(remote, tags=True)
         self.assertRaises(IndexError, get_info, res, remote, str(rtag))
 
-        # provoke to receive actual objects to see what kind of output we have to
-        # expect. For that we need a remote transport protocol
+        # Provoke to receive actual objects to see what kind of output we have to
+        # expect. For that we need a remote transport protocol.
         # Create a new UN-shared repo and fetch into it after we pushed a change
-        # to the shared repo
+        # to the shared repo.
         other_repo_dir = tempfile.mktemp("other_repo")
-        # must clone with a local path for the repo implementation not to freak out
-        # as it wants local paths only ( which I can understand )
+        # Must clone with a local path for the repo implementation not to freak out
+        # as it wants local paths only (which I can understand).
         other_repo = remote_repo.clone(other_repo_dir, shared=False)
         remote_repo_url = osp.basename(remote_repo.git_dir)  # git-daemon runs with appropriate `--base-path`.
         remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, remote_repo_url))
 
-        # put origin to git-url
+        # Put origin to git-url.
         other_origin = other_repo.remotes.origin
         with other_origin.config_writer as cw:
             cw.set("url", remote_repo_url)
-        # it automatically creates alternates as remote_repo is shared as well.
-        # It will use the transport though and ignore alternates when fetching
+        # It automatically creates alternates as remote_repo is shared as well.
+        # It will use the transport though and ignore alternates when fetching.
         # assert not other_repo.alternates  # this would fail
 
-        # assure we are in the right state
+        # Ensure we are in the right state.
         rw_repo.head.reset(remote.refs.master, working_tree=True)
         try:
             self._commit_random_file(rw_repo)
             remote.push(rw_repo.head.reference)
 
-            # here I would expect to see remote-information about packing
+            # Here I would expect to see remote-information about packing
             # objects and so on. Unfortunately, this does not happen
             # if we are redirecting the output - git explicitly checks for this
-            # and only provides progress information to ttys
+            # and only provides progress information to ttys.
             res = fetch_and_test(other_origin)
         finally:
             rmtree(other_repo_dir)
         # END test and cleanup
 
     def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
-        # push our changes
+        # Push our changes.
         lhead = rw_repo.head
-        # assure we are on master and it is checked out where the remote is
+        # Ensure we are on master and it is checked out where the remote is.
         try:
             lhead.reference = rw_repo.heads.master
         except AttributeError:
-            # if the author is on a non-master branch, the clones might not have
-            # a local master yet. We simply create it
+            # If the author is on a non-master branch, the clones might not have
+            # a local master yet. We simply create it.
             lhead.reference = rw_repo.create_head("master")
         # END master handling
         lhead.reset(remote.refs.master, working_tree=True)
 
-        # push without spec should fail ( without further configuration )
+        # Push without spec should fail (without further configuration)
         # well, works nicely
         # self.assertRaises(GitCommandError, remote.push)
 
-        # simple file push
+        # Simple file push.
         self._commit_random_file(rw_repo)
         progress = TestRemoteProgress()
         res = remote.push(lhead.reference, progress)
@@ -349,23 +351,23 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
         self._do_test_push_result(res, remote)
         progress.make_assertion()
 
-        # rejected - undo last commit
+        # Rejected - undo last commit.
         lhead.reset("HEAD~1")
         res = remote.push(lhead.reference)
         self.assertTrue(res[0].flags & PushInfo.ERROR)
         self.assertTrue(res[0].flags & PushInfo.REJECTED)
         self._do_test_push_result(res, remote)
 
-        # force rejected pull
+        # Force rejected pull.
         res = remote.push("+%s" % lhead.reference)
         self.assertEqual(res[0].flags & PushInfo.ERROR, 0)
         self.assertTrue(res[0].flags & PushInfo.FORCED_UPDATE)
         self._do_test_push_result(res, remote)
 
-        # invalid refspec
+        # Invalid refspec.
         self.assertRaises(GitCommandError, remote.push, "hellothere")
 
-        # push new tags
+        # Push new tags.
         progress = TestRemoteProgress()
         to_be_updated = "my_tag.1.0RV"
         new_tag = TagReference.create(rw_repo, to_be_updated)  # @UnusedVariable
@@ -375,28 +377,28 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
         progress.make_assertion()
         self._do_test_push_result(res, remote)
 
-        # update push new tags
-        # Rejection is default
+        # Update push new tags.
+        # Rejection is default.
         new_tag = TagReference.create(rw_repo, to_be_updated, reference="HEAD~1", force=True)
         res = remote.push(tags=True)
         self._do_test_push_result(res, remote)
         self.assertTrue(res[-1].flags & PushInfo.REJECTED)
         self.assertTrue(res[-1].flags & PushInfo.ERROR)
 
-        # push force this tag
+        # Force push this tag.
         res = remote.push("+%s" % new_tag.path)
         self.assertEqual(res[-1].flags & PushInfo.ERROR, 0)
         self.assertTrue(res[-1].flags & PushInfo.FORCED_UPDATE)
 
-        # delete tag - have to do it using refspec
+        # Delete tag - have to do it using refspec.
         res = remote.push(":%s" % new_tag.path)
         self._do_test_push_result(res, remote)
         self.assertTrue(res[0].flags & PushInfo.DELETED)
         # Currently progress is not properly transferred, especially not using
-        # the git daemon
+        # the git daemon.
         # progress.assert_received_message()
 
-        # push new branch
+        # Push new branch.
         new_head = Head.create(rw_repo, "my_new_branch")
         progress = TestRemoteProgress()
         res = remote.push(new_head, progress)
@@ -405,7 +407,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
         progress.make_assertion()
         self._do_test_push_result(res, remote)
 
-        # rejected stale delete
+        # Rejected stale delete.
         force_with_lease = "%s:0000000000000000000000000000000000000000" % new_head.path
         res = remote.push(":%s" % new_head.path, force_with_lease=force_with_lease)
         self.assertTrue(res[0].flags & PushInfo.ERROR)
@@ -413,7 +415,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
         self.assertIsNone(res[0].local_ref)
         self._do_test_push_result(res, remote)
 
-        # delete new branch on the remote end and locally
+        # Delete new branch on the remote end and locally.
         res = remote.push(":%s" % new_head.path)
         self._do_test_push_result(res, remote)
         Head.delete(rw_repo, new_head)
@@ -425,8 +427,8 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
 
         remote.pull("master", kill_after_timeout=10.0)
 
-        # cleanup - delete created tags and branches as we are in an innerloop on
-        # the same repository
+        # Cleanup - delete created tags and branches as we are in an inner loop on
+        # the same repository.
         TagReference.delete(rw_repo, new_tag, other_tag)
         remote.push(":%s" % other_tag.path, kill_after_timeout=10.0)
 
@@ -442,7 +444,7 @@ def test_base(self, rw_repo, remote_repo):
             self.assertEqual(remote, remote)
             self.assertNotEqual(str(remote), repr(remote))
             remote_set.add(remote)
-            remote_set.add(remote)  # should already exist
+            remote_set.add(remote)  # Should already exist.
             # REFS
             refs = remote.refs
             self.assertTrue(refs)
@@ -452,17 +454,17 @@ def test_base(self, rw_repo, remote_repo):
             # END for each ref
 
             # OPTIONS
-            # cannot use 'fetch' key anymore as it is now a method
+            # Cannot use 'fetch' key anymore as it is now a method.
             for opt in ("url",):
                 val = getattr(remote, opt)
                 reader = remote.config_reader
                 assert reader.get(opt) == val
                 assert reader.get_value(opt, None) == val
 
-                # unable to write with a reader
+                # Unable to write with a reader.
                 self.assertRaises(IOError, reader.set, opt, "test")
 
-                # change value
+                # Change value.
                 with remote.config_writer as writer:
                     new_val = "myval"
                     writer.set(opt, new_val)
@@ -477,7 +479,7 @@ def test_base(self, rw_repo, remote_repo):
             prev_name = remote.name
             self.assertEqual(remote.rename(other_name), remote)
             self.assertNotEqual(prev_name, remote.name)
-            # multiple times
+            # Multiple times.
             for _ in range(2):
                 self.assertEqual(remote.rename(prev_name).name, prev_name)
             # END for each rename ( back to prev_name )
@@ -487,7 +489,7 @@ def test_base(self, rw_repo, remote_repo):
 
             # FETCH TESTING
             # Only for remotes - local cases are the same or less complicated
-            # as additional progress information will never be emitted
+            # as additional progress information will never be emitted.
             if remote.name == "daemon_origin":
                 self._do_test_fetch(remote, rw_repo, remote_repo, kill_after_timeout=10.0)
                 ran_fetch_test = True
@@ -503,10 +505,10 @@ def test_base(self, rw_repo, remote_repo):
         origin = rw_repo.remote("origin")
         assert origin == rw_repo.remotes.origin
 
-        # Verify we can handle prunes when fetching
+        # Verify we can handle prunes when fetching.
         # stderr lines look like this:  x [deleted]         (none)     -> origin/experiment-2012
-        # These should just be skipped
-        # If we don't have a manual checkout, we can't actually assume there are any non-master branches
+        # These should just be skipped.
+        # If we don't have a manual checkout, we can't actually assume there are any non-master branches.
         remote_repo.create_head("myone_for_deletion")
         # Get the branch - to be pruned later
         origin.fetch()
@@ -534,13 +536,13 @@ def test_creation_and_removal(self, bare_rw_repo):
         self.assertIn(remote, bare_rw_repo.remotes)
         self.assertTrue(remote.exists())
 
-        # create same one again
+        # Create same one again.
         self.assertRaises(GitCommandError, Remote.create, bare_rw_repo, *arg_list)
 
         Remote.remove(bare_rw_repo, new_name)
-        self.assertTrue(remote.exists())  # We still have a cache that doesn't know we were deleted by name
+        self.assertTrue(remote.exists())  # We still have a cache that doesn't know we were deleted by name.
         remote._clear_cache()
-        assert not remote.exists()  # Cache should be renewed now. This is an issue ...
+        assert not remote.exists()  # Cache should be renewed now. This is an issue...
 
         for remote in bare_rw_repo.remotes:
             if remote.name == new_name:
@@ -548,11 +550,11 @@ def test_creation_and_removal(self, bare_rw_repo):
             # END if deleted remote matches existing remote's name
         # END for each remote
 
-        # Issue #262 - the next call would fail if bug wasn't fixed
+        # Issue #262 - the next call would fail if bug wasn't fixed.
         bare_rw_repo.create_remote("bogus", "/bogus/path", mirror="push")
 
     def test_fetch_info(self):
-        # assure we can handle remote-tracking branches
+        # Ensure we can handle remote-tracking branches.
         fetch_info_line_fmt = "c437ee5deb8d00cf02f03720693e4c802e99f390	not-for-merge	%s '0.3' of "
         fetch_info_line_fmt += "git://github.com/gitpython-developers/GitPython"
         remote_info_line_fmt = "* [new branch]      nomatter     -> %s"
@@ -573,8 +575,8 @@ def test_fetch_info(self):
         assert not fi.ref.is_valid()
         self.assertEqual(fi.ref.name, "local/master")
 
-        # handles non-default refspecs: One can specify a different path in refs/remotes
-        # or a special path just in refs/something for instance
+        # Handles non-default refspecs: One can specify a different path in refs/remotes
+        # or a special path just in refs/something for instance.
 
         fi = FetchInfo._from_line(
             self.rorepo,
@@ -585,7 +587,7 @@ def test_fetch_info(self):
         self.assertIsInstance(fi.ref, TagReference)
         assert fi.ref.path.startswith("refs/tags"), fi.ref.path
 
-        # it could be in a remote direcftory though
+        # It could be in a remote directory though.
         fi = FetchInfo._from_line(
             self.rorepo,
             remote_info_line_fmt % "remotename/tags/tagname",
@@ -595,14 +597,14 @@ def test_fetch_info(self):
         self.assertIsInstance(fi.ref, TagReference)
         assert fi.ref.path.startswith("refs/remotes/"), fi.ref.path
 
-        # it can also be anywhere !
+        # It can also be anywhere!
         tag_path = "refs/something/remotename/tags/tagname"
         fi = FetchInfo._from_line(self.rorepo, remote_info_line_fmt % tag_path, fetch_info_line_fmt % "tag")
 
         self.assertIsInstance(fi.ref, TagReference)
         self.assertEqual(fi.ref.path, tag_path)
 
-        # branches default to refs/remotes
+        # Branches default to refs/remotes.
         fi = FetchInfo._from_line(
             self.rorepo,
             remote_info_line_fmt % "remotename/branch",
@@ -612,7 +614,7 @@ def test_fetch_info(self):
         self.assertIsInstance(fi.ref, RemoteReference)
         self.assertEqual(fi.ref.remote_name, "remotename")
 
-        # but you can force it anywhere, in which case we only have a references
+        # But you can force it anywhere, in which case we only have a references.
         fi = FetchInfo._from_line(
             self.rorepo,
             remote_info_line_fmt % "refs/something/branch",
@@ -639,46 +641,46 @@ def test_uncommon_branch_names(self):
 
     @with_rw_repo("HEAD", bare=False)
     def test_multiple_urls(self, rw_repo):
-        # test addresses
+        # Test addresses.
         test1 = "https://github.com/gitpython-developers/GitPython"
         test2 = "https://github.com/gitpython-developers/gitdb"
         test3 = "https://github.com/gitpython-developers/smmap"
 
         remote = rw_repo.remotes[0]
-        # Testing setting a single URL
+        # Test setting a single URL.
         remote.set_url(test1)
         self.assertEqual(list(remote.urls), [test1])
 
-        # Testing replacing that single URL
+        # Test replacing that single URL.
         remote.set_url(test1)
         self.assertEqual(list(remote.urls), [test1])
-        # Testing adding new URLs
+        # Test adding new URLs.
         remote.set_url(test2, add=True)
         self.assertEqual(list(remote.urls), [test1, test2])
         remote.set_url(test3, add=True)
         self.assertEqual(list(remote.urls), [test1, test2, test3])
-        # Testing removing an URL
+        # Test removing a URL.
         remote.set_url(test2, delete=True)
         self.assertEqual(list(remote.urls), [test1, test3])
-        # Testing changing an URL
+        # Test changing a URL.
         remote.set_url(test2, test3)
         self.assertEqual(list(remote.urls), [test1, test2])
 
         # will raise: fatal: --add --delete doesn't make sense
         self.assertRaises(GitCommandError, remote.set_url, test2, add=True, delete=True)
 
-        # Testing on another remote, with the add/delete URL
+        # Test on another remote, with the add/delete URL.
         remote = rw_repo.create_remote("another", url=test1)
         remote.add_url(test2)
         self.assertEqual(list(remote.urls), [test1, test2])
         remote.add_url(test3)
         self.assertEqual(list(remote.urls), [test1, test2, test3])
-        # Testing removing all the URLs
+        # Test removing all the URLs.
         remote.delete_url(test2)
         self.assertEqual(list(remote.urls), [test1, test3])
         remote.delete_url(test1)
         self.assertEqual(list(remote.urls), [test3])
-        # will raise fatal: Will not delete all non-push URLs
+        # Will raise fatal: Will not delete all non-push URLs.
         self.assertRaises(GitCommandError, remote.delete_url, test3)
 
     def test_fetch_error(self):
@@ -972,14 +974,13 @@ def test_push_unsafe_options_allowed(self, rw_repo):
 class TestTimeouts(TestBase):
     @with_rw_repo("HEAD", bare=False)
     def test_timeout_funcs(self, repo):
-        # Force error code to prevent a race condition if the python thread is
-        # slow
+        # Force error code to prevent a race condition if the python thread is slow.
         default = Git.AutoInterrupt._status_code_if_terminate
         Git.AutoInterrupt._status_code_if_terminate = -15
-        for function in ["pull", "fetch"]:  # can't get push to timeout
+        for function in ["pull", "fetch"]:  # Can't get push to time out.
             f = getattr(repo.remotes.origin, function)
-            assert f is not None  # Make sure these functions exist
-            _ = f()  # Make sure the function runs
+            assert f is not None  # Make sure these functions exist.
+            _ = f()  # Make sure the function runs.
             with pytest.raises(GitCommandError, match="kill_after_timeout=0 s"):
                 f(kill_after_timeout=0)
 
diff --git a/test/test_repo.py b/test/test_repo.py
index 364b895fb..c7c13dc7f 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -4,6 +4,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import glob
 import io
 from io import BytesIO
@@ -163,28 +164,29 @@ def test_trees(self):
         self.assertEqual(num_trees, mc)
 
     def _assert_empty_repo(self, repo):
-        # test all kinds of things with an empty, freshly initialized repo.
-        # It should throw good errors
+        """Test all kinds of things with an empty, freshly initialized repo.
 
-        # entries should be empty
+        It should throw good errors.
+        """
+        # Entries should be empty.
         self.assertEqual(len(repo.index.entries), 0)
 
-        # head is accessible
+        # head is accessible.
         assert repo.head
         assert repo.head.ref
         assert not repo.head.is_valid()
 
-        # we can change the head to some other ref
+        # We can change the head to some other ref.
         head_ref = Head.from_path(repo, Head.to_full_path("some_head"))
         assert not head_ref.is_valid()
         repo.head.ref = head_ref
 
-        # is_dirty can handle all kwargs
+        # is_dirty can handle all kwargs.
         for args in ((1, 0, 0), (0, 1, 0), (0, 0, 1)):
             assert not repo.is_dirty(*args)
         # END for each arg
 
-        # we can add a file to the index ( if we are not bare )
+        # We can add a file to the index (if we are not bare).
         if not repo.bare:
             pass
         # END test repos with working tree
@@ -201,7 +203,7 @@ def test_clone_from_keeps_env(self, rw_dir):
     @with_rw_directory
     def test_date_format(self, rw_dir):
         repo = Repo.init(osp.join(rw_dir, "repo"))
-        # @-timestamp is the format used by git commit hooks
+        # @-timestamp is the format used by git commit hooks.
         repo.index.commit("Commit messages", commit_date="@1400000000 +0000")
 
     @with_rw_directory
@@ -262,7 +264,7 @@ def test_leaking_password_in_clone_logs(self, rw_dir):
             )
         except GitCommandError as err:
             assert password not in str(err), "The error message '%s' should not contain the password" % err
-        # Working example from a blank private project
+        # Working example from a blank private project.
         Repo.clone_from(
             url="https://gitlab+deploy-token-392045:mLWhVus7bjLsy8xj8q2V@gitlab.com/mercierm/test_git_python",
             to_path=rw_dir,
@@ -514,12 +516,12 @@ def test_init(self):
                 try:
                     rmtree(clone_path)
                 except OSError:
-                    # when relative paths are used, the clone may actually be inside
-                    # of the parent directory
+                    # When relative paths are used, the clone may actually be inside
+                    # of the parent directory.
                     pass
                 # END exception handling
 
-                # try again, this time with the absolute version
+                # Try again, this time with the absolute version.
                 rc = Repo.clone_from(r.git_dir, clone_path)
                 self._assert_empty_repo(rc)
 
@@ -527,8 +529,8 @@ def test_init(self):
                 try:
                     rmtree(clone_path)
                 except OSError:
-                    # when relative paths are used, the clone may actually be inside
-                    # of the parent directory
+                    # When relative paths are used, the clone may actually be inside
+                    # of the parent directory.
                     pass
                 # END exception handling
 
@@ -675,11 +677,11 @@ def test_should_display_blame_information(self, git):
             lambda: c.message,
         )
 
-        # test the 'lines per commit' entries
+        # Test the 'lines per commit' entries.
         tlist = b[0][1]
         self.assertTrue(tlist)
         self.assertTrue(isinstance(tlist[0], str))
-        self.assertTrue(len(tlist) < sum(len(t) for t in tlist))  # test for single-char bug
+        self.assertTrue(len(tlist) < sum(len(t) for t in tlist))  # Test for single-char bug.
 
         # BINARY BLAME
         git.return_value = fixture("blame_binary")
@@ -688,7 +690,7 @@ def test_should_display_blame_information(self, git):
 
     def test_blame_real(self):
         c = 0
-        nml = 0  # amount of multi-lines per blame
+        nml = 0  # Amount of multi-lines per blame.
         for item in self.rorepo.head.commit.tree.traverse(
             predicate=lambda i, d: i.type == "blob" and i.path.endswith(".py")
         ):
@@ -702,14 +704,14 @@ def test_blame_real(self):
 
     @mock.patch.object(Git, "_call_process")
     def test_blame_incremental(self, git):
-        # loop over two fixtures, create a test fixture for 2.11.1+ syntax
+        # Loop over two fixtures, create a test fixture for 2.11.1+ syntax.
         for git_fixture in ("blame_incremental", "blame_incremental_2.11.1_plus"):
             git.return_value = fixture(git_fixture)
             blame_output = self.rorepo.blame_incremental("9debf6b0aafb6f7781ea9d1383c86939a1aacde3", "AUTHORS")
             blame_output = list(blame_output)
             self.assertEqual(len(blame_output), 5)
 
-            # Check all outputted line numbers
+            # Check all outputted line numbers.
             ranges = flatten([entry.linenos for entry in blame_output])
             self.assertEqual(
                 ranges,
@@ -727,13 +729,13 @@ def test_blame_incremental(self, git):
             commits = [entry.commit.hexsha[:7] for entry in blame_output]
             self.assertEqual(commits, ["82b8902", "82b8902", "c76852d", "c76852d", "c76852d"])
 
-            # Original filenames
+            # Original filenames.
             self.assertSequenceEqual(
                 [entry.orig_path for entry in blame_output],
                 ["AUTHORS"] * len(blame_output),
             )
 
-            # Original line numbers
+            # Original line numbers.
             orig_ranges = flatten([entry.orig_linenos for entry in blame_output])
             self.assertEqual(
                 orig_ranges,
@@ -780,7 +782,7 @@ def test_untracked_files(self, rwrepo):
             untracked_files = rwrepo.untracked_files
             num_recently_untracked = len(untracked_files)
 
-            # assure we have all names - they are relative to the git-dir
+            # Ensure we have all names - they are relative to the git-dir.
             num_test_untracked = 0
             for utfile in untracked_files:
                 num_test_untracked += join_path_native(base, utfile) in files
@@ -791,9 +793,9 @@ def test_untracked_files(self, rwrepo):
         # end for each run
 
     def test_config_reader(self):
-        reader = self.rorepo.config_reader()  # all config files
+        reader = self.rorepo.config_reader()  # All config files.
         assert reader.read_only
-        reader = self.rorepo.config_reader("repository")  # single config file
+        reader = self.rorepo.config_reader("repository")  # Single config file.
         assert reader.read_only
 
     def test_config_writer(self):
@@ -802,8 +804,8 @@ def test_config_writer(self):
                 with self.rorepo.config_writer(config_level) as writer:
                     self.assertFalse(writer.read_only)
             except IOError:
-                # its okay not to get a writer for some configuration files if we
-                # have no permissions
+                # It's okay not to get a writer for some configuration files if we
+                # have no permissions.
                 pass
 
     def test_config_level_paths(self):
@@ -811,8 +813,8 @@ def test_config_level_paths(self):
             assert self.rorepo._get_config_path(config_level)
 
     def test_creation_deletion(self):
-        # just a very quick test to assure it generally works. There are
-        # specialized cases in the test_refs module
+        # Just a very quick test to assure it generally works. There are
+        # specialized cases in the test_refs module.
         head = self.rorepo.create_head("new_head", "HEAD~1")
         self.rorepo.delete_head(head)
 
@@ -828,7 +830,7 @@ def test_creation_deletion(self):
             self.rorepo.delete_remote(remote)
 
     def test_comparison_and_hash(self):
-        # this is only a preliminary test, more testing done in test_index
+        # This is only a preliminary test, more testing done in test_index.
         self.assertEqual(self.rorepo, self.rorepo)
         self.assertFalse(self.rorepo != self.rorepo)
         self.assertEqual(len({self.rorepo, self.rorepo}), 1)
@@ -844,8 +846,8 @@ def test_tilde_and_env_vars_in_repo_path(self, rw_dir):
             Repo.init(osp.join("$FOO", "test.git"), bare=True)
 
     def test_git_cmd(self):
-        # test CatFileContentStream, just to be very sure we have no fencepost errors
-        # last \n is the terminating newline that it expects
+        # Test CatFileContentStream, just to be very sure we have no fencepost errors.
+        # The last \n is the terminating newline that it expects.
         l1 = b"0123456789\n"
         l2 = b"abcdefghijklmnopqrstxy\n"
         l3 = b"z\n"
@@ -853,8 +855,8 @@ def test_git_cmd(self):
 
         l1p = l1[:5]
 
-        # full size
-        # size is without terminating newline
+        # Full size.
+        # Size is without terminating newline.
         def mkfull():
             return Git.CatFileContentStream(len(d) - 1, BytesIO(d))
 
@@ -868,7 +870,7 @@ def mktiny():
         lines = s.readlines()
         self.assertEqual(len(lines), 3)
         self.assertTrue(lines[-1].endswith(b"\n"), lines[-1])
-        self.assertEqual(s._stream.tell(), len(d))  # must have scrubbed to the end
+        self.assertEqual(s._stream.tell(), len(d))  # Must have scrubbed to the end.
 
         # realines line limit
         s = mkfull()
@@ -911,7 +913,7 @@ def mktiny():
         s = mkfull()
         self.assertEqual(s.read(5), l1p)
         self.assertEqual(s.read(6), l1[5:])
-        self.assertEqual(s._stream.tell(), 5 + 6)  # its not yet done
+        self.assertEqual(s._stream.tell(), 5 + 6)  # It's not yet done.
 
         # read tiny
         s = mktiny()
@@ -926,7 +928,7 @@ def _assert_rev_parse_types(self, name, rev_obj):
         if rev_obj.type == "tag":
             rev_obj = rev_obj.object
 
-        # tree and blob type
+        # Tree and blob type.
         obj = rev_parse(name + "^{tree}")
         self.assertEqual(obj, rev_obj.tree)
 
@@ -946,13 +948,13 @@ def _assert_rev_parse(self, name):
             obj = orig_obj
         # END deref tags by default
 
-        # try history
+        # Try history
         rev = name + "~"
         obj2 = rev_parse(rev)
         self.assertEqual(obj2, obj.parents[0])
         self._assert_rev_parse_types(rev, obj2)
 
-        # history with number
+        # History with number
         ni = 11
         history = [obj.parents[0]]
         for _ in range(ni):
@@ -966,13 +968,13 @@ def _assert_rev_parse(self, name):
             self._assert_rev_parse_types(rev, obj2)
         # END history check
 
-        # parent ( default )
+        # Parent (default)
         rev = name + "^"
         obj2 = rev_parse(rev)
         self.assertEqual(obj2, obj.parents[0])
         self._assert_rev_parse_types(rev, obj2)
 
-        # parent with number
+        # Parent with number
         for pn, parent in enumerate(obj.parents):
             rev = name + "^%i" % (pn + 1)
             self.assertEqual(rev_parse(rev), parent)
@@ -983,17 +985,17 @@ def _assert_rev_parse(self, name):
 
     @with_rw_repo("HEAD", bare=False)
     def test_rw_rev_parse(self, rwrepo):
-        # verify it does not confuse branches with hexsha ids
+        # Verify it does not confuse branches with hexsha ids.
         ahead = rwrepo.create_head("aaaaaaaa")
         assert rwrepo.rev_parse(str(ahead)) == ahead.commit
 
     def test_rev_parse(self):
         rev_parse = self.rorepo.rev_parse
 
-        # try special case: This one failed at some point, make sure its fixed
+        # Try special case: This one failed at some point, make sure its fixed.
         self.assertEqual(rev_parse("33ebe").hexsha, "33ebe7acec14b25c5f84f35a664803fcab2f7781")
 
-        # start from reference
+        # Start from reference.
         num_resolved = 0
 
         for ref_no, ref in enumerate(Reference.iter_items(self.rorepo)):
@@ -1006,7 +1008,7 @@ def test_rev_parse(self):
                     num_resolved += 1
                 except (BadName, BadObject):
                     print("failed on %s" % path_section)
-                    # is fine, in case we have something like 112, which belongs to remotes/rname/merge-requests/112
+                    # This is fine if we have something like 112, which belongs to remotes/rname/merge-requests/112.
                 # END exception handling
             # END for each token
             if ref_no == 3 - 1:
@@ -1014,21 +1016,21 @@ def test_rev_parse(self):
         # END for each reference
         assert num_resolved
 
-        # it works with tags !
+        # It works with tags!
         tag = self._assert_rev_parse("0.1.4")
         self.assertEqual(tag.type, "tag")
 
-        # try full sha directly ( including type conversion )
+        # try full sha directly (including type conversion).
         self.assertEqual(tag.object, rev_parse(tag.object.hexsha))
         self._assert_rev_parse_types(tag.object.hexsha, tag.object)
 
-        # multiple tree types result in the same tree: HEAD^{tree}^{tree}:CHANGES
+        # Multiple tree types result in the same tree: HEAD^{tree}^{tree}:CHANGES
         rev = "0.1.4^{tree}^{tree}"
         self.assertEqual(rev_parse(rev), tag.object.tree)
         self.assertEqual(rev_parse(rev + ":CHANGES"), tag.object.tree["CHANGES"])
 
-        # try to get parents from first revision - it should fail as no such revision
-        # exists
+        # Try to get parents from first revision - it should fail as no such revision
+        # exists.
         first_rev = "33ebe7acec14b25c5f84f35a664803fcab2f7781"
         commit = rev_parse(first_rev)
         self.assertEqual(len(commit.parents), 0)
@@ -1036,14 +1038,14 @@ def test_rev_parse(self):
         self.assertRaises(BadName, rev_parse, first_rev + "~")
         self.assertRaises(BadName, rev_parse, first_rev + "^")
 
-        # short SHA1
+        # Short SHA1.
         commit2 = rev_parse(first_rev[:20])
         self.assertEqual(commit2, commit)
         commit2 = rev_parse(first_rev[:5])
         self.assertEqual(commit2, commit)
 
-        # todo: dereference tag into a blob 0.1.7^{blob} - quite a special one
-        # needs a tag which points to a blob
+        # TODO: Dereference tag into a blob 0.1.7^{blob} - quite a special one.
+        # Needs a tag which points to a blob.
 
         # ref^0 returns commit being pointed to, same with ref~0, and ^{}
         tag = rev_parse("0.1.4")
@@ -1051,7 +1053,7 @@ def test_rev_parse(self):
             self.assertEqual(tag.object, rev_parse("0.1.4%s" % token))
         # END handle multiple tokens
 
-        # try partial parsing
+        # Try partial parsing.
         max_items = 40
         for i, binsha in enumerate(self.rorepo.odb.sha_iter()):
             self.assertEqual(
@@ -1059,41 +1061,41 @@ def test_rev_parse(self):
                 binsha,
             )
             if i > max_items:
-                # this is rather slow currently, as rev_parse returns an object
-                # which requires accessing packs, it has some additional overhead
+                # This is rather slow currently, as rev_parse returns an object that
+                # requires accessing packs, so it has some additional overhead.
                 break
         # END for each binsha in repo
 
-        # missing closing brace commit^{tree
+        # Missing closing brace: commit^{tree
         self.assertRaises(ValueError, rev_parse, "0.1.4^{tree")
 
-        # missing starting brace
+        # Missing starting brace.
         self.assertRaises(ValueError, rev_parse, "0.1.4^tree}")
 
         # REVLOG
         #######
         head = self.rorepo.head
 
-        # need to specify a ref when using the @ syntax
+        # Need to specify a ref when using the @ syntax.
         self.assertRaises(BadObject, rev_parse, "%s@{0}" % head.commit.hexsha)
 
-        # uses HEAD.ref by default
+        # Uses HEAD.ref by default.
         self.assertEqual(rev_parse("@{0}"), head.commit)
         if not head.is_detached:
             refspec = "%s@{0}" % head.ref.name
             self.assertEqual(rev_parse(refspec), head.ref.commit)
-            # all additional specs work as well
+            # All additional specs work as well.
             self.assertEqual(rev_parse(refspec + "^{tree}"), head.commit.tree)
             self.assertEqual(rev_parse(refspec + ":CHANGES").type, "blob")
         # END operate on non-detached head
 
-        # position doesn't exist
+        # Position doesn't exist.
         self.assertRaises(IndexError, rev_parse, "@{10000}")
 
-        # currently, nothing more is supported
+        # Currently, nothing more is supported.
         self.assertRaises(NotImplementedError, rev_parse, "@{1 week ago}")
 
-        # the last position
+        # The last position.
         assert rev_parse("@{1}") != head.commit
 
     def test_repo_odbtype(self):
@@ -1114,12 +1116,12 @@ def test_submodules(self):
 
     @with_rw_repo("HEAD", bare=False)
     def test_submodule_update(self, rwrepo):
-        # fails in bare mode
+        # Fails in bare mode.
         rwrepo._bare = True
         self.assertRaises(InvalidGitRepositoryError, rwrepo.submodule_update)
         rwrepo._bare = False
 
-        # test create submodule
+        # Test submodule creation.
         sm = rwrepo.submodules[0]
         sm = rwrepo.create_submodule(
             "my_new_sub",
@@ -1128,7 +1130,7 @@ def test_submodule_update(self, rwrepo):
         )
         self.assertIsInstance(sm, Submodule)
 
-        # note: the rest of this functionality is tested in test_submodule
+        # NOTE: the rest of this functionality is tested in test_submodule.
 
     @with_rw_repo("HEAD")
     def test_git_file(self, rwrepo):
@@ -1154,11 +1156,11 @@ def last_commit(repo, rev, path):
             commit = next(repo.iter_commits(rev, path, max_count=1))
             commit.tree[path]
 
-        # This is based on this comment
+        # This is based on this comment:
         # https://github.com/gitpython-developers/GitPython/issues/60#issuecomment-23558741
-        # And we expect to set max handles to a low value, like 64
+        # And we expect to set max handles to a low value, like 64.
         # You should set ulimit -n X, see .travis.yml
-        # The loops below would easily create 500 handles if these would leak (4 pipes + multiple mapped files)
+        # The loops below would easily create 500 handles if these would leak (4 pipes + multiple mapped files).
         for _ in range(64):
             for repo_type in (GitCmdObjectDB, GitDB):
                 repo = Repo(self.rorepo.working_tree_dir, odbt=repo_type)
@@ -1174,13 +1176,13 @@ def test_remote_method(self):
     def test_empty_repo(self, rw_dir):
         """Assure we can handle empty repositories"""
         r = Repo.init(rw_dir, mkdir=False)
-        # It's ok not to be able to iterate a commit, as there is none
+        # It's ok not to be able to iterate a commit, as there is none.
         self.assertRaises(ValueError, r.iter_commits)
         self.assertEqual(r.active_branch.name, "master")
         assert not r.active_branch.is_valid(), "Branch is yet to be born"
 
-        # actually, when trying to create a new branch without a commit, git itself fails
-        # We should, however, not fail ungracefully
+        # Actually, when trying to create a new branch without a commit, git itself fails.
+        # We should, however, not fail ungracefully.
         self.assertRaises(BadName, r.create_head, "foo")
         self.assertRaises(BadName, r.create_head, "master")
         # It's expected to not be able to access a tree
@@ -1191,7 +1193,7 @@ def test_empty_repo(self, rw_dir):
         r.index.add([new_file_path])
         r.index.commit("initial commit\nBAD MESSAGE 1\n")
 
-        # Now a branch should be creatable
+        # Now a branch should be creatable.
         nb = r.create_head("foo")
         assert nb.is_valid()
 
@@ -1214,7 +1216,7 @@ def test_merge_base(self):
         self.assertRaises(ValueError, repo.merge_base)
         self.assertRaises(ValueError, repo.merge_base, "foo")
 
-        # two commit merge-base
+        # Two commit merge-base.
         res = repo.merge_base(c1, c2)
         self.assertIsInstance(res, list)
         self.assertEqual(len(res), 1)
@@ -1227,7 +1229,7 @@ def test_merge_base(self):
             self.assertEqual(len(res), 1)
         # end for each keyword signalling all merge-bases to be returned
 
-        # Test for no merge base - can't do as we have
+        # Test for no merge base - can't do as we have.
         self.assertRaises(GitCommandError, repo.merge_base, c1, "ffffff")
 
     def test_is_ancestor(self):
@@ -1254,22 +1256,22 @@ def test_is_valid_object(self):
         tree_sha = "960b40fe36"
         tag_sha = "42c2f60c43"
 
-        # Check for valid objects
+        # Check for valid objects.
         self.assertTrue(repo.is_valid_object(commit_sha))
         self.assertTrue(repo.is_valid_object(blob_sha))
         self.assertTrue(repo.is_valid_object(tree_sha))
         self.assertTrue(repo.is_valid_object(tag_sha))
 
-        # Check for valid objects of specific type
+        # Check for valid objects of specific type.
         self.assertTrue(repo.is_valid_object(commit_sha, "commit"))
         self.assertTrue(repo.is_valid_object(blob_sha, "blob"))
         self.assertTrue(repo.is_valid_object(tree_sha, "tree"))
         self.assertTrue(repo.is_valid_object(tag_sha, "tag"))
 
-        # Check for invalid objects
+        # Check for invalid objects.
         self.assertFalse(repo.is_valid_object(b"1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a", "blob"))
 
-        # Check for invalid objects of specific type
+        # Check for invalid objects of specific type.
         self.assertFalse(repo.is_valid_object(commit_sha, "blob"))
         self.assertFalse(repo.is_valid_object(blob_sha, "commit"))
         self.assertFalse(repo.is_valid_object(tree_sha, "commit"))
@@ -1290,16 +1292,16 @@ def test_git_work_tree_dotgit(self, rw_dir):
             worktree_path = cygpath(worktree_path)
         rw_master.git.worktree("add", worktree_path, branch.name)
 
-        # this ensures that we can read the repo's gitdir correctly
+        # This ensures that we can read the repo's gitdir correctly.
         repo = Repo(worktree_path)
         self.assertIsInstance(repo, Repo)
 
-        # this ensures we're able to actually read the refs in the tree, which
+        # This ensures we're able to actually read the refs in the tree, which
         # means we can read commondir correctly.
         commit = repo.head.commit
         self.assertIsInstance(commit, Object)
 
-        # this ensures we can read the remotes, which confirms we're reading
+        # This ensures we can read the remotes, which confirms we're reading
         # the config correctly.
         origin = repo.remotes.origin
         self.assertIsInstance(origin, Remote)
@@ -1308,11 +1310,11 @@ def test_git_work_tree_dotgit(self, rw_dir):
 
     @with_rw_directory
     def test_git_work_tree_env(self, rw_dir):
-        """Check that we yield to GIT_WORK_TREE"""
-        # clone a repo
-        # move .git directory to a subdirectory
-        # set GIT_DIR and GIT_WORK_TREE appropriately
-        # check that repo.working_tree_dir == rw_dir
+        """Check that we yield to GIT_WORK_TREE."""
+        # Clone a repo.
+        # 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"))
 
@@ -1376,7 +1378,7 @@ def test_clone_command_injection(self, rw_repo):
             rw_repo.clone(payload)
 
             assert not unexpected_file.exists()
-            # A repo was cloned with the payload as name
+            # A repo was cloned with the payload as name.
             assert pathlib.Path(payload).exists()
 
     @with_rw_repo("HEAD")
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 31a555ce2..e6e54c858 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import contextlib
 import os
 import shutil
@@ -74,48 +75,48 @@ def tearDown(self):
 
     def _do_base_tests(self, rwrepo):
         """Perform all tests in the given repository, it may be bare or nonbare"""
-        # manual instantiation
+        # Manual instantiation.
         smm = Submodule(rwrepo, "\0" * 20)
-        # name needs to be set in advance
+        # Name needs to be set in advance.
         self.assertRaises(AttributeError, getattr, smm, "name")
 
-        # iterate - 1 submodule
+        # Iterate - 1 submodule.
         sms = Submodule.list_items(rwrepo, self.k_subm_current)
         assert len(sms) == 1
         sm = sms[0]
 
-        # at a different time, there is None
+        # At a different time, there is None.
         assert len(Submodule.list_items(rwrepo, self.k_no_subm_tag)) == 0
 
         assert sm.path == "git/ext/gitdb"
-        assert sm.path != sm.name  # in our case, we have ids there, which don't equal the path
+        assert sm.path != sm.name  # In our case, we have ids there, which don't equal the path.
         assert sm.url.endswith("github.com/gitpython-developers/gitdb.git")
         assert sm.branch_path == "refs/heads/master"  # the default ...
         assert sm.branch_name == "master"
         assert sm.parent_commit == rwrepo.head.commit
-        # size is always 0
+        # Size is always 0.
         assert sm.size == 0
-        # the module is not checked-out yet
+        # The module is not checked-out yet.
         self.assertRaises(InvalidGitRepositoryError, sm.module)
 
-        # which is why we can't get the branch either - it points into the module() repository
+        # ...which is why we can't get the branch either - it points into the module() repository.
         self.assertRaises(InvalidGitRepositoryError, getattr, sm, "branch")
 
-        # branch_path works, as its just a string
+        # branch_path works, as it's just a string.
         assert isinstance(sm.branch_path, str)
 
-        # some commits earlier we still have a submodule, but its at a different commit
+        # Some commits earlier we still have a submodule, but it's at a different commit.
         smold = next(Submodule.iter_items(rwrepo, self.k_subm_changed))
         assert smold.binsha != sm.binsha
         assert smold != sm  # the name changed
 
-        # force it to reread its information
+        # Force it to reread its information.
         del smold._url
         smold.url == sm.url  # noqa: B015  # FIXME: Should this be an assertion?
 
-        # test config_reader/writer methods
+        # Test config_reader/writer methods.
         sm.config_reader()
-        new_smclone_path = None  # keep custom paths for later
+        new_smclone_path = None  # Keep custom paths for later.
         new_csmclone_path = None  #
         if rwrepo.bare:
             with self.assertRaises(InvalidGitRepositoryError):
@@ -123,7 +124,7 @@ def _do_base_tests(self, rwrepo):
                     pass
         else:
             with sm.config_writer() as writer:
-                # for faster checkout, set the url to the local path
+                # For faster checkout, set the url to the local path.
                 new_smclone_path = Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path))
                 writer.set_value("url", new_smclone_path)
                 writer.release()
@@ -132,76 +133,76 @@ def _do_base_tests(self, rwrepo):
         # END handle bare repo
         smold.config_reader()
 
-        # cannot get a writer on historical submodules
+        # Cannot get a writer on historical submodules.
         if not rwrepo.bare:
             with self.assertRaises(ValueError):
                 with smold.config_writer():
                     pass
         # END handle bare repo
 
-        # make the old into a new - this doesn't work as the name changed
+        # Make the old into a new - this doesn't work as the name changed.
         self.assertRaises(ValueError, smold.set_parent_commit, self.k_subm_current)
         # the sha is properly updated
         smold.set_parent_commit(self.k_subm_changed + "~1")
         assert smold.binsha != sm.binsha
 
-        # raises if the sm didn't exist in new parent - it keeps its
-        # parent_commit unchanged
+        # Raises if the sm didn't exist in new parent - it keeps its
+        # parent_commit unchanged.
         self.assertRaises(ValueError, smold.set_parent_commit, self.k_no_subm_tag)
 
-        # TEST TODO: if a path in the gitmodules file, but not in the index, it raises
+        # TEST TODO: If a path is in the .gitmodules file, but not in the index, it raises.
 
         # TEST UPDATE
         ##############
-        # module retrieval is not always possible
+        # Module retrieval is not always possible.
         if rwrepo.bare:
             self.assertRaises(InvalidGitRepositoryError, sm.module)
             self.assertRaises(InvalidGitRepositoryError, sm.remove)
             self.assertRaises(InvalidGitRepositoryError, sm.add, rwrepo, "here", "there")
         else:
-            # its not checked out in our case
+            # It's not checked out in our case.
             self.assertRaises(InvalidGitRepositoryError, sm.module)
             assert not sm.module_exists()
 
-            # currently there is only one submodule
+            # Currently there is only one submodule.
             assert len(list(rwrepo.iter_submodules())) == 1
             assert sm.binsha != "\0" * 20
 
             # TEST ADD
             ###########
-            # preliminary tests
-            # adding existing returns exactly the existing
+            # Preliminary tests.
+            # Adding existing returns exactly the existing.
             sma = Submodule.add(rwrepo, sm.name, sm.path)
             assert sma.path == sm.path
 
-            # no url and no module at path fails
+            # No url and no module at path fails.
             self.assertRaises(ValueError, Submodule.add, rwrepo, "newsubm", "pathtorepo", url=None)
 
             # CONTINUE UPDATE
             #################
 
-            # lets update it - its a recursive one too
+            # Let's update it - it's a recursive one too.
             newdir = osp.join(sm.abspath, "dir")
             os.makedirs(newdir)
 
-            # update fails if the path already exists non-empty
+            # Update fails if the path already exists non-empty.
             self.assertRaises(OSError, sm.update)
             os.rmdir(newdir)
 
-            # dry-run does nothing
+            # Dry-run does nothing.
             sm.update(dry_run=True, progress=prog)
             assert not sm.module_exists()
 
             assert sm.update() is sm
-            sm_repopath = sm.path  # cache for later
+            sm_repopath = sm.path  # Cache for later.
             assert sm.module_exists()
             assert isinstance(sm.module(), git.Repo)
             assert sm.module().working_tree_dir == sm.abspath
 
             # INTERLEAVE ADD TEST
             #####################
-            # url must match the one in the existing repository ( if submodule name suggests a new one )
-            # or we raise
+            # url must match the one in the existing repository (if submodule name suggests a new one)
+            # or we raise.
             self.assertRaises(
                 ValueError,
                 Submodule.add,
@@ -213,53 +214,53 @@ def _do_base_tests(self, rwrepo):
 
             # CONTINUE UPDATE
             #################
-            # we should have setup a tracking branch, which is also active
+            # We should have setup a tracking branch, which is also active.
             assert sm.module().head.ref.tracking_branch() is not None
 
-            # delete the whole directory and re-initialize
+            # Delete the whole directory and re-initialize.
             assert len(sm.children()) != 0
             # shutil.rmtree(sm.abspath)
             sm.remove(force=True, configuration=False)
             assert len(sm.children()) == 0
-            # dry-run does nothing
+            # Dry-run does nothing.
             sm.update(dry_run=True, recursive=False, progress=prog)
             assert len(sm.children()) == 0
 
             sm.update(recursive=False)
             assert len(list(rwrepo.iter_submodules())) == 2
-            assert len(sm.children()) == 1  # its not checked out yet
+            assert len(sm.children()) == 1  # It's not checked out yet.
             csm = sm.children()[0]
             assert not csm.module_exists()
             csm_repopath = csm.path
 
-            # adjust the path of the submodules module to point to the local destination
+            # Adjust the path of the submodules module to point to the local destination.
             new_csmclone_path = Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path, csm.path))
             with csm.config_writer() as writer:
                 writer.set_value("url", new_csmclone_path)
             assert csm.url == new_csmclone_path
 
-            # dry-run does nothing
+            # Dry-run does nothing.
             assert not csm.module_exists()
             sm.update(recursive=True, dry_run=True, progress=prog)
             assert not csm.module_exists()
 
-            # update recursively again
+            # Update recursively again.
             sm.update(recursive=True)
             assert csm.module_exists()
 
-            # tracking branch once again
+            # Tracking branch once again.
             assert csm.module().head.ref.tracking_branch() is not None
 
-            # this flushed in a sub-submodule
+            # This flushed in a sub-submodule.
             assert len(list(rwrepo.iter_submodules())) == 2
 
-            # reset both heads to the previous version, verify that to_latest_revision works
+            # Reset both heads to the previous version, verify that to_latest_revision works.
             smods = (sm.module(), csm.module())
             for repo in smods:
                 repo.head.reset("HEAD~2", working_tree=1)
             # END for each repo to reset
 
-            # dry run does nothing
+            # Dry-run does nothing.
             self.assertRaises(
                 RepositoryDirtyError,
                 sm.update,
@@ -279,89 +280,89 @@ def _do_base_tests(self, rwrepo):
             # END for each repo to check
             del smods
 
-            # if the head is detached, it still works ( but warns )
+            # If the head is detached, it still works (but warns).
             smref = sm.module().head.ref
             sm.module().head.ref = "HEAD~1"
-            # if there is no tracking branch, we get a warning as well
+            # If there is no tracking branch, we get a warning as well.
             csm_tracking_branch = csm.module().head.ref.tracking_branch()
             csm.module().head.ref.set_tracking_branch(None)
             sm.update(recursive=True, to_latest_revision=True)
 
             # to_latest_revision changes the child submodule's commit, it needs an
-            # update now
+            # update now.
             csm.set_parent_commit(csm.repo.head.commit)
 
-            # undo the changes
+            # Undo the changes.
             sm.module().head.ref = smref
             csm.module().head.ref.set_tracking_branch(csm_tracking_branch)
 
             # REMOVAL OF REPOSITORY
             #######################
-            # must delete something
+            # Must delete something.
             self.assertRaises(ValueError, csm.remove, module=False, configuration=False)
 
             # module() is supposed to point to gitdb, which has a child-submodule whose URL is still pointing
-            # to GitHub. To save time, we will change it to
+            # to GitHub. To save time, we will change it to:
             csm.set_parent_commit(csm.repo.head.commit)
             with csm.config_writer() as cw:
                 cw.set_value("url", self._small_repo_url())
             csm.repo.index.commit("adjusted URL to point to local source, instead of the internet")
 
             # We have modified the configuration, hence the index is dirty, and the
-            # deletion will fail
-            # NOTE: As we did  a few updates in the meanwhile, the indices were reset
-            # Hence we create some changes
+            # deletion will fail.
+            # NOTE: As we did a few updates in the meanwhile, the indices were reset.
+            # Hence we create some changes.
             csm.set_parent_commit(csm.repo.head.commit)
             with sm.config_writer() as writer:
                 writer.set_value("somekey", "somevalue")
             with csm.config_writer() as writer:
                 writer.set_value("okey", "ovalue")
             self.assertRaises(InvalidGitRepositoryError, sm.remove)
-            # if we remove the dirty index, it would work
+            # If we remove the dirty index, it would work.
             sm.module().index.reset()
-            # still, we have the file modified
+            # Still, we have the file modified.
             self.assertRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
             sm.module().index.reset(working_tree=True)
 
-            # enforce the submodule to be checked out at the right spot as well.
+            # Enforce the submodule to be checked out at the right spot as well.
             csm.update()
             assert csm.module_exists()
             assert csm.exists()
             assert osp.isdir(csm.module().working_tree_dir)
 
-            # this would work
+            # This would work.
             assert sm.remove(force=True, dry_run=True) is sm
             assert sm.module_exists()
             sm.remove(force=True, dry_run=True)
             assert sm.module_exists()
 
-            # but ... we have untracked files in the child submodule
+            # But... we have untracked files in the child submodule.
             fn = join_path_native(csm.module().working_tree_dir, "newfile")
             with open(fn, "w") as fd:
                 fd.write("hi")
             self.assertRaises(InvalidGitRepositoryError, sm.remove)
 
-            # forcibly delete the child repository
+            # Forcibly delete the child repository.
             prev_count = len(sm.children())
             self.assertRaises(ValueError, csm.remove, force=True)
             # We removed sm, which removed all submodules. However, the instance we
-            # have still points to the commit prior to that, where it still existed
+            # have still points to the commit prior to that, where it still existed.
             csm.set_parent_commit(csm.repo.commit(), check=False)
             assert not csm.exists()
             assert not csm.module_exists()
             assert len(sm.children()) == prev_count
-            # now we have a changed index, as configuration was altered.
-            # fix this
+            # Now we have a changed index, as configuration was altered.
+            # Fix this.
             sm.module().index.reset(working_tree=True)
 
-            # now delete only the module of the main submodule
+            # Now delete only the module of the main submodule.
             assert sm.module_exists()
             sm.remove(configuration=False, force=True)
             assert sm.exists()
             assert not sm.module_exists()
             assert sm.config_reader().get_value("url")
 
-            # delete the rest
+            # Delete the rest.
             sm_path = sm.path
             sm.remove()
             assert not sm.exists()
@@ -372,7 +373,7 @@ def _do_base_tests(self, rwrepo):
 
             # ADD NEW SUBMODULE
             ###################
-            # add a simple remote repo - trailing slashes are no problem
+            # Add a simple remote repo - trailing slashes are no problem.
             smid = "newsub"
             osmid = "othersub"
             nsm = Submodule.add(
@@ -386,11 +387,11 @@ def _do_base_tests(self, rwrepo):
             assert nsm.name == smid
             assert nsm.module_exists()
             assert nsm.exists()
-            # its not checked out
+            # It's not checked out.
             assert not osp.isfile(join_path_native(nsm.module().working_tree_dir, Submodule.k_modules_file))
             assert len(rwrepo.submodules) == 1
 
-            # add another submodule, but into the root, not as submodule
+            # Add another submodule, but into the root, not as submodule.
             osm = Submodule.add(rwrepo, osmid, csm_repopath, new_csmclone_path, Submodule.k_head_default)
             assert osm != nsm
             assert osm.module_exists()
@@ -399,28 +400,28 @@ def _do_base_tests(self, rwrepo):
 
             assert len(rwrepo.submodules) == 2
 
-            # commit the changes, just to finalize the operation
+            # Commit the changes, just to finalize the operation.
             rwrepo.index.commit("my submod commit")
             assert len(rwrepo.submodules) == 2
 
-            # needs update as the head changed, it thinks its in the history
-            # of the repo otherwise
+            # Needs update, as the head changed. It thinks it's in the history
+            # of the repo otherwise.
             nsm.set_parent_commit(rwrepo.head.commit)
             osm.set_parent_commit(rwrepo.head.commit)
 
             # MOVE MODULE
             #############
-            # invalid input
+            # Invalid input.
             self.assertRaises(ValueError, nsm.move, "doesntmatter", module=False, configuration=False)
 
-            # renaming to the same path does nothing
+            # Renaming to the same path does nothing.
             assert nsm.move(sm_path) is nsm
 
-            # rename a module
-            nmp = join_path_native("new", "module", "dir") + "/"  # new module path
+            # Rename a module.
+            nmp = join_path_native("new", "module", "dir") + "/"  # New module path.
             pmp = nsm.path
             assert nsm.move(nmp) is nsm
-            nmp = nmp[:-1]  # cut last /
+            nmp = nmp[:-1]  # Cut last /
             nmpl = to_native_path_linux(nmp)
             assert nsm.path == nmpl
             assert rwrepo.submodules[0].path == nmpl
@@ -431,14 +432,14 @@ def _do_base_tests(self, rwrepo):
             self.assertRaises(ValueError, nsm.move, mpath)
             os.remove(absmpath)
 
-            # now it works, as we just move it back
+            # Now it works, as we just move it back.
             nsm.move(pmp)
             assert nsm.path == pmp
             assert rwrepo.submodules[0].path == pmp
 
             # REMOVE 'EM ALL
             ################
-            # if a submodule's repo has no remotes, it can't be added without an explicit url
+            # If a submodule's repo has no remotes, it can't be added without an explicit url.
             osmod = osm.module()
 
             osm.remove(module=False)
@@ -448,7 +449,7 @@ def _do_base_tests(self, rwrepo):
             self.assertRaises(ValueError, Submodule.add, rwrepo, osmid, csm_repopath, url=None)
         # END handle bare mode
 
-        # Error if there is no submodule file here
+        # Error if there is no submodule file here.
         self.assertRaises(
             IOError,
             Submodule._config_parser,
@@ -487,11 +488,11 @@ def test_base_bare(self, rwrepo):
     )
     @with_rw_repo(k_subm_current, bare=False)
     def test_root_module(self, rwrepo):
-        # Can query everything without problems
+        # Can query everything without problems.
         rm = RootModule(self.rorepo)
         assert rm.module() is self.rorepo
 
-        # try attributes
+        # Try attributes.
         rm.binsha
         rm.mode
         rm.path
@@ -505,110 +506,110 @@ def test_root_module(self, rwrepo):
         with rm.config_writer():
             pass
 
-        # deep traversal gitdb / async
+        # Deep traversal gitdb / async.
         rsmsp = [sm.path for sm in rm.traverse()]
-        assert len(rsmsp) >= 2  # gitdb and async [and smmap], async being a child of gitdb
+        assert len(rsmsp) >= 2  # gitdb and async [and smmap], async being a child of gitdb.
 
-        # cannot set the parent commit as root module's path didn't exist
+        # Cannot set the parent commit as root module's path didn't exist.
         self.assertRaises(ValueError, rm.set_parent_commit, "HEAD")
 
         # TEST UPDATE
         #############
-        # setup commit which remove existing, add new and modify existing submodules
+        # Set up a commit that removes existing, adds new and modifies existing submodules.
         rm = RootModule(rwrepo)
         assert len(rm.children()) == 1
 
-        # modify path without modifying the index entry
-        # ( which is what the move method would do properly )
+        # Modify path without modifying the index entry.
+        # (Which is what the move method would do properly.)
         # ==================================================
         sm = rm.children()[0]
         pp = "path/prefix"
         fp = join_path_native(pp, sm.path)
         prep = sm.path
-        assert not sm.module_exists()  # was never updated after rwrepo's clone
+        assert not sm.module_exists()  # It was never updated after rwrepo's clone.
 
-        # assure we clone from a local source
+        # Ensure we clone from a local source.
         with sm.config_writer() as writer:
             writer.set_value("url", Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path)))
 
-        # dry-run does nothing
+        # Dry-run does nothing.
         sm.update(recursive=False, dry_run=True, progress=prog)
         assert not sm.module_exists()
 
         sm.update(recursive=False)
         assert sm.module_exists()
         with sm.config_writer() as writer:
-            writer.set_value("path", fp)  # change path to something with prefix AFTER url change
+            writer.set_value("path", fp)  # Change path to something with prefix AFTER url change.
 
-        # update doesn't fail, because list_items ignores the wrong path in such situations.
+        # Update doesn't fail, because list_items ignores the wrong path in such situations.
         rm.update(recursive=False)
 
-        # move it properly - doesn't work as it its path currently points to an indexentry
-        # which doesn't exist ( move it to some path, it doesn't matter here )
+        # Move it properly - doesn't work as it its path currently points to an indexentry
+        # which doesn't exist (move it to some path, it doesn't matter here).
         self.assertRaises(InvalidGitRepositoryError, sm.move, pp)
-        # reset the path(cache) to where it was, now it works
+        # Reset the path(cache) to where it was, now it works.
         sm.path = prep
-        sm.move(fp, module=False)  # leave it at the old location
+        sm.move(fp, module=False)  # Leave it at the old location.
 
         assert not sm.module_exists()
-        cpathchange = rwrepo.index.commit("changed sm path")  # finally we can commit
+        cpathchange = rwrepo.index.commit("changed sm path")  # Finally we can commit.
 
-        # update puts the module into place
+        # Update puts the module into place.
         rm.update(recursive=False, progress=prog)
         sm.set_parent_commit(cpathchange)
         assert sm.module_exists()
 
-        # add submodule
-        # ================
+        # Add submodule.
+        # ==============
         nsmn = "newsubmodule"
         nsmp = "submrepo"
         subrepo_url = Git.polish_url(osp.join(self.rorepo.working_tree_dir, rsmsp[0], rsmsp[1]))
         nsm = Submodule.add(rwrepo, nsmn, nsmp, url=subrepo_url)
-        csmadded = rwrepo.index.commit("Added submodule").hexsha  # make sure we don't keep the repo reference
+        csmadded = rwrepo.index.commit("Added submodule").hexsha  # Make sure we don't keep the repo reference.
         nsm.set_parent_commit(csmadded)
         assert nsm.module_exists()
-        # in our case, the module should not exist, which happens if we update a parent
-        # repo and a new submodule comes into life
+        # In our case, the module should not exist, which happens if we update a parent
+        # repo and a new submodule comes into life.
         nsm.remove(configuration=False, module=True)
         assert not nsm.module_exists() and nsm.exists()
 
-        # dry-run does nothing
+        # Dry-run does nothing.
         rm.update(recursive=False, dry_run=True, progress=prog)
 
-        # otherwise it will work
+        # Otherwise it will work.
         rm.update(recursive=False, progress=prog)
         assert nsm.module_exists()
 
-        # remove submodule - the previous one
+        # Remove submodule - the previous one.
         # ====================================
         sm.set_parent_commit(csmadded)
         smp = sm.abspath
         assert not sm.remove(module=False).exists()
-        assert osp.isdir(smp)  # module still exists
+        assert osp.isdir(smp)  # Module still exists.
         csmremoved = rwrepo.index.commit("Removed submodule")
 
-        # an update will remove the module
-        # not in dry_run
+        # An update will remove the module.
+        # Not in dry_run.
         rm.update(recursive=False, dry_run=True, force_remove=True)
         assert osp.isdir(smp)
 
-        # when removing submodules, we may get new commits as nested submodules are auto-committing changes
+        # When removing submodules, we may get new commits as nested submodules are auto-committing changes
         # to allow deletions without force, as the index would be dirty otherwise.
         # QUESTION: Why does this seem to work in test_git_submodule_compatibility() ?
         self.assertRaises(InvalidGitRepositoryError, rm.update, recursive=False, force_remove=False)
         rm.update(recursive=False, force_remove=True)
         assert not osp.isdir(smp)
 
-        # 'apply work' to the nested submodule and assure this is not removed/altered during updates
-        # Need to commit first, otherwise submodule.update wouldn't have a reason to change the head
+        # 'Apply work' to the nested submodule and ensure this is not removed/altered during updates
+        # Need to commit first, otherwise submodule.update wouldn't have a reason to change the head.
         touch(osp.join(nsm.module().working_tree_dir, "new-file"))
-        # We cannot expect is_dirty to even run as we wouldn't reset a head to the same location
+        # We cannot expect is_dirty to even run as we wouldn't reset a head to the same location.
         assert nsm.module().head.commit.hexsha == nsm.hexsha
         nsm.module().index.add([nsm])
         nsm.module().index.commit("added new file")
-        rm.update(recursive=False, dry_run=True, progress=prog)  # would not change head, and thus doesn't fail
-        # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run
-        # This would only run if our local branch is in the past and we have uncommitted changes
+        rm.update(recursive=False, dry_run=True, progress=prog)  # Would not change head, and thus doesn't fail.
+        # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run.
+        # This would only run if our local branch is in the past and we have uncommitted changes.
 
         prev_commit = nsm.module().head.commit
         rm.update(recursive=False, dry_run=False, progress=prog)
@@ -618,10 +619,10 @@ def test_root_module(self, rwrepo):
         rm.update(recursive=True, progress=prog, force_reset=True)
         assert prev_commit != nsm.module().head.commit, "head changed, as the remote url and its commit changed"
 
-        # change url ...
-        # ===============
-        # ... to the first repository, this way we have a fast checkout, and a completely different
-        # repository at the different url
+        # Change url...
+        # =============
+        # ...to the first repository. This way we have a fast checkout, and a completely different
+        # repository at the different url.
         nsm.set_parent_commit(csmremoved)
         nsmurl = Git.polish_url(osp.join(self.rorepo.working_tree_dir, rsmsp[0]))
         with nsm.config_writer() as writer:
@@ -629,7 +630,7 @@ def test_root_module(self, rwrepo):
         csmpathchange = rwrepo.index.commit("changed url")
         nsm.set_parent_commit(csmpathchange)
 
-        # Now nsm head is in the future of the tracked remote branch
+        # Now nsm head is in the future of the tracked remote branch.
         prev_commit = nsm.module().head.commit
         # dry-run does nothing
         rm.update(recursive=False, dry_run=True, progress=prog)
@@ -641,16 +642,16 @@ def test_root_module(self, rwrepo):
         assert len(rwrepo.submodules) == 1
         assert not rwrepo.submodules[0].children()[0].module_exists(), "nested submodule should not be checked out"
 
-        # add the submodule's changed commit to the index, which is what the
-        # user would do
-        # beforehand, update our instance's binsha with the new one
+        # Add the submodule's changed commit to the index, which is what the
+        # user would do.
+        # Beforehand, update our instance's binsha with the new one.
         nsm.binsha = nsm.module().head.commit.binsha
         rwrepo.index.add([nsm])
 
-        # change branch
-        # =================
-        # we only have one branch, so we switch to a virtual one, and back
-        # to the current one to trigger the difference
+        # Change branch.
+        # ==============
+        # We only have one branch, so we switch to a virtual one, and back
+        # to the current one to trigger the difference.
         cur_branch = nsm.branch
         nsmm = nsm.module()
         prev_commit = nsmm.head.commit
@@ -661,35 +662,35 @@ def test_root_module(self, rwrepo):
             nsm.set_parent_commit(csmbranchchange)
         # END for each branch to change
 
-        # Lets remove our tracking branch to simulate some changes
+        # Let's remove our tracking branch to simulate some changes.
         nsmmh = nsmm.head
-        assert nsmmh.ref.tracking_branch() is None  # never set it up until now
+        assert nsmmh.ref.tracking_branch() is None  # Never set it up until now.
         assert not nsmmh.is_detached
 
-        # dry run does nothing
+        # Dry-run does nothing.
         rm.update(recursive=False, dry_run=True, progress=prog)
         assert nsmmh.ref.tracking_branch() is None
 
-        # the real thing does
+        # The real thing does.
         rm.update(recursive=False, progress=prog)
 
         assert nsmmh.ref.tracking_branch() is not None
         assert not nsmmh.is_detached
 
-        # recursive update
+        # Recursive update.
         # =================
-        # finally we recursively update a module, just to run the code at least once
-        # remove the module so that it has more work
-        assert len(nsm.children()) >= 1  # could include smmap
+        # Finally we recursively update a module, just to run the code at least once
+        # remove the module so that it has more work.
+        assert len(nsm.children()) >= 1  # Could include smmap.
         assert nsm.exists() and nsm.module_exists() and len(nsm.children()) >= 1
-        # assure we pull locally only
+        # Ensure we pull locally only.
         nsmc = nsm.children()[0]
         with nsmc.config_writer() as writer:
             writer.set_value("url", subrepo_url)
-        rm.update(recursive=True, progress=prog, dry_run=True)  # just to run the code
+        rm.update(recursive=True, progress=prog, dry_run=True)  # Just to run the code.
         rm.update(recursive=True, progress=prog)
 
-        # gitdb: has either 1 or 2 submodules depending on the version
+        # gitdb: has either 1 or 2 submodules depending on the version.
         assert len(nsm.children()) >= 1 and nsmc.module_exists()
 
     @with_rw_repo(k_no_subm_tag, bare=False)
@@ -742,7 +743,7 @@ def test_list_only_valid_submodules(self, rwdir):
 
         assert len(repo.submodules) == 1
 
-        # Delete the directory from submodule
+        # Delete the directory from submodule.
         submodule_path = osp.join(repo_path, "module")
         shutil.rmtree(submodule_path)
         repo.git.add([submodule_path])
@@ -800,13 +801,13 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):
         smm.git.add(Git.polish_url(fp))
         smm.git.commit(m="new file added")
 
-        # submodules are retrieved from the current commit's tree, therefore we can't really get a new submodule
-        # object pointing to the new submodule commit
+        # Submodules are retrieved from the current commit's tree, therefore we can't really get a new submodule
+        # object pointing to the new submodule commit.
         sm_too = parent.submodules["module_moved"]
         assert parent.head.commit.tree[sm.path].binsha == sm.binsha
         assert sm_too.binsha == sm.binsha, "cached submodule should point to the same commit as updated one"
 
-        added_bies = parent.index.add([sm])  # added base-index-entries
+        added_bies = parent.index.add([sm])  # Added base-index-entries.
         assert len(added_bies) == 1
         parent.index.commit("add same submodule entry")
         commit_sm = parent.head.commit.tree[sm.path]
@@ -840,8 +841,8 @@ def assert_exists(sm, value=True):
 
         # end
 
-        # As git is backwards compatible itself, it would still recognize what we do here ... unless we really
-        # muss it up. That's the only reason why the test is still here ... .
+        # As git is backwards compatible itself, it would still recognize what we do here... unless we really
+        # muss it up. That's the only reason why the test is still here...
         assert len(parent.git.submodule().splitlines()) == 1
 
         module_repo_path = osp.join(sm.module().working_tree_dir, ".git")
@@ -855,12 +856,12 @@ def assert_exists(sm, value=True):
             assert find_submodule_git_dir(module_repo_path) is not None, "module pointed to by .git file must be valid"
         # end verify submodule 'style'
 
-        # test move
+        # Test move.
         new_sm_path = join_path_native("submodules", "one")
         sm.move(new_sm_path)
         assert_exists(sm)
 
-        # Add additional submodule level
+        # Add additional submodule level.
         csm = sm.module().create_submodule(
             "nested-submodule",
             join_path_native("nested-submodule", "working-tree"),
@@ -870,21 +871,21 @@ def assert_exists(sm, value=True):
         sm_head_commit = sm.module().commit()
         assert_exists(csm)
 
-        # Fails because there are new commits, compared to the remote we cloned from
+        # Fails because there are new commits, compared to the remote we cloned from.
         self.assertRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
         assert_exists(sm)
         assert sm.module().commit() == sm_head_commit
         assert_exists(csm)
 
-        # rename nested submodule
-        # This name would move itself one level deeper - needs special handling internally
+        # Rename nested submodule.
+        # This name would move itself one level deeper - needs special handling internally.
         new_name = csm.name + "/mine"
         assert csm.rename(new_name).name == new_name
         assert_exists(csm)
         assert csm.repo.is_dirty(index=True, working_tree=False), "index must contain changed .gitmodules file"
         csm.repo.index.commit("renamed module")
 
-        # keep_going evaluation
+        # keep_going evaluation.
         rsm = parent.submodule_update()
         assert_exists(sm)
         assert_exists(csm)
@@ -944,11 +945,11 @@ def test_remove_norefs(self, rwdir):
         parent.index.commit("Added submodule")
 
         assert sm.repo is parent  # yoh was surprised since expected sm repo!!
-        # so created a new instance for submodule
+        # So created a new instance for submodule.
         smrepo = git.Repo(osp.join(rwdir, "parent", sm.path))
-        # Adding a remote without fetching so would have no references
+        # Adding a remote without fetching so would have no references.
         smrepo.create_remote("special", "git@server-shouldnotmatter:repo.git")
-        # And we should be able to remove it just fine
+        # And we should be able to remove it just fine.
         sm.remove()
         assert not sm.exists()
 
@@ -977,8 +978,8 @@ def test_rename(self, rwdir):
 
     @with_rw_directory
     def test_branch_renames(self, rw_dir):
-        # Setup initial sandbox:
-        # parent repo has one submodule, which has all the latest changes
+        # Set up initial sandbox:
+        # The parent repo has one submodule, which has all the latest changes.
         source_url = self._small_repo_url()
         sm_source_repo = git.Repo.clone_from(source_url, osp.join(rw_dir, "sm-source"), b="master")
         parent_repo = git.Repo.init(osp.join(rw_dir, "parent"))
@@ -991,20 +992,20 @@ def test_branch_renames(self, rw_dir):
         parent_repo.index.commit("added submodule")
         assert sm.exists()
 
-        # Create feature branch with one new commit in submodule source
+        # Create feature branch with one new commit in submodule source.
         sm_fb = sm_source_repo.create_head("feature")
         sm_fb.checkout()
         new_file = touch(osp.join(sm_source_repo.working_tree_dir, "new-file"))
         sm_source_repo.index.add([new_file])
         sm.repo.index.commit("added new file")
 
-        # change designated submodule checkout branch to the new upstream feature branch
+        # Change designated submodule checkout branch to the new upstream feature branch.
         with sm.config_writer() as smcw:
             smcw.set_value("branch", sm_fb.name)
         assert sm.repo.is_dirty(index=True, working_tree=False)
         sm.repo.index.commit("changed submodule branch to '%s'" % sm_fb)
 
-        # verify submodule update with feature branch that leaves currently checked out branch in it's past
+        # Verify submodule update with feature branch that leaves currently checked out branch in it's past.
         sm_mod = sm.module()
         prev_commit = sm_mod.commit()
         assert sm_mod.head.ref.name == "master"
@@ -1016,22 +1017,22 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.head.ref.name == sm_fb.name
         assert sm_mod.commit() == sm_fb.commit
 
-        # Create new branch which is in our past, and thus seemingly unrelated to the currently checked out one
-        # To make it even 'harder', we shall fork and create a new commit
+        # Create new branch which is in our past, and thus seemingly unrelated to the currently checked out one.
+        # To make it even 'harder', we shall fork and create a new commit.
         sm_pfb = sm_source_repo.create_head("past-feature", commit="HEAD~20")
         sm_pfb.checkout()
         sm_source_repo.index.add([touch(osp.join(sm_source_repo.working_tree_dir, "new-file"))])
         sm_source_repo.index.commit("new file added, to past of '%r'" % sm_fb)
 
-        # Change designated submodule checkout branch to a new commit in its own past
+        # Change designated submodule checkout branch to a new commit in its own past.
         with sm.config_writer() as smcw:
             smcw.set_value("branch", sm_pfb.path)
         sm.repo.index.commit("changed submodule branch to '%s'" % sm_pfb)
 
-        # Test submodule updates - must fail if submodule is dirty
+        # Test submodule updates - must fail if submodule is dirty.
         touch(osp.join(sm_mod.working_tree_dir, "unstaged file"))
         # This doesn't fail as our own submodule binsha didn't change, and the reset is only triggered if
-        # to latest revision is True.
+        # to_latest_revision is True.
         parent_repo.submodule_update(to_latest_revision=False)
         assert sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head"
         assert sm_mod.commit() == sm_fb.commit, "Head wasn't reset"
diff --git a/test/test_tree.py b/test/test_tree.py
index c5ac8d539..5fc98e40c 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -14,14 +14,14 @@
 
 class TestTree(TestBase):
     def test_serializable(self):
-        # tree at the given commit contains a submodule as well
+        # Tree at the given commit contains a submodule as well.
         roottree = self.rorepo.tree("6c1faef799095f3990e9970bc2cb10aa0221cf9c")
         for item in roottree.traverse(ignore_self=False):
             if item.type != Tree.type:
                 continue
             # END skip non-trees
             tree = item
-            # trees have no dict
+            # Trees have no dict.
             self.assertRaises(AttributeError, setattr, tree, "someattr", 1)
 
             orig_data = tree.data_stream.read()
@@ -36,7 +36,7 @@ def test_serializable(self):
             testtree._deserialize(stream)
             assert testtree._cache == orig_cache
 
-            # replaces cache, but we make sure of it
+            # Replaces cache, but we make sure of it.
             del testtree._cache
             testtree._deserialize(stream)
         # END for each item in tree
@@ -54,29 +54,29 @@ def test_traverse(self):
         # END for each object
         assert all_items == root.list_traverse()
 
-        # limit recursion level to 0 - should be same as default iteration
+        # Limit recursion level to 0 - should be same as default iteration.
         assert all_items
         assert "CHANGES" in root
         assert len(list(root)) == len(list(root.traverse(depth=1)))
 
-        # only choose trees
+        # Only choose trees.
         trees_only = lambda i, d: i.type == "tree"
         trees = list(root.traverse(predicate=trees_only))
         assert len(trees) == len([i for i in root.traverse() if trees_only(i, 0)])
 
-        # test prune
+        # Test prune.
         lib_folder = lambda t, d: t.path == "lib"
         pruned_trees = list(root.traverse(predicate=trees_only, prune=lib_folder))
         assert len(pruned_trees) < len(trees)
 
-        # trees and blobs
+        # Trees and blobs.
         assert len(set(trees) | set(root.trees)) == len(trees)
         assert len({b for b in root if isinstance(b, Blob)} | set(root.blobs)) == len(root.blobs)
         subitem = trees[0][0]
         assert "/" in subitem.path
         assert subitem.name == osp.basename(subitem.path)
 
-        # assure that at some point the traversed paths have a slash in them
+        # Check that at some point the traversed paths have a slash in them.
         found_slash = False
         for item in root.traverse():
             assert osp.isabs(item.abspath)
@@ -84,8 +84,8 @@ def test_traverse(self):
                 found_slash = True
             # END check for slash
 
-            # slashes in paths are supported as well
-            # NOTE: on py3, / doesn't work with strings anymore ...
+            # Slashes in paths are supported as well.
+            # NOTE: On Python 3, / doesn't work with strings anymore...
             assert root[item.path] == item == root / item.path
         # END for each item
         assert found_slash

From cf9243a265a07b834e19974cd0c8d3a290379da7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 22 Oct 2023 20:20:14 -0400
Subject: [PATCH 225/994] Add a module docstring to tstrunner.py

It benefits from a docstring because it is not referenced in code
or documentation, and its purpose is/was otherwise unclear.

The docstring includes a reference to #1188 for more information.
---
 test/tstrunner.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/test/tstrunner.py b/test/tstrunner.py
index 441050c68..8613538eb 100644
--- a/test/tstrunner.py
+++ b/test/tstrunner.py
@@ -1,3 +1,5 @@
+"""Hook for MonkeyType (see PR #1188)."""
+
 import unittest
 
 loader = unittest.TestLoader()

From b5c3ca4bd29ee42d6a9f51ff21645e9165c7f0e7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 22 Oct 2023 20:26:12 -0400
Subject: [PATCH 226/994] Slightly improve readability of installation-test
 strings

This modifies content of the strings themselves, but only slightly.

- Make an exception message more readable.

- Improve spacing of the "python -c" script string in the sys.path
  test.
---
 test/test_installation.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/test_installation.py b/test/test_installation.py
index 3a0e43973..e7774d29d 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -45,7 +45,7 @@ def test_installation(self, rw_dir):
         self.assertEqual(
             0,
             result.returncode,
-            msg=result.stderr or result.stdout or "Selftest failed",
+            msg=result.stderr or result.stdout or "Self-test failed",
         )
 
         result = subprocess.run(
@@ -63,7 +63,7 @@ def test_installation(self, rw_dir):
         # 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(
-            [self.python, "-c", "import sys;import git; print(sys.path)"],
+            [self.python, "-c", "import sys; import git; print(sys.path)"],
             stdout=subprocess.PIPE,
             cwd=self.sources,
         )

From f444470bc2614b6b148ec4d8761ac804f72343b3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 22 Oct 2023 21:09:41 -0400
Subject: [PATCH 227/994] More wording improvements (in git module)

This futher improves some wording in docstrings and comments in a
handful of places within the git module.
---
 git/cmd.py                    |  2 +-
 git/config.py                 | 13 +++++--------
 git/diff.py                   |  4 ++--
 git/index/base.py             |  8 ++++----
 git/objects/submodule/root.py |  6 +++---
 git/objects/tree.py           |  4 ++--
 git/repo/base.py              |  2 +-
 git/util.py                   | 10 +++++-----
 8 files changed, 23 insertions(+), 26 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 384a39077..de7d34bf5 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -610,7 +610,7 @@ class CatFileContentStream:
         stream once our sized content region is empty.
 
         If not all data are read to the end of the object's lifetime, we read the
-        rest to assure the underlying stream continues to work.
+        rest to ensure the underlying stream continues to work.
         """
 
         __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size")
diff --git a/git/config.py b/git/config.py
index 31b8a665d..f0002658b 100644
--- a/git/config.py
+++ b/git/config.py
@@ -56,7 +56,7 @@
 T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool)
 
 if sys.version_info[:3] < (3, 7, 2):
-    # typing.Ordereddict not added until py 3.7.2
+    # typing.Ordereddict not added until Python 3.7.2.
     from collections import OrderedDict
 
     OrderedDict_OMD = OrderedDict
@@ -73,13 +73,10 @@
 log = logging.getLogger("git.config")
 log.addHandler(logging.NullHandler())
 
-# invariants
-# represents the configuration level of a configuration file
-
 
+# The configuration level of a configuration file.
 CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
 
-
 # Section pattern to detect conditional includes.
 # https://git-scm.com/docs/git-config#_conditional_includes
 CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
@@ -603,7 +600,7 @@ def read(self) -> None:  # type: ignore[override]
             files_to_read = [self._file_or_files]
         else:  # for lists or tuples
             files_to_read = list(self._file_or_files)
-        # end assure we have a copy of the paths to handle
+        # end ensure we have a copy of the paths to handle
 
         seen = set(files_to_read)
         num_read_include_files = 0
@@ -612,7 +609,7 @@ def read(self) -> None:  # type: ignore[override]
             file_ok = False
 
             if hasattr(file_path, "seek"):
-                # Must be a file objectfile-object.
+                # Must be a file-object.
                 file_path = cast(IO[bytes], file_path)  # TODO: Replace with assert to narrow type, once sure.
                 self._read(file_path, file_path.name)
             else:
@@ -626,7 +623,7 @@ def read(self) -> None:  # type: ignore[override]
                     continue
 
             # Read includes and append those that we didn't handle yet.
-            # We expect all paths to be normalized and absolute (and will assure that is the case).
+            # We expect all paths to be normalized and absolute (and will ensure that is the case).
             if self._has_includes():
                 for _, include_path in self._included_paths():
                     if include_path.startswith("~"):
diff --git a/git/diff.py b/git/diff.py
index d5740a7fc..08ae09444 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -122,8 +122,8 @@ def diff(
               the respective tree.
             * If :class:`Index <Diffable.Index>`, it will be compared against the index.
             * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
-            * It defaults to :class:`Index <Diffable.Index>` to assure the method will
-              not by-default fail on bare repositories.
+            * It defaults to :class:`Index <Diffable.Index>` so that the method will not
+              by default fail on bare repositories.
 
         :param paths:
             This a list of paths or a single path to limit the diff to. It will only
diff --git a/git/index/base.py b/git/index/base.py
index 4f1b6c469..a43afb320 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -340,9 +340,9 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             resolve more cases in a commonly correct manner. Specify trivial=True as kwarg
             to override that.
 
-            As the underlying git-read-tree command takes into account the current index,
-            it will be temporarily moved out of the way to assure there are no unsuspected
-            interferences.
+            As the underlying git-read-tree command takes into account the current
+            index, it will be temporarily moved out of the way to prevent any unexpected
+            interference.
         """
         if len(treeish) == 0 or len(treeish) > 3:
             raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
@@ -366,7 +366,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             # 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.
+            # The TemporaryFileSwap ensures the original file gets put back.
 
             stack.enter_context(TemporaryFileSwap(join_path_native(repo.git_dir, "index")))
             repo.git.read_tree(*arg_list, **kwargs)
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index b3c06ce9a..bc61f4ac2 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -133,7 +133,7 @@ def update(
 
         if progress is None:
             progress = RootUpdateProgress()
-        # END assure progress is set
+        # END ensure progress is set
 
         prefix = ""
         if dry_run:
@@ -300,7 +300,7 @@ def update(
                                 smr.rename(orig_name)
 
                                 # Early on, we verified that the our current tracking branch
-                                # exists in the remote. Now we have to assure that the
+                                # exists in the remote. Now we have to ensure that the
                                 # sha we point to is still contained in the new remote
                                 # tracking branch.
                                 smsha = sm.binsha
@@ -367,7 +367,7 @@ def update(
                             except OSError:
                                 # ...or reuse the existing one.
                                 tbr = git.Head(smm, sm.branch_path)
-                            # END assure tracking branch exists
+                            # END ensure tracking branch exists
 
                             tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
                             # NOTE: All head-resetting is done in the base implementation of update
diff --git a/git/objects/tree.py b/git/objects/tree.py
index c91c01807..708ab3edd 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -180,8 +180,8 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
         return self
 
     def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
-        """Add the given item to the tree. Its correctness is assumed, which
-        puts the caller into responsibility to assure the input is correct.
+        """Add the given item to the tree. Its correctness is assumed, so it is the
+        caller's responsibility to ensure that the input is correct.
 
         For more information on the parameters, see :meth:`add`.
 
diff --git a/git/repo/base.py b/git/repo/base.py
index 2694282d0..5d0a06198 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1448,7 +1448,7 @@ def archive(
         path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
         if not isinstance(path, (tuple, list)):
             path = [path]
-        # end assure paths is list
+        # end ensure paths is list (or tuple)
         self.git.archive("--", treeish, *path, **kwargs)
         return self
 
diff --git a/git/util.py b/git/util.py
index 4e96f7394..adbc8146f 100644
--- a/git/util.py
+++ b/git/util.py
@@ -283,7 +283,7 @@ def to_native_path_linux(path: PathLike) -> str:
 def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
     R"""Like join_path, but makes sure an OS native path is returned.
 
-    This is only needed to play it safe on Windows and to assure nice paths that only
+    This is only needed to play it safe on Windows and to ensure nice paths that only
     use '\'.
     """
     return to_native_path(join_path(a, *p))
@@ -1185,8 +1185,8 @@ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
 
 
 class Iterable(metaclass=IterableClassWatcher):
-    """Defines an interface for iterable items which is to assure a uniform
-    way to retrieve and iterate items within the git repository."""
+    """Defines an interface for iterable items, so there is a uniform way to retrieve
+    and iterate items within the git repository."""
 
     __slots__ = ()
 
@@ -1221,8 +1221,8 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
 @runtime_checkable
 class IterableObj(Protocol):
-    """Defines an interface for iterable items which is to assure a uniform
-    way to retrieve and iterate items within the git repository.
+    """Defines an interface for iterable items, so there is a uniform way to retrieve
+    and iterate items within the git repository.
 
     Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]
     """

From db317f1086b6886cbd43605d93b0f7f28a53f917 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 23 Oct 2023 01:59:18 -0400
Subject: [PATCH 228/994] Change :returns: to :return:

:return: was used extensively, while :returns: only appeared in two
docstrings.
---
 git/diff.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 08ae09444..829d53924 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -467,7 +467,7 @@ def rename_to(self) -> Optional[str]:
     @property
     def renamed(self) -> bool:
         """
-        :returns: True if the blob of our diff has been renamed
+        :return: True if the blob of our diff has been renamed
 
         :note: This property is deprecated.
             Please use the :attr:`renamed_file` property instead.
@@ -476,7 +476,7 @@ def renamed(self) -> bool:
 
     @property
     def renamed_file(self) -> bool:
-        """:returns: True if the blob of our diff has been renamed"""
+        """:return: True if the blob of our diff has been renamed"""
         return self.rename_from != self.rename_to
 
     @classmethod

From 720e4bbf2bfe2dc5dcf8c2fe68a30c797231120c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 23 Oct 2023 02:02:58 -0400
Subject: [PATCH 229/994] Fix TestBigRepoR.setUp info message

Changes that fix the message itself:

- Add a missing space between words (two parts were concatenated,
  with no space at the edge of either).

- Capitalize "GitPython" since that is the repo and project name.

Changes that improve how the message is produced:

- Make the entire literal part of the string the format string,
  instead of formatting the first part and concatenating the second
  part.

- Pass the format string and k_env_git_repo variable as separate
  arguments to logging.info, so the logging machinery takes care of
  substituting it for %s, rather than doing the substitution.
---
 test/performance/lib.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test/performance/lib.py b/test/performance/lib.py
index c793d771f..d1f0523f5 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -45,8 +45,9 @@ def setUp(self):
         repo_path = os.environ.get(k_env_git_repo)
         if repo_path is None:
             logging.info(
-                ("You can set the %s environment variable to a .git repository of" % k_env_git_repo)
-                + "your choice - defaulting to the gitpython repository"
+                "You can set the %s environment variable to a .git repository of your"
+                " choice - defaulting to the GitPython repository",
+                k_env_git_repo,
             )
             repo_path = osp.dirname(__file__)
         # end set some repo path

From faa19ac8bec46bb2d3dd26c9ea0e9dc1fb205de4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 00:05:56 -0400
Subject: [PATCH 230/994] Remove outdated git_daemon_launched Windows info

This removes the Windows-specific information in the warning
message in git_daemon_launched.

After the associated functionality was updated in #1684 and the
warning message was abridged accordingly, the functionality was
updated again in #1697, causing the message to be outdated and no
longer helpeful (since having git-daemon.exe in a PATH directory is
no longer necessary or useful), without any corresponding change to
the message.
---
 test/lib/helper.py | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index d415ba2e7..cef6779be 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -213,14 +213,6 @@ def git_daemon_launched(base_path, ip, port):
           and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
         """
         )
-        if is_win:
-            msg += textwrap.dedent(
-                R"""
-
-            On Windows,
-              the `git-daemon.exe` must be in PATH.
-              For MINGW, look into \Git\mingw64\libexec\git-core\, but problems with paths might appear."""
-            )
         log.warning(msg, ex, ip, port, base_path, base_path, exc_info=1)
 
         yield  # OK, assume daemon started manually.

From bffc537d17a47c354d5fd36338ff7318a08995c2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 01:02:55 -0400
Subject: [PATCH 231/994] Revise docstrings/comments in test helpers

---
 test/fixtures/env_case.py |  2 +-
 test/lib/helper.py        | 70 +++++++++++++++++++++------------------
 test/performance/lib.py   | 23 +++++--------
 3 files changed, 47 insertions(+), 48 deletions(-)

diff --git a/test/fixtures/env_case.py b/test/fixtures/env_case.py
index fe85ac41d..03b4df222 100644
--- a/test/fixtures/env_case.py
+++ b/test/fixtures/env_case.py
@@ -1,4 +1,4 @@
-# Steps 3 and 4 for test_it_avoids_upcasing_unrelated_environment_variable_names.
+"""Steps 3 and 4 for test_it_avoids_upcasing_unrelated_environment_variable_names."""
 
 import subprocess
 import sys
diff --git a/test/lib/helper.py b/test/lib/helper.py
index cef6779be..03ae62239 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -3,6 +3,7 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
 import contextlib
 from functools import wraps
 import gc
@@ -65,9 +66,10 @@ def fixture(name):
 
 
 class StringProcessAdapter(object):
+    """Allows strings to be used as process objects returned by subprocess.Popen.
 
-    """Allows to use strings as Process object as returned by SubProcess.Popen.
-    Its tailored to work with the test system only"""
+    This is tailored to work with the test system only.
+    """
 
     def __init__(self, input_string):
         self.stdout = io.BytesIO(input_string)
@@ -86,7 +88,7 @@ def wait(self):
 
 def with_rw_directory(func):
     """Create a temporary directory which can be written to, remove it if the
-    test succeeds, but leave it otherwise to aid additional debugging"""
+    test succeeds, but leave it otherwise to aid additional debugging."""
 
     @wraps(func)
     def wrapper(self):
@@ -106,7 +108,7 @@ def wrapper(self):
             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
+            # a Windows-only issue. In fact things should be deleted, as well as
             # memory maps closed, once objects go out of scope. For some reason
             # though this is not the case here unless we collect explicitly.
             gc.collect()
@@ -117,8 +119,7 @@ def wrapper(self):
 
 
 def with_rw_repo(working_tree_ref, bare=False):
-    """
-    Same as with_bare_repo, but clones the rorepo as non-bare repository, checking
+    """Same as with_bare_repo, but clones the rorepo as non-bare repository, checking
     out the working tree at the given working_tree_ref.
 
     This repository type is more costly due to the working copy checkout.
@@ -199,7 +200,7 @@ def git_daemon_launched(base_path, ip, port):
                 base_path=base_path,
                 as_process=True,
             )
-        # yes, I know ... fortunately, this is always going to work if sleep time is just large enough
+        # Yes, I know... fortunately, this is always going to work if sleep time is just large enough.
         time.sleep(0.5 * (1 + is_win))
     except Exception as ex:
         msg = textwrap.dedent(
@@ -225,33 +226,36 @@ def git_daemon_launched(base_path, ip, port):
                 log.debug("Killing git-daemon...")
                 gd.proc.kill()
             except Exception as ex:
-                ## Either it has died (and we're here), or it won't die, again here...
+                # Either it has died (and we're here), or it won't die, again here...
                 log.debug("Hidden error while Killing git-daemon: %s", ex, exc_info=1)
 
 
 def with_rw_and_rw_remote_repo(working_tree_ref):
-    """
-    Same as with_rw_repo, but also provides a writable remote repository from which the
-    rw_repo has been forked as well as a handle for a git-daemon that may be started to
-    run the remote_repo.
-    The remote repository was cloned as bare repository from the ro repo, whereas
-    the rw repo has a working tree and was cloned from the remote repository.
+    """Same as with_rw_repo, but also provides a writable remote repository from which
+    the rw_repo has been forked as well as a handle for a git-daemon that may be started
+    to run the remote_repo.
 
-    remote_repo has two remotes: origin and daemon_origin. One uses a local url,
-    the other uses a server url. The daemon setup must be done on system level
-    and should be an inetd service that serves tempdir.gettempdir() and all
-    directories in it.
+    The remote repository was cloned as bare repository from the ro repo, whereas the rw
+    repo has a working tree and was cloned from the remote repository.
+
+    remote_repo has two remotes: origin and daemon_origin. One uses a local url, the
+    other uses a server url. The daemon setup must be done on system level and should be
+    an inetd service that serves tempdir.gettempdir() and all directories in it.
 
     The following sketch demonstrates this::
-     rorepo ---<bare clone>---> rw_remote_repo ---<clone>---> rw_repo
+
+        rorepo ---<bare clone>---> rw_remote_repo ---<clone>---> rw_repo
 
     The test case needs to support the following signature::
+
         def case(self, rw_repo, rw_daemon_repo)
 
     This setup allows you to test push and pull scenarios and hooks nicely.
 
-    See working dir info in with_rw_repo
-    :note: We attempt to launch our own invocation of git-daemon, which will be shutdown at the end of the test.
+    See working dir info in :func:`with_rw_repo`.
+
+    :note: We attempt to launch our own invocation of git-daemon, which will be shut
+        down at the end of the test.
     """
     from git import Git, Remote  # To avoid circular deps.
 
@@ -264,16 +268,16 @@ def remote_repo_creator(self):
             rw_repo_dir = tempfile.mktemp(prefix="daemon_cloned_repo-%s-" % func.__name__)
 
             rw_daemon_repo = self.rorepo.clone(rw_daemon_repo_dir, shared=True, bare=True)
-            # recursive alternates info ?
+            # Recursive alternates info?
             rw_repo = rw_daemon_repo.clone(rw_repo_dir, shared=True, bare=False, n=True)
             try:
                 rw_repo.head.commit = working_tree_ref
                 rw_repo.head.reference.checkout()
 
-                # prepare for git-daemon
+                # Prepare for git-daemon.
                 rw_daemon_repo.daemon_export = True
 
-                # this thing is just annoying !
+                # This thing is just annoying!
                 with rw_daemon_repo.config_writer() as crw:
                     section = "daemon"
                     try:
@@ -340,9 +344,7 @@ def remote_repo_creator(self):
 
 
 class TestBase(TestCase):
-
-    """
-    Base Class providing default functionality to all tests such as:
+    """Base class providing default functionality to all tests such as:
 
     - Utility functions provided by the TestCase base of the unittest method such as::
         self.fail("todo")
@@ -355,20 +357,20 @@ class TestBase(TestCase):
 
       The rorepo is in fact your current project's git repo. If you refer to specific
       shas for your objects, be sure you choose some that are part of the immutable portion
-      of the project history ( to assure tests don't fail for others ).
+      of the project history (so that tests don't fail for others).
     """
 
     def _small_repo_url(self):
-        """:return" a path to a small, clonable repository"""
+        """:return: A path to a small, clonable repository"""
         from git.cmd import Git
 
         return Git.polish_url(osp.join(self.rorepo.working_tree_dir, "git/ext/gitdb/gitdb/ext/smmap"))
 
     @classmethod
     def setUpClass(cls):
-        """
-        Dynamically add a read-only repository to our actual type. This way
-        each test type has its own repository
+        """Dynamically add a read-only repository to our actual type.
+
+        This way, each test type has its own repository.
         """
         from git import Repo
 
@@ -383,7 +385,9 @@ def tearDownClass(cls):
     def _make_file(self, rela_path, data, repo=None):
         """
         Create a file at the given path relative to our repository, filled
-        with the given data. Returns absolute path to created file.
+        with the given data.
+
+        :return: An absolute path to the created file.
         """
         repo = repo or self.rorepo
         abs_path = osp.join(repo.working_tree_dir, rela_path)
diff --git a/test/performance/lib.py b/test/performance/lib.py
index d1f0523f5..8c68db782 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -1,4 +1,5 @@
-"""Contains library functions"""
+"""Support library for tests."""
+
 import logging
 import os
 import tempfile
@@ -20,22 +21,16 @@
 
 
 class TestBigRepoR(TestBase):
-
     """TestCase providing access to readonly 'big' repositories using the following
     member variables:
 
-    * gitrorepo
-
-     * Read-Only git repository - actually the repo of git itself
-
-    * puregitrorepo
+    * gitrorepo:
+      Read-Only git repository - actually (by default) the repo of GitPython itself.
 
-     * As gitrepo, but uses pure python implementation
+    * puregitrorepo:
+      Like gitrorepo, but uses a pure Python implementation for its object database.
     """
 
-    # { Invariants
-    # } END invariants
-
     def setUp(self):
         try:
             super(TestBigRepoR, self).setUp()
@@ -62,10 +57,10 @@ def tearDown(self):
 
 
 class TestBigRepoRW(TestBigRepoR):
+    """Like :class:`TestBigRepoR`, but provides a big repository that we can write to.
 
-    """As above, but provides a big repository that we can write to.
-
-    Provides ``self.gitrwrepo`` and ``self.puregitrwrepo``"""
+    Provides ``self.gitrwrepo`` and ``self.puregitrwrepo``.
+    """
 
     def setUp(self):
         self.gitrwrepo = None

From 30f49d9deaa0489b42d93c96df584690b39aa79d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 01:10:55 -0400
Subject: [PATCH 232/994] Revise docstrings/comments in performance tests

---
 test/performance/test_commit.py  | 20 ++++++++--------
 test/performance/test_odb.py     |  9 ++++----
 test/performance/test_streams.py | 39 ++++++++++++++++----------------
 3 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py
index dbe2ad43e..fad0641be 100644
--- a/test/performance/test_commit.py
+++ b/test/performance/test_commit.py
@@ -1,8 +1,10 @@
-# test_performance.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
+
+"""Performance tests for commits (iteration, traversal, and serialization)."""
+
 from io import BytesIO
 from time import time
 import sys
@@ -19,7 +21,7 @@ def tearDown(self):
 
         gc.collect()
 
-    # ref with about 100 commits in its history
+    # ref with about 100 commits in its history.
     ref_100 = "0.1.6"
 
     def _query_commit_info(self, c):
@@ -36,9 +38,9 @@ def test_iteration(self):
         no = 0
         nc = 0
 
-        # find the first commit containing the given path - always do a full
-        # iteration ( restricted to the path in question ), but in fact it should
-        # return quite a lot of commits, we just take one and hence abort the operation
+        # Find the first commit containing the given path. Always do a full iteration
+        # (restricted to the path in question). This should return quite a lot of
+        # commits. We just take one and hence abort the operation.
 
         st = time()
         for c in self.rorepo.iter_commits(self.ref_100):
@@ -57,7 +59,7 @@ def test_iteration(self):
         )
 
     def test_commit_traversal(self):
-        # bound to cat-file parsing performance
+        # Bound to cat-file parsing performance.
         nc = 0
         st = time()
         for c in self.gitrorepo.commit().traverse(branch_first=False):
@@ -71,7 +73,7 @@ def test_commit_traversal(self):
         )
 
     def test_commit_iteration(self):
-        # bound to stream parsing performance
+        # Bound to stream parsing performance.
         nc = 0
         st = time()
         for c in Commit.iter_items(self.gitrorepo, self.gitrorepo.head):
@@ -89,8 +91,8 @@ def test_commit_serialization(self):
 
         rwrepo = self.gitrwrepo
         make_object = rwrepo.odb.store
-        # direct serialization - deserialization can be tested afterwards
-        # serialization is probably limited on IO
+        # Direct serialization - deserialization can be tested afterwards.
+        # Serialization is probably limited on IO.
         hc = rwrepo.commit(rwrepo.head)
 
         nc = 5000
diff --git a/test/performance/test_odb.py b/test/performance/test_odb.py
index 4208c4181..70934ad6b 100644
--- a/test/performance/test_odb.py
+++ b/test/performance/test_odb.py
@@ -1,4 +1,5 @@
-"""Performance tests for object store"""
+"""Performance tests for object store."""
+
 import sys
 from time import time
 
@@ -24,7 +25,7 @@ def test_random_access(self):
             results[0].append(elapsed)
 
             # GET TREES
-            # walk all trees of all commits
+            # Walk all trees of all commits.
             st = time()
             blobs_per_commit = []
             nt = 0
@@ -35,7 +36,7 @@ def test_random_access(self):
                     nt += 1
                     if item.type == "blob":
                         blobs.append(item)
-                    # direct access for speed
+                    # Direct access for speed.
                 # END while trees are there for walking
                 blobs_per_commit.append(blobs)
             # END for each commit
@@ -75,7 +76,7 @@ def test_random_access(self):
             results[2].append(elapsed)
         # END for each repo type
 
-        # final results
+        # Final results.
         for test_name, a, b in results:
             print(
                 "%s: %f s vs %f s, pure is %f times slower" % (test_name, a, b, b / a),
diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py
index 25e081578..619126921 100644
--- a/test/performance/test_streams.py
+++ b/test/performance/test_streams.py
@@ -1,4 +1,5 @@
-"""Performance data streaming performance"""
+"""Performance tests for data streaming."""
+
 import os
 import subprocess
 import sys
@@ -15,13 +16,13 @@
 
 
 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
+    large_data_size_bytes = 1000 * 1000 * 10  # Some MiB should do it.
+    moderate_data_size_bytes = 1000 * 1000 * 1  # Just 1 MiB.
 
     @with_rw_repo("HEAD", bare=True)
     def test_large_data_streaming(self, rwrepo):
-        # TODO: This part overlaps with the same file in gitdb.test.performance.test_stream
-        # It should be shared if possible
+        # TODO: This part overlaps with the same file in gitdb.test.performance.test_stream.
+        # It should be shared if possible.
         ldb = LooseObjectDB(osp.join(rwrepo.git_dir, "objects"))
 
         for randomize in range(2):
@@ -32,7 +33,7 @@ def test_large_data_streaming(self, rwrepo):
             elapsed = time() - st
             print("Done (in %f s)" % elapsed, file=sys.stderr)
 
-            # writing - due to the compression it will seem faster than it is
+            # Writing - due to the compression it will seem faster than it is.
             st = time()
             binsha = ldb.store(IStream("blob", size, stream)).binsha
             elapsed_add = time() - st
@@ -45,7 +46,7 @@ def test_large_data_streaming(self, rwrepo):
             msg %= (size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add)
             print(msg, file=sys.stderr)
 
-            # reading all at once
+            # Reading all at once.
             st = time()
             ostream = ldb.stream(binsha)
             shadata = ostream.read()
@@ -57,7 +58,7 @@ def test_large_data_streaming(self, rwrepo):
             msg %= (size_kib, desc, elapsed_readall, size_kib / elapsed_readall)
             print(msg, file=sys.stderr)
 
-            # reading in chunks of 1 MiB
+            # Reading in chunks of 1 MiB.
             cs = 512 * 1000
             chunks = []
             st = time()
@@ -86,7 +87,7 @@ def test_large_data_streaming(self, rwrepo):
                 file=sys.stderr,
             )
 
-            # del db file so git has something to do
+            # del db file so git has something to do.
             ostream = None
             import gc
 
@@ -95,11 +96,11 @@ def test_large_data_streaming(self, rwrepo):
 
             # VS. CGIT
             ##########
-            # CGIT ! Can using the cgit programs be faster ?
+            # CGIT! Can using the cgit programs be faster?
             proc = rwrepo.git.hash_object("-w", "--stdin", as_process=True, istream=subprocess.PIPE)
 
-            # write file - pump everything in at once to be a fast as possible
-            data = stream.getvalue()  # cache it
+            # Write file - pump everything in at once to be a fast as possible.
+            data = stream.getvalue()  # Cache it.
             st = time()
             proc.stdin.write(data)
             proc.stdin.close()
@@ -107,22 +108,22 @@ def test_large_data_streaming(self, rwrepo):
             proc.wait()
             gelapsed_add = time() - st
             del data
-            assert gitsha == bin_to_hex(binsha)  # we do it the same way, right ?
+            assert gitsha == bin_to_hex(binsha)  # We do it the same way, right?
 
-            #  as its the same sha, we reuse our path
+            # As it's the same sha, we reuse our path.
             fsize_kib = osp.getsize(db_file) / 1000
             msg = "Added %i KiB (filesize = %i KiB) of %s data to using git-hash-object in %f s ( %f Write KiB / s)"
             msg %= (size_kib, fsize_kib, desc, gelapsed_add, size_kib / gelapsed_add)
             print(msg, file=sys.stderr)
 
-            # compare ...
+            # Compare.
             print(
                 "Git-Python is %f %% faster than git when adding big %s files"
                 % (100.0 - (elapsed_add / gelapsed_add) * 100, desc),
                 file=sys.stderr,
             )
 
-            # read all
+            # Read all.
             st = time()
             _hexsha, _typename, size, data = rwrepo.git.get_object_data(gitsha)
             gelapsed_readall = time() - st
@@ -132,14 +133,14 @@ def test_large_data_streaming(self, rwrepo):
                 file=sys.stderr,
             )
 
-            # compare
+            # Compare.
             print(
                 "Git-Python is %f %% faster than git when reading big %sfiles"
                 % (100.0 - (elapsed_readall / gelapsed_readall) * 100, desc),
                 file=sys.stderr,
             )
 
-            # read chunks
+            # Read chunks.
             st = time()
             _hexsha, _typename, size, stream = rwrepo.git.stream_object_data(gitsha)
             while True:
@@ -158,7 +159,7 @@ def test_large_data_streaming(self, rwrepo):
             )
             print(msg, file=sys.stderr)
 
-            # compare
+            # Compare.
             print(
                 "Git-Python is %f %% faster than git when reading big %s files in chunks"
                 % (100.0 - (elapsed_readchunks / gelapsed_readchunks) * 100, desc),

From 8d3efc54ea3be84b5d29fe388ebe566cc4e8f704 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 01:39:49 -0400
Subject: [PATCH 233/994] Remove explicit inheritance from object in test suite

---
 test/lib/helper.py     | 2 +-
 test/test_index.py     | 2 +-
 test/test_submodule.py | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 03ae62239..8725cd13f 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -65,7 +65,7 @@ def fixture(name):
 # { Adapters
 
 
-class StringProcessAdapter(object):
+class StringProcessAdapter:
     """Allows strings to be used as process objects returned by subprocess.Popen.
 
     This is tailored to work with the test system only.
diff --git a/test/test_index.py b/test/test_index.py
index c73d25543..3c88ce8e1 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -868,7 +868,7 @@ def test_add_a_file_with_wildcard_chars(self, rw_dir):
     def test__to_relative_path_at_root(self):
         root = osp.abspath(os.sep)
 
-        class Mocked(object):
+        class Mocked:
             bare = False
             git_dir = root
             working_tree_dir = root
diff --git a/test/test_submodule.py b/test/test_submodule.py
index e6e54c858..3eec41ee1 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1044,7 +1044,7 @@ def test_branch_renames(self, rw_dir):
 
     @skipUnless(is_win, "Specifically for Windows.")
     def test_to_relative_path_with_super_at_root_drive(self):
-        class Repo(object):
+        class Repo:
             working_tree_dir = "D:\\"
 
         super_repo = Repo()

From a5fc1d863be74db0e2b9c4e962937e9ee4efb8a4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 01:55:36 -0400
Subject: [PATCH 234/994] Improve consistency of "END" comments

In the git module (including modules it contains).

This does not add any new "# end" or "# END" comments. Instead, it
improves the consistency and clarity of existing ones by converting
each "# end" comment to "# END" (since capitalization was not used
systematically to indicate nesting level or other semantic
information, and capitalizing "END" makes clear the nature of the
comment), and by adding some information to a few of the comments.

This also removes end comments that did not provide information or
significant visual indication that a "section" was ending, or where
the visual indication they provided was at least as well provided
by replacing them with a blank line. However, it does *not* attempt
to apply this change everywhere it might be applicable.
---
 git/cmd.py                    | 10 ++++----
 git/config.py                 | 28 +++++++++++-----------
 git/diff.py                   |  8 +++----
 git/index/base.py             |  6 ++---
 git/index/fun.py              |  2 +-
 git/objects/commit.py         |  6 ++---
 git/objects/submodule/base.py | 44 ++++++++++++++++-------------------
 git/objects/submodule/root.py |  4 ++--
 git/refs/symbolic.py          |  2 +-
 git/remote.py                 |  7 +++---
 git/repo/base.py              | 12 +++++-----
 git/repo/fun.py               |  2 +-
 git/util.py                   |  3 ++-
 13 files changed, 64 insertions(+), 70 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index de7d34bf5..f76c19a66 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -979,7 +979,7 @@ def execute(
         else:
             cmd_not_found_exception = FileNotFoundError  # NOQA # exists, flake8 unknown @UndefinedVariable
             maybe_patch_caller_env = contextlib.nullcontext()
-        # end handle
+        # END handle
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
         if shell is None:
@@ -1048,7 +1048,7 @@ def kill_process(pid: int) -> None:
                 pass
             return
 
-        # end
+        # END def kill_process
 
         if kill_after_timeout is not None:
             kill_check = threading.Event()
@@ -1100,7 +1100,7 @@ def kill_process(pid: int) -> None:
             def as_text(stdout_value: Union[bytes, str]) -> str:
                 return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
 
-            # end
+            # END def as_text
 
             if stderr_value:
                 log.info(
@@ -1304,9 +1304,9 @@ def _call_process(
                     "Couldn't find argument '%s' in args %s to insert cmd options after"
                     % (insert_after_this_arg, str(ext_args))
                 ) from err
-            # end handle error
+            # END handle error
             args_list = ext_args[: index + 1] + opt_args + ext_args[index + 1 :]
-        # end handle opts_kwargs
+        # END handle opts_kwargs
 
         call = [self.GIT_PYTHON_GIT_EXECUTABLE]
 
diff --git a/git/config.py b/git/config.py
index f0002658b..020d823d0 100644
--- a/git/config.py
+++ b/git/config.py
@@ -446,12 +446,11 @@ def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
         def string_decode(v: str) -> str:
             if v[-1] == "\\":
                 v = v[:-1]
-            # end cut trailing escapes to prevent decode error
+            # END cut trailing escapes to prevent decode error
 
             return v.encode(defenc).decode("unicode_escape")
-            # end
 
-        # end
+        # END def string_decode
 
         while True:
             # We assume to read binary!
@@ -496,12 +495,12 @@ def string_decode(v: str) -> str:
                     optval = optval.strip()
                     if optval == '""':
                         optval = ""
-                    # end handle empty string
+                    # END handle empty string
                     optname = self.optionxform(optname.rstrip())
                     if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
                         is_multi_line = True
                         optval = string_decode(optval[1:])
-                    # end handle multi-line
+                    # END handle multi-line
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)
                 else:
@@ -516,7 +515,7 @@ def string_decode(v: str) -> str:
                 if line.endswith('"'):
                     is_multi_line = False
                     line = line[:-1]
-                # end handle quotations
+                # END handle quotations
                 optval = cursect.getlast(optname)
                 cursect.setlast(optname, optval + string_decode(line))
             # END parse section or option
@@ -600,7 +599,7 @@ def read(self) -> None:  # type: ignore[override]
             files_to_read = [self._file_or_files]
         else:  # for lists or tuples
             files_to_read = list(self._file_or_files)
-        # end ensure we have a copy of the paths to handle
+        # END ensure we have a copy of the paths to handle
 
         seen = set(files_to_read)
         num_read_include_files = 0
@@ -631,11 +630,11 @@ def read(self) -> None:  # type: ignore[override]
                     if not osp.isabs(include_path):
                         if not file_ok:
                             continue
-                        # end ignore relative paths if we don't know the configuration file path
+                        # END ignore relative paths if we don't know the configuration file path
                         file_path = cast(PathLike, file_path)
                         assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
                         include_path = osp.join(osp.dirname(file_path), include_path)
-                    # end make include path absolute
+                    # END make include path absolute
                     include_path = osp.normpath(include_path)
                     if include_path in seen or not os.access(include_path, os.R_OK):
                         continue
@@ -643,15 +642,14 @@ def read(self) -> None:  # type: ignore[override]
                     # Insert included file to the top to be considered first.
                     files_to_read.insert(0, include_path)
                     num_read_include_files += 1
-                # each include path in configuration file
-            # end handle includes
+                # END each include path in configuration file
+            # END handle includes
         # END for each file object to read
 
         # If there was no file included, we can safely write back (potentially) the
         # configuration file without altering its meaning.
         if num_read_include_files == 0:
             self._merge_includes = False
-        # end
 
     def _write(self, fp: IO) -> None:
         """Write an .ini-format representation of the configuration state in
@@ -714,7 +712,7 @@ def write(self) -> None:
                 "Cannot write back if there is not exactly a single file to write to, have %i files"
                 % len(self._file_or_files)
             )
-        # end assert multiple files
+        # END assert multiple files
 
         if self._has_includes():
             log.debug(
@@ -722,7 +720,7 @@ def write(self) -> None:
                 + "Set merge_includes=False to prevent this."
             )
             return None
-        # end
+        # END stop if we have include files
 
         fp = self._file_or_files
 
@@ -904,7 +902,7 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
         new_section = self._sections[new_name]
         for k, vs in self.items_all(section):
             new_section.setall(k, vs)
-        # end for each value to copy
+        # END for each value to copy
 
         # This call writes back the changes, which is why we don't have the respective decorator.
         self.remove_section(section)
diff --git a/git/diff.py b/git/diff.py
index 829d53924..acf32af7d 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -442,7 +442,7 @@ def __str__(self) -> str:
                 msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
             except UnicodeDecodeError:
                 msg += "OMITTED BINARY DATA"
-            # end handle encoding
+            # END handle encoding
             msg += "\n---"
         # END diff info
 
@@ -543,7 +543,7 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
             # and then retro-actively assign it to our index.
             if previous_header is not None:
                 index[-1].diff = text[previous_header.end() : _header.start()]
-            # end assign actual diff
+            # END assign actual diff
 
             # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid.
             # We just use the one mode we should have parsed.
@@ -571,10 +571,10 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
 
             previous_header = _header
             header = _header
-        # end for each header we parse
+        # END for each header we parse
         if index and header:
             index[-1].diff = text[header.end() :]
-        # end assign last diff
+        # END assign last diff
 
         return index
 
diff --git a/git/index/base.py b/git/index/base.py
index a43afb320..f7c519df4 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -406,7 +406,7 @@ def raise_exc(e: Exception) -> NoReturn:
                 if S_ISLNK(st.st_mode):
                     yield abs_path.replace(rs, "")
                     continue
-            # end check symlink
+            # END check symlink
 
             # If the path is not already pointing to an existing file, resolve globs if possible.
             if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
@@ -689,7 +689,7 @@ def _entries_for_paths(
                     gitrelative_path = path
                     if self.repo.working_tree_dir:
                         abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
-                # end obtain relative and absolute paths
+                # END obtain relative and absolute paths
 
                 blob = Blob(
                     self.repo,
@@ -867,7 +867,7 @@ def handle_null_entries(self: "IndexFile") -> None:
                         )
                     # END for each entry index
 
-                # end closure
+                # END closure
                 handle_null_entries(self)
             # END null_entry handling
 
diff --git a/git/index/fun.py b/git/index/fun.py
index ace0866f4..a35990d6d 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -123,7 +123,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
             stdout = force_text(stdout, defenc)
             stderr = force_text(stderr, defenc)
             raise HookExecutionError(hp, process.returncode, stderr, stdout)
-    # end handle return code
+    # END handle return code
 
 
 def stat_mode_to_index_mode(mode: int) -> int:
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 29123a44f..d39ca52bb 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -553,7 +553,7 @@ def create_from_tree(
             for p in parent_commits:
                 if not isinstance(p, cls):
                     raise ValueError(f"Parent commit '{p!r}' must be of type {cls}")
-            # end check parent commit types
+            # END check parent commit types
         # END if parent commits are unset
 
         # Retrieve all additional information, create a commit object, and serialize it.
@@ -730,7 +730,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
             next_line = readline()
             while next_line.startswith(b" "):
                 next_line = readline()
-        # end skip mergetags
+        # END skip mergetags
 
         # Now we can have the encoding line, or an empty line followed by the optional message.
         self.encoding = self.default_encoding
@@ -754,7 +754,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                         is_next_header = True
                         break
                     sig += sigbuf[1:]
-                # end read all signature
+                # END read all signature
                 self.gpgsig = sig.rstrip(b"\n").decode(self.encoding, "ignore")
                 if is_next_header:
                     continue
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 24ea5569c..74a6abd54 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -145,7 +145,7 @@ def _set_cache_(self, attr: str) -> None:
                         "This submodule instance does not exist anymore in '%s' file"
                         % osp.join(self.repo.working_tree_dir, ".gitmodules")
                     ) from e
-            # end
+
             self._url = reader.get("url")
             # git-python extension values - optional
             self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
@@ -213,7 +213,7 @@ def _config_parser(
             except ValueError:
                 # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref.
                 pass
-        # end handle parent_commit
+        # END handle parent_commit
         fp_module: Union[str, BytesIO]
         if not repo.bare and parent_matches_head and repo.working_tree_dir:
             fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
@@ -257,7 +257,7 @@ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
             pc: Union["Commit_ish", None] = self.parent_commit
         except ValueError:
             pc = None
-        # end handle empty parent repository
+        # END handle empty parent repository
         parser = self._config_parser(self.repo, pc, read_only)
         parser.set_submodule(self)
         return SectionConstraint(parser, sm_section(self.name))
@@ -269,7 +269,6 @@ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> Path
         if parent_repo.working_tree_dir:
             return osp.join(parent_repo.working_tree_dir, path)
         raise NotADirectoryError()
-        # end
 
     @classmethod
     def _clone_repo(
@@ -300,7 +299,6 @@ def _clone_repo(
             if not osp.isdir(module_abspath_dir):
                 os.makedirs(module_abspath_dir)
             module_checkout_path = osp.join(str(repo.working_tree_dir), path)
-        # end
 
         clone = git.Repo.clone_from(
             url,
@@ -311,7 +309,7 @@ def _clone_repo(
         )
         if cls._need_gitfile_submodules(repo.git):
             cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
-        # end
+
         return clone
 
     @classmethod
@@ -333,8 +331,8 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
             path = path[len(working_tree_linux.rstrip("/")) + 1 :]
             if not path:
                 raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
-            # end verify converted relative path makes sense
-        # end convert to a relative path
+            # END verify converted relative path makes sense
+        # END convert to a relative path
 
         return path
 
@@ -792,15 +790,15 @@ def update(
                             )
                             log.info(msg)
                             may_reset = False
-                        # end handle force
-                    # end handle if we are in the future
+                        # END handle force
+                    # END handle if we are in the future
 
                     if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
                         raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
-                    # end handle force and dirty state
-                # end handle empty repo
+                    # END handle force and dirty state
+                # END handle empty repo
 
-                # end verify future/past
+                # END verify future/past
                 progress.update(
                     BEGIN | UPDWKTREE,
                     0,
@@ -831,7 +829,7 @@ def update(
             if not keep_going:
                 raise
             log.error(str(err))
-        # end handle keep_going
+        # END handle keep_going
 
         # HANDLE RECURSION
         ##################
@@ -926,7 +924,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             if osp.isfile(osp.join(module_checkout_abspath, ".git")):
                 module_abspath = self._module_abspath(self.repo, self.path, self.name)
                 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
-            # end handle git file rewrite
+            # END handle git file rewrite
         # END move physical module
 
         # Rename the index entry - we have to manipulate the index directly as
@@ -959,7 +957,6 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
         # Auto-rename submodule if it's name was 'default', that is, the checkout directory.
         if previous_sm_path == self.name:
             self.rename(module_checkout_path)
-        # end
 
         return self
 
@@ -1007,12 +1004,12 @@ def remove(
             nc += 1
             csm.remove(module, force, configuration, dry_run)
             del csm
-        # end
+
         if configuration and not dry_run and nc > 0:
             # Ensure we don't leave the parent repository in a dirty state, and commit our changes.
             # It's important for recursive, unforced, deletions to work as expected.
             self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
-        # end handle recursion
+        # END handle recursion
 
         # DELETE REPOSITORY WORKING TREE
         ################################
@@ -1088,7 +1085,7 @@ def remove(
             if not dry_run and osp.isdir(git_dir):
                 self._clear_cache()
                 rmtree(git_dir)
-            # end handle separate bare repository
+            # END handle separate bare repository
         # END handle module deletion
 
         # Void our data so as not to delay invalid access.
@@ -1137,7 +1134,7 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
         if commit is None:
             self._parent_commit = None
             return self
-        # end handle None
+        # END handle None
         pcommit = self.repo.commit(commit)
         pctree = pcommit.tree
         if self.k_modules_file not in pctree:
@@ -1162,7 +1159,6 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
             self.binsha = pctree[str(self.path)].binsha
         except KeyError:
             self.binsha = self.NULL_BIN_SHA
-        # end
 
         self._clear_cache()
         return self
@@ -1236,11 +1232,11 @@ def rename(self, new_name: str) -> "Submodule":
                 tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
                 os.renames(source_dir, tmp_dir)
                 source_dir = tmp_dir
-            # end handle self-containment
+            # END handle self-containment
             os.renames(source_dir, destination_module_abspath)
             if mod.working_tree_dir:
                 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
-        # end move separate git repository
+        # END move separate git repository
 
         return self
 
@@ -1439,7 +1435,7 @@ def iter_items(
             sm._name = n
             if pc != repo.commit():
                 sm._parent_commit = pc
-            # end set only if not most recent!
+            # END set only if not most recent!
             sm._branch_path = git.Head.to_full_path(b)
             sm._url = u
 
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index bc61f4ac2..0ac8a22db 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -356,7 +356,7 @@ def update(
                             # to be sure...
                             for remote in smmr:
                                 remote.fetch(progress=progress)
-                            # end for each remote
+                            # END for each remote
 
                             try:
                                 tbr = git.Head.create(
@@ -391,7 +391,7 @@ def update(
             if not keep_going:
                 raise
             log.error(str(err))
-        # end handle keep_going
+        # END handle keep_going
 
         # FINALLY UPDATE ALL ACTUAL SUBMODULES
         ######################################
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 3587a52d3..7866772e3 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -523,7 +523,7 @@ def log_append(
             committer_or_reader: Union["Actor", "GitConfigParser"] = self.commit.committer
         except ValueError:
             committer_or_reader = self.repo.config_reader()
-        # end handle newly cloned repositories
+        # END handle newly cloned repositories
         if newbinsha is None:
             newbinsha = self.commit.binsha
 
diff --git a/git/remote.py b/git/remote.py
index 76352087a..08101bb6c 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -622,7 +622,6 @@ def exists(self) -> bool:
             return True
         except cp.NoSectionError:
             return False
-        # end
 
     @classmethod
     def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]:
@@ -757,7 +756,7 @@ def stale_refs(self) -> IterableList[Reference]:
             else:
                 fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name)
                 out_refs.append(RemoteReference(self.repo, fqhn))
-            # end special case handling
+            # END special case handling
         # END for each line
         return out_refs
 
@@ -887,8 +886,8 @@ def _get_fetch_info_from_stderr(
                 fetch_head_info = fetch_head_info[:l_fil]
             else:
                 fetch_info_lines = fetch_info_lines[:l_fhi]
-            # end truncate correct list
-        # end sanity check + sanitization
+            # END truncate correct list
+        # END sanity check + sanitization
 
         for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info):
             try:
diff --git a/git/repo/base.py b/git/repo/base.py
index 5d0a06198..51c83d720 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -728,7 +728,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
         """
         if len(rev) < 2:
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
-        # end handle input
+        # END handle input
 
         res: List[Union[Commit_ish, None]] = []
         try:
@@ -736,15 +736,15 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
         except GitCommandError as err:
             if err.status == 128:
                 raise
-            # end handle invalid rev
+            # END handle invalid rev
             # Status code 1 is returned if there is no merge-base
             # (see https://github.com/git/git/blob/master/builtin/merge-base.c#L16)
             return res
-        # end exception handling
+        # END exception handling
 
         for line in lines:
             res.append(self.commit(line))
-        # end for each merge-base
+        # END for each merge-base
 
         return res
 
@@ -1098,7 +1098,7 @@ class InfoTD(TypedDict, total=False):
                 parts = self.re_whitespace.split(line_str, 1)
                 firstpart = parts[0]
                 is_binary = False
-            # end handle decode of line
+            # END handle decode of line
 
             if self.re_hexsha_only.search(firstpart):
                 # handles
@@ -1448,7 +1448,7 @@ def archive(
         path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
         if not isinstance(path, (tuple, list)):
             path = [path]
-        # end ensure paths is list (or tuple)
+        # END ensure paths is list (or tuple)
         self.git.archive("--", treeish, *path, **kwargs)
         return self
 
diff --git a/git/repo/fun.py b/git/repo/fun.py
index f0bb4cd1f..29a899ea8 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -118,7 +118,7 @@ def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
             if not osp.isabs(path):
                 path = osp.normpath(osp.join(osp.dirname(d), path))
             return find_submodule_git_dir(path)
-    # end handle exception
+    # END handle exception
     return None
 
 
diff --git a/git/util.py b/git/util.py
index adbc8146f..78d6169ac 100644
--- a/git/util.py
+++ b/git/util.py
@@ -668,7 +668,8 @@ def new_message_handler(self) -> Callable[[str], None]:
         def handler(line: AnyStr) -> None:
             return self._parse_progress_line(line.rstrip())
 
-        # end
+        # END def handler
+
         return handler
 
     def line_dropped(self, line: str) -> None:

From c2eb6b544f1f65deb493e49d9a0cabdf8a714966 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 02:02:38 -0400
Subject: [PATCH 235/994] Add missing comment revisions in
 git/objects/submodule/base.py

---
 git/objects/submodule/base.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 74a6abd54..41abe9223 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -136,7 +136,7 @@ def __init__(
     def _set_cache_(self, attr: str) -> None:
         if attr in ("path", "_url", "_branch_path"):
             reader: SectionConstraint = self.config_reader()
-            # default submodule values
+            # Default submodule values.
             try:
                 self.path = reader.get("path")
             except cp.NoSectionError as e:
@@ -147,7 +147,7 @@ def _set_cache_(self, attr: str) -> None:
                     ) from e
 
             self._url = reader.get("url")
-            # git-python extension values - optional
+            # GitPython extension values - optional.
             self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
         elif attr == "_name":
             raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
@@ -484,7 +484,7 @@ def add(
             # END verify we have url
             url = urls[0]
         else:
-            # clone new repo
+            # Clone new repo.
             kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout}
             if not branch_is_default:
                 kwargs["b"] = br.name
@@ -531,7 +531,7 @@ def add(
 
             sm._url = url
             if not branch_is_default:
-                # store full path
+                # Store full path.
                 writer.set_value(cls.k_head_option, br.path)
                 sm._branch_path = br.path
 

From ddb44178788f63e207641ab579bf7031b0f5c8dc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 02:07:05 -0400
Subject: [PATCH 236/994] Improve consistency of "END" comments in test suite

As in the recent roughly corresponding change inside git/, this
does not add new END comments (and actually removes some), but
instead focuses on making the existing ones more consistent in
style, mainly adjusting capitalization (turning "end" to "END").
---
 test/performance/lib.py |  2 +-
 test/test_base.py       |  2 +-
 test/test_config.py     |  6 +-----
 test/test_diff.py       |  2 +-
 test/test_docs.py       |  2 --
 test/test_index.py      |  2 +-
 test/test_remote.py     |  3 +--
 test/test_repo.py       |  8 ++++----
 test/test_submodule.py  | 15 +++++++--------
 9 files changed, 17 insertions(+), 25 deletions(-)

diff --git a/test/performance/lib.py b/test/performance/lib.py
index 8c68db782..8e76fe815 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -45,7 +45,7 @@ def setUp(self):
                 k_env_git_repo,
             )
             repo_path = osp.dirname(__file__)
-        # end set some repo path
+        # END set some repo path
         self.gitrorepo = Repo(repo_path, odbt=GitCmdObjectDB, search_parent_directories=True)
         self.puregitrorepo = Repo(repo_path, odbt=GitDB, search_parent_directories=True)
 
diff --git a/test/test_base.py b/test/test_base.py
index 94a268ecd..051b96354 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -142,5 +142,5 @@ def test_add_unicode(self, rw_repo):
         else:
             # On POSIX, we can just add Unicode files without problems.
             rw_repo.git.add(rw_repo.working_dir)
-        # end
+
         rw_repo.index.commit("message")
diff --git a/test/test_config.py b/test/test_config.py
index 2e278bec2..63fbc61e6 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -190,13 +190,9 @@ def test_config_include(self, rw_dir):
         def write_test_value(cw, value):
             cw.set_value(value, "value", value)
 
-        # end
-
         def check_test_value(cr, value):
             assert cr.get_value(value, "value") == value
 
-        # end
-
         # PREPARE CONFIG FILE A
         fpa = osp.join(rw_dir, "a")
         with GitConfigParser(fpa, read_only=False) as cw:
@@ -225,7 +221,7 @@ def check_test_value(cr, value):
         with GitConfigParser(fpa, read_only=True) as cr:
             for tv in ("a", "b", "c"):
                 check_test_value(cr, tv)
-            # end for each test to verify
+            # END for each test to verify
             assert len(cr.items("include")) == 8, "Expected all include sections to be merged"
 
         # Test writable config writers - assure write-back doesn't involve includes.
diff --git a/test/test_diff.py b/test/test_diff.py
index 1d9c398c2..a46555ce7 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -223,7 +223,7 @@ def test_diff_index(self):
         for dr in res:
             self.assertTrue(dr.diff.startswith(b"@@"), dr)
             self.assertIsNotNone(str(dr), "Diff to string conversion should be possible")
-        # end for each diff
+        # END for each diff
 
         dr = res[3]
         assert dr.diff.endswith(b"+Binary files a/rps and b/rps differ\n")
diff --git a/test/test_docs.py b/test/test_docs.py
index 1ca07f886..206560edd 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -146,8 +146,6 @@ def update(self, op_code, cur_count, max_count=None, message=""):
                     message or "NO MESSAGE",
                 )
 
-        # end
-
         self.assertEqual(len(cloned_repo.remotes), 1)  # We have been cloned, so should be one remote.
         self.assertEqual(len(bare_repo.remotes), 0)  # This one was just initialized.
         origin = bare_repo.create_remote("origin", url=cloned_repo.working_tree_dir)
diff --git a/test/test_index.py b/test/test_index.py
index 3c88ce8e1..95da472e7 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -635,7 +635,7 @@ def mixed_iterator():
                 )
 
                 os.remove(link_file)
-            # end for each target
+            # END for each target
         # END real symlink test
 
         # Add fake symlink and assure it checks-our as symlink.
diff --git a/test/test_remote.py b/test/test_remote.py
index b102730ac..3e7a025c5 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -518,8 +518,7 @@ def test_base(self, rw_repo, remote_repo):
             if branch.name != "master":
                 branch.delete(remote_repo, branch, force=True)
                 num_deleted += 1
-            # end
-        # end for each branch
+        # END for each branch
         self.assertGreater(num_deleted, 0)
         self.assertEqual(
             len(rw_repo.remotes.origin.fetch(prune=True)),
diff --git a/test/test_repo.py b/test/test_repo.py
index c7c13dc7f..a9d52ced0 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -790,7 +790,7 @@ def test_untracked_files(self, rwrepo):
 
             repo_add(untracked_files)
             self.assertEqual(len(rwrepo.untracked_files), (num_recently_untracked - len(files)))
-        # end for each run
+        # END for each run
 
     def test_config_reader(self):
         reader = self.rorepo.config_reader()  # All config files.
@@ -1165,8 +1165,8 @@ def last_commit(repo, rev, path):
             for repo_type in (GitCmdObjectDB, GitDB):
                 repo = Repo(self.rorepo.working_tree_dir, odbt=repo_type)
                 last_commit(repo, "master", "test/test_base.py")
-            # end for each repository type
-        # end for each iteration
+            # END for each repository type
+        # END for each iteration
 
     def test_remote_method(self):
         self.assertRaises(ValueError, self.rorepo.remote, "foo-blue")
@@ -1227,7 +1227,7 @@ def test_merge_base(self):
             res = repo.merge_base(c1, c2, c3, **{kw: True})
             self.assertIsInstance(res, list)
             self.assertEqual(len(res), 1)
-        # end for each keyword signalling all merge-bases to be returned
+        # END for each keyword signalling all merge-bases to be returned
 
         # Test for no merge base - can't do as we have.
         self.assertRaises(GitCommandError, repo.merge_base, c1, "ffffff")
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 3eec41ee1..da918b4d0 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -704,7 +704,7 @@ def test_first_submodule(self, rwrepo):
             sm = rwrepo.create_submodule(sm_name, sm_path, rwrepo.git_dir, no_checkout=True)
             assert sm.exists() and sm.module_exists()
             rwrepo.index.commit("Added submodule " + sm_name)
-        # end for each submodule path to add
+        # END for each submodule path to add
 
         self.assertRaises(ValueError, rwrepo.create_submodule, "fail", osp.expanduser("~"))
         self.assertRaises(
@@ -731,7 +731,7 @@ def test_add_empty_repo(self, rwdir):
                 url=empty_repo_dir,
                 no_checkout=checkout_mode and True or False,
             )
-        # end for each checkout mode
+        # END for each checkout mode
 
     @with_rw_directory
     @_patch_git_config("protocol.file.allow", "always")
@@ -783,8 +783,8 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):
             for init in (False, True):
                 sm.update(init=init)
                 sm2.update(init=init)
-            # end for each init state
-        # end for each iteration
+            # END for each init state
+        # END for each iteration
 
         sm.move(sm.path + "_moved")
         sm2.move(sm2.path + "_moved")
@@ -839,7 +839,7 @@ def assert_exists(sm, value=True):
             assert sm.exists() == value
             assert sm.module_exists() == value
 
-        # end
+        # END def assert_exists
 
         # As git is backwards compatible itself, it would still recognize what we do here... unless we really
         # muss it up. That's the only reason why the test is still here...
@@ -854,7 +854,7 @@ def assert_exists(sm, value=True):
             assert osp.isfile(module_repo_path)
             assert sm.module().has_separate_working_tree()
             assert find_submodule_git_dir(module_repo_path) is not None, "module pointed to by .git file must be valid"
-        # end verify submodule 'style'
+        # END verify submodule 'style'
 
         # Test move.
         new_sm_path = join_path_native("submodules", "one")
@@ -911,7 +911,7 @@ def assert_exists(sm, value=True):
             sm.remove(dry_run=dry_run, force=True)
             assert_exists(sm, value=dry_run)
             assert osp.isdir(sm_module_path) == dry_run
-        # end for each dry-run mode
+        # END for each dry-run mode
 
     @with_rw_directory
     def test_ignore_non_submodule_file(self, rwdir):
@@ -974,7 +974,6 @@ def test_rename(self, rwdir):
         sm_mod = sm.module()
         if osp.isfile(osp.join(sm_mod.working_tree_dir, ".git")) == sm._need_gitfile_submodules(parent.git):
             assert sm_mod.git_dir.endswith(join_path_native(".git", "modules", new_sm_name))
-        # end
 
     @with_rw_directory
     def test_branch_renames(self, rw_dir):

From 1d3f275841a96ea4ba40b3813114258cc886b9fc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 05:27:11 -0400
Subject: [PATCH 237/994] Avoid making "END" notation more verbose

This changes the style "END" comments appearing after local
function definitions from "END def X" to "END X".

Note that this still does not add such comments where they were not
present, it only adjusts their style where they already existed.
---
 git/cmd.py             | 4 ++--
 git/config.py          | 2 +-
 git/index/base.py      | 1 +
 git/util.py            | 2 +-
 test/test_submodule.py | 2 +-
 5 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index f76c19a66..5359924f0 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1048,7 +1048,7 @@ def kill_process(pid: int) -> None:
                 pass
             return
 
-        # END def kill_process
+        # END kill_process
 
         if kill_after_timeout is not None:
             kill_check = threading.Event()
@@ -1100,7 +1100,7 @@ def kill_process(pid: int) -> None:
             def as_text(stdout_value: Union[bytes, str]) -> str:
                 return not output_stream and safe_decode(stdout_value) or "<OUTPUT_STREAM>"
 
-            # END def as_text
+            # END as_text
 
             if stderr_value:
                 log.info(
diff --git a/git/config.py b/git/config.py
index 020d823d0..8f68c4e66 100644
--- a/git/config.py
+++ b/git/config.py
@@ -450,7 +450,7 @@ def string_decode(v: str) -> str:
 
             return v.encode(defenc).decode("unicode_escape")
 
-        # END def string_decode
+        # END string_decode
 
         while True:
             # We assume to read binary!
diff --git a/git/index/base.py b/git/index/base.py
index f7c519df4..be6137a4f 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -868,6 +868,7 @@ def handle_null_entries(self: "IndexFile") -> None:
                     # END for each entry index
 
                 # END closure
+
                 handle_null_entries(self)
             # END null_entry handling
 
diff --git a/git/util.py b/git/util.py
index 78d6169ac..2c3d6f33f 100644
--- a/git/util.py
+++ b/git/util.py
@@ -668,7 +668,7 @@ def new_message_handler(self) -> Callable[[str], None]:
         def handler(line: AnyStr) -> None:
             return self._parse_progress_line(line.rstrip())
 
-        # END def handler
+        # END handler
 
         return handler
 
diff --git a/test/test_submodule.py b/test/test_submodule.py
index da918b4d0..6b331b3bc 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -839,7 +839,7 @@ def assert_exists(sm, value=True):
             assert sm.exists() == value
             assert sm.module_exists() == value
 
-        # END def assert_exists
+        # END assert_exists
 
         # As git is backwards compatible itself, it would still recognize what we do here... unless we really
         # muss it up. That's the only reason why the test is still here...

From 81dad7e62230501444620f0a5fd479a4cd9eda19 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 05:33:58 -0400
Subject: [PATCH 238/994] Add missing comment revisions in git/index/base.py

---
 git/index/base.py | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index be6137a4f..a101fd371 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1023,8 +1023,8 @@ def move(
         was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
         kwargs["dry_run"] = True
 
-        # First execute rename in dryrun so the command tells us what it actually does.
-        # (for later output)
+        # First execute rename in dryrun so the command tells us what it actually does
+        # (for later output).
         out = []
         mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
 
@@ -1186,7 +1186,7 @@ def checkout(
         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
+                return None  # Return early if stderr empty.
             else:
                 stderr_bytes = stderr_IO.read()
             # line contents:
@@ -1252,9 +1252,9 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
             if isinstance(paths, str):
                 paths = [paths]
 
-            # make sure we have our entries loaded before we start checkout_index
+            # Make sure we have our entries loaded before we start checkout_index,
             # which will hold a lock on it. We try to get the lock as well during
-            # our entries initialization
+            # our entries initialization.
             self.entries
 
             args.append("--stdin")
@@ -1267,7 +1267,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
 
             for path in paths:
                 co_path = to_native_path_linux(self._to_relative_path(path))
-                # if the item is not in the index, it could be a directory
+                # If the item is not in the index, it could be a directory.
                 path_is_directory = False
 
                 try:
@@ -1347,8 +1347,8 @@ def reset(
             If you want git-reset like behaviour, use *HEAD.reset* instead.
 
         :return: self"""
-        # what we actually want to do is to merge the tree into our existing
-        # index, which is what git-read-tree does
+        # What we actually want to do is to merge the tree into our existing
+        # index, which is what git-read-tree does.
         new_inst = type(self).from_tree(self.repo, commit)
         if not paths:
             self.entries = new_inst.entries
@@ -1360,7 +1360,7 @@ def reset(
                     key = entry_key(path, 0)
                     self.entries[key] = nie[key]
                 except KeyError:
-                    # if key is not in theirs, it musn't be in ours
+                    # If key is not in theirs, it musn't be in ours.
                     try:
                         del self.entries[key]
                     except KeyError:
@@ -1391,7 +1391,7 @@ def diff(
     ) -> git_diff.DiffIndex:
         """Diff this index against the working copy or a Tree or Commit object.
 
-        For a documentation of the parameters and return values, see
+        For documentation of the parameters and return values, see
         :meth:`Diffable.diff <git.diff.Diffable.diff>`.
 
         :note:
@@ -1413,7 +1413,7 @@ def diff(
             other = self.repo.rev_parse(other)
         # END object conversion
 
-        if isinstance(other, Object):  # for Tree or Commit
+        if isinstance(other, Object):  # For Tree or Commit.
             # Invert the existing R flag.
             cur_val = kwargs.get("R", False)
             kwargs["R"] = not cur_val

From a040edbf09a5fd2d2355c7042d313edcad74eb40 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 07:40:01 -0400
Subject: [PATCH 239/994] Shorten some docstring references with tilde notation

This uses the Sphinx ~ notation to express references like:

    :class:`Z <a.b.c.Z>`

In the abbreviated form, which Sphinx renders the same way:

    :class:`~a.b.c.Z`

(This commit also makes some other polishing changes to affected
and nearby text.)
---
 git/diff.py                   | 20 +++++++++-----------
 git/objects/base.py           |  2 +-
 git/objects/submodule/base.py | 18 ++++++++++--------
 git/refs/head.py              |  4 ++--
 git/refs/reference.py         |  4 ++--
 git/refs/symbolic.py          | 32 +++++++++++++++++---------------
 git/refs/tag.py               |  4 ++--
 git/remote.py                 | 20 ++++++++++----------
 git/repo/base.py              | 35 +++++++++++++++++------------------
 9 files changed, 70 insertions(+), 69 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index acf32af7d..275534bbf 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -118,12 +118,12 @@ def diff(
             This the item to compare us with.
 
             * If None, we will be compared to the working tree.
-            * If :class:`Treeish <git.index.base.Treeish>`, it will be compared against
-              the respective tree.
-            * If :class:`Index <Diffable.Index>`, it will be compared against the index.
+            * If :class:`~git.index.base.Treeish`, it will be compared against the
+              respective tree.
+            * If :class:`~Diffable.Index`, it will be compared against the index.
             * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
-            * It defaults to :class:`Index <Diffable.Index>` so that the method will not
-              by default fail on bare repositories.
+            * It defaults to :class:`~Diffable.Index` so that the method will not by
+              default fail on bare repositories.
 
         :param paths:
             This a list of paths or a single path to limit the diff to. It will only
@@ -141,11 +141,9 @@ def diff(
         :return: git.DiffIndex
 
         :note:
-            On a bare repository, 'other' needs to be provided
-            as :class:`Index <Diffable.Index>`,
-            or as :class:`Tree <git.objects.tree.Tree>`
-            or :class:`Commit <git.objects.commit.Commit>`, or a git command error will
-            occur.
+            On a bare repository, 'other' needs to be provided as
+            :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or
+            :class:`~git.objects.commit.Commit`, or a git command error will occur.
         """
         args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = []
         args.append("--abbrev=40")  # We need full shas.
@@ -222,7 +220,7 @@ class DiffIndex(List[T_Diff]):
     def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
         """
         :return:
-            iterator yielding :class:`Diff` instances that match the given `change_type`
+            Iterator yielding :class:`Diff` instances that match the given `change_type`
 
         :param change_type:
             Member of :attr:`DiffIndex.change_type`, namely:
diff --git a/git/objects/base.py b/git/objects/base.py
index 0d88aa185..a771f9fbf 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -178,7 +178,7 @@ def __init__(
     ) -> None:
         """Initialize a newly instanced IndexObject.
 
-        :param repo: The :class:`Repo <git.repo.base.Repo>` we are located in.
+        :param repo: The :class:`~git.repo.base.Repo` we are located in.
         :param binsha: 20 byte sha1.
         :param mode:
             The stat compatible file mode as int, use the :mod:`stat` module to evaluate
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 41abe9223..018f9a39c 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -113,14 +113,16 @@ def __init__(
     ) -> None:
         """Initialize this instance with its attributes.
 
-        We only document the parameters that differ
-        from :class:`IndexObject <git.objects.base.IndexObject>`.
-
-        :param repo: Our parent repository
-        :param binsha: binary sha referring to a commit in the remote repository, see url parameter
-        :param parent_commit: see set_parent_commit()
-        :param url: The url to the remote repository which is the submodule
-        :param branch_path: full (relative) path to ref to checkout when cloning the remote repository
+        We only document the parameters that differ from
+        :class:`~git.objects.base.IndexObject`.
+
+        :param repo: Our parent repository.
+        :param binsha: Binary sha referring to a commit in the remote repository. See
+            the ``url`` parameter.
+        :param parent_commit: See :meth:`set_parent_commit`.
+        :param url: The URL to the remote repository which is the submodule.
+        :param branch_path: Full (relative) path to ref to checkout when cloning the
+            remote repository.
         """
         super(Submodule, self).__init__(repo, binsha, mode, path)
         self.size = 0
diff --git a/git/refs/head.py b/git/refs/head.py
index 194f51e78..30ff336d5 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -231,8 +231,8 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
 
         :param force:
             If True, changes to the index and the working tree will be discarded.
-            If False, :class:`GitCommandError <git.exc.GitCommandError>` will be
-            raised in that situation.
+            If False, :class:`~git.exc.GitCommandError` will be raised in that
+            situation.
 
         :param kwargs:
             Additional keyword arguments to be passed to git checkout, e.g.
diff --git a/git/refs/reference.py b/git/refs/reference.py
index ca265c0e4..8a0262ea4 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -40,8 +40,8 @@ def wrapper(self: T_References, *args: Any) -> _T:
 class Reference(SymbolicReference, LazyMixin, IterableObj):
     """A named reference to any object.
 
-    Subclasses may apply restrictions though, e.g., a :class:`Head <git.refs.head.Head>`
-    can only point to commits.
+    Subclasses may apply restrictions though, e.g., a :class:`~git.refs.head.Head` can
+    only point to commits.
     """
 
     __slots__ = ()
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 7866772e3..6e4b657d6 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -59,7 +59,7 @@ class SymbolicReference:
     """Special case of a reference that is symbolic.
 
     This does not point to a specific commit, but to another
-    :class:`Head <git.refs.head.Head>`, which itself specifies a commit.
+    :class:`~git.refs.head.Head`, which itself specifies a commit.
 
     A typical example for a symbolic reference is ``HEAD``.
     """
@@ -345,7 +345,7 @@ def set_object(
         If the reference does not exist, it will be created.
 
         :param object: A refspec, a :class:`SymbolicReference` or an
-            :class:`Object <git.objects.base.Object>` instance.
+            :class:`~git.objects.base.Object` instance.
             :class:`SymbolicReference` instances will be dereferenced beforehand to
             obtain the object they point to.
         :param logmsg: If not None, the message will be used in the reflog entry to be
@@ -396,10 +396,10 @@ def set_reference(
         symbolic one.
 
         :param ref:
-            A :class:`SymbolicReference` instance,
-            an :class:`Object <git.objects.base.Object>` instance, or a refspec string.
-            Only if the ref is a :class:`SymbolicReference` instance, we will point to
-            it. Everything else is dereferenced to obtain the actual object.
+            A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object`
+            instance, or a refspec string. Only if the ref is a
+            :class:`SymbolicReference` instance, we will point to it. Everything else is
+            dereferenced to obtain the actual object.
 
         :param logmsg: If set to a string, the message will be used in the reflog.
             Otherwise, a reflog entry is not written for the changed reference.
@@ -514,7 +514,7 @@ def log_append(
         :param message: A message describing the change.
         :param newbinsha: The sha the ref points to now. If None, our current commit sha
             will be used.
-        :return: The added :class:`RefLogEntry <git.refs.log.RefLogEntry>` instance.
+        :return: The added :class:`~git.refs.log.RefLogEntry` instance.
         """
         # NOTE: We use the committer of the currently active commit - this should be
         # correct to allow overriding the committer on a per-commit level.
@@ -540,7 +540,7 @@ def log_entry(self, index: int) -> "RefLogEntry":
 
         .. note:: This method must read part of the reflog during execution, hence
             it should be used sparingly, or only if you need just one index.
-            In that case, it will be faster than the ``log()`` method.
+            In that case, it will be faster than the :meth:`log` method.
         """
         return RefLog.entry_at(RefLog.path(self), index)
 
@@ -816,8 +816,8 @@ def iter_items(
             which is not detached and pointing to a valid ref.
 
             The list is lexicographically sorted. The returned objects are instances of
-            concrete subclasses, such as :class:`Head <git.refs.head.Head>` or
-            :class:`TagReference <git.refs.tag.TagReference>`.
+            concrete subclasses, such as :class:`~git.refs.head.Head` or
+            :class:`~git.refs.tag.TagReference`.
         """
         return (r for r in cls._iter_items(repo, common_path) if r.__class__ is SymbolicReference or not r.is_detached)
 
@@ -826,14 +826,16 @@ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_Refere
         """
         Make a symbolic reference from a path.
 
-        :param path: Full ``.git``-directory-relative path name to the Reference to instantiate.
+        :param path: Full ``.git``-directory-relative path name to the Reference to
+            instantiate.
 
-        :note: Use :meth:`to_full_path` if you only have a partial path of a known Reference type.
+        :note: Use :meth:`to_full_path` if you only have a partial path of a known
+            Reference type.
 
         :return:
-            Instance of type :class:`Reference <git.refs.reference.Reference>`,
-            :class:`Head <git.refs.head.Head>`, or :class:`Tag <git.refs.tag.Tag>`,
-            depending on the given path.
+            Instance of type :class:`~git.refs.reference.Reference`,
+            :class:`~git.refs.head.Head`, or :class:`~git.refs.tag.Tag`, depending on
+            the given path.
         """
         if not path:
             raise ValueError("Cannot create Reference from %r" % path)
diff --git a/git/refs/tag.py b/git/refs/tag.py
index 3c269d9ba..d00adc121 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -89,8 +89,8 @@ def create(
             The prefix ``refs/tags`` is implied.
 
         :param ref:
-            A reference to the :class:`Object <git.objects.base.Object>` you want to
-            tag. The Object can be a commit, tree or blob.
+            A reference to the :class:`~git.objects.base.Object` you want to tag.
+            The Object can be a commit, tree or blob.
 
         :param logmsg:
             If not None, the message will be used in your tag object. This will also
diff --git a/git/remote.py b/git/remote.py
index 08101bb6c..880e87232 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -210,9 +210,9 @@ def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]:
     def remote_ref(self) -> Union[RemoteReference, TagReference]:
         """
         :return:
-            Remote :class:`Reference <git.refs.reference.Reference>` or
-            :class:`TagReference <git.refs.tag.TagReference>` in the local repository
-            corresponding to the :attr:`remote_ref_string` kept in this instance.
+            Remote :class:`~git.refs.reference.Reference` or
+            :class:`~git.refs.tag.TagReference` in the local repository corresponding to
+            the :attr:`remote_ref_string` kept in this instance.
         """
         # Translate heads to a local remote. Tags stay as they are.
         if self.remote_ref_string.startswith("refs/tags"):
@@ -1093,7 +1093,7 @@ def push(
               `Click here <http://goo.gl/NPa7st>`__ for a description of all arguments
               given to the function.
             * An instance of a class derived from :class:`git.RemoteProgress` that
-              overrides the :meth:`update <git.RemoteProgress.update>` method.
+              overrides the :meth:`~git.RemoteProgress.update` method.
 
         :note: No further progress information is returned after push returns.
 
@@ -1109,14 +1109,14 @@ def push(
         :param kwargs: Additional arguments to be passed to git-push.
 
         :return:
-            A :class:`PushInfoList` object, where each list member
-            represents an individual head which had been updated on the remote side.
+            A :class:`PushInfoList` object, where each list member represents an
+            individual head which had been updated on the remote side.
             If the push contains rejected heads, these will have the
             :attr:`PushInfo.ERROR` bit set in their flags.
-            If the operation fails completely, the length of the returned PushInfoList will
-            be 0.
-            Call :meth:`raise_if_error <PushInfoList.raise_if_error>` on the returned
-            object to raise on any failure.
+            If the operation fails completely, the length of the returned PushInfoList
+            will be 0.
+            Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on
+            any failure.
         """
         kwargs = add_progress(kwargs, self.repo.git, progress)
 
diff --git a/git/repo/base.py b/git/repo/base.py
index 51c83d720..411a321ee 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -387,7 +387,7 @@ def heads(self) -> "IterableList[Head]":
     def references(self) -> "IterableList[Reference]":
         """A list of Reference objects representing tags, heads and remote references.
 
-        :return: IterableList(Reference, ...)
+        :return: ``git.IterableList(Reference, ...)``
         """
         return Reference.list_items(self)
 
@@ -400,11 +400,11 @@ def references(self) -> "IterableList[Reference]":
     @property
     def index(self) -> "IndexFile":
         """
-        :return: :class:`IndexFile <git.index.base.IndexFile>` representing this
-            repository's index.
+        :return: A :class:`~git.index.base.IndexFile` representing this repository's
+            index.
 
         :note: This property can be expensive, as the returned
-            :class:`IndexFile <git.index.base.IndexFile>` will be reinitialized.
+            :class:`~git.index.base.IndexFile` will be reinitialized.
             It is recommended to reuse the object.
         """
         return IndexFile(self)
@@ -520,7 +520,7 @@ def create_head(
         :note: For more documentation, please see the
             :meth:`Head.create <git.refs.head.Head.create>` method.
 
-        :return: Newly created :class:`Head <git.refs.head.Head>` Reference
+        :return: Newly created :class:`~git.refs.head.Head` Reference
         """
         return Head.create(self, path, commit, logmsg, force)
 
@@ -544,7 +544,7 @@ def create_tag(
         :note: For more documentation, please see the
             :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
 
-        :return: :class:`TagReference <git.refs.tag.TagReference>` object
+        :return: :class:`~git.refs.tag.TagReference` object
         """
         return TagReference.create(self, path, ref, message, force, **kwargs)
 
@@ -558,7 +558,7 @@ def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
         For more information, please see the documentation of the
         :meth:`Remote.create <git.remote.Remote.create>` method.
 
-        :return: :class:`Remote <git.remote.Remote>` reference
+        :return: :class:`~git.remote.Remote` reference
         """
         return Remote.create(self, name, url, **kwargs)
 
@@ -599,8 +599,8 @@ def config_reader(
     ) -> GitConfigParser:
         """
         :return:
-            :class:`GitConfigParser <git.config.GitConfigParser>` allowing to read the
-            full git configuration, but not to write it.
+            :class:`~git.config.GitConfigParser` allowing to read the full git
+            configuration, but not to write it.
 
             The configuration will include values from the system, user and repository
             configuration files.
@@ -633,11 +633,10 @@ def _config_reader(
     def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
         """
         :return:
-            A :class:`GitConfigParser <git.config.GitConfigParser>` allowing to write
-            values of the specified configuration file level. Config writers should be
-            retrieved, used to change the configuration, and written right away as they
-            will lock the configuration file in question and prevent other's to write
-            it.
+            A :class:`~git.config.GitConfigParser` allowing to write values of the
+            specified configuration file level. Config writers should be retrieved, used
+            to change the configuration, and written right away as they will lock the
+            configuration file in question and prevent other's to write it.
 
         :param config_level:
             One of the following values:
@@ -720,10 +719,10 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
         :param rev: At least two revs to find the common ancestor for.
         :param kwargs: Additional arguments to be passed to the
             ``repo.git.merge_base()`` command which does all the work.
-        :return: A list of :class:`Commit <git.objects.commit.Commit>` objects. If
-            ``--all`` was not passed as a keyword argument, the list will have at max
-            one :class:`Commit <git.objects.commit.Commit>`, or is empty if no common
-            merge base exists.
+        :return: A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
+            not passed as a keyword argument, the list will have at max one
+            :class:`~git.objects.commit.Commit`, or is empty if no common merge base
+            exists.
         :raises ValueError: If not at least two revs are provided.
         """
         if len(rev) < 2:

From af1b5d4a2e7dac065c19129b448bf76866a4afa2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 13:39:56 -0400
Subject: [PATCH 240/994] Fix commented *.py names at the top of modules

A number of them were incorrect, due to how files were renamed, and
code moved between them, over time. Some were corrected earlier,
and most of the rest are corrected here.

Not all modules have these at all. This does not add any to modules
that don't have them. It only fixes them where present but wrong.
---
 git/index/base.py  | 2 +-
 git/objects/tag.py | 2 +-
 git/repo/base.py   | 2 +-
 git/util.py        | 2 +-
 test/test_db.py    | 2 +-
 test/test_docs.py  | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index a101fd371..b87fefd56 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1,4 +1,4 @@
-# index.py
+# base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 55f7e19da..a7ff7f263 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -1,4 +1,4 @@
-# objects.py
+# tag.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
diff --git a/git/repo/base.py b/git/repo/base.py
index 411a321ee..4790ea4e7 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1,4 +1,4 @@
-# repo.py
+# base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
diff --git a/git/util.py b/git/util.py
index 2c3d6f33f..73b3a2edd 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1,4 +1,4 @@
-# utils.py
+# util.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
diff --git a/test/test_db.py b/test/test_db.py
index acf8379db..d59aa6cc0 100644
--- a/test/test_db.py
+++ b/test/test_db.py
@@ -1,4 +1,4 @@
-# test_repo.py
+# test_db.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
diff --git a/test/test_docs.py b/test/test_docs.py
index 206560edd..73f85d540 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# test_git.py
+# test_docs.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under

From b970d425754df37e9f70d55544c8fdf2e6eb7b29 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 14:29:20 -0400
Subject: [PATCH 241/994] Remove encoding declarations

In Python 3, UTF-8 is the default encoding when no encoding
declaration is present, and omitting the encoding declaration when
UTF-8 is intended is officially suggested.

- https://docs.python.org/3/reference/lexical_analysis.html
- https://pep8.org/#source-file-encoding

Furthermore, not all files in this project had them (and their
presence or absence seems to have been mainly for historical
reasons rather than file contents).
---
 doc/source/conf.py     | 4 +---
 git/compat.py          | 1 -
 git/types.py           | 1 -
 test/test_base.py      | 1 -
 test/test_clone.py     | 1 -
 test/test_commit.py    | 1 -
 test/test_diff.py      | 1 -
 test/test_docs.py      | 1 -
 test/test_exc.py       | 1 -
 test/test_git.py       | 1 -
 test/test_index.py     | 1 -
 test/test_repo.py      | 1 -
 test/test_submodule.py | 1 -
 13 files changed, 1 insertion(+), 15 deletions(-)

diff --git a/doc/source/conf.py b/doc/source/conf.py
index d19b0c0ec..9c22ca06a 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,6 +1,4 @@
-# -*- coding: utf-8 -*-
-#
-# GitPython documentation build configuration file, created by
+# GitPython documentation build configuration file, originally created by
 # sphinx-quickstart on Sat Jan 24 11:51:01 2009.
 #
 # This file is execfile()d with the current directory set to its containing dir.
diff --git a/git/compat.py b/git/compat.py
index 93e06d2ec..f17e52f7b 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # compat.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/git/types.py b/git/types.py
index 22bb91c16..2709bbf34 100644
--- a/git/types.py
+++ b/git/types.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
diff --git a/test/test_base.py b/test/test_base.py
index 051b96354..e4704c7d8 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_clone.py b/test/test_clone.py
index f66130cf0..7624b317b 100644
--- a/test/test_clone.py
+++ b/test/test_clone.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
diff --git a/test/test_commit.py b/test/test_commit.py
index 3f3c52f1b..21d17ae2c 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_commit.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_diff.py b/test/test_diff.py
index a46555ce7..50b96efff 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -1,4 +1,3 @@
-# coding: utf-8
 # test_diff.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_docs.py b/test/test_docs.py
index 73f85d540..394b58b5f 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_docs.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_exc.py b/test/test_exc.py
index 0c3ff35a8..ad43695b3 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_exc.py
 # Copyright (C) 2008, 2009, 2016 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_git.py b/test/test_git.py
index aa7a415e5..814316edd 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_git.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_index.py b/test/test_index.py
index 95da472e7..bf582bad9 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_index.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_repo.py b/test/test_repo.py
index a9d52ced0..1ba85acf9 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # test_repo.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 6b331b3bc..f63db1495 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # This module is part of GitPython and is released under
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 

From 36a3b7618479f200e0d4ad0fca38dab217db7dc5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Nov 2023 12:40:24 -0400
Subject: [PATCH 242/994] Revise some comments and strings

These are a few things I had missed in #1725.
---
 git/config.py        | 1 +
 git/objects/base.py  | 4 ++--
 git/refs/symbolic.py | 6 +++---
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/git/config.py b/git/config.py
index 8f68c4e66..1931f142d 100644
--- a/git/config.py
+++ b/git/config.py
@@ -150,6 +150,7 @@ class SectionConstraint(Generic[T_ConfigParser]):
     """
 
     __slots__ = ("_config", "_section_name")
+
     _valid_attrs_ = (
         "get_value",
         "set_value",
diff --git a/git/objects/base.py b/git/objects/base.py
index a771f9fbf..6d2efa32a 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -137,7 +137,7 @@ def __repr__(self) -> str:
     @property
     def hexsha(self) -> str:
         """:return: 40 byte hex version of our 20 byte binary sha"""
-        # b2a_hex produces bytes
+        # b2a_hex produces bytes.
         return bin_to_hex(self.binsha).decode("ascii")
 
     @property
@@ -206,7 +206,7 @@ def __hash__(self) -> int:
 
     def _set_cache_(self, attr: str) -> None:
         if attr in IndexObject.__slots__:
-            # they cannot be retrieved lateron ( not without searching for them )
+            # They cannot be retrieved later on (not without searching for them).
             raise AttributeError(
                 "Attribute '%s' unset: path and mode attributes must have been set during %s object creation"
                 % (attr, type(self).__name__)
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 6e4b657d6..73f64a57b 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -215,7 +215,7 @@ def _check_ref_name_valid(ref_path: PathLike) -> None:
         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'"
+                " '.lock'"
             )
 
     @classmethod
@@ -309,8 +309,8 @@ def set_commit(
     ) -> "SymbolicReference":
         """As set_object, but restricts the type of object to be a Commit.
 
-        :raise ValueError: If commit is not a Commit object or doesn't point to
-            a commit
+        :raise ValueError: If commit is not a :class:`~git.objects.commit.Commit` object
+            or doesn't point to a commit
         :return: self
         """
         # Check the type - assume the best if it is a base-string.

From a47e46d7512c595dc230b2caeedb4ba615fadfa9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Nov 2023 13:30:12 -0400
Subject: [PATCH 243/994] Use zero-argument super() where applicable

This replaces 2-argument super(...) calls with zero-argument
super() calls, where doing so preserves the exact semantics. This
turns out to be everywhere in the actual code (now all uses of the
super builtin are calls to it with zero arguments), and also in
most places code was discussed in comments.

The single exception, occurring in comments (technically a string
literal, but being used as a comment), appears in
git.objects.util.TraversableIterableObj, where super(Commit, self),
rather than super(TraversableIterableObj, self) which would have
the same meaning as super(), is shown. It may be that this should
be changed, but such a revision would not signify identical
semantics, and may require other changes to preserve or recover
clarity or documentary accuracy.
---
 git/cmd.py                    |  4 ++--
 git/config.py                 | 30 +++++++++++++++---------------
 git/db.py                     |  2 +-
 git/exc.py                    |  6 +++---
 git/index/base.py             |  4 ++--
 git/objects/base.py           |  8 ++++----
 git/objects/commit.py         |  4 ++--
 git/objects/submodule/base.py |  6 +++---
 git/objects/submodule/root.py |  2 +-
 git/objects/submodule/util.py |  4 ++--
 git/objects/tag.py            |  4 ++--
 git/objects/tree.py           | 10 +++++-----
 git/objects/util.py           |  6 ++----
 git/refs/head.py              |  2 +-
 git/refs/log.py               |  2 +-
 git/refs/reference.py         |  4 ++--
 git/refs/remote.py            |  2 +-
 git/remote.py                 |  6 +++---
 git/util.py                   |  8 ++++----
 test/performance/lib.py       |  6 +++---
 test/test_commit.py           |  2 +-
 test/test_git.py              |  2 +-
 test/test_index.py            |  2 +-
 test/test_remote.py           |  4 ++--
 24 files changed, 64 insertions(+), 66 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 5359924f0..fd39a8eeb 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -718,7 +718,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
            It is meant to be the working tree directory if available, or the
            ``.git`` directory in case of bare repositories.
         """
-        super(Git, self).__init__()
+        super().__init__()
         self._working_dir = expand_path(working_dir)
         self._git_options: Union[List[str], Tuple[str, ...]] = ()
         self._persistent_git_options: List[str] = []
@@ -765,7 +765,7 @@ def _set_cache_(self, attr: str) -> None:
                 tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
             )
         else:
-            super(Git, self)._set_cache_(attr)
+            super()._set_cache_(attr)
         # END handle version info
 
     @property
diff --git a/git/config.py b/git/config.py
index 1931f142d..2cb057021 100644
--- a/git/config.py
+++ b/git/config.py
@@ -107,7 +107,7 @@ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParse
             # END for each base
         # END if mutating methods configuration is set
 
-        new_type = super(MetaParserBuilder, cls).__new__(cls, name, bases, clsdict)
+        new_type = super().__new__(cls, name, bases, clsdict)
         return new_type
 
 
@@ -178,7 +178,7 @@ def __del__(self) -> None:
     def __getattr__(self, attr: str) -> Any:
         if attr in self._valid_attrs_:
             return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs)
-        return super(SectionConstraint, self).__getattribute__(attr)
+        return super().__getattribute__(attr)
 
     def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any:
         """Call the configuration at the given method which must take a section name
@@ -207,36 +207,36 @@ class _OMD(OrderedDict_OMD):
     """Ordered multi-dict."""
 
     def __setitem__(self, key: str, value: _T) -> None:
-        super(_OMD, self).__setitem__(key, [value])
+        super().__setitem__(key, [value])
 
     def add(self, key: str, value: Any) -> None:
         if key not in self:
-            super(_OMD, self).__setitem__(key, [value])
+            super().__setitem__(key, [value])
             return None
-        super(_OMD, self).__getitem__(key).append(value)
+        super().__getitem__(key).append(value)
 
     def setall(self, key: str, values: List[_T]) -> None:
-        super(_OMD, self).__setitem__(key, values)
+        super().__setitem__(key, values)
 
     def __getitem__(self, key: str) -> Any:
-        return super(_OMD, self).__getitem__(key)[-1]
+        return super().__getitem__(key)[-1]
 
     def getlast(self, key: str) -> Any:
-        return super(_OMD, self).__getitem__(key)[-1]
+        return super().__getitem__(key)[-1]
 
     def setlast(self, key: str, value: Any) -> None:
         if key not in self:
-            super(_OMD, self).__setitem__(key, [value])
+            super().__setitem__(key, [value])
             return
 
-        prior = super(_OMD, self).__getitem__(key)
+        prior = super().__getitem__(key)
         prior[-1] = value
 
     def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]:
-        return super(_OMD, self).get(key, [default])[-1]
+        return super().get(key, [default])[-1]
 
     def getall(self, key: str) -> List[_T]:
-        return super(_OMD, self).__getitem__(key)
+        return super().__getitem__(key)
 
     def items(self) -> List[Tuple[str, _T]]:  # type: ignore[override]
         """List of (key, last value for key)."""
@@ -680,7 +680,7 @@ def write_section(name: str, section_dict: _OMD) -> None:
 
     def items(self, section_name: str) -> List[Tuple[str, str]]:  # type: ignore[override]
         """:return: list((option, value), ...) pairs of all items in the given section"""
-        return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != "__name__"]
+        return [(k, v) for k, v in super().items(section_name) if k != "__name__"]
 
     def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
         """:return: list((option, [values...]), ...) pairs of all items in the given section"""
@@ -748,7 +748,7 @@ def _assure_writable(self, method_name: str) -> None:
 
     def add_section(self, section: str) -> None:
         """Assures added options will stay in order"""
-        return super(GitConfigParser, self).add_section(section)
+        return super().add_section(section)
 
     @property
     def read_only(self) -> bool:
@@ -899,7 +899,7 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
         if self.has_section(new_name):
             raise ValueError("Destination section '%s' already exists" % new_name)
 
-        super(GitConfigParser, self).add_section(new_name)
+        super().add_section(new_name)
         new_section = self._sections[new_name]
         for k, vs in self.items_all(section):
             new_section.setall(k, vs)
diff --git a/git/db.py b/git/db.py
index 1aacd0c84..9e278ea75 100644
--- a/git/db.py
+++ b/git/db.py
@@ -34,7 +34,7 @@ class GitCmdObjectDB(LooseObjectDB):
 
     def __init__(self, root_path: PathLike, git: "Git") -> None:
         """Initialize this instance with the root and a git command."""
-        super(GitCmdObjectDB, self).__init__(root_path)
+        super().__init__(root_path)
         self._git = git
 
     def info(self, binsha: bytes) -> OInfo:
diff --git a/git/exc.py b/git/exc.py
index bfb023fa5..124c5eeea 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -137,7 +137,7 @@ class GitCommandNotFound(CommandError):
     the GIT_PYTHON_GIT_EXECUTABLE environment variable."""
 
     def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
-        super(GitCommandNotFound, self).__init__(command, cause)
+        super().__init__(command, cause)
         self._msg = "Cmd('%s') not found%s"
 
 
@@ -151,7 +151,7 @@ def __init__(
         stderr: Union[bytes, str, None] = None,
         stdout: Union[bytes, str, None] = None,
     ) -> None:
-        super(GitCommandError, self).__init__(command, status, stderr, stdout)
+        super().__init__(command, status, stderr, stdout)
 
 
 class CheckoutError(GitError):
@@ -207,7 +207,7 @@ def __init__(
         stderr: Union[bytes, str, None] = None,
         stdout: Union[bytes, str, None] = None,
     ) -> None:
-        super(HookExecutionError, self).__init__(command, status, stderr, stdout)
+        super().__init__(command, status, stderr, stdout)
         self._msg = "Hook('%s') failed%s"
 
 
diff --git a/git/index/base.py b/git/index/base.py
index b87fefd56..c2333a2c2 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -153,7 +153,7 @@ def _set_cache_(self, attr: str) -> None:
 
             self._deserialize(stream)
         else:
-            super(IndexFile, self)._set_cache_(attr)
+            super()._set_cache_(attr)
 
     def _index_path(self) -> PathLike:
         if self.repo.git_dir:
@@ -1425,4 +1425,4 @@ def diff(
             raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other)
 
         # Diff against working copy - can be handled by superclass natively.
-        return super(IndexFile, self).diff(other, paths, create_patch, **kwargs)
+        return super().diff(other, paths, create_patch, **kwargs)
diff --git a/git/objects/base.py b/git/objects/base.py
index 6d2efa32a..9f188a955 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -62,7 +62,7 @@ def __init__(self, repo: "Repo", binsha: bytes):
 
         :param binsha: 20 byte SHA1
         """
-        super(Object, self).__init__()
+        super().__init__()
         self.repo = repo
         self.binsha = binsha
         assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (
@@ -108,7 +108,7 @@ def _set_cache_(self, attr: str) -> None:
             self.size = oinfo.size  # type:  int
             # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type)
         else:
-            super(Object, self)._set_cache_(attr)
+            super()._set_cache_(attr)
 
     def __eq__(self, other: Any) -> bool:
         """:return: True if the objects have the same SHA1"""
@@ -190,7 +190,7 @@ def __init__(
             Path may not be set if the index object has been created directly, as it
             cannot be retrieved without knowing the parent tree.
         """
-        super(IndexObject, self).__init__(repo, binsha)
+        super().__init__(repo, binsha)
         if mode is not None:
             self.mode = mode
         if path is not None:
@@ -212,7 +212,7 @@ def _set_cache_(self, attr: str) -> None:
                 % (attr, type(self).__name__)
             )
         else:
-            super(IndexObject, self)._set_cache_(attr)
+            super()._set_cache_(attr)
         # END handle slot attribute
 
     @property
diff --git a/git/objects/commit.py b/git/objects/commit.py
index d39ca52bb..04acb668b 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -146,7 +146,7 @@ def __init__(
             as what time.altzone returns. The sign is inverted compared to git's
             UTC timezone.
         """
-        super(Commit, self).__init__(repo, binsha)
+        super().__init__(repo, binsha)
         self.binsha = binsha
         if tree is not None:
             assert isinstance(tree, Tree), "Tree needs to be a Tree instance, was %s" % type(tree)
@@ -218,7 +218,7 @@ def _set_cache_(self, attr: str) -> None:
             _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha)
             self._deserialize(BytesIO(stream.read()))
         else:
-            super(Commit, self)._set_cache_(attr)
+            super()._set_cache_(attr)
         # END handle attrs
 
     @property
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 018f9a39c..1516306ec 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -124,7 +124,7 @@ def __init__(
         :param branch_path: Full (relative) path to ref to checkout when cloning the
             remote repository.
         """
-        super(Submodule, self).__init__(repo, binsha, mode, path)
+        super().__init__(repo, binsha, mode, path)
         self.size = 0
         self._parent_commit = parent_commit
         if url is not None:
@@ -154,7 +154,7 @@ def _set_cache_(self, attr: str) -> None:
         elif attr == "_name":
             raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
         else:
-            super(Submodule, self)._set_cache_(attr)
+            super()._set_cache_(attr)
         # END handle attribute name
 
     @classmethod
@@ -174,7 +174,7 @@ def __eq__(self, other: Any) -> bool:
         """Compare with another submodule."""
         # We may only compare by name as this should be the ID they are hashed with.
         # Otherwise this type wouldn't be hashable.
-        # return self.path == other.path and self.url == other.url and super(Submodule, self).__eq__(other)
+        # return self.path == other.path and self.url == other.url and super().__eq__(other)
         return self._name == other._name
 
     def __ne__(self, other: object) -> bool:
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 0ac8a22db..cfcbb4cb7 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -55,7 +55,7 @@ class RootModule(Submodule):
 
     def __init__(self, repo: "Repo"):
         # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
-        super(RootModule, self).__init__(
+        super().__init__(
             repo,
             binsha=self.NULL_BIN_SHA,
             mode=self.k_default_mode,
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index e13528a8f..3fc0b0b56 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -79,7 +79,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
         self._smref: Union["ReferenceType[Submodule]", None] = None
         self._index = None
         self._auto_write = True
-        super(SubmoduleConfigParser, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     # { Interface
     def set_submodule(self, submodule: "Submodule") -> None:
@@ -107,7 +107,7 @@ def flush_to_index(self) -> None:
 
     # { Overridden Methods
     def write(self) -> None:  # type: ignore[override]
-        rval: None = super(SubmoduleConfigParser, self).write()
+        rval: None = super().write()
         self.flush_to_index()
         return rval
 
diff --git a/git/objects/tag.py b/git/objects/tag.py
index a7ff7f263..6eb1c8d90 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -64,7 +64,7 @@ def __init__(
             The timezone that the authored_date is in, in a format similar
             to :attr:`time.altzone`.
         """
-        super(TagObject, self).__init__(repo, binsha)
+        super().__init__(repo, binsha)
         if object is not None:
             self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object
         if tag is not None:
@@ -108,4 +108,4 @@ def _set_cache_(self, attr: str) -> None:
                 self.message = ""
         # END check our attributes
         else:
-            super(TagObject, self)._set_cache_(attr)
+            super()._set_cache_(attr)
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 708ab3edd..1be6f193e 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -237,7 +237,7 @@ def __init__(
         mode: int = tree_id << 12,
         path: Union[PathLike, None] = None,
     ):
-        super(Tree, self).__init__(repo, binsha, mode, path)
+        super().__init__(repo, binsha, mode, path)
 
     @classmethod
     def _get_intermediate_items(
@@ -254,7 +254,7 @@ def _set_cache_(self, attr: str) -> None:
             ostream = self.repo.odb.stream(self.binsha)
             self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read())
         else:
-            super(Tree, self)._set_cache_(attr)
+            super()._set_cache_(attr)
         # END handle attribute
 
     def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
@@ -352,13 +352,13 @@ def traverse(
         # def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]:
         #     return all(isinstance(x, (Blob, Tree, Submodule)) for x in inp[1])
 
-        # ret = super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
+        # ret = super().traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
         # ret_tup = itertools.tee(ret, 2)
         # assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}"
         # return ret_tup[0]"""
         return cast(
             Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
-            super(Tree, self)._traverse(
+            super()._traverse(
                 predicate,
                 prune,
                 depth,  # type: ignore
@@ -374,7 +374,7 @@ def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion
             traverse()
             Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
         """
-        return super(Tree, self)._list_traverse(*args, **kwargs)
+        return super()._list_traverse(*args, **kwargs)
 
     # List protocol
 
diff --git a/git/objects/util.py b/git/objects/util.py
index 5ccb3ec37..7af7fa0e5 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -577,7 +577,7 @@ class TraversableIterableObj(IterableObj, Traversable):
     TIobj_tuple = Tuple[Union[T_TIobj, None], T_TIobj]
 
     def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]:
-        return super(TraversableIterableObj, self)._list_traverse(*args, **kwargs)
+        return super()._list_traverse(*args, **kwargs)
 
     @overload  # type: ignore
     def traverse(self: T_TIobj) -> Iterator[T_TIobj]:
@@ -652,7 +652,5 @@ def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit',
         """
         return cast(
             Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
-            super(TraversableIterableObj, self)._traverse(
-                predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge  # type: ignore
-            ),
+            super()._traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge),  # type: ignore
         )
diff --git a/git/refs/head.py b/git/refs/head.py
index 30ff336d5..fa40943c6 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -39,7 +39,7 @@ class HEAD(SymbolicReference):
     def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME):
         if path != self._HEAD_NAME:
             raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path))
-        super(HEAD, self).__init__(repo, path)
+        super().__init__(repo, path)
         self.commit: "Commit"
 
     def orig_head(self) -> SymbolicReference:
diff --git a/git/refs/log.py b/git/refs/log.py
index 21c757ccd..ebdaf04d1 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -155,7 +155,7 @@ class RefLog(List[RefLogEntry], Serializable):
     __slots__ = ("_path",)
 
     def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog":
-        inst = super(RefLog, cls).__new__(cls)
+        inst = super().__new__(cls)
         return inst
 
     def __init__(self, filepath: Union[PathLike, None] = None):
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 8a0262ea4..f0eb6bfaa 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -62,7 +62,7 @@ def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> Non
         if check_path and not str(path).startswith(self._common_path_default + "/"):
             raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
         self.path: str  # SymbolicReference converts to string at the moment.
-        super(Reference, self).__init__(repo, path)
+        super().__init__(repo, path)
 
     def __str__(self) -> str:
         return self.name
@@ -87,7 +87,7 @@ def set_object(
             # END handle commit retrieval
         # END handle message is set
 
-        super(Reference, self).set_object(object, logmsg)
+        super().set_object(object, logmsg)
 
         if oldbinsha is not None:
             # From refs/files-backend.c in git-source:
diff --git a/git/refs/remote.py b/git/refs/remote.py
index e4b1f4392..f26ee08fc 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -40,7 +40,7 @@ def iter_items(
             common_path = join_path(common_path, str(remote))
         # END handle remote constraint
         # super is Reference
-        return super(RemoteReference, cls).iter_items(repo, common_path)
+        return super().iter_items(repo, common_path)
 
     # The Head implementation of delete also accepts strs, but this
     # implementation does not.  mypy doesn't have a way of representing
diff --git a/git/remote.py b/git/remote.py
index 880e87232..ccf70a25c 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -573,14 +573,14 @@ def __getattr__(self, attr: str) -> Any:
         """Allows to call this instance like
         remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name."""
         if attr == "_config_reader":
-            return super(Remote, self).__getattr__(attr)
+            return super().__getattr__(attr)
 
         # Sometimes, probably due to a bug in Python itself, we are being called
         # even though a slot of the same name exists.
         try:
             return self._config_reader.get(attr)
         except cp.NoOptionError:
-            return super(Remote, self).__getattr__(attr)
+            return super().__getattr__(attr)
         # END handle exception
 
     def _config_section_name(self) -> str:
@@ -592,7 +592,7 @@ def _set_cache_(self, attr: str) -> None:
             # values implicitly, such as in print(r.pushurl).
             self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name())
         else:
-            super(Remote, self)._set_cache_(attr)
+            super()._set_cache_(attr)
 
     def __str__(self) -> str:
         return self.name
diff --git a/git/util.py b/git/util.py
index 73b3a2edd..bd1fbe247 100644
--- a/git/util.py
+++ b/git/util.py
@@ -718,7 +718,7 @@ class CallableRemoteProgress(RemoteProgress):
 
     def __init__(self, fn: Callable) -> None:
         self._callable = fn
-        super(CallableRemoteProgress, self).__init__()
+        super().__init__()
 
     def update(self, *args: Any, **kwargs: Any) -> None:
         self._callable(*args, **kwargs)
@@ -1040,7 +1040,7 @@ def __init__(
 
         :param max_block_time_s: Maximum amount of seconds we may lock.
         """
-        super(BlockingLockFile, self).__init__(file_path)
+        super().__init__(file_path)
         self._check_interval = check_interval_s
         self._max_block_time = max_block_time_s
 
@@ -1054,7 +1054,7 @@ def _obtain_lock(self) -> None:
         maxtime = starttime + float(self._max_block_time)
         while True:
             try:
-                super(BlockingLockFile, self)._obtain_lock()
+                super()._obtain_lock()
             except IOError as e:
                 # synity check: if the directory leading to the lockfile is not
                 # readable anymore, raise an exception
@@ -1103,7 +1103,7 @@ class IterableList(List[T_IterableObj]):
     __slots__ = ("_id_attr", "_prefix")
 
     def __new__(cls, id_attr: str, prefix: str = "") -> "IterableList[T_IterableObj]":
-        return super(IterableList, cls).__new__(cls)
+        return super().__new__(cls)
 
     def __init__(self, id_attr: str, prefix: str = "") -> None:
         self._id_attr = id_attr
diff --git a/test/performance/lib.py b/test/performance/lib.py
index 8e76fe815..1ac3e0b60 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -33,7 +33,7 @@ class TestBigRepoR(TestBase):
 
     def setUp(self):
         try:
-            super(TestBigRepoR, self).setUp()
+            super().setUp()
         except AttributeError:
             pass
 
@@ -65,7 +65,7 @@ class TestBigRepoRW(TestBigRepoR):
     def setUp(self):
         self.gitrwrepo = None
         try:
-            super(TestBigRepoRW, self).setUp()
+            super().setUp()
         except AttributeError:
             pass
         dirname = tempfile.mktemp()
@@ -74,7 +74,7 @@ def setUp(self):
         self.puregitrwrepo = Repo(dirname, odbt=GitDB)
 
     def tearDown(self):
-        super(TestBigRepoRW, self).tearDown()
+        super().tearDown()
         if self.gitrwrepo is not None:
             rmtree(self.gitrwrepo.working_dir)
             self.gitrwrepo.git.clear_cache()
diff --git a/test/test_commit.py b/test/test_commit.py
index 21d17ae2c..1327616ed 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -275,7 +275,7 @@ def test_iteration(self):
 
         class Child(Commit):
             def __init__(self, *args, **kwargs):
-                super(Child, self).__init__(*args, **kwargs)
+                super().__init__(*args, **kwargs)
 
         child_commits = list(Child.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS")))
         assert type(child_commits[0]) is Child
diff --git a/test/test_git.py b/test/test_git.py
index 814316edd..06f8f5c97 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -32,7 +32,7 @@
 class TestGit(TestBase):
     @classmethod
     def setUpClass(cls):
-        super(TestGit, cls).setUpClass()
+        super().setUpClass()
         cls.git = Git(cls.rorepo.working_dir)
 
     def tearDown(self):
diff --git a/test/test_index.py b/test/test_index.py
index bf582bad9..3357dc880 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -66,7 +66,7 @@ def _make_hook(git_dir, name, content, make_exec=True):
 
 class TestIndex(TestBase):
     def __init__(self, *args):
-        super(TestIndex, self).__init__(*args)
+        super().__init__(*args)
         self._reset_progress()
 
     def _assert_fprogress(self, entries):
diff --git a/test/test_remote.py b/test/test_remote.py
index 3e7a025c5..8205c0bcd 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):
-        super(TestRemoteProgress, self).__init__()
+        super().__init__()
         self._seen_lines = []
         self._stages_per_op = {}
         self._num_progress_messages = 0
@@ -53,7 +53,7 @@ def _parse_progress_line(self, line):
         # We may remove the line later if it is dropped.
         # Keep it for debugging.
         self._seen_lines.append(line)
-        rval = super(TestRemoteProgress, self)._parse_progress_line(line)
+        rval = super()._parse_progress_line(line)
         return rval
 
     def line_dropped(self, line):

From 91131770eb0e932b9bc0918fe8374875ecbbd816 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Nov 2023 13:45:32 -0400
Subject: [PATCH 244/994] Don't swallow AttributeError from super().setUp()

This is conceptually independent of the immediately preceding
super() call refactoring, but serves the same goal of simplifying
and clarifying super() calls.

In test.performance.lib, the TestBigRepoR and TestBigRepoRW
classes' setUp methods had calls to setUp through super proxies,
which were wrapped in try-blocks to swallow AttributeError
exceptions. This removes that, relying on the presence of setUp
methods in some parent or sibling class in the MRO.

The intent appeared to be solely to account for the possibility
that no class in the MRO would define a setUp method. However,
the unittest.TestCase base class defines noop setUp and tearDown
methods to ensure this does not have to be done.

This may also make the code more robust, because the form in which
AttributeError was being swallowed was:

    try:
        super().setUp()
    except AttributeError:
        pass

But that has the disadvantage of also catching AttributeError due
to a bug or other problem in code that runs *in* an ancestor or
sibling class's existing setUp method. This could alternatively
be addressed by using:

    try:
        other_setUp = super().setUp
    except AttributeError:
        pass
    else:
        other_setUp()

However, because unittest.TestCase provides empty setUp and
tearDown methods to allow such special-casing to be avoided (both
in cases like this and for the test runner), this isn't needed.
---
 test/performance/lib.py | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/test/performance/lib.py b/test/performance/lib.py
index 1ac3e0b60..2b2a632d9 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -32,10 +32,7 @@ class TestBigRepoR(TestBase):
     """
 
     def setUp(self):
-        try:
-            super().setUp()
-        except AttributeError:
-            pass
+        super().setUp()
 
         repo_path = os.environ.get(k_env_git_repo)
         if repo_path is None:
@@ -64,10 +61,7 @@ class TestBigRepoRW(TestBigRepoR):
 
     def setUp(self):
         self.gitrwrepo = None
-        try:
-            super().setUp()
-        except AttributeError:
-            pass
+        super().setUp()
         dirname = tempfile.mktemp()
         os.mkdir(dirname)
         self.gitrwrepo = self.gitrorepo.clone(dirname, shared=True, bare=True, odbt=GitCmdObjectDB)

From 2c7fce0c289d871111ef1610a76785cc8c536cb6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 3 Nov 2023 08:01:13 -0400
Subject: [PATCH 245/994] Remove obsolete note in _iter_packed_refs

This removes a comment noting that a try-finally block had been
present (or had been intended), but was removed because some
version of Python had imposed a limitation on yield appearing in
try-finally.

That comment was obsolete as of 58c5b99 (#326), which wrapped the
relevant code in a with-statement, because:

1. Since then, the cleanup is done in a manner equivaent to
   try-finally.

2. It turned out, as noted in that PR, that cleanup had not
   always been done automatically. (This was contrary to the
   prediction given in the comment.)

3. At some point before that, the limitation that had prevented
   the use of try-finally no longer affected any supported version
   of Python.

   Specifically, it appears the only limitation that this could be
   was the limitation lifted in Python 2.5, where along with the
   introduction of close(), which is automatically called when a
   generator object is finalized, it became permitted for yield to
   appear in a try-block with an associated finally-block, on the
   grounds that calling close() runs the finally-block (by raising
   GeneratorExit).

   For details, see:
   https://docs.python.org/3/whatsnew/2.5.html#pep-342-new-generator-features

(This obsolete comment was one of the things I discovered while
working on #1725, but I didn't include this change there, having
not yet looked into the history of the code enough to be sure.)
---
 git/refs/symbolic.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 73f64a57b..99a60201f 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -149,10 +149,6 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
         except OSError:
             return None
         # END no packed-refs file handling
-        # NOTE: Had try-finally block around here to close the fp,
-        # but some python version wouldn't allow yields within that.
-        # I believe files are closing themselves on destruction, so it is
-        # alright.
 
     @classmethod
     def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> str:

From 09826f4ecf9cacc617627ad54452235d449345fc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 04:50:02 -0400
Subject: [PATCH 246/994] Revise test_util comment style

---
 test/test_util.py | 48 +++++++++++++++++++++++------------------------
 1 file changed, 24 insertions(+), 24 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index f75231c98..5cac56f3b 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -1,4 +1,4 @@
-# test_utils.py
+# test_util.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
 # This module is part of GitPython and is released under
@@ -275,14 +275,14 @@ def test_lock_file(self):
         my_file = tempfile.mktemp()
         lock_file = LockFile(my_file)
         assert not lock_file._has_lock()
-        # release lock we don't have  - fine
+        # Release lock we don't have - fine.
         lock_file._release_lock()
 
-        # get lock
+        # Get lock.
         lock_file._obtain_lock_or_raise()
         assert lock_file._has_lock()
 
-        # concurrent access
+        # Concurrent access.
         other_lock_file = LockFile(my_file)
         assert not other_lock_file._has_lock()
         self.assertRaises(IOError, other_lock_file._obtain_lock_or_raise)
@@ -293,7 +293,7 @@ def test_lock_file(self):
         other_lock_file._obtain_lock_or_raise()
         self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
 
-        # auto-release on destruction
+        # Auto-release on destruction.
         del other_lock_file
         lock_file._obtain_lock_or_raise()
         lock_file._release_lock()
@@ -303,7 +303,7 @@ def test_blocking_lock_file(self):
         lock_file = BlockingLockFile(my_file)
         lock_file._obtain_lock()
 
-        # next one waits for the lock
+        # Next one waits for the lock.
         start = time.time()
         wait_time = 0.1
         wait_lock = BlockingLockFile(my_file, 0.05, wait_time)
@@ -318,7 +318,7 @@ def test_user_id(self):
         self.assertIn("@", get_user_id())
 
     def test_parse_date(self):
-        # parse_date(from_timestamp()) must return the tuple unchanged
+        # parse_date(from_timestamp()) must return the tuple unchanged.
         for timestamp, offset in (
             (1522827734, -7200),
             (1522827734, 0),
@@ -326,7 +326,7 @@ def test_parse_date(self):
         ):
             self.assertEqual(parse_date(from_timestamp(timestamp, offset)), (timestamp, offset))
 
-        # test all supported formats
+        # Test all supported formats.
         def assert_rval(rval, veri_time, offset=0):
             self.assertEqual(len(rval), 2)
             self.assertIsInstance(rval[0], int)
@@ -334,7 +334,7 @@ def assert_rval(rval, veri_time, offset=0):
             self.assertEqual(rval[0], veri_time)
             self.assertEqual(rval[1], offset)
 
-            # now that we are here, test our conversion functions as well
+            # Now that we are here, test our conversion functions as well.
             utctz = altz_to_utctz_str(offset)
             self.assertIsInstance(utctz, str)
             self.assertEqual(utctz_to_altz(verify_utctz(utctz)), offset)
@@ -347,13 +347,13 @@ def assert_rval(rval, veri_time, offset=0):
         iso3 = ("2005.04.07 22:13:11 -0000", 0)
         alt = ("04/07/2005 22:13:11", 0)
         alt2 = ("07.04.2005 22:13:11", 0)
-        veri_time_utc = 1112911991  # the time this represents, in time since epoch, UTC
+        veri_time_utc = 1112911991  # The time this represents, in time since epoch, UTC.
         for date, offset in (rfc, iso, iso2, iso3, alt, alt2):
             assert_rval(parse_date(date), veri_time_utc, offset)
         # END for each date type
 
-        # and failure
-        self.assertRaises(ValueError, parse_date, datetime.now())  # non-aware datetime
+        # ...and failure.
+        self.assertRaises(ValueError, parse_date, datetime.now())  # Non-aware datetime.
         self.assertRaises(ValueError, parse_date, "invalid format")
         self.assertRaises(ValueError, parse_date, "123456789 -02000")
         self.assertRaises(ValueError, parse_date, " 123456789 -0200")
@@ -362,7 +362,7 @@ def test_actor(self):
         for cr in (None, self.rorepo.config_reader()):
             self.assertIsInstance(Actor.committer(cr), Actor)
             self.assertIsInstance(Actor.author(cr), Actor)
-        # END assure config reader is handled
+        # END ensure config reader is handled
 
     @with_rw_repo("HEAD")
     @mock.patch("getpass.getuser")
@@ -402,7 +402,7 @@ def test_actor_get_uid_laziness_called(self, mock_get_uid):
         mock_get_uid.return_value = "user"
         committer = Actor.committer(None)
         author = Actor.author(None)
-        # We can't test with `self.rorepo.config_reader()` here, as the uuid laziness
+        # We can't test with `self.rorepo.config_reader()` here, as the UUID laziness
         # depends on whether the user running the test has their global user.name config set.
         self.assertEqual(committer.name, "user")
         self.assertTrue(committer.email.startswith("user@"))
@@ -436,30 +436,30 @@ def test_iterable_list(self, case):
 
         self.assertEqual(len(ilist), 2)
 
-        # contains works with name and identity
+        # Contains works with name and identity.
         self.assertIn(name1, ilist)
         self.assertIn(name2, ilist)
         self.assertIn(m2, ilist)
         self.assertIn(m2, ilist)
         self.assertNotIn("invalid", ilist)
 
-        # with string index
+        # With string index.
         self.assertIs(ilist[name1], m1)
         self.assertIs(ilist[name2], m2)
 
-        # with int index
+        # With int index.
         self.assertIs(ilist[0], m1)
         self.assertIs(ilist[1], m2)
 
-        # with getattr
+        # With getattr.
         self.assertIs(ilist.one, m1)
         self.assertIs(ilist.two, m2)
 
-        # test exceptions
+        # Test exceptions.
         self.assertRaises(AttributeError, getattr, ilist, "something")
         self.assertRaises(IndexError, ilist.__getitem__, "something")
 
-        # delete by name and index
+        # Delete by name and index.
         self.assertRaises(IndexError, ilist.__delitem__, "something")
         del ilist[name2]
         self.assertEqual(len(ilist), 1)
@@ -494,21 +494,21 @@ def test_altz_to_utctz_str(self):
         self.assertEqual(altz_to_utctz_str(-59), "+0000")
 
     def test_from_timestamp(self):
-        # Correct offset: UTC+2, should return datetime + tzoffset(+2)
+        # Correct offset: UTC+2, should return datetime + tzoffset(+2).
         altz = utctz_to_altz("+0200")
         self.assertEqual(
             datetime.fromtimestamp(1522827734, tzoffset(altz)),
             from_timestamp(1522827734, altz),
         )
 
-        # Wrong offset: UTC+58, should return datetime + tzoffset(UTC)
+        # Wrong offset: UTC+58, should return datetime + tzoffset(UTC).
         altz = utctz_to_altz("+5800")
         self.assertEqual(
             datetime.fromtimestamp(1522827734, tzoffset(0)),
             from_timestamp(1522827734, altz),
         )
 
-        # Wrong offset: UTC-9000, should return datetime + tzoffset(UTC)
+        # Wrong offset: UTC-9000, should return datetime + tzoffset(UTC).
         altz = utctz_to_altz("-9000")
         self.assertEqual(
             datetime.fromtimestamp(1522827734, tzoffset(0)),
@@ -538,7 +538,7 @@ def test_remove_password_from_command_line(self):
         redacted_cmd_1 = remove_password_if_present(cmd_1)
         assert username not in " ".join(redacted_cmd_1)
         assert password not in " ".join(redacted_cmd_1)
-        # Check that we use a copy
+        # Check that we use a copy.
         assert cmd_1 is not redacted_cmd_1
         assert username in " ".join(cmd_1)
         assert password in " ".join(cmd_1)

From e2fa5e2225fbcdd0f34108f1b9a6b4b1e8653555 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 09:06:11 -0400
Subject: [PATCH 247/994] Pull rmtree tests out of TestUtils class

And rework them as pure pytest tests (using pytest fixtures). This
creates the TestRmtree class, which does not derive from TestCase.
---
 pyproject.toml        |  13 +--
 test-requirements.txt |   3 +-
 test/test_util.py     | 203 ++++++++++++++++++++++--------------------
 3 files changed, 115 insertions(+), 104 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index f4fc33fec..eae5943ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,10 +3,11 @@ requires = ["setuptools"]
 build-backend = "setuptools.build_meta"
 
 [tool.pytest.ini_options]
-python_files = 'test_*.py'
-testpaths = 'test'  # space separated list of paths from root e.g test tests doc/testing
-addopts = '--cov=git --cov-report=term --disable-warnings'
-filterwarnings = 'ignore::DeprecationWarning'
+addopts = "--cov=git --cov-report=term --disable-warnings"
+filterwarnings = "ignore::DeprecationWarning"
+python_files = "test_*.py"
+tmp_path_retention_policy = "failed"
+testpaths = "test"  # Space separated list of paths from root e.g test tests doc/testing.
 # --cov   coverage
 # --cov-report term  # send report to terminal term-missing -> terminal with line numbers  html  xml
 # --cov-report term-missing # to terminal with line numbers
@@ -29,7 +30,7 @@ show_error_codes = true
 implicit_reexport = true
 # strict = true
 
-# TODO: remove when 'gitdb' is fully annotated
+# TODO: Remove when 'gitdb' is fully annotated.
 exclude = ["^git/ext/gitdb"]
 [[tool.mypy.overrides]]
 module = "gitdb.*"
@@ -44,5 +45,5 @@ omit = ["*/git/ext/*"]
 
 [tool.black]
 line-length = 120
-target-version = ['py37']
+target-version = ["py37"]
 extend-exclude = "git/ext/gitdb"
diff --git a/test-requirements.txt b/test-requirements.txt
index a69181be1..8cadfb836 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,8 +4,9 @@ ddt >= 1.1.1, != 1.4.3
 mock ; python_version < "3.8"
 mypy
 pre-commit
-pytest
+pytest >= 7.3.1
 pytest-cov
 pytest-instafail
+pytest-mock
 pytest-subtests
 pytest-sugar
diff --git a/test/test_util.py b/test/test_util.py
index 5cac56f3b..784dc9bfc 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -5,7 +5,6 @@
 # the BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ast
-import contextlib
 from datetime import datetime
 import os
 import pathlib
@@ -15,7 +14,7 @@
 import sys
 import tempfile
 import time
-from unittest import SkipTest, mock, skipIf, skipUnless
+from unittest import SkipTest, mock, skipUnless
 
 import ddt
 import pytest
@@ -44,123 +43,133 @@
 from test.lib import TestBase, with_rw_repo
 
 
-class _Member:
-    """A member of an IterableList."""
-
-    __slots__ = ("name",)
-
-    def __init__(self, name):
-        self.name = name
-
-    def __repr__(self):
-        return f"{type(self).__name__}({self.name!r})"
-
-
-@contextlib.contextmanager
-def _tmpdir_to_force_permission_error():
-    """Context manager to test permission errors in situations where they are not overcome."""
+@pytest.fixture
+def permission_error_tmpdir(tmp_path):
+    """Fixture to test permissions errors situations where they are not overcome."""
     if sys.platform == "cygwin":
         raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
     if sys.version_info < (3, 8):
         raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
 
-    with tempfile.TemporaryDirectory() as parent:
-        td = pathlib.Path(parent, "testdir")
-        td.mkdir()
-        (td / "x").write_bytes(b"")
-        (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
-        td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
-        yield td
+    td = tmp_path / "testdir"
+    td.mkdir()
+    (td / "x").write_bytes(b"")
+    (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
+    td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
+    yield td
 
 
-@contextlib.contextmanager
-def _tmpdir_for_file_not_found():
-    """Context manager to test errors deleting a directory that are not due to permissions."""
-    with tempfile.TemporaryDirectory() as parent:
-        yield pathlib.Path(parent, "testdir")  # It is deliberately never created.
+@pytest.fixture
+def file_not_found_tmpdir(tmp_path):
+    """Fixture to test errors deleting a directory that are not due to permissions."""
+    yield tmp_path / "testdir"  # It is deliberately never created.
 
 
-@ddt.ddt
-class TestUtils(TestBase):
-    def test_rmtree_deletes_nested_dir_with_files(self):
-        with tempfile.TemporaryDirectory() as parent:
-            td = pathlib.Path(parent, "testdir")
-            for d in td, td / "q", td / "s":
-                d.mkdir()
-            for f in (
-                td / "p",
-                td / "q" / "w",
-                td / "q" / "x",
-                td / "r",
-                td / "s" / "y",
-                td / "s" / "z",
-            ):
-                f.write_bytes(b"")
+class TestRmtree:
+    """Tests for :func:`git.util.rmtree`."""
 
-            try:
-                rmtree(td)
-            except SkipTest as ex:
-                self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+    def test_deletes_nested_dir_with_files(self, tmp_path):
+        td = tmp_path / "testdir"
 
-            self.assertFalse(td.exists())
+        for d in td, td / "q", td / "s":
+            d.mkdir()
+        for f in (
+            td / "p",
+            td / "q" / "w",
+            td / "q" / "x",
+            td / "r",
+            td / "s" / "y",
+            td / "s" / "z",
+        ):
+            f.write_bytes(b"")
 
-    @skipIf(
+        try:
+            rmtree(td)
+        except SkipTest as ex:
+            pytest.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
+        assert not td.exists()
+
+    @pytest.mark.skipif(
         sys.platform == "cygwin",
-        "Cygwin can't set the permissions that make the test meaningful.",
+        reason="Cygwin can't set the permissions that make the test meaningful.",
     )
-    def test_rmtree_deletes_dir_with_readonly_files(self):
+    def test_deletes_dir_with_readonly_files(self, tmp_path):
         # Automatically works on Unix, but requires special handling on Windows.
-        # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below).
-        with tempfile.TemporaryDirectory() as parent:
-            td = pathlib.Path(parent, "testdir")
-            for d in td, td / "sub":
-                d.mkdir()
-            for f in td / "x", td / "sub" / "y":
-                f.write_bytes(b"")
-                f.chmod(0)
+        # Not to be confused with what permission_error_tmpdir sets up (see below).
+
+        td = tmp_path / "testdir"
+
+        for d in td, td / "sub":
+            d.mkdir()
+        for f in td / "x", td / "sub" / "y":
+            f.write_bytes(b"")
+            f.chmod(0)
+
+        try:
+            rmtree(td)
+        except SkipTest as ex:
+            self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
+        assert not td.exists()
+
+    def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
+        """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        # Access the module through sys.modules so it is unambiguous which module's
+        # attribute we patch: the original git.util, not git.index.util even though
+        # git.index.util "replaces" git.util and is what "import git.util" gives us.
+        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True)
+
+        # Disable common chmod functions so the callback can't fix the problem.
+        mocker.patch.object(os, "chmod")
+        mocker.patch.object(pathlib.Path, "chmod")
+
+        # Now we can see how an intractable PermissionError is treated.
+        with pytest.raises(SkipTest):
+            rmtree(permission_error_tmpdir)
+
+    def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir):
+        """rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false."""
+        # See comments in test_wraps_perm_error_if_enabled for details about patching.
+        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", False)
+        mocker.patch.object(os, "chmod")
+        mocker.patch.object(pathlib.Path, "chmod")
+
+        with pytest.raises(PermissionError):
+            try:
+                rmtree(permission_error_tmpdir)
+            except SkipTest as ex:
+                pytest.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
+    @pytest.mark.parametrize("hide_windows_known_errors", [False, True])
+    def test_does_not_wrap_other_errors(self, mocker, file_not_found_tmpdir, hide_windows_known_errors):
+        # See comments in test_wraps_perm_error_if_enabled for details about patching.
+        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
+        mocker.patch.object(os, "chmod")
+        mocker.patch.object(pathlib.Path, "chmod")
+
+        with pytest.raises(FileNotFoundError):
             try:
-                rmtree(td)
+                rmtree(file_not_found_tmpdir)
             except SkipTest as ex:
                 self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
-            self.assertFalse(td.exists())
 
-    def test_rmtree_can_wrap_exceptions(self):
-        """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
-        with _tmpdir_to_force_permission_error() as td:
-            # Access the module through sys.modules so it is unambiguous which module's
-            # attribute we patch: the original git.util, not git.index.util even though
-            # git.index.util "replaces" git.util and is what "import git.util" gives us.
-            with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True):
-                # Disable common chmod functions so the callback can't fix the problem.
-                with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"):
-                    # Now we can see how an intractable PermissionError is treated.
-                    with self.assertRaises(SkipTest):
-                        rmtree(td)
+class _Member:
+    """A member of an IterableList."""
 
-    @ddt.data(
-        (False, PermissionError, _tmpdir_to_force_permission_error),
-        (False, FileNotFoundError, _tmpdir_for_file_not_found),
-        (True, FileNotFoundError, _tmpdir_for_file_not_found),
-    )
-    def test_rmtree_does_not_wrap_unless_called_for(self, case):
-        """rmtree doesn't wrap non-PermissionError, nor if HIDE_WINDOWS_KNOWN_ERRORS is false."""
-        hide_windows_known_errors, exception_type, tmpdir_context_factory = case
-
-        with tmpdir_context_factory() as td:
-            # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here.
-            with mock.patch.object(
-                sys.modules["git.util"],
-                "HIDE_WINDOWS_KNOWN_ERRORS",
-                hide_windows_known_errors,
-            ):
-                with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"):
-                    with self.assertRaises(exception_type):
-                        try:
-                            rmtree(td)
-                        except SkipTest as ex:
-                            self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+    __slots__ = ("name",)
+
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return f"{type(self).__name__}({self.name!r})"
+
+
+@ddt.ddt
+class TestUtils(TestBase):
+    """Tests for utilities in :mod:`git.util` other than :func:`git.util.rmtree`."""
 
     @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS")
     def test_env_vars_for_windows_tests(self, name):

From c4da058ba99aafd16a01b0692b13bafb833969ac Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 09:43:41 -0400
Subject: [PATCH 248/994] Pull HIDE_WINDOWS_*_ERRORS tests out of TestUtils

And make them pure pytest tests. This creates the TestEnvParsing
class, which does not inherit from TestCase.

Because unlike @ddt.data (and @ddt.idata) @pytest.mark.parametrize
can be nested (applied multiple times, creating tests for all
combinations), that is used for the HIDE_WINDOWS_*_ERRORS tests
instead of subtests. Because there were not any other uses of
subtests in the test suite, the pytest-subtest plugin is
accordingly removed from test-requirements. It may be put back
anytime in the future.
---
 test-requirements.txt |  1 -
 test/test_util.py     | 82 +++++++++++++++++++++++++------------------
 2 files changed, 48 insertions(+), 35 deletions(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index 8cadfb836..7cfb977a1 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -8,5 +8,4 @@ pytest >= 7.3.1
 pytest-cov
 pytest-instafail
 pytest-mock
-pytest-subtests
 pytest-sugar
diff --git a/test/test_util.py b/test/test_util.py
index 784dc9bfc..c04f390d1 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -155,38 +155,26 @@ def test_does_not_wrap_other_errors(self, mocker, file_not_found_tmpdir, hide_wi
                 self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
 
-class _Member:
-    """A member of an IterableList."""
-
-    __slots__ = ("name",)
-
-    def __init__(self, name):
-        self.name = name
-
-    def __repr__(self):
-        return f"{type(self).__name__}({self.name!r})"
-
+class TestEnvParsing:
+    """Tests for environment variable parsing logic in :mod:`git.util`."""
+
+    @staticmethod
+    def _run_parse(name, value):
+        command = [
+            sys.executable,
+            "-c",
+            f"from git.util import {name}; print(repr({name}))",
+        ]
+        output = subprocess.check_output(
+            command,
+            env=None if value is None else dict(os.environ, **{name: value}),
+            text=True,
+        )
+        return ast.literal_eval(output)
 
-@ddt.ddt
-class TestUtils(TestBase):
-    """Tests for utilities in :mod:`git.util` other than :func:`git.util.rmtree`."""
-
-    @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS")
-    def test_env_vars_for_windows_tests(self, name):
-        def run_parse(value):
-            command = [
-                sys.executable,
-                "-c",
-                f"from git.util import {name}; print(repr({name}))",
-            ]
-            output = subprocess.check_output(
-                command,
-                env=None if value is None else dict(os.environ, **{name: value}),
-                text=True,
-            )
-            return ast.literal_eval(output)
-
-        for env_var_value, expected_truth_value in (
+    @pytest.mark.parametrize(
+        "env_var_value, expected_truth_value",
+        [
             (None, os.name == "nt"),  # True on Windows when the environment variable is unset.
             ("", False),
             (" ", False),
@@ -202,9 +190,35 @@ def run_parse(value):
             ("YES", os.name == "nt"),
             (" no  ", False),
             (" yes  ", os.name == "nt"),
-        ):
-            with self.subTest(env_var_value=env_var_value):
-                self.assertIs(run_parse(env_var_value), expected_truth_value)
+        ],
+    )
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "HIDE_WINDOWS_KNOWN_ERRORS",
+            "HIDE_WINDOWS_FREEZE_ERRORS",
+        ],
+    )
+    def test_env_vars_for_windows_tests(self, name, env_var_value, expected_truth_value):
+        actual_parsed_value = self._run_parse(name, env_var_value)
+        assert actual_parsed_value is expected_truth_value
+
+
+class _Member:
+    """A member of an IterableList."""
+
+    __slots__ = ("name",)
+
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return f"{type(self).__name__}({self.name!r})"
+
+
+@ddt.ddt
+class TestUtils(TestBase):
+    """Tests for most utilities in :mod:`git.util`."""
 
     _norm_cygpath_pairs = (
         (R"foo\bar", "foo/bar"),

From aa1799f0de449c50fb5e35ba0befb4ff2fc6f98c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 09:51:40 -0400
Subject: [PATCH 249/994] Remove file_not_found_tmpdir fixture for TestRmtree

Since its presence doesn't make things any simpler or more elegant.
(It was left over from a previous approach where it was used in
some @ddt parameters.)
---
 test/test_util.py | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index c04f390d1..a02ca0cce 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -59,12 +59,6 @@ def permission_error_tmpdir(tmp_path):
     yield td
 
 
-@pytest.fixture
-def file_not_found_tmpdir(tmp_path):
-    """Fixture to test errors deleting a directory that are not due to permissions."""
-    yield tmp_path / "testdir"  # It is deliberately never created.
-
-
 class TestRmtree:
     """Tests for :func:`git.util.rmtree`."""
 
@@ -142,7 +136,9 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
                 pytest.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
 
     @pytest.mark.parametrize("hide_windows_known_errors", [False, True])
-    def test_does_not_wrap_other_errors(self, mocker, file_not_found_tmpdir, hide_windows_known_errors):
+    def test_does_not_wrap_other_errors(self, tmp_path, mocker, hide_windows_known_errors):
+        file_not_found_tmpdir = tmp_path / "testdir"  # It is deliberately never created.
+
         # See comments in test_wraps_perm_error_if_enabled for details about patching.
         mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
         mocker.patch.object(os, "chmod")

From 2fb8c64a898e4c4bf93febadffa216c8a6ff2411 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 09:57:05 -0400
Subject: [PATCH 250/994] Move permission_error_tmpdir skips to test cases

This moves test skipping logic from the permission_error_tmpdir
fixture to the two TestRmtree test case methods that use it.

This produces some duplication, which is undesirable, but it makes
it so that which tests are skipped under what conditions is
immediately clear, easy to identify as skipping logic (which is
usually achieved by decoration), and clear in its relationship
to the skipping logic associated with other TestRmtree test cases.
---
 test/test_util.py | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index a02ca0cce..1147676bc 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -46,11 +46,6 @@
 @pytest.fixture
 def permission_error_tmpdir(tmp_path):
     """Fixture to test permissions errors situations where they are not overcome."""
-    if sys.platform == "cygwin":
-        raise SkipTest("Cygwin can't set the permissions that make the test meaningful.")
-    if sys.version_info < (3, 8):
-        raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.")
-
     td = tmp_path / "testdir"
     td.mkdir()
     (td / "x").write_bytes(b"")
@@ -107,6 +102,14 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
 
         assert not td.exists()
 
+    @pytest.mark.skipif(
+        sys.platform == "cygwin",
+        reason="Cygwin can't set the permissions that make the test meaningful.",
+    )
+    @pytest.mark.skipif(
+        sys.version_info < (3, 8),
+        reason="In 3.7, TemporaryDirectory doesn't clean up after weird permissions.",
+    )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
         # Access the module through sys.modules so it is unambiguous which module's
@@ -122,6 +125,14 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         with pytest.raises(SkipTest):
             rmtree(permission_error_tmpdir)
 
+    @pytest.mark.skipif(
+        sys.platform == "cygwin",
+        reason="Cygwin can't set the permissions that make the test meaningful.",
+    )
+    @pytest.mark.skipif(
+        sys.version_info < (3, 8),
+        reason="In 3.7, TemporaryDirectory doesn't clean up after weird permissions.",
+    )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir):
         """rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false."""
         # See comments in test_wraps_perm_error_if_enabled for details about patching.

From 1dccb8e3a90ad2227d0689171526212cd5740451 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 24 Oct 2023 11:16:31 -0400
Subject: [PATCH 251/994] Pull cygpath and decygpath tests out of TestUtils

This turns them into pure pytest tests, in a new class TestCygpath.
---
 test/test_util.py | 98 ++++++++++++++++++++++++-----------------------
 1 file changed, 51 insertions(+), 47 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 1147676bc..f5d716d4c 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -14,7 +14,7 @@
 import sys
 import tempfile
 import time
-from unittest import SkipTest, mock, skipUnless
+from unittest import SkipTest, mock
 
 import ddt
 import pytest
@@ -45,7 +45,7 @@
 
 @pytest.fixture
 def permission_error_tmpdir(tmp_path):
-    """Fixture to test permissions errors situations where they are not overcome."""
+    """Fixture to test permissions errors in situations where they are not overcome."""
     td = tmp_path / "testdir"
     td.mkdir()
     (td / "x").write_bytes(b"")
@@ -211,21 +211,9 @@ def test_env_vars_for_windows_tests(self, name, env_var_value, expected_truth_va
         assert actual_parsed_value is expected_truth_value
 
 
-class _Member:
-    """A member of an IterableList."""
-
-    __slots__ = ("name",)
-
-    def __init__(self, name):
-        self.name = name
-
-    def __repr__(self):
-        return f"{type(self).__name__}({self.name!r})"
-
-
-@ddt.ddt
-class TestUtils(TestBase):
-    """Tests for most utilities in :mod:`git.util`."""
+@pytest.mark.skipif(sys.platform != "cygwin", reason="Paths specifically for Cygwin.")
+class TestCygpath:
+    """Tests for :func:`git.util.cygpath` and :func:`git.util.decygpath`."""
 
     _norm_cygpath_pairs = (
         (R"foo\bar", "foo/bar"),
@@ -248,54 +236,70 @@ class TestUtils(TestBase):
         (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
     )
 
-    # FIXME: Mark only the /proc-prefixing cases xfail, somehow (or fix them).
+    # FIXME: Mark only the /proc-prefixing cases xfail (or fix them).
     @pytest.mark.xfail(
         reason="Many return paths prefixed /proc/cygdrive instead.",
         raises=AssertionError,
     )
-    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
-    @ddt.idata(_norm_cygpath_pairs + _unc_cygpath_pairs)
-    def test_cygpath_ok(self, case):
-        wpath, cpath = case
+    @pytest.mark.parametrize("wpath, cpath", _norm_cygpath_pairs + _unc_cygpath_pairs)
+    def test_cygpath_ok(self, wpath, cpath):
         cwpath = cygpath(wpath)
-        self.assertEqual(cwpath, cpath, wpath)
+        assert cwpath == cpath, wpath
 
     @pytest.mark.xfail(
         reason=R'2nd example r".\bar" -> "bar" fails, returns "./bar"',
         raises=AssertionError,
     )
-    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
-    @ddt.data(
-        (R"./bar", "bar"),
-        (R".\bar", "bar"),  # FIXME: Mark only this one xfail, somehow (or fix it).
-        (R"../bar", "../bar"),
-        (R"..\bar", "../bar"),
-        (R"../bar/.\foo/../chu", "../bar/chu"),
+    @pytest.mark.parametrize(
+        "wpath, cpath",
+        [
+            (R"./bar", "bar"),
+            (R".\bar", "bar"),  # FIXME: Mark only this one xfail (or fix it).
+            (R"../bar", "../bar"),
+            (R"..\bar", "../bar"),
+            (R"../bar/.\foo/../chu", "../bar/chu"),
+        ],
     )
-    def test_cygpath_norm_ok(self, case):
-        wpath, cpath = case
+    def test_cygpath_norm_ok(self, wpath, cpath):
         cwpath = cygpath(wpath)
-        self.assertEqual(cwpath, cpath or wpath, wpath)
+        assert cwpath == (cpath or wpath), wpath
 
-    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
-    @ddt.data(
-        R"C:",
-        R"C:Relative",
-        R"D:Apps\123",
-        R"D:Apps/123",
-        R"\\?\a:rel",
-        R"\\share\a:rel",
+    @pytest.mark.parametrize(
+        "wpath",
+        [
+            R"C:",
+            R"C:Relative",
+            R"D:Apps\123",
+            R"D:Apps/123",
+            R"\\?\a:rel",
+            R"\\share\a:rel",
+        ],
     )
     def test_cygpath_invalids(self, wpath):
         cwpath = cygpath(wpath)
-        self.assertEqual(cwpath, wpath.replace("\\", "/"), wpath)
+        assert cwpath == wpath.replace("\\", "/"), wpath
 
-    @skipUnless(sys.platform == "cygwin", "Paths specifically for Cygwin.")
-    @ddt.idata(_norm_cygpath_pairs)
-    def test_decygpath(self, case):
-        wpath, cpath = case
+    @pytest.mark.parametrize("wpath, cpath", _norm_cygpath_pairs)
+    def test_decygpath(self, wpath, cpath):
         wcpath = decygpath(cpath)
-        self.assertEqual(wcpath, wpath.replace("/", "\\"), cpath)
+        assert wcpath == wpath.replace("/", "\\"), cpath
+
+
+class _Member:
+    """A member of an IterableList."""
+
+    __slots__ = ("name",)
+
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return f"{type(self).__name__}({self.name!r})"
+
+
+@ddt.ddt
+class TestUtils(TestBase):
+    """Tests for most utilities in :mod:`git.util`."""
 
     def test_it_should_dashify(self):
         self.assertEqual("this-is-my-argument", dashify("this_is_my_argument"))

From 487bc8aa71ebfdfa455c4037d0bbcfd51eb4997c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 25 Oct 2023 06:27:35 -0400
Subject: [PATCH 252/994] Start making TestCygpath xfail markings granular

This marks only the one really expected failure case in
in test_cygpath_norm_ok as xfail, preventing the others from
giving "XPASS" results each time the tests are run.
---
 test/test_util.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index f5d716d4c..39dbe5e56 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -211,6 +211,11 @@ def test_env_vars_for_windows_tests(self, name, env_var_value, expected_truth_va
         assert actual_parsed_value is expected_truth_value
 
 
+def _xfail_param(*values, **xfail_kwargs):
+    """Build a pytest.mark.parametrize parameter that carries an xfail mark."""
+    return pytest.param(*values, marks=pytest.mark.xfail(**xfail_kwargs))
+
+
 @pytest.mark.skipif(sys.platform != "cygwin", reason="Paths specifically for Cygwin.")
 class TestCygpath:
     """Tests for :func:`git.util.cygpath` and :func:`git.util.decygpath`."""
@@ -246,15 +251,11 @@ def test_cygpath_ok(self, wpath, cpath):
         cwpath = cygpath(wpath)
         assert cwpath == cpath, wpath
 
-    @pytest.mark.xfail(
-        reason=R'2nd example r".\bar" -> "bar" fails, returns "./bar"',
-        raises=AssertionError,
-    )
     @pytest.mark.parametrize(
         "wpath, cpath",
         [
             (R"./bar", "bar"),
-            (R".\bar", "bar"),  # FIXME: Mark only this one xfail (or fix it).
+            _xfail_param(R".\bar", "bar", reason=R'Returns: "./bar"', raises=AssertionError),
             (R"../bar", "../bar"),
             (R"..\bar", "../bar"),
             (R"../bar/.\foo/../chu", "../bar/chu"),

From c3d3d1ecd3ce8a2ab547a7f1d94590a676f36d18 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 25 Oct 2023 10:24:44 -0400
Subject: [PATCH 253/994] Finish making TestCygpath xfail markings granular

This marks only the (many, but not all) really expected failure
cases in test_cygpath_ok as xfail, preventing the others from
giving "XPASS" results each time the tests are run.

This finishes making the xfail markings in TestCygpath granular, in
the sense that now there should be no "XPASS" results. However, the
approach taken here might still benefit from future reorganization
or refactoring.
---
 test/test_util.py | 78 ++++++++++++++++++++++++++++++-----------------
 1 file changed, 50 insertions(+), 28 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 39dbe5e56..a12d17db3 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -216,37 +216,59 @@ def _xfail_param(*values, **xfail_kwargs):
     return pytest.param(*values, marks=pytest.mark.xfail(**xfail_kwargs))
 
 
-@pytest.mark.skipif(sys.platform != "cygwin", reason="Paths specifically for Cygwin.")
-class TestCygpath:
-    """Tests for :func:`git.util.cygpath` and :func:`git.util.decygpath`."""
+_norm_cygpath_pairs = (
+    (R"foo\bar", "foo/bar"),
+    (R"foo/bar", "foo/bar"),
+    (R"C:\Users", "/cygdrive/c/Users"),
+    (R"C:\d/e", "/cygdrive/c/d/e"),
+    ("C:\\", "/cygdrive/c/"),
+    (R"\\server\C$\Users", "//server/C$/Users"),
+    (R"\\server\C$", "//server/C$"),
+    ("\\\\server\\c$\\", "//server/c$/"),
+    (R"\\server\BAR/", "//server/BAR/"),
+    (R"D:/Apps", "/cygdrive/d/Apps"),
+    (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"),
+    (R"D:\Apps/123", "/cygdrive/d/Apps/123"),
+)
 
-    _norm_cygpath_pairs = (
-        (R"foo\bar", "foo/bar"),
-        (R"foo/bar", "foo/bar"),
-        (R"C:\Users", "/cygdrive/c/Users"),
-        (R"C:\d/e", "/cygdrive/c/d/e"),
-        ("C:\\", "/cygdrive/c/"),
-        (R"\\server\C$\Users", "//server/C$/Users"),
-        (R"\\server\C$", "//server/C$"),
-        ("\\\\server\\c$\\", "//server/c$/"),
-        (R"\\server\BAR/", "//server/BAR/"),
-        (R"D:/Apps", "/cygdrive/d/Apps"),
-        (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"),
-        (R"D:\Apps/123", "/cygdrive/d/Apps/123"),
-    )
+_unc_cygpath_pairs = (
+    (R"\\?\a:\com", "/cygdrive/a/com"),
+    (R"\\?\a:/com", "/cygdrive/a/com"),
+    (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
+)
 
-    _unc_cygpath_pairs = (
-        (R"\\?\a:\com", "/cygdrive/a/com"),
-        (R"\\?\a:/com", "/cygdrive/a/com"),
-        (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
+# Mapping of expected failures for the test_cygpath_ok test.
+_cygpath_ok_xfails = {
+    # From _norm_cygpath_pairs:
+    (R"C:\Users", "/cygdrive/c/Users"): "/proc/cygdrive/c/Users",
+    (R"C:\d/e", "/cygdrive/c/d/e"): "/proc/cygdrive/c/d/e",
+    ("C:\\", "/cygdrive/c/"): "/proc/cygdrive/c/",
+    (R"\\server\BAR/", "//server/BAR/"): "//server/BAR",
+    (R"D:/Apps", "/cygdrive/d/Apps"): "/proc/cygdrive/d/Apps",
+    (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"): "/proc/cygdrive/d/Apps/fOO",
+    (R"D:\Apps/123", "/cygdrive/d/Apps/123"): "/proc/cygdrive/d/Apps/123",
+    # From _unc_cygpath_pairs:
+    (R"\\?\a:\com", "/cygdrive/a/com"): "/proc/cygdrive/a/com",
+    (R"\\?\a:/com", "/cygdrive/a/com"): "/proc/cygdrive/a/com",
+}
+
+
+# Parameter sets for the test_cygpath_ok test.
+_cygpath_ok_params = [
+    (
+        _xfail_param(*case, reason=f"Returns: {_cygpath_ok_xfails[case]!r}", raises=AssertionError)
+        if case in _cygpath_ok_xfails
+        else case
     )
+    for case in _norm_cygpath_pairs + _unc_cygpath_pairs
+]
 
-    # FIXME: Mark only the /proc-prefixing cases xfail (or fix them).
-    @pytest.mark.xfail(
-        reason="Many return paths prefixed /proc/cygdrive instead.",
-        raises=AssertionError,
-    )
-    @pytest.mark.parametrize("wpath, cpath", _norm_cygpath_pairs + _unc_cygpath_pairs)
+
+@pytest.mark.skipif(sys.platform != "cygwin", reason="Paths specifically for Cygwin.")
+class TestCygpath:
+    """Tests for :func:`git.util.cygpath` and :func:`git.util.decygpath`."""
+
+    @pytest.mark.parametrize("wpath, cpath", _cygpath_ok_params)
     def test_cygpath_ok(self, wpath, cpath):
         cwpath = cygpath(wpath)
         assert cwpath == cpath, wpath
@@ -255,7 +277,7 @@ def test_cygpath_ok(self, wpath, cpath):
         "wpath, cpath",
         [
             (R"./bar", "bar"),
-            _xfail_param(R".\bar", "bar", reason=R'Returns: "./bar"', raises=AssertionError),
+            _xfail_param(R".\bar", "bar", reason="Returns: './bar'", raises=AssertionError),
             (R"../bar", "../bar"),
             (R"..\bar", "../bar"),
             (R"../bar/.\foo/../chu", "../bar/chu"),

From 7d98f94a60b6108709fc1155cd23c3224880ec94 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 27 Oct 2023 01:10:53 -0400
Subject: [PATCH 254/994] Let all TestRmtree tests run on 3.7

They are no longer using TemporaryDirectory (because they use the
pytest tmp_path fixture instead), so the limitations of
TemporaryDirectory on Python 3.7 are no longer relevant.
---
 test/test_util.py | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index a12d17db3..55f87ed9b 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -106,10 +106,6 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
         sys.platform == "cygwin",
         reason="Cygwin can't set the permissions that make the test meaningful.",
     )
-    @pytest.mark.skipif(
-        sys.version_info < (3, 8),
-        reason="In 3.7, TemporaryDirectory doesn't clean up after weird permissions.",
-    )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
         # Access the module through sys.modules so it is unambiguous which module's
@@ -129,10 +125,6 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         sys.platform == "cygwin",
         reason="Cygwin can't set the permissions that make the test meaningful.",
     )
-    @pytest.mark.skipif(
-        sys.version_info < (3, 8),
-        reason="In 3.7, TemporaryDirectory doesn't clean up after weird permissions.",
-    )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir):
         """rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false."""
         # See comments in test_wraps_perm_error_if_enabled for details about patching.

From ba5bc45b3a570ef628dba97128ce4bfe9856d736 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 29 Oct 2023 03:34:03 -0400
Subject: [PATCH 255/994] Better explain the Windows and Unix cases

Of PermissionError, in the rmtree tests.
---
 test/test_util.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 55f87ed9b..d345247b1 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -49,8 +49,13 @@ def permission_error_tmpdir(tmp_path):
     td = tmp_path / "testdir"
     td.mkdir()
     (td / "x").write_bytes(b"")
-    (td / "x").chmod(stat.S_IRUSR)  # Set up PermissionError on Windows.
-    td.chmod(stat.S_IRUSR | stat.S_IXUSR)  # Set up PermissionError on Unix.
+
+    # Set up PermissionError on Windows, where we can't delete read-only files.
+    (td / "x").chmod(stat.S_IRUSR)
+
+    # Set up PermissionError on Unix, where we can't delete files in read-only directories.
+    td.chmod(stat.S_IRUSR | stat.S_IXUSR)
+
     yield td
 
 
@@ -113,7 +118,7 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         # git.index.util "replaces" git.util and is what "import git.util" gives us.
         mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True)
 
-        # Disable common chmod functions so the callback can't fix the problem.
+        # Disable common chmod functions so the callback can never fix the problem.
         mocker.patch.object(os, "chmod")
         mocker.patch.object(pathlib.Path, "chmod")
 

From 454032cf5b783bca7d1e8b572e8a8944a5ef2036 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 3 Nov 2023 12:51:29 -0400
Subject: [PATCH 256/994] Make comments more consistent and clarify license

This improves the consistency of top-of-module comments as follows:

- All names of the current file are removed. Some included these
  while others didn't. In general, this can be useful information,
  which can remind readers and developers of what the file is and
  may even reduce mistakes. However, in GitPython, many modules
  inside git/ have the same name as other modules in other
  subdirectories of git/. So the presence of filenames would
  often be the same for multiple files, a condition that would be
  intensified if consistency were achieved by adding them
  everywhere. This instead removes them, which should (albeit
  slightly) decrease the risk of confusing modules that have the
  same name as each other.

- All modules (.py files) inside git/ and test/, except for .py
  files that are entirely empty (without even comments) or are
  inside test/fixtures/, now have comments indicating the license
  and linking to it on opensource.org. Previously, some modules
  had this, while others did not.

The comment about the license is short, and does not contain an
explicit copyright statement. No new explicit copyright statements
are added, but some top-of-modules already contained them, and this
does not remove (nor update or otherwise modify) them.

Although explicit copyright statements are not touched, all the
license comments are modified, including where they had previously
appeared, to say "the 3-Clause BSD License" instead of
"the BSD License", since there is no specific license known as the
"BSD License" (and both the 2-clause and 3-clause BSD licenses are
very popular).

This change should not be confused with #1662, which fixed an
originally correct hyperlink that had come to redirect to a page
about a different license. The change here does not change the link
again. It makes the commented wording more specific, so that it is
clear, even without looking at the link, which BSD license is being
referred to.
---
 git/__init__.py                   | 5 ++---
 git/cmd.py                        | 5 ++---
 git/compat.py                     | 5 ++---
 git/config.py                     | 5 ++---
 git/db.py                         | 3 +++
 git/diff.py                       | 5 ++---
 git/exc.py                        | 5 ++---
 git/index/__init__.py             | 3 +++
 git/index/base.py                 | 5 ++---
 git/index/fun.py                  | 3 +++
 git/index/typ.py                  | 3 +++
 git/index/util.py                 | 3 +++
 git/objects/__init__.py           | 3 +++
 git/objects/base.py               | 5 ++---
 git/objects/blob.py               | 5 ++---
 git/objects/commit.py             | 5 ++---
 git/objects/fun.py                | 3 +++
 git/objects/submodule/__init__.py | 3 +++
 git/objects/submodule/base.py     | 3 +++
 git/objects/submodule/root.py     | 3 +++
 git/objects/submodule/util.py     | 3 +++
 git/objects/tag.py                | 5 ++---
 git/objects/tree.py               | 5 ++---
 git/objects/util.py               | 5 ++---
 git/refs/__init__.py              | 4 ++++
 git/refs/head.py                  | 3 +++
 git/refs/log.py                   | 3 +++
 git/refs/reference.py             | 3 +++
 git/refs/remote.py                | 3 +++
 git/refs/symbolic.py              | 3 +++
 git/refs/tag.py                   | 3 +++
 git/remote.py                     | 5 ++---
 git/repo/__init__.py              | 3 +++
 git/repo/base.py                  | 5 ++---
 git/repo/fun.py                   | 3 +++
 git/types.py                      | 4 ++--
 git/util.py                       | 5 ++---
 test/__init__.py                  | 5 ++---
 test/lib/__init__.py              | 5 ++---
 test/lib/helper.py                | 5 ++---
 test/performance/lib.py           | 3 +++
 test/performance/test_commit.py   | 4 ++--
 test/performance/test_odb.py      | 3 +++
 test/performance/test_streams.py  | 3 +++
 test/test_actor.py                | 5 ++---
 test/test_base.py                 | 5 ++---
 test/test_blob.py                 | 5 ++---
 test/test_blob_filter.py          | 3 +++
 test/test_clone.py                | 4 ++--
 test/test_commit.py               | 5 ++---
 test/test_config.py               | 5 ++---
 test/test_db.py                   | 5 ++---
 test/test_diff.py                 | 5 ++---
 test/test_docs.py                 | 5 ++---
 test/test_exc.py                  | 5 ++---
 test/test_fun.py                  | 3 +++
 test/test_git.py                  | 5 ++---
 test/test_index.py                | 5 ++---
 test/test_installation.py         | 4 ++--
 test/test_quick_doc.py            | 3 +++
 test/test_reflog.py               | 3 +++
 test/test_refs.py                 | 5 ++---
 test/test_remote.py               | 5 ++---
 test/test_repo.py                 | 6 ++----
 test/test_stats.py                | 5 ++---
 test/test_submodule.py            | 4 ++--
 test/test_tree.py                 | 5 ++---
 test/test_util.py                 | 5 ++---
 test/tstrunner.py                 | 3 +++
 69 files changed, 167 insertions(+), 119 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 46d54a960..defc679cb 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -1,8 +1,7 @@
-# __init__.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 # flake8: noqa
 # @PydevCodeAnalysisIgnore
diff --git a/git/cmd.py b/git/cmd.py
index fd39a8eeb..c7c84c360 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1,8 +1,7 @@
-# cmd.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from __future__ import annotations
 
diff --git a/git/compat.py b/git/compat.py
index f17e52f7b..9a2116d44 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -1,8 +1,7 @@
-# compat.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Utilities to help provide compatibility with Python 3."""
 
diff --git a/git/config.py b/git/config.py
index 2cb057021..2d207363f 100644
--- a/git/config.py
+++ b/git/config.py
@@ -1,8 +1,7 @@
-# config.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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/db.py b/git/db.py
index 9e278ea75..03b631084 100644
--- a/git/db.py
+++ b/git/db.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module with our own gitdb implementation - it uses the git command."""
 
 from git.util import bin_to_hex, hex_to_bin
diff --git a/git/diff.py b/git/diff.py
index 275534bbf..25334512a 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -1,8 +1,7 @@
-# diff.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 124c5eeea..35008c29a 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -1,8 +1,7 @@
-# exc.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Module containing all exceptions thrown throughout the git package."""
 
diff --git a/git/index/__init__.py b/git/index/__init__.py
index f9a534ee7..9954b9e88 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Initialize the index package."""
 
 # flake8: noqa
diff --git a/git/index/base.py b/git/index/base.py
index c2333a2c2..dbfa2c9bf 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1,8 +1,7 @@
-# base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from contextlib import ExitStack
 import datetime
diff --git a/git/index/fun.py b/git/index/fun.py
index a35990d6d..5dac62a63 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 # Standalone functions to accompany the index implementation and make it more versatile.
 # NOTE: Autodoc hates it if this is a docstring.
 
diff --git a/git/index/typ.py b/git/index/typ.py
index 9f57f067b..7011fd03d 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module with additional types used by the index."""
 
 from binascii import b2a_hex
diff --git a/git/index/util.py b/git/index/util.py
index 08e49d860..2c558e9d9 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module containing index utilities."""
 
 from functools import wraps
diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 2a4a114c7..9ca430285 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Import all submodules' main classes into the package space."""
 
 # flake8: noqa
diff --git a/git/objects/base.py b/git/objects/base.py
index 9f188a955..934fb40bc 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -1,8 +1,7 @@
-# base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 f0d3181c2..6d7e859af 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -1,8 +1,7 @@
-# blob.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 04acb668b..7310d66b0 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -1,8 +1,7 @@
-# commit.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import datetime
 import re
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 7756154be..6d8a23d35 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module with functions which are supposed to be as fast as possible."""
 
 from stat import S_ISDIR
diff --git a/git/objects/submodule/__init__.py b/git/objects/submodule/__init__.py
index 8edc13be4..b11b568f2 100644
--- a/git/objects/submodule/__init__.py
+++ b/git/objects/submodule/__init__.py
@@ -1,2 +1,5 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 # NOTE: Cannot import anything here as the top-level __init__ has to handle
 # our dependencies.
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 1516306ec..25c2d0fef 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from io import BytesIO
 import logging
 import os
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index cfcbb4cb7..d9d9f6d24 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from .base import Submodule, UpdateProgress
 from .util import find_first_remote_branch
 from git.exc import InvalidGitRepositoryError
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index 3fc0b0b56..f8265798d 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 import git
 from git.exc import InvalidGitRepositoryError
 from git.config import GitConfigParser
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 6eb1c8d90..500879d54 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -1,8 +1,7 @@
-# tag.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Module containing all Object-based types."""
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 1be6f193e..4d94a5d24 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -1,8 +1,7 @@
-# tree.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 7af7fa0e5..9f42227d0 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -1,8 +1,7 @@
-# util.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Module for general utility functions."""
 
diff --git a/git/refs/__init__.py b/git/refs/__init__.py
index 18ea2013c..3a82b9796 100644
--- a/git/refs/__init__.py
+++ b/git/refs/__init__.py
@@ -1,5 +1,9 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 # flake8: noqa
 # Import all modules in order, fix the names they require.
+
 from .symbolic import *
 from .reference import *
 from .head import *
diff --git a/git/refs/head.py b/git/refs/head.py
index fa40943c6..fba195aaa 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from git.config import GitConfigParser, SectionConstraint
 from git.util import join_path
 from git.exc import GitCommandError
diff --git a/git/refs/log.py b/git/refs/log.py
index ebdaf04d1..aeebac48c 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from mmap import mmap
 import re
 import time as _time
diff --git a/git/refs/reference.py b/git/refs/reference.py
index f0eb6bfaa..c2ad13bd6 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from git.util import (
     LazyMixin,
     IterableObj,
diff --git a/git/refs/remote.py b/git/refs/remote.py
index f26ee08fc..dd4117fa7 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 import os
 
 from git.util import join_path
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 99a60201f..84c2057e1 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from git.types import PathLike
 import os
 
diff --git a/git/refs/tag.py b/git/refs/tag.py
index d00adc121..a59a51337 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from .reference import Reference
 
 __all__ = ["TagReference", "Tag"]
diff --git a/git/remote.py b/git/remote.py
index ccf70a25c..4055dba2e 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -1,8 +1,7 @@
-# remote.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Module implementing a remote object allowing easy access to git remotes."""
 
diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index f1eac3311..c01a1e034 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Initialize the Repo package."""
 
 # flake8: noqa
diff --git a/git/repo/base.py b/git/repo/base.py
index 4790ea4e7..ebe72d1e7 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1,8 +1,7 @@
-# base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from __future__ import annotations
 
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 29a899ea8..ee831332f 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Module with general repository-related functions."""
 
 from __future__ import annotations
diff --git a/git/types.py b/git/types.py
index 2709bbf34..6f2b7c513 100644
--- a/git/types.py
+++ b/git/types.py
@@ -1,5 +1,5 @@
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 # flake8: noqa
 
diff --git a/git/util.py b/git/util.py
index bd1fbe247..e5d03d157 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1,8 +1,7 @@
-# util.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from abc import abstractmethod
 import contextlib
diff --git a/test/__init__.py b/test/__init__.py
index a3d514523..fbaebcd3b 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,5 +1,4 @@
-# __init__.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
diff --git a/test/lib/__init__.py b/test/lib/__init__.py
index 299317c0b..cc1e48483 100644
--- a/test/lib/__init__.py
+++ b/test/lib/__init__.py
@@ -1,8 +1,7 @@
-# __init__.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 8725cd13f..387686327 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -1,8 +1,7 @@
-# helper.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import contextlib
 from functools import wraps
diff --git a/test/performance/lib.py b/test/performance/lib.py
index 2b2a632d9..ceee6c2a1 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Support library for tests."""
 
 import logging
diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py
index fad0641be..9e136a6c1 100644
--- a/test/performance/test_commit.py
+++ b/test/performance/test_commit.py
@@ -1,7 +1,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: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 """Performance tests for commits (iteration, traversal, and serialization)."""
 
diff --git a/test/performance/test_odb.py b/test/performance/test_odb.py
index 70934ad6b..00e245fb7 100644
--- a/test/performance/test_odb.py
+++ b/test/performance/test_odb.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Performance tests for object store."""
 
 import sys
diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py
index 619126921..9ee7cf5e2 100644
--- a/test/performance/test_streams.py
+++ b/test/performance/test_streams.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Performance tests for data streaming."""
 
 import os
diff --git a/test/test_actor.py b/test/test_actor.py
index 80b93d7bc..caf095739 100644
--- a/test/test_actor.py
+++ b/test/test_actor.py
@@ -1,8 +1,7 @@
-# test_actor.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 e4704c7d8..725562c10 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -1,8 +1,7 @@
-# test_base.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import os
 import sys
diff --git a/test/test_blob.py b/test/test_blob.py
index 692522b52..ff59c67ea 100644
--- a/test/test_blob.py
+++ b/test/test_blob.py
@@ -1,8 +1,7 @@
-# test_blob.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from test.lib import TestBase
 from git import Blob
diff --git a/test/test_blob_filter.py b/test/test_blob_filter.py
index ad4f0e7ff..5cc6b48c9 100644
--- a/test/test_blob_filter.py
+++ b/test/test_blob_filter.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Test the blob filter."""
 
 from pathlib import Path
diff --git a/test/test_clone.py b/test/test_clone.py
index 7624b317b..dcab7ad6f 100644
--- a/test/test_clone.py
+++ b/test/test_clone.py
@@ -1,5 +1,5 @@
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 1327616ed..b6fb09aef 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -1,8 +1,7 @@
-# test_commit.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import copy
 from datetime import datetime
diff --git a/test/test_config.py b/test/test_config.py
index 63fbc61e6..0e1bba08a 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -1,8 +1,7 @@
-# test_config.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 d59aa6cc0..de093cbd8 100644
--- a/test/test_db.py
+++ b/test/test_db.py
@@ -1,8 +1,7 @@
-# test_db.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from git.db import GitCmdObjectDB
 from git.exc import BadObject
diff --git a/test/test_diff.py b/test/test_diff.py
index 50b96efff..1678e737d 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -1,8 +1,7 @@
-# test_diff.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ddt
 import shutil
diff --git a/test/test_docs.py b/test/test_docs.py
index 394b58b5f..2f4b2e8d8 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -1,8 +1,7 @@
-# test_docs.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 ad43695b3..62bb4fb6e 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -1,8 +1,7 @@
-# test_exc.py
 # 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: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 
 import re
diff --git a/test/test_fun.py b/test/test_fun.py
index 0015b30c6..566bc9aae 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from io import BytesIO
 from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR
 from os import stat
diff --git a/test/test_git.py b/test/test_git.py
index 06f8f5c97..c2fdf8feb 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -1,8 +1,7 @@
-# test_git.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import inspect
 import logging
diff --git a/test/test_index.py b/test/test_index.py
index 3357dc880..3e9e6124d 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1,8 +1,7 @@
-# test_index.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from io import BytesIO
 import os
diff --git a/test/test_installation.py b/test/test_installation.py
index e7774d29d..8f8c1adf2 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: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ast
 import os
diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py
index 13b587bd5..504dca237 100644
--- a/test/test_quick_doc.py
+++ b/test/test_quick_doc.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 from test.lib import TestBase
 from test.lib.helper import with_rw_directory
 
diff --git a/test/test_reflog.py b/test/test_reflog.py
index d5173d2f4..625466d40 100644
--- a/test/test_reflog.py
+++ b/test/test_reflog.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 import os
 import tempfile
 
diff --git a/test/test_refs.py b/test/test_refs.py
index ae07ce421..6ee385007 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -1,8 +1,7 @@
-# test_refs.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 8205c0bcd..12de18476 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -1,8 +1,7 @@
-# test_remote.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 1ba85acf9..e77bf2503 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -1,9 +1,7 @@
-# test_repo.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
-
+# This module is part of GitPython and is released under the
+# 3-Clause 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 335ce483b..4efb6f313 100644
--- a/test/test_stats.py
+++ b/test/test_stats.py
@@ -1,8 +1,7 @@
-# test_stats.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause 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 f63db1495..f92c0e4a4 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -1,5 +1,5 @@
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import contextlib
 import os
diff --git a/test/test_tree.py b/test/test_tree.py
index 5fc98e40c..7713413a6 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -1,8 +1,7 @@
-# test_tree.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from io import BytesIO
 
diff --git a/test/test_util.py b/test/test_util.py
index d345247b1..4fb30f77f 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -1,8 +1,7 @@
-# test_util.py
 # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 #
-# This module is part of GitPython and is released under
-# the BSD License: https://opensource.org/license/bsd-3-clause/
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ast
 from datetime import datetime
diff --git a/test/tstrunner.py b/test/tstrunner.py
index 8613538eb..fc9a59c8c 100644
--- a/test/tstrunner.py
+++ b/test/tstrunner.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Hook for MonkeyType (see PR #1188)."""
 
 import unittest

From 7387dab1e5a7e93187674c8ea9b126210fd2f535 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 3 Nov 2023 13:15:52 -0400
Subject: [PATCH 257/994] Update README to clarify license name

The license in README.md was named as "New BSD License", which is
in practice unambiguous, but no longer as readily recognized as is
"3-Clause BSD License". This updates the wording to call it by the
latter name, for that reason and for consistency with the previous
commit's changes to top-of-module comments, while also noting
parenthetically that it is also called the New BSD License.

The main reason for retaining "New BSD License" parenthetically is
to prevent anyone from being confused into thinking this change is
in any way associated with a change to the license, or that any
license change has occurred or is forthcoming. (This does *not*
change how GitPython is actually licensed in any way.)
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 889ea635f..0e020a5fe 100644
--- a/README.md
+++ b/README.md
@@ -289,7 +289,7 @@ gpg --edit-key 4C08421980C9
 
 ### LICENSE
 
-[New BSD License](https://opensource.org/license/bsd-3-clause/). See the [LICENSE file][license].
+[3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
 
 [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
 [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE

From 209162a4c53afe65054dd7ecdd06774e6196861f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 3 Nov 2023 13:51:14 -0400
Subject: [PATCH 258/994] Pass more specific license argument in setup.py

This uses "BSD-3-Clause" instead of "BSD" as the "license" metadata
in setup.py. "BSD-3-Clause" is the SPDX license identifier for the
BSD 3-Clause "New" or "Revised" License (the license GitPython
uses): https://spdx.org/licenses/BSD-3-Clause.html

There is no requirement to use an SPDX license identifier here,
but it is one of the common approaches, and it has the advantage of
making unambiguously clear, when a package is published on PyPI,
exactly what license it uses.

In contrast, the license-related *classifier* is unchanged, since
no more specific classfifier than what is in use now is currently
available.

The combination should result in

    License: BSD License (BSD-3-Clause)

being shown under "Meta" on PyPI, as of the next PyPI release. This
can be seen in other projects that use this combination of license
keyword argument and license-related classifier, such as:
https://pypi.org/project/flask-restx/
---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 90df8d7ea..f40f7280c 100755
--- a/setup.py
+++ b/setup.py
@@ -68,7 +68,7 @@ def _stamp_version(filename: str) -> None:
     description="GitPython is a Python library used to interact with Git repositories",
     author="Sebastian Thiel, Michael Trier",
     author_email="byronimo@gmail.com, mtrier@gmail.com",
-    license="BSD",
+    license="BSD-3-Clause",
     url="https://github.com/gitpython-developers/GitPython",
     packages=find_packages(exclude=["test", "test.*"]),
     include_package_data=True,

From c01fe76f2351d06671296bffcd98a3f50543d785 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 29 Oct 2023 16:34:23 -0400
Subject: [PATCH 259/994] Deprecate compat.is_<platform>, rewriting all uses

Major changes:

- Add a comment in the git.compat module above the definitions of
  is_win, is_posix, and is_darwin stating that they are deprecated
  and recommending that os.name (or, where applicable,
  sys.platform) be used directly instead for clarity (and sometimes
  accuracy).

- Remove all uses of is_win and is_posix in git/ and test/,
  replacing them with `os.name == "nt"` and `os.name == "posix"`,
  respectively.

There were no uses of is_darwin to be replaced. Although it had
been used at one time, the last reference to it appears to have
been removed in 4545762 (#1295).

This doesn't emit a DeprecationWarning when those attributes are
accessed in the git.compat module. (That might be valuable thing to
do in the future, if the git.compat module is to remain
non-deprecated overall.)

Two related less consequential changes are also included:

- Improve ordering and grouping of imports, in modules where they
  were already being changed as a result of no longer needing to
  import is_<platform> (usually is_win) from git.compat.

- Make minor revisions to a few comments, for readability. (This is
  in addition to somewhat more substantial revisions of comments
  where rewording was related to replacing uses of is_<platform>.)
---
 .github/workflows/cygwin-test.yml   |  2 +-
 .github/workflows/pythonpackage.yml |  2 +-
 git/cmd.py                          | 64 ++++++++++++++---------------
 git/compat.py                       | 11 +++++
 git/config.py                       | 19 +++------
 git/index/fun.py                    | 22 ++++------
 git/index/util.py                   |  8 +---
 git/objects/submodule/base.py       |  7 ++--
 git/repo/base.py                    | 22 ++++------
 git/util.py                         | 36 ++++++++++------
 test/lib/helper.py                  | 10 ++---
 test/test_base.py                   |  3 +-
 test/test_git.py                    | 10 +++--
 test/test_index.py                  |  9 ++--
 test/test_installation.py           |  3 +-
 test/test_submodule.py              | 15 +++----
 test/test_util.py                   |  3 +-
 17 files changed, 118 insertions(+), 128 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 89c03a394..96728e51a 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -72,7 +72,7 @@ jobs:
         python --version
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
-        python -c 'import git; print(git.compat.is_win)'
+        python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
 
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 2dd97183b..35fecf16c 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -63,7 +63,7 @@ jobs:
         python --version
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
-        python -c 'import git; print(git.compat.is_win)'
+        python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
 
     - name: Check types with mypy
       run: |
diff --git a/git/cmd.py b/git/cmd.py
index fd39a8eeb..f052c72cf 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -17,19 +17,21 @@
 import threading
 from textwrap import dedent
 
-from git.compat import (
-    defenc,
-    force_bytes,
-    safe_decode,
-    is_posix,
-    is_win,
+from git.compat import defenc, force_bytes, safe_decode
+from git.exc import (
+    CommandError,
+    GitCommandError,
+    GitCommandNotFound,
+    UnsafeOptionError,
+    UnsafeProtocolError,
 )
-from git.exc import CommandError
-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 (
+from git.util import (
     LazyMixin,
+    cygpath,
+    expand_path,
+    is_cygwin_git,
+    patch_env,
+    remove_password_if_present,
     stream_copy,
 )
 
@@ -180,14 +182,13 @@ def pump_stream(
         t.start()
         threads.append(t)
 
-    ## FIXME: Why Join??  Will block if `stdin` needs feeding...
-    #
+    # FIXME: Why join? Will block if stdin needs feeding...
     for t in threads:
         t.join(timeout=kill_after_timeout)
         if t.is_alive():
             if isinstance(process, Git.AutoInterrupt):
                 process._terminate()
-            else:  # Don't want to deal with the other case
+            else:  # Don't want to deal with the other case.
                 raise RuntimeError(
                     "Thread join() timed out in cmd.handle_process_output()."
                     f" kill_after_timeout={kill_after_timeout} seconds"
@@ -197,11 +198,11 @@ def pump_stream(
                     "error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds"
                 )
                 if not decode_streams and isinstance(p_stderr, BinaryIO):
-                    #  Assume stderr_handler needs binary input
+                    #  Assume stderr_handler needs binary input.
                     error_str = cast(str, error_str)
                     error_str = error_str.encode()
                 # We ignore typing on the next line because mypy does not like
-                # the way we inferred that stderr takes str or bytes
+                # the way we inferred that stderr takes str or bytes.
                 stderr_handler(error_str)  # type: ignore
 
     if finalizer:
@@ -228,14 +229,12 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 ## -- End Utilities -- @}
 
 
-# value of Windows process creation flag taken from MSDN
-CREATE_NO_WINDOW = 0x08000000
-
-## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
-# see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
-PROC_CREATIONFLAGS = (
-    CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP if is_win else 0  # type: ignore[attr-defined]
-)  # mypy error if not Windows.
+if os.name == "nt":
+    # CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards. See:
+    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+    PROC_CREATIONFLAGS = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+else:
+    PROC_CREATIONFLAGS = 0
 
 
 class Git(LazyMixin):
@@ -551,7 +550,7 @@ def _terminate(self) -> None:
                 # For some reason, providing None for stdout/stderr still prints something. This is why
                 # we simply use the shell and redirect to nul. Slower than CreateProcess. The question
                 # is whether we really want to see all these messages. It's annoying no matter what.
-                if is_win:
+                if os.name == "nt":
                     call(
                         ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)),
                         shell=True,
@@ -967,7 +966,7 @@ def execute(
         if inline_env is not None:
             env.update(inline_env)
 
-        if is_win:
+        if os.name == "nt":
             cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
                 raise GitCommandError(
@@ -999,11 +998,11 @@ def execute(
                     env=env,
                     cwd=cwd,
                     bufsize=-1,
-                    stdin=istream or DEVNULL,
+                    stdin=(istream or DEVNULL),
                     stderr=PIPE,
                     stdout=stdout_sink,
                     shell=shell,
-                    close_fds=is_posix,  # Unsupported on Windows.
+                    close_fds=(os.name == "posix"),  # Unsupported on Windows.
                     universal_newlines=universal_newlines,
                     creationflags=PROC_CREATIONFLAGS,
                     **subprocess_kwargs,
@@ -1073,7 +1072,7 @@ def kill_process(pid: int) -> None:
                         )
                         if not universal_newlines:
                             stderr_value = stderr_value.encode(defenc)
-                # strip trailing "\n"
+                # Strip trailing "\n".
                 if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore
                     stdout_value = stdout_value[:-1]
                 if stderr_value.endswith(newline):  # type: ignore
@@ -1147,11 +1146,11 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
         """
         old_env = {}
         for key, value in kwargs.items():
-            # set value if it is None
+            # Set value if it is None.
             if value is not None:
                 old_env[key] = self._environment.get(key)
                 self._environment[key] = value
-            # remove key from environment if its value is None
+            # Remove key from environment if its value is None.
             elif key in self._environment:
                 old_env[key] = self._environment[key]
                 del self._environment[key]
@@ -1330,7 +1329,8 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
         :return: (hex_sha, type_string, size_as_int)
 
         :raise ValueError: If the header contains indication for an error due to
-            incorrect input sha"""
+            incorrect input sha
+        """
         tokens = header_line.split()
         if len(tokens) != 3:
             if not tokens:
diff --git a/git/compat.py b/git/compat.py
index f17e52f7b..545bf65b4 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -34,9 +34,20 @@
 # ---------------------------------------------------------------------------
 
 
+# DEPRECATED attributes providing shortcuts to operating system checks based on os.name.
+#
+# - is_win and is_posix are deprecated because it is clearer, and helps avoid bugs, to
+#   write out the os.name checks explicitly. For example, is_win is False on Cygwin, but
+#   is often assumed to be True.
+#
+# - is_darwin is deprecated because it is always False on all systems, as os.name is
+#   never "darwin". For macOS, you can check for sys.platform == "darwin". (As on other
+#   Unix-like systems, os.name == "posix" on macOS. This is also the case on Cygwin.)
+#
 is_win: bool = os.name == "nt"
 is_posix = os.name == "posix"
 is_darwin = os.name == "darwin"
+
 defenc = sys.getfilesystemencoding()
 
 
diff --git a/git/config.py b/git/config.py
index 2cb057021..0846bbf94 100644
--- a/git/config.py
+++ b/git/config.py
@@ -7,28 +7,21 @@
 """Module containing module parser implementation able to properly read and write
 configuration files."""
 
-import sys
 import abc
+import configparser as cp
+import fnmatch
 from functools import wraps
 import inspect
 from io import BufferedReader, IOBase
 import logging
 import os
+import os.path as osp
 import re
-import fnmatch
-
-from git.compat import (
-    defenc,
-    force_text,
-    is_win,
-)
+import sys
 
+from git.compat import defenc, force_text
 from git.util import LockFile
 
-import os.path as osp
-
-import configparser as cp
-
 # typing-------------------------------------------------------
 
 from typing import (
@@ -250,7 +243,7 @@ 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":
+    if os.name == "nt" and config_level == "system":
         config_level = "global"
 
     if config_level == "system":
diff --git a/git/index/fun.py b/git/index/fun.py
index a35990d6d..fe8f373d8 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -2,8 +2,9 @@
 # NOTE: Autodoc hates it if this is a docstring.
 
 from io import BytesIO
-from pathlib import Path
 import os
+import os.path as osp
+from pathlib import Path
 from stat import (
     S_IFDIR,
     S_IFLNK,
@@ -16,26 +17,17 @@
 import subprocess
 
 from git.cmd import PROC_CREATIONFLAGS, handle_process_output
-from git.compat import (
-    defenc,
-    force_text,
-    force_bytes,
-    is_posix,
-    is_win,
-    safe_decode,
-)
-from git.exc import UnmergedEntriesError, HookExecutionError
+from git.compat import defenc, force_bytes, force_text, safe_decode
+from git.exc import HookExecutionError, UnmergedEntriesError
 from git.objects.fun import (
-    tree_to_stream,
     traverse_tree_recursive,
     traverse_trees_recursive,
+    tree_to_stream,
 )
 from git.util import IndexFileSHA1Writer, finalize_process
 from gitdb.base import IStream
 from gitdb.typ import str_tree_type
 
-import os.path as osp
-
 from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
 from .util import pack, unpack
 
@@ -96,7 +88,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     env["GIT_EDITOR"] = ":"
     cmd = [hp]
     try:
-        if is_win and not _has_file_extension(hp):
+        if os.name == "nt" and not _has_file_extension(hp):
             # Windows only uses extensions to determine how to open files
             # (doesn't understand shebangs). Try using bash to run the hook.
             relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
@@ -108,7 +100,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             cwd=index.repo.working_dir,
-            close_fds=is_posix,
+            close_fds=(os.name == "posix"),
             creationflags=PROC_CREATIONFLAGS,
         )
     except Exception as ex:
diff --git a/git/index/util.py b/git/index/util.py
index 08e49d860..5c2270abd 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -2,15 +2,11 @@
 
 from functools import wraps
 import os
+import os.path as osp
 import struct
 import tempfile
 from types import TracebackType
 
-from git.compat import is_win
-
-import os.path as osp
-
-
 # typing ----------------------------------------------------------------------
 
 from typing import Any, Callable, TYPE_CHECKING, Optional, Type
@@ -58,7 +54,7 @@ def __exit__(
         exc_tb: Optional[TracebackType],
     ) -> bool:
         if osp.isfile(self.tmp_file_path):
-            if is_win and osp.exists(self.file_path):
+            if os.name == "nt" and osp.exists(self.file_path):
                 os.remove(self.file_path)
             os.rename(self.tmp_file_path, self.file_path)
 
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 1516306ec..d88524ecb 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -7,7 +7,7 @@
 
 import git
 from git.cmd import Git
-from git.compat import defenc, is_win
+from git.compat import defenc
 from git.config import GitConfigParser, SectionConstraint, cp
 from git.exc import (
     BadName,
@@ -353,9 +353,8 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab
         """
         git_file = osp.join(working_tree_dir, ".git")
         rela_path = osp.relpath(module_abspath, start=working_tree_dir)
-        if is_win:
-            if osp.isfile(git_file):
-                os.remove(git_file)
+        if os.name == "nt" and osp.isfile(git_file):
+            os.remove(git_file)
         with open(git_file, "wb") as fp:
             fp.write(("gitdir: %s" % rela_path).encode(defenc))
 
diff --git a/git/repo/base.py b/git/repo/base.py
index 4790ea4e7..bbbf242db 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -6,24 +6,21 @@
 
 from __future__ import annotations
 
+import gc
 import logging
 import os
+import os.path as osp
+from pathlib import Path
 import re
 import shlex
 import warnings
 
-from pathlib import Path
-
+import gitdb
 from gitdb.db.loose import LooseObjectDB
-
 from gitdb.exc import BadObject
 
 from git.cmd import Git, handle_process_output
-from git.compat import (
-    defenc,
-    safe_decode,
-    is_win,
-)
+from git.compat import defenc, safe_decode
 from git.config import GitConfigParser
 from git.db import GitCmdObjectDB
 from git.exc import (
@@ -43,7 +40,6 @@
     expand_path,
     remove_password_if_present,
 )
-import os.path as osp
 
 from .fun import (
     rev_parse,
@@ -52,8 +48,6 @@
     touch,
     find_worktree_git_dir,
 )
-import gc
-import gitdb
 
 # typing ------------------------------------------------------
 
@@ -322,10 +316,10 @@ def close(self) -> None:
             # they are collected by the garbage collector, thus preventing deletion.
             # TODO: Find these references and ensure they are closed and deleted
             # synchronously rather than forcing a gc collection.
-            if is_win:
+            if os.name == "nt":
                 gc.collect()
             gitdb.util.mman.collect()
-            if is_win:
+            if os.name == "nt":
                 gc.collect()
 
     def __eq__(self, rhs: object) -> bool:
@@ -571,7 +565,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa
             git_dir = self.git_dir
         # We do not support an absolute path of the gitconfig on Windows.
         # Use the global config instead.
-        if is_win and config_level == "system":
+        if os.name == "nt" and config_level == "system":
             config_level = "global"
 
         if config_level == "system":
diff --git a/git/util.py b/git/util.py
index bd1fbe247..be267fc9f 100644
--- a/git/util.py
+++ b/git/util.py
@@ -22,8 +22,6 @@
 from urllib.parse import urlsplit, urlunsplit
 import warnings
 
-from .compat import is_win
-
 # typing ---------------------------------------------------------
 
 from typing import (
@@ -110,7 +108,18 @@
 log = logging.getLogger(__name__)
 
 
-def _read_env_flag(name: str, default: bool) -> bool:
+def _read_win_env_flag(name: str, default: bool) -> bool:
+    """Read a boolean flag from an environment variable on Windows.
+
+    :return:
+        On Windows, the flag, or the ``default`` value if absent or ambiguous.
+        On all other operating systems, ``False``.
+
+    :note: This only accesses the environment on Windows.
+    """
+    if os.name != "nt":
+        return False
+
     try:
         value = os.environ[name]
     except KeyError:
@@ -134,8 +143,8 @@ def _read_env_flag(name: str, default: bool) -> bool:
 #: We need an easy way to see if Appveyor TCs start failing,
 #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
 #: till then, we wish to hide them.
-HIDE_WINDOWS_KNOWN_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_KNOWN_ERRORS", True)
-HIDE_WINDOWS_FREEZE_ERRORS = is_win and _read_env_flag("HIDE_WINDOWS_FREEZE_ERRORS", True)
+HIDE_WINDOWS_KNOWN_ERRORS = _read_win_env_flag("HIDE_WINDOWS_KNOWN_ERRORS", True)
+HIDE_WINDOWS_FREEZE_ERRORS = _read_win_env_flag("HIDE_WINDOWS_FREEZE_ERRORS", True)
 
 # { Utility Methods
 
@@ -220,7 +229,7 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
 def rmfile(path: PathLike) -> None:
     """Ensure file deleted also on *Windows* where read-only files need special treatment."""
     if osp.isfile(path):
-        if is_win:
+        if os.name == "nt":
             os.chmod(path, 0o777)
         os.remove(path)
 
@@ -260,7 +269,7 @@ def join_path(a: PathLike, *p: PathLike) -> PathLike:
     return path
 
 
-if is_win:
+if os.name == "nt":
 
     def to_native_path_windows(path: PathLike) -> PathLike:
         path = str(path)
@@ -308,9 +317,12 @@ def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
 
 def _get_exe_extensions() -> Sequence[str]:
     PATHEXT = os.environ.get("PATHEXT", None)
-    return (
-        tuple(p.upper() for p in PATHEXT.split(os.pathsep)) if PATHEXT else (".BAT", "COM", ".EXE") if is_win else ("")
-    )
+    if PATHEXT:
+        return tuple(p.upper() for p in PATHEXT.split(os.pathsep))
+    elif os.name == "nt":
+        return (".BAT", "COM", ".EXE")
+    else:
+        return ()
 
 
 def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
@@ -418,8 +430,8 @@ def is_cygwin_git(git_executable: PathLike) -> bool:
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if is_win:
-        # is_win is only True on native Windows systems. On Cygwin, os.name == "posix".
+    if os.name == "nt":
+        # This is Windows-native Python, since Cygwin has os.name == "posix".
         return False
 
     if git_executable is None:
diff --git a/test/lib/helper.py b/test/lib/helper.py
index 8725cd13f..950ca3c0b 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -10,17 +10,15 @@
 import io
 import logging
 import os
+import os.path as osp
 import tempfile
 import textwrap
 import time
 import unittest
 
-from git.compat import is_win
-from git.util import rmtree, cwd
 import gitdb
 
-import os.path as osp
-
+from git.util import rmtree, cwd
 
 TestCase = unittest.TestCase
 SkipTest = unittest.SkipTest
@@ -177,7 +175,7 @@ def git_daemon_launched(base_path, ip, port):
 
     gd = None
     try:
-        if is_win:
+        if os.name == "nt":
             # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\,
             # but if invoked as 'git daemon', it detaches from parent `git` cmd,
             # and then CANNOT DIE!
@@ -201,7 +199,7 @@ def git_daemon_launched(base_path, ip, port):
                 as_process=True,
             )
         # Yes, I know... fortunately, this is always going to work if sleep time is just large enough.
-        time.sleep(0.5 * (1 + is_win))
+        time.sleep(1.0 if os.name == "nt" else 0.5)
     except Exception as ex:
         msg = textwrap.dedent(
             """
diff --git a/test/test_base.py b/test/test_base.py
index e4704c7d8..302d696fe 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -11,7 +11,6 @@
 
 from git import Repo
 from git.objects import Blob, Tree, Commit, TagObject
-from git.compat import is_win
 from git.objects.util import get_object_type_by_name
 from test.lib import TestBase as _TestBase, with_rw_repo, with_rw_and_rw_remote_repo
 from git.util import hex_to_bin, HIDE_WINDOWS_FREEZE_ERRORS
@@ -131,7 +130,7 @@ def test_add_unicode(self, rw_repo):
         with open(file_path, "wb") as fp:
             fp.write(b"something")
 
-        if is_win:
+        if os.name == "nt":
             # On Windows, there is no way this works, see images on:
             # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
             # Therefore, it must be added using the Python implementation.
diff --git a/test/test_git.py b/test/test_git.py
index 06f8f5c97..b85be6359 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -23,7 +23,6 @@
 import ddt
 
 from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
-from git.compat import is_win
 from git.util import cwd, finalize_process
 from test.lib import TestBase, fixture_path, with_rw_directory
 
@@ -139,7 +138,7 @@ def test_it_executes_git_and_returns_result(self):
 
     def test_it_executes_git_not_from_cwd(self):
         with TemporaryDirectory() as tmpdir:
-            if is_win:
+            if os.name == "nt":
                 # 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")
@@ -154,7 +153,10 @@ def test_it_executes_git_not_from_cwd(self):
             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.")
+    @skipUnless(
+        os.name == "nt",
+        "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():
@@ -268,7 +270,7 @@ def test_refresh(self):
         self.assertRaises(GitCommandNotFound, refresh, "yada")
 
         # Test a good path refresh.
-        which_cmd = "where" if is_win else "command -v"
+        which_cmd = "where" if os.name == "nt" else "command -v"
         path = os.popen("{0} git".format(which_cmd)).read().strip().split("\n")[0]
         refresh(path)
 
diff --git a/test/test_index.py b/test/test_index.py
index 3357dc880..b9d1e78ab 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -25,7 +25,6 @@
     GitCommandError,
     CheckoutError,
 )
-from git.compat import is_win
 from git.exc import HookExecutionError, InvalidGitRepositoryError
 from git.index.fun import hook_path
 from git.index.typ import BaseIndexEntry, IndexEntry
@@ -43,9 +42,9 @@ def _found_in(cmd, directory):
     return path and Path(path).parent == Path(directory)
 
 
-is_win_without_bash = is_win and not shutil.which("bash.exe")
+is_win_without_bash = os.name == "nt" and not shutil.which("bash.exe")
 
-is_win_with_wsl_bash = is_win and _found_in(
+is_win_with_wsl_bash = os.name == "nt" and _found_in(
     cmd="bash.exe",
     directory=Path(os.getenv("WINDIR")) / "System32",
 )
@@ -614,7 +613,7 @@ def mixed_iterator():
         self.assertNotEqual(entries[0].hexsha, null_hex_sha)
 
         # Add symlink.
-        if not is_win:
+        if os.name != "nt":
             for target in ("/etc/nonexisting", "/etc/passwd", "/etc"):
                 basename = "my_real_symlink"
 
@@ -672,7 +671,7 @@ def mixed_iterator():
         index.checkout(fake_symlink_path)
 
         # On Windows, we will never get symlinks.
-        if is_win:
+        if os.name == "nt":
             # Symlinks should contain the link as text (which is what a
             # symlink actually is).
             with open(fake_symlink_path, "rt") as fd:
diff --git a/test/test_installation.py b/test/test_installation.py
index e7774d29d..8e5f55d28 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -6,7 +6,6 @@
 import subprocess
 import sys
 
-from git.compat import is_win
 from test.lib import TestBase
 from test.lib.helper import with_rw_directory
 
@@ -15,7 +14,7 @@ class TestInstallation(TestBase):
     def setUp_venv(self, rw_dir):
         self.venv = rw_dir
         subprocess.run([sys.executable, "-m", "venv", self.venv], stdout=subprocess.PIPE)
-        bin_name = "Scripts" if is_win else "bin"
+        bin_name = "Scripts" if os.name == "nt" 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")
diff --git a/test/test_submodule.py b/test/test_submodule.py
index f63db1495..f706769c0 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -3,17 +3,17 @@
 
 import contextlib
 import os
-import shutil
-import tempfile
+import os.path as osp
 from pathlib import Path
+import shutil
 import sys
+import tempfile
 from unittest import mock, skipUnless
 
 import pytest
 
 import git
 from git.cmd import Git
-from git.compat import is_win
 from git.config import GitConfigParser, cp
 from git.exc import (
     GitCommandError,
@@ -25,11 +25,8 @@
 from git.objects.submodule.base import Submodule
 from git.objects.submodule.root import RootModule, RootUpdateProgress
 from git.repo.fun import find_submodule_git_dir, touch
-from test.lib import TestBase, with_rw_repo
-from test.lib import with_rw_directory
-from git.util import HIDE_WINDOWS_KNOWN_ERRORS
-from git.util import to_native_path_linux, join_path_native
-import os.path as osp
+from git.util import HIDE_WINDOWS_KNOWN_ERRORS, join_path_native, to_native_path_linux
+from test.lib import TestBase, with_rw_directory, with_rw_repo
 
 
 @contextlib.contextmanager
@@ -1040,7 +1037,7 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset"
         assert sm_mod.head.ref.name == sm_pfb.name
 
-    @skipUnless(is_win, "Specifically for Windows.")
+    @skipUnless(os.name == "nt", "Specifically for Windows.")
     def test_to_relative_path_with_super_at_root_drive(self):
         class Repo:
             working_tree_dir = "D:\\"
diff --git a/test/test_util.py b/test/test_util.py
index d345247b1..4ef98fd73 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -20,7 +20,6 @@
 import pytest
 
 from git.cmd import dashify
-from git.compat import is_win
 from git.objects.util import (
     altz_to_utctz_str,
     from_timestamp,
@@ -364,7 +363,7 @@ def test_blocking_lock_file(self):
         self.assertRaises(IOError, wait_lock._obtain_lock)
         elapsed = time.time() - start
         extra_time = 0.02
-        if is_win or sys.platform == "cygwin":
+        if os.name == "nt" or sys.platform == "cygwin":
             extra_time *= 6  # NOTE: Indeterministic failures without this...
         self.assertLess(elapsed, wait_time + extra_time)
 

From 562bdee8110267f2861625f40aa403a72f000def Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 4 Nov 2023 17:33:58 -0400
Subject: [PATCH 260/994] Fix compat.is_darwin

This fixes compat.is_darwin to use sys.platform instead of os.name
so that it is True on macOS systems instead of always being False.
The deprecation rationale in compat.py is accordingly adjusted.

Together with c01fe76, this largely addresses #1731.
---
 git/compat.py | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/git/compat.py b/git/compat.py
index 545bf65b4..629be075a 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -36,17 +36,14 @@
 
 # DEPRECATED attributes providing shortcuts to operating system checks based on os.name.
 #
-# - is_win and is_posix are deprecated because it is clearer, and helps avoid bugs, to
-#   write out the os.name checks explicitly. For example, is_win is False on Cygwin, but
-#   is often assumed to be True.
+# These are deprecated because it is clearer, and helps avoid bugs, to write out the
+# os.name or sys.platform checks explicitly, especially in cases where it matters which
+# is used. For example, is_win is False on Cygwin, but is often assumed True. To detect
+# Cygwin, use sys.platform == "cygwin". (Also, in the past, is_darwin was unreliable.)
 #
-# - is_darwin is deprecated because it is always False on all systems, as os.name is
-#   never "darwin". For macOS, you can check for sys.platform == "darwin". (As on other
-#   Unix-like systems, os.name == "posix" on macOS. This is also the case on Cygwin.)
-#
-is_win: bool = os.name == "nt"
+is_win = os.name == "nt"
 is_posix = os.name == "posix"
-is_darwin = os.name == "darwin"
+is_darwin = sys.platform == "darwin"
 
 defenc = sys.getfilesystemencoding()
 

From 86453efa47c8a32b8b66446dac10686bb06f2e0a Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sun, 5 Nov 2023 14:53:17 +0100
Subject: [PATCH 261/994] Create FUNDING.json to support Drips

---
 FUNDING.json | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 FUNDING.json

diff --git a/FUNDING.json b/FUNDING.json
new file mode 100644
index 000000000..bf3faa662
--- /dev/null
+++ b/FUNDING.json
@@ -0,0 +1,7 @@
+{
+  "drips": {
+    "ethereum": {
+      "ownedBy": "0xD0d4dCFc194ec24bCc777e635289e0b10E1a7b87"
+    }
+  }
+}

From aed3b59d50b7105debcb11d03c7abd8a1a4e5c96 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 6 Nov 2023 10:05:57 -0500
Subject: [PATCH 262/994] Revise and restore some module docstrings

In 9ccd777, several module docstrings were converted into comments
or outright deleted, apparently due to at least one of them (in
git/index/fun.py) and possibly others having broken Sphinx autodoc.

As discussed later in #1733, this no longer seems to be a problem,
not even in git/index/fun.py where the comment included a note
indicating it had.

So this converts them back to docstrings, re-adding those that were
removed in 9ccd777. This also revises them, partly to avoid making
outdated claims, but mostly for style and clarity. It also revises
some already-present module docstrings. Other than modules 9ccd777
affected, this does not add new docstrings to modules without them.

One of the revisions made to some module docstrings here, to
improve readability, is to remove language like "Module
implementing" or "Module for", which was already used only in some
places. However, this language is retained in the specific cases
where it is clarifying, for example by avoiding documenting a
module as if it the same thing as a single class implemented in it
(which would be distracting because it would read as a mistake, and
which could be confusing in cases where a developer uses an import
with an "as" clause and needs to debug it because it was intended
to be an "import" or "from" statement but was accidentally written
as the other of those).
---
 git/config.py            | 3 +--
 git/exc.py               | 2 +-
 git/index/base.py        | 3 +++
 git/index/fun.py         | 3 +--
 git/index/typ.py         | 2 +-
 git/index/util.py        | 2 +-
 git/objects/fun.py       | 2 +-
 git/objects/tag.py       | 2 +-
 git/objects/util.py      | 2 +-
 git/refs/head.py         | 5 +++++
 git/refs/remote.py       | 2 ++
 git/repo/fun.py          | 2 +-
 test/test_blob_filter.py | 2 +-
 test/test_exc.py         | 1 -
 test/test_repo.py        | 1 +
 15 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/git/config.py b/git/config.py
index 17a07fba9..983d71e86 100644
--- a/git/config.py
+++ b/git/config.py
@@ -3,8 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module containing module parser implementation able to properly read and write
-configuration files."""
+"""Parser for reading and writing configuration files."""
 
 import abc
 import configparser as cp
diff --git a/git/exc.py b/git/exc.py
index 35008c29a..85113bc44 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module containing all exceptions thrown throughout the git package."""
+"""Exceptions thrown throughout the git package."""
 
 __all__ = [
     # Defined in gitdb.exc:
diff --git a/git/index/base.py b/git/index/base.py
index dbfa2c9bf..94437ac88 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -3,6 +3,9 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+"""Module containing IndexFile, an Index implementation facilitating all kinds of index
+manipulations such as querying and merging."""
+
 from contextlib import ExitStack
 import datetime
 import glob
diff --git a/git/index/fun.py b/git/index/fun.py
index 45317429f..7b3b06269 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -1,8 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# Standalone functions to accompany the index implementation and make it more versatile.
-# NOTE: Autodoc hates it if this is a docstring.
+"""Standalone functions to accompany the index implementation and make it more versatile."""
 
 from io import BytesIO
 import os
diff --git a/git/index/typ.py b/git/index/typ.py
index 7011fd03d..894e6f16d 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module with additional types used by the index."""
+"""Additional types used by the index."""
 
 from binascii import b2a_hex
 from pathlib import Path
diff --git a/git/index/util.py b/git/index/util.py
index 5ee20a8ef..b1aaa58fd 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module containing index utilities."""
+"""Index utilities."""
 
 from functools import wraps
 import os
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 6d8a23d35..bf6bc21d6 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module with functions which are supposed to be as fast as possible."""
+"""Functions that are supposed to be as fast as possible."""
 
 from stat import S_ISDIR
 
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 500879d54..f7aaaf477 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module containing all Object-based types."""
+"""Object-based types."""
 
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
diff --git a/git/objects/util.py b/git/objects/util.py
index 9f42227d0..3021fec39 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module for general utility functions."""
+"""General utility functions."""
 
 # flake8: noqa F401
 
diff --git a/git/refs/head.py b/git/refs/head.py
index fba195aaa..ebc71eb96 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -1,6 +1,11 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+"""Some ref-based objects.
+
+Note the distinction between the :class:`HEAD` and :class:`Head` classes.
+"""
+
 from git.config import GitConfigParser, SectionConstraint
 from git.util import join_path
 from git.exc import GitCommandError
diff --git a/git/refs/remote.py b/git/refs/remote.py
index dd4117fa7..59d02a755 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -1,6 +1,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+"""Module implementing a remote object allowing easy access to git remotes."""
+
 import os
 
 from git.util import join_path
diff --git a/git/repo/fun.py b/git/repo/fun.py
index ee831332f..63bcfdfc7 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module with general repository-related functions."""
+"""General repository-related functions."""
 
 from __future__ import annotations
 
diff --git a/test/test_blob_filter.py b/test/test_blob_filter.py
index 5cc6b48c9..a91f211bf 100644
--- a/test/test_blob_filter.py
+++ b/test/test_blob_filter.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Test the blob filter."""
+"""Tests for the blob filter."""
 
 from pathlib import Path
 from typing import Sequence, Tuple
diff --git a/test/test_exc.py b/test/test_exc.py
index 62bb4fb6e..cecd4f342 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -3,7 +3,6 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-
 import re
 
 import ddt
diff --git a/test/test_repo.py b/test/test_repo.py
index e77bf2503..e39aee05e 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -2,6 +2,7 @@
 #
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 import glob
 import io
 from io import BytesIO

From 53354162f8e785ce39fb5dda85929fdc10e2b1b6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 04:36:43 -0400
Subject: [PATCH 263/994] Test that rmtree doesn't chmod outside the tree

This adds a test in test_util that reveals the bug where
git.util.rmtree will change the permissions on files outside the
tree being removed, if the tree being removed contains a symlink to
a file outside the tree, and the symlink is in a (sub)directory
whose own permissions prevent the symlink itself from being
removed.

The new test failure shows how git.util.rmtree currently calls
os.chmod in a way that dereferences symlinks, including those that
point from inside the tree being deleted to outside it.

Another similar demonstration is temporarily included in the
perm.sh script. That script served as scratchwork for writing the
unit test, and it can be removed soon (while keeping the test).
---
 perm.sh           | 17 +++++++++++++++++
 test/test_util.py | 29 +++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)
 create mode 100755 perm.sh

diff --git a/perm.sh b/perm.sh
new file mode 100755
index 000000000..419f0c1a8
--- /dev/null
+++ b/perm.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+set -e
+
+mkdir dir1
+touch dir1/file
+chmod -w dir1/file
+printf 'Permissions BEFORE rmtree call:\n'
+ls -l dir1/file
+printf '\n'
+
+mkdir dir2
+ln -s ../dir1/file dir2/symlink
+chmod -w dir2
+python -c 'from git.util import rmtree; rmtree("dir2")' || true
+printf '\nPermissions AFTER rmtree call:\n'
+ls -l dir1/file
diff --git a/test/test_util.py b/test/test_util.py
index 07568c32a..148f852d5 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -105,6 +105,35 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
 
         assert not td.exists()
 
+    @pytest.mark.skipif(
+        sys.platform == "cygwin",
+        reason="Cygwin can't set the permissions that make the test meaningful.",
+    )
+    def test_avoids_changing_permissions_outside_tree(self, tmp_path: pathlib.Path):
+        # Automatically works on Windows, but on Unix requires either special handling
+        # or refraining from attempting to fix PermissionError by making chmod calls.
+
+        dir1 = tmp_path / "dir1"
+        dir1.mkdir()
+        (dir1 / "file").write_bytes(b"")
+        (dir1 / "file").chmod(stat.S_IRUSR)
+        old_mode = (dir1 / "file").stat().st_mode
+
+        dir2 = tmp_path / "dir2"
+        dir2.mkdir()
+        (dir2 / "symlink").symlink_to(dir1 / "file")
+        dir2.chmod(stat.S_IRUSR | stat.S_IXUSR)
+
+        try:
+            rmtree(dir2)
+        except PermissionError:
+            pass  # On Unix, dir2 is not writable, so dir2/symlink may not be deleted.
+        except SkipTest as ex:
+            self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
+
+        new_mode = (dir1 / "file").stat().st_mode
+        assert old_mode == new_mode, f"Should stay {old_mode:#o}, became {new_mode:#o}."
+
     @pytest.mark.skipif(
         sys.platform == "cygwin",
         reason="Cygwin can't set the permissions that make the test meaningful.",

From fd78a53f70a70c2ce8d891f8eb0692f4ab787e25 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 30 Oct 2023 17:58:44 -0400
Subject: [PATCH 264/994] One approach to the symlink-following bug

---
 git/util.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 63619b87d..c0cd2f152 100644
--- a/git/util.py
+++ b/git/util.py
@@ -197,7 +197,7 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]:
             os.environ[name] = old_value
 
 
-def rmtree(path: PathLike) -> None:
+def _rmtree_windows(path: PathLike) -> None:
     """Remove the given directory tree recursively.
 
     :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files
@@ -225,6 +225,17 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
         shutil.rmtree(path, onerror=handler)
 
 
+def _rmtree_posix(path: PathLike) -> None:
+    """Remove the given directory tree recursively."""
+    return shutil.rmtree(path)
+
+
+if os.name == "nt":
+    rmtree = _rmtree_windows
+else:
+    rmtree = _rmtree_posix
+
+
 def rmfile(path: PathLike) -> None:
     """Ensure file deleted also on *Windows* where read-only files need special treatment."""
     if osp.isfile(path):

From 6de8e676c55170f073b64471e78b606c9c01b757 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Nov 2023 16:43:48 -0500
Subject: [PATCH 265/994] Refactor current fix for symlink-following bug

This keeps the same approach, but expresses it better. This does
not update the tests (yet), so one test is still failing.
---
 git/util.py | 17 ++++-------------
 1 file changed, 4 insertions(+), 13 deletions(-)

diff --git a/git/util.py b/git/util.py
index c0cd2f152..01cdcf773 100644
--- a/git/util.py
+++ b/git/util.py
@@ -197,7 +197,7 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]:
             os.environ[name] = old_value
 
 
-def _rmtree_windows(path: PathLike) -> None:
+def rmtree(path: PathLike) -> None:
     """Remove the given directory tree recursively.
 
     :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files
@@ -219,23 +219,14 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
                 raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
-    if sys.version_info >= (3, 12):
+    if os.name != "nt":
+        shutil.rmtree(path)
+    elif sys.version_info >= (3, 12):
         shutil.rmtree(path, onexc=handler)
     else:
         shutil.rmtree(path, onerror=handler)
 
 
-def _rmtree_posix(path: PathLike) -> None:
-    """Remove the given directory tree recursively."""
-    return shutil.rmtree(path)
-
-
-if os.name == "nt":
-    rmtree = _rmtree_windows
-else:
-    rmtree = _rmtree_posix
-
-
 def rmfile(path: PathLike) -> None:
     """Ensure file deleted also on *Windows* where read-only files need special treatment."""
     if osp.isfile(path):

From e8cfae89657b941ffd824afc9c11891d79952624 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Nov 2023 18:26:02 -0500
Subject: [PATCH 266/994] Test that PermissionError is only wrapped on Windows

This changes tests in test_util to verify the opposite behavior
from what was enforced before, in the unusual case (that hopefully
never happens outside of monkey-patching in test_util.py itself)
where the system is not Windows but HIDE_WINDOWS_KNOWN_ERRORS is
set to True.

The same-named environment variable will not, and never has, set
HIDE_WINDOWS_KNOWN_ERRORS to True on non-Windows systems, but it is
possible to set it to True directly. Since it is named as a
constant and no documentation has ever suggested changing its value
directly, nor otherwise attempting to use it outside Windows, it
shouldn't matter what happens in this unusual case. But asserting
that wrapping never occurs in this combination of circumstances is
what makes the most sense in light of the recent change to pass no
callback to shutil.rmtree on non-Windows systems.
---
 test/test_util.py | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 148f852d5..40dd4f1fd 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -135,11 +135,11 @@ def test_avoids_changing_permissions_outside_tree(self, tmp_path: pathlib.Path):
         assert old_mode == new_mode, f"Should stay {old_mode:#o}, became {new_mode:#o}."
 
     @pytest.mark.skipif(
-        sys.platform == "cygwin",
-        reason="Cygwin can't set the permissions that make the test meaningful.",
+        os.name != "nt",
+        reason="PermissionError is only ever wrapped on Windows",
     )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
-        """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
         # Access the module through sys.modules so it is unambiguous which module's
         # attribute we patch: the original git.util, not git.index.util even though
         # git.index.util "replaces" git.util and is what "import git.util" gives us.
@@ -157,10 +157,17 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         sys.platform == "cygwin",
         reason="Cygwin can't set the permissions that make the test meaningful.",
     )
-    def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir):
-        """rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false."""
+    @pytest.mark.parametrize(
+        "hide_windows_known_errors",
+        [
+            pytest.param(False),
+            pytest.param(True, marks=pytest.mark.skipif(os.name == "nt", reason="We would wrap on Windows")),
+        ],
+    )
+    def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
+        """rmtree does not wrap PermissionError on non-Windows systems or when HIDE_WINDOWS_KNOWN_ERRORS is false."""
         # See comments in test_wraps_perm_error_if_enabled for details about patching.
-        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", False)
+        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
         mocker.patch.object(os, "chmod")
         mocker.patch.object(pathlib.Path, "chmod")
 

From 8a22352407638923b9b89be66a30a44b0bb50690 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Nov 2023 18:47:19 -0500
Subject: [PATCH 267/994] Extract some shared patching code to a helper

Because of how it is parameterized, but only in some of the test
cases that use it, making this a pytest fixture would be cumbersome
without further refactoring, so this is a helper method instead.

(It may be feasible to further refactor the test suite to improve
this more.)

This also removes a type annotation from a test method. It may
eventually be helpful to add annotations, in which case that would
come back along with others (if that test still exists in this
form), but it's the only test with such an annotation in this
test class, and the annotation had been added accidentally.
---
 test/test_util.py | 33 ++++++++++++++-------------------
 1 file changed, 14 insertions(+), 19 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 40dd4f1fd..e5a5a0557 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -109,7 +109,7 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
         sys.platform == "cygwin",
         reason="Cygwin can't set the permissions that make the test meaningful.",
     )
-    def test_avoids_changing_permissions_outside_tree(self, tmp_path: pathlib.Path):
+    def test_avoids_changing_permissions_outside_tree(self, tmp_path):
         # Automatically works on Windows, but on Unix requires either special handling
         # or refraining from attempting to fix PermissionError by making chmod calls.
 
@@ -134,22 +134,24 @@ def test_avoids_changing_permissions_outside_tree(self, tmp_path: pathlib.Path):
         new_mode = (dir1 / "file").stat().st_mode
         assert old_mode == new_mode, f"Should stay {old_mode:#o}, became {new_mode:#o}."
 
-    @pytest.mark.skipif(
-        os.name != "nt",
-        reason="PermissionError is only ever wrapped on Windows",
-    )
-    def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
-        """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+    def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         # Access the module through sys.modules so it is unambiguous which module's
         # attribute we patch: the original git.util, not git.index.util even though
         # git.index.util "replaces" git.util and is what "import git.util" gives us.
-        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True)
+        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
 
-        # Disable common chmod functions so the callback can never fix the problem.
+        # Disable common chmod functions so the callback can't fix a PermissionError.
         mocker.patch.object(os, "chmod")
         mocker.patch.object(pathlib.Path, "chmod")
 
-        # Now we can see how an intractable PermissionError is treated.
+    @pytest.mark.skipif(
+        os.name != "nt",
+        reason="PermissionError is only ever wrapped on Windows",
+    )
+    def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
+        """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        self._patch_for_wrapping_test(mocker, True)
+
         with pytest.raises(SkipTest):
             rmtree(permission_error_tmpdir)
 
@@ -166,10 +168,7 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
     )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
         """rmtree does not wrap PermissionError on non-Windows systems or when HIDE_WINDOWS_KNOWN_ERRORS is false."""
-        # See comments in test_wraps_perm_error_if_enabled for details about patching.
-        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
-        mocker.patch.object(os, "chmod")
-        mocker.patch.object(pathlib.Path, "chmod")
+        self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
 
         with pytest.raises(PermissionError):
             try:
@@ -180,11 +179,7 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
     @pytest.mark.parametrize("hide_windows_known_errors", [False, True])
     def test_does_not_wrap_other_errors(self, tmp_path, mocker, hide_windows_known_errors):
         file_not_found_tmpdir = tmp_path / "testdir"  # It is deliberately never created.
-
-        # See comments in test_wraps_perm_error_if_enabled for details about patching.
-        mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
-        mocker.patch.object(os, "chmod")
-        mocker.patch.object(pathlib.Path, "chmod")
+        self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
 
         with pytest.raises(FileNotFoundError):
             try:

From b226f3dd04c16d8fc4ef4044ad81bb72be8683d1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Nov 2023 19:28:03 -0500
Subject: [PATCH 268/994] Remove demonstration script

This removes the perm.sh script that demonstrated the cross-tree
permission change bug. Tests for test_util test for this (in case
of a regression) and the bug is fixed, so the script, which served
to inform the writing of the corresponding unit test, is no longer
needed.
---
 perm.sh | 17 -----------------
 1 file changed, 17 deletions(-)
 delete mode 100755 perm.sh

diff --git a/perm.sh b/perm.sh
deleted file mode 100755
index 419f0c1a8..000000000
--- a/perm.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/sh
-
-set -e
-
-mkdir dir1
-touch dir1/file
-chmod -w dir1/file
-printf 'Permissions BEFORE rmtree call:\n'
-ls -l dir1/file
-printf '\n'
-
-mkdir dir2
-ln -s ../dir1/file dir2/symlink
-chmod -w dir2
-python -c 'from git.util import rmtree; rmtree("dir2")' || true
-printf '\nPermissions AFTER rmtree call:\n'
-ls -l dir1/file

From 359116b3b9b7011eb7893fa584a43522f1b9bb57 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 13 Nov 2023 23:48:13 -0500
Subject: [PATCH 269/994] List all non-passing tests in test summaries

This changes the default in pyproject.toml so that pytest lists
a line for each non-passing test at the end of a run, showing the
test name and, where available, condensed information about the
status, such as the "reason" argument for an xfailing or skipped
test.

Previously only failed and errored tests were listed in the
summary. Now skipped, xfailed, and xpassed tests are listed too.
The benefit is in keeping track of the status of tests. Although
showing the full failure output with stack trace and relevant code
under test would be too distracting for tests marked xfail, it is
valuable to not merely run those tests but be able to see a line
showing their names and statuses. Likewise, a number of tests are
currently marked skipped, and while some of them are skipped on a
particular platform because they don't make sense to run on that
platform, a number of others are skipped by raising SkipTest in
response to a failure condition on Windows. (Those consist mostly
of the tests skipped as a result of code discussed in #790.)

This also has the more specific benefit of making it easier to mark
tests as xfail in order to add CI jobs for native Windows, and more
importantly to allow information about their status to later be
used to understand and fix bugs on Windows.)
---
 pyproject.toml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index eae5943ea..7109389d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools"]
 build-backend = "setuptools.build_meta"
 
 [tool.pytest.ini_options]
-addopts = "--cov=git --cov-report=term --disable-warnings"
+addopts = "--cov=git --cov-report=term --disable-warnings -ra"
 filterwarnings = "ignore::DeprecationWarning"
 python_files = "test_*.py"
 tmp_path_retention_policy = "failed"
@@ -14,8 +14,8 @@ testpaths = "test"  # Space separated list of paths from root e.g test tests doc
 # --cov-report html:path  # html file at path
 # --maxfail  # number of errors before giving up
 # -disable-warnings  # Disable pytest warnings (not codebase warnings)
-# -rf   # increased reporting of failures
-# -rE   # increased reporting of errors
+# -rfE  # default test summary: list fail and error
+# -ra   # test summary: list all non-passing (fail, error, skip, xfail, xpass)
 # --ignore-glob=**/gitdb/*  # ignore glob paths
 # filterwarnings ignore::WarningType  # ignores those warnings
 

From 2fd79f41fe1304be7bcaebec84394ab9e45961c5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 15 Oct 2023 02:35:44 -0400
Subject: [PATCH 270/994] Add native Windows test jobs to CI matrix

This expands the CI test matrix in the main testing workflow to
test on both Ubuntu and Windows, instead of just Ubuntu.

It does not attempt to merge in the Cygwin workflow at this time,
which may or may not end up being useful to do in the future.

The new Windows test jobs all fail currently: the runs fail as a
result of various tests being consistently unable to pass but not
yet being marked xfail (or skip). It should be feasible to mark
these xfail with informative messages, but this commit doesn't
include that.
---
 .github/workflows/pythonpackage.yml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 35fecf16c..95b12004c 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -10,18 +10,19 @@ permissions:
 
 jobs:
   build:
-    runs-on: ubuntu-latest
-
     strategy:
       fail-fast: false
       matrix:
+        os: ["ubuntu-latest", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false
 
+    runs-on: ${{ matrix.os }}
+
     defaults:
       run:
-        shell: /bin/bash --noprofile --norc -exo pipefail {0}
+        shell: bash --noprofile --norc -exo pipefail {0}
 
     steps:
     - uses: actions/checkout@v4

From 6e477e3a06e4b11d43ea118a9f18cae03fa211fd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 14 Nov 2023 00:41:57 -0500
Subject: [PATCH 271/994] Add xfail marks for IndexFile.from_tree failures

8 of the tests that fail on native Windows systems fail due to
IndexFile.from_tree being broken on Windows, causing #1630.

This commit marks those tests as xfail. This is part, though not
all, of the changes to get CI test jobs for native Windows that
are passing, to guard against new regressions and to allow the code
and tests to be gradually fixed (see discussion in #1654).

When fixing the bug, this commit can be reverted.
---
 test/test_docs.py  | 11 +++++++++--
 test/test_fun.py   | 17 ++++++++++++++---
 test/test_index.py | 40 ++++++++++++++++++++++++++++++++++++++++
 test/test_refs.py  | 11 +++++++++++
 4 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/test/test_docs.py b/test/test_docs.py
index 2f4b2e8d8..2ff1c794a 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -8,11 +8,10 @@
 
 import pytest
 
+from git.exc import GitCommandError
 from test.lib import TestBase
 from test.lib.helper import with_rw_directory
 
-import os.path
-
 
 class Tutorials(TestBase):
     def tearDown(self):
@@ -207,6 +206,14 @@ def update(self, op_code, cur_count, max_count=None, message=""):
         assert sm.module_exists()  # The submodule's working tree was checked out by update.
         # ![14-test_init_repo_object]
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_directory
     def test_references_and_objects(self, rw_dir):
         # [1-test_references_and_objects]
diff --git a/test/test_fun.py b/test/test_fun.py
index 566bc9aae..8ea5b7e46 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -3,10 +3,13 @@
 
 from io import BytesIO
 from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR
-from os import stat
+import os
 import os.path as osp
 
+import pytest
+
 from git import Git
+from git.exc import GitCommandError
 from git.index import IndexFile
 from git.index.fun import (
     aggressive_tree_merge,
@@ -34,6 +37,14 @@ def _assert_index_entries(self, entries, trees):
             assert (entry.path, entry.stage) in index.entries
         # END assert entry matches fully
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     def test_aggressive_tree_merge(self):
         # Head tree with additions, removals and modification compared to its predecessor.
         odb = self.rorepo.odb
@@ -291,12 +302,12 @@ def test_linked_worktree_traversal(self, rw_dir):
         rw_master.git.worktree("add", worktree_path, branch.name)
 
         dotgit = osp.join(worktree_path, ".git")
-        statbuf = stat(dotgit)
+        statbuf = os.stat(dotgit)
         self.assertTrue(statbuf.st_mode & S_IFREG)
 
         gitdir = find_worktree_git_dir(dotgit)
         self.assertIsNotNone(gitdir)
-        statbuf = stat(gitdir)
+        statbuf = os.stat(gitdir)
         self.assertTrue(statbuf.st_mode & S_IFDIR)
 
     def test_tree_entries_from_data_with_failing_name_decode_py3(self):
diff --git a/test/test_index.py b/test/test_index.py
index 3d1042be8..9fe35d42b 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -179,6 +179,14 @@ def add_bad_blob():
         except Exception as ex:
             assert "index.lock' could not be obtained" not in str(ex)
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("0.1.6")
     def test_index_file_from_tree(self, rw_repo):
         common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541"
@@ -229,6 +237,14 @@ def test_index_file_from_tree(self, rw_repo):
         # END for each blob
         self.assertEqual(num_blobs, len(three_way_index.entries))
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("0.1.6")
     def test_index_merge_tree(self, rw_repo):
         # A bit out of place, but we need a different repo for this:
@@ -291,6 +307,14 @@ def test_index_merge_tree(self, rw_repo):
         self.assertEqual(len(unmerged_blobs), 1)
         self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0])
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("0.1.6")
     def test_index_file_diffing(self, rw_repo):
         # Default Index instance points to our index.
@@ -425,6 +449,14 @@ def _count_existing(self, repo, files):
 
     # END num existing helper
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("0.1.6")
     def test_index_mutation(self, rw_repo):
         index = rw_repo.index
@@ -778,6 +810,14 @@ def make_paths():
         for absfile in absfiles:
             assert osp.isfile(absfile)
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("HEAD")
     def test_compare_write_tree(self, rw_repo):
         """Test writing all trees, comparing them for equality."""
diff --git a/test/test_refs.py b/test/test_refs.py
index 6ee385007..a1573c11b 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -4,8 +4,11 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from itertools import chain
+import os
 from pathlib import Path
 
+import pytest
+
 from git import (
     Reference,
     Head,
@@ -215,6 +218,14 @@ def test_head_checkout_detached_head(self, rw_repo):
         assert isinstance(res, SymbolicReference)
         assert res.name == "HEAD"
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
+            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
+        ),
+        raises=GitCommandError,
+    )
     @with_rw_repo("0.1.6")
     def test_head_reset(self, rw_repo):
         cur_head = rw_repo.head

From cd9d7a9d558273a1f82527890a1d69529a3ef1d5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 09:55:06 -0500
Subject: [PATCH 272/994] Mark test_clone_command_injection xfail on Windows

This other GitCommandError on Windows is not related to
IndexFile.from_tree whose 8 related failing tests were marked xfail
in the preceding commit.

Also, test_clone_command_injection should not be confused with
test_clone_from_command_injection, which passes on all platforms.

The problem here appears to be that, on Windows, the path of the
directory GitPython is intended to clone to -- when the possible
security vulnerability this test checks for is absent -- is not
valid. This suggests the bug may only be in the test and that the
code under test may be working on Windows. But the test does not
establish that, for which it would need to test with a payload
clearly capable of creating the file unexpected_path refers to when
run on its own.

This doesn't currently seem to be reported as a bug, but some
general context about the implementation can be examined in #1518
where it was introduced, and #1531 where it was modified.
---
 test/test_repo.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/test/test_repo.py b/test/test_repo.py
index e39aee05e..f853e2b36 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -1365,6 +1365,11 @@ def test_do_not_strip_newline_in_stdout(self, rw_dir):
         r.git.commit(message="init")
         self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), "hello\n")
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=R"fatal: could not create leading directories of '--upload-pack=touch C:\Users\ek\AppData\Local\Temp\tmpnantqizc\pwn': Invalid argument",  # noqa: E501
+        raises=GitCommandError,
+    )
     @with_rw_repo("HEAD")
     def test_clone_command_injection(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:

From f72e2821a3073c4e87915fe7e721512e16e4fa74 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 12:19:10 -0500
Subject: [PATCH 273/994] Mark test_diff_submodule xfail on Windows

Everything attempted in the test case method is actually working
and passes, but when the tearDown method calls shutil.rmtree, it
fails with "Access is denied" and raises PermissionError. The
reason and exception are accordingly noted in the xfail decoration.

While adding a pytest import to apply the pytest xfail mark, I also
improved grouping/sorting of other imports in the test_diff module.
---
 test/test_diff.py | 30 +++++++++++++-----------------
 1 file changed, 13 insertions(+), 17 deletions(-)

diff --git a/test/test_diff.py b/test/test_diff.py
index 1678e737d..1d138a086 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -3,26 +3,17 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import ddt
+import os
+import os.path as osp
 import shutil
 import tempfile
-from git import (
-    Repo,
-    GitCommandError,
-    Diff,
-    DiffIndex,
-    NULL_TREE,
-    Submodule,
-)
-from git.cmd import Git
-from test.lib import (
-    TestBase,
-    StringProcessAdapter,
-    fixture,
-)
-from test.lib import with_rw_directory
 
-import os.path as osp
+import ddt
+import pytest
+
+from git import NULL_TREE, Diff, DiffIndex, GitCommandError, Repo, Submodule
+from git.cmd import Git
+from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
 
 
 def to_raw(input):
@@ -318,6 +309,11 @@ def test_diff_with_spaces(self):
         self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
         self.assertEqual(diff_index[0].b_path, "file with spaces", repr(diff_index[0].b_path))
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason='"Access is denied" when tearDown calls shutil.rmtree',
+        raises=PermissionError,
+    )
     def test_diff_submodule(self):
         """Test that diff is able to correctly diff commits that cover submodule changes"""
         # Init a temp git repo that will be referenced as a submodule.

From 42a3d74f529f34382b205c0ada46fb18df80c034 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 14:14:44 -0500
Subject: [PATCH 274/994] Mark TestSubmodule.test_rename xfail on Windows

The test fails when attempting to evaluate the expression:

    sm.move(new_path).name

as part of the assertion:

    assert sm.move(new_path).name == new_path

But it is the call to sm.move that fails, not the assertion itself.
The condition is never evaluated, because the subexpression raises
PermissionError. Details are in the xfail reason string.

This test_submodule.TestSubmodule.test_rename method should not be
confused with test_config.TestBase.test_rename, which passes on all
platforms.

On CI this has XPASS status on Python 3.7 and possibly some other
versions. This should be investigated and, if possible, the xfail
markings should be made precise. (When working on this change
before a rebase, I had thought only 3.7 xpassed, and that the XPASS
was only on CI, never locally. However, I am unable to reproduce
that or find CI logs of it, so some of these observations may have
been mistaken and/or or specific to a narrow environment.)
---
 test/test_submodule.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 5be1a1077..68dbcc491 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -949,6 +949,17 @@ def test_remove_norefs(self, rwdir):
         sm.remove()
         assert not sm.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "The sm.move call fails. Submodule.move calls os.renames, which raises:\n"
+            "PermissionError: [WinError 32] "
+            "The process cannot access the file because it is being used by another process: "
+            R"'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\mymodules\myname' "
+            R"-> 'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\renamed\myname'"
+        ),
+        raises=PermissionError,
+    )
     @with_rw_directory
     def test_rename(self, rwdir):
         parent = git.Repo.init(osp.join(rwdir, "parent"))

From 4abab92cb0f4caf569cc4bd9a8084994a80733ce Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 14:54:53 -0500
Subject: [PATCH 275/994] Mark test_conditional_includes_from_git_dir xfail on
 Windows

As noted, the second of the config._included_paths() assertions
fails, which is in the "Ensure that config is included if path is
matching git_dir" sub-case.

It is returning 0 on Windows. THe GitConfigParser._has_includes
function returns the expression:

    self._merge_includes and len(self._included_paths())

Since _merge_includes is a bool, it appears the first branch of the
"and" is True, but then _included_paths returns an empty list.
---
 test/test_config.py | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/test/test_config.py b/test/test_config.py
index 0e1bba08a..c1b26c91f 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -6,19 +6,15 @@
 import glob
 import io
 import os
+import os.path as osp
 from unittest import mock
 
+import pytest
+
 from git import GitConfigParser
 from git.config import _OMD, cp
-from test.lib import (
-    TestCase,
-    fixture_path,
-    SkipTest,
-)
-from test.lib import with_rw_directory
-
-import os.path as osp
 from git.util import rmfile
+from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory
 
 
 _tc_lock_fpaths = osp.join(osp.dirname(__file__), "fixtures/*.lock")
@@ -239,6 +235,11 @@ def check_test_value(cr, value):
         with GitConfigParser(fpa, read_only=True) as cr:
             check_test_value(cr, tv)
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")',
+        raises=AssertionError,
+    )
     @with_rw_directory
     def test_conditional_includes_from_git_dir(self, rw_dir):
         # Initiate repository path.

From 799c8536800e75d7ddaef2b588754234db363245 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 19:57:15 -0500
Subject: [PATCH 276/994] Improve ordering/grouping of a few imports

In modules soon to be modified, so if subsequent commits are later
reverted, these import tweaks are not automatically undone.
---
 test/test_remote.py | 27 ++++++++++++++-------------
 test/test_repo.py   | 25 ++++++++++++-------------
 2 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/test/test_remote.py b/test/test_remote.py
index 12de18476..711c7b1bc 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -3,36 +3,37 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import os.path as osp
+from pathlib import Path
 import random
 import tempfile
-import pytest
 from unittest import skipIf
 
+import pytest
+
 from git import (
-    RemoteProgress,
+    Commit,
     FetchInfo,
-    Reference,
-    SymbolicReference,
+    GitCommandError,
     Head,
-    Commit,
     PushInfo,
+    Reference,
+    Remote,
+    RemoteProgress,
     RemoteReference,
+    SymbolicReference,
     TagReference,
-    Remote,
-    GitCommandError,
 )
 from git.cmd import Git
-from pathlib import Path
 from git.exc import UnsafeOptionError, UnsafeProtocolError
+from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList
 from test.lib import (
+    GIT_DAEMON_PORT,
     TestBase,
-    with_rw_repo,
-    with_rw_and_rw_remote_repo,
     fixture,
-    GIT_DAEMON_PORT,
+    with_rw_and_rw_remote_repo,
+    with_rw_repo,
 )
-from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList
-import os.path as osp
 
 
 # Make sure we have repeatable results.
diff --git a/test/test_repo.py b/test/test_repo.py
index f853e2b36..3cde25184 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -8,6 +8,7 @@
 from io import BytesIO
 import itertools
 import os
+import os.path as osp
 import pathlib
 import pickle
 import sys
@@ -17,22 +18,22 @@
 import pytest
 
 from git import (
+    BadName,
+    Commit,
+    Git,
+    GitCmdObjectDB,
+    GitCommandError,
+    GitDB,
+    Head,
+    IndexFile,
     InvalidGitRepositoryError,
-    Repo,
     NoSuchPathError,
-    Head,
-    Commit,
     Object,
-    Tree,
-    IndexFile,
-    Git,
     Reference,
-    GitDB,
-    Submodule,
-    GitCmdObjectDB,
     Remote,
-    BadName,
-    GitCommandError,
+    Repo,
+    Submodule,
+    Tree,
 )
 from git.exc import (
     BadObject,
@@ -43,8 +44,6 @@
 from git.util import bin_to_hex, cygpath, join_path_native, rmfile, rmtree
 from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo
 
-import os.path as osp
-
 
 def iter_flatten(lol):
     for items in lol:

From b284ad70291d24024b840fa659f54276cf9ceaa5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 20:17:51 -0500
Subject: [PATCH 277/994] Mark test_create_remote_unsafe_url_allowed xfail on
 Windows

The test mostly works on Windows, but it fails because the tmp_file
path is expected to appear in remote_url with backslash separators,
but (forward) slashes appear instead.
---
 test/test_remote.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/test/test_remote.py b/test/test_remote.py
index 711c7b1bc..f9f35e5d8 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import os
 import os.path as osp
 from pathlib import Path
 import random
@@ -767,6 +768,11 @@ def test_create_remote_unsafe_url(self, rw_repo):
                     Remote.create(rw_repo, "origin", url)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value",
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_create_remote_unsafe_url_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:

From 61d1fba6dd318e7f68b93b0e8b46050db0d03fa8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 15 Nov 2023 21:04:04 -0500
Subject: [PATCH 278/994] Mark unsafe-options "allowed" tests xfail on Windows

The tests of unsafe options are among those introduced originally
in #1521. They are regression tests for #1515 (CVE-2022-24439).
The unsafe options tests are paired: a test for the usual, default
behavior of forbidding the option, and a test for the behavior when
the option is explicitly allowed. In each such pair, both tests use
a payload that is intended to produce the side effect of a file of
a specific name being created in a temporary directory.

All the tests work on Unix-like systems. On Windows, the tests of
the *allowed* cases are broken, and this commit marks them xfail.
However, this has implications for the tests of the default, secure
behavior, because until the "allowed" versions work on Windows, it
will be unclear if either are using a payload that is effective and
that corresponds to the way its effect is examined.

What *seems* to happen is this: The "\" characters in the path are
treated as shell escape characters rather than literally, with the
effect of disappearing in most paths since most letters lack
special meaning when escaped. Also, "touch" is not a native Windows
command, and the "touch" command provided by Git for Windows is
linked against MSYS2 libraries, causing it to map (some?)
occurrences of ":" in filenames to a separate code point in the
Private Use Area of the Basic Multilingual Plane. The result is a
path with no directory separators or drive letter. It denotes a
file of an unintended name in the current directory, which is never
the intended location. The current directory depends on GitPython
implementation details, but at present it's the top-level directory
of the rw_repo working tree. A new unstaged file, named like
"C\357\200\272UsersekAppDataLocalTemptmpc7x4xik5pwn", can be
observed there (this is how "git status" will format the name).

Fortunately, this and all related tests are working on other OSes,
and the affected code under test does not appear highly dependent
on OS. So the fix is *probably* fully working on Windows as well.
---
 test/test_remote.py | 27 +++++++++++++++++++++++++++
 test/test_repo.py   | 18 ++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/test/test_remote.py b/test/test_remote.py
index f9f35e5d8..080718a1c 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -831,6 +831,15 @@ def test_fetch_unsafe_options(self, rw_repo):
                     remote.fetch(**unsafe_option)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "File not created. A separate Windows command may be needed. This and the "
+            "currently passing test test_fetch_unsafe_options must be adjusted in the "
+            "same way. Until then, test_fetch_unsafe_options is unreliable on Windows."
+        ),
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_fetch_unsafe_options_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:
@@ -890,6 +899,15 @@ def test_pull_unsafe_options(self, rw_repo):
                     remote.pull(**unsafe_option)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "File not created. A separate Windows command may be needed. This and the "
+            "currently passing test test_pull_unsafe_options must be adjusted in the "
+            "same way. Until then, test_pull_unsafe_options is unreliable on Windows."
+        ),
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_pull_unsafe_options_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:
@@ -955,6 +973,15 @@ def test_push_unsafe_options(self, rw_repo):
                     remote.push(**unsafe_option)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "File not created. A separate Windows command may be needed. This and the "
+            "currently passing test test_push_unsafe_options must be adjusted in the "
+            "same way. Until then, test_push_unsafe_options is unreliable on Windows."
+        ),
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_push_unsafe_options_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:
diff --git a/test/test_repo.py b/test/test_repo.py
index 3cde25184..033deca1e 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -294,6 +294,15 @@ def test_clone_unsafe_options(self, rw_repo):
                     rw_repo.clone(tmp_dir, **unsafe_option)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "File not created. A separate Windows command may be needed. This and the "
+            "currently passing test test_clone_unsafe_options must be adjusted in the "
+            "same way. Until then, test_clone_unsafe_options is unreliable on Windows."
+        ),
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_clone_unsafe_options_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:
@@ -364,6 +373,15 @@ def test_clone_from_unsafe_options(self, rw_repo):
                     Repo.clone_from(rw_repo.working_dir, tmp_dir, **unsafe_option)
                 assert not tmp_file.exists()
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason=(
+            "File not created. A separate Windows command may be needed. This and the "
+            "currently passing test test_clone_from_unsafe_options must be adjusted in the "
+            "same way. Until then, test_clone_from_unsafe_options is unreliable on Windows."
+        ),
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD")
     def test_clone_from_unsafe_options_allowed(self, rw_repo):
         with tempfile.TemporaryDirectory() as tdir:

From ad07ecb0151e64f3fa774099d8c69232df6bda23 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 22 Nov 2023 11:00:54 -0500
Subject: [PATCH 279/994] Show PATH on CI

Along with the version and platform information that is already
shown. This is to make debugging easier.
---
 .github/workflows/cygwin-test.yml   | 1 +
 .github/workflows/pythonpackage.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 96728e51a..36329f93f 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -73,6 +73,7 @@ jobs:
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
         python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
+        printenv PATH | tr ':' '\n'
 
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 95b12004c..890681b7d 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -65,6 +65,7 @@ jobs:
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
         python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
+        printenv PATH | tr ':' '\n'
 
     - name: Check types with mypy
       run: |

From 2784e403d6fd812650f29f1842db1e67344170a3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 22 Nov 2023 12:25:22 -0500
Subject: [PATCH 280/994] Show bash and other WSL-relevant info but not PATH

Only on native Windows systems (since this is for debugging WSL).

The Windows CI runners have WSL itself installed but no WSL systems
installed. So some of these commands may fail if they call the
bash.exe in the System32 directory, but this should still be
illuminating. Results after a WSL system is set up will likely be
more important, but doing this first allows for comparison.

The reason PATH directories are no longer listed is to decrease
noise.
---
 .github/workflows/cygwin-test.yml   |  1 -
 .github/workflows/pythonpackage.yml | 22 +++++++++++++++++++++-
 2 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 36329f93f..96728e51a 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -73,7 +73,6 @@ jobs:
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
         python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
-        printenv PATH | tr ':' '\n'
 
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 890681b7d..d21ea3949 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -65,7 +65,27 @@ jobs:
         python -c 'import sys; print(sys.platform)'
         python -c 'import os; print(os.name)'
         python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
-        printenv PATH | tr ':' '\n'
+
+    # For debugging hook tests on native Windows systems that may have WSL.
+    - name: Show where bash.exe may be found
+      if: startsWith(matrix.os, 'windows')
+      run: |
+        set +e
+        type -a bash.exe
+        python -c 'import shutil; print(shutil.which("bash.exe"))'
+        bash.exe --version
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "--version"]); print(f"result: {p!r}")'
+        bash.exe -c 'echo "$BASH"'
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", """echo "$BASH" """]); print(f"result: {p!r}")'
+        bash.exe -c 'echo "$BASH_VERSION"'
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", """echo "$BASH_VERSION" """]); print(f"result: {p!r}")'
+        bash.exe -c 'printenv WSL_DISTRO_NAME'
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "printenv WSL_DISTRO_NAME"]); print(f"result: {p!r}")'
+        bash.exe -c 'ls -l /proc/sys/fs/binfmt_misc/WSLInterop'
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "ls -l /proc/sys/fs/binfmt_misc/WSLInterop"]); print(f"result: {p!r}")'
+        bash.exe -c 'uname -a'
+        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "uname -a"]); print(f"result: {p!r}")'
+      continue-on-error: true
 
     - name: Check types with mypy
       run: |

From 9717b8d847b514f761ecaa423226054c73f2a325 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 17 Nov 2023 11:22:12 -0500
Subject: [PATCH 281/994] Install WSL system on CI for hook tests

This makes three of the four hook-related tests pass instead of
failing, and changes the way the fourth fails.

GitPython uses bash.exe to run hooks that are (or appear to be)
shell scripts. On many Windows systems, this is the bash.exe in the
system32 directory, which delegates to bash in a WSL system if at
least one such system is installed (for the current user), and
gives an error otherwise. It may be a bug that GitPython ends up
using this bash.exe when WSL is installed but no WSL systems exist,
since that is actually a fairly common situation. One place that
happened was on the GitHub Actions runners used for Windows jobs.
Those runners have WSL available, and are capable of running WSL 1
systems (not currently WSL 2 systems), but no WSL systems were
actually installed.

This commit fixes that cause of failure, for all four tests it
happened in, by setting up a Debian WSL system on the test runner.
(This increases the amount of time it takes Windows jobs to run,
but that might be possible to improve on.) Three of those four
tests now pass, while the other fails for another reason.

The newly passing tests are:

- test/test_index.py::TestIndex::test_commit_msg_hook_fail
- test/test_index.py::TestIndex::test_pre_commit_hook_fail
- test/test_index.py::TestIndex::test_pre_commit_hook_success

The test that still fails, but differently, is:

- test/test_index.py::TestIndex::test_commit_msg_hook_success

I had previously found that test to fail on a local WSL 2 system,
and attempted to add a suitable xfail marking in 881456b (#1679).
But the condition I wrote there *appears* to have a bug related to
the different orders in which subproces.Popen and shutil.which find
executables, causing it not always to detect when the WSL-related
bash.exe is the one the Popen call in git.index.fun.run_commit_hook
will use.
---
 .github/workflows/pythonpackage.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index d21ea3949..c2bbc1bbb 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -35,6 +35,12 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
+    - name: Set up WSL
+      if: startsWith(matrix.os, 'windows')
+      uses: Vampire/setup-wsl@v2.0.2
+      with:
+        distribution: Debian
+
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From 5d113942786b2cc86f5fd7bb228f9f75c8c78beb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 23 Nov 2023 18:46:00 -0500
Subject: [PATCH 282/994] Fix and expand bash.exe xfail marks on hook tests

881456b (#1679) expanded the use of shutil.which in test_index.py
to attempt to mark test_commit_msg_hook_success xfail when bash.exe
is a WSL bash wrapper in System32 (because that test currently
is not passing when the hook is run via bash in a WSL system, which
the WSL bash.exe wraps). But this was not reliable, due to
significant differences between shell and non-shell search behavior
for executable commands on Windows. As the new docstring notes,
lookup due to Popen generally checks System32 before going through
directories in PATH, as this is how CreateProcessW behaves.

- https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
- https://github.com/python/cpython/issues/91558#issuecomment-1100942950

The technique I had used in 881456b also had the shortcoming of
assuming that a bash.exe in System32 was the WSL wrapper. But such
a file may exist on some systems without WSL, and be a bash
interpreter unrelated to WSL that may be able to run hooks.

In addition, one common situation, which was the case on CI before
a step to install a WSL distribution was added, is that WSL is
present but no WSL distributions are installed. In that situation
bash.exe is found in System32, but it can't be used to run any
hooks, because there's no actual system with a bash in it to wrap.
This was not covered before. Unlike some conditions that prevent
a WSL system from being used, such as resource exhaustion blocking
it from being started, the absence of a WSL system should probably
not fail the pytest run, for the same reason as we are trying not
to have the complete *absence* of bash.exe fail the pytest run.
Both conditions should be xfail. Fortunately, the error message
when no distribution exists is distinctive and can be checked for.

There is probably no correct and reasonable way to check LBYL-style
which executable file bash.exe resolves to by using shutil.which,
due to shutil.which and subprocess.Popen's differing seach orders
and other subtleties. So this adds code to do it EAFP-style using
subprocess.run (which itself uses Popen, so giving the same
CreateProcessW behavior). It tries to run a command with bash.exe
whose output pretty reliably shows if the system is WSL or not.

We may fail to get to the point of running that command at all, if
bash.exe is not usable, in which case the failure's details tell us
if bash.exe is absent (xfail), present as the WSL wrapper with no WSL
systems (xfail), or has some other error (not xfail, so the user can
become aware of and proably fix the problem). If we do get to that
point, then a file that is almost always present on both WSL 1 and
WSL 2 systems and almost always absent on any other system is
checked for, to distinguish whether the working bash shell operates
in a WSL system, or outside any such system as e.g. Git Bash does.

See https://superuser.com/a/1749811 on various techniques for
checking for WSL, including the /proc/sys/fs/binfmt_misc/WSLInterop
technique used here (which seems overall may be the most reliable).

Although the Windows CI runners have Git Bash, and this is even the
bash.exe that appears first in PATH (giving rise to the problem
with shutil.which detailed above), it would be a bit awkward to
test the behavior when Git Bash is actually used to run hooks on
CI, because of how Popen selects the System32 bash.exe first,
whether or not any WSL distribution is present. However, local
testing shows that when Git Bash's bash.exe is selected, all four
hook tests in the module pass, both before and after this change,
and furthermore that bash.exe is correctly detected as "native",
continuing to avoid an erroneous xfail mark in that case.
---
 test/test_index.py | 126 ++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 113 insertions(+), 13 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 9fe35d42b..dfacb60db 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -3,12 +3,14 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import enum
 from io import BytesIO
+import logging
 import os
 import os.path as osp
 from pathlib import Path
 from stat import S_ISLNK, ST_MODE
-import shutil
+import subprocess
 import tempfile
 
 import pytest
@@ -34,19 +36,97 @@
 
 HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
 
+log = logging.getLogger(__name__)
 
-def _found_in(cmd, directory):
-    """Check if a command is resolved in a directory (without following symlinks)."""
-    path = shutil.which(cmd)
-    return path and Path(path).parent == Path(directory)
 
+class _WinBashMeta(enum.EnumMeta):
+    """Metaclass allowing :class:`_WinBash` custom behavior when called."""
 
-is_win_without_bash = os.name == "nt" and not shutil.which("bash.exe")
+    def __call__(cls):
+        return cls._check()
 
-is_win_with_wsl_bash = os.name == "nt" and _found_in(
-    cmd="bash.exe",
-    directory=Path(os.getenv("WINDIR")) / "System32",
-)
+
+@enum.unique
+class _WinBash(enum.Enum, metaclass=_WinBashMeta):
+    """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
+
+    Call ``_WinBash()`` to check the status.
+
+    This can't be reliably discovered using :func:`shutil.which`, as that approximates
+    how a shell is expected to search for an executable. On Windows, there are major
+    differences between how executables are found by a shell and otherwise. (This is the
+    cmd.exe Windows shell and should not be confused with bash.exe.) Our run_commit_hook
+    function in GitPython uses subprocess.Popen, including to run bash.exe on Windows.
+    It does not pass shell=True (and should not). Popen calls CreateProcessW, which
+    searches several locations prior to using the PATH environment variable. It is
+    expected to search the System32 directory, even if another directory containing the
+    executable precedes it in PATH. (There are other differences, less relevant here.)
+    When WSL is installed, even if no WSL *systems* are installed, bash.exe exists in
+    System32, and Popen finds it even if another bash.exe precedes it in PATH, as
+    happens on CI. If WSL is absent, System32 may still have bash.exe, as Windows users
+    and administrators occasionally copy executables there in lieu of extending PATH.
+    """
+
+    INAPPLICABLE = enum.auto()
+    """This system is not native Windows: either not Windows at all, or Cygwin."""
+
+    ABSENT = enum.auto()
+    """No command for ``bash.exe`` is found on the system."""
+
+    NATIVE = enum.auto()
+    """Running ``bash.exe`` operates outside any WSL environment (as with Git Bash)."""
+
+    WSL = enum.auto()
+    """Running ``bash.exe`` runs bash on a WSL system."""
+
+    WSL_NO_DISTRO = enum.auto()
+    """Running ``bash.exe` tries to run bash on a WSL system, but none exists."""
+
+    ERROR_WHILE_CHECKING = enum.auto()
+    """Could not determine the status.
+
+    This should not trigger a skip or xfail, as it typically indicates either a fixable
+    problem on the test machine, such as an "Insufficient system resources exist to
+    complete the requested service" error starting WSL, or a bug in this detection code.
+    ``ERROR_WHILE_CHECKING.error_or_process`` has details about the most recent failure.
+    """
+
+    @classmethod
+    def _check(cls):
+        if os.name != "nt":
+            return cls.INAPPLICABLE
+
+        try:
+            # Print rather than returning the test command's exit status so that if a
+            # failure occurs before we even get to this point, we will detect it.
+            script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
+            command = ["bash.exe", "-c", script]
+            proc = subprocess.run(command, capture_output=True, check=True, text=True)
+        except FileNotFoundError:
+            return cls.ABSENT
+        except OSError as error:
+            return cls._error(error)
+        except subprocess.CalledProcessError as error:
+            no_distro_message = "Windows Subsystem for Linux has no installed distributions."
+            if error.returncode == 1 and error.stdout.startswith(no_distro_message):
+                return cls.WSL_NO_DISTRO
+            return cls._error(error)
+
+        status = proc.stdout.rstrip()
+        if status == "0":
+            return cls.WSL
+        if status == "1":
+            return cls.NATIVE
+        return cls._error(proc)
+
+    @classmethod
+    def _error(cls, error_or_process):
+        if isinstance(error_or_process, subprocess.CompletedProcess):
+            log.error("Strange output checking WSL status: %s", error_or_process.stdout)
+        else:
+            log.error("Error running bash.exe to check WSL status: %s", error_or_process)
+        cls.ERROR_WHILE_CHECKING.error_or_process = error_or_process
+        return cls.ERROR_WHILE_CHECKING
 
 
 def _make_hook(git_dir, name, content, make_exec=True):
@@ -917,12 +997,22 @@ class Mocked:
         rel = index._to_relative_path(path)
         self.assertEqual(rel, os.path.relpath(path, root))
 
+    @pytest.mark.xfail(
+        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        raises=HookExecutionError,
+    )
     @with_rw_repo("HEAD", bare=True)
     def test_pre_commit_hook_success(self, rw_repo):
         index = rw_repo.index
         _make_hook(index.repo.git_dir, "pre-commit", "exit 0")
         index.commit("This should not fail")
 
+    @pytest.mark.xfail(
+        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD", bare=True)
     def test_pre_commit_hook_fail(self, rw_repo):
         index = rw_repo.index
@@ -930,7 +1020,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if is_win_without_bash:
+            if _WinBash() is _WinBash.ABSENT:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")
@@ -946,10 +1036,15 @@ def test_pre_commit_hook_fail(self, rw_repo):
             raise AssertionError("Should have caught a HookExecutionError")
 
     @pytest.mark.xfail(
-        is_win_without_bash or is_win_with_wsl_bash,
+        _WinBash() in {_WinBash.ABSENT, _WinBash.WSL},
         reason="Specifically seems to fail on WSL bash (in spite of #1399)",
         raises=AssertionError,
     )
+    @pytest.mark.xfail(
+        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        raises=HookExecutionError,
+    )
     @with_rw_repo("HEAD", bare=True)
     def test_commit_msg_hook_success(self, rw_repo):
         commit_message = "commit default head by Frèderic Çaufl€"
@@ -963,6 +1058,11 @@ def test_commit_msg_hook_success(self, rw_repo):
         new_commit = index.commit(commit_message)
         self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
 
+    @pytest.mark.xfail(
+        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        raises=AssertionError,
+    )
     @with_rw_repo("HEAD", bare=True)
     def test_commit_msg_hook_fail(self, rw_repo):
         index = rw_repo.index
@@ -970,7 +1070,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if is_win_without_bash:
+            if _WinBash() is _WinBash.ABSENT:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")

From b21535729ca695e47c52086abe390ca4e6792ae3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 23 Nov 2023 22:07:06 -0500
Subject: [PATCH 283/994] Simplify/clarify bash.exe check for hook tests; do it
 only once

- Make the design of the enum simpler.
- Move some informaton to the check method's docstring.
- Call the method once instead of in each decoration.

Doing the check only once makes things a little faster, but the
more important reason is that the checks can become stale very
quickly in some situations. This is unlikely to be an issue on CI,
but locally WSL may fail (or not fail) to start up when bash.exe
is used in a check, then not fail (or fail) when actaully needed
in the test, if the reason it fails is due to resource usage, such
as most RAM being used on the system.

Although this seems like a reason to do the check multiple times,
doing it multiple times in the decorations still does it ahead of
when any of the tests is actually run. In contrast, with the change
here, we may get outdated results by the time the tests run, but
the xfail effects of the check are always consistent with each
other and are no longer written in a way that suggests they are
more current than they really are.
---
 test/test_index.py | 73 ++++++++++++++++++++++++----------------------
 1 file changed, 38 insertions(+), 35 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index dfacb60db..9a520275c 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -39,32 +39,11 @@
 log = logging.getLogger(__name__)
 
 
-class _WinBashMeta(enum.EnumMeta):
-    """Metaclass allowing :class:`_WinBash` custom behavior when called."""
-
-    def __call__(cls):
-        return cls._check()
-
-
 @enum.unique
-class _WinBash(enum.Enum, metaclass=_WinBashMeta):
+class _WinBashStatus(enum.Enum):
     """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
 
-    Call ``_WinBash()`` to check the status.
-
-    This can't be reliably discovered using :func:`shutil.which`, as that approximates
-    how a shell is expected to search for an executable. On Windows, there are major
-    differences between how executables are found by a shell and otherwise. (This is the
-    cmd.exe Windows shell and should not be confused with bash.exe.) Our run_commit_hook
-    function in GitPython uses subprocess.Popen, including to run bash.exe on Windows.
-    It does not pass shell=True (and should not). Popen calls CreateProcessW, which
-    searches several locations prior to using the PATH environment variable. It is
-    expected to search the System32 directory, even if another directory containing the
-    executable precedes it in PATH. (There are other differences, less relevant here.)
-    When WSL is installed, even if no WSL *systems* are installed, bash.exe exists in
-    System32, and Popen finds it even if another bash.exe precedes it in PATH, as
-    happens on CI. If WSL is absent, System32 may still have bash.exe, as Windows users
-    and administrators occasionally copy executables there in lieu of extending PATH.
+    Call :meth:`check` to check the status.
     """
 
     INAPPLICABLE = enum.auto()
@@ -74,13 +53,13 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
     """No command for ``bash.exe`` is found on the system."""
 
     NATIVE = enum.auto()
-    """Running ``bash.exe`` operates outside any WSL environment (as with Git Bash)."""
+    """Running ``bash.exe`` operates outside any WSL distribution (as with Git Bash)."""
 
     WSL = enum.auto()
-    """Running ``bash.exe`` runs bash on a WSL system."""
+    """Running ``bash.exe`` calls ``bash`` in a WSL distribution."""
 
     WSL_NO_DISTRO = enum.auto()
-    """Running ``bash.exe` tries to run bash on a WSL system, but none exists."""
+    """Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
 
     ERROR_WHILE_CHECKING = enum.auto()
     """Could not determine the status.
@@ -92,13 +71,34 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
     """
 
     @classmethod
-    def _check(cls):
+    def check(cls):
+        """Check the status of the ``bash.exe`` :func:`index.fun.run_commit_hook` uses.
+
+        This uses EAFP, attempting to run a command via ``bash.exe``. Which ``bash.exe``
+        is used can't be reliably discovered by :func:`shutil.which`, which approximates
+        how a shell is expected to search for an executable. On Windows, there are major
+        differences between how executables are found by a shell and otherwise. (This is
+        the cmd.exe Windows shell, and shouldn't be confused with bash.exe itself. That
+        the command being looked up also happens to be an interpreter is not relevant.)
+
+        :func:`index.fun.run_commit_hook` uses :class:`subprocess.Popen`, including when
+        it runs ``bash.exe`` on Windows. It doesn't pass ``shell=True`` (and shouldn't).
+        On Windows, `Popen` calls ``CreateProcessW``, which searches several locations
+        prior to using the ``PATH`` environment variable. It is expected to search the
+        ``System32`` directory, even if another directory containing the executable
+        precedes it in ``PATH``. (Other differences are less relevant here.) When WSL is
+        installed, even with no distributions, ``bash.exe`` exists in ``System32``, and
+        `Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
+        If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
+        administrators occasionally put executables there in lieu of extending ``PATH``.
+        """
         if os.name != "nt":
             return cls.INAPPLICABLE
 
         try:
             # Print rather than returning the test command's exit status so that if a
-            # failure occurs before we even get to this point, we will detect it.
+            # failure occurs before we even get to this point, we will detect it. See
+            # https://superuser.com/a/1749811 for information on ways to check for WSL.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
             proc = subprocess.run(command, capture_output=True, check=True, text=True)
@@ -129,6 +129,9 @@ def _error(cls, error_or_process):
         return cls.ERROR_WHILE_CHECKING
 
 
+_win_bash_status = _WinBashStatus.check()
+
+
 def _make_hook(git_dir, name, content, make_exec=True):
     """A helper to create a hook"""
     hp = hook_path(name, git_dir)
@@ -998,7 +1001,7 @@ class Mocked:
         self.assertEqual(rel, os.path.relpath(path, root))
 
     @pytest.mark.xfail(
-        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1009,7 +1012,7 @@ def test_pre_commit_hook_success(self, rw_repo):
         index.commit("This should not fail")
 
     @pytest.mark.xfail(
-        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1020,7 +1023,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if _WinBash() is _WinBash.ABSENT:
+            if _win_bash_status is _WinBashStatus.ABSENT:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")
@@ -1036,12 +1039,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
             raise AssertionError("Should have caught a HookExecutionError")
 
     @pytest.mark.xfail(
-        _WinBash() in {_WinBash.ABSENT, _WinBash.WSL},
+        _win_bash_status in {_WinBashStatus.ABSENT, _WinBashStatus.WSL},
         reason="Specifically seems to fail on WSL bash (in spite of #1399)",
         raises=AssertionError,
     )
     @pytest.mark.xfail(
-        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1059,7 +1062,7 @@ def test_commit_msg_hook_success(self, rw_repo):
         self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
 
     @pytest.mark.xfail(
-        _WinBash() is _WinBash.WSL_NO_DISTRO,
+        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1070,7 +1073,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if _WinBash() is _WinBash.ABSENT:
+            if _win_bash_status is _WinBashStatus.ABSENT:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")

From cabb5728e75aab3ed73376702e219bd1514ee615 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 23 Nov 2023 22:24:24 -0500
Subject: [PATCH 284/994] Temporarily don't install WSL system to test xfail

This reverts "Install WSL system on CI for hook tests",
commit 9717b8d847b514f761ecaa423226054c73f2a325.

Assuming both cases are found to be working, the removed step will
either be brought back or replaced with a very similar step that
installs a different distribution.
---
 .github/workflows/pythonpackage.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index c2bbc1bbb..d21ea3949 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -35,12 +35,6 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
-    - name: Set up WSL
-      if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v2.0.2
-      with:
-        distribution: Debian
-
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From 2875ffa083b1e136cd647e6d087a4747aa85a4bc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 23 Nov 2023 22:33:26 -0500
Subject: [PATCH 285/994] Put back WSL on Windows CI; pare down debug info

This undoes "Temporarily don't install WSL system to test xfail"
(cabb572). It keeps Debian as the distribution. (Although the Debian
WSL system installs pretty fast already, it may still make sense to
try switching to Alpine in the future. But that might need to wait
until https://github.com/Vampire/setup-wsl/issues/50 is fixed.)

This also removes most of the commands in the WSL debugging step,
since the related machinery in test_index.py (_WinBashStatus) seems
to be in okay shape, condenses the smaller number of commands that
are retained there, and makes much less extensive reduction in the
general version and platform information commands as well. This is
to make workflow output easier to read, understand, and navigate.
---
 .github/workflows/cygwin-test.yml   |  4 +---
 .github/workflows/pythonpackage.yml | 28 ++++++++++------------------
 2 files changed, 11 insertions(+), 21 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 96728e51a..13a01dec4 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -70,9 +70,7 @@ jobs:
         command -v git python
         git version
         python --version
-        python -c 'import sys; print(sys.platform)'
-        python -c 'import os; print(os.name)'
-        python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
+        python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
 
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index d21ea3949..3915296ef 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -35,6 +35,12 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
+    - name: Set up WSL (Windows)
+      if: startsWith(matrix.os, 'windows')
+      uses: Vampire/setup-wsl@v2.0.2
+      with:
+        distribution: Debian
+
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh
@@ -62,29 +68,15 @@ jobs:
         command -v git python
         git version
         python --version
-        python -c 'import sys; print(sys.platform)'
-        python -c 'import os; print(os.name)'
-        python -c 'import git; print(git.compat.is_win)'  # NOTE: Deprecated. Use os.name directly.
+        python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
 
     # For debugging hook tests on native Windows systems that may have WSL.
-    - name: Show where bash.exe may be found
+    - name: Show bash.exe candidates (Windows)
       if: startsWith(matrix.os, 'windows')
       run: |
         set +e
-        type -a bash.exe
-        python -c 'import shutil; print(shutil.which("bash.exe"))'
-        bash.exe --version
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "--version"]); print(f"result: {p!r}")'
-        bash.exe -c 'echo "$BASH"'
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", """echo "$BASH" """]); print(f"result: {p!r}")'
-        bash.exe -c 'echo "$BASH_VERSION"'
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", """echo "$BASH_VERSION" """]); print(f"result: {p!r}")'
-        bash.exe -c 'printenv WSL_DISTRO_NAME'
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "printenv WSL_DISTRO_NAME"]); print(f"result: {p!r}")'
-        bash.exe -c 'ls -l /proc/sys/fs/binfmt_misc/WSLInterop'
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "ls -l /proc/sys/fs/binfmt_misc/WSLInterop"]); print(f"result: {p!r}")'
-        bash.exe -c 'uname -a'
-        python -c 'import subprocess; p = subprocess.run(["bash.exe", "-c", "uname -a"]); print(f"result: {p!r}")'
+        bash.exe -c 'printenv WSL_DISTRO_NAME; uname -a'
+        python -c 'import subprocess; subprocess.run(["bash.exe", "-c", "printenv WSL_DISTRO_NAME; uname -a"])'
       continue-on-error: true
 
     - name: Check types with mypy

From 0f8cd4ce4a7f942d793a8670257f8738ab18b757 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 23 Nov 2023 23:52:27 -0500
Subject: [PATCH 286/994] Treat XPASS status as a test failure

This causes full "failure" output to be printed when a test marked
xfail unexpectedly passes, and for the test run to be considered
failing as a result.

The immediate purpose of this change is to facilitate efficient
identification of recently introduced wrong or overbroad xfail
markings.

This behavior may eventually become the pytest default (see #1728
and references therein), and this could be retained even after the
current xpassing tests are investigated, to facilitate timely
detection of tests marked xfail of code that is newly working.

(Individual tests decorated `@pytest.mark.xfail` can still be
allowed to unexpectedly pass without it being treated like a test
failure, by passing strict=False explicitly.)
---
 pyproject.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pyproject.toml b/pyproject.toml
index 7109389d7..5c3117096 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@ filterwarnings = "ignore::DeprecationWarning"
 python_files = "test_*.py"
 tmp_path_retention_policy = "failed"
 testpaths = "test"  # Space separated list of paths from root e.g test tests doc/testing.
+xfail_strict = true  # Treat the XPASS status as a test failure (unless strict=False is passed).
 # --cov   coverage
 # --cov-report term  # send report to terminal term-missing -> terminal with line numbers  html  xml
 # --cov-report term-missing # to terminal with line numbers

From 82c361e7e1a20df96cc9e5c443b2c18fc20b18c9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 24 Nov 2023 02:05:29 -0500
Subject: [PATCH 287/994] Correct TestSubmodule.test_rename xfail condition

The xfail mark was added in 42a3d74, where the XPASS status on 3.7
was observed but not included in the condition. It turns out this
seems to XPASS on a much wider range of versions: all but 3.12.

Currently 3.12.0 is the latest stable version and no testing has
been done with alpha for 3.13. Most likely whatever causes this
test to fail on 3.12 would also apply to 3.13, but because I don't
understand the cause, I don't want to guess that, and instead wrote
the new condition to expect failure only on 3.12.* versions.
---
 test/test_submodule.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 68dbcc491..1e39924dd 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -950,13 +950,14 @@ def test_remove_norefs(self, rwdir):
         assert not sm.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        os.name == "nt" and (3, 12) <= sys.version_info < (3, 13),
         reason=(
             "The sm.move call fails. Submodule.move calls os.renames, which raises:\n"
             "PermissionError: [WinError 32] "
             "The process cannot access the file because it is being used by another process: "
             R"'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\mymodules\myname' "
             R"-> 'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\renamed\myname'"
+            "\nThis resembles other Windows errors, but seems only to affect Python 3.12 somehow."
         ),
         raises=PermissionError,
     )

From 0ae5dd19d5e7256b0876987948774b48e207f231 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 24 Nov 2023 02:19:56 -0500
Subject: [PATCH 288/994] Revert "Treat XPASS status as a test failure"

For its immediate goal, this facilitated identifying the XPASS
status of TestSubmodule.test_rename and verifying that the revised
xfail condition eliminated it.

In the test output, XPASS is written as a failure, with strict
xfail behavior noted. Contributors less familiar with marking tests
xfail may mistake this to mean some behavior of the code under test
was broken. So if strict_xfail is to be enabled, it might be best
to do that in a pull request devoted to it, and maybe even to add
to the docs, about how to recognize and handle newly xpassing
tests.

This reverts commit 0f8cd4ce4a7f942d793a8670257f8738ab18b757.
---
 pyproject.toml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 5c3117096..7109389d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,6 @@ filterwarnings = "ignore::DeprecationWarning"
 python_files = "test_*.py"
 tmp_path_retention_policy = "failed"
 testpaths = "test"  # Space separated list of paths from root e.g test tests doc/testing.
-xfail_strict = true  # Treat the XPASS status as a test failure (unless strict=False is passed).
 # --cov   coverage
 # --cov-report term  # send report to terminal term-missing -> terminal with line numbers  html  xml
 # --cov-report term-missing # to terminal with line numbers

From 0b7ee17849a48ca691eaa3cc5d3eb81a4e6592b7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 24 Nov 2023 20:14:35 -0500
Subject: [PATCH 289/994] Refine TestSubmodule.test_rename xfail condition

This further improves the condition that was corrected in 82c361e.
Testing on Python 3.13.0 alpha 2 shows the same failure as on 3.12
(that I'm at least right now consistently unable to produce on any
lower versions).

In addition, on both versions of CPython on Windows, the failure
seems to consistently resolve if two gc.collect() are placed just
above the code that calls sm.move(). A single call is consistently
insufficient. I haven't included any such calls in this commit,
since the focus here is on fixing xfail markings, and becuse such a
change may benefit from being evaluated separately and may merit
further accompanying changes. But that this behavior is exhibited
on both 3.12 and the 3.13 alpha further supports removing the
upper bound on the xfail marking.
---
 test/test_submodule.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 1e39924dd..7fbccf831 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -950,14 +950,14 @@ def test_remove_norefs(self, rwdir):
         assert not sm.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt" and (3, 12) <= sys.version_info < (3, 13),
+        os.name == "nt" and sys.version_info >= (3, 12),
         reason=(
             "The sm.move call fails. Submodule.move calls os.renames, which raises:\n"
             "PermissionError: [WinError 32] "
             "The process cannot access the file because it is being used by another process: "
             R"'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\mymodules\myname' "
             R"-> 'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\renamed\myname'"
-            "\nThis resembles other Windows errors, but seems only to affect Python 3.12 somehow."
+            "\nThis resembles other Windows errors, but only occurs starting in Python 3.12."
         ),
         raises=PermissionError,
     )

From 8621e892ed5ed05b1a304c9ea25a460f376677bb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 25 Nov 2023 00:01:33 -0500
Subject: [PATCH 290/994] Reword comment in _WinBashStatus.check for clarity

---
 test/test_index.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 9a520275c..e1bcd5b25 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -96,9 +96,9 @@ def check(cls):
             return cls.INAPPLICABLE
 
         try:
-            # Print rather than returning the test command's exit status so that if a
-            # failure occurs before we even get to this point, we will detect it. See
-            # https://superuser.com/a/1749811 for information on ways to check for WSL.
+            # Output rather than forwarding the test command's exit status so that if a
+            # failure occurs before we even get to this point, we will detect it. For
+            # information on ways to check for WSL, see https://superuser.com/a/1749811.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
             proc = subprocess.run(command, capture_output=True, check=True, text=True)

From 7ff3cee63c53520650db14bdf6e55551b8848567 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 25 Nov 2023 00:23:40 -0500
Subject: [PATCH 291/994] Make _WinBashStatus instances carry all their info

This changes _WinBashStatus from an enum to a sum type so that,
rather than having a global _WinBashStatus.ERROR_WHILE_CHECKING
object whose error_or_process attribute may be absent and, if
present, carries an object set on the most recent call to check(),
we get a _WinBashStatus.ErrorWhileChecking instance that carries
an error_or_process attribute specific to the call.

Ordinarily this would not make a difference, other than that the
global attribute usage was confusing in the code, because typically
_WinBashStatus.check() is called only once. However, when
debugging, having the old attribute on the same object be clobbered
is undesirable.

This adds the sumtypes library as a test dependency, to allow the
enum to be rewritten as a sum type without loss of clarity. This is
a development-only dependency; the main dependencies are unchanged.
---
 test-requirements.txt |  1 +
 test/test_index.py    | 66 ++++++++++++++++++++-----------------------
 2 files changed, 32 insertions(+), 35 deletions(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index 7cfb977a1..fcdc93c1d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -9,3 +9,4 @@ pytest-cov
 pytest-instafail
 pytest-mock
 pytest-sugar
+sumtypes
diff --git a/test/test_index.py b/test/test_index.py
index e1bcd5b25..f60a86309 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -3,7 +3,6 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import enum
 from io import BytesIO
 import logging
 import os
@@ -14,6 +13,7 @@
 import tempfile
 
 import pytest
+from sumtypes import constructor, sumtype
 
 from git import (
     IndexFile,
@@ -39,35 +39,34 @@
 log = logging.getLogger(__name__)
 
 
-@enum.unique
-class _WinBashStatus(enum.Enum):
+@sumtype
+class _WinBashStatus:
     """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
 
     Call :meth:`check` to check the status.
     """
 
-    INAPPLICABLE = enum.auto()
+    Inapplicable = constructor()
     """This system is not native Windows: either not Windows at all, or Cygwin."""
 
-    ABSENT = enum.auto()
+    Absent = constructor()
     """No command for ``bash.exe`` is found on the system."""
 
-    NATIVE = enum.auto()
+    Native = constructor()
     """Running ``bash.exe`` operates outside any WSL distribution (as with Git Bash)."""
 
-    WSL = enum.auto()
+    Wsl = constructor()
     """Running ``bash.exe`` calls ``bash`` in a WSL distribution."""
 
-    WSL_NO_DISTRO = enum.auto()
+    WslNoDistro = constructor()
     """Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
 
-    ERROR_WHILE_CHECKING = enum.auto()
+    ErrorWhileChecking = constructor("error_or_process")
     """Could not determine the status.
 
     This should not trigger a skip or xfail, as it typically indicates either a fixable
     problem on the test machine, such as an "Insufficient system resources exist to
     complete the requested service" error starting WSL, or a bug in this detection code.
-    ``ERROR_WHILE_CHECKING.error_or_process`` has details about the most recent failure.
     """
 
     @classmethod
@@ -93,7 +92,13 @@ def check(cls):
         administrators occasionally put executables there in lieu of extending ``PATH``.
         """
         if os.name != "nt":
-            return cls.INAPPLICABLE
+            return cls.Inapplicable()
+
+        no_distro_message = "Windows Subsystem for Linux has no installed distributions."
+
+        def error_running_bash(error):
+            log.error("Error running bash.exe to check WSL status: %s", error)
+            return cls.ErrorWhileChecking(error)
 
         try:
             # Output rather than forwarding the test command's exit status so that if a
@@ -103,30 +108,21 @@ def check(cls):
             command = ["bash.exe", "-c", script]
             proc = subprocess.run(command, capture_output=True, check=True, text=True)
         except FileNotFoundError:
-            return cls.ABSENT
+            return cls.Absent()
         except OSError as error:
-            return cls._error(error)
+            return error_running_bash(error)
         except subprocess.CalledProcessError as error:
-            no_distro_message = "Windows Subsystem for Linux has no installed distributions."
             if error.returncode == 1 and error.stdout.startswith(no_distro_message):
-                return cls.WSL_NO_DISTRO
-            return cls._error(error)
+                return cls.WslNoDistro()
+            return error_running_bash(error)
 
         status = proc.stdout.rstrip()
         if status == "0":
-            return cls.WSL
+            return cls.Wsl()
         if status == "1":
-            return cls.NATIVE
-        return cls._error(proc)
-
-    @classmethod
-    def _error(cls, error_or_process):
-        if isinstance(error_or_process, subprocess.CompletedProcess):
-            log.error("Strange output checking WSL status: %s", error_or_process.stdout)
-        else:
-            log.error("Error running bash.exe to check WSL status: %s", error_or_process)
-        cls.ERROR_WHILE_CHECKING.error_or_process = error_or_process
-        return cls.ERROR_WHILE_CHECKING
+            return cls.Native()
+        log.error("Strange output checking WSL status: %s", proc.stdout)
+        return cls.ErrorWhileChecking(proc)
 
 
 _win_bash_status = _WinBashStatus.check()
@@ -1001,7 +997,7 @@ class Mocked:
         self.assertEqual(rel, os.path.relpath(path, root))
 
     @pytest.mark.xfail(
-        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
+        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1012,7 +1008,7 @@ def test_pre_commit_hook_success(self, rw_repo):
         index.commit("This should not fail")
 
     @pytest.mark.xfail(
-        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
+        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1023,7 +1019,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if _win_bash_status is _WinBashStatus.ABSENT:
+            if type(_win_bash_status) is _WinBashStatus.Absent:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")
@@ -1039,12 +1035,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
             raise AssertionError("Should have caught a HookExecutionError")
 
     @pytest.mark.xfail(
-        _win_bash_status in {_WinBashStatus.ABSENT, _WinBashStatus.WSL},
+        type(_win_bash_status) in {_WinBashStatus.Absent, _WinBashStatus.Wsl},
         reason="Specifically seems to fail on WSL bash (in spite of #1399)",
         raises=AssertionError,
     )
     @pytest.mark.xfail(
-        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
+        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1062,7 +1058,7 @@ def test_commit_msg_hook_success(self, rw_repo):
         self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
 
     @pytest.mark.xfail(
-        _win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
+        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1073,7 +1069,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if _win_bash_status is _WinBashStatus.ABSENT:
+            if type(_win_bash_status) is _WinBashStatus.Absent:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")

From d5ed266f2208367e4300c29fa4cbda9cd2db0fb9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 25 Nov 2023 05:06:23 -0500
Subject: [PATCH 292/994] Use bytes in bash.exe check; retest no-distro case

This removes text=True from the subprocess.run call, changing str
literals to bytes where appropriate and (less importantly) using
"%r" instead of "%s" in log messages so it's clear that printing
the repr of a bytes object is, at least for now, intentional.

The reason for this is that the encoding of error messages produced
by running the WSL bash.exe, when it attempts but fails to use a
WSL system, varies based on what error occurred. When no systems
are installed, the output can be decoded as UTF-8. When an error
from "deeper down" is reported, at least for Bash/Service errors,
the output is encoded in UTF-16LE, and attempting to decode it as
UTF-8 interleaves lots of null characters in the best case. With a
bytes object, loss of information is avoided, and it is clear on
inspection that the output requires decoding.

The most common case of such an error is *probably*:

    Insufficient system resources exist to complete the requested service.
    Error code: Bash/Service/CreateInstance/CreateVm/HCS/0x800705aa

However, that is tricky to produce intentionally on some systems.
To produce a test error, "wsl --shutdown" can be run repeatedly
while a _WinBashStatus.check() call is in progress. This produces:

    The virtual machine or container was forcefully exited.
    Error code: Bash/Service/0x80370107

Assuming the output always includes the text "Error code:", it may
be feasible to reliably detect which cases it is. This could
especially improve the log message. But for now that is not done.

In adddition to changing from text to binary mode, this commit also
temporarily removes the setup-wsl step from the CI workflow again,
to verify on CI that the text-to-binary change doesn't break the
WslNoDistro case. Manual testing shows the other cases still work.
---
 .github/workflows/pythonpackage.yml |  6 ------
 test/test_index.py                  | 13 +++++++------
 2 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 3915296ef..cef24799e 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -35,12 +35,6 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
-    - name: Set up WSL (Windows)
-      if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v2.0.2
-      with:
-        distribution: Debian
-
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh
diff --git a/test/test_index.py b/test/test_index.py
index f60a86309..8824d6648 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -94,10 +94,11 @@ def check(cls):
         if os.name != "nt":
             return cls.Inapplicable()
 
-        no_distro_message = "Windows Subsystem for Linux has no installed distributions."
+        # Use bytes because messages for different WSL errors use different encodings.
+        no_distro_message = b"Windows Subsystem for Linux has no installed distributions."
 
         def error_running_bash(error):
-            log.error("Error running bash.exe to check WSL status: %s", error)
+            log.error("Error running bash.exe to check WSL status: %r", error)
             return cls.ErrorWhileChecking(error)
 
         try:
@@ -106,7 +107,7 @@ def error_running_bash(error):
             # information on ways to check for WSL, see https://superuser.com/a/1749811.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
-            proc = subprocess.run(command, capture_output=True, check=True, text=True)
+            proc = subprocess.run(command, capture_output=True, check=True)
         except FileNotFoundError:
             return cls.Absent()
         except OSError as error:
@@ -117,11 +118,11 @@ def error_running_bash(error):
             return error_running_bash(error)
 
         status = proc.stdout.rstrip()
-        if status == "0":
+        if status == b"0":
             return cls.Wsl()
-        if status == "1":
+        if status == b"1":
             return cls.Native()
-        log.error("Strange output checking WSL status: %s", proc.stdout)
+        log.error("Strange output checking WSL status: %r", proc.stdout)
         return cls.ErrorWhileChecking(proc)
 
 

From 496acaac5d2cfcbe4edf8ace1bfae452f281ff15 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 26 Nov 2023 00:27:57 -0500
Subject: [PATCH 293/994] Handle multiple encodings for WSL error messages

This affects the test suite only. It improves _WinBashStatus.

When bash.exe is the WSL wrapper, and it reports an error that
prevents bash in a WSL system from ever actually being run, the
message may be encoded in a narrow or multibyte encoding, or may
instead be in Windows's native UTF-16LE encoding. This was partly
handled before, by assuming the message indicating the absence of
any installed WSL distribuions could be interpreted as UTF-8, but
matching against bytes and not actually decoding it or other error
messages. That presented a few problems, which are rememedied here:

- It turns out that this "Windows Subsystem for Linux has no
  installed distributions" message actually can be in UTF-16LE too.
  This happens when it is part of a message that also reports a
  textual error code like:

  Bash/Service/CreateInstance/GetDefaultDistro/WSL_E_DEFAULT_DISTRO_NOT_FOUND

  Therefore, narrow-encoding matching was not sufficient.

- Logged messages were hard to understand, because they were
  printing the reprs of bytes objects, which sometimes were
  UTF-16LE and thus had many "\x00" interspersed in them.

- The cases were not broken down elegantly. The ErrorWhileChecking
  case could hold a CompletedProcess, a CalledProcessError, or an
  OSError. This is now replaced with a WinError case for the rare
  scenario where CreateProcessW fails in a completely unexpected
  way, and a CheckError case for when the exit status or output of
  bash.exe indicates an error other than one we want to handle.
  CheckError has two attributes: `process` for the CompletedProcess
  (CalledProcessError is no longer used, and CompletedProcess has
  `returncode` and other attributes that provide the same info),
  and `message` for the decoded output.
---
 test/test_index.py | 44 ++++++++++++++++++++------------------------
 1 file changed, 20 insertions(+), 24 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 8824d6648..bd38ef3bb 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -61,13 +61,11 @@ class _WinBashStatus:
     WslNoDistro = constructor()
     """Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
 
-    ErrorWhileChecking = constructor("error_or_process")
-    """Could not determine the status.
+    CheckError = constructor("process", "message")
+    """Running ``bash.exe`` fails in an unexpected error or gives unexpected output."""
 
-    This should not trigger a skip or xfail, as it typically indicates either a fixable
-    problem on the test machine, such as an "Insufficient system resources exist to
-    complete the requested service" error starting WSL, or a bug in this detection code.
-    """
+    WinError = constructor("exception")
+    """``bash.exe`` may exist but can't run. ``CreateProcessW`` fails unexpectedly."""
 
     @classmethod
     def check(cls):
@@ -94,12 +92,7 @@ def check(cls):
         if os.name != "nt":
             return cls.Inapplicable()
 
-        # Use bytes because messages for different WSL errors use different encodings.
-        no_distro_message = b"Windows Subsystem for Linux has no installed distributions."
-
-        def error_running_bash(error):
-            log.error("Error running bash.exe to check WSL status: %r", error)
-            return cls.ErrorWhileChecking(error)
+        no_distro_message = "Windows Subsystem for Linux has no installed distributions."
 
         try:
             # Output rather than forwarding the test command's exit status so that if a
@@ -107,23 +100,26 @@ def error_running_bash(error):
             # information on ways to check for WSL, see https://superuser.com/a/1749811.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
-            proc = subprocess.run(command, capture_output=True, check=True)
+            process = subprocess.run(command, capture_output=True)
         except FileNotFoundError:
             return cls.Absent()
         except OSError as error:
-            return error_running_bash(error)
-        except subprocess.CalledProcessError as error:
-            if error.returncode == 1 and error.stdout.startswith(no_distro_message):
-                return cls.WslNoDistro()
-            return error_running_bash(error)
-
-        status = proc.stdout.rstrip()
-        if status == b"0":
+            return cls.WinError(error)
+
+        encoding = "utf-16le" if b"\r\0\n\0" in process.stdout else "utf-8"
+        text = process.stdout.decode(encoding).rstrip()  # stdout includes WSL errors.
+
+        if process.returncode == 1 and text.startswith(no_distro_message):
+            return cls.WslNoDistro()
+        if process.returncode != 0:
+            log.error("Error running bash.exe to check WSL status: %s", text)
+            return cls.CheckError(process, text)
+        if text == "0":
             return cls.Wsl()
-        if status == b"1":
+        if text == "1":
             return cls.Native()
-        log.error("Strange output checking WSL status: %r", proc.stdout)
-        return cls.ErrorWhileChecking(proc)
+        log.error("Strange output checking WSL status: %s", text)
+        return cls.CheckError(process, text)
 
 
 _win_bash_status = _WinBashStatus.check()

From d779a7546f4bf69783cf38be4679a0a229578ef9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 27 Nov 2023 04:35:02 -0500
Subject: [PATCH 294/994] Don't assume WSL-related bash.exe error is English

Instead of searching for an English sentence from the error
message, this searches for the specific aka.ms short URL it
contains in all languages, which points to a page about downloading
distributions for WSL from the Microsoft Store (Windows Store).

The URL is controlled and hosted by Microsoft and intended as a
permalink. Thus this approach should be more reliable even for
English, as the specific wording of the message might be rephrased
over time, so this may have a lower risk of false negatives.
Because it only makes sense to give a link for obtaining a
distribution in an error message when no distribution is installed
already, the risk of false positives should also be fairly low.

The URL is included in the output telling the user they have no
distributions installed even on systems like Windows Server where
the Microsoft Store is not itself available, and even in situations
where WSL has successfully downloaded and unpacked a distribution
but could not run it the first time because it is using WSL 2 but
the necessary virtualization is unavailable. Because these are
included among the situations where the hook tests should be marked
xfail, it is suitable for the purpose at hand.

Furthermore, while this only works if we correctly guess if the
encoding is UTF-16LE or not, it should be *fairly* robust against
incorrect decoding of the message where a single-byte encoding is
treated as UTF-8, or UTF-8 is treated as a single-byte encoding, or
one single-byte encoding is treated as another (i.e., wrong ANSI
code page). But some related encoding subtleties remain to be
resolved.

Although reliability is likely improved even for English, to
faciliate debugging the WslNoDistro case now carries analogous
`process` and `message` arguments to the `CheckError` case.

On some Windows systems, wsl.exe exists in System32 but bash.exe
does not. This is not inherently a problem for run_commit_hook as
it currently stands, which is just trying to use whatever bash.exe
is available. (It is not clear that WSL should be preferred.) This
is currently so, on some systems, where WSL is not really set up;
wsl.exe can be used to do so. This is a situation where no WSL
distributions are present, but because the bash.exe WSL wrapper is
also absent, it is not a problem.

However, I had wrongly claimed in the _WinBashStatus.check
docstring that bash.exe is in System32 whenever WSL is present. So
this also revises the docstring to say only that this is usually so
(plus some further revision for clarity).
---
 test/test_index.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index bd38ef3bb..a7ca2158b 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -8,6 +8,7 @@
 import os
 import os.path as osp
 from pathlib import Path
+import re
 from stat import S_ISLNK, ST_MODE
 import subprocess
 import tempfile
@@ -58,7 +59,7 @@ class _WinBashStatus:
     Wsl = constructor()
     """Running ``bash.exe`` calls ``bash`` in a WSL distribution."""
 
-    WslNoDistro = constructor()
+    WslNoDistro = constructor("process", "message")
     """Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
 
     CheckError = constructor("process", "message")
@@ -80,20 +81,18 @@ def check(cls):
 
         :func:`index.fun.run_commit_hook` uses :class:`subprocess.Popen`, including when
         it runs ``bash.exe`` on Windows. It doesn't pass ``shell=True`` (and shouldn't).
-        On Windows, `Popen` calls ``CreateProcessW``, which searches several locations
-        prior to using the ``PATH`` environment variable. It is expected to search the
-        ``System32`` directory, even if another directory containing the executable
-        precedes it in ``PATH``. (Other differences are less relevant here.) When WSL is
-        installed, even with no distributions, ``bash.exe`` exists in ``System32``, and
-        `Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
-        If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
+        On Windows, `Popen` calls ``CreateProcessW``, which checks some locations before
+        using the ``PATH`` environment variable. It is expected to try the ``System32``
+        directory, even if another directory containing the executable precedes it in
+        ``PATH``. (Other differences are less relevant here.) When WSL is present, even
+        with no distributions, ``bash.exe`` usually exists in ``System32``, and `Popen`
+        finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI. If WSL
+        is absent, ``System32`` may still have ``bash.exe``, as Windows users and
         administrators occasionally put executables there in lieu of extending ``PATH``.
         """
         if os.name != "nt":
             return cls.Inapplicable()
 
-        no_distro_message = "Windows Subsystem for Linux has no installed distributions."
-
         try:
             # Output rather than forwarding the test command's exit status so that if a
             # failure occurs before we even get to this point, we will detect it. For
@@ -106,11 +105,12 @@ def check(cls):
         except OSError as error:
             return cls.WinError(error)
 
+        # FIXME: When not UTF-16LE: try local ANSI code page, then fall back to UTF-8.
         encoding = "utf-16le" if b"\r\0\n\0" in process.stdout else "utf-8"
         text = process.stdout.decode(encoding).rstrip()  # stdout includes WSL errors.
 
-        if process.returncode == 1 and text.startswith(no_distro_message):
-            return cls.WslNoDistro()
+        if process.returncode == 1 and re.search(r"\bhttps://aka.ms/wslstore\b", text):
+            return cls.WslNoDistro(process, text)
         if process.returncode != 0:
             log.error("Error running bash.exe to check WSL status: %s", text)
             return cls.CheckError(process, text)

From 9ac243884ebe2d614f03302104064ef22b2aee0f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 27 Nov 2023 17:09:31 -0500
Subject: [PATCH 295/994] Handle encodings better; make the sum type "public"

Windows does not have a direct analogue of LANG=C or LC_ALL=C. Some
programs give them special treatment, but they do not affect the
way localized behavior of the Windows API operates. In particular,
the bash.exe WSL wrapper, as well as wsl.exe and wslconfig.exe, do
not produce their own localized messages (for errors not
originating in a running distribution) when they are set. Windows
also provides significant localization through localized versions
of Windows, so changing language settings in Windows, even
system-wide, does not always produce the same effect that many or
most Windows users who use a particular language would experience.

Various encodings may appear when bash.exe is WSL-related but gives
its own error message. Such a message is often in UTF-16LE, which
is what Windows uses internally, and preserves any localization.
That is the more well behaved scenario and already was detected;
this commit moves, but does not change, the code for that.

The situation where it is not UTF-16LE was previously handled by
treating it as UTF-8. Because the default strict error treatment
was used, this would error out in test discovery in some localized
setups, preventing all tests in test_index from running, including
the majority of them that are not related to hooks. This fixes that
by doing better detection that should decode the mesages correctly
most of the time, that should in practice decode them well enough
to tell (by the aka.ms URL) if the message is complaining about
there being no installed distribution all(?) of the time, and that
should avoid breaking unrelated tests even if that can't be done.

An English non-UTF-16LE message appears on GitHub Actions CI when
no distribution is installed. Testing of this situation on other
languages was performed in a virtual machine on a development
machine.

That the message is output in a narrow character set some of the
time when bash.exe produces it appears to be a limitation of the
bash.exe wrapper. In particular, with a zh-CN version of Windows
(and with the language not changed to anything else), a localized
message in Simplified Chinese was correctly printed when running
wsl.exe, but running bash.exe produced literal "?" characters in
place of Chinese characters (it was not a display or font issue,
and they were "?" and not Unicode replacement characters). The
change here does not overcome that; the literal "?" characters will
be included. But "https://aka.ms/wslstore" is still present if it
is an error about not having any distributions, so the correct
status is still inferred.

For more information on code pages in Windows, see:
https://serverfault.com/a/836221

The following alternatives to all or part of the approach taken
here were considered but, at least for now, not done, because they
would not clearly be simpler or more correct:

- Abandoning this "run bash.exe and see what it shows us" approach
  altogether and instead reimplementing the rules CreateProcessW
  uses, to find if the bash.exe the system finds is the one in
  System32, and then, if so, checking the metadata in that
  executable to determine if it's the WSL wrapper. I believe that
  would be even more complex to do correctly than it seems; the
  behavior noted in the WinBashStatus docstring and recent commit
  messages is not the whole story. The documented search order for
  CreateProcessW seems not to be followed in some circumstances.
  One is that the Windows Store version of Python seems always to
  forgo the usual System32 search that precedes seaching directories
  in PATH. It looks like there may also be some subtleties in which
  directories 32-bit builds search in.

- Using chardet. Although the chardet library is excellent, it is
  not clear that the code needed to bridge this highly specific use
  case to it would be simpler than the code currently in use. Some
  of the work might still need to be done by other means; when I
  tested it out for this, this did not detect the UTF-16LE messages
  as such for English. (They are often also valid UTF-8, because
  interleaving null characters is, while strange, permitted.)

- Also calling wsl.exe and/or wslconfig.exe. It's still necessary
  to call bash.exe, because it may not be the WSL bash, even on a
  system with WSL fully set up. Furthermore, those tools' output
  seem to vary in some complex ways, too. Using only one subprocess
  for the detection seemed the simplest. Even using "wsl --list"
  would introduce significant additional logic. Sometimes its
  output is a list of distributions, sometimes it is an error
  message, and if WSL is not set up it may be a help message.

- Using the Windows API to check for WSL systems.
  https://learn.microsoft.com/en-us/windows/win32/api/wslapi/ does
  not currently include functions to list registered distributions.

- Attempting to get wsl.exe to produce an English message using
  Windows API techniques like those used in Locale Emulator. This
  would be complicated, somewhat unintuitive and error prone to do
  in Python, and I am not sure how well it would work on a system
  that does not have an English language pack installed.

- Checking on disk for WSL distributions in the places they are
  most often expected to be. This would intertwine WinBashStatus
  with deep details of how WSL actually operates, and this seems
  like the sort of thing that is likely to change in the future.

However, there may be a more straightforward way to do this (that
is at least as correct and that remains transparent to debug).
Especially if the WinBashStatus class remains in test_index for
long (rather than just being used to aid in debugging existing test
falures and possible subsequent design decisions for making commit
hooks work more robustly on Windows in GitPython), then this may
be worth revisiting.

Thus it is *not* with the intention of treating WinBashStatus as a
"stable" part of the test suite that it is renamed from
_WinBashStatus. This is instead done because:

- Like HOOKS_SHEBANG, it makes sense to import it when figuring out
  how the tests work or debugging them.

- When debugging, it is intended that it be imported to call
  check() and examine the resulting `process` and `message`
  information, at least in the CheckError case.
---
 test/test_index.py | 70 +++++++++++++++++++++++++++++++++++-----------
 1 file changed, 54 insertions(+), 16 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index a7ca2158b..39408e836 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -41,10 +41,13 @@
 
 
 @sumtype
-class _WinBashStatus:
+class WinBashStatus:
     """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
 
     Call :meth:`check` to check the status.
+
+    The :class:`CheckError` and :class:`WinError` cases should not typically be used in
+    ``skip`` or ``xfail`` mark conditions, because they represent unexpected situations.
     """
 
     Inapplicable = constructor()
@@ -84,10 +87,10 @@ def check(cls):
         On Windows, `Popen` calls ``CreateProcessW``, which checks some locations before
         using the ``PATH`` environment variable. It is expected to try the ``System32``
         directory, even if another directory containing the executable precedes it in
-        ``PATH``. (Other differences are less relevant here.) When WSL is present, even
-        with no distributions, ``bash.exe`` usually exists in ``System32``, and `Popen`
-        finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI. If WSL
-        is absent, ``System32`` may still have ``bash.exe``, as Windows users and
+        ``PATH``. (The other differences are less relevant here.) When WSL is present,
+        even with no distributions, ``bash.exe`` usually exists in ``System32``, and
+        `Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
+        If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
         administrators occasionally put executables there in lieu of extending ``PATH``.
         """
         if os.name != "nt":
@@ -105,9 +108,7 @@ def check(cls):
         except OSError as error:
             return cls.WinError(error)
 
-        # FIXME: When not UTF-16LE: try local ANSI code page, then fall back to UTF-8.
-        encoding = "utf-16le" if b"\r\0\n\0" in process.stdout else "utf-8"
-        text = process.stdout.decode(encoding).rstrip()  # stdout includes WSL errors.
+        text = cls._decode(process.stdout).rstrip()  # stdout includes WSL's own errors.
 
         if process.returncode == 1 and re.search(r"\bhttps://aka.ms/wslstore\b", text):
             return cls.WslNoDistro(process, text)
@@ -121,8 +122,45 @@ def check(cls):
         log.error("Strange output checking WSL status: %s", text)
         return cls.CheckError(process, text)
 
+    @staticmethod
+    def _decode(stdout):
+        """Decode ``bash.exe`` output as best we can. (This is used only on Windows.)"""
+        # When bash.exe is the WSL wrapper but the output is from WSL itself rather than
+        # code running in a distribution, the output is often in UTF-16LE, which Windows
+        # uses internally. The UTF-16LE representation of a Windows-style line ending is
+        # rarely seen otherwise, so use it to detect this situation.
+        if b"\r\0\n\0" in stdout:
+            return stdout.decode("utf-16le")
+
+        import winreg
+
+        # At this point, the output is probably either empty or not UTF-16LE. It's often
+        # UTF-8 from inside a WSL distro or a non-WSL bash shell. But our test command
+        # only uses the ASCII subset, so it's safe to guess wrong for that command's
+        # output. Errors from inside a WSL distro or non-WSL bash.exe are arbitrary, but
+        # unlike WSL's own messages, go to stderr, not stdout. So we can try the system
+        # active code page first. (Although console programs usually use the OEM code
+        # page, the ACP seems more accurate here. For example, on en-US Windows set to
+        # fr-FR, the message, if not UTF-16LE, is windows-1252, same as the ACP, while
+        # the OEM code page on such a system defaults to 437, which can't decode it.)
+        hklm_path = R"SYSTEM\CurrentControlSet\Control\Nls\CodePage"
+        with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, hklm_path) as key:
+            value, _ = winreg.QueryValueEx(key, "ACP")
+        try:
+            return stdout.decode(f"cp{value}")
+        except UnicodeDecodeError:
+            pass
+        except LookupError as error:
+            log.warning("%s", str(error))  # Message already says "Unknown encoding:".
+
+        # Assume UTF-8. If we don't have valid UTF-8, substitute Unicode replacement
+        # characters. (For example, on zh-CN Windows set to fr-FR, error messages from
+        # WSL itself, if not UTF-16LE, are in windows-1252, even though the ACP and OEM
+        # code pages are 936; decoding as code page 936 or as UTF-8 both have errors.)
+        return stdout.decode("utf-8", errors="replace")
+
 
-_win_bash_status = _WinBashStatus.check()
+_win_bash_status = WinBashStatus.check()
 
 
 def _make_hook(git_dir, name, content, make_exec=True):
@@ -994,7 +1032,7 @@ class Mocked:
         self.assertEqual(rel, os.path.relpath(path, root))
 
     @pytest.mark.xfail(
-        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1005,7 +1043,7 @@ def test_pre_commit_hook_success(self, rw_repo):
         index.commit("This should not fail")
 
     @pytest.mark.xfail(
-        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1016,7 +1054,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if type(_win_bash_status) is _WinBashStatus.Absent:
+            if type(_win_bash_status) is WinBashStatus.Absent:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")
@@ -1032,12 +1070,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
             raise AssertionError("Should have caught a HookExecutionError")
 
     @pytest.mark.xfail(
-        type(_win_bash_status) in {_WinBashStatus.Absent, _WinBashStatus.Wsl},
+        type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.Wsl},
         reason="Specifically seems to fail on WSL bash (in spite of #1399)",
         raises=AssertionError,
     )
     @pytest.mark.xfail(
-        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=HookExecutionError,
     )
@@ -1055,7 +1093,7 @@ def test_commit_msg_hook_success(self, rw_repo):
         self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
 
     @pytest.mark.xfail(
-        type(_win_bash_status) is _WinBashStatus.WslNoDistro,
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
         reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
         raises=AssertionError,
     )
@@ -1066,7 +1104,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
         try:
             index.commit("This should fail")
         except HookExecutionError as err:
-            if type(_win_bash_status) is _WinBashStatus.Absent:
+            if type(_win_bash_status) is WinBashStatus.Absent:
                 self.assertIsInstance(err.status, OSError)
                 self.assertEqual(err.command, [hp])
                 self.assertEqual(err.stdout, "")

From b07e5c7d997b49d980bf3d2d749839726663b274 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 27 Nov 2023 19:58:08 -0500
Subject: [PATCH 296/994] Put back WSL on Windows CI

So the Windows test jobs can run the three out of four hook tests
that are able to pass with WSL bash, and so that we are able to
observe the expected failure on the one that still fails.

This undoes the removal of the setup-wsl step in d5ed266, now that
the test suite's bash.exe status checking logic, used for xfail
marks (and also usable in debugging), is internationalized. The CI
step was omitted during some of this process, to make sure
WinBashStatus would still detect the specific situation on the
GitHub Actions runner that occurs when no WSL distro is available.

(This commit thus has much of the same relationship to d5ed266
as 2875ffa had to cabb572.)
---
 .github/workflows/pythonpackage.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index cef24799e..3915296ef 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -35,6 +35,12 @@ jobs:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
+    - name: Set up WSL (Windows)
+      if: startsWith(matrix.os, 'windows')
+      uses: Vampire/setup-wsl@v2.0.2
+      with:
+        distribution: Debian
+
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From 3303c740bd9aae3ec2fa6b0f1a750bba9ad2b60e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 28 Nov 2023 15:55:29 -0500
Subject: [PATCH 297/994] Improve readability of WinBashStatus class

- Factor out the code to get the Windows ACP to a helper function,
  and comment to explain why it doesn't use locale.getencoding.

- Remove nonessential material in the WinBashStatus.check docstring
  and reword the rest for clarity.

- Drop reStructuredText notation in the WinBashStatus docstrings,
  because in this case it seems to be making them harder to read in
  the code (we are not generating Sphinx documentation for tests.)

- Revise the comments on specific steps in WinBashStatus._decode
  for accuracy and clarity.
---
 test/test_index.py | 96 +++++++++++++++++++++++-----------------------
 1 file changed, 47 insertions(+), 49 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 39408e836..bfdbca867 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -40,58 +40,60 @@
 log = logging.getLogger(__name__)
 
 
+def _get_windows_ansi_encoding():
+    """Get the encoding specified by the Windows system-wide ANSI active code page."""
+    # locale.getencoding may work but is only in Python 3.11+. Use the registry instead.
+    import winreg
+
+    hklm_path = R"SYSTEM\CurrentControlSet\Control\Nls\CodePage"
+    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, hklm_path) as key:
+        value, _ = winreg.QueryValueEx(key, "ACP")
+    return f"cp{value}"
+
+
 @sumtype
 class WinBashStatus:
     """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
 
-    Call :meth:`check` to check the status.
-
-    The :class:`CheckError` and :class:`WinError` cases should not typically be used in
-    ``skip`` or ``xfail`` mark conditions, because they represent unexpected situations.
+    Call check() to check the status. (CheckError and WinError should not typically be
+    used to trigger skip or xfail, because they represent unexpected situations.)
     """
 
     Inapplicable = constructor()
     """This system is not native Windows: either not Windows at all, or Cygwin."""
 
     Absent = constructor()
-    """No command for ``bash.exe`` is found on the system."""
+    """No command for bash.exe is found on the system."""
 
     Native = constructor()
-    """Running ``bash.exe`` operates outside any WSL distribution (as with Git Bash)."""
+    """Running bash.exe operates outside any WSL distribution (as with Git Bash)."""
 
     Wsl = constructor()
-    """Running ``bash.exe`` calls ``bash`` in a WSL distribution."""
+    """Running bash.exe calls bash in a WSL distribution."""
 
     WslNoDistro = constructor("process", "message")
-    """Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
+    """Running bash.exe tries to run bash on a WSL distribution, but none exists."""
 
     CheckError = constructor("process", "message")
-    """Running ``bash.exe`` fails in an unexpected error or gives unexpected output."""
+    """Running bash.exe fails in an unexpected error or gives unexpected output."""
 
     WinError = constructor("exception")
-    """``bash.exe`` may exist but can't run. ``CreateProcessW`` fails unexpectedly."""
+    """bash.exe may exist but can't run. CreateProcessW fails unexpectedly."""
 
     @classmethod
     def check(cls):
-        """Check the status of the ``bash.exe`` :func:`index.fun.run_commit_hook` uses.
-
-        This uses EAFP, attempting to run a command via ``bash.exe``. Which ``bash.exe``
-        is used can't be reliably discovered by :func:`shutil.which`, which approximates
-        how a shell is expected to search for an executable. On Windows, there are major
-        differences between how executables are found by a shell and otherwise. (This is
-        the cmd.exe Windows shell, and shouldn't be confused with bash.exe itself. That
-        the command being looked up also happens to be an interpreter is not relevant.)
-
-        :func:`index.fun.run_commit_hook` uses :class:`subprocess.Popen`, including when
-        it runs ``bash.exe`` on Windows. It doesn't pass ``shell=True`` (and shouldn't).
-        On Windows, `Popen` calls ``CreateProcessW``, which checks some locations before
-        using the ``PATH`` environment variable. It is expected to try the ``System32``
-        directory, even if another directory containing the executable precedes it in
-        ``PATH``. (The other differences are less relevant here.) When WSL is present,
-        even with no distributions, ``bash.exe`` usually exists in ``System32``, and
-        `Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
-        If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
-        administrators occasionally put executables there in lieu of extending ``PATH``.
+        """Check the status of the bash.exe that run_commit_hook will try to use.
+
+        This runs a command with bash.exe and checks the result. On Windows, shell and
+        non-shell executable search differ; shutil.which often finds the wrong bash.exe.
+
+        run_commit_hook uses Popen, including to run bash.exe on Windows. It doesn't
+        pass shell=True (and shouldn't). On Windows, Popen calls CreateProcessW, which
+        checks some locations before using the PATH environment variable. It is expected
+        to try System32, even if another directory with the executable precedes it in
+        PATH. When WSL is present, even with no distributions, bash.exe usually exists
+        in System32; Popen finds it even if a shell would run another one, as on CI.
+        (Without WSL, System32 may still have bash.exe; users sometimes put it there.)
         """
         if os.name != "nt":
             return cls.Inapplicable()
@@ -124,7 +126,7 @@ def check(cls):
 
     @staticmethod
     def _decode(stdout):
-        """Decode ``bash.exe`` output as best we can. (This is used only on Windows.)"""
+        """Decode bash.exe output as best we can."""
         # When bash.exe is the WSL wrapper but the output is from WSL itself rather than
         # code running in a distribution, the output is often in UTF-16LE, which Windows
         # uses internally. The UTF-16LE representation of a Windows-style line ending is
@@ -132,31 +134,27 @@ def _decode(stdout):
         if b"\r\0\n\0" in stdout:
             return stdout.decode("utf-16le")
 
-        import winreg
-
-        # At this point, the output is probably either empty or not UTF-16LE. It's often
-        # UTF-8 from inside a WSL distro or a non-WSL bash shell. But our test command
-        # only uses the ASCII subset, so it's safe to guess wrong for that command's
-        # output. Errors from inside a WSL distro or non-WSL bash.exe are arbitrary, but
-        # unlike WSL's own messages, go to stderr, not stdout. So we can try the system
-        # active code page first. (Although console programs usually use the OEM code
-        # page, the ACP seems more accurate here. For example, on en-US Windows set to
-        # fr-FR, the message, if not UTF-16LE, is windows-1252, same as the ACP, while
-        # the OEM code page on such a system defaults to 437, which can't decode it.)
-        hklm_path = R"SYSTEM\CurrentControlSet\Control\Nls\CodePage"
-        with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, hklm_path) as key:
-            value, _ = winreg.QueryValueEx(key, "ACP")
+        # At this point, the output is either blank or probably not UTF-16LE. It's often
+        # UTF-8 from inside a WSL distro or non-WSL bash shell. Our test command only
+        # uses the ASCII subset, so we can safely guess a wrong code page for it. Errors
+        # from such an environment can contain any text, but unlike WSL's own messages,
+        # they go to stderr, not stdout. So we can try the system ANSI code page first.
+        # (Console programs often use the OEM code page, but the ACP seems more accurate
+        # here. For example, on en-US Windows with the original system code page but the
+        # display language set to fr-FR, the message, if not UTF-16LE, is windows-1252,
+        # same as the ACP, while the OEMCP is 437, which can't decode its accents.)
+        acp = _get_windows_ansi_encoding()
         try:
-            return stdout.decode(f"cp{value}")
+            return stdout.decode(acp)
         except UnicodeDecodeError:
             pass
         except LookupError as error:
             log.warning("%s", str(error))  # Message already says "Unknown encoding:".
 
-        # Assume UTF-8. If we don't have valid UTF-8, substitute Unicode replacement
-        # characters. (For example, on zh-CN Windows set to fr-FR, error messages from
-        # WSL itself, if not UTF-16LE, are in windows-1252, even though the ACP and OEM
-        # code pages are 936; decoding as code page 936 or as UTF-8 both have errors.)
+        # Assume UTF-8. If invalid, substitute Unicode replacement characters. (For
+        # example, on zh-CN Windows set to display fr-FR, errors from WSL itself, if not
+        # UTF-16LE, are in windows-1252, even though the ANSI and OEM code pages both
+        # default to 936, and decoding as code page 936 or as UTF-8 both have errors.)
         return stdout.decode("utf-8", errors="replace")
 
 

From e00fffc918da5cd6c3c749d1d2e59d8ae6835189 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 28 Nov 2023 16:51:10 -0500
Subject: [PATCH 298/994] Shorten comments on _decode steps

This removes the parenthesized examples from the per-step comments
in the WinBashStatus._decode helper.
---
 test/test_index.py | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index bfdbca867..cd1c37efc 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -139,10 +139,6 @@ def _decode(stdout):
         # uses the ASCII subset, so we can safely guess a wrong code page for it. Errors
         # from such an environment can contain any text, but unlike WSL's own messages,
         # they go to stderr, not stdout. So we can try the system ANSI code page first.
-        # (Console programs often use the OEM code page, but the ACP seems more accurate
-        # here. For example, on en-US Windows with the original system code page but the
-        # display language set to fr-FR, the message, if not UTF-16LE, is windows-1252,
-        # same as the ACP, while the OEMCP is 437, which can't decode its accents.)
         acp = _get_windows_ansi_encoding()
         try:
             return stdout.decode(acp)
@@ -151,10 +147,7 @@ def _decode(stdout):
         except LookupError as error:
             log.warning("%s", str(error))  # Message already says "Unknown encoding:".
 
-        # Assume UTF-8. If invalid, substitute Unicode replacement characters. (For
-        # example, on zh-CN Windows set to display fr-FR, errors from WSL itself, if not
-        # UTF-16LE, are in windows-1252, even though the ANSI and OEM code pages both
-        # default to 936, and decoding as code page 936 or as UTF-8 both have errors.)
+        # Assume UTF-8. If invalid, substitute Unicode replacement characters.
         return stdout.decode("utf-8", errors="replace")
 
 

From 6727d0e5d8e6e1ef066781b46122b210d2117497 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 29 Nov 2023 07:09:08 +0800
Subject: [PATCH 299/994] Always read metadata files as UTF-8 in setup.py

This passes `encoding="utf-8"` to the `open` calls in setup.py, so
the readme, version, and requirements files are always read as
UTF-8, even on systems whose locale is not UTF-8. This fixes the
bug described in #1747 where installation other than from a
pre-built wheel would fail on many Windows systems using
non-European languages.

The specific problem occurred with the README.md file. The
requirements files are less likely to contain characters not in the
ASCII subset, though maybe they could come to contain them, perhaps
in comments. The VERSION file is even less likely to ever contain
such characters. Nonetheless, for consistency, because it is a best
practice, and because it appears to be the intent of the existing
code, encoding="utf=8" is added for opening all of them.

This change is tested on a system whose locale uses Windows code
page 936. Editable installation, as well as the other affected ways
of installing (and building) described in #1747, are now working.
---
 setup.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/setup.py b/setup.py
index f40f7280c..73d1ae952 100755
--- a/setup.py
+++ b/setup.py
@@ -7,16 +7,16 @@
 import os
 import sys
 
-with open(os.path.join(os.path.dirname(__file__), "VERSION")) as ver_file:
+with open(os.path.join(os.path.dirname(__file__), "VERSION"), encoding="utf-8") as ver_file:
     VERSION = ver_file.readline().strip()
 
-with open("requirements.txt") as reqs_file:
+with open("requirements.txt", encoding="utf-8") as reqs_file:
     requirements = reqs_file.read().splitlines()
 
-with open("test-requirements.txt") as reqs_file:
+with open("test-requirements.txt", encoding="utf-8") as reqs_file:
     test_requirements = reqs_file.read().splitlines()
 
-with open("README.md") as rm_file:
+with open("README.md", encoding="utf-8") as rm_file:
     long_description = rm_file.read()
 
 

From e309b35dc216db4596374e1c1a5730e2690deb77 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 13 Nov 2023 22:44:08 -0500
Subject: [PATCH 300/994] Mock out lchmod functions in
 _patch_for_wrapping_test?

TestRmtree._patch_for_wrapping_test already mocked out the regular
chmod functions in the os module and the Path class, to test what
happens when changing permissions cannot fix an error. But there
are also, on some systems and Python versions, lchmod versions of
these functions. This patches those as well.

I am not sure this should really be done. The problem is that
calling such functions is fairly likely to raise an exception if
it is not properly conditioned on a check for their actual
usability, and mocking them out could obscure such a bug.
---
 test/test_util.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/test/test_util.py b/test/test_util.py
index e5a5a0557..e003fcb05 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -140,9 +140,15 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         # git.index.util "replaces" git.util and is what "import git.util" gives us.
         mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
 
-        # Disable common chmod functions so the callback can't fix a PermissionError.
+        # Disable some chmod functions so the callback can't fix a PermissionError.
         mocker.patch.object(os, "chmod")
+        if hasattr(os, "lchmod"):
+            # Exists on some operating systems. Mocking out os.chmod doesn't affect it.
+            mocker.patch.object(os, "lchmod")
         mocker.patch.object(pathlib.Path, "chmod")
+        if hasattr(pathlib.Path, "lchmod"):
+            # Exists on some Python versions. Don't rely on it calling a public chmod.
+            mocker.patch.object(pathlib.Path, "lchmod")
 
     @pytest.mark.skipif(
         os.name != "nt",

From a09e5383cc9bbba290c18c22d925065453b47747 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 28 Nov 2023 20:47:38 -0500
Subject: [PATCH 301/994] Don't mock the lchmod functions, and explain why

This undoes the mocking of lchmod functions from e309b35, and
instead notes why they may be better left alone in the tests.

This also rewords the existing comment to better explain the reason
for the mocking that is being done.
---
 test/test_util.py | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index e003fcb05..68f75aa45 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -140,15 +140,11 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         # git.index.util "replaces" git.util and is what "import git.util" gives us.
         mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
 
-        # Disable some chmod functions so the callback can't fix a PermissionError.
+        # Mock out common chmod functions to simulate PermissionError the callback can't
+        # fix. (We leave the corresponding lchmod functions alone. If they're used, it's
+        # more important we detect any failures from inadequate compatibility checks.)
         mocker.patch.object(os, "chmod")
-        if hasattr(os, "lchmod"):
-            # Exists on some operating systems. Mocking out os.chmod doesn't affect it.
-            mocker.patch.object(os, "lchmod")
         mocker.patch.object(pathlib.Path, "chmod")
-        if hasattr(pathlib.Path, "lchmod"):
-            # Exists on some Python versions. Don't rely on it calling a public chmod.
-            mocker.patch.object(pathlib.Path, "lchmod")
 
     @pytest.mark.skipif(
         os.name != "nt",

From 2fabe71b7ba6feb5e63795c51bf60a1361ed936d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 28 Nov 2023 23:00:29 -0500
Subject: [PATCH 302/994] Further document cygpath test parameter collections

---
 test/test_util.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 68f75aa45..f1ce17bca 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -258,14 +258,15 @@ def _xfail_param(*values, **xfail_kwargs):
     (R"D:/Apps\fOO", "/cygdrive/d/Apps/fOO"),
     (R"D:\Apps/123", "/cygdrive/d/Apps/123"),
 )
+"""Path test cases for cygpath and decygpath, other than extended UNC paths."""
 
 _unc_cygpath_pairs = (
     (R"\\?\a:\com", "/cygdrive/a/com"),
     (R"\\?\a:/com", "/cygdrive/a/com"),
     (R"\\?\UNC\server\D$\Apps", "//server/D$/Apps"),
 )
+"""Extended UNC path test cases for cygpath."""
 
-# Mapping of expected failures for the test_cygpath_ok test.
 _cygpath_ok_xfails = {
     # From _norm_cygpath_pairs:
     (R"C:\Users", "/cygdrive/c/Users"): "/proc/cygdrive/c/Users",
@@ -279,9 +280,9 @@ def _xfail_param(*values, **xfail_kwargs):
     (R"\\?\a:\com", "/cygdrive/a/com"): "/proc/cygdrive/a/com",
     (R"\\?\a:/com", "/cygdrive/a/com"): "/proc/cygdrive/a/com",
 }
+"""Mapping of expected failures for the test_cygpath_ok test."""
 
 
-# Parameter sets for the test_cygpath_ok test.
 _cygpath_ok_params = [
     (
         _xfail_param(*case, reason=f"Returns: {_cygpath_ok_xfails[case]!r}", raises=AssertionError)
@@ -290,6 +291,7 @@ def _xfail_param(*values, **xfail_kwargs):
     )
     for case in _norm_cygpath_pairs + _unc_cygpath_pairs
 ]
+"""Parameter sets for the test_cygpath_ok test."""
 
 
 @pytest.mark.skipif(sys.platform != "cygwin", reason="Paths specifically for Cygwin.")

From e3597180c74d4d1aae444ca42c1e1afdaec3f19a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 29 Nov 2023 00:14:54 -0500
Subject: [PATCH 303/994] Revert "Add xfail marks for IndexFile.from_tree
 failures"

This removes the xfail marks from 8 tests that fail due to #1630,
to be fixed in the subsequent commits.

This reverts commit 6e477e3a06e4b11d43ea118a9f18cae03fa211fd.
---
 test/test_docs.py  | 11 ++---------
 test/test_fun.py   | 17 +++--------------
 test/test_index.py | 40 ----------------------------------------
 test/test_refs.py  | 11 -----------
 4 files changed, 5 insertions(+), 74 deletions(-)

diff --git a/test/test_docs.py b/test/test_docs.py
index 2ff1c794a..2f4b2e8d8 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -8,10 +8,11 @@
 
 import pytest
 
-from git.exc import GitCommandError
 from test.lib import TestBase
 from test.lib.helper import with_rw_directory
 
+import os.path
+
 
 class Tutorials(TestBase):
     def tearDown(self):
@@ -206,14 +207,6 @@ def update(self, op_code, cur_count, max_count=None, message=""):
         assert sm.module_exists()  # The submodule's working tree was checked out by update.
         # ![14-test_init_repo_object]
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_directory
     def test_references_and_objects(self, rw_dir):
         # [1-test_references_and_objects]
diff --git a/test/test_fun.py b/test/test_fun.py
index 8ea5b7e46..566bc9aae 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -3,13 +3,10 @@
 
 from io import BytesIO
 from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR
-import os
+from os import stat
 import os.path as osp
 
-import pytest
-
 from git import Git
-from git.exc import GitCommandError
 from git.index import IndexFile
 from git.index.fun import (
     aggressive_tree_merge,
@@ -37,14 +34,6 @@ def _assert_index_entries(self, entries, trees):
             assert (entry.path, entry.stage) in index.entries
         # END assert entry matches fully
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     def test_aggressive_tree_merge(self):
         # Head tree with additions, removals and modification compared to its predecessor.
         odb = self.rorepo.odb
@@ -302,12 +291,12 @@ def test_linked_worktree_traversal(self, rw_dir):
         rw_master.git.worktree("add", worktree_path, branch.name)
 
         dotgit = osp.join(worktree_path, ".git")
-        statbuf = os.stat(dotgit)
+        statbuf = stat(dotgit)
         self.assertTrue(statbuf.st_mode & S_IFREG)
 
         gitdir = find_worktree_git_dir(dotgit)
         self.assertIsNotNone(gitdir)
-        statbuf = os.stat(gitdir)
+        statbuf = stat(gitdir)
         self.assertTrue(statbuf.st_mode & S_IFDIR)
 
     def test_tree_entries_from_data_with_failing_name_decode_py3(self):
diff --git a/test/test_index.py b/test/test_index.py
index cd1c37efc..5bf34757a 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -284,14 +284,6 @@ def add_bad_blob():
         except Exception as ex:
             assert "index.lock' could not be obtained" not in str(ex)
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("0.1.6")
     def test_index_file_from_tree(self, rw_repo):
         common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541"
@@ -342,14 +334,6 @@ def test_index_file_from_tree(self, rw_repo):
         # END for each blob
         self.assertEqual(num_blobs, len(three_way_index.entries))
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("0.1.6")
     def test_index_merge_tree(self, rw_repo):
         # A bit out of place, but we need a different repo for this:
@@ -412,14 +396,6 @@ def test_index_merge_tree(self, rw_repo):
         self.assertEqual(len(unmerged_blobs), 1)
         self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0])
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("0.1.6")
     def test_index_file_diffing(self, rw_repo):
         # Default Index instance points to our index.
@@ -554,14 +530,6 @@ def _count_existing(self, repo, files):
 
     # END num existing helper
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("0.1.6")
     def test_index_mutation(self, rw_repo):
         index = rw_repo.index
@@ -915,14 +883,6 @@ def make_paths():
         for absfile in absfiles:
             assert osp.isfile(absfile)
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("HEAD")
     def test_compare_write_tree(self, rw_repo):
         """Test writing all trees, comparing them for equality."""
diff --git a/test/test_refs.py b/test/test_refs.py
index a1573c11b..6ee385007 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -4,11 +4,8 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from itertools import chain
-import os
 from pathlib import Path
 
-import pytest
-
 from git import (
     Reference,
     Head,
@@ -218,14 +215,6 @@ def test_head_checkout_detached_head(self, rw_repo):
         assert isinstance(res, SymbolicReference)
         assert res.name == "HEAD"
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason=(
-            "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n"
-            "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'."
-        ),
-        raises=GitCommandError,
-    )
     @with_rw_repo("0.1.6")
     def test_head_reset(self, rw_repo):
         cur_head = rw_repo.head

From b12fd4a8ea6a541efb06c45c83617ec25ea7bd8a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 6 Nov 2023 21:08:24 -0500
Subject: [PATCH 304/994] Let Windows subprocess open or rename onto temp file

On Windows, a file created by NamedTemporaryFile cannot be reopened
while already open, under most circumstances. This applies to
attempts to open it both from the original process and from other
processes. This differs from the behavior on other operating
systems, and it works this way to help ensure the file can be
automatically deleted (since having a file open on Windows usually
prevents the file from being deleted, unlike on other OSes).

However, this can cause problems when NamedTemporaryFile is used to
produce a safe filename for accessing with an external process, as
IndexFile.from_tree does. On Windows, git subprocess can't open and
write to the file. This breaks IndexFile.from_tree on Windows. This
is the cause of #1630 -- IndexFile.reset uses IndexFile.from_tree,
and the Repo.index property returns an IndexFile instance, so
Repo.index.reset fails -- and some other breakages.

While passing delete=False to NamedTemporaryFile (and deleting
separately) often overcomes that, it won't fix things here, because
IndexFile.from_tree uses "git read-tree --index-output=<file>".
That needs more than the ability to open the file for write. It
needs to be able to create or overwrite the given path by renaming
an existing file to it. The description of "--index-output=<file>"
in git-read-tree(1) notes:

    The file must allow to be rename(2)ed into from a temporary
    file that is created next to the usual index file; typically
    this means it needs to be on the same filesystem as the index
    file itself, and you need write permission to the directories
    the index file and index output file are located in.

On Windows, more is required: there must be no currently open file
with that path, because on Windows, open files typically cannot be
replaced, just as, on Windows, they typically cannot be deleted
(nor renamed). This is to say that passing delete=False to
NamedTemporaryFile isn't enough to solve this because it merely
causes NamedTemporaryFile to behave like the lower-level mkstemp
function, leaving the responsibility of deletion to the caller but
still *opening* the file -- and while the file is open, the git
subprocess can't replace it. Switching to mkstemp, with no further
change, would likewise not solve this.

This commit is an initial fix for the problem, but not the best
fix. On Windows, it closes the temporary file created by
NamedTemporaryFile -- so the file can be opened by name, including
by a git subprocess, and also overwritten, include by a file move.

As currently implemented, this is not ideal, as it creates a race
condition, because closing the file actually deletes it. During the
time the file does not exist, the filename may end up being reused
by another call to NamedTemporaryFile, mkstemp, or similar facility
(in any process, written in any language) before the git subprocess
can recreate it. Passing delete=False would avoid this, but then
deletion would have to be handled separately, and at that point it
is not obvious that NamedTemporaryFile is the best facility to use.

This fix, though not yet adequate, confirms the impact on #1630.
The following 8 tests go from failing to passing due to this change
on a local Windows development machine (as listed in the summary at
the end of pytest output):

- test/test_docs.py:210 Tutorials.test_references_and_objects
- test/test_fun.py:37 TestFun.test_aggressive_tree_merge
- test/test_index.py:781 TestIndex.test_compare_write_tree
- test/test_index.py:294 TestIndex.test_index_file_diffing
- test/test_index.py:182 TestIndex.test_index_file_from_tree
- test/test_index.py:232 TestIndex.test_index_merge_tree
- test/test_index.py:428 TestIndex.test_index_mutation
- test/test_refs.py:218 TestRefs.test_head_reset

On CI, one test still fails, but later and for an unrelated reason:

- test/test_index.py:428 TestIndex.test_index_mutation

That `open(fake_symlink_path, "rt")` call raises FileNotFoundError.
---
 git/index/base.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/git/index/base.py b/git/index/base.py
index 94437ac88..aa9bfe994 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -362,6 +362,8 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
         # works - /tmp/ dirs could be on another device.
         with ExitStack() as stack:
             tmp_index = stack.enter_context(tempfile.NamedTemporaryFile(dir=repo.git_dir))
+            if os.name == "nt":
+                tmp_index.close()
             arg_list.append("--index-output=%s" % tmp_index.name)
             arg_list.extend(treeish)
 

From 12bbacee90f3810367a2ce986f42c3172c598c58 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 30 Nov 2023 14:38:10 -0500
Subject: [PATCH 305/994] Add xfail mark for new test_index_mutation failure

The test assumes symlinks are never created on Windows. This often
turns out to be correct, because core.symlinks defaults to false on
Windows. (On some Windows systems, creating them is a privileged
operation; this can be reconfigured, and unprivileged creation of
symlinks is often enabled on systems used for development.)

However, on a Windows system with core.symlinks set to true, git
will create symlinks if it can, when checking out entries committed
to a repository as symlinks. GitHub Actions runners for Windows do
this ever since https://github.com/actions/runner-images/pull/1186
(the file is now at images/windows/scripts/build/Install-Git.ps1;
the `/o:EnableSymlinks=Enabled` option continues to be passed,
causing Git for Windows to be installed with core.symlinks set to
true in the system scope).

For now, this adds an xfail marking to test_index_mutation, for the
FileNotFoundError raised when a symlink, which is expected not to
be a symlink, is passed to `open`, causing an attempt to open its
nonexistent target. (The check itself might bear refinement: as
written, it reads the core.symlinks variable from any scope,
including the local scope, which at the time of the check will
usually be the cloned GitPython directory, where pytest is run.)

While adding an import, this commit also improves the grouping and
sorting of existing ones.
---
 test/test_index.py | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 5bf34757a..2f97f0af8 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -17,17 +17,21 @@
 from sumtypes import constructor, sumtype
 
 from git import (
+    BlobFilter,
+    Diff,
+    Git,
     IndexFile,
+    Object,
     Repo,
-    BlobFilter,
-    UnmergedEntriesError,
     Tree,
-    Object,
-    Diff,
-    GitCommandError,
+)
+from git.exc import (
     CheckoutError,
+    GitCommandError,
+    HookExecutionError,
+    InvalidGitRepositoryError,
+    UnmergedEntriesError,
 )
-from git.exc import HookExecutionError, InvalidGitRepositoryError
 from git.index.fun import hook_path
 from git.index.typ import BaseIndexEntry, IndexEntry
 from git.objects import Blob
@@ -530,6 +534,11 @@ def _count_existing(self, repo, files):
 
     # END num existing helper
 
+    @pytest.mark.xfail(
+        os.name == "nt" and Git().config("core.symlinks") == "true",
+        reason="Assumes symlinks are not created on Windows and opens a symlink to a nonexistent target.",
+        raises=FileNotFoundError,
+    )
     @with_rw_repo("0.1.6")
     def test_index_mutation(self, rw_repo):
         index = rw_repo.index
@@ -740,7 +749,7 @@ def mixed_iterator():
             # END for each target
         # END real symlink test
 
-        # Add fake symlink and assure it checks-our as symlink.
+        # Add fake symlink and assure it checks out as a symlink.
         fake_symlink_relapath = "my_fake_symlink"
         link_target = "/etc/that"
         fake_symlink_path = self._make_file(fake_symlink_relapath, link_target, rw_repo)
@@ -774,7 +783,7 @@ def mixed_iterator():
         os.remove(fake_symlink_path)
         index.checkout(fake_symlink_path)
 
-        # On Windows, we will never get symlinks.
+        # On Windows, we currently assume we will never get symlinks.
         if os.name == "nt":
             # Symlinks should contain the link as text (which is what a
             # symlink actually is).

From 928ca1e9c39ffe3cef9e77cc09efc08dd7d47eee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 7 Nov 2023 11:25:38 -0500
Subject: [PATCH 306/994] Minor formatting consistency improvement

---
 git/index/base.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index aa9bfe994..4fa0714d8 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -97,9 +97,8 @@
 
 
 class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
-    """
-    An Index that can be manipulated using a native implementation in order to save git
-    command function calls wherever possible.
+    """An Index that can be manipulated using a native implementation in order to save
+    git command function calls wherever possible.
 
     This provides custom merging facilities allowing to merge without actually changing
     your index or your working tree. This way you can perform own test-merges based
@@ -380,6 +379,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             # END index merge handling
 
     # UTILITIES
+
     @unbare_repo
     def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
         """Expand the directories in list of paths to the corresponding paths accordingly.

From 9e5d0aaa0542c0e861dbbb33d48225f4745f56a5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 7 Nov 2023 16:54:44 -0500
Subject: [PATCH 307/994] Avoid subprocess-writable temp file race condition

This lets the Windows subprocess open or rename onto the temporary
file using a more robust approach than in b12fd4a, avoiding the
race condition described there, where the filename could be
inadvertently reused between deletion and recreation of the file.

This creates a context manager helper for the temporary index file
used in IndexFile.from_tree, whose implementation differs by
operating system:

- Delegating straightforwardly to NamedTempoaryFile on POSIX
  systems where an open file can replaced by having another file
  renamed to it (just as it can be deleted).

- Employing custom logic on Windows, using mkstemp, closing the
  temporary file without immediately deleting it (so it won't be
  reused by any process seeking to create a temporary file), and
  then deleting it on context manager exit.

IndexFile.from_tree now calls this helper instead of
NamedTemporaryFile. For convenience, the helper provides the path,
i.e. the "name", when entered, so tmp_index is now just that path.

(At least for now, this helper is implemented as a nonpublic
function in the git.index.base module, rather than in the
git.index.util module. If it were public in git.index.util like the
other facilities there, then some later changes to it, or its later
removal, would be a breaking change to GitPython. If it were
nonpublic in git.index.util, then this would not be a concern, but
it would be unintuitive for it to be accessed from code in the
git.index.base module. In the future, one way to address this might
be to have one or more nonpublic _util modules with public members.
Because it would still be a breaking change to drop existing public
util modules, that would be more utility modules in total, so such
a change isn't included here just for this one used-once function.)
---
 git/index/base.py | 34 +++++++++++++++++++++++++++-------
 1 file changed, 27 insertions(+), 7 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 4fa0714d8..6c6462039 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -6,7 +6,7 @@
 """Module containing IndexFile, an Index implementation facilitating all kinds of index
 manipulations such as querying and merging."""
 
-from contextlib import ExitStack
+import contextlib
 import datetime
 import glob
 from io import BytesIO
@@ -67,6 +67,7 @@
     BinaryIO,
     Callable,
     Dict,
+    Generator,
     IO,
     Iterable,
     Iterator,
@@ -96,6 +97,27 @@
 __all__ = ("IndexFile", "CheckoutError", "StageType")
 
 
+@contextlib.contextmanager
+def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
+    """Create a named temporary file git subprocesses can open, deleting it afterward.
+
+    :param directory: The directory in which the file is created.
+
+    :return: A context manager object that creates the file and provides its name on
+        entry, and deletes it on exit.
+    """
+    if os.name == "nt":
+        fd, name = tempfile.mkstemp(dir=directory)
+        os.close(fd)
+        try:
+            yield name
+        finally:
+            os.remove(name)
+    else:
+        with tempfile.NamedTemporaryFile(dir=directory) as ctx:
+            yield ctx.name
+
+
 class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
     """An Index that can be manipulated using a native implementation in order to save
     git command function calls wherever possible.
@@ -359,11 +381,9 @@ 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.
-        with ExitStack() as stack:
-            tmp_index = stack.enter_context(tempfile.NamedTemporaryFile(dir=repo.git_dir))
-            if os.name == "nt":
-                tmp_index.close()
-            arg_list.append("--index-output=%s" % tmp_index.name)
+        with contextlib.ExitStack() as stack:
+            tmp_index = stack.enter_context(_named_temporary_file_for_subprocess(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
@@ -373,7 +393,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
 
             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 = cls(repo, tmp_index)
             index.entries  # Force it to read the file as we will delete the temp-file.
             return index
             # END index merge handling

From 782c06293f96b4a0b92f4a156a12cc506ea4eaf7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 29 Nov 2023 00:53:37 -0500
Subject: [PATCH 308/994] Add myself to AUTHORS

---
 AUTHORS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/AUTHORS b/AUTHORS
index 3e99ff785..3b97c9473 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -52,5 +52,6 @@ Contributors are:
 -Joseph Hale <me _at_ jhale.dev>
 -Santos Gallegos <stsewd _at_ proton.me>
 -Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
+-Eliah Kagan <eliah.kagan _at_ gmail.com>
 
 Portions derived from other open source works and are clearly marked.

From 2813e94ca80c8e41caaa58ff90c063ca0a26a80c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 30 Nov 2023 22:42:05 -0500
Subject: [PATCH 309/994] Add macOS test jobs to CI matrix

This now tests on macOS in as well as Ubuntu and Windows.
---
 .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 3915296ef..273e63668 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "windows-latest"]
+        os: ["ubuntu-latest", "macos-latest", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false

From 789baa1c205411545b1105035e773e795ab30302 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 30 Nov 2023 22:53:32 -0500
Subject: [PATCH 310/994] Use macOS 13 on CI

"macos-latest" is currently macOS 12. Using macOS 13 may be faster.
---
 .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 273e63668..003ed92c1 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+        os: ["ubuntu-latest", "macos-13", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false

From f6f335ffcd55af5367692d338d78cf0465c34514 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 30 Nov 2023 23:24:21 -0500
Subject: [PATCH 311/994] Temporarily break test_blocking_lock_file on Windows

To compare how much extra wait time Windows and macOS usually need.
---
 test/test_util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_util.py b/test/test_util.py
index f1ce17bca..634bd765d 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -397,7 +397,7 @@ def test_blocking_lock_file(self):
         self.assertRaises(IOError, wait_lock._obtain_lock)
         elapsed = time.time() - start
         extra_time = 0.02
-        if os.name == "nt" or sys.platform == "cygwin":
+        if sys.platform == "cygwin":  # FIXME: Put back native Windows check.
             extra_time *= 6  # NOTE: Indeterministic failures without this...
         self.assertLess(elapsed, wait_time + extra_time)
 

From c16e4f371b1ee193af92c4ef685315ca1d41e6e9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Dec 2023 00:33:50 -0500
Subject: [PATCH 312/994] Restore longer extra time for Windows, and add for
 macOS

This undoes the testing breakage of test_blocking_lock_file for
Windows, and gives macOS the same amount of extra time as Windows.

I had expected that macOS, even though it needs more time than
Ubuntu, might not need as much extra time as Windows. That turned
out not to be the case, in the limited testing done so far on CI.
Windows, while still slower than Ubuntu and still too slow to
reliably pass the test_blocking_lock_file test, was usually faster
than macOS and passed more often.

These relative timings may turn out not to be a trend and only to
apply to the current GHA runners. That's probably okay since the
adjustment for macOS wasn't present before and is being added to
allow newly introduced macOS CI test jobs to pass.

That is all in regard to the very specific issue of the extra time
required for the test_blocking_lock_file test after the lock, which
makes an assertion about that not taking too long. Regarding the
overall time of entire test jobs, macOS 13 seems to have usually
been a little faster than macOS 12, so it is retained. Unlike
"macos-latest", which currently is macOS 12, "macos-13" will never
refer to a later version of the operating system, so the version
given in the workflow should be revisited later, at or after the
time "macos-latest" becomes a synonym of "macos-13".
---
 test/test_util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_util.py b/test/test_util.py
index 634bd765d..77332a49d 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -397,7 +397,7 @@ def test_blocking_lock_file(self):
         self.assertRaises(IOError, wait_lock._obtain_lock)
         elapsed = time.time() - start
         extra_time = 0.02
-        if sys.platform == "cygwin":  # FIXME: Put back native Windows check.
+        if os.name == "nt" or sys.platform == "cygwin" or sys.platform == "darwin":
             extra_time *= 6  # NOTE: Indeterministic failures without this...
         self.assertLess(elapsed, wait_time + extra_time)
 

From d15f891c3127e6b855656b31e04bb1c089f8d54d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Dec 2023 01:07:34 -0500
Subject: [PATCH 313/994] macOS needs even more extra time in
 test_blocking_lock_file

As seen in:
https://github.com/EliahKagan/GitPython/actions/runs/7056321920/attempts/1

Usually an extra_time of 0.12 (6x) is sufficient for both Windows
and macOS, but sometimes macOS needs even more, so this increases
it to 0.18 (9x) for macOS.
---
 test/test_util.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 77332a49d..428bd07c3 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -397,8 +397,10 @@ def test_blocking_lock_file(self):
         self.assertRaises(IOError, wait_lock._obtain_lock)
         elapsed = time.time() - start
         extra_time = 0.02
-        if os.name == "nt" or sys.platform == "cygwin" or sys.platform == "darwin":
-            extra_time *= 6  # NOTE: Indeterministic failures without this...
+        if os.name == "nt" or sys.platform == "cygwin":
+            extra_time *= 6  # Without this, we get indeterministic failures on Windows.
+        elif sys.platform == "darwin":
+            extra_time *= 9  # The situation on macOS is similar, but with more delay.
         self.assertLess(elapsed, wait_time + extra_time)
 
     def test_user_id(self):

From 78d63d9ecf4cfbeea34dbc464890b97137f3be1b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Dec 2023 02:53:51 -0500
Subject: [PATCH 314/994] Let close_fds be True on all platforms

Since Python 3.7, subprocess.Popen supports close_fds=True on all
platforms, including Windows, and it is the default, including
when arguments for standard streams have non-None values passed.
3.7 is the lowest version of Python that GitPython supports. So
this omits the close_fds=True argument from the calls where it was
present. This has the same effect (in 3.7 and higher) as passing
close_fds=True.

When the the close_fd argument was added to the Popen call in
git.cmd.Git.execute in 1ee2afb, Python 2 was still supported. In
Python 2, close_fds defaulted to False. This appears to be the
reason it had been passed explicitly. It was conditioned on being
on a Unix-like system because having it True on Windows would
prevent stdin, stdout, or stderr redirection.
---
 git/cmd.py       | 1 -
 git/index/fun.py | 1 -
 2 files changed, 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index cde6073da..05e199dc0 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1001,7 +1001,6 @@ def execute(
                     stderr=PIPE,
                     stdout=stdout_sink,
                     shell=shell,
-                    close_fds=(os.name == "posix"),  # Unsupported on Windows.
                     universal_newlines=universal_newlines,
                     creationflags=PROC_CREATIONFLAGS,
                     **subprocess_kwargs,
diff --git a/git/index/fun.py b/git/index/fun.py
index 7b3b06269..eaf5f51ff 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -102,7 +102,6 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             cwd=index.repo.working_dir,
-            close_fds=(os.name == "posix"),
             creationflags=PROC_CREATIONFLAGS,
         )
     except Exception as ex:

From 3f21391e9aab5eb89aa3bac6fd32293b129167fc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Dec 2023 14:34:11 -0500
Subject: [PATCH 315/994] Remove unused TASKKILL fallback in AutoInterrupt

When Git.AutoInterrupt was first implemented in ead94f2, it used
os.kill (sending SIGINT to the process to terminate it). This was
in 2009, and not all supported versions of Python provided os.kill
on Windows, since it was added for Windows in 2.7 and 3.2 (2.6 and
3.1 reached EoL in 2013 and 2012, respectively). Catching
AttributeError and running TASKKILL provided a fallback for Python
versions in which os.kill was absent on Windows.

Since then, all supported versions have os.kill on Windows, and
also the code of GitPython, in the try-block, has changed, and no
longer uses os.kill. It now contains four attribute access
expressions:

- proc.terminate. All currently suppported versions of Python
  (including 3.7, which GitPython still supports, and some before
  that) have Popen.terminate.

- proc.wait. Same situation as proc.terminate. Popen.wait exists.

- self._status_code_if_terminate. This is a class attribute of
  AutoInterrupt, accessible through its instances.

- self.status. This is assigned to. AutoInterrupt is slotted, but
  "status" appears in __slots__, so this isn't an AttributeError
  either.

The "except AttributeError" clause is thus no longer used and can
be removed, which is done here. Removing it shortens and simplifies
the code, better expresses the logic for how the wrapped process is
actually being terminated, and eliminates the need to engage with
any subtleties of TASKKILL (and of how it is called) when reading
the code to verify its correctness.

In addition, because they are now expected always to be available,
if somehow an AttributeError managed to be raised in the terminate
or wait calls, this would be very strange and we probably shouldn't
silently catch that.

(Because this AutoInterrupt._terminate method is sometimes called
due to __del__ methods running as the interpreter is shutting down,
it is possible for some attributes to unexpectedly be set to None,
which could cause AttributeError indirectly if another attribute is
looked up on the None object; and perhaps on some non-CPython
implementations some attributes might even be deleted during
shutdown. The _terminate method handles this where relevant. But
the TASKKILL fallback removed here seems unrelated to that, which
affects module attributes and global variables. The names used in
the try-block are proc, status, and self, which are local
variables; the except clause itself accessed os.name, which used a
global variable and a module attribute, indicating that the intent
was not to handle this interpreter shutdown scenario; and this
whole issue, while not gone completely, is much less significant
since Python 3.4, due to
https://docs.python.org/3/whatsnew/3.4.html#whatsnew-pep-442.)
---
 git/cmd.py | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 05e199dc0..4dc76556e 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -11,7 +11,7 @@
 import logging
 import os
 import signal
-from subprocess import call, Popen, PIPE, DEVNULL
+from subprocess import Popen, PIPE, DEVNULL
 import subprocess
 import threading
 from textwrap import dedent
@@ -544,16 +544,6 @@ def _terminate(self) -> None:
                 self.status = self._status_code_if_terminate or status
             except OSError as ex:
                 log.info("Ignored error after process had died: %r", ex)
-            except AttributeError:
-                # Try Windows.
-                # For some reason, providing None for stdout/stderr still prints something. This is why
-                # we simply use the shell and redirect to nul. Slower than CreateProcess. The question
-                # is whether we really want to see all these messages. It's annoying no matter what.
-                if os.name == "nt":
-                    call(
-                        ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)),
-                        shell=True,
-                    )
             # END exception handling
 
         def __del__(self) -> None:

From 39e2dff35798a08a18a14c03311924def092860c Mon Sep 17 00:00:00 2001
From: Travis Runner <degeneracypressure@gmail.com>
Date: Sun, 3 Dec 2023 00:24:28 -0500
Subject: [PATCH 316/994] Don't return with operand when conceptually void

This changes `return None` to `return` where it appears in
functions that are "conceptually void," i.e. cases where a function
always returns None, its purpose is never to provide a return value
to the caller, and it would be a bug (or at least confusing style)
to use the return value or have the call as a subexpression.

This is already the prevailing style, so this is a consistency
improvement, as well as improving clarity by avoiding giving the
impression that "None" is significant in those cases.

Of course, functions that sometimes return values other than None
do not have "return None" replaced with a bare "return" statement.
Those are left alone (when they return None, it means something).

For the most part this is straightforward, but:

- The handle_stderr local function in IndexFile.checkout is also
  slightly further refactored to better express what the early
  return is doing.

- The "None" operand is removed as usual in GitConfigParser.read,
  even though a superclass has a same-named method with a return
  type that is not None. Usually when this happens, the superclass
  method returns something like Optional[Something], and it is a
  case of return type covariance, in which case the None operands
  should still be written. Here, however, the overriding method
  does not intend to be an override: it has an incompatible
  signature and cannot be called as if it were the superclass
  method, nor is its return type compatible.

- The "None" operand is *retained* in git.cmd.handle_process_output
  even though the return type is annotated as None, because the
  function also returns the expression `finalizer(process)`. The
  argument `finalizer` is itself annotated to return None, but it
  looks like the intent may be to play along with the strange case
  that `finalizer` returns a non-None value, and forward it.
---
 git/cmd.py          | 4 ++--
 git/config.py       | 9 +++++----
 git/index/base.py   | 8 ++++----
 git/index/fun.py    | 2 +-
 git/objects/tree.py | 2 +-
 git/objects/util.py | 4 ++--
 git/util.py         | 2 +-
 7 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 4dc76556e..5f96e33c9 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -528,13 +528,13 @@ def _terminate(self) -> None:
             try:
                 if proc.poll() is not None:
                     self.status = self._status_code_if_terminate or proc.poll()
-                    return None
+                    return
             except OSError as ex:
                 log.info("Ignored error after process had died: %r", ex)
 
             # It can be that nothing really exists anymore...
             if os is None or getattr(os, "kill", None) is None:
-                return None
+                return
 
             # Try to kill it.
             try:
diff --git a/git/config.py b/git/config.py
index 983d71e86..5708a7385 100644
--- a/git/config.py
+++ b/git/config.py
@@ -203,7 +203,8 @@ def __setitem__(self, key: str, value: _T) -> None:
     def add(self, key: str, value: Any) -> None:
         if key not in self:
             super().__setitem__(key, [value])
-            return None
+            return
+
         super().__getitem__(key).append(value)
 
     def setall(self, key: str, values: List[_T]) -> None:
@@ -579,7 +580,7 @@ def read(self) -> None:  # type: ignore[override]
         :raise IOError: If a file cannot be handled
         """
         if self._is_initialized:
-            return None
+            return
         self._is_initialized = True
 
         files_to_read: List[Union[PathLike, IO]] = [""]
@@ -697,7 +698,7 @@ def write(self) -> None:
             a file lock"""
         self._assure_writable("write")
         if not self._dirty:
-            return None
+            return
 
         if isinstance(self._file_or_files, (list, tuple)):
             raise AssertionError(
@@ -711,7 +712,7 @@ def write(self) -> None:
                 "Skipping write-back of configuration file as include files were merged in."
                 + "Set merge_includes=False to prevent this."
             )
-            return None
+            return
         # END stop if we have include files
 
         fp = self._file_or_files
diff --git a/git/index/base.py b/git/index/base.py
index 6c6462039..112ad3feb 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -166,7 +166,7 @@ def _set_cache_(self, attr: str) -> None:
             except OSError:
                 # In new repositories, there may be no index, which means we are empty.
                 self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
-                return None
+                return
             # END exception handling
 
             try:
@@ -1210,9 +1210,9 @@ def checkout(
         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.
-            else:
-                stderr_bytes = stderr_IO.read()
+                return  # Return early if stderr empty.
+
+            stderr_bytes = stderr_IO.read()
             # line contents:
             stderr = stderr_bytes.decode(defenc)
             # git-checkout-index: this already exists
diff --git a/git/index/fun.py b/git/index/fun.py
index eaf5f51ff..97f2c88e6 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -83,7 +83,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     """
     hp = hook_path(name, index.repo.git_dir)
     if not os.access(hp, os.X_OK):
-        return None
+        return
 
     env = os.environ.copy()
     env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 4d94a5d24..a08adf48b 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -68,7 +68,7 @@ def git_cmp(t1: TreeCacheTup, t2: TreeCacheTup) -> int:
 
 def merge_sort(a: List[TreeCacheTup], cmp: Callable[[TreeCacheTup, TreeCacheTup], int]) -> None:
     if len(a) < 2:
-        return None
+        return
 
     mid = len(a) // 2
     lefthalf = a[:mid]
diff --git a/git/objects/util.py b/git/objects/util.py
index 3021fec39..3e8f8dcc9 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -500,8 +500,8 @@ def addToStack(
             depth: int,
         ) -> None:
             lst = self._get_intermediate_items(item)
-            if not lst:  # empty list
-                return None
+            if not lst:  # Empty list
+                return
             if branch_first:
                 stack.extendleft(TraverseNT(depth, i, src_item) for i in lst)
             else:
diff --git a/git/util.py b/git/util.py
index 01cdcf773..aaf662060 100644
--- a/git/util.py
+++ b/git/util.py
@@ -644,7 +644,7 @@ def _parse_progress_line(self, line: AnyStr) -> None:
             self.line_dropped(line_str)
             # Note: Don't add this line to the other lines, as we have to silently
             # drop it.
-            return None
+            return
         # END handle op code
 
         # Figure out stage.

From b9d09c1f5ebceb1810206228e52a6a429cbb463a Mon Sep 17 00:00:00 2001
From: Travis Runner <degeneracypressure@gmail.com>
Date: Tue, 5 Dec 2023 00:15:18 -0500
Subject: [PATCH 317/994] Group .gitignore entries by purpose

- Divide .gitignore entries into groups
- Label the groups with comments
- Add `__pycache__/` even though it should only contain *.pyc and *.pyo
- Add `monkeytype.sqlite3.*` for compressed copies of that database
- Remove `/*.egg-info` because `/*egg-info` covers it
---
 .gitignore | 48 +++++++++++++++++++++++++++++++++++-------------
 1 file changed, 35 insertions(+), 13 deletions(-)

diff --git a/.gitignore b/.gitignore
index 191e0e6c3..7765293d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,27 +1,49 @@
+# Cached Python bytecode
+__pycache__/
 *.py[co]
+
+# Other caches
+.cache/
+.mypy_cache/
+.pytest_cache/
+
+# Transient editor files
 *.swp
 *~
+
+# Editor configuration
+nbproject
+*.sublime-workspace
+/.vscode/
+.idea/
+
+# Virtual environments
 .env/
 env/
 .venv/
 venv/
-/*.egg-info
+
+# Build output
+/*egg-info
 /lib/GitPython.egg-info
-cover/
-.coverage
-.coverage.*
 /build
 /dist
 /doc/_build
-nbproject
-*.sublime-workspace
-.DS_Store
-/*egg-info
+
+# Tox builds/environments
 /.tox
-/.vscode/
-.idea/
-.cache/
-.mypy_cache/
-.pytest_cache/
+
+# Code coverage output
+cover/
+.coverage
+.coverage.*
+
+# Monkeytype output
 monkeytype.sqlite3
+monkeytype.sqlite3.*
+
+# Manual command output
 output.txt
+
+# Finder metadata
+.DS_Store

From 1808dddfa64b79a05e678f57bfe87a4b6aca9cfe Mon Sep 17 00:00:00 2001
From: Mario Alvarado <mario.alberto.galindo@ibm.com>
Date: Tue, 28 Nov 2023 10:49:28 -0600
Subject: [PATCH 318/994] Adding dubious ownership handling

---
 git/cmd.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 5f96e33c9..d67bb1478 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1322,7 +1322,12 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
         tokens = header_line.split()
         if len(tokens) != 3:
             if not tokens:
-                raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip()))
+                err_msg = (
+                    f"SHA is empty, possible dubious ownership in the repository "
+                    f"""at {self._working_dir}.\n            If this is unintended run:\n\n         """
+                    f"""             "git config --global --add safe.directory {self._working_dir}" """
+                )
+                raise ValueError(err_msg)
             else:
                 raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip()))
             # END handle actual return value

From ad570de497a4512a336eed141726af5b7b7e126b Mon Sep 17 00:00:00 2001
From: Travis Runner <degeneracypressure@gmail.com>
Date: Thu, 7 Dec 2023 17:10:17 -0500
Subject: [PATCH 319/994] Avoid unsafe assumptions about tempdir content in
 tests

test_new_should_raise_on_invalid_repo_location had previously used
tempfile.gettempdir() directly to get a path assumed not to be a
valid repository, assuming more than necessary about the directory
in which temporary files and directories are created. A newly
created temporary directory is now used for that check, instead.

test_new_should_raise_on_non_existent_path had assumed no
repos/foobar existed relative to the current directory. Typically
there would be no such directory, but is unnecessary for the test
to rely on this. A newly created temporary directory known to be
empty is now used in place of the "repos" component, for that test.
---
 test/test_repo.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index 033deca1e..c68dd074c 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -71,15 +71,19 @@ def tearDown(self):
         for lfp in glob.glob(_tc_lock_fpaths):
             if osp.isfile(lfp):
                 raise AssertionError("Previous TC left hanging git-lock file: {}".format(lfp))
+
         import gc
 
         gc.collect()
 
     def test_new_should_raise_on_invalid_repo_location(self):
-        self.assertRaises(InvalidGitRepositoryError, Repo, tempfile.gettempdir())
+        with tempfile.TemporaryDirectory() as tdir:
+            self.assertRaises(InvalidGitRepositoryError, Repo, tdir)
 
     def test_new_should_raise_on_non_existent_path(self):
-        self.assertRaises(NoSuchPathError, Repo, "repos/foobar")
+        with tempfile.TemporaryDirectory() as tdir:
+            nonexistent = osp.join(tdir, "foobar")
+            self.assertRaises(NoSuchPathError, Repo, nonexistent)
 
     @with_rw_repo("0.3.2.1")
     def test_repo_creation_from_different_paths(self, rw_repo):

From 9277ff561bcc37c70b9e6e015f623af20216b9e0 Mon Sep 17 00:00:00 2001
From: Travis Runner <degeneracypressure@gmail.com>
Date: Thu, 7 Dec 2023 19:06:05 -0500
Subject: [PATCH 320/994] Avoid another tempdir content assumption in test

test_init was using tempfile.gettempdir() directly to get the
location where the hard-coded path repos/foo/bar.git would be used
to test repository creation with relative and absolute paths. That
reused the same location each time, and also assumed the directory
would be usable, which could fail due to previous runs or due to
the path being used separately from GitPython's tests. This commit
fixes that by using that path inside a temporary directory, known
at the start of the test to be empty.

Reorganizing the acquision and cleanup logic also has had the
effect of causing the test no longer to be skipped due to the logic
in git.util.rmtree due to the final cleanup attempt (after all
assertions). The directory is now successfully removed on Windows,
and the test passes on all platforms.
---
 test/test_repo.py | 23 +++++++----------------
 1 file changed, 7 insertions(+), 16 deletions(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index c68dd074c..845fabf15 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -41,7 +41,7 @@
     UnsafeProtocolError,
 )
 from git.repo.fun import touch
-from git.util import bin_to_hex, cygpath, join_path_native, rmfile, rmtree
+from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree
 from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo
 
 
@@ -511,13 +511,11 @@ def write(self, b):
         repo.git.log(n=100, output_stream=TestOutputStream(io.DEFAULT_BUFFER_SIZE))
 
     def test_init(self):
-        prev_cwd = os.getcwd()
-        os.chdir(tempfile.gettempdir())
-        git_dir_rela = "repos/foo/bar.git"
-        del_dir_abs = osp.abspath("repos")
-        git_dir_abs = osp.abspath(git_dir_rela)
-        try:
-            # with specific path
+        with tempfile.TemporaryDirectory() as tdir, cwd(tdir):
+            git_dir_rela = "repos/foo/bar.git"
+            git_dir_abs = osp.abspath(git_dir_rela)
+
+            # With specific path
             for path in (git_dir_rela, git_dir_abs):
                 r = Repo.init(path=path, bare=True)
                 self.assertIsInstance(r, Repo)
@@ -527,7 +525,7 @@ def test_init(self):
 
                 self._assert_empty_repo(r)
 
-                # test clone
+                # Test clone
                 clone_path = path + "_clone"
                 rc = r.clone(clone_path)
                 self._assert_empty_repo(rc)
@@ -562,13 +560,6 @@ def test_init(self):
             assert not r.has_separate_working_tree()
 
             self._assert_empty_repo(r)
-        finally:
-            try:
-                rmtree(del_dir_abs)
-            except OSError:
-                pass
-            os.chdir(prev_cwd)
-        # END restore previous state
 
     def test_bare_property(self):
         self.rorepo.bare

From c09ac1ab2c86a87080dcfb2fc0af64cbcc2bc9eb Mon Sep 17 00:00:00 2001
From: Travis Runner <degeneracypressure@gmail.com>
Date: Fri, 8 Dec 2023 01:18:54 -0500
Subject: [PATCH 321/994] Test InvalidGitRepositoryError in repo subdir

A Repo object can of course be constructed from a path to a
directory that is the root of an existing repository, and raises
InvalidGitRepositoryError on a directory that is outside of any
repository. Tests intended to show both conditions already exist.

This adds a test to verify that InvalidGitRepositoryError is also
raised when an attempt is made to construct a Repo object from a
path to a directory that is not the root of a repository but that
is known to be located inside an existing git repository.
---
 test/test_repo.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/test/test_repo.py b/test/test_repo.py
index 845fabf15..836862b0c 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -77,9 +77,20 @@ def tearDown(self):
         gc.collect()
 
     def test_new_should_raise_on_invalid_repo_location(self):
+        # Ideally this tests a directory that is outside of any repository. In the rare
+        # case tempfile.gettempdir() is inside a repo, this still passes, but tests the
+        # same scenario as test_new_should_raise_on_invalid_repo_location_within_repo.
         with tempfile.TemporaryDirectory() as tdir:
             self.assertRaises(InvalidGitRepositoryError, Repo, tdir)
 
+    @with_rw_directory
+    def test_new_should_raise_on_invalid_repo_location_within_repo(self, rw_dir):
+        repo_dir = osp.join(rw_dir, "repo")
+        Repo.init(repo_dir)
+        subdir = osp.join(repo_dir, "subdir")
+        os.mkdir(subdir)
+        self.assertRaises(InvalidGitRepositoryError, Repo, subdir)
+
     def test_new_should_raise_on_non_existent_path(self):
         with tempfile.TemporaryDirectory() as tdir:
             nonexistent = osp.join(tdir, "foobar")
@@ -122,7 +133,7 @@ def test_tree_from_revision(self):
         self.assertEqual(tree.type, "tree")
         self.assertEqual(self.rorepo.tree(tree), tree)
 
-        # try from invalid revision that does not exist
+        # Try from an invalid revision that does not exist.
         self.assertRaises(BadName, self.rorepo.tree, "hello world")
 
     def test_pickleable(self):

From 86fcb1b54afb77d44298d3f2aebd48d371296f10 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Fri, 8 Dec 2023 09:36:13 +0100
Subject: [PATCH 322/994] be even more generous with 'extra-time' to wait on
 MacOS

That way, flaky failures are hopefully reliably avoided.
---
 git/ext/gitdb     | 2 +-
 test/test_util.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index ec58b7e55..3d3e9572d 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit ec58b7e55c8fec2479290a22faa293799f6805fc
+Subproject commit 3d3e9572dc452fea53d328c101b3d1440bbefe40
diff --git a/test/test_util.py b/test/test_util.py
index 428bd07c3..1cb255cfe 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -400,7 +400,7 @@ def test_blocking_lock_file(self):
         if os.name == "nt" or sys.platform == "cygwin":
             extra_time *= 6  # Without this, we get indeterministic failures on Windows.
         elif sys.platform == "darwin":
-            extra_time *= 9  # The situation on macOS is similar, but with more delay.
+            extra_time *= 18  # The situation on macOS is similar, but with more delay.
         self.assertLess(elapsed, wait_time + extra_time)
 
     def test_user_id(self):

From e54277fc868a521e360dd9eba5f643321088e813 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Dec 2023 18:59:36 -0500
Subject: [PATCH 323/994] Compare types in test_orig_head with "is"

This performs an exact type comparison with "is" instead of "==".
Most (maybe all?) of the rest of the places where "==" was used
have been changed previously, but this on fell through the cracks.
Like the None object, type objects use reference-based equality
comparison, and the idiomatic ways to check against them are "is"
for exactly comparisons and isinstance/issubclass otherwise.
---
 test/test_refs.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_refs.py b/test/test_refs.py
index 6ee385007..2656ceab1 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -207,7 +207,7 @@ def test_is_valid(self):
         assert not SymbolicReference(self.rorepo, "hellothere").is_valid()
 
     def test_orig_head(self):
-        assert type(self.rorepo.head.orig_head()) == SymbolicReference
+        assert type(self.rorepo.head.orig_head()) is SymbolicReference
 
     @with_rw_repo("0.1.6")
     def test_head_checkout_detached_head(self, rw_repo):

From cf5cb8472fa74834274e7077b4c703783ed5112e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Dec 2023 19:04:27 -0500
Subject: [PATCH 324/994] Small docstring copyedits

- Slightly improve grammar ("it's" -> "its").
- Add a missing space after a "." (between sentences).
- Use more consistent hard wrap in a place where 88 was intended.
---
 git/objects/submodule/base.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 9ed1487a5..acf16b556 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -958,7 +958,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             raise
         # END handle undo rename
 
-        # Auto-rename submodule if it's name was 'default', that is, the checkout directory.
+        # Auto-rename submodule if its name was 'default', that is, the checkout directory.
         if previous_sm_path == self.name:
             self.rename(module_checkout_path)
 
@@ -976,19 +976,19 @@ def remove(
         from the .gitmodules file and the entry in the .git/config file.
 
         :param module: If True, the checked out module we point to will be deleted as
-            well.If that module is currently on a commit outside any branch in the
+            well. If that module is currently on a commit outside any branch in the
             remote, or if it is ahead of its tracking branch, or if there are modified
-            or untracked files in its working tree, then the removal will fail.
-            In case the removal of the repository fails for these reasons, the
-            submodule status will not have been altered.
+            or untracked files in its working tree, then the removal will fail. In case
+            the removal of the repository fails for these reasons, the submodule status
+            will not have been altered.
             If this submodule has child modules of its own, these will be deleted prior
             to touching the direct submodule.
         :param force: Enforces the deletion of the module even though it contains
             modifications. This basically enforces a brute-force file system based
             deletion.
         :param configuration: If True, the submodule is deleted from the configuration,
-            otherwise it isn't. Although this should be enabled most of the time,
-            this flag enables you to safely delete the repository of your submodule.
+            otherwise it isn't. Although this should be enabled most of the time, this
+            flag enables you to safely delete the repository of your submodule.
         :param dry_run: If True, we will not actually do anything, but throw the errors
             we would usually throw.
         :return: self

From 41d22b277cb0866b3a742e7de63bc33aa881dd04 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Dec 2023 19:08:57 -0500
Subject: [PATCH 325/994] Overhaul noqa directives

- Remove some noqa directives that are not needed at all anymore.
  But there may still be some others I did not find.

- Fix the code and remove the noqa direcive in places where the
  change is very simple and does not affect the interface (e.g.,
  what could be taken to be public or not, what symbols are
  available and what they refer to, etc.), the change makes the
  code more idiomatic or clearer, and it fixes the flake8 complaint
  completely.

- Turn file-level noqa directives into statement-level ones. A noqa
  directive at file level, even if it lists one or more specific
  error/warning numbers, actually suppresses flake8 for the entire
  file. So the main benefit here is to enable other warnings. But
  this also makes it clear what is producing the ones we suppress,
  which makes it easier to know what is intentional and how it may
  be reasonable to change it in the future.

- Move noqa directives at the end of multi-line imports to the top
  line, where flake8 honors them, and eliminate redundant
  directives that were added to work around their ineffectiveness.
  (This is not really separate from the above point, since this is
  often what allowed file-level directives to be removed.)

- Specify the errors/warnings each "noqa" should suppress. That is,
  this turns every bare "noqa", aside from those eliminated as
  described above, into a "noqa" for one or more specific
  errors/warnings. This makes the intent clearer and avoids
  oversuppression.

- Add missing ":" between "noqa" and error/warning numbers. When
  these were absent, the intention was to suppress only the
  specifically listed errors/warnings, but the effect was that the
  listed numbers were ignored and *everything* was suppressed. In a
  couple of cases it was necessary to change the listed numbers to
  correct the list to what actually needed to be suppressed.

- Write every "noqa" in lower-case (not "NOQA"). There did not
  appear to be a systematic reason for the different casing, and
  having them all cased the same makes it easier to avoid mistakes
  when grepping for them.

- Where an import is unused and does not appear intended to be
  accesed from other modules, and is present only to support code
  in the same module that is commented out, but whose removal
  should be considered separately, comment out the statement or
  part of the statement that imports it as well, rather than
  writing a "noqa" for the unused import. This applies to only one
  file, git/objects/util.py.
---
 git/__init__.py         | 27 +++++++++++++--------------
 git/cmd.py              |  2 +-
 git/compat.py           |  9 ++-------
 git/index/__init__.py   |  6 ++----
 git/objects/__init__.py | 20 +++++++++-----------
 git/objects/util.py     | 22 +++++++++-------------
 git/refs/__init__.py    | 13 ++++++-------
 git/refs/log.py         |  4 ++--
 git/refs/reference.py   |  4 ++--
 git/refs/symbolic.py    |  5 ++---
 git/repo/__init__.py    |  4 +---
 git/types.py            | 14 ++++++--------
 git/util.py             |  8 ++++----
 test/lib/__init__.py    |  4 ++--
 test/lib/helper.py      |  4 ++--
 test/test_commit.py     |  2 +-
 test/test_config.py     |  2 +-
 test/test_docs.py       |  2 +-
 test/test_exc.py        | 14 +++++++-------
 test/test_repo.py       |  2 +-
 test/test_submodule.py  |  2 +-
 21 files changed, 75 insertions(+), 95 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index defc679cb..c6a52ef30 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -3,28 +3,27 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# flake8: noqa
 # @PydevCodeAnalysisIgnore
 
-from git.exc import *  # @NoMove @IgnorePep8
-from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
-from git.types import PathLike
-
 __version__ = "git"
 
+from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
+
 from gitdb.util import to_hex_sha
+from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
+from git.types import PathLike
 
 try:
     from git.compat import safe_decode  # @NoMove @IgnorePep8
     from git.config import GitConfigParser  # @NoMove @IgnorePep8
-    from git.objects import *  # @NoMove @IgnorePep8
-    from git.refs import *  # @NoMove @IgnorePep8
-    from git.diff import *  # @NoMove @IgnorePep8
-    from git.db import *  # @NoMove @IgnorePep8
+    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
     from git.cmd import Git  # @NoMove @IgnorePep8
     from git.repo import Repo  # @NoMove @IgnorePep8
-    from git.remote import *  # @NoMove @IgnorePep8
-    from git.index import *  # @NoMove @IgnorePep8
+    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
     from git.util import (  # @NoMove @IgnorePep8
         LockFile,
         BlockingLockFile,
@@ -33,12 +32,12 @@
         remove_password_if_present,
         rmtree,
     )
-except GitError as _exc:
+except GitError as _exc:  # noqa: F405
     raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
 
 # __all__ must be statically defined by py.typed support
 # __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
-__all__ = [
+__all__ = [  # noqa: F405
     "Actor",
     "AmbiguousObjectName",
     "BadName",
@@ -127,7 +126,7 @@ def refresh(path: Optional[PathLike] = None) -> None:
 
     if not Git.refresh(path=path):
         return
-    if not FetchInfo.refresh():
+    if not FetchInfo.refresh():  # noqa: F405
         return  # type: ignore [unreachable]
 
     GIT_OK = True
diff --git a/git/cmd.py b/git/cmd.py
index d67bb1478..8b42dec52 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -965,7 +965,7 @@ def execute(
             # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
             maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
         else:
-            cmd_not_found_exception = FileNotFoundError  # NOQA # exists, flake8 unknown @UndefinedVariable
+            cmd_not_found_exception = FileNotFoundError
             maybe_patch_caller_env = contextlib.nullcontext()
         # END handle
 
diff --git a/git/compat.py b/git/compat.py
index 64bd38397..6b1b4f547 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -5,20 +5,15 @@
 
 """Utilities to help provide compatibility with Python 3."""
 
-# flake8: noqa
-
 import locale
 import os
 import sys
 
-from gitdb.utils.encoding import (
-    force_bytes,  # @UnusedImport
-    force_text,  # @UnusedImport
-)
+from gitdb.utils.encoding import force_bytes, force_text  # noqa: F401  # @UnusedImport
 
 # typing --------------------------------------------------------------------
 
-from typing import (
+from typing import (  # noqa: F401
     Any,
     AnyStr,
     Dict,
diff --git a/git/index/__init__.py b/git/index/__init__.py
index 9954b9e88..c65722cd8 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -3,7 +3,5 @@
 
 """Initialize the index package."""
 
-# flake8: noqa
-
-from .base import *
-from .typ import *
+from .base import *  # noqa: F401 F403
+from .typ import *  # noqa: F401 F403
diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 9ca430285..fc8d8fbb2 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -3,23 +3,21 @@
 
 """Import all submodules' main classes into the package space."""
 
-# flake8: noqa
-
 import inspect
 
-from .base import *
-from .blob import *
-from .commit import *
+from .base import *  # noqa: F403
+from .blob import *  # noqa: F403
+from .commit import *  # noqa: F403
 from .submodule import util as smutil
-from .submodule.base import *
-from .submodule.root import *
-from .tag import *
-from .tree import *
+from .submodule.base import *  # noqa: F403
+from .submodule.root import *  # noqa: F403
+from .tag import *  # noqa: F403
+from .tree import *  # noqa: F403
 
 # Fix import dependency - add IndexObject to the util module, so that it can be
 # imported by the submodule.base.
-smutil.IndexObject = IndexObject  # type: ignore[attr-defined]
-smutil.Object = Object  # type: ignore[attr-defined]
+smutil.IndexObject = IndexObject  # type: ignore[attr-defined]  # noqa: F405
+smutil.Object = Object  # type: ignore[attr-defined]  # noqa: F405
 del smutil
 
 # Must come after submodule was made available.
diff --git a/git/objects/util.py b/git/objects/util.py
index 3e8f8dcc9..c52dc7564 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -5,20 +5,16 @@
 
 """General utility functions."""
 
-# flake8: noqa F401
-
-
 from abc import ABC, abstractmethod
-import warnings
-from git.util import IterableList, IterableObj, Actor
-
-import re
+import calendar
 from collections import deque
-
+from datetime import datetime, timedelta, tzinfo
 from string import digits
+import re
 import time
-import calendar
-from datetime import datetime, timedelta, tzinfo
+import warnings
+
+from git.util import IterableList, IterableObj, Actor
 
 # typing ------------------------------------------------------------
 from typing import (
@@ -26,10 +22,10 @@
     Callable,
     Deque,
     Iterator,
-    Generic,
+    # Generic,
     NamedTuple,
     overload,
-    Sequence,  # NOQA: F401
+    Sequence,
     TYPE_CHECKING,
     Tuple,
     Type,
@@ -38,7 +34,7 @@
     cast,
 )
 
-from git.types import Has_id_attribute, Literal, _T  # NOQA: F401
+from git.types import Has_id_attribute, Literal  # , _T
 
 if TYPE_CHECKING:
     from io import BytesIO, StringIO
diff --git a/git/refs/__init__.py b/git/refs/__init__.py
index 3a82b9796..b0233e902 100644
--- a/git/refs/__init__.py
+++ b/git/refs/__init__.py
@@ -1,13 +1,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# flake8: noqa
 # Import all modules in order, fix the names they require.
 
-from .symbolic import *
-from .reference import *
-from .head import *
-from .tag import *
-from .remote import *
+from .symbolic import *  # noqa: F401 F403
+from .reference import *  # noqa: F401 F403
+from .head import *  # noqa: F401 F403
+from .tag import *  # noqa: F401 F403
+from .remote import *  # noqa: F401 F403
 
-from .log import *
+from .log import *  # noqa: F401 F403
diff --git a/git/refs/log.py b/git/refs/log.py
index aeebac48c..e45798d8a 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -31,9 +31,9 @@
 from git.types import PathLike
 
 if TYPE_CHECKING:
-    from git.refs import SymbolicReference
     from io import BytesIO
-    from git.config import GitConfigParser, SectionConstraint  # NOQA
+    from git.refs import SymbolicReference
+    from git.config import GitConfigParser, SectionConstraint
 
 # ------------------------------------------------------------------------------
 
diff --git a/git/refs/reference.py b/git/refs/reference.py
index c2ad13bd6..20d42472d 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -10,8 +10,8 @@
 
 # typing ------------------------------------------------------------------
 
-from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING  # NOQA
-from git.types import Commit_ish, PathLike, _T  # NOQA
+from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING
+from git.types import Commit_ish, PathLike, _T
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 84c2057e1..31f959ac2 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -1,7 +1,6 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.types import PathLike
 import os
 
 from git.compat import defenc
@@ -31,8 +30,8 @@
     Union,
     TYPE_CHECKING,
     cast,
-)  # NOQA
-from git.types import Commit_ish, PathLike  # NOQA
+)
+from git.types import Commit_ish, PathLike
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index c01a1e034..a63d77878 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -3,6 +3,4 @@
 
 """Initialize the Repo package."""
 
-# flake8: noqa
-
-from .base import Repo as Repo
+from .base import Repo as Repo  # noqa: F401
diff --git a/git/types.py b/git/types.py
index 6f2b7c513..efb393471 100644
--- a/git/types.py
+++ b/git/types.py
@@ -1,11 +1,9 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# flake8: noqa
-
 import os
 import sys
-from typing import (
+from typing import (  # noqa: F401
     Dict,
     NoReturn,
     Sequence as Sequence,
@@ -16,24 +14,24 @@
     Callable,
     TYPE_CHECKING,
     TypeVar,
-)  # noqa: F401
+)
 
 if sys.version_info >= (3, 8):
-    from typing import (
+    from typing import (  # noqa: F401
         Literal,
         TypedDict,
         Protocol,
         SupportsIndex as SupportsIndex,
         runtime_checkable,
-    )  # noqa: F401
+    )
 else:
-    from typing_extensions import (
+    from typing_extensions import (  # noqa: F401
         Literal,
         SupportsIndex as SupportsIndex,
         TypedDict,
         Protocol,
         runtime_checkable,
-    )  # noqa: F401
+    )
 
 # if sys.version_info >= (3, 10):
 #     from typing import TypeGuard  # noqa: F401
diff --git a/git/util.py b/git/util.py
index aaf662060..0a5da7d71 100644
--- a/git/util.py
+++ b/git/util.py
@@ -62,12 +62,9 @@
     Has_id_attribute,
 )
 
-T_IterableObj = TypeVar("T_IterableObj", bound=Union["IterableObj", "Has_id_attribute"], covariant=True)
-# So IterableList[Head] is subtype of IterableList[IterableObj]
-
 # ---------------------------------------------------------------------
 
-from gitdb.util import (  # NOQA @IgnorePep8
+from gitdb.util import (  # noqa: F401  # @IgnorePep8
     make_sha,
     LockedFD,  # @UnusedImport
     file_contents_ro,  # @UnusedImport
@@ -79,6 +76,9 @@
     hex_to_bin,  # @UnusedImport
 )
 
+T_IterableObj = TypeVar("T_IterableObj", bound=Union["IterableObj", "Has_id_attribute"], covariant=True)
+# So IterableList[Head] is subtype of IterableList[IterableObj].
+
 # NOTE:  Some of the unused imports might be used/imported by others.
 # Handle once test-cases are back up and running.
 # Most of these are unused here, but are for use by git-python modules so these
diff --git a/test/lib/__init__.py b/test/lib/__init__.py
index cc1e48483..f96072cb5 100644
--- a/test/lib/__init__.py
+++ b/test/lib/__init__.py
@@ -3,8 +3,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# flake8: noqa
 import inspect
-from .helper import *
+
+from .helper import *  # noqa: F401 F403
 
 __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
diff --git a/test/lib/helper.py b/test/lib/helper.py
index f4e22245b..58d96534a 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -145,7 +145,7 @@ def repo_creator(self):
             os.chdir(rw_repo.working_dir)
             try:
                 return func(self, rw_repo)
-            except:  # noqa E722
+            except:  # noqa: E722 B001
                 log.info("Keeping repo after failure: %s", repo_dir)
                 repo_dir = None
                 raise
@@ -305,7 +305,7 @@ def remote_repo_creator(self):
                     with cwd(rw_repo.working_dir):
                         try:
                             return func(self, rw_repo, rw_daemon_repo)
-                        except:  # noqa E722
+                        except:  # noqa: E722 B001
                             log.info(
                                 "Keeping repos after failure: \n  rw_repo_dir: %s \n  rw_daemon_repo_dir: %s",
                                 rw_repo_dir,
diff --git a/test/test_commit.py b/test/test_commit.py
index b6fb09aef..fddb91c17 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -475,7 +475,7 @@ def test_datetimes(self):
             commit.authored_datetime,
             datetime(2009, 10, 8, 18, 17, 5, tzinfo=tzoffset(-7200)),
             commit.authored_datetime,
-        )  # noqa
+        )
         self.assertEqual(
             commit.authored_datetime,
             datetime(2009, 10, 8, 16, 17, 5, tzinfo=utc),
diff --git a/test/test_config.py b/test/test_config.py
index c1b26c91f..f493c5672 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -125,7 +125,7 @@ def test_multi_line_config(self):
             ev = "ruby -e '\n"
             ev += "		system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n"
             ev += "		b = File.read(%(%A))\n"
-            ev += "		b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\."  # noqa E501
+            ev += "		b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\."  # noqa: E501
             ev += "define.:version => (\\d+). do\\n>+ .*/) do\n"
             ev += "		  %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n"
             ev += "		end\n"
diff --git a/test/test_docs.py b/test/test_docs.py
index 2f4b2e8d8..a03a9c71b 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -25,7 +25,7 @@ def tearDown(self):
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
-    #         "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx")  # noqa E501
+    #         "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx")  # noqa: E501
     @with_rw_directory
     def test_init_repo_object(self, rw_dir):
         # [1-test_init_repo_object]
diff --git a/test/test_exc.py b/test/test_exc.py
index cecd4f342..3f4d0b803 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -40,13 +40,13 @@
     ),
 )
 _causes_n_substrings = (
-    (None, None),  # noqa: E241 @IgnorePep8
-    (7, "exit code(7)"),  # noqa: E241 @IgnorePep8
-    ("Some string", "'Some string'"),  # noqa: E241 @IgnorePep8
-    ("παλιο string", "'παλιο string'"),  # noqa: E241 @IgnorePep8
-    (Exception("An exc."), "Exception('An exc.')"),  # noqa: E241 @IgnorePep8
-    (Exception("Κακια exc."), "Exception('Κακια exc.')"),  # noqa: E241 @IgnorePep8
-    (object(), "<object object at "),  # noqa: E241 @IgnorePep8
+    (None, None),
+    (7, "exit code(7)"),
+    ("Some string", "'Some string'"),
+    ("παλιο string", "'παλιο string'"),
+    (Exception("An exc."), "Exception('An exc.')"),
+    (Exception("Κακια exc."), "Exception('Κακια exc.')"),
+    (object(), "<object object at "),
 )
 
 _streams_n_substrings = (
diff --git a/test/test_repo.py b/test/test_repo.py
index 836862b0c..7560c519f 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -769,7 +769,7 @@ def test_blame_incremental(self, git):
                         range(13, 15),
                     ]
                 ),
-            )  # noqa E501
+            )
 
     @mock.patch.object(Git, "_call_process")
     def test_blame_complex_revision(self, git):
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 7fbccf831..b68ca3fed 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -460,7 +460,7 @@ def _do_base_tests(self, rwrepo):
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"
     #         "it is being used by another process: "
-    #         "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'")  # noqa E501
+    #         "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'")  # noqa: E501
     @with_rw_repo(k_subm_current)
     def test_base_rw(self, rwrepo):
         self._do_base_tests(rwrepo)

From f42a63bf54f63fa7e266331906d3939a98c3696c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Dec 2023 02:37:11 -0500
Subject: [PATCH 326/994] Document more Git.execute kill_after_timeout
 limitations

See discussion in #1756.
---
 git/cmd.py | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 8b42dec52..af748e529 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -879,11 +879,19 @@ def execute(
             Specifies a timeout in seconds for the git command, after which the process
             should be killed. This will have no effect if `as_process` is set to True.
             It is set to None by default and will let the process run until the timeout
-            is explicitly specified. This feature is not supported on Windows. It's also
-            worth noting that `kill_after_timeout` uses SIGKILL, which can have negative
-            side effects on a repository. For example, stale locks in case of ``git gc``
-            could render the repository incapable of accepting changes until the lock is
-            manually removed.
+            is explicitly specified. Uses of this feature should be carefully
+            considered, due to the following limitations:
+
+            1. This feature is not supported at all on Windows.
+            2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
+               enumerate child processes, which is available on most GNU/Linux systems
+               but not most others.
+            3. Deeper descendants do not receive signals, though they may sometimes
+               terminate as a consequence of their parent processes being killed.
+            4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side
+               effects on a repository. For example, stale locks in case of ``git gc``
+               could render the repository incapable of accepting changes until the lock
+               is manually removed.
 
         :param with_stdout:
             If True, default True, we open stdout on the created process.

From 2f017ac879eece80e04fd2a30238f261a6b49fb4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Dec 2023 02:38:05 -0500
Subject: [PATCH 327/994] Avoid making it look like kill_process works on
 Windows

This changes the code in Git.execute's local kill_process function,
which it uses as the timed callback for kill_after_timeout, to
remove code that is unnecessary because kill_process doesn't
support Windows, and to avoid giving the false impression that its
code could be used unmodified on Windows without serious problems.

- Raise AssertionError explicitly if it is called on Windows. This
  is done with "raise" rather than "assert" so its behavior doesn't
  vary depending on "-O".

- Don't pass process creation flags, because they were 0 except on
  Windows.

- Don't fall back to SIGTERM if Python's signal module doesn't know
  about SIGKILL. This was specifically for Windows which has no
  SIGKILL.

See #1756 for discussion.
---
 git/cmd.py | 18 +++++++-----------
 1 file changed, 7 insertions(+), 11 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index af748e529..27148d3d6 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1015,11 +1015,9 @@ def execute(
 
         def kill_process(pid: int) -> None:
             """Callback to kill a process."""
-            p = Popen(
-                ["ps", "--ppid", str(pid)],
-                stdout=PIPE,
-                creationflags=PROC_CREATIONFLAGS,
-            )
+            if os.name == "nt":
+                raise AssertionError("Bug: This callback would be ineffective and unsafe on Windows, stopping.")
+            p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
             child_pids = []
             if p.stdout is not None:
                 for line in p.stdout:
@@ -1028,18 +1026,16 @@ def kill_process(pid: int) -> None:
                         if local_pid.isdigit():
                             child_pids.append(int(local_pid))
             try:
-                # Windows does not have SIGKILL, so use SIGTERM instead.
-                sig = getattr(signal, "SIGKILL", signal.SIGTERM)
-                os.kill(pid, sig)
+                os.kill(pid, signal.SIGKILL)
                 for child_pid in child_pids:
                     try:
-                        os.kill(child_pid, sig)
+                        os.kill(child_pid, signal.SIGKILL)
                     except OSError:
                         pass
                 kill_check.set()  # Tell the main routine that the process was killed.
             except OSError:
-                # It is possible that the process gets completed in the duration after timeout
-                # happens and before we try to kill the process.
+                # It is possible that the process gets completed in the duration after
+                # timeout happens and before we try to kill the process.
                 pass
             return
 

From d1cb8c72e4671fd2b027f9326cf32943ba861d67 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 13:03:20 +0000
Subject: [PATCH 328/994] Bump actions/setup-python from 4 to 5

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 91dd919e0..f9c5b70b3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -9,7 +9,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
-    - uses: actions/setup-python@v4
+    - uses: actions/setup-python@v5
       with:
         python-version: "3.x"
 
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 003ed92c1..e9a7486be 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -30,7 +30,7 @@ jobs:
         fetch-depth: 0
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}

From d70ba6941cf0f005f3f247c70233f51a0cc35969 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 06:52:35 -0500
Subject: [PATCH 329/994] Extract all "import gc" to module level

The gc module was already imported at module scope in
git/repo/base.py, since f1a82e4 (#555). Importing the top-level
git module or any submodule of it runs that import statement.
Because the gc module is already imported, reimporting it is fast.

Imports that there is no specific reason to do locally should be at
module scope. Having them local decreased readability, in part
because of how black inserts a black line between them and
gc.collect() calls they are imported to allow.

An alternative to this change would be to remove the preexisting
top-level "import gc" (there is also another one in the test suite)
and replace it with a local import as well. I am unsure if that
would affect performance and, if so, whether the effect would be
good or bad, since the small delay of the import might potentially
be less desirable to an applicaion if it occurs while the work of
the application is already in progress.

If a gc.collect() call runs as a consequence of a finally block or
__del__ method being called during interpreter shutdown, then in
(very) rare cases the variable may have been set to None. But this
does not appear to have been the intent behind making the imports
local. More importantly, a local import should not be expected to
succeed, or the imported module usable, in such a situation.
---
 git/objects/submodule/base.py    | 3 +--
 test/performance/test_commit.py  | 3 +--
 test/performance/test_streams.py | 3 +--
 test/test_base.py                | 3 +--
 test/test_docs.py                | 3 +--
 test/test_git.py                 | 3 +--
 test/test_quick_doc.py           | 4 ++--
 test/test_remote.py              | 3 +--
 test/test_repo.py                | 3 +--
 test/test_submodule.py           | 3 +--
 10 files changed, 11 insertions(+), 20 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index acf16b556..651d9535c 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1,6 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 from io import BytesIO
 import logging
 import os
@@ -1079,8 +1080,6 @@ def remove(
                     self._clear_cache()
                     wtd = mod.working_tree_dir
                     del mod  # Release file-handles (Windows).
-                    import gc
-
                     gc.collect()
                     rmtree(str(wtd))
                 # END delete tree if possible
diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py
index 9e136a6c1..00d768f0a 100644
--- a/test/performance/test_commit.py
+++ b/test/performance/test_commit.py
@@ -5,6 +5,7 @@
 
 """Performance tests for commits (iteration, traversal, and serialization)."""
 
+import gc
 from io import BytesIO
 from time import time
 import sys
@@ -17,8 +18,6 @@
 
 class TestPerformance(TestBigRepoRW, TestCommitSerialization):
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     # ref with about 100 commits in its history.
diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py
index 9ee7cf5e2..ba5cbe415 100644
--- a/test/performance/test_streams.py
+++ b/test/performance/test_streams.py
@@ -3,6 +3,7 @@
 
 """Performance tests for data streaming."""
 
+import gc
 import os
 import subprocess
 import sys
@@ -92,8 +93,6 @@ def test_large_data_streaming(self, rwrepo):
 
             # del db file so git has something to do.
             ostream = None
-            import gc
-
             gc.collect()
             os.remove(db_file)
 
diff --git a/test/test_base.py b/test/test_base.py
index a667b194c..cdf82b74d 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import os
 import sys
 import tempfile
@@ -20,8 +21,6 @@
 
 class TestBase(_TestBase):
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     type_tuples = (
diff --git a/test/test_docs.py b/test/test_docs.py
index a03a9c71b..48922eba8 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import os
 import sys
 
@@ -16,8 +17,6 @@
 
 class Tutorials(TestBase):
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via
diff --git a/test/test_git.py b/test/test_git.py
index 5986a07da..6d0ae82d0 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import inspect
 import logging
 import os
@@ -34,8 +35,6 @@ def setUpClass(cls):
         cls.git = Git(cls.rorepo.working_dir)
 
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     def _assert_logged_for_popen(self, log_watcher, name, value):
diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py
index 504dca237..4ef75f4aa 100644
--- a/test/test_quick_doc.py
+++ b/test/test_quick_doc.py
@@ -1,14 +1,14 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
+
 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
diff --git a/test/test_remote.py b/test/test_remote.py
index 080718a1c..df6034326 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import os
 import os.path as osp
 from pathlib import Path
@@ -105,8 +106,6 @@ def assert_received_message(self):
 
 class TestRemote(TestBase):
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     def _print_fetchhead(self, repo):
diff --git a/test/test_repo.py b/test/test_repo.py
index 7560c519f..007b8ecb0 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import glob
 import io
 from io import BytesIO
@@ -72,8 +73,6 @@ def tearDown(self):
             if osp.isfile(lfp):
                 raise AssertionError("Previous TC left hanging git-lock file: {}".format(lfp))
 
-        import gc
-
         gc.collect()
 
     def test_new_should_raise_on_invalid_repo_location(self):
diff --git a/test/test_submodule.py b/test/test_submodule.py
index b68ca3fed..852a5ef6f 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -2,6 +2,7 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import contextlib
+import gc
 import os
 import os.path as osp
 from pathlib import Path
@@ -61,8 +62,6 @@ def update(self, op, cur_count, max_count, message=""):
 
 class TestSubmodule(TestBase):
     def tearDown(self):
-        import gc
-
         gc.collect()
 
     k_subm_current = "c15a6e1923a14bc760851913858a3942a4193cdb"

From 43f3b52f17aac918a76a79665a61461e74f4ca1a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 07:26:04 -0500
Subject: [PATCH 330/994] Don't install black on Cygwin

As of black 23.12.0, installing black builds a wheel on Cygwin,
downloading dependencies for the "d" extra (i.e., black[d]), which
provides dependencies for running the blackd daemon. This includes
dependencies, such as asyncio, whose compilation fails on CI as it
is currently set up, fails under some common Cygwin setups (since
it needs a C compiler), and even if successful would cause the
Cygwin test workflow to take significantly longer to run.
---
 test-requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index fcdc93c1d..63fa0a9cd 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,4 @@
-black
+black ; sys_platform != "cygwin"
 coverage[toml]
 ddt >= 1.1.1, != 1.4.3
 mock ; python_version < "3.8"

From f62df523e0121ddead413da6d6eaf944a39c74e5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 09:15:19 -0500
Subject: [PATCH 331/994] Remove TestSubmodule.test_rename xfail mark

This is to clearly establish the failure still occurs (since we do
not have strict=True set). It is in preparation for working around
the problems with a call to gc.collect().

See 82c361e and 0b7ee17 for context.
---
 test/test_submodule.py | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 852a5ef6f..e2380d522 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -948,18 +948,6 @@ def test_remove_norefs(self, rwdir):
         sm.remove()
         assert not sm.exists()
 
-    @pytest.mark.xfail(
-        os.name == "nt" and sys.version_info >= (3, 12),
-        reason=(
-            "The sm.move call fails. Submodule.move calls os.renames, which raises:\n"
-            "PermissionError: [WinError 32] "
-            "The process cannot access the file because it is being used by another process: "
-            R"'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\mymodules\myname' "
-            R"-> 'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\renamed\myname'"
-            "\nThis resembles other Windows errors, but only occurs starting in Python 3.12."
-        ),
-        raises=PermissionError,
-    )
     @with_rw_directory
     def test_rename(self, rwdir):
         parent = git.Repo.init(osp.join(rwdir, "parent"))

From b66be7ca948307460933939bdbebc8f4ed665299 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 08:04:01 -0500
Subject: [PATCH 332/994] Replace xfail with gc.collect in
 TestSubmodule.test_rename

Like the xfail was, this is conditional, being done only in the
specific situation the PermissionError occurs. Besides that it does
not always run even on Windows (only in 3.12 and later), this
resembles various other conditional and non-conditional gc.collect
calls.

It had previously appeared to me that two calls to gc.collect were
required, but I am unable to reproduce that. It may have been
specific to how I was running it on my system at that time. The
need for only one call may have been brought about by changes to
the code in the mean time, but I have tested that only one call
appears required even without the changes in #1765.
---
 test/test_submodule.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index e2380d522..4dc89f98f 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -958,6 +958,12 @@ def test_rename(self, rwdir):
         assert sm.rename(sm_name) is sm and sm.name == sm_name
         assert not sm.repo.is_dirty(index=True, working_tree=False, untracked_files=False)
 
+        # This is needed to work around a PermissionError on Windows, resembling others,
+        # except new in Python 3.12. (*Maybe* this could be due to changes in CPython's
+        # garbage collector detailed in https://github.com/python/cpython/issues/97922.)
+        if os.name == "nt" and sys.version_info >= (3, 12):
+            gc.collect()
+
         new_path = "renamed/myname"
         assert sm.move(new_path).name == new_path
 

From 96cae0002efe5c108be4a3dad0ae2c42069f1ca6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 10:09:34 -0500
Subject: [PATCH 333/994] Extract remaining local "import gc" to module level

In #1765, I missed one, somehow. This fixes that.
---
 test/test_diff.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_diff.py b/test/test_diff.py
index 1d138a086..87f92f5d1 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import gc
 import os
 import os.path as osp
 import shutil
@@ -27,8 +28,6 @@ def setUp(self):
         self.submodule_dir = tempfile.mkdtemp()
 
     def tearDown(self):
-        import gc
-
         gc.collect()
         shutil.rmtree(self.repo_dir)
         shutil.rmtree(self.submodule_dir)

From 41fac851fb62b4224e0400be46a3884708d185c7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 19:02:14 -0500
Subject: [PATCH 334/994] Avoid mktemp in tests, in straightforward cases

The tempfile.mktemp function is deprecated, because of a race
condition where the file may be concurrently created between when
its name is generated and when it is opened. Other faciliies in the
tempfile module overcome this by generating a name, attempting to
create the file or directory in a way that guarantees failure if it
already existed, and, in the occasional case that it did already
exist, generating another name and trying again (stopping after a
predefined limit). For further information on mktemp deprecation:

- https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
- https://github.com/gitpython-developers/smmap/issues/41

The security risk of calls to mktemp in this project's test suite
is low. However, it is still best to avoid using it, because it is
deprecated, because it is (at least slightly) brittle, and because
any use of mktemp looks like a potential security risk and thereby
imposes a burden on working with the code (which could potentially
be addressed with detailed comments analyzing why it is believed
safe in particular cases, but this would typically be more verbose,
and at least as challenging to add, as replacing mktemp with a
better alternative).

This commit replaces *some* uses of mktemp in the test suite: those
where it is readily clear how to do so in a way that preserves the
code's intent:

- Where a name for a temporary directory is generated with mktemp
  and os.mkdir is called immediately, mkdtemp is now used.

- Where a name for a temporary file that is not customized (such as
  with a prefix) is generated with mktemp, such that the code under
  test never uses the filename but only the already-open file-like
  object, TemporaryFile is now used. As the name isn't customized,
  the test code in these cases does not express an intent to allow
  the developer to inspect the file after a test failure, so even
  if the file wasn't guaranteed to be deleted with a finally block
  or context manager, it is fine to do so. TemporaryFile supports
  this use case well on all systems including Windows, and
  automatically deletes the file.

- Where a name for a temporary file that *is* customized (such as
  with a prefix) to reflect the way the test uses it is generated
  with mktemp, and the test code does not attempt deterministic
  deletion of the file when an exception would make the test fail,
  NamedTemporaryFile with delete=False is now used. The original
  code to remove the file when the test succeeds is modified
  accordingly to do the same job, and also commented to explain
  that it is being handled this way to allow the file to be kept
  and examined when a test failure occurs.

Other cases in the test suite should also be feasible to replace,
but are left alone for now. Some of them are ambiguous in their
intent, with respect to whether the file should be retained after a
test failure. Others appear deliberately to avoid creating a file
or directory where the code under test should do so, possibly to
check that this is done properly. (One way to preserve that latter
behavior, while avoiding the weakness of using mktemp and also
avoiding inadverently reproducing that weakness by other means,
could be to use a path in a temporary directory made for the test.)

This commit also doesn't address the one use of mktemp in the code
under test (i.e., outside the test suite, inside the git module).
---
 test/lib/helper.py      |  3 +-
 test/performance/lib.py |  3 +-
 test/test_base.py       |  5 ++--
 test/test_reflog.py     |  7 ++---
 test/test_repo.py       |  5 ++--
 test/test_util.py       | 64 ++++++++++++++++++++++-------------------
 6 files changed, 42 insertions(+), 45 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 58d96534a..d662b632d 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -89,8 +89,7 @@ def with_rw_directory(func):
 
     @wraps(func)
     def wrapper(self):
-        path = tempfile.mktemp(prefix=func.__name__)
-        os.mkdir(path)
+        path = tempfile.mkdtemp(prefix=func.__name__)
         keep = False
         try:
             return func(self, path)
diff --git a/test/performance/lib.py b/test/performance/lib.py
index ceee6c2a1..d08e1027f 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -65,8 +65,7 @@ class TestBigRepoRW(TestBigRepoR):
     def setUp(self):
         self.gitrwrepo = None
         super().setUp()
-        dirname = tempfile.mktemp()
-        os.mkdir(dirname)
+        dirname = tempfile.mkdtemp()
         self.gitrwrepo = self.gitrorepo.clone(dirname, shared=True, bare=True, odbt=GitCmdObjectDB)
         self.puregitrwrepo = Repo(dirname, odbt=GitDB)
 
diff --git a/test/test_base.py b/test/test_base.py
index cdf82b74d..74f342071 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -68,12 +68,11 @@ def test_base_object(self):
             data = data_stream.read()
             assert data
 
-            tmpfilename = tempfile.mktemp(suffix="test-stream")
-            with open(tmpfilename, "wb+") as tmpfile:
+            with tempfile.NamedTemporaryFile(suffix="test-stream", delete=False) as tmpfile:
                 self.assertEqual(item, item.stream_data(tmpfile))
                 tmpfile.seek(0)
                 self.assertEqual(tmpfile.read(), data)
-            os.remove(tmpfilename)
+            os.remove(tmpfile.name)  # Do it this way so we can inspect the file on failure.
         # END for each object type to create
 
         # Each has a unique sha.
diff --git a/test/test_reflog.py b/test/test_reflog.py
index 625466d40..1bd2e5dab 100644
--- a/test/test_reflog.py
+++ b/test/test_reflog.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import os
+import os.path as osp
 import tempfile
 
 from git.objects import IndexObject
@@ -9,8 +9,6 @@
 from test.lib import TestBase, fixture_path
 from git.util import Actor, rmtree, hex_to_bin
 
-import os.path as osp
-
 
 class TestRefLog(TestBase):
     def test_reflogentry(self):
@@ -35,8 +33,7 @@ def test_reflogentry(self):
     def test_base(self):
         rlp_head = fixture_path("reflog_HEAD")
         rlp_master = fixture_path("reflog_master")
-        tdir = tempfile.mktemp(suffix="test_reflogs")
-        os.mkdir(tdir)
+        tdir = tempfile.mkdtemp(suffix="test_reflogs")
 
         rlp_master_ro = RefLog.path(self.rorepo.head)
         assert osp.isfile(rlp_master_ro)
diff --git a/test/test_repo.py b/test/test_repo.py
index 007b8ecb0..465bb2574 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -667,11 +667,10 @@ def test_tag_to_full_tag_path(self):
         self.assertEqual(value_errors, [])
 
     def test_archive(self):
-        tmpfile = tempfile.mktemp(suffix="archive-test")
-        with open(tmpfile, "wb") as stream:
+        with tempfile.NamedTemporaryFile("wb", suffix="archive-test", delete=False) as stream:
             self.rorepo.archive(stream, "0.1.6", path="doc")
             assert stream.tell()
-        os.remove(tmpfile)
+        os.remove(stream.name)  # Do it this way so we can inspect the file on failure.
 
     @mock.patch.object(Git, "_call_process")
     def test_should_display_blame_information(self, git):
diff --git a/test/test_util.py b/test/test_util.py
index 1cb255cfe..6616e1067 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -359,48 +359,52 @@ def test_it_should_dashify(self):
         self.assertEqual("foo", dashify("foo"))
 
     def test_lock_file(self):
-        my_file = tempfile.mktemp()
-        lock_file = LockFile(my_file)
-        assert not lock_file._has_lock()
-        # Release lock we don't have - fine.
-        lock_file._release_lock()
+        with tempfile.TemporaryDirectory() as tdir:
+            my_file = os.path.join(tdir, "my-lock-file")
+            lock_file = LockFile(my_file)
+            assert not lock_file._has_lock()
+            # Release lock we don't have - fine.
+            lock_file._release_lock()
 
-        # Get lock.
-        lock_file._obtain_lock_or_raise()
-        assert lock_file._has_lock()
+            # Get lock.
+            lock_file._obtain_lock_or_raise()
+            assert lock_file._has_lock()
 
-        # Concurrent access.
-        other_lock_file = LockFile(my_file)
-        assert not other_lock_file._has_lock()
-        self.assertRaises(IOError, other_lock_file._obtain_lock_or_raise)
+            # Concurrent access.
+            other_lock_file = LockFile(my_file)
+            assert not other_lock_file._has_lock()
+            self.assertRaises(IOError, other_lock_file._obtain_lock_or_raise)
 
-        lock_file._release_lock()
-        assert not lock_file._has_lock()
+            lock_file._release_lock()
+            assert not lock_file._has_lock()
 
-        other_lock_file._obtain_lock_or_raise()
-        self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
+            other_lock_file._obtain_lock_or_raise()
+            self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
 
-        # Auto-release on destruction.
-        del other_lock_file
-        lock_file._obtain_lock_or_raise()
-        lock_file._release_lock()
+            # Auto-release on destruction.
+            del other_lock_file
+            lock_file._obtain_lock_or_raise()
+            lock_file._release_lock()
 
     def test_blocking_lock_file(self):
-        my_file = tempfile.mktemp()
-        lock_file = BlockingLockFile(my_file)
-        lock_file._obtain_lock()
-
-        # Next one waits for the lock.
-        start = time.time()
-        wait_time = 0.1
-        wait_lock = BlockingLockFile(my_file, 0.05, wait_time)
-        self.assertRaises(IOError, wait_lock._obtain_lock)
-        elapsed = time.time() - start
+        with tempfile.TemporaryDirectory() as tdir:
+            my_file = os.path.join(tdir, "my-lock-file")
+            lock_file = BlockingLockFile(my_file)
+            lock_file._obtain_lock()
+
+            # Next one waits for the lock.
+            start = time.time()
+            wait_time = 0.1
+            wait_lock = BlockingLockFile(my_file, 0.05, wait_time)
+            self.assertRaises(IOError, wait_lock._obtain_lock)
+            elapsed = time.time() - start
+
         extra_time = 0.02
         if os.name == "nt" or sys.platform == "cygwin":
             extra_time *= 6  # Without this, we get indeterministic failures on Windows.
         elif sys.platform == "darwin":
             extra_time *= 18  # The situation on macOS is similar, but with more delay.
+
         self.assertLess(elapsed, wait_time + extra_time)
 
     def test_user_id(self):

From 9e86053c885a27360d9d4699fafaef5e995d6ec5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Dec 2023 22:30:51 -0500
Subject: [PATCH 335/994] Replace the one use of mktemp in the git module

This makes two related changes to git.index.util.TemporaryFileSwap:

- Replace mktemp with mkstemp and then immediately closing the file.
  This avoids two possible name clashes: the usual name clash where
  the file may be created by another process between when mktemp
  generates the name and when the file is created, and the problem
  that mktemp does not check for files that contain the generated
  name as a part. The latter is specific to the use here, where
  a string generated by mktemp was manually concatenated to the
  base filename. This addresses that by passing the filename as the
  prefix for mkstemp to use.

- Replace os.remove with os.replace and simplify. This is made
  necessary on Windows by the file already existing, due to mkstemp
  creating it. Deleting the file and allowing it to be recreated
  would reproduce the mktemp race condition in full (obscured and
  with extra steps). But os.replace supports an existing target
  file on all platforms. It is now also used in __exit__, where it
  allows the Windows check for the file to be removed, and (in any
  OS) better expresses the intent of the code to human readers.
  Furthermore, because one of the "look before you leap" checks in
  __exit__ is removed, the minor race condition in cleaning up the
  file is somewhat decreased.

A small amount of related refactoring is included. The interface is
not changed, even where it could be simplified (by letting __exit__
return None), and resource acquisition remains done on construction
rather than in __enter__, because changing those aspects of the
design could break some existing uses.

Although the use of mktemp replaced here was in the git module and
not the test suite, its use was to generate filenames for use in a
directory that would ordinarily be under the user's control, such
that the ability to carry out typical mktemp-related attacks would
already require the ability to achieve the same goals more easily
and reliably. Although TemporaryFileSwap is a public class that may
be used directly by applications, it is only useful for making a
temporary file in the same directory as an existing file. Thus the
intended benefits of this change are mainly to code quality and
a slight improvement in robustness.
---
 git/index/util.py | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/git/index/util.py b/git/index/util.py
index b1aaa58fd..1c3b1c4ad 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -3,6 +3,7 @@
 
 """Index utilities."""
 
+import contextlib
 from functools import wraps
 import os
 import os.path as osp
@@ -40,12 +41,10 @@ class TemporaryFileSwap:
 
     def __init__(self, file_path: PathLike) -> None:
         self.file_path = file_path
-        self.tmp_file_path = str(self.file_path) + tempfile.mktemp("", "", "")
-        # It may be that the source does not exist.
-        try:
-            os.rename(self.file_path, self.tmp_file_path)
-        except OSError:
-            pass
+        fd, self.tmp_file_path = tempfile.mkstemp(prefix=self.file_path, dir="")
+        os.close(fd)
+        with contextlib.suppress(OSError):  # It may be that the source does not exist.
+            os.replace(self.file_path, self.tmp_file_path)
 
     def __enter__(self) -> "TemporaryFileSwap":
         return self
@@ -57,10 +56,7 @@ def __exit__(
         exc_tb: Optional[TracebackType],
     ) -> bool:
         if osp.isfile(self.tmp_file_path):
-            if os.name == "nt" and osp.exists(self.file_path):
-                os.remove(self.file_path)
-            os.rename(self.tmp_file_path, self.file_path)
-
+            os.replace(self.tmp_file_path, self.file_path)
         return False
 
 

From 83b7ec61a1596971d4834e3c93a51b6efdf9e766 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Dec 2023 03:40:28 -0500
Subject: [PATCH 336/994] Set up CodeQL

This is an automatically generated CodeQL workflow for the project.
It is not yet customized.
---
 .github/workflows/codeql.yml | 81 ++++++++++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)
 create mode 100644 .github/workflows/codeql.yml

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..7f64e35cb
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,81 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+  schedule:
+    - cron: '27 10 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    # Runner size impacts CodeQL analysis time. To learn more, please see:
+    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
+    #   - https://gh.io/supported-runners-and-hardware-resources
+    #   - https://gh.io/using-larger-runners
+    # Consider using larger runners for possible analysis time improvements.
+    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
+        # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
+        # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+
+        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+
+    # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines.
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #     echo "Run, Build Application using script"
+    #     ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+      with:
+        category: "/language:${{matrix.language}}"

From 58547d82fa36165f611563c6bc449fde54dd3b2e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Dec 2023 03:41:16 -0500
Subject: [PATCH 337/994] Customize CodeQL

- Run CodeQL on all branches.
- Don't install Python dependencies.
- Use v4 of actions/checkout.
---
 .github/workflows/codeql.yml | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 7f64e35cb..0d4d81efd 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -13,9 +13,7 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ "main" ]
   pull_request:
-    branches: [ "main" ]
   schedule:
     - cron: '27 10 * * 3'
 
@@ -45,13 +43,14 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
+        setup-python-dependencies: false
         # If you wish to specify custom queries, you can do so here or in a config file.
         # By default, queries listed here will override any specified in a config file.
         # Prefix the list here with "+" to use these queries and those in the config file.

From d46c70d8059f20cab655720ec5792d18b71daad0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Dec 2023 13:11:25 +0000
Subject: [PATCH 338/994] Bump github/codeql-action from 2 to 3

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [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/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .github/workflows/codeql.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 0d4d81efd..ae5241898 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -47,7 +47,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
         setup-python-dependencies: false
@@ -62,7 +62,7 @@ jobs:
     # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -75,6 +75,6 @@ jobs:
     #     ./location_of_script_within_repo/buildscript.sh
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
       with:
         category: "/language:${{matrix.language}}"

From b12a54a2fc7e988667f95145833a0dc8402b084d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Dec 2023 19:51:34 -0500
Subject: [PATCH 339/994] Use Path.touch to create files for rmtree tests

It's not necessary to use `write_bytes(b"")`, because pathlib.Path
has `touch()`.
---
 test/test_util.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 6616e1067..9408a91a0 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -46,7 +46,7 @@ def permission_error_tmpdir(tmp_path):
     """Fixture to test permissions errors in situations where they are not overcome."""
     td = tmp_path / "testdir"
     td.mkdir()
-    (td / "x").write_bytes(b"")
+    (td / "x").touch()
 
     # Set up PermissionError on Windows, where we can't delete read-only files.
     (td / "x").chmod(stat.S_IRUSR)
@@ -73,7 +73,7 @@ def test_deletes_nested_dir_with_files(self, tmp_path):
             td / "s" / "y",
             td / "s" / "z",
         ):
-            f.write_bytes(b"")
+            f.touch()
 
         try:
             rmtree(td)
@@ -95,7 +95,7 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
         for d in td, td / "sub":
             d.mkdir()
         for f in td / "x", td / "sub" / "y":
-            f.write_bytes(b"")
+            f.touch()
             f.chmod(0)
 
         try:
@@ -115,7 +115,7 @@ def test_avoids_changing_permissions_outside_tree(self, tmp_path):
 
         dir1 = tmp_path / "dir1"
         dir1.mkdir()
-        (dir1 / "file").write_bytes(b"")
+        (dir1 / "file").touch()
         (dir1 / "file").chmod(stat.S_IRUSR)
         old_mode = (dir1 / "file").stat().st_mode
 

From 6a8ed70a6003c13800d908ab055038eb61ba4ce9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Dec 2023 20:00:56 -0500
Subject: [PATCH 340/994] Run test_env_vars_for_windows_tests only on Windows

This skips the tests of how the HIDE_WINDOWS_KNOWN_ERRORS and
HIDE_WINDOWS_FREEZE_ERRORS environment variables affect the
same-named attributes of git.util, except when testing on Windows.

These are parsed only to ever set a True value on Windows, but
checking that this is the case is less important ever since
git.util.rmtree was changed to not check HIDE_WINDOWS_KNOWN_ERRORS
on other systems (and this is covered in other tests).

Setting the variables to True on non-Windows systems would still
have a bad effect on the tests themselves, some of which use them
as skip or xfail conditions separate from the skipping logic in
git.util.rmtree. However, this is effectively using them as part of
the test suite (which they were initially meant for and which they
may eventually go back to being, for #790), where they would not
ordinarily have tests.

The benefit and motivation for running these tests only on Windows
is that the tests can be simplified, so that their parameter sets
are no longer confusing. That change is also made here.
---
 test/test_util.py | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index 9408a91a0..f3769088c 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -207,24 +207,28 @@ def _run_parse(name, value):
         )
         return ast.literal_eval(output)
 
+    @pytest.mark.skipif(
+        os.name != "nt",
+        reason="These environment variables are only used on Windows.",
+    )
     @pytest.mark.parametrize(
         "env_var_value, expected_truth_value",
         [
-            (None, os.name == "nt"),  # True on Windows when the environment variable is unset.
+            (None, True),  # When the environment variable is unset.
             ("", False),
             (" ", False),
             ("0", False),
-            ("1", os.name == "nt"),
+            ("1", True),
             ("false", False),
-            ("true", os.name == "nt"),
+            ("true", True),
             ("False", False),
-            ("True", os.name == "nt"),
+            ("True", True),
             ("no", False),
-            ("yes", os.name == "nt"),
+            ("yes", True),
             ("NO", False),
-            ("YES", os.name == "nt"),
+            ("YES", True),
             (" no  ", False),
-            (" yes  ", os.name == "nt"),
+            (" yes  ", True),
         ],
     )
     @pytest.mark.parametrize(

From 487a4fdb38efc82d30111e49f941a25cba4aa7a7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Dec 2023 22:39:11 -0500
Subject: [PATCH 341/994] Add a test for git.index.util.TemporaryFileSwap

This is a general test for TemporaryFileSwap, but by being
parametrized by the type of file_path, it reveals a regression
introduced in 9e86053 (#1770). TemporaryFileSwap still works when
file_path is a string, but is now broken when it is a Path. That
worked before, and the type annotations document that it should
be able to work. This is at least a bug because TemporaryFileSwap
is public. (I am unsure whether, in practice, GitPython itself uses
it in a way that sometimes passes a Path object as file_path. But
code that uses GitPython may call it directly and pass Path.)
---
 test/test_index.py | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/test/test_index.py b/test/test_index.py
index 2f97f0af8..c3f3b4fae 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -34,10 +34,11 @@
 )
 from git.index.fun import hook_path
 from git.index.typ import BaseIndexEntry, IndexEntry
+from git.index.util import TemporaryFileSwap
 from git.objects import Blob
-from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
 from git.util import Actor, hex_to_bin, rmtree
 from gitdb.base import IStream
+from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
 
 HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
 
@@ -1087,3 +1088,25 @@ def test_index_add_pathlike(self, rw_repo):
         file.touch()
 
         rw_repo.index.add(file)
+
+
+class TestIndexUtils:
+    @pytest.mark.parametrize("file_path_type", [str, Path])
+    def test_temporary_file_swap(self, tmp_path, file_path_type):
+        file_path = tmp_path / "foo"
+        file_path.write_bytes(b"some data")
+
+        with TemporaryFileSwap(file_path_type(file_path)) as ctx:
+            assert Path(ctx.file_path) == file_path
+            assert not file_path.exists()
+
+            # Recreate it with new data, so we can observe that they're really separate.
+            file_path.write_bytes(b"other data")
+
+            temp_file_path = Path(ctx.tmp_file_path)
+            assert temp_file_path.parent == file_path.parent
+            assert temp_file_path.name.startswith(file_path.name)
+            assert temp_file_path.read_bytes() == b"some data"
+
+        assert not temp_file_path.exists()
+        assert file_path.read_bytes() == b"some data"  # Not b"other data".

From 1ddf953ea0d7625485ed02c90b03aae7f939ec46 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Dec 2023 22:58:17 -0500
Subject: [PATCH 342/994] Fix TemporaryFileSwap bug when file_path is a Path

This fixes the regression introduced in 9e86053 (#1770) where the
file_path argument to TemporaryFileSwap.__init__ could no longer be
a Path object.

The change also makes this truer to the code from before #1770,
still without the race condition fixed there, in that str was
called on file_path then as well. However, it is not clear that
this is a good thing, because this is not an idiomatic use of
mkstemp. The reason the `prefix` cannot be a Path is that it is
expected to be a filename prefix, with leading directories given in
the `dir` argument.
---
 git/index/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/index/util.py b/git/index/util.py
index 1c3b1c4ad..03de80f71 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -41,7 +41,7 @@ class TemporaryFileSwap:
 
     def __init__(self, file_path: PathLike) -> None:
         self.file_path = file_path
-        fd, self.tmp_file_path = tempfile.mkstemp(prefix=self.file_path, dir="")
+        fd, self.tmp_file_path = tempfile.mkstemp(prefix=str(self.file_path), dir="")
         os.close(fd)
         with contextlib.suppress(OSError):  # It may be that the source does not exist.
             os.replace(self.file_path, self.tmp_file_path)

From b438c459d91243b685f540ccb1c124260e9d28b9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Dec 2023 23:29:14 -0500
Subject: [PATCH 343/994] Refactor TemporaryFileSwap.__init__ for clarity

This changes the mkstemp call TemporaryFileSwap uses to atomically
create (and thus reserve) the temporary file, so that it passes
only a filename (i.e., basename) prefix as `prefix`, and passes all
other path components (i.e., the directory) as `dir`. (If the
original path has no directory, then `dir` is "" as before.)

This makes the mkstemp call *slightly* more idiomatic. This also
makes it clearer, as it is no longer using `prefix` for something
that feels like it should be possible to pass as a Path object.

Although mkstemp does not accept a Path as `prefix`, it does (as
expected) accept one as `dir`. However, to keep the code simple,
I'm passing str for both. The os.path.split function accepts both
str and Path (since Python 3.6), and returns str objects, which are
now used for the `dir` and `prefix` arguments to mkstemp.

For unusual cases, this may technically not be a refactoring. For
example, a file_path of "a/b//c" will be split into "a/b" and "c".
If the automatically generated temporary file suffix is "xyz", then
that results in a tmp_file_path of "a/b/cxyz" where "a/b//cxyz"
would have been used before. The tmp_file_path attribute of a
TemporaryFileSwap object is public (and used in filesystem calls).

However, no guarantee has ever been given that the temporary file
path have the original path as an exact string prefix. I believe
the slightly weaker relationship I expressed in the recently
introduced test_temporary_file_swap -- another file in the same
directory, named with the original filename with more characters,
consisting of equivalent path components in the same order -- has
always been the intended one.

Note that this slight possible variation does not apply to the
file_path attribute. That attribute is always kept exactly as it
was, both in its type and its value, and it always used unmodified
in calls that access the filesystem.
---
 git/index/util.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/git/index/util.py b/git/index/util.py
index 03de80f71..61039fe7c 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -41,7 +41,8 @@ class TemporaryFileSwap:
 
     def __init__(self, file_path: PathLike) -> None:
         self.file_path = file_path
-        fd, self.tmp_file_path = tempfile.mkstemp(prefix=str(self.file_path), dir="")
+        dirname, basename = osp.split(file_path)
+        fd, self.tmp_file_path = tempfile.mkstemp(prefix=basename, dir=dirname)
         os.close(fd)
         with contextlib.suppress(OSError):  # It may be that the source does not exist.
             os.replace(self.file_path, self.tmp_file_path)

From 4e91a6c7b521dc7d0331dbb5455e9c424d26d655 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Dec 2023 23:56:55 -0500
Subject: [PATCH 344/994] Tweak formatting for `@pytest.mark.parametrize`

This removes the "fmt: off" / "fmt: on" directives around the
`@pytest.mark.parametrize` decoration on test_blob_filter, and
reformats it with black, for consistency with other such
decorations.

The style used there, *if* it could be specified as a rule and thus
used without "fmt:" directives, may be nicer than how black formats
multi-line mark decorations. However, since that decoration was
written, there have been a number of other such decorations, which
have been in black style.

This also removes the only (or only remaining?) "fmt:" directive in
the codebase. As such, it should possibly have been done in #1760.
---
 test/test_blob_filter.py | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/test/test_blob_filter.py b/test/test_blob_filter.py
index a91f211bf..ddd83079a 100644
--- a/test/test_blob_filter.py
+++ b/test/test_blob_filter.py
@@ -14,14 +14,15 @@
 from git.types import PathLike
 
 
-# fmt: off
-@pytest.mark.parametrize('paths, path, expected_result', [
-    ((Path("foo"),), Path("foo"), True),
-    ((Path("foo"),), Path("foo/bar"), True),
-    ((Path("foo/bar"),), Path("foo"), False),
-    ((Path("foo"), Path("bar")), Path("foo"), True),
-])
-# fmt: on
+@pytest.mark.parametrize(
+    "paths, path, expected_result",
+    [
+        ((Path("foo"),), Path("foo"), True),
+        ((Path("foo"),), Path("foo/bar"), True),
+        ((Path("foo/bar"),), Path("foo"), False),
+        ((Path("foo"), Path("bar")), Path("foo"), True),
+    ],
+)
 def test_blob_filter(paths: Sequence[PathLike], path: PathLike, expected_result: bool) -> None:
     """Test the blob filter."""
     blob_filter = BlobFilter(paths)

From f664a0b85dd6be4bb8fd16d7283fd1be0f81c8c8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 21 Dec 2023 00:58:33 -0500
Subject: [PATCH 345/994] Add xfail marks to hook tests for
 WinBashStatus.Absent

Precise xfail marks were added to commit hook tests in #1745, but
in a few tests I didn't get it right for WinBashStatus.Absent. That
is, on a Windows system that has no bash.exe at all:

- More of the tests are unable to pass than have xfail marks.

- One of the tests, test_commit_msg_hook_success, which does have
  an xfail mark for that, wrongly combines it with the xfail mark
  for WinBashStatus.Wsl. That test is the only one where the WSL
  bash.exe, even when a working WSL distribution is installed, is
  unable to pass. But using a single mark there is wrong, in part
  because the "reason" is not correct for both, but even more so
  because the exceptions they raise are different: AssertionError
  is raised when the WSL bash.exe is used in that test, but when
  bash.exe is altogether absent, HookExecutionError is raised.

This fixes that by adding xfail marks for WinBashStatus.Absent
where missing, and splitting test_commit_msg_hook_success's xfail
mark that unsuccessfully tried to cover two conditions into two
separate marks, each of which gives a correct reason and exception.

This commit also rewords the xfail reason given for WslNoDistro,
which was somewhat unclear and potentially ambiguous, to make it
clearer.
---
 test/test_index.py | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 2f97f0af8..3f0990ecc 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -991,9 +991,14 @@ class Mocked:
         rel = index._to_relative_path(path)
         self.assertEqual(rel, os.path.relpath(path, root))
 
+    @pytest.mark.xfail(
+        type(_win_bash_status) is WinBashStatus.Absent,
+        reason="Can't run a hook on Windows without bash.exe.",
+        rasies=HookExecutionError,
+    )
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
-        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
         raises=HookExecutionError,
     )
     @with_rw_repo("HEAD", bare=True)
@@ -1004,7 +1009,7 @@ def test_pre_commit_hook_success(self, rw_repo):
 
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
-        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
         raises=AssertionError,
     )
     @with_rw_repo("HEAD", bare=True)
@@ -1030,13 +1035,18 @@ def test_pre_commit_hook_fail(self, rw_repo):
             raise AssertionError("Should have caught a HookExecutionError")
 
     @pytest.mark.xfail(
-        type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.Wsl},
+        type(_win_bash_status) is WinBashStatus.Absent,
+        reason="Can't run a hook on Windows without bash.exe.",
+        rasies=HookExecutionError,
+    )
+    @pytest.mark.xfail(
+        type(_win_bash_status) is WinBashStatus.Wsl,
         reason="Specifically seems to fail on WSL bash (in spite of #1399)",
         raises=AssertionError,
     )
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
-        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
         raises=HookExecutionError,
     )
     @with_rw_repo("HEAD", bare=True)
@@ -1054,7 +1064,7 @@ def test_commit_msg_hook_success(self, rw_repo):
 
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
-        reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
         raises=AssertionError,
     )
     @with_rw_repo("HEAD", bare=True)

From e1486474a2381763e11932b3bb15e08f6e9faf53 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 21 Dec 2023 01:31:54 -0500
Subject: [PATCH 346/994] Add a direct test of run_commit_hook

This has three benefits:

- run_commit_hook is public, being listed in git.index.fun.__all__,
  so it makes sense for it to have its own test.

- When investigating (future, or current xfail-covered) failure of
  functions that use run_commit_hook, it will be useful to compare
  the results of other tests that already do exist to that of a
  direct test of run_commit_hook.

- When changing which bash.exe run_commit_hook selects to use on
  Windows (including to ameliorate the limitation covered by the
  WinBashStatus.WslNoDistro xfail marks, or to attempt other
  possible changes suggested in #1745), or even just to investigate
  the possibility of doing so, it will make sense to add tests like
  this test but for more specific conditions or edge cases. Having
  this typical-case test to compare to should be helpful both for
  writing such tests and for efficiently verifying that the
  conditions they test are really the triggers for any failures.
---
 test/test_index.py | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/test/test_index.py b/test/test_index.py
index 3f0990ecc..afceda131 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -32,7 +32,7 @@
     InvalidGitRepositoryError,
     UnmergedEntriesError,
 )
-from git.index.fun import hook_path
+from git.index.fun import hook_path, run_commit_hook
 from git.index.typ import BaseIndexEntry, IndexEntry
 from git.objects import Blob
 from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
@@ -991,6 +991,24 @@ class Mocked:
         rel = index._to_relative_path(path)
         self.assertEqual(rel, os.path.relpath(path, root))
 
+    @pytest.mark.xfail(
+        type(_win_bash_status) is WinBashStatus.Absent,
+        reason="Can't run a hook on Windows without bash.exe.",
+        rasies=HookExecutionError,
+    )
+    @pytest.mark.xfail(
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
+        raises=HookExecutionError,
+    )
+    @with_rw_repo("HEAD", bare=True)
+    def test_run_commit_hook(self, rw_repo):
+        index = rw_repo.index
+        _make_hook(index.repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt")
+        run_commit_hook("fake-hook", index)
+        output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
+        self.assertEqual(output, "ran fake hook\n")
+
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",

From 6e4cee4fa708465cf714e36a9dc8b9b6b94acc0c Mon Sep 17 00:00:00 2001
From: Stefan Gmeiner <stefangm42@gmail.com>
Date: Thu, 21 Dec 2023 21:33:37 +0100
Subject: [PATCH 347/994] Fix Items of type PathLike

---
 git/index/base.py  |  2 +-
 test/test_index.py | 12 +++++++-----
 2 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 112ad3feb..cffb213a9 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -940,7 +940,7 @@ def _items_to_rela_paths(
         for item in items:
             if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
                 paths.append(self._to_relative_path(item.path))
-            elif isinstance(item, str):
+            elif isinstance(item, (str, os.PathLike)):
                 paths.append(self._to_relative_path(item))
             else:
                 raise TypeError("Invalid item type: %r" % item)
diff --git a/test/test_index.py b/test/test_index.py
index 6b746b8b4..50d941e83 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -558,14 +558,16 @@ def test_index_mutation(self, rw_repo):
         def mixed_iterator():
             count = 0
             for entry in index.entries.values():
-                type_id = count % 4
-                if type_id == 0:  # path
+                type_id = count % 5
+                if type_id == 0:  # path (str)
                     yield entry.path
-                elif type_id == 1:  # blob
+                elif type_id == 1:  # path (PathLike)
+                    yield Path(entry.path)
+                elif type_id == 2:  # blob
                     yield Blob(rw_repo, entry.binsha, entry.mode, entry.path)
-                elif type_id == 2:  # BaseIndexEntry
+                elif type_id == 3:  # BaseIndexEntry
                     yield BaseIndexEntry(entry[:4])
-                elif type_id == 3:  # IndexEntry
+                elif type_id == 4:  # IndexEntry
                     yield entry
                 else:
                     raise AssertionError("Invalid Type")

From 53e73830f64e13a88b0e246fb825944a6fe2848e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 00:57:14 -0500
Subject: [PATCH 348/994] Remove explicit PushInfo/FetchInfo inheritance from
 object

I had missed these in f78587f (#1725).
---
 git/remote.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 4055dba2e..98a421b3a 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -130,7 +130,7 @@ def to_progress_instance(
     return progress
 
 
-class PushInfo(IterableObj, object):
+class PushInfo(IterableObj):
     """
     Carries information about the result of a push operation of a single head::
 
@@ -300,7 +300,7 @@ def raise_if_error(self) -> None:
             raise self.error
 
 
-class FetchInfo(IterableObj, object):
+class FetchInfo(IterableObj):
     """
     Carries information about the results of a fetch operation of a single head::
 

From 96fc3547cf62c5ade1477c24566c8b34254a1507 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 01:55:30 -0500
Subject: [PATCH 349/994] Add tests for current Submodule.iter_items behavior

Where the behavior is intended.

In the case of an invalid hash (or IOError, which in Python 2 was
a subclass of OSError but now is just another name for it), the
behavior of just yielding no items may be unintuitive, since on
most other errors an exception is raised.

However, examining the code reveals this behavior is clearly
intentional. Furthrmore, it may be reasonable for applications to
rely on it, and it may be convenient in some situations. For
backward compatibility, it probably can't be changed significantly.

This adds tests that show both an error that does raise an
error-representing exception -- a well-formed hash not present in
the repository raising ValueError with a suitable message -- and an
error that silently causes the iterator to yield zero items.
---
 test/test_submodule.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 4dc89f98f..5226d7a6e 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -688,6 +688,17 @@ def test_root_module(self, rwrepo):
         # gitdb: has either 1 or 2 submodules depending on the version.
         assert len(nsm.children()) >= 1 and nsmc.module_exists()
 
+    def test_iter_items_from_nonexistent_hash(self):
+        it = Submodule.iter_items(self.rorepo, "b4ecbfaa90c8be6ed6d9fb4e57cc824663ae15b4")
+        with self.assertRaisesRegex(ValueError, r"\bcould not be resolved\b"):
+            next(it)
+
+    def test_iter_items_from_invalid_hash(self):
+        """Check legacy behavaior on BadName (also applies to IOError, i.e. OSError)."""
+        it = Submodule.iter_items(self.rorepo, "xyz")
+        with self.assertRaises(StopIteration):
+            next(it)
+
     @with_rw_repo(k_no_subm_tag, bare=False)
     def test_first_submodule(self, rwrepo):
         assert len(list(rwrepo.iter_submodules())) == 0

From f5dc1c4713dfb937d45d10a595ac879d6e76481c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 02:16:28 -0500
Subject: [PATCH 350/994] Expand "invalid hash" test to assert normal
 StopIteration

Returning an explicit value from a generator function causes that
value to be bound to the `value` attribute of the StopIteration
exception. This is available as the result of "yield from" when it
is used as an expression; or by explicitly catching StopIteration,
binding the StopIteration exception to a variable, and accessing
the attribute. This feature of generators is rarely used.

The `return iter([])` statement in Submodule.iter_items uses this
feature, causing the resulting StopIteration exception object to
have a `value` attribute that refers to a separate second iterator
that also yields no values (#1779).

From context, this behavior is clearly not the goal; a bare return
statement should be used here (which has the same effect except for
the `value` attribute of the StopIteration exception). The code had
used a bare return prior to 82b131c (#1282), when `return` was
changed to `return iter([])`. That was part of a change that added
numerous type annotations. It looks like it was either a mistake,
or possibly an attempt to work around an old bug in a static type
checker.

This commit extends the test_iter_items_from_invalid_hash test to
assert that the `value` attribute of the StopIteration is its usual
default value of None. This commit only extends the test; it does
not fix the bug.
---
 test/test_submodule.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/test_submodule.py b/test/test_submodule.py
index 5226d7a6e..993f6b57e 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -696,8 +696,9 @@ def test_iter_items_from_nonexistent_hash(self):
     def test_iter_items_from_invalid_hash(self):
         """Check legacy behavaior on BadName (also applies to IOError, i.e. OSError)."""
         it = Submodule.iter_items(self.rorepo, "xyz")
-        with self.assertRaises(StopIteration):
+        with self.assertRaises(StopIteration) as ctx:
             next(it)
+        self.assertIsNone(ctx.exception.value)
 
     @with_rw_repo(k_no_subm_tag, bare=False)
     def test_first_submodule(self, rwrepo):

From c3c008c4971f9d4189dc08e88334a207ce14298c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 02:44:56 -0500
Subject: [PATCH 351/994] In Submodule.iter_items, don't attach second empty
 iterator

This fixes the minor bug where a separate empty iterator was bound
to the StopIteration exception raised as a result of returning from
the generator function (#1779).

This change does not cause what exceptions are raised from
GitPython code in any situations, nor how many items any iterators
yield.
---
 git/objects/submodule/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 651d9535c..49dfedf9a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1401,7 +1401,7 @@ def iter_items(
             pc = repo.commit(parent_commit)  # Parent commit instance
             parser = cls._config_parser(repo, pc, read_only=True)
         except (IOError, BadName):
-            return iter([])
+            return
         # END handle empty iterator
 
         for sms in parser.sections():

From dfee31f2100d7f4a653c69c4a5a505607fe328e1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 03:48:57 -0500
Subject: [PATCH 352/994] Improve self-documentation of IterableObj and related
 classes

- Fill in the missing part of the explanation of why to favor
  iter_items over list_items in IterableObj and Iterable (#1775).

- Move the explanation of how subclasses must treat arguments from
  the list_items methods to the iter_items methods, because the
  iter_items methdos are the ones that are abstract and must be
  overridden by a well-behaved subclass, and also because, since
  the iter_items methods are preferred for use, they should be the
  place where less duplicated shared documentation resides.

- Subtantially reword the existing documentation for clarity,
  especially regarding the signifance of extra args and kwargs.

- Put the iter_items method before (i.e. above) the list_items
  method (in each of the IterableObj and Iterable classes), because
  that method is the one that should be used more often, and
  because it is also now the one with the more detailed docstring.

- Remove and old comment on a return type that said exactly the
  exact same thing as the annotation.

- In Iterable, note deprecation more consistently (and thus in more
  places).

- Rewrite the IterableClassWatcher class docstring to explain
  exactly what that metaclass achieves.
---
 git/util.py | 84 ++++++++++++++++++++++++++++++++---------------------
 1 file changed, 51 insertions(+), 33 deletions(-)

diff --git a/git/util.py b/git/util.py
index 0a5da7d71..5acc001f7 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1183,7 +1183,8 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None:
 
 
 class IterableClassWatcher(type):
-    """Metaclass that watches."""
+    """Metaclass that issues :class:`DeprecationWarning` when :class:`git.util.Iterable`
+    is subclassed."""
 
     def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
         for base in bases:
@@ -1199,23 +1200,42 @@ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
 
 
 class Iterable(metaclass=IterableClassWatcher):
-    """Defines an interface for iterable items, so there is a uniform way to retrieve
-    and iterate items within the git repository."""
+    """Deprecated, use :class:`IterableObj` instead.
+
+    Defines an interface for iterable items, so there is a uniform way to retrieve
+    and iterate items within the git repository.
+    """
 
     __slots__ = ()
 
     _id_attribute_ = "attribute that most suitably identifies your instance"
 
     @classmethod
-    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
+    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
+        # return typed to be compatible with subtypes e.g. Remote
+        """Deprecated, use :class:`IterableObj` instead.
+
+        Find (all) items of this type.
+
+        Subclasses can specify ``args`` and ``kwargs`` differently, and may use them for
+        filtering. However, when the method is called with no additional positional or
+        keyword arguments, subclasses are obliged to to yield all items.
+
+        :return: Iterator yielding Items
         """
-        Deprecated, use IterableObj instead.
+        raise NotImplementedError("To be implemented by Subclass")
+
+    @classmethod
+    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
+        """Deprecated, use :class:`IterableObj` instead.
+
+        Find (all) items of this type and collect them into a list.
 
-        Find all items of this type - subclasses can specify args and kwargs differently.
-        If no args are given, subclasses are obliged to return all items if no additional
-        arguments arg given.
+        For more information about the arguments, see :meth:`list_items`.
 
-        :note: Favor the iter_items method as it will
+        :note: Favor the :meth:`iter_items` method as it will avoid eagerly collecting
+            all items. When there are many items, that can slow performance and increase
+            memory usage.
 
         :return: list(Item,...) list of item instances
         """
@@ -1223,15 +1243,6 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
 
-    @classmethod
-    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
-        # return typed to be compatible with subtypes e.g. Remote
-        """For more information about the arguments, see list_items.
-
-        :return: Iterator yielding Items
-        """
-        raise NotImplementedError("To be implemented by Subclass")
-
 
 @runtime_checkable
 class IterableObj(Protocol):
@@ -1246,13 +1257,30 @@ class IterableObj(Protocol):
     _id_attribute_: str
 
     @classmethod
-    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
+    @abstractmethod
+    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:
+        # Return-typed to be compatible with subtypes e.g. Remote.
+        """Find (all) items of this type.
+
+        Subclasses can specify ``args`` and ``kwargs`` differently, and may use them for
+        filtering. However, when the method is called with no additional positional or
+        keyword arguments, subclasses are obliged to to yield all items.
+
+        For more information about the arguments, see list_items.
+
+        :return: Iterator yielding Items
         """
-        Find all items of this type - subclasses can specify args and kwargs differently.
-        If no args are given, subclasses are obliged to return all items if no additional
-        arguments arg given.
+        raise NotImplementedError("To be implemented by Subclass")
+
+    @classmethod
+    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
+        """Find (all) items of this type and collect them into a list.
+
+        For more information about the arguments, see :meth:`list_items`.
 
-        :note: Favor the iter_items method as it will
+        :note: Favor the :meth:`iter_items` method as it will avoid eagerly collecting
+            all items. When there are many items, that can slow performance and increase
+            memory usage.
 
         :return: list(Item,...) list of item instances
         """
@@ -1260,16 +1288,6 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
 
-    @classmethod
-    @abstractmethod
-    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:  # Iterator[T_IterableObj]:
-        # Return-typed to be compatible with subtypes e.g. Remote.
-        """For more information about the arguments, see list_items.
-
-        :return: Iterator yielding Items
-        """
-        raise NotImplementedError("To be implemented by Subclass")
-
 
 # } END classes
 

From 94a85d1159e6374e901df4e3bd284cf55b623e66 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 14:31:28 -0500
Subject: [PATCH 353/994] Convert constant and attribute comments to docstrings

These are "non-reified docstrings" as described in #1734. They are
not made part of the data model, but most editors will display
them, including on symbols present in code of other projects that
use the GitPython library. A number of these have already been
added, but this adds what will probably be most of the remaining
ones.

For the most part, this doesn't create documentation where there
wasn't any, but instead converts existing comments to "docstrings."
In a handful of cases, they are expanded, reworded, or a docstring
added.

This also fixes some small style inconsistencies that were missed
in #1725, and moves a comment that had become inadvertently
displaced due to autoformatting to the item it was meant for.

The major omission here is HIDE_WINDOWS_KNOWN_ERRORS and
HIDE_WINDOWS_FREEZE_ERRORS. This doesn't convert the comments above
them to "docstrings," for a few reasons. They are not specific to
either of the symbols, they are oriented toward considerations that
are not really relevant except when developing GitPython itself,
and they are out of date. Also, because HIDE_WINDOWS_KNOWN_ERRORS
is listed in __all__, increasing the level of documentation for it
might be taken as a committment to preserve some aspect of its
current behavior, which could interfere with progress on #790. So
I've kept those comments as comments, and unchanged, for now.
---
 git/cmd.py                    | 25 ++++++++++++++++---------
 git/config.py                 | 20 ++++++++++++--------
 git/diff.py                   |  2 +-
 git/index/base.py             |  6 ++++--
 git/index/fun.py              |  4 +++-
 git/objects/submodule/base.py | 11 ++++++-----
 git/repo/base.py              | 31 ++++++++++++++++++++-----------
 git/util.py                   |  4 ++--
 8 files changed, 64 insertions(+), 39 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 27148d3d6..9518c2c8c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -273,23 +273,30 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     # CONFIGURATION
 
-    git_exec_name = "git"  # Default that should work on Linux and Windows.
+    git_exec_name = "git"
+    """Default git command that should work on Linux, Windows, and other systems."""
 
-    # Enables debugging of GitPython's git commands.
     GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
+    """Enables debugging of GitPython's git commands."""
 
-    # If True, a shell will be used when executing git commands.
-    # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126
-    # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required.
-    # Override this value using `Git.USE_SHELL = True`.
     USE_SHELL = False
+    """If True, a shell will be used when executing git commands.
+
+    This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126
+    and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required.
+
+    Override this value using ``Git.USE_SHELL = True``.
+    """
 
-    # Provide the full path to the git executable. Otherwise it assumes git is in the path.
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
     _refresh_env_var = "GIT_PYTHON_REFRESH"
+
     GIT_PYTHON_GIT_EXECUTABLE = None
-    # Note that the git executable is actually found during the refresh step in
-    # the top level __init__.
+    """Provide the full path to the git executable. Otherwise it assumes git is in the path.
+
+    Note that the git executable is actually found during the refresh step in
+    the top level ``__init__``.
+    """
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
diff --git a/git/config.py b/git/config.py
index 5708a7385..2730ddaf3 100644
--- a/git/config.py
+++ b/git/config.py
@@ -65,12 +65,14 @@
 log.addHandler(logging.NullHandler())
 
 
-# The configuration level of a configuration file.
 CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
+"""The configuration level of a configuration file."""
 
-# Section pattern to detect conditional includes.
-# https://git-scm.com/docs/git-config#_conditional_includes
 CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
+"""Section pattern to detect conditional includes.
+
+See: https://git-scm.com/docs/git-config#_conditional_includes
+"""
 
 
 class MetaParserBuilder(abc.ABCMeta):  # noqa: B024
@@ -283,12 +285,14 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     """
 
     # { Configuration
-    # The lock type determines the type of lock to use in new configuration readers.
-    # They must be compatible to the LockFile interface.
-    # A suitable alternative would be the BlockingLockFile
     t_lock = LockFile
-    re_comment = re.compile(r"^\s*[#;]")
+    """The lock type determines the type of lock to use in new configuration readers.
 
+    They must be compatible to the LockFile interface.
+    A suitable alternative would be the :class:`~git.util.BlockingLockFile`.
+    """
+
+    re_comment = re.compile(r"^\s*[#;]")
     # } END configuration
 
     optvalueonly_source = r"\s*(?P<option>[^:=\s][^:=]*)"
@@ -299,8 +303,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
 
     del optvalueonly_source
 
-    # list of RawConfigParser methods able to change the instance
     _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
+    """List of RawConfigParser methods able to change the instance."""
 
     def __init__(
         self,
diff --git a/git/diff.py b/git/diff.py
index 25334512a..3c6c3fb13 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -49,8 +49,8 @@
 
 __all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE")
 
-# Special object to compare against the empty tree in diffs.
 NULL_TREE = object()
+"""Special object to compare against the empty tree in diffs."""
 
 _octal_byte_re = re.compile(rb"\\([0-9]{3})")
 
diff --git a/git/index/base.py b/git/index/base.py
index cffb213a9..5ba817ed3 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -142,9 +142,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
 
     __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
 
-    _VERSION = 2  # Latest version we support.
+    _VERSION = 2
+    """The latest version we support."""
 
-    S_IFGITLINK = S_IFGITLINK  # A submodule.
+    S_IFGITLINK = S_IFGITLINK
+    """Flags for a submodule."""
 
     def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
         """Initialize this Index instance, optionally from the given ``file_path``.
diff --git a/git/index/fun.py b/git/index/fun.py
index 97f2c88e6..402f85d2b 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -49,7 +49,9 @@
 # ------------------------------------------------------------------------------------
 
 
-S_IFGITLINK = S_IFLNK | S_IFDIR  # A submodule.
+S_IFGITLINK = S_IFLNK | S_IFDIR
+"""Flags for a submodule."""
+
 CE_NAMEMASK_INV = ~CE_NAMEMASK
 
 __all__ = (
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 651d9535c..d1eb12cac 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -77,9 +77,9 @@ class UpdateProgress(RemoteProgress):
 UPDWKTREE = UpdateProgress.UPDWKTREE
 
 
-# IndexObject comes via util module, its a 'hacky' fix thanks to pythons import
-# mechanism which cause plenty of trouble of the only reason for packages and
-# modules is refactoring - subpackages shouldn't depend on parent packages
+# IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
+# mechanism, which causes plenty of trouble if the only reason for packages and
+# modules is refactoring - subpackages shouldn't depend on parent packages.
 class Submodule(IndexObject, TraversableIterableObj):
     """Implements access to a git submodule. They are special in that their sha
     represents a commit in the submodule's repository which is to be checked out
@@ -95,10 +95,11 @@ class Submodule(IndexObject, TraversableIterableObj):
     k_modules_file = ".gitmodules"
     k_head_option = "branch"
     k_head_default = "master"
-    k_default_mode = stat.S_IFDIR | stat.S_IFLNK  # Submodules are directories with link-status.
+    k_default_mode = stat.S_IFDIR | stat.S_IFLNK
+    """Submodule flags. Submodules are directories with link-status."""
 
-    # This is a bogus type for base class compatibility.
     type: Literal["submodule"] = "submodule"  # type: ignore
+    """This is a bogus type for base class compatibility."""
 
     __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
 
diff --git a/git/repo/base.py b/git/repo/base.py
index 1eb5fe362..c252ad1f3 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -114,17 +114,18 @@ class Repo:
     'working_tree_dir' is the working tree directory, but will return None
     if we are a bare repository.
 
-    'git_dir' is the .git repository directory, which is always set."""
+    'git_dir' is the .git repository directory, which is always set.
+    """
 
     DAEMON_EXPORT_FILE = "git-daemon-export-ok"
 
-    git = cast("Git", None)  # Must exist, or  __del__  will fail in case we raise on `__init__()`
+    git = cast("Git", None)  # Must exist, or  __del__  will fail in case we raise on `__init__()`.
     working_dir: PathLike
     _working_tree_dir: Optional[PathLike] = None
     git_dir: PathLike
     _common_dir: PathLike = ""
 
-    # precompiled regex
+    # Precompiled regex
     re_whitespace = re.compile(r"\s+")
     re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
     re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$")
@@ -133,24 +134,32 @@ class Repo:
     re_tab_full_line = re.compile(r"^\t(.*)$")
 
     unsafe_git_clone_options = [
-        # This option allows users to execute arbitrary commands.
-        # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
+        # Executes arbitrary commands:
         "--upload-pack",
         "-u",
-        # Users can override configuration variables
-        # like `protocol.allow` or `core.gitProxy` to execute arbitrary commands.
-        # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
+        # Can override configuration variables that execute arbitrary commands:
         "--config",
         "-c",
     ]
+    """Options to ``git clone`` that allow arbitrary commands to be executed.
 
-    # invariants
-    # represents the configuration level of a configuration file
+    The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
+    directly:
+    https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
+
+    The ``--config``/``-c`` option allows users to override configuration variables like
+    ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands:
+    https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
+    """
+
+    # Invariants
     config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
+    """Represents the configuration level of a configuration file."""
 
     # Subclass configuration
-    # Subclasses may easily bring in their own custom types by placing a constructor or type here
     GitCommandWrapperType = Git
+    """Subclasses may easily bring in their own custom types by placing a constructor or
+    type here."""
 
     def __init__(
         self,
diff --git a/git/util.py b/git/util.py
index 0a5da7d71..4f25fbefe 100644
--- a/git/util.py
+++ b/git/util.py
@@ -557,8 +557,8 @@ class RemoteProgress:
         "_cur_line",
         "_seen_ops",
         "error_lines",  # Lines that started with 'error:' or 'fatal:'.
-        "other_lines",
-    )  # Lines not denoting progress (i.e.g. push-infos).
+        "other_lines",  # Lines not denoting progress (i.e.g. push-infos).
+    )
     re_op_absolute = re.compile(r"(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
     re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")
 

From 106bbe6017b8b460fe2a579ee20a93cccfdcb878 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 17:02:20 -0500
Subject: [PATCH 354/994] Update USE_SHELL docstring on why not to use it
 anymore

This expands the "docstring" associated with the Git.USE_SHELL
attribute to mention the dangers of setting it to True and explain
the old purpose it once served for graphical Windows applications
and why it is no longer required for that since 2.0.8. (See #1781.)

Although setting `Git.USE_SHELL = True` or passing `shell=True`
should rarely if ever be done and is no longer necessary even in
the specific scenario for which it was once recommended, I have
deliberately avoided claiming USE_SHELL is deprecated at this time.

Whether GitPython should formally deprecate it (documenting it as
such and issuing DeprecationWarning on some or all uses) may hinge
on whether it is possible for GitPython to incorporate enhancements
that account for and suppress at least some unintended shell
expansions when shell=True is passed through dynamic methods that
indirectly call Git.execute. The decision may also benefit from
examination of existing common uses, if any, of `USE_SHELL = True`.
---
 git/cmd.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9518c2c8c..7297e4ae6 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -282,10 +282,20 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     USE_SHELL = False
     """If True, a shell will be used when executing git commands.
 
-    This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126
-    and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required.
-
-    Override this value using ``Git.USE_SHELL = True``.
+    This exists to avoid breaking old code that may access it, but it is no longer
+    needed and should rarely if ever be used. Prior to GitPython 2.0.8, it had a narrow
+    purpose in suppressing console windows in graphical Windows applications. In 2.0.8
+    and higher, it provides no benefit, as GitPython solves that problem more robustly
+    and safely by using the ``CREATE_NO_WINDOW`` process creation flag on Windows.
+
+    Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
+    functions should be updated to use the default value of ``False`` instead. ``True``
+    is unsafe unless the effect of shell expansions is fully considered and accounted
+    for, which is not possible under most circumstances.
+
+    See:
+    - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
+    - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
     """
 
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"

From 79642247b5994093743a02a87f4448050f9e4581 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Dec 2023 18:01:55 -0600
Subject: [PATCH 355/994] Revert "Don't install black on Cygwin"

This reverts #1766.

Black 23.12.1 has been released. This has the fix for the issue
that kept GitPython's test dependencies from being installed on
Cygwin.

- https://github.com/psf/black/issues/4107
- https://github.com/psf/black/releases/tag/23.12.1
- https://pypi.org/project/black/23.12.1/
---
 test-requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index 63fa0a9cd..fcdc93c1d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,4 @@
-black ; sys_platform != "cygwin"
+black
 coverage[toml]
 ddt >= 1.1.1, != 1.4.3
 mock ; python_version < "3.8"

From c567f6fc2e2e15e5d0c1d854a1dc65651257265b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Dec 2023 05:46:54 -0500
Subject: [PATCH 356/994] Add missing pip in $PATH on Cygwin CI

This makes a pip -> pip3 symlink in /usr/bin in a new step prior to
the first step that runs the pip command. Using "pip3", "pip3.9",
or a command like "python -m pip" would work, but this allows the
Cygwin workflow to continue using the same installation commands as
the main testing workflow.

Adding this fixes two problems:

1. When the pip version installed by the python39-pip Cygwin
   package is current, so that upgrading pip doesn't install any
   new commmand, no "pip" command is created in /usr/local. This
   has happened not to be the case for a long time, which is why
   the Cygwin workflow was able to pass. (That the recent failures
   started at the merge of #1783 turns out to be a coincidence:
   rerunning jobs on prior commits has the failure, as does
   experimentally reverting it.)

2. Even when the pip version installed by python39-pip is behind
   the latest available version, pip is still used before being
   upgraded to check if setuptools is installed, to decide whether
   to upgrade it. This is to keep similar steps in the two testing
   workflows similar, since the Cygwin workflow only uses Python
   3.9, which always has setuptools. Because pip was never in $PATH
   in that step, the Cygwin workflow wrongly refrained from trying
   to upgrade setuptools.

When the "Update PyPA packages" step does find a newer version of
pip to upgrade to, it installs it in /usr/local/bin, which we have
in $PATH before /usr/bin, so the upgraded version, when present,
will still be preferred in subsequent commands, as before.

Running "pip" on Cygwin when it may not be in $PATH -- and, for one
step, never is -- was a bug introduced in e8956e5 (#1709). Before
that, "pip" still was not always available, but it was not used.
This change fixes the bug by making sure "pip" is always available.
---
 .github/workflows/cygwin-test.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 13a01dec4..13804031b 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -55,6 +55,11 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
+    - name: Ensure the "pip" command is available
+      run: |
+        # This is used unless, and before, an updated pip is installed.
+        ln -s pip3 /usr/bin/pip
+
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.

From 7ef7245456ad3953b5d1760281984f17c2a0640f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Dec 2023 06:17:09 -0500
Subject: [PATCH 357/994] Make setuptools check on CI more precise

This checks for setuptools as a full word at the very beginning of
a line of output from "pip freeze --all", to:

- Avoid the unlikely but possible case that setuptools is absent
  while something besides setuptools has "setuptools" in its line.

- Prevent "setuptools" from being passed to install multiple times.
  This causes no problems -- it's still only installed/upgraded at
  most once -- but it was making the GitHub Actions log confusing
  on Cygwin. (It could happen on other platforms, but hasn't been.)
---
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 13804031b..f3937d21e 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -63,7 +63,7 @@ jobs:
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel
+        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index e9a7486be..08ff4efdf 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -56,7 +56,7 @@ jobs:
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -oF setuptools) wheel
+        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |

From 2b768c700103a80789dff93dcb36b9530d303fd3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Dec 2023 07:06:29 -0500
Subject: [PATCH 358/994] Shorten Iterable docstrings and put IterableObj first

This shortens the git.util.Iterable docstrings, removing most of
the text duplicated from git.util.IterableObj docstrings and
instead referring the reader to the specific corresponding methods
in IterableObj. A comment about return types that appeared in both
places but really only documented IterableObj is likewise removed
from Iterable.

This also reorders the classes, placing IterableObj before Iterable
and its associated metaclass. This makes it easier to find, and may
help remind users that IterableObj is the class they should use.

These changes build on those in dfee31f (#1780).
---
 git/util.py | 99 +++++++++++++++++++++++++----------------------------
 1 file changed, 46 insertions(+), 53 deletions(-)

diff --git a/git/util.py b/git/util.py
index 5acc001f7..110961b93 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1182,54 +1182,37 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None:
         list.__delitem__(self, delindex)
 
 
-class IterableClassWatcher(type):
-    """Metaclass that issues :class:`DeprecationWarning` when :class:`git.util.Iterable`
-    is subclassed."""
-
-    def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
-        for base in bases:
-            if type(base) is IterableClassWatcher:
-                warnings.warn(
-                    f"GitPython Iterable subclassed by {name}. "
-                    "Iterable is deprecated due to naming clash since v3.1.18"
-                    " and will be removed in 3.1.20, "
-                    "Use IterableObj instead \n",
-                    DeprecationWarning,
-                    stacklevel=2,
-                )
-
-
-class Iterable(metaclass=IterableClassWatcher):
-    """Deprecated, use :class:`IterableObj` instead.
-
-    Defines an interface for iterable items, so there is a uniform way to retrieve
+@runtime_checkable
+class IterableObj(Protocol):
+    """Defines an interface for iterable items, so there is a uniform way to retrieve
     and iterate items within the git repository.
+
+    Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]
     """
 
     __slots__ = ()
 
-    _id_attribute_ = "attribute that most suitably identifies your instance"
+    _id_attribute_: str
 
     @classmethod
-    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
-        # return typed to be compatible with subtypes e.g. Remote
-        """Deprecated, use :class:`IterableObj` instead.
-
-        Find (all) items of this type.
+    @abstractmethod
+    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:
+        # Return-typed to be compatible with subtypes e.g. Remote.
+        """Find (all) items of this type.
 
         Subclasses can specify ``args`` and ``kwargs`` differently, and may use them for
         filtering. However, when the method is called with no additional positional or
         keyword arguments, subclasses are obliged to to yield all items.
 
+        For more information about the arguments, see list_items.
+
         :return: Iterator yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
 
     @classmethod
-    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
-        """Deprecated, use :class:`IterableObj` instead.
-
-        Find (all) items of this type and collect them into a list.
+    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
+        """Find (all) items of this type and collect them into a list.
 
         For more information about the arguments, see :meth:`list_items`.
 
@@ -1239,52 +1222,62 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
         :return: list(Item,...) list of item instances
         """
-        out_list: Any = IterableList(cls._id_attribute_)
+        out_list: IterableList = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
 
 
-@runtime_checkable
-class IterableObj(Protocol):
-    """Defines an interface for iterable items, so there is a uniform way to retrieve
-    and iterate items within the git repository.
+class IterableClassWatcher(type):
+    """Metaclass that issues :class:`DeprecationWarning` when :class:`git.util.Iterable`
+    is subclassed."""
 
-    Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]
+    def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
+        for base in bases:
+            if type(base) is IterableClassWatcher:
+                warnings.warn(
+                    f"GitPython Iterable subclassed by {name}. "
+                    "Iterable is deprecated due to naming clash since v3.1.18"
+                    " and will be removed in 3.1.20, "
+                    "Use IterableObj instead \n",
+                    DeprecationWarning,
+                    stacklevel=2,
+                )
+
+
+class Iterable(metaclass=IterableClassWatcher):
+    """Deprecated, use :class:`IterableObj` instead.
+
+    Defines an interface for iterable items, so there is a uniform way to retrieve
+    and iterate items within the git repository.
     """
 
     __slots__ = ()
 
-    _id_attribute_: str
+    _id_attribute_ = "attribute that most suitably identifies your instance"
 
     @classmethod
-    @abstractmethod
-    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]:
-        # Return-typed to be compatible with subtypes e.g. Remote.
-        """Find (all) items of this type.
+    def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
+        """Deprecated, use :class:`IterableObj` instead.
 
-        Subclasses can specify ``args`` and ``kwargs`` differently, and may use them for
-        filtering. However, when the method is called with no additional positional or
-        keyword arguments, subclasses are obliged to to yield all items.
+        Find (all) items of this type.
 
-        For more information about the arguments, see list_items.
+        See :meth:`IterableObj.list_items` for details on usage.
 
         :return: Iterator yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
 
     @classmethod
-    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
-        """Find (all) items of this type and collect them into a list.
+    def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
+        """Deprecated, use :class:`IterableObj` instead.
 
-        For more information about the arguments, see :meth:`list_items`.
+        Find (all) items of this type and collect them into a list.
 
-        :note: Favor the :meth:`iter_items` method as it will avoid eagerly collecting
-            all items. When there are many items, that can slow performance and increase
-            memory usage.
+        See :meth:`IterableObj.list_items` for details on usage.
 
         :return: list(Item,...) list of item instances
         """
-        out_list: IterableList = IterableList(cls._id_attribute_)
+        out_list: Any = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
         return out_list
 

From 501005f234e0d08c74463bf29592fe60a537c0d4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Dec 2023 19:49:34 -0500
Subject: [PATCH 359/994] Fix incompletely revised Iterable/IterableObj
 docstrings

Three wrong references in docstrings to list_items methods had
accidentally remained: one that was meant to be removed altogether,
and two that were meant to be references to iter_items.

This completes those changes, correcting the errors from
dfee31f (#1780) and 2b768c7 (#1785) so the docstrings make sense
and references to further information are to places with that
information.
---
 git/util.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/git/util.py b/git/util.py
index 110961b93..f6e9b1945 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1204,8 +1204,6 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_Itera
         filtering. However, when the method is called with no additional positional or
         keyword arguments, subclasses are obliged to to yield all items.
 
-        For more information about the arguments, see list_items.
-
         :return: Iterator yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
@@ -1214,7 +1212,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_Itera
     def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
         """Find (all) items of this type and collect them into a list.
 
-        For more information about the arguments, see :meth:`list_items`.
+        For more information about the arguments, see :meth:`iter_items`.
 
         :note: Favor the :meth:`iter_items` method as it will avoid eagerly collecting
             all items. When there are many items, that can slow performance and increase
@@ -1261,7 +1259,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
         Find (all) items of this type.
 
-        See :meth:`IterableObj.list_items` for details on usage.
+        See :meth:`IterableObj.iter_items` for details on usage.
 
         :return: Iterator yielding Items
         """

From a0fa2bd40f163b959d67a76c011b8b3c6d0a37c8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Dec 2023 06:01:20 -0500
Subject: [PATCH 360/994] Improve deprecation messages and other docstrings

- Fix spacing in deprecation messages.
- Update versions in deprecation messages to reflect SemVer.
- Add and fix up docstrings for methods with some deprecated uses.
- Add a couple missing docstrings on nearby attributes and methods.
- Convert is_<platform> deprecation comment to three docstrings.
- Avoid wrongly claiming is_darwin still uses os.name (see #1732).
- Convert some other comments to docstrings.
- Make disembodied """-strings docstrings or comments, per intent.
- Move some comments that were misplaced during auto-formatting.

This commit builds on the changes in 94a85d1 (#1782).
---
 git/compat.py         | 37 ++++++++++++----
 git/diff.py           | 15 ++++---
 git/objects/commit.py |  4 +-
 git/objects/util.py   | 98 ++++++++++++++++++++++++-------------------
 git/util.py           |  8 ++--
 5 files changed, 99 insertions(+), 63 deletions(-)

diff --git a/git/compat.py b/git/compat.py
index 6b1b4f547..920e44b7f 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -28,18 +28,41 @@
 # ---------------------------------------------------------------------------
 
 
-# DEPRECATED attributes providing shortcuts to operating system checks based on os.name.
-#
-# These are deprecated because it is clearer, and helps avoid bugs, to write out the
-# os.name or sys.platform checks explicitly, especially in cases where it matters which
-# is used. For example, is_win is False on Cygwin, but is often assumed True. To detect
-# Cygwin, use sys.platform == "cygwin". (Also, in the past, is_darwin was unreliable.)
-#
 is_win = os.name == "nt"
+"""Deprecated alias for ``os.name == "nt"`` to check for native Windows.
+
+This is deprecated because it is clearer to write out :attr:`os.name` or
+:attr:`sys.platform` checks explicitly, especially in cases where it matters which is
+used.
+
+:note: ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To
+    detect Cygwin, use ``sys.platform == "cygwin"``.
+"""
+
 is_posix = os.name == "posix"
+"""Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
+
+This is deprecated because it clearer to write out :attr:`os.name` or
+:attr:`sys.platform` checks explicitly, especially in cases where it matters which is
+used.
+
+:note: For POSIX systems, more detailed information is available in
+    :attr:`sys.platform`, while :attr:`os.name` is always ``"posix"`` on such systems,
+    including macOS (Darwin).
+"""
+
 is_darwin = sys.platform == "darwin"
+"""Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
+
+This is deprecated because it clearer to write out :attr:`os.name` or
+:attr:`sys.platform` checks explicitly.
+
+:note: For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
+    ``sys.platform == "darwin"`.
+"""
 
 defenc = sys.getfilesystemencoding()
+"""The encoding used to convert between Unicode and bytes filenames."""
 
 
 @overload
diff --git a/git/diff.py b/git/diff.py
index 3c6c3fb13..aba1a1080 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -4,6 +4,7 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import re
+
 from git.cmd import handle_process_output
 from git.compat import defenc
 from git.util import finalize_process, hex_to_bin
@@ -208,13 +209,15 @@ class DiffIndex(List[T_Diff]):
     The class improves the diff handling convenience.
     """
 
-    # Change type invariant identifying possible ways a blob can have changed:
-    # A = Added
-    # D = Deleted
-    # R = Renamed
-    # M = Modified
-    # T = Changed in the type
     change_type = ("A", "C", "D", "R", "M", "T")
+    """Change type invariant identifying possible ways a blob can have changed:
+
+    * ``A`` = Added
+    * ``D`` = Deleted
+    * ``R`` = Renamed
+    * ``M`` = Modified
+    * ``T`` = Changed in the type
+    """
 
     def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
         """
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 7310d66b0..5a069848c 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -352,8 +352,8 @@ def stats(self) -> Stats:
     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``.
+        :note: This property is deprecated, please use either :attr:`trailers_list` or
+            :attr:`trailers_dict``.
 
         :return:
             Dictionary containing whitespace stripped trailer information. Only contains
diff --git a/git/objects/util.py b/git/objects/util.py
index c52dc7564..b25a3f5ff 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -62,9 +62,9 @@ class TraverseNT(NamedTuple):
 T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj")  # For TraversableIterableObj.traverse()
 
 TraversedTup = Union[
-    Tuple[Union["Traversable", None], "Traversable"],  # For commit, submodule
-    "TraversedTreeTup",
-]  # for tree.traverse()
+    Tuple[Union["Traversable", None], "Traversable"],  # For Commit, Submodule.
+    "TraversedTreeTup",  # For Tree.traverse().
+]
 
 # --------------------------------------------------------------------
 
@@ -380,11 +380,15 @@ class Tree::       (cls, Tree) -> Tuple[Tree, ...]
 
     @abstractmethod
     def list_traverse(self, *args: Any, **kwargs: Any) -> Any:
-        """ """
+        """Traverse self and collect all items found.
+
+        Calling this directly only the abstract base class, including via a ``super()``
+        proxy, is deprecated. Only overridden implementations should be called.
+        """
         warnings.warn(
             "list_traverse() method should only be called from subclasses."
-            "Calling from Traversable abstract class will raise NotImplementedError in 3.1.20"
-            "Builtin sublclasses are 'Submodule', 'Tree' and 'Commit",
+            " Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
+            " The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
             DeprecationWarning,
             stacklevel=2,
         )
@@ -393,12 +397,14 @@ def list_traverse(self, *args: Any, **kwargs: Any) -> Any:
     def _list_traverse(
         self, as_edge: bool = False, *args: Any, **kwargs: Any
     ) -> IterableList[Union["Commit", "Submodule", "Tree", "Blob"]]:
-        """
+        """Traverse self and collect all items found.
+
         :return: IterableList with the results of the traversal as produced by
-            traverse()
-            Commit -> IterableList['Commit']
-            Submodule ->  IterableList['Submodule']
-            Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
+            :meth:`traverse`::
+
+                Commit -> IterableList['Commit']
+                Submodule ->  IterableList['Submodule']
+                Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
         """
         # Commit and Submodule have id.__attribute__ as IterableObj.
         # Tree has id.__attribute__ inherited from IndexObject.
@@ -421,11 +427,15 @@ def _list_traverse(
 
     @abstractmethod
     def traverse(self, *args: Any, **kwargs: Any) -> Any:
-        """ """
+        """Iterator yielding items found when traversing self.
+
+        Calling this directly on the abstract base class, including via a ``super()``
+        proxy, is deprecated. Only overridden implementations should be called.
+        """
         warnings.warn(
             "traverse() method should only be called from subclasses."
-            "Calling from Traversable abstract class will raise NotImplementedError in 3.1.20"
-            "Builtin sublclasses are 'Submodule', 'Tree' and 'Commit",
+            " Calling from Traversable abstract class will raise NotImplementedError in 4.0.0."
+            " The concrete subclasses in GitPython itself are 'Commit', 'RootModule', 'Submodule', and 'Tree'.",
             DeprecationWarning,
             stacklevel=2,
         )
@@ -441,7 +451,7 @@ def _traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
-        """:return: Iterator yielding items found when traversing self
+        """Iterator yielding items found when traversing self.
 
         :param predicate: f(i,d) returns False if item i at depth d should not be
             included in the result.
@@ -471,18 +481,18 @@ def _traverse(
             if True, return a pair of items, first being the source, second the
             destination, i.e. tuple(src, dest) with the edge spanning from source to
             destination
-        """
 
-        """
-            Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]]
-            Submodule -> Iterator[Submodule, Tuple[Submodule, Submodule]]
-            Tree -> Iterator[Union[Blob, Tree, Submodule,
-                                    Tuple[Union[Submodule, Tree], Union[Blob, Tree, Submodule]]]
-
-           ignore_self=True is_edge=True -> Iterator[item]
-           ignore_self=True is_edge=False --> Iterator[item]
-           ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]]
-           ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]
+        :return: Iterator yielding items found when traversing self::
+
+                Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]]
+                Submodule -> Iterator[Submodule, Tuple[Submodule, Submodule]]
+                Tree -> Iterator[Union[Blob, Tree, Submodule,
+                                        Tuple[Union[Submodule, Tree], Union[Blob, Tree, Submodule]]]
+
+                ignore_self=True is_edge=True -> Iterator[item]
+                ignore_self=True is_edge=False --> Iterator[item]
+                ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]]
+                ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]
         """
 
         visited = set()
@@ -547,7 +557,7 @@ class Serializable(Protocol):
     def _serialize(self, stream: "BytesIO") -> "Serializable":
         """Serialize the data of this object into the given data stream.
 
-        :note: A serialized object would ``_deserialize`` into the same object.
+        :note: A serialized object would :meth:`_deserialize` into the same object.
 
         :param stream: a file-like object
 
@@ -627,24 +637,24 @@ def traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[T_TIobj], Iterator[Tuple[T_TIobj, T_TIobj]], Iterator[TIobj_tuple]]:
-        """For documentation, see util.Traversable._traverse()"""
+        """For documentation, see :meth:`Traversable._traverse`."""
+
+        ## To typecheck instead of using cast:
+        #
+        # import itertools
+        # from git.types import TypeGuard
+        # def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit', 'Commit']]]]:
+        #     for x in inp[1]:
+        #         if not isinstance(x, tuple) and len(x) != 2:
+        #             if all(isinstance(inner, Commit) for inner in x):
+        #                 continue
+        #     return True
+        #
+        # ret = super(Commit, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge)
+        # ret_tup = itertools.tee(ret, 2)
+        # assert is_commit_traversed(ret_tup), f"{[type(x) for x in list(ret_tup[0])]}"
+        # return ret_tup[0]
 
-        """
-        # To typecheck instead of using cast.
-        import itertools
-        from git.types import TypeGuard
-        def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit', 'Commit']]]]:
-            for x in inp[1]:
-                if not isinstance(x, tuple) and len(x) != 2:
-                    if all(isinstance(inner, Commit) for inner in x):
-                        continue
-            return True
-
-        ret = super(Commit, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge)
-        ret_tup = itertools.tee(ret, 2)
-        assert is_commit_traversed(ret_tup), f"{[type(x) for x in list(ret_tup[0])]}"
-        return ret_tup[0]
-        """
         return cast(
             Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
             super()._traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge),  # type: ignore
diff --git a/git/util.py b/git/util.py
index b18dccdfe..c5afea241 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1233,10 +1233,10 @@ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
         for base in bases:
             if type(base) is IterableClassWatcher:
                 warnings.warn(
-                    f"GitPython Iterable subclassed by {name}. "
-                    "Iterable is deprecated due to naming clash since v3.1.18"
-                    " and will be removed in 3.1.20, "
-                    "Use IterableObj instead \n",
+                    f"GitPython Iterable subclassed by {name}."
+                    " Iterable is deprecated due to naming clash since v3.1.18"
+                    " and will be removed in 4.0.0."
+                    " Use IterableObj instead.",
                     DeprecationWarning,
                     stacklevel=2,
                 )

From 3af3d4314031890744ef008eda376bb32e23de66 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Dec 2023 06:59:02 -0500
Subject: [PATCH 361/994] Deprecate USE_SHELL; caution in Git.execute shell doc

This builds on 106bbe6 (#1782) to actually deprecate Git.USE_SHELL.
Its docstring is updated to state this, but no DeprecationWarning
is issued for its use at this time.

The documentation for the `shell` parameter of Git.execute is also
updated to caution against passing shell=True, but the deprecation
is not currently extended to that parameter.

Some information is duplicated across the two docstrings. I have
done this because the the USE_SHELL attribute's docstring, while
it is displayed by many editors, doesn't currently appear in
generated HTML documentation (see #1734).
---
 git/cmd.py | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7297e4ae6..488c0dea8 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -280,13 +280,12 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     """Enables debugging of GitPython's git commands."""
 
     USE_SHELL = False
-    """If True, a shell will be used when executing git commands.
+    """Deprecated. If set to True, a shell will be used when executing git commands.
 
-    This exists to avoid breaking old code that may access it, but it is no longer
-    needed and should rarely if ever be used. Prior to GitPython 2.0.8, it had a narrow
-    purpose in suppressing console windows in graphical Windows applications. In 2.0.8
-    and higher, it provides no benefit, as GitPython solves that problem more robustly
-    and safely by using the ``CREATE_NO_WINDOW`` process creation flag on Windows.
+    Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
+    in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
+    GitPython solves that problem more robustly and safely by using the
+    ``CREATE_NO_WINDOW`` process creation flag on Windows.
 
     Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
     functions should be updated to use the default value of ``False`` instead. ``True``
@@ -294,6 +293,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     for, which is not possible under most circumstances.
 
     See:
+    - :meth:`Git.execute` (on the ``shell`` parameter).
     - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
     - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
     """
@@ -919,7 +919,15 @@ def execute(
 
         :param shell:
             Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
-            It overrides :attr:`USE_SHELL` if it is not `None`.
+            If this is not `None`, it overrides :attr:`USE_SHELL`.
+
+            Passing ``shell=True`` to this or any other GitPython function should be
+            avoided, as it is unsafe under most circumstances. This is because it is
+            typically not feasible to fully consider and account for the effect of shell
+            expansions, especially when passing ``shell=True`` to other methods that
+            forward it to :meth:`Git.execute`. Passing ``shell=True`` is also no longer
+            needed (nor useful) to work around any known operating system specific
+            issues.
 
         :param env:
             A dictionary of environment variables to be passed to :class:`subprocess.Popen`.

From 7057b9b81d69a734217b2ee3d6f817c12e3d880a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Dec 2023 09:09:25 -0500
Subject: [PATCH 362/994] In handle_process_output don't forward finalizer
 result

The git.cmd.handle_process_output function is non-public
(git.cmd.__all__ only lists Git) but used throughout GitPython and
referenced in the git.util.RemoteProgress.new_message_handler
docstring. Its finalizer argument is annotated to accept an
optional callable that always returns None. It is always used this
way in GitPython and RemoteProcess.new_message_handler is annotated
accordingly. However, the handle_process_output docstring and
implementation had documented the return value as the result of the
finalizer, and the implementation had forwarded that result if
passed.

This modifies the docstring and implementation to disregard any
result, in accordance with the everywhere-annotated assumption that
the finalizer is conceptually void and the absence of any code in
GitPython that uses the result of calling handle_process_output.
None is now implicitly returned, simplifying the implementation and
bringing it and the docstring in line with annotationd and usage.

This would be a breaking change if done on a public function, but
because handle_process_output is nonpublic (and documentation for
it is omitted by Sphinx, so users probably don't wrongly think it
is public), I believe this is safe and non-breaking.
---
 git/cmd.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7297e4ae6..532086bc3 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -111,7 +111,6 @@ def handle_process_output(
 
     This function returns once the finalizer returns.
 
-    :return: Result of finalizer
     :param process: :class:`subprocess.Popen` instance
     :param stdout_handler: f(stdout_line_string), or None
     :param stderr_handler: f(stderr_line_string), or None
@@ -205,9 +204,7 @@ def pump_stream(
                 stderr_handler(error_str)  # type: ignore
 
     if finalizer:
-        return finalizer(process)
-    else:
-        return None
+        finalizer(process)
 
 
 def dashify(string: str) -> str:

From e194d46f1a8347e83949ad53e15fb51948ab278a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Dec 2023 10:04:44 -0500
Subject: [PATCH 363/994] Fix mypy warning "Missing return statement"

In git.index.base.IndexFile.from_tree, control was never able to
fall off the end, but mypy reported a "Missing return statement"
because it could not infer that the context manager didn't suppress
any exceptions.

This was for two reasons. An ExitStack context manager suppresses
exceptions in some uses and not others, depending on the context
manager objects its enter_context method is called with. In this
case, that was sufficient to produce the mypy warning regardless of
other changes, so I converted it to nested with-statements. The
nesting depth is only 2, so this is not really any worse. (I also
reworded the comments for clarity and adjusted their spacing.)

The more important cause, however, could also produce this warning
in code that uses GitPython, as git.index.util.TemporaryFileSwap is
public. Its __exit__ method was annotated to return bool. This was
itself reported by mypy as an error, because a context manager
__exit__ method that never suppresses exceptions should either
always (implicitly) return None and be annotated as such, or it can
return False as this implementation does but should then have its
return type annotated as Literal[False].

So this also changes TemporaryFileSwap.__exit__'s return type
annotation from bool to Literal[False]. If this were nonpublic or
being newly designed, it may be better for it to instead return
None implicitly, but I think that would technically be a breaking
change (though it would be very unusual, and not a good idea, for
any code to ever rely on the distinction).

With both these changes made, both those mypy warnings go away, and
code using GitPython will not get a warning if it makes similar
direct use of TemporaryFileSwap.

(An alternative might be to dedent the return statement out of the
with-statement in IndexFile.from_tree, but this would make less
clear to readers that it does not suppress exceptions. Furthermore,
the change to TemporaryFileSwap.__exit__ would still have to be
made for mypy to consider it correctly typed.)
---
 git/index/base.py | 20 +++++++++-----------
 git/index/util.py |  4 ++--
 2 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 5ba817ed3..9d1db4e35 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -381,23 +381,21 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             arg_list.append("--aggressive")
         # END merge handling
 
-        # tmp file created in git home directory to be sure renaming
-        # works - /tmp/ dirs could be on another device.
-        with contextlib.ExitStack() as stack:
-            tmp_index = stack.enter_context(_named_temporary_file_for_subprocess(repo.git_dir))
+        # Create the temporary file in the .git directory to be sure renaming
+        # works - /tmp/ directories could be on another device.
+        with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index:
             arg_list.append("--index-output=%s" % tmp_index)
             arg_list.extend(treeish)
 
-            # Move current index out of the way - otherwise the merge may fail
+            # Move the 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 ensures the original file gets 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)
-            index.entries  # Force it to read the file as we will delete the temp-file.
-            return index
+            with 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
             # END index merge handling
 
     # UTILITIES
diff --git a/git/index/util.py b/git/index/util.py
index 61039fe7c..125925783 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -15,7 +15,7 @@
 
 from typing import Any, Callable, TYPE_CHECKING, Optional, Type
 
-from git.types import PathLike, _T
+from git.types import Literal, PathLike, _T
 
 if TYPE_CHECKING:
     from git.index import IndexFile
@@ -55,7 +55,7 @@ def __exit__(
         exc_type: Optional[Type[BaseException]],
         exc_val: Optional[BaseException],
         exc_tb: Optional[TracebackType],
-    ) -> bool:
+    ) -> Literal[False]:
         if osp.isfile(self.tmp_file_path):
             os.replace(self.tmp_file_path, self.file_path)
         return False

From 1c65efbe73513028510fe3fb599777a721f8d80c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 08:48:59 -0500
Subject: [PATCH 364/994] Show "not from cwd" test is broken for shell=True

This adds a test_it_executes_git_not_from_cwd case for shell=True.
(This case also gives the command as a string, so the test need not
be further special-cased for non-Windows systems, where argument
lists aren't accepted with shell=True.)

The test did not attempt to cover the shell=True case before,
because I had erroneously assumed it worked similarity. It is
actually very different, because when a shell is used, both the
shell and the command the shell runs must be found and executed,
and because the process creation GitPython performs is that of the
shell process, with the state of the shell process being what is
relevant to how the path search is done for the git (or other)
command.

The code change here does not itself demonstrate that the test is
broken for shell=True, because that case passes. However, manually
undoing the fix in cmd.py for CVE-2023-40590, which as expected
causes the preexisting (implicitly shell=False case) to fail, does
*not* cause the new shell=True case to fail. That case passes!

That passing result in the absence of a fix for CVE-2023-40590 is
erroneous, because the cmd.exe shell does search the CWD first when
nothing has been done to prevent it.
---
 test/test_git.py | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 6d0ae82d0..beb2767f7 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -134,7 +134,13 @@ def test_it_logs_istream_summary_for_stdin(self, case):
     def test_it_executes_git_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):
+    @ddt.data(
+        (["git", "version"], False),
+        ("git version", True),
+    )
+    def test_it_executes_git_not_from_cwd(self, case):
+        command, shell = case
+
         with TemporaryDirectory() as tmpdir:
             if os.name == "nt":
                 # Copy an actual binary executable that is not git.
@@ -149,7 +155,9 @@ def test_it_executes_git_not_from_cwd(self):
                 os.chmod(impostor_path, 0o755)
 
             with cwd(tmpdir):
-                self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b")
+                output = self.git.execute(command, shell=shell)
+
+        self.assertRegex(output, r"^git version\b")
 
     @skipUnless(
         os.name == "nt",

From 2b47933fba1e9266edb1bb71163768288bbfd409 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 08:54:12 -0500
Subject: [PATCH 365/994] Correct the "not from cwd" test and add more cases

This shows that CVE-2023-40590 is only partially patched. This
commit does not fix the vulnerbility, only the test.

The problem is that, when shell=True, the environment of the shell
subprocess, instead of the GitPython process, determines whether
searching for the "git" command will use the current directory.
Currently NoDefaultCurrentDirectoryInExePath is set only in the
current process, and this is done after the environment for the
subprocess (env) is computed. So when git is an indirect subprocess
due to shell=True, Windows still checks a CWD when looking it up.

(Note that this should not be a problem for indirect subprocesses
started by git itself. When a standard git command is implemented
as an external executable, when git runs a custom command, and when
one git command delegates some of its work to another -- e.g.
"git clone" running git-remote-https -- Git for Windows already
does not search the current directory. Custom git commands that
start their own git subprocesses could have an analogous path
search bug, but this would be separate from the bug in GitPython.)

This is an exploitable vulnerability in GitPython. Although
shell=True is rarer and inherently less secure than the default of
shell=False, it still ought to avoid automatically running an
executable that may exist only due to having been cloned as part of
an untrusted repository. In addition, historically programs on
Windows had sometimes used shell=True as a workaround for console
windows being displayed for subprocesses, and some such code may
still be in use.

Furthermore, even when GitPython's current working directory is
outside the repository being worked on, the Git object in a Repo
instance's "git" attribute holds the repository directory as its
"_working_dir" attribute, which Git.execute consults to determine
the value of "cwd" to pass to subprocess.Popen. When the created
direct subprocess is really a shell, this is the CWD where git.exe
is looked for before searching PATH directories.

This is also why previous, more limited testing (including
accompanying manual testing with shell=True) didn't discover the
bug. Even when modified to test with shell=True, the old test had
the "impostor" git.exe in the GitPython process's own current
directory (which was changed to a temporary directory for the test,
where the "impostor" was created, but this was separate from the
working tree of the self.git repository). This was effective for
the shell=False case, but not the shell=True case where the
impostor would be found and run in the repository directory even
when it differs from the GitPython process's CWD.
---
 test/lib/helper.py |  4 ++--
 test/test_git.py   | 51 +++++++++++++++++++++++++++-------------------
 2 files changed, 32 insertions(+), 23 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index d662b632d..bfc8cdc43 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -88,11 +88,11 @@ def with_rw_directory(func):
     test succeeds, but leave it otherwise to aid additional debugging."""
 
     @wraps(func)
-    def wrapper(self):
+    def wrapper(self, *args, **kwargs):
         path = tempfile.mkdtemp(prefix=func.__name__)
         keep = False
         try:
-            return func(self, path)
+            return func(self, path, *args, **kwargs)
         except Exception:
             log.info(
                 "Test %s.%s failed, output is at %r\n",
diff --git a/test/test_git.py b/test/test_git.py
index beb2767f7..4421ab0ee 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import contextlib
 import gc
 import inspect
 import logging
@@ -12,7 +13,7 @@
 import shutil
 import subprocess
 import sys
-from tempfile import TemporaryDirectory, TemporaryFile
+from tempfile import TemporaryFile
 from unittest import skipUnless
 
 if sys.version_info >= (3, 8):
@@ -135,27 +136,35 @@ def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
 
     @ddt.data(
-        (["git", "version"], False),
-        ("git version", True),
+        (False, False, ["git", "version"]),
+        (False, True, "git version"),
+        (True, False, ["git", "version"]),
+        (True, True, "git version"),
     )
-    def test_it_executes_git_not_from_cwd(self, case):
-        command, shell = case
-
-        with TemporaryDirectory() as tmpdir:
-            if os.name == "nt":
-                # 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 cwd(tmpdir):
-                output = self.git.execute(command, shell=shell)
+    @with_rw_directory
+    def test_it_executes_git_not_from_cwd(self, rw_dir, case):
+        chdir_to_repo, shell, command = case
+
+        repo = Repo.init(rw_dir)
+
+        if os.name == "nt":
+            # Copy an actual binary executable that is not git. (On Windows, running
+            # "hostname" only displays the hostname, it never tries to change it.)
+            other_exe_path = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe")
+            impostor_path = os.path.join(rw_dir, "git.exe")
+            shutil.copy(other_exe_path, impostor_path)
+        else:
+            # Create a shell script that doesn't do anything.
+            impostor_path = os.path.join(rw_dir, "git")
+            with open(impostor_path, mode="w", encoding="utf-8") as file:
+                print("#!/bin/sh", file=file)
+            os.chmod(impostor_path, 0o755)
+
+        with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():
+            # Run the command without raising an exception on failure, as the exception
+            # message is currently misleading when the command is a string rather than a
+            # sequence of strings (it really runs "git", but then wrongly reports "g").
+            output = repo.git.execute(command, with_exceptions=False, shell=shell)
 
         self.assertRegex(output, r"^git version\b")
 

From c1f6c17182d59b9098aeac272dba79d912c5fa42 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 11:22:18 -0500
Subject: [PATCH 366/994] Use SystemRoot instead of WINDIR, to fix tox

In most cases they will be the same, but WINDIR may be absent (or
have a different value?) in rare cases.

The practical reason it matters in GitPython's tests is that tox
automatically passes SystemRoot through to environments on Windows.
---
 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 4421ab0ee..7f42821a8 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -150,7 +150,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
         if os.name == "nt":
             # Copy an actual binary executable that is not git. (On Windows, running
             # "hostname" only displays the hostname, it never tries to change it.)
-            other_exe_path = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe")
+            other_exe_path = os.path.join(os.environ["SystemRoot"], "system32", "hostname.exe")
             impostor_path = os.path.join(rw_dir, "git.exe")
             shutil.copy(other_exe_path, impostor_path)
         else:

From 06bd2c7ee2b9a4169c02ba6f4e3bc34ab674af73 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 11:47:45 -0500
Subject: [PATCH 367/994] Omit CWD in executable search even when shell=True

---
 git/cmd.py | 24 ++++++++++++++++--------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9c94748c5..b3a9959b7 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -985,23 +985,31 @@ def execute(
         if inline_env is not None:
             env.update(inline_env)
 
+        if shell is None:
+            shell = self.USE_SHELL
+
+        cmd_not_found_exception = FileNotFoundError
+        maybe_patch_caller_env = contextlib.nullcontext()
+
         if os.name == "nt":
-            cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
                 raise GitCommandError(
                     redacted_command,
                     '"kill_after_timeout" feature is not supported on Windows.',
                 )
-            # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
-            maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
-        else:
-            cmd_not_found_exception = FileNotFoundError
-            maybe_patch_caller_env = contextlib.nullcontext()
+
+            cmd_not_found_exception = OSError
+
+            # Search PATH, but do not search CWD. The "1" can be any value.
+            if shell:
+                # If the direct subprocess is a shell, this must go in its environment.
+                env["NoDefaultCurrentDirectoryInExePath"] = "1"
+            else:
+                # If we're not using a shell, the variable goes in our own environment.
+                maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
         # END handle
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
-        if shell is None:
-            shell = self.USE_SHELL
         log.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,

From 7da9c3b45a19302b346474ea3fdcda1594c33b08 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 12:01:43 -0500
Subject: [PATCH 368/994] Refactor "not from cwd" test for readability

By using pathlib.Path instead of os.path functions.
---
 test/test_git.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 7f42821a8..f4f716660 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -9,6 +9,7 @@
 import logging
 import os
 import os.path as osp
+from pathlib import Path
 import re
 import shutil
 import subprocess
@@ -150,14 +151,13 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
         if os.name == "nt":
             # Copy an actual binary executable that is not git. (On Windows, running
             # "hostname" only displays the hostname, it never tries to change it.)
-            other_exe_path = os.path.join(os.environ["SystemRoot"], "system32", "hostname.exe")
-            impostor_path = os.path.join(rw_dir, "git.exe")
+            other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
+            impostor_path = Path(rw_dir, "git.exe")
             shutil.copy(other_exe_path, impostor_path)
         else:
             # Create a shell script that doesn't do anything.
-            impostor_path = os.path.join(rw_dir, "git")
-            with open(impostor_path, mode="w", encoding="utf-8") as file:
-                print("#!/bin/sh", file=file)
+            impostor_path = Path(rw_dir, "git")
+            impostor_path.write_text("#!/bin/sh\n", encoding="utf-8")
             os.chmod(impostor_path, 0o755)
 
         with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():

From 865c6e8315e8700737981e8fe43a1ac62eb1423a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 14:13:18 -0500
Subject: [PATCH 369/994] Further expand "not from cwd" test re: cmd.exe

On Python versions for which python/cpython#101283 is not patched,
using Popen with shell=True can find cmd.exe in the current
directory on Windows, in the rare case that the ComSpec environment
variable is not defined.

This is not necessarily worth addressing, because it is a bug in
CPython rahter than GitPython, because that bug has been patched,
and because it is very rare that ComSpec is undefined. However:

- Changing the code to avoid it would also make that code simpler.
- Patched versions of Python <=3.9 don't have python.org builds.

This commit just expands the test to add cases where a repository
also has a file cmd.exe and where ComSpec is undefined, showing
that this case is not covered.
---
 test/test_git.py | 39 +++++++++++++++++++++++++++++++++------
 1 file changed, 33 insertions(+), 6 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index f4f716660..e80ad37ef 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -29,6 +29,21 @@
 from test.lib import TestBase, fixture_path, with_rw_directory
 
 
+@contextlib.contextmanager
+def _patch_out_env(name):
+    try:
+        old_value = os.environ[name]
+    except KeyError:
+        old_value = None
+    else:
+        del os.environ[name]
+    try:
+        yield
+    finally:
+        if old_value is not None:
+            os.environ[name] = old_value
+
+
 @ddt.ddt
 class TestGit(TestBase):
     @classmethod
@@ -137,14 +152,17 @@ def test_it_executes_git_and_returns_result(self):
         self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
 
     @ddt.data(
-        (False, False, ["git", "version"]),
-        (False, True, "git version"),
-        (True, False, ["git", "version"]),
-        (True, True, "git version"),
+        # chdir_to_repo, shell, command, use_shell_impostor
+        (False, False, ["git", "version"], False),
+        (False, True, "git version", False),
+        (False, True, "git version", True),
+        (True, False, ["git", "version"], False),
+        (True, True, "git version", False),
+        (True, True, "git version", True),
     )
     @with_rw_directory
     def test_it_executes_git_not_from_cwd(self, rw_dir, case):
-        chdir_to_repo, shell, command = case
+        chdir_to_repo, shell, command, use_shell_impostor = case
 
         repo = Repo.init(rw_dir)
 
@@ -160,7 +178,16 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
             impostor_path.write_text("#!/bin/sh\n", encoding="utf-8")
             os.chmod(impostor_path, 0o755)
 
-        with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():
+        if use_shell_impostor:
+            shell_name = "cmd.exe" if os.name == "nt" else "sh"
+            shutil.copy(impostor_path, Path(rw_dir, shell_name))
+
+        with contextlib.ExitStack() as stack:
+            if chdir_to_repo:
+                stack.enter_context(cwd(rw_dir))
+            if use_shell_impostor:
+                stack.enter_context(_patch_out_env("ComSpec"))
+
             # Run the command without raising an exception on failure, as the exception
             # message is currently misleading when the command is a string rather than a
             # sequence of strings (it really runs "git", but then wrongly reports "g").

From d2506c73b6ad59bff1f2f58c4edd15f717012591 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 15:56:58 -0500
Subject: [PATCH 370/994] Make Git.execute a bit simpler and very slightly more
 robust

This covers the rare unpatched python/cpython#101283 case the
previous commit added tests for, that only applies in the unusual
situation that the ComSpec environment variable is unset and an old
build (but this includes downloadable builds and current
actions/setup-python builds) of Python <=3.9 for Windows is in use.

The main benefit of this change is really to slightly simplify the
code under test. (It might even be justified to remove the
use_shell_impostor=True test cases at some point.)
---
 git/cmd.py | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index b3a9959b7..3d26dbad2 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -988,25 +988,22 @@ def execute(
         if shell is None:
             shell = self.USE_SHELL
 
-        cmd_not_found_exception = FileNotFoundError
-        maybe_patch_caller_env = contextlib.nullcontext()
-
         if os.name == "nt":
+            cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
                 raise GitCommandError(
                     redacted_command,
                     '"kill_after_timeout" feature is not supported on Windows.',
                 )
-
-            cmd_not_found_exception = OSError
-
-            # Search PATH, but do not search CWD. The "1" can be any value.
+            # Search PATH but not CWD. The "1" can be any value. We'll patch just before
+            # the Popen call and unpatch just after, or we get a worse race condition.
+            maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
             if shell:
-                # If the direct subprocess is a shell, this must go in its environment.
+                # Modify the direct shell subprocess's own search behavior accordingly.
                 env["NoDefaultCurrentDirectoryInExePath"] = "1"
-            else:
-                # If we're not using a shell, the variable goes in our own environment.
-                maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
+        else:
+            cmd_not_found_exception = FileNotFoundError
+            maybe_patch_caller_env = contextlib.nullcontext()
         # END handle
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")

From 61b4ddacc509b67ec809e15d74de1b60a2a28282 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Dec 2023 18:35:01 -0500
Subject: [PATCH 371/994] Start on test_hook_uses_shell_not_from_cwd

This shows that run_commit_hook is vulnerable to an untrusted
search path bug on Windows, when running script hooks: bash.exe is
run without setting NoDefaultCurrentDirectoryInExePath or otherwise
excluding the current directory from the path search.

The new test uses a non-bare repo, even though the surrounding
tests use bare repos. Although the test also works if the repo is
initialized with `Repo.init(rw_dir, bare=True)`, using a bare repo
would obscure how the bug this test reveals would typically be
exploited, where a command that uses a hook is run after a
malicious bash.exe is checked out into the working tree from an
untrusted branch.

Running hooks that are themselves provided by an untrusted
repository or branch is of course never safe. If an attacker can
deceive a user into doing that, then this vulnerability is not
needed. Instead, an attack that leverages this untrusted search
path vulnerability would most likely be of roughly this form:

1. The user clones a trusted repository and installs hooks.
2. A malicious party offers a contribution to the project.
3. The user checks out the malicious party's untrusted branch.
4. The user performs an action that runs a hook.

The hook the user runs is still trusted, but it runs with the
malicious bash.exe found in the current directory (which is the
working tree or perhaps some subdirectory of it).

The test added in this commit should, if possible, be improved to
be able to run and detect the bug (or its absence) even when bash
is absent from the Windows system and, preferably, also even when
the WSL bash.exe is present but no WSL distribution is installed.
---
 test/test_index.py | 37 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/test/test_index.py b/test/test_index.py
index 50d941e83..eb676add8 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -3,16 +3,19 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import contextlib
 from io import BytesIO
 import logging
 import os
 import os.path as osp
 from pathlib import Path
 import re
+import shutil
 from stat import S_ISLNK, ST_MODE
 import subprocess
 import tempfile
 
+import ddt
 import pytest
 from sumtypes import constructor, sumtype
 
@@ -36,7 +39,7 @@
 from git.index.typ import BaseIndexEntry, IndexEntry
 from git.index.util import TemporaryFileSwap
 from git.objects import Blob
-from git.util import Actor, hex_to_bin, rmtree
+from git.util import Actor, cwd, hex_to_bin, rmtree
 from gitdb.base import IStream
 from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
 
@@ -172,6 +175,7 @@ def _make_hook(git_dir, name, content, make_exec=True):
     return hp
 
 
+@ddt.ddt
 class TestIndex(TestBase):
     def __init__(self, *args):
         super().__init__(*args)
@@ -1012,6 +1016,37 @@ def test_run_commit_hook(self, rw_repo):
         output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
         self.assertEqual(output, "ran fake hook\n")
 
+    # FIXME: Figure out a way to make this test also work with Absent and WslNoDistro.
+    @pytest.mark.xfail(
+        type(_win_bash_status) is WinBashStatus.WslNoDistro,
+        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
+        raises=HookExecutionError,
+    )
+    @ddt.data((False,), (True,))
+    @with_rw_directory
+    def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
+        (chdir_to_repo,) = case
+
+        repo = Repo.init(rw_dir)
+        _make_hook(repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt")
+
+        if os.name == "nt":
+            # Copy an actual binary that is not bash.
+            other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
+            impostor_path = Path(rw_dir, "bash.exe")
+            shutil.copy(other_exe_path, impostor_path)
+        else:
+            # Create a shell script that doesn't do anything.
+            impostor_path = Path(rw_dir, "sh")
+            impostor_path.write_text("#!/bin/sh\n", encoding="utf-8")
+            os.chmod(impostor_path, 0o755)
+
+        with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():
+            run_commit_hook("fake-hook", repo.index)
+
+        output = Path(rw_dir, "output.txt").read_text(encoding="utf-8")
+        self.assertEqual(output, "ran fake hook\n")
+
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",

From 66ff4c177accfb4f21d3eb476381d248d99fd8b5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 17 Dec 2023 06:08:57 -0500
Subject: [PATCH 372/994] Omit CWD in search for bash.exe to run hooks on
 Windows

This uses the same NoDefaultCurrentDirectoryInExePath technique for
the Popen call in git.index.fun.run_commit_hook on Windows as is
already used in git.cmd.Git.execute.

The code is simpler in run_commit_hook because a shell is never
used to run bash.exe. (bash.exe is itself a shell, but we never
run it *via* a shell by passing shell=True to Popen.) Nonetheless,
it may make sense to extract out a helper function both can call.
This commit does not do that, so there is some code duplication.
---
 git/index/fun.py | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/git/index/fun.py b/git/index/fun.py
index 402f85d2b..303141bbe 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -3,6 +3,7 @@
 
 """Standalone functions to accompany the index implementation and make it more versatile."""
 
+import contextlib
 from io import BytesIO
 import os
 import os.path as osp
@@ -26,7 +27,7 @@
     traverse_trees_recursive,
     tree_to_stream,
 )
-from git.util import IndexFileSHA1Writer, finalize_process
+from git.util import IndexFileSHA1Writer, finalize_process, patch_env
 from gitdb.base import IStream
 from gitdb.typ import str_tree_type
 
@@ -90,6 +91,10 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     env = os.environ.copy()
     env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
     env["GIT_EDITOR"] = ":"
+    if os.name == "nt":
+        maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
+    else:
+        maybe_patch_caller_env = contextlib.nullcontext()
     cmd = [hp]
     try:
         if os.name == "nt" and not _has_file_extension(hp):
@@ -98,14 +103,15 @@ 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]
 
-        process = subprocess.Popen(
-            cmd + list(args),
-            env=env,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            cwd=index.repo.working_dir,
-            creationflags=PROC_CREATIONFLAGS,
-        )
+        with maybe_patch_caller_env:
+            process = subprocess.Popen(
+                cmd + list(args),
+                env=env,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                cwd=index.repo.working_dir,
+                creationflags=PROC_CREATIONFLAGS,
+            )
     except Exception as ex:
         raise HookExecutionError(hp, ex) from ex
     else:

From 7751436b94d96ce0978b301681b851edd6efed63 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Dec 2023 06:17:00 -0500
Subject: [PATCH 373/994] Extract venv management from test_installation

- Create and use a test.lib.helper.VirtualEnvironment class.
- Import and use venv module instead of running "python -m venv".

These changes make no significant difference in speed or clarity
for the existing test in test_installation. The reason for this
change is instead to support the use of new per-test virtual
environments in at least one other test.
---
 test/lib/helper.py        | 41 ++++++++++++++++++++++++++++++++++++++
 test/test_installation.py | 42 +++++++++++++++++++--------------------
 2 files changed, 61 insertions(+), 22 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index bfc8cdc43..2fb8990dd 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -14,6 +14,7 @@
 import textwrap
 import time
 import unittest
+import venv
 
 import gitdb
 
@@ -36,6 +37,7 @@
     "with_rw_repo",
     "with_rw_and_rw_remote_repo",
     "TestBase",
+    "VirtualEnvironment",
     "TestCase",
     "SkipTest",
     "skipIf",
@@ -390,3 +392,42 @@ def _make_file(self, rela_path, data, repo=None):
         with open(abs_path, "w") as fp:
             fp.write(data)
         return abs_path
+
+
+class VirtualEnvironment:
+    """A newly created Python virtual environment for use in a test."""
+
+    __slots__ = ("_env_dir",)
+
+    def __init__(self, env_dir, *, with_pip):
+        self._env_dir = env_dir
+        venv.create(env_dir, symlinks=(os.name != "nt"), with_pip=with_pip)
+
+    @property
+    def env_dir(self):
+        """The top-level directory of the environment."""
+        return self._env_dir
+
+    @property
+    def python(self):
+        """Path to the Python executable in the environment."""
+        return self._executable("python")
+
+    @property
+    def pip(self):
+        """Path to the pip executable in the environment, or RuntimeError if absent."""
+        return self._executable("pip")
+
+    @property
+    def sources(self):
+        """Path to a src directory in the environment, which may not exist yet."""
+        return os.path.join(self.env_dir, "src")
+
+    def _executable(self, basename):
+        if os.name == "nt":
+            path = osp.join(self.env_dir, "Scripts", basename + ".exe")
+        else:
+            path = osp.join(self.env_dir, "bin", basename)
+        if osp.isfile(path) or osp.islink(path):
+            return path
+        raise RuntimeError(f"no regular file or symlink {path!r}")
diff --git a/test/test_installation.py b/test/test_installation.py
index 0a200415b..15ed5b13b 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -4,31 +4,19 @@
 import ast
 import os
 import subprocess
-import sys
 
-from test.lib import TestBase
-from test.lib.helper import with_rw_directory
+from test.lib import TestBase, VirtualEnvironment, with_rw_directory
 
 
 class TestInstallation(TestBase):
-    def setUp_venv(self, rw_dir):
-        self.venv = rw_dir
-        subprocess.run([sys.executable, "-m", "venv", self.venv], stdout=subprocess.PIPE)
-        bin_name = "Scripts" if os.name == "nt" 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)
-
     @with_rw_directory
     def test_installation(self, rw_dir):
-        self.setUp_venv(rw_dir)
+        venv = self._set_up_venv(rw_dir)
 
         result = subprocess.run(
-            [self.pip, "install", "."],
+            [venv.pip, "install", "."],
             stdout=subprocess.PIPE,
-            cwd=self.sources,
+            cwd=venv.sources,
         )
         self.assertEqual(
             0,
@@ -37,9 +25,9 @@ def test_installation(self, rw_dir):
         )
 
         result = subprocess.run(
-            [self.python, "-c", "import git"],
+            [venv.python, "-c", "import git"],
             stdout=subprocess.PIPE,
-            cwd=self.sources,
+            cwd=venv.sources,
         )
         self.assertEqual(
             0,
@@ -48,9 +36,9 @@ def test_installation(self, rw_dir):
         )
 
         result = subprocess.run(
-            [self.python, "-c", "import gitdb; import smmap"],
+            [venv.python, "-c", "import gitdb; import smmap"],
             stdout=subprocess.PIPE,
-            cwd=self.sources,
+            cwd=venv.sources,
         )
         self.assertEqual(
             0,
@@ -62,9 +50,9 @@ def test_installation(self, rw_dir):
         # 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(
-            [self.python, "-c", "import sys; import git; print(sys.path)"],
+            [venv.python, "-c", "import sys; import git; print(sys.path)"],
             stdout=subprocess.PIPE,
-            cwd=self.sources,
+            cwd=venv.sources,
         )
         syspath = result.stdout.decode("utf-8").splitlines()[0]
         syspath = ast.literal_eval(syspath)
@@ -73,3 +61,13 @@ def test_installation(self, rw_dir):
             syspath[0],
             msg="Failed to follow the conventions for https://docs.python.org/3/library/sys.html#sys.path",
         )
+
+    @staticmethod
+    def _set_up_venv(rw_dir):
+        venv = VirtualEnvironment(rw_dir, with_pip=True)
+        os.symlink(
+            os.path.dirname(os.path.dirname(__file__)),
+            venv.sources,
+            target_is_directory=True,
+        )
+        return venv

From a42ea0a38c489caf9969836141120d760d3754b4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Dec 2023 11:54:28 -0500
Subject: [PATCH 374/994] Cover absent/no-distro bash.exe in hooks "not from
 cwd" test

See comments for details on the test's new implementation and what
it achieves.
---
 .pre-commit-config.yaml |  2 +-
 test/fixtures/polyglot  |  8 ++++++
 test/test_index.py      | 63 ++++++++++++++++++++++++++---------------
 3 files changed, 49 insertions(+), 24 deletions(-)
 create mode 100755 test/fixtures/polyglot

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index be97d5f9b..1ac5baa00 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,7 +29,7 @@ repos:
   hooks:
   - id: shellcheck
     args: [--color]
-    exclude: ^git/ext/
+    exclude: ^test/fixtures/polyglot$|^git/ext/
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
   rev: v4.4.0
diff --git a/test/fixtures/polyglot b/test/fixtures/polyglot
new file mode 100755
index 000000000..f1dd56b26
--- /dev/null
+++ b/test/fixtures/polyglot
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+# Valid script in both Bash and Python, but with different behavior.
+""":"
+echo 'Ran intended hook.' >output.txt
+exit
+" """
+from pathlib import Path
+Path('payload.txt').write_text('Ran impostor hook!', encoding='utf-8')
diff --git a/test/test_index.py b/test/test_index.py
index eb676add8..ff28f48ae 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -41,7 +41,14 @@
 from git.objects import Blob
 from git.util import Actor, cwd, hex_to_bin, rmtree
 from gitdb.base import IStream
-from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
+from test.lib import (
+    TestBase,
+    VirtualEnvironment,
+    fixture,
+    fixture_path,
+    with_rw_directory,
+    with_rw_repo,
+)
 
 HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
 
@@ -1016,36 +1023,46 @@ def test_run_commit_hook(self, rw_repo):
         output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
         self.assertEqual(output, "ran fake hook\n")
 
-    # FIXME: Figure out a way to make this test also work with Absent and WslNoDistro.
-    @pytest.mark.xfail(
-        type(_win_bash_status) is WinBashStatus.WslNoDistro,
-        reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
-        raises=HookExecutionError,
-    )
     @ddt.data((False,), (True,))
     @with_rw_directory
     def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         (chdir_to_repo,) = case
 
+        shell_name = "bash.exe" if os.name == "nt" else "sh"
+        maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext()
         repo = Repo.init(rw_dir)
-        _make_hook(repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt")
 
-        if os.name == "nt":
-            # Copy an actual binary that is not bash.
-            other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
-            impostor_path = Path(rw_dir, "bash.exe")
-            shutil.copy(other_exe_path, impostor_path)
+        # We need an impostor shell that works on Windows and that can be distinguished
+        # from the real bash.exe. But even if the real bash.exe is absent or unusable,
+        # we should verify that the impostor is not run. So the impostor needs a clear
+        # side effect (unlike in TestGit.test_it_executes_git_not_from_cwd). Popen on
+        # Windows uses CreateProcessW, which disregards PATHEXT; the impostor may need
+        # to be a binary executable to ensure the vulnerability is found if present. No
+        # compiler need exist, shipping a binary in the test suite may target the wrong
+        # architecture, and generating one in a bespoke way may cause virus scanners to
+        # give a false positive. So we use a Bash/Python polyglot for the hook and use
+        # the Python interpreter itself as the bash.exe impostor. But an interpreter
+        # from a venv may not run outside of it, and a global interpreter won't run from
+        # a different location if it was installed from the Microsoft Store. So we make
+        # a new venv in rw_dir and use its interpreter.
+        venv = VirtualEnvironment(rw_dir, with_pip=False)
+        shutil.copy(venv.python, Path(rw_dir, shell_name))
+        shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir))
+        payload = Path(rw_dir, "payload.txt")
+
+        if type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.WslNoDistro}:
+            # The real shell can't run, but the impostor should still not be used.
+            with self.assertRaises(HookExecutionError):
+                with maybe_chdir:
+                    run_commit_hook("polyglot", repo.index)
+            self.assertFalse(payload.exists())
         else:
-            # Create a shell script that doesn't do anything.
-            impostor_path = Path(rw_dir, "sh")
-            impostor_path.write_text("#!/bin/sh\n", encoding="utf-8")
-            os.chmod(impostor_path, 0o755)
-
-        with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():
-            run_commit_hook("fake-hook", repo.index)
-
-        output = Path(rw_dir, "output.txt").read_text(encoding="utf-8")
-        self.assertEqual(output, "ran fake hook\n")
+            # The real shell should run, and not the impostor.
+            with maybe_chdir:
+                run_commit_hook("polyglot", repo.index)
+            self.assertFalse(payload.exists())
+            output = Path(rw_dir, "output.txt").read_text(encoding="utf-8")
+            self.assertEqual(output, "Ran intended hook.\n")
 
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,

From f44524a9a9c8122b9b98d6e5797e1dfc3211c0b7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Dec 2023 08:11:04 +0800
Subject: [PATCH 375/994] Avoid spurious "location may have moved" on Windows

The main case where this happens is when tempfile.gettempdir() has
a component in it that uses an 8.3-encoded path, e.g.,
C:\Users\Administrator\... -> C:\Users\ADMINI~1\...

This is a workaround for python/cpython#90329. I call realpath only
once, when the venv is created, and not on any paths inside the
venv, to make it less likely this masks the problems the warning is
meant for. (For example, if Scripts, or python.exe in it, produced
this even with the venv created as it is now, then that may indicte
an actual problem.)

Note that copying python.exe from Scripts to one level up in the
venv, and changing its name to bash.exe to use it to simulate the
bash.exe impostor, as is done in test_hook_uses_shell_not_from_cwd,
should not (and does not) produce this warning. If that ever starts
to do so, then that should be examined as a sign of brittleness.
---
 test/lib/helper.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 2fb8990dd..1fdc56fd1 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -400,8 +400,12 @@ class VirtualEnvironment:
     __slots__ = ("_env_dir",)
 
     def __init__(self, env_dir, *, with_pip):
-        self._env_dir = env_dir
-        venv.create(env_dir, symlinks=(os.name != "nt"), with_pip=with_pip)
+        if os.name == "nt":
+            self._env_dir = osp.realpath(env_dir)
+            venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
+        else:
+            self._env_dir = env_dir
+            venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
 
     @property
     def env_dir(self):

From 15ebb258d4eebd9bf0f38780570d57e0b968b8de Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Dec 2023 19:28:26 -0500
Subject: [PATCH 376/994] Clarify comment in test_hook_uses_shell_not_from_cwd

---
 test/test_index.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index ff28f48ae..d352faa6c 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1042,9 +1042,9 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         # architecture, and generating one in a bespoke way may cause virus scanners to
         # give a false positive. So we use a Bash/Python polyglot for the hook and use
         # the Python interpreter itself as the bash.exe impostor. But an interpreter
-        # from a venv may not run outside of it, and a global interpreter won't run from
-        # a different location if it was installed from the Microsoft Store. So we make
-        # a new venv in rw_dir and use its interpreter.
+        # from a venv may not run when copied outside of it, and a global interpreter
+        # won't run when copied to a different location if it was installed from the
+        # Microsoft Store. So we make a new venv in rw_dir and use its interpreter.
         venv = VirtualEnvironment(rw_dir, with_pip=False)
         shutil.copy(venv.python, Path(rw_dir, shell_name))
         shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir))

From c551e916c7b9e2d623b9d76f3352849a707d9bbe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Dec 2023 00:40:43 -0500
Subject: [PATCH 377/994] Extract shared logic for using Popen safely on
 Windows

This creates git.util.safer_popen that, on non-Windows systems, is
bound to subprocess.Popen (to avoid introducing unnecessary
latency). On Windows, it is a function that wraps subprocess.Popen,
consolidating two pieces of logic that had previously been
duplicated:

1. Temporarily setting NoDefaultCurrentDirectoryInExePath in the
   calling environment and, when shell=True is used, setting it in
   the subprocess environment as well. This prevents executables
   specified as single names (which are mainly "git" and, for
   hooks, "bash.exe") from being searched for in the current
   working directory of GitPython or, when a shell is used, the
   current working directory of the shell used to run them.

2. Passing the CREATE_NO_WINDOW and CREATE_NEW_PROCESS_GROUP flags
   as creationflags. This is not a security measure. It is
   indirectly related to safety in that CREATE_NO_WINDOW eliminated
   at least some, and possibly all, cases where calling Git.execute
   (directly, or indirectly via a dynamic method) with shell=True
   conferred an advantage over the inherently more secure default
   of shell=False; and CREATE_NEW_PROCESS facilitates some ways of
   terminating subprocesses that would otherwise be unavailable,
   thereby making resource exhaustion less likely. But really the
   reason I included creationflags here is that it seems it should
   always be used in the same situations as preventing the current
   directory from being searched (and always was), and including it
   further reduces code duplication and simplifies calling code.

This commit does not improve security or robustness, because these
features were already present. Instead, this moves them to a single
location. It also documents them by giving the function bound to
safer_popen on Windows, _safer_popen_windows, a detailed docstring.

Because there would otherwise be potential for confusion on the
different ways to perform or customize path searches, I have also
added a doctring to py_where noting its limited use case and its
relationship to shutil.which and non-shell search.

(The search in _safer_popen_windows is typically a non-shell
search, which is why it cannot be reimplemented to do its own
lookup by calling an only slightly modified version of
shutil.which, without a risk of breaking some currently working
uses. It may, however, be possible to fix the race condition by
doing something analogous for Windows non-shell search behavior,
which is largely but not entirely described in the documentation
for CreateProcessW.)
---
 git/cmd.py       | 48 ++++++++++---------------------
 git/index/fun.py | 25 ++++++-----------
 git/util.py      | 73 ++++++++++++++++++++++++++++++++++++++++++++++++
 test/test_git.py | 19 ++++++-------
 4 files changed, 106 insertions(+), 59 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 3d26dbad2..c4dea0393 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -29,8 +29,8 @@
     cygpath,
     expand_path,
     is_cygwin_git,
-    patch_env,
     remove_password_if_present,
+    safer_popen,
     stream_copy,
 )
 
@@ -225,14 +225,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 ## -- End Utilities -- @}
 
 
-if os.name == "nt":
-    # CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards. See:
-    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
-    PROC_CREATIONFLAGS = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
-else:
-    PROC_CREATIONFLAGS = 0
-
-
 class Git(LazyMixin):
     """The Git class manages communication with the Git binary.
 
@@ -985,9 +977,6 @@ def execute(
         if inline_env is not None:
             env.update(inline_env)
 
-        if shell is None:
-            shell = self.USE_SHELL
-
         if os.name == "nt":
             cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
@@ -995,18 +984,13 @@ def execute(
                     redacted_command,
                     '"kill_after_timeout" feature is not supported on Windows.',
                 )
-            # Search PATH but not CWD. The "1" can be any value. We'll patch just before
-            # the Popen call and unpatch just after, or we get a worse race condition.
-            maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
-            if shell:
-                # Modify the direct shell subprocess's own search behavior accordingly.
-                env["NoDefaultCurrentDirectoryInExePath"] = "1"
         else:
             cmd_not_found_exception = FileNotFoundError
-            maybe_patch_caller_env = contextlib.nullcontext()
         # END handle
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
+        if shell is None:
+            shell = self.USE_SHELL
         log.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,
@@ -1016,20 +1000,18 @@ def execute(
             universal_newlines,
         )
         try:
-            with maybe_patch_caller_env:
-                proc = Popen(
-                    command,
-                    env=env,
-                    cwd=cwd,
-                    bufsize=-1,
-                    stdin=(istream or DEVNULL),
-                    stderr=PIPE,
-                    stdout=stdout_sink,
-                    shell=shell,
-                    universal_newlines=universal_newlines,
-                    creationflags=PROC_CREATIONFLAGS,
-                    **subprocess_kwargs,
-                )
+            proc = safer_popen(
+                command,
+                env=env,
+                cwd=cwd,
+                bufsize=-1,
+                stdin=(istream or DEVNULL),
+                stderr=PIPE,
+                stdout=stdout_sink,
+                shell=shell,
+                universal_newlines=universal_newlines,
+                **subprocess_kwargs,
+            )
         except cmd_not_found_exception as err:
             raise GitCommandNotFound(redacted_command, err) from err
         else:
diff --git a/git/index/fun.py b/git/index/fun.py
index 303141bbe..500fb251d 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -3,7 +3,6 @@
 
 """Standalone functions to accompany the index implementation and make it more versatile."""
 
-import contextlib
 from io import BytesIO
 import os
 import os.path as osp
@@ -19,7 +18,7 @@
 )
 import subprocess
 
-from git.cmd import PROC_CREATIONFLAGS, handle_process_output
+from git.cmd import handle_process_output
 from git.compat import defenc, force_bytes, force_text, safe_decode
 from git.exc import HookExecutionError, UnmergedEntriesError
 from git.objects.fun import (
@@ -27,7 +26,7 @@
     traverse_trees_recursive,
     tree_to_stream,
 )
-from git.util import IndexFileSHA1Writer, finalize_process, patch_env
+from git.util import IndexFileSHA1Writer, finalize_process, safer_popen
 from gitdb.base import IStream
 from gitdb.typ import str_tree_type
 
@@ -91,10 +90,6 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     env = os.environ.copy()
     env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
     env["GIT_EDITOR"] = ":"
-    if os.name == "nt":
-        maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
-    else:
-        maybe_patch_caller_env = contextlib.nullcontext()
     cmd = [hp]
     try:
         if os.name == "nt" and not _has_file_extension(hp):
@@ -103,15 +98,13 @@ 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]
 
-        with maybe_patch_caller_env:
-            process = subprocess.Popen(
-                cmd + list(args),
-                env=env,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                cwd=index.repo.working_dir,
-                creationflags=PROC_CREATIONFLAGS,
-            )
+        process = safer_popen(
+            cmd + list(args),
+            env=env,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            cwd=index.repo.working_dir,
+        )
     except Exception as ex:
         raise HookExecutionError(hp, ex) from ex
     else:
diff --git a/git/util.py b/git/util.py
index c5afea241..5f5fb8566 100644
--- a/git/util.py
+++ b/git/util.py
@@ -33,6 +33,7 @@
     IO,
     Iterator,
     List,
+    Mapping,
     Optional,
     Pattern,
     Sequence,
@@ -327,6 +328,17 @@ def _get_exe_extensions() -> Sequence[str]:
 
 
 def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
+    """Perform a path search to assist :func:`is_cygwin_git`.
+
+    This is not robust for general use. It is an implementation detail of
+    :func:`is_cygwin_git`. When a search following all shell rules is needed,
+    :func:`shutil.which` can be used instead.
+
+    :note: Neither this function nor :func:`shutil.which` will predict the effect of an
+        executable search on a native Windows system due to a :class:`subprocess.Popen`
+        call without ``shell=True``, because shell and non-shell executable search on
+        Windows differ considerably.
+    """
     # From: http://stackoverflow.com/a/377028/548792
     winprog_exts = _get_exe_extensions()
 
@@ -524,6 +536,67 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
     return new_cmdline
 
 
+def _safer_popen_windows(
+    command: Union[str, Sequence[Any]],
+    *,
+    shell: bool = False,
+    env: Optional[Mapping[str, str]] = None,
+    **kwargs: Any,
+) -> subprocess.Popen:
+    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
+
+    This avoids an untrusted search path condition where a file like ``git.exe`` in a
+    malicious repository would be run when GitPython operates on the repository. The
+    process using GitPython may have an untrusted repository's working tree as its
+    current working directory. Some operations may temporarily change to that directory
+    before running a subprocess. In addition, while by default GitPython does not run
+    external commands with a shell, it can be made to do so, in which case the CWD of
+    the subprocess, which GitPython usually sets to a repository working tree, can
+    itself be searched automatically by the shell. This wrapper covers all those cases.
+
+    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
+        environment variable during subprocess creation. It also takes care of passing
+        Windows-specific process creation flags, but that is unrelated to path search.
+
+    :note: The current implementation contains a race condition on :attr:`os.environ`.
+        GitPython isn't thread-safe, but a program using it on one thread should ideally
+        be able to mutate :attr:`os.environ` on another, without unpredictable results.
+        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
+    """
+    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
+    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
+    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+
+    # When using a shell, the shell is the direct subprocess, so the variable must be
+    # set in its environment, to affect its search behavior. (The "1" can be any value.)
+    if shell:
+        safer_env = {} if env is None else dict(env)
+        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
+    else:
+        safer_env = env
+
+    # When not using a shell, the current process does the search in a CreateProcessW
+    # API call, so the variable must be set in our environment. With a shell, this is
+    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
+    # patched. If not, in the rare case the ComSpec environment variable is unset, the
+    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
+    # cases, as here, is simpler and protects against that. (The "1" can be any value.)
+    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
+        return subprocess.Popen(
+            command,
+            shell=shell,
+            env=safer_env,
+            creationflags=creationflags,
+            **kwargs,
+        )
+
+
+if os.name == "nt":
+    safer_popen = _safer_popen_windows
+else:
+    safer_popen = subprocess.Popen
+
 # } END utilities
 
 # { Classes
diff --git a/test/test_git.py b/test/test_git.py
index e80ad37ef..39e19bb8a 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -25,7 +25,7 @@
 import ddt
 
 from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
-from git.util import cwd, finalize_process
+from git.util import cwd, finalize_process, safer_popen
 from test.lib import TestBase, fixture_path, with_rw_directory
 
 
@@ -115,28 +115,28 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
     def _do_shell_combo(self, value_in_call, value_from_class):
         with mock.patch.object(Git, "USE_SHELL", value_from_class):
             # git.cmd gets Popen via a "from" import, so patch it there.
-            with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen:
+            with mock.patch.object(cmd, "safer_popen", wraps=cmd.safer_popen) as mock_safer_popen:
                 # Use a command with no arguments (besides the program name), so it runs
                 # with or without a shell, on all OSes, with the same effect.
                 self.git.execute(["git"], with_exceptions=False, shell=value_in_call)
 
-        return mock_popen
+        return mock_safer_popen
 
     @ddt.idata(_shell_cases)
     def test_it_uses_shell_or_not_as_specified(self, case):
         """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`."""
         value_in_call, value_from_class, expected_popen_arg = case
-        mock_popen = self._do_shell_combo(value_in_call, value_from_class)
-        mock_popen.assert_called_once()
-        self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg)
+        mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
+        mock_safer_popen.assert_called_once()
+        self.assertIs(mock_safer_popen.call_args.kwargs["shell"], expected_popen_arg)
 
     @ddt.idata(full_case[:2] for full_case in _shell_cases)
     def test_it_logs_if_it_uses_a_shell(self, case):
         """``shell=`` in the log message agrees with what is passed to `Popen`."""
         value_in_call, value_from_class = case
         with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
-            mock_popen = self._do_shell_combo(value_in_call, value_from_class)
-        self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"])
+            mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
+        self._assert_logged_for_popen(log_watcher, "shell", mock_safer_popen.call_args.kwargs["shell"])
 
     @ddt.data(
         ("None", None),
@@ -405,13 +405,12 @@ def counter_stderr(line):
             fixture_path("cat_file.py"),
             str(fixture_path("issue-301_stderr")),
         ]
-        proc = subprocess.Popen(
+        proc = safer_popen(
             cmdline,
             stdin=None,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             shell=False,
-            creationflags=cmd.PROC_CREATIONFLAGS,
         )
 
         handle_process_output(proc, counter_stdout, counter_stderr, finalize_process)

From 3eb7c2ab82e6dbe101ff916fca29d539cc2793af Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 21 Dec 2023 03:48:53 -0500
Subject: [PATCH 378/994] Move safer_popen from git.util to git.cmd

I had originally written it in git.util because it is used from two
separate modules (git.cmd and git.index.fun) and is arguably the
same sort of thing as remove_password_if_present, in that both
relate to running processes (and to security) and both are used
from multiple modules yet are not meant for use outside GitPython.

However, all of this is also true of git.cmd.handle_process_output,
which this really has more in common with: it's a utility related
*only* to the use of subprocesses, while remove_password_if_present
can be used for other sanitization. In addition, it also replaces
git.cmd.PROC_CREATIONFLAGS (also imported in git.index.fun) by
passing that to Popen on Windows (since the situations where a
custom value of creationflags should be used are the same as those
where safer_popen should be called for its primary benefit of
avoiding an untrusted path search when creating the subprocess).

safer_popen and its Windows implementation _safer_popen_windows are
thus moved from git/util.py to git/cmd.py, with related changes,
such as to imports, done everywhere they are needed.
---
 git/cmd.py       | 67 ++++++++++++++++++++++++++++++++++++++++++++++--
 git/index/fun.py |  4 +--
 git/util.py      | 62 --------------------------------------------
 test/test_git.py |  5 ++--
 4 files changed, 69 insertions(+), 69 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index c4dea0393..4413182e0 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -29,8 +29,8 @@
     cygpath,
     expand_path,
     is_cygwin_git,
+    patch_env,
     remove_password_if_present,
-    safer_popen,
     stream_copy,
 )
 
@@ -46,6 +46,7 @@
     Iterator,
     List,
     Mapping,
+    Optional,
     Sequence,
     TYPE_CHECKING,
     TextIO,
@@ -102,7 +103,7 @@ def handle_process_output(
         Callable[[bytes, "Repo", "DiffIndex"], None],
     ],
     stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
-    finalizer: Union[None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None]] = None,
+    finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
     decode_streams: bool = True,
     kill_after_timeout: Union[None, float] = None,
 ) -> None:
@@ -207,6 +208,68 @@ def pump_stream(
         finalizer(process)
 
 
+def _safer_popen_windows(
+    command: Union[str, Sequence[Any]],
+    *,
+    shell: bool = False,
+    env: Optional[Mapping[str, str]] = None,
+    **kwargs: Any,
+) -> Popen:
+    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
+
+    This avoids an untrusted search path condition where a file like ``git.exe`` in a
+    malicious repository would be run when GitPython operates on the repository. The
+    process using GitPython may have an untrusted repository's working tree as its
+    current working directory. Some operations may temporarily change to that directory
+    before running a subprocess. In addition, while by default GitPython does not run
+    external commands with a shell, it can be made to do so, in which case the CWD of
+    the subprocess, which GitPython usually sets to a repository working tree, can
+    itself be searched automatically by the shell. This wrapper covers all those cases.
+
+    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
+        environment variable during subprocess creation. It also takes care of passing
+        Windows-specific process creation flags, but that is unrelated to path search.
+
+    :note: The current implementation contains a race condition on :attr:`os.environ`.
+        GitPython isn't thread-safe, but a program using it on one thread should ideally
+        be able to mutate :attr:`os.environ` on another, without unpredictable results.
+        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
+    """
+    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
+    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
+    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+
+    # When using a shell, the shell is the direct subprocess, so the variable must be
+    # set in its environment, to affect its search behavior. (The "1" can be any value.)
+    if shell:
+        safer_env = {} if env is None else dict(env)
+        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
+    else:
+        safer_env = env
+
+    # When not using a shell, the current process does the search in a CreateProcessW
+    # API call, so the variable must be set in our environment. With a shell, this is
+    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
+    # patched. If not, in the rare case the ComSpec environment variable is unset, the
+    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
+    # cases, as here, is simpler and protects against that. (The "1" can be any value.)
+    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
+        return Popen(
+            command,
+            shell=shell,
+            env=safer_env,
+            creationflags=creationflags,
+            **kwargs,
+        )
+
+
+if os.name == "nt":
+    safer_popen = _safer_popen_windows
+else:
+    safer_popen = Popen
+
+
 def dashify(string: str) -> str:
     return string.replace("_", "-")
 
diff --git a/git/index/fun.py b/git/index/fun.py
index 500fb251d..580493f6d 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -18,7 +18,7 @@
 )
 import subprocess
 
-from git.cmd import handle_process_output
+from git.cmd import handle_process_output, safer_popen
 from git.compat import defenc, force_bytes, force_text, safe_decode
 from git.exc import HookExecutionError, UnmergedEntriesError
 from git.objects.fun import (
@@ -26,7 +26,7 @@
     traverse_trees_recursive,
     tree_to_stream,
 )
-from git.util import IndexFileSHA1Writer, finalize_process, safer_popen
+from git.util import IndexFileSHA1Writer, finalize_process
 from gitdb.base import IStream
 from gitdb.typ import str_tree_type
 
diff --git a/git/util.py b/git/util.py
index 5f5fb8566..5ccd2b04c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -33,7 +33,6 @@
     IO,
     Iterator,
     List,
-    Mapping,
     Optional,
     Pattern,
     Sequence,
@@ -536,67 +535,6 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
     return new_cmdline
 
 
-def _safer_popen_windows(
-    command: Union[str, Sequence[Any]],
-    *,
-    shell: bool = False,
-    env: Optional[Mapping[str, str]] = None,
-    **kwargs: Any,
-) -> subprocess.Popen:
-    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
-
-    This avoids an untrusted search path condition where a file like ``git.exe`` in a
-    malicious repository would be run when GitPython operates on the repository. The
-    process using GitPython may have an untrusted repository's working tree as its
-    current working directory. Some operations may temporarily change to that directory
-    before running a subprocess. In addition, while by default GitPython does not run
-    external commands with a shell, it can be made to do so, in which case the CWD of
-    the subprocess, which GitPython usually sets to a repository working tree, can
-    itself be searched automatically by the shell. This wrapper covers all those cases.
-
-    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
-        environment variable during subprocess creation. It also takes care of passing
-        Windows-specific process creation flags, but that is unrelated to path search.
-
-    :note: The current implementation contains a race condition on :attr:`os.environ`.
-        GitPython isn't thread-safe, but a program using it on one thread should ideally
-        be able to mutate :attr:`os.environ` on another, without unpredictable results.
-        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
-    """
-    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
-    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
-    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
-    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
-
-    # When using a shell, the shell is the direct subprocess, so the variable must be
-    # set in its environment, to affect its search behavior. (The "1" can be any value.)
-    if shell:
-        safer_env = {} if env is None else dict(env)
-        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
-    else:
-        safer_env = env
-
-    # When not using a shell, the current process does the search in a CreateProcessW
-    # API call, so the variable must be set in our environment. With a shell, this is
-    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
-    # patched. If not, in the rare case the ComSpec environment variable is unset, the
-    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
-    # cases, as here, is simpler and protects against that. (The "1" can be any value.)
-    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
-        return subprocess.Popen(
-            command,
-            shell=shell,
-            env=safer_env,
-            creationflags=creationflags,
-            **kwargs,
-        )
-
-
-if os.name == "nt":
-    safer_popen = _safer_popen_windows
-else:
-    safer_popen = subprocess.Popen
-
 # } END utilities
 
 # { Classes
diff --git a/test/test_git.py b/test/test_git.py
index 39e19bb8a..3b9abc712 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -25,7 +25,7 @@
 import ddt
 
 from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
-from git.util import cwd, finalize_process, safer_popen
+from git.util import cwd, finalize_process
 from test.lib import TestBase, fixture_path, with_rw_directory
 
 
@@ -114,7 +114,6 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
 
     def _do_shell_combo(self, value_in_call, value_from_class):
         with mock.patch.object(Git, "USE_SHELL", value_from_class):
-            # git.cmd gets Popen via a "from" import, so patch it there.
             with mock.patch.object(cmd, "safer_popen", wraps=cmd.safer_popen) as mock_safer_popen:
                 # Use a command with no arguments (besides the program name), so it runs
                 # with or without a shell, on all OSes, with the same effect.
@@ -389,7 +388,7 @@ def test_environment(self, rw_dir):
                 self.assertIn("FOO", str(err))
 
     def test_handle_process_output(self):
-        from git.cmd import handle_process_output
+        from git.cmd import handle_process_output, safer_popen
 
         line_count = 5002
         count = [None, 0, 0]

From 1f3caa31f1b63908235e341418a0804ed37a320a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Dec 2023 11:33:40 -0500
Subject: [PATCH 379/994] Further clarify comment in
 test_hook_uses_shell_not_from_cwd

---
 test/test_index.py | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index d352faa6c..8a64e2293 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1032,16 +1032,16 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext()
         repo = Repo.init(rw_dir)
 
-        # We need an impostor shell that works on Windows and that can be distinguished
-        # from the real bash.exe. But even if the real bash.exe is absent or unusable,
-        # we should verify that the impostor is not run. So the impostor needs a clear
-        # side effect (unlike in TestGit.test_it_executes_git_not_from_cwd). Popen on
-        # Windows uses CreateProcessW, which disregards PATHEXT; the impostor may need
-        # to be a binary executable to ensure the vulnerability is found if present. No
-        # compiler need exist, shipping a binary in the test suite may target the wrong
-        # architecture, and generating one in a bespoke way may cause virus scanners to
-        # give a false positive. So we use a Bash/Python polyglot for the hook and use
-        # the Python interpreter itself as the bash.exe impostor. But an interpreter
+        # We need an impostor shell that works on Windows and that the test can
+        # distinguish from the real bash.exe. But even if the real bash.exe is absent or
+        # unusable, we should verify the impostor is not run. So the impostor needs a
+        # clear side effect (unlike in TestGit.test_it_executes_git_not_from_cwd). Popen
+        # on Windows uses CreateProcessW, which disregards PATHEXT; the impostor may
+        # need to be a binary executable to ensure the vulnerability is found if
+        # present. No compiler need exist, shipping a binary in the test suite may
+        # target the wrong architecture, and generating one in a bespoke way may trigger
+        # false positive virus scans. So we use a Bash/Python polyglot for the hook and
+        # use the Python interpreter itself as the bash.exe impostor. But an interpreter
         # from a venv may not run when copied outside of it, and a global interpreter
         # won't run when copied to a different location if it was installed from the
         # Microsoft Store. So we make a new venv in rw_dir and use its interpreter.

From f28873828496a6632b3a99101e3582ad164e9bb9 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Wed, 10 Jan 2024 13:13:37 +0100
Subject: [PATCH 380/994] bump patch level

---
 VERSION                |  2 +-
 doc/source/changes.rst | 12 ++++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/VERSION b/VERSION
index efb1eb44f..6c105e676 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.40
+3.1.41
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 71f0bd74c..79628bee2 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,18 @@
 Changelog
 =========
 
+3.1.41
+======
+
+This release is relevant for security as it fixes a possible arbitary
+code execution on Windows.
+
+See this PR for details: https://github.com/gitpython-developers/GitPython/pull/1792
+An advisory is available soon at: https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-2mqj-m65w-jghx
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.40
+
 3.1.40
 ======
 

From 9b1b820e1bfd8e78041ad93ced4fe8bfa8c20b41 Mon Sep 17 00:00:00 2001
From: Peter Law <PeterJCLaw@gmail.com>
Date: Wed, 10 Jan 2024 17:31:49 +0000
Subject: [PATCH 381/994] Fix typo in release link in changelog

I'm assumig this was a typo.
---
 doc/source/changes.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 79628bee2..d7bf1731d 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -12,7 +12,7 @@ See this PR for details: https://github.com/gitpython-developers/GitPython/pull/
 An advisory is available soon at: https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-2mqj-m65w-jghx
 
 See the following for all changes.
-https://github.com/gitpython-developers/GitPython/releases/tag/3.1.40
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.41
 
 3.1.40
 ======

From 634151a2a1d0aad31e022157e741cc14950a49dc Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Wed, 10 Jan 2024 20:55:30 +0100
Subject: [PATCH 382/994] maybe fix readthedocs by adding configuration (#1794)

It seems they now require a configuration file.
Also the theme configuration changed.
It's nice they were stable for more than 10 years though.
---
 .readthedocs.yaml  | 35 +++++++++++++++++++++++++++++++++++
 doc/source/conf.py |  2 +-
 2 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 .readthedocs.yaml

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..3a73b5a3c
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,35 @@
+# Read the Docs configuration file for Sphinx projects
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.12"
+    # You can also specify other tool versions:
+    # nodejs: "20"
+    # rust: "1.70"
+    # golang: "1.20"
+
+# Build documentation in the "docs/" directory with Sphinx
+sphinx:
+  configuration: doc/source/conf.py
+  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
+  # builder: "dirhtml"
+  # Fail on all warnings to avoid broken references
+  # fail_on_warning: true
+
+# Optionally build your docs in additional formats such as PDF and ePub
+# formats:
+#   - pdf
+#   - epub
+
+# Optional but recommended, declare the Python requirements required
+# to build your documentation
+# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+# python:
+#   install:
+#     - requirements: docs/requirements.txt
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 9c22ca06a..ea7a28291 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -93,7 +93,7 @@
 # Options for HTML output
 # -----------------------
 
-html_theme = "sphinx_rtd_theme"
+# html_theme = "sphinx_rtd_theme"
 html_theme_options = {}
 
 # The name for this set of Sphinx documents.  If None, it defaults to

From a5414bf8736edb68e0a97233720f5de10d408e2d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 11 Jan 2024 13:33:36 -0500
Subject: [PATCH 383/994] Remove test dependency on sumtypes library

---
 test-requirements.txt |  1 -
 test/test_index.py    | 52 +++++++++++++++++++++++++++----------------
 2 files changed, 33 insertions(+), 20 deletions(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index fcdc93c1d..7cfb977a1 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -9,4 +9,3 @@ pytest-cov
 pytest-instafail
 pytest-mock
 pytest-sugar
-sumtypes
diff --git a/test/test_index.py b/test/test_index.py
index 8a64e2293..876d1220d 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -4,6 +4,7 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import contextlib
+from dataclasses import dataclass
 from io import BytesIO
 import logging
 import os
@@ -12,12 +13,11 @@
 import re
 import shutil
 from stat import S_ISLNK, ST_MODE
-import subprocess
+from subprocess import CompletedProcess, run
 import tempfile
 
 import ddt
 import pytest
-from sumtypes import constructor, sumtype
 
 from git import (
     BlobFilter,
@@ -66,34 +66,48 @@ def _get_windows_ansi_encoding():
     return f"cp{value}"
 
 
-@sumtype
 class WinBashStatus:
-    """Status of bash.exe for native Windows. Affects which commit hook tests can pass.
+    """Namespace of native-Windows bash.exe statuses. Affects what hook tests can pass.
 
     Call check() to check the status. (CheckError and WinError should not typically be
     used to trigger skip or xfail, because they represent unexpected situations.)
     """
 
-    Inapplicable = constructor()
-    """This system is not native Windows: either not Windows at all, or Cygwin."""
+    @dataclass
+    class Inapplicable:
+        """This system is not native Windows: either not Windows at all, or Cygwin."""
 
-    Absent = constructor()
-    """No command for bash.exe is found on the system."""
+    @dataclass
+    class Absent:
+        """No command for bash.exe is found on the system."""
 
-    Native = constructor()
-    """Running bash.exe operates outside any WSL distribution (as with Git Bash)."""
+    @dataclass
+    class Native:
+        """Running bash.exe operates outside any WSL distribution (as with Git Bash)."""
 
-    Wsl = constructor()
-    """Running bash.exe calls bash in a WSL distribution."""
+    @dataclass
+    class Wsl:
+        """Running bash.exe calls bash in a WSL distribution."""
 
-    WslNoDistro = constructor("process", "message")
-    """Running bash.exe tries to run bash on a WSL distribution, but none exists."""
+    @dataclass
+    class WslNoDistro:
+        """Running bash.exe tries to run bash on a WSL distribution, but none exists."""
 
-    CheckError = constructor("process", "message")
-    """Running bash.exe fails in an unexpected error or gives unexpected output."""
+        process: CompletedProcess[bytes]
+        message: str
 
-    WinError = constructor("exception")
-    """bash.exe may exist but can't run. CreateProcessW fails unexpectedly."""
+    @dataclass
+    class CheckError:
+        """Running bash.exe fails in an unexpected error or gives unexpected output."""
+
+        process: CompletedProcess[bytes]
+        message: str
+
+    @dataclass
+    class WinError:
+        """bash.exe may exist but can't run. CreateProcessW fails unexpectedly."""
+
+        exception: OSError
 
     @classmethod
     def check(cls):
@@ -119,7 +133,7 @@ def check(cls):
             # information on ways to check for WSL, see https://superuser.com/a/1749811.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
-            process = subprocess.run(command, capture_output=True)
+            process = run(command, capture_output=True)
         except FileNotFoundError:
             return cls.Absent()
         except OSError as error:

From 70ea7ec475a82b2483cf151abb9e542f5085af4c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 11 Jan 2024 13:35:05 -0500
Subject: [PATCH 384/994] Fix annotations for Python 3.8 and lower

---
 test/test_index.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/test_index.py b/test/test_index.py
index 876d1220d..22eac355b 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -13,7 +13,7 @@
 import re
 import shutil
 from stat import S_ISLNK, ST_MODE
-from subprocess import CompletedProcess, run
+import subprocess
 import tempfile
 
 import ddt
@@ -93,14 +93,14 @@ class Wsl:
     class WslNoDistro:
         """Running bash.exe tries to run bash on a WSL distribution, but none exists."""
 
-        process: CompletedProcess[bytes]
+        process: "subprocess.CompletedProcess[bytes]"
         message: str
 
     @dataclass
     class CheckError:
         """Running bash.exe fails in an unexpected error or gives unexpected output."""
 
-        process: CompletedProcess[bytes]
+        process: "subprocess.CompletedProcess[bytes]"
         message: str
 
     @dataclass
@@ -133,7 +133,7 @@ def check(cls):
             # information on ways to check for WSL, see https://superuser.com/a/1749811.
             script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
             command = ["bash.exe", "-c", script]
-            process = run(command, capture_output=True)
+            process = subprocess.run(command, capture_output=True)
         except FileNotFoundError:
             return cls.Absent()
         except OSError as error:

From b061a012d43496fcec861886944ab8cdb9e49415 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 15 Jan 2024 15:50:01 -0500
Subject: [PATCH 385/994] Pin Sphinx plugins to compatible versions

This fixes version incompatibilities with major version 4 of
Sphinx, which GitPython is still using for the time being. Some
plugins that previously had depended back on Sphinx have had those
dependencies removed to avoid dependency cycles, but the effect is
that `pip` no longer is aware of or able to enforce version
compatibility, and newer plugin versions than can actually run with
Sphinx 4 are installed instead of older, usable versions.

This fixes the problem by pinning each of the affected Sphinx
plugins' latest actually compatible versions, i.e., latest versions
that do not need Sphinx 5 or higher.

The alternative of moving to Sphinx 5 or higher should probably be
done eventually, but will require addressing a cross-reference
ambiguity in type annotations for the `Actor` class.

For details on the bug, see:

- https://github.com/gitpython-developers/GitPython/pull/1799#discussion_r1451969138
- https://github.com/gitpython-developers/GitPython/issues/1802
---
 doc/requirements.txt | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/doc/requirements.txt b/doc/requirements.txt
index 41a7c90f1..12cd19c1e 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,3 +1,8 @@
 sphinx==4.3.0
 sphinx_rtd_theme
+sphinxcontrib-applehelp==1.0.4
+sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-htmlhelp==2.0.1
+sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-serializinghtml==1.1.5
 sphinx-autodoc-typehints

From 370822cdaa1e46ec9469513f30e4142e15a345fe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 15 Jan 2024 16:45:01 -0500
Subject: [PATCH 386/994] Extend plugin version ranges down for Python 3.7

This uses narrow ranges, rather than pinning single versions, for
two of the Sphinx plugins whose versions are recently pinned.

This is so that plugin versions compatible with Python 3.7 (which
GitPython still supports) can be selected.
---
 doc/requirements.txt | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/doc/requirements.txt b/doc/requirements.txt
index 12cd19c1e..90449cd02 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,8 +1,8 @@
-sphinx==4.3.0
+sphinx == 4.3.0
 sphinx_rtd_theme
-sphinxcontrib-applehelp==1.0.4
-sphinxcontrib-devhelp==1.0.2
-sphinxcontrib-htmlhelp==2.0.1
-sphinxcontrib-qthelp==1.0.3
-sphinxcontrib-serializinghtml==1.1.5
+sphinxcontrib-applehelp >= 1.0.2, <= 1.0.4
+sphinxcontrib-devhelp == 1.0.2
+sphinxcontrib-htmlhelp >= 2.0.0, <= 2.0.1
+sphinxcontrib-qthelp == 1.0.3
+sphinxcontrib-serializinghtml == 1.1.5
 sphinx-autodoc-typehints

From 74ef6c7447f83265f9981883b62e610340b75e5c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 15 Jan 2024 18:04:29 -0500
Subject: [PATCH 387/994] Bump Sphinx from 4.3.0 to 4.3.2

Although 4.4.0 or greater do not seem usable for the time being due
to finding "more than one target found for cross-reference 'Actor'"
as examined in #1802, bugfixes from 4.3.* patch releases are okay.
---
 doc/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/requirements.txt b/doc/requirements.txt
index 90449cd02..7769af5ae 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,4 +1,4 @@
-sphinx == 4.3.0
+sphinx == 4.3.2
 sphinx_rtd_theme
 sphinxcontrib-applehelp >= 1.0.2, <= 1.0.4
 sphinxcontrib-devhelp == 1.0.2

From 08a819c13501fa5e91e5e74d77907a18092ee25b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EF=BC=A5than?= <et-repositories@proton.me>
Date: Mon, 15 Jan 2024 14:48:12 +0800
Subject: [PATCH 388/994] fix: add treeNotSorted test

---
 test/test_tree.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/test/test_tree.py b/test/test_tree.py
index 7713413a6..695cb803e 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -8,7 +8,9 @@
 from git.objects import Tree, Blob
 from test.lib import TestBase
 
+import os
 import os.path as osp
+import subprocess
 
 
 class TestTree(TestBase):
@@ -40,6 +42,62 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
+    def test_tree_modifier_ordering(self):
+        def setup_git_repository_and_get_ordered_files():
+            os.mkdir("tmp")
+            os.chdir("tmp")
+            subprocess.run(["git", "init", "-q"], check=True)
+            os.mkdir("file")
+            for filename in [
+                "bin",
+                "bin.d",
+                "file.to",
+                "file.toml",
+                "file.toml.bin",
+                "file0",
+                "file/a",
+            ]:
+                open(filename, "a").close()
+
+            subprocess.run(["git", "add", "."], check=True)
+            subprocess.run(["git", "commit", "-m", "c1"], check=True)
+            tree_hash = subprocess.check_output(["git", "rev-parse", "HEAD^{tree}"]).decode().strip()
+            cat_file_output = subprocess.check_output(["git", "cat-file", "-p", tree_hash]).decode()
+            return [line.split()[-1] for line in cat_file_output.split("\n") if line]
+
+        hexsha = "6c1faef799095f3990e9970bc2cb10aa0221cf9c"
+        roottree = self.rorepo.tree(hexsha)
+        blob_mode = Tree.blob_id << 12
+        tree_mode = Tree.tree_id << 12
+
+        files_in_desired_order = [
+            (blob_mode, "bin"),
+            (blob_mode, "bin.d"),
+            (blob_mode, "file.to"),
+            (blob_mode, "file.toml"),
+            (blob_mode, "file.toml.bin"),
+            (blob_mode, "file0"),
+            (tree_mode, "file"),
+        ]
+        mod = roottree.cache
+        for file_mode, file_name in files_in_desired_order:
+            mod.add(hexsha, file_mode, file_name)
+        # end for each file
+
+        def file_names_in_order():
+            return [t[1] for t in files_in_desired_order]
+
+        def names_in_mod_cache():
+            a = [t[2] for t in mod._cache]
+            here = file_names_in_order()
+            return [e for e in a if e in here]
+
+        git_file_names_in_order = setup_git_repository_and_get_ordered_files()
+        os.chdir("..")
+
+        mod.set_done()
+        assert names_in_mod_cache() == git_file_names_in_order, "set_done() performs git-sorting"
+
     def test_traverse(self):
         root = self.rorepo.tree("0.1.6")
         num_recursive = 0

From 365d44f50a3d72d7ebfa063b142d2abd4082cfaa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EF=BC=A5than?= <et-repositories@proton.me>
Date: Mon, 15 Jan 2024 14:50:43 +0800
Subject: [PATCH 389/994] fix: treeNotSorted issue

---
 git/objects/tree.py | 50 +--------------------------------------------
 1 file changed, 1 insertion(+), 49 deletions(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index a08adf48b..450973280 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -53,54 +53,6 @@
 __all__ = ("TreeModifier", "Tree")
 
 
-def git_cmp(t1: TreeCacheTup, t2: TreeCacheTup) -> int:
-    a, b = t1[2], t2[2]
-    # assert isinstance(a, str) and isinstance(b, str)
-    len_a, len_b = len(a), len(b)
-    min_len = min(len_a, len_b)
-    min_cmp = cmp(a[:min_len], b[:min_len])
-
-    if min_cmp:
-        return min_cmp
-
-    return len_a - len_b
-
-
-def merge_sort(a: List[TreeCacheTup], cmp: Callable[[TreeCacheTup, TreeCacheTup], int]) -> None:
-    if len(a) < 2:
-        return
-
-    mid = len(a) // 2
-    lefthalf = a[:mid]
-    righthalf = a[mid:]
-
-    merge_sort(lefthalf, cmp)
-    merge_sort(righthalf, cmp)
-
-    i = 0
-    j = 0
-    k = 0
-
-    while i < len(lefthalf) and j < len(righthalf):
-        if cmp(lefthalf[i], righthalf[j]) <= 0:
-            a[k] = lefthalf[i]
-            i = i + 1
-        else:
-            a[k] = righthalf[j]
-            j = j + 1
-        k = k + 1
-
-    while i < len(lefthalf):
-        a[k] = lefthalf[i]
-        i = i + 1
-        k = k + 1
-
-    while j < len(righthalf):
-        a[k] = righthalf[j]
-        j = j + 1
-        k = k + 1
-
-
 class TreeModifier:
     """A utility class providing methods to alter the underlying cache in a list-like fashion.
 
@@ -131,7 +83,7 @@ def set_done(self) -> "TreeModifier":
 
         :return self:
         """
-        merge_sort(self._cache, git_cmp)
+        self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
         return self
 
     # } END interface

From bda5a178f2a1cd16716fe7f289eaf8296e8e6f83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EF=BC=A5than?= <et-repositories@proton.me>
Date: Tue, 16 Jan 2024 16:13:50 +0800
Subject: [PATCH 390/994] chore: update AUTHORS

---
 AUTHORS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/AUTHORS b/AUTHORS
index 3b97c9473..9311b3962 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -53,5 +53,6 @@ Contributors are:
 -Santos Gallegos <stsewd _at_ proton.me>
 -Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
 -Eliah Kagan <eliah.kagan _at_ gmail.com>
+-Ethan Lin <et.repositories _at_ gmail.com>
 
 Portions derived from other open source works and are clearly marked.

From 741dfd392d2b79c4f4b84ed2e08e4838714469bf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 23 Jan 2024 18:17:00 -0500
Subject: [PATCH 391/994] Deprecate git.util.NullHandler

The NullHandler class in git.util was added when merging #300, to
allow a noop handler to be used on Python 2.6, since the standard
library logging.NullHandler class was added in Python 2.7.

When introduced in d1a9a23, the git.util.NullHandler class was also
patched into the logging module, but that has no longer been done
since 2fced2e (#979), nor does GitPython make other use of it.

This also changes the parameter type annotation on the emit method
from `object` to `logging.LogRecord`, which is the expeced type.
---
 git/util.py | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 5ccd2b04c..4a284b785 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1295,5 +1295,20 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
 
 class NullHandler(logging.Handler):
-    def emit(self, record: object) -> None:
+    """Deprecated, use :class:`logging.NullHandler` instead.
+
+    This noop handler is like :class:`~logging.NullHandler` in the standard library,
+    which should be used instead, because it is now always available, and it overrides
+    more logging methods to make them noop. This class only overrides :meth:`emit`.
+    """
+
+    def __init__(self, level: int = logging.NOTSET) -> None:
+        warnings.warn(
+            "NullHandler in git.util is deprecated. Use logging.NullHandler instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        super().__init__(level)
+
+    def emit(self, record: logging.LogRecord) -> None:
         pass

From 01e768b8b8b1af0b56d35ee244e54cae1b22126c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 23 Jan 2024 18:43:27 -0500
Subject: [PATCH 392/994] Remove git.util.NullHandler

git.util has an __all__ attribute, which does not list NullHandler.
As NullHandler is not otherwise documented to be public, removing
it is not a breaking change, and there is no need for a deprecation
period or to wait until the next major version of GitPython.

See 741dfd3 for information about its history.
---
 git/util.py | 20 --------------------
 1 file changed, 20 deletions(-)

diff --git a/git/util.py b/git/util.py
index 4a284b785..27ca06bcd 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1292,23 +1292,3 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
 
 # } END classes
-
-
-class NullHandler(logging.Handler):
-    """Deprecated, use :class:`logging.NullHandler` instead.
-
-    This noop handler is like :class:`~logging.NullHandler` in the standard library,
-    which should be used instead, because it is now always available, and it overrides
-    more logging methods to make them noop. This class only overrides :meth:`emit`.
-    """
-
-    def __init__(self, level: int = logging.NOTSET) -> None:
-        warnings.warn(
-            "NullHandler in git.util is deprecated. Use logging.NullHandler instead.",
-            DeprecationWarning,
-            stacklevel=2,
-        )
-        super().__init__(level)
-
-    def emit(self, record: logging.LogRecord) -> None:
-        pass

From a11b89a40eb3a3b8197b0a8966cf54f4d63d5e3f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 23 Jan 2024 23:11:34 -0500
Subject: [PATCH 393/994] Clarify why GIT_PYTHON_GIT_EXECUTABLE may be set on
 failure

This clarifies the comment that explains the significance of
setting the Git.GIT_PYTHON_GIT_EXECUTABLE attribute when running
Git failed but the attribute wasn't set before.

This happens in the code path in Git.refresh where has_git is False
and old_git is None, provided the refresh mode doesn't call for the
initial refresh to raise an exception on failure.

It was previously described in terms of a "first" and "second"
import, but as discussed in #1804, the rationale and effect were
not altogether clear.
---
 git/cmd.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 4413182e0..76b71e98e 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -475,9 +475,10 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                     )
                     raise ImportError(err)
 
-                # We get here if this was the init refresh and the refresh mode was not
-                # error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
-                # discern the difference between a first import and a second import.
+                # We get here if this was the initial refresh and the refresh mode was
+                # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
+                # discern the the difference between the first refresh at import time
+                # and subsequent calls to refresh.
                 cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
             else:
                 # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer

From 82cbc6c58e6949a9b92aa30df78f9ce98f40ab02 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Wed, 24 Jan 2024 08:44:28 +0100
Subject: [PATCH 394/994] Apply suggestions from code review

---
 git/cmd.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 76b71e98e..24ba71b5f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -477,8 +477,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
 
                 # We get here if this was the initial refresh and the refresh mode was
                 # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
-                # discern the the difference between the first refresh at import time
-                # and subsequent calls to refresh.
+                # discern the difference between the first refresh at import time
+                # and subsequent calls to refresh().
                 cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
             else:
                 # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer

From ba4cbae2f5dcc417b5c09054b539d30b2ea1b33d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 13:38:24 -0500
Subject: [PATCH 395/994] Split test_refresh into two tests

For when refreshing should succeed and when it should fail.
---
 test/test_git.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 3b9abc712..fc15b7842 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -306,11 +306,10 @@ def test_cmd_override(self):
         ):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
-    def test_refresh(self):
-        # Test a bad git path refresh.
+    def test_refresh_bad_git_path(self):
         self.assertRaises(GitCommandNotFound, refresh, "yada")
 
-        # Test a good path refresh.
+    def test_refresh_good_git_path(self):
         which_cmd = "where" if os.name == "nt" else "command -v"
         path = os.popen("{0} git".format(which_cmd)).read().strip().split("\n")[0]
         refresh(path)

From f24180873b4fb7e8790e3f36e1d570f456565f42 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 14:05:01 -0500
Subject: [PATCH 396/994] Simplify test of refresh that should succeed

shutil.which does not always find the git that Popen would run, at
least on Windows, but it is no worse than running a "command -v" or
"where" command as was previously done here via os.popen.
---
 test/test_git.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index fc15b7842..6200896d9 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -310,8 +310,7 @@ def test_refresh_bad_git_path(self):
         self.assertRaises(GitCommandNotFound, refresh, "yada")
 
     def test_refresh_good_git_path(self):
-        which_cmd = "where" if os.name == "nt" else "command -v"
-        path = os.popen("{0} git".format(which_cmd)).read().strip().split("\n")[0]
+        path = shutil.which("git")
         refresh(path)
 
     def test_options_are_passed_to_git(self):

From f98aadddfe390348c2858690fec9577301ea9ba9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 17:14:26 -0500
Subject: [PATCH 397/994] Have test of refresh that should fail assert command

This extends test_refresh_bad_git_path so that it asserts that the
exception message shows the command the failed refresh used.

This test fails due to a bug where "git" is always shown (#1809).
---
 test/test_git.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/test/test_git.py b/test/test_git.py
index 6200896d9..ea47abfae 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -307,7 +307,11 @@ def test_cmd_override(self):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
     def test_refresh_bad_git_path(self):
-        self.assertRaises(GitCommandNotFound, refresh, "yada")
+        path = "yada"
+        escaped_abspath = re.escape(str(Path(path).absolute()))
+        expected_pattern = rf"\n[ \t]*cmdline: {escaped_abspath}\Z"
+        with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+            refresh(path)
 
     def test_refresh_good_git_path(self):
         path = shutil.which("git")

From 24d6067d9f2a3666d5812e98d6f6bb2bc7cce27f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 17:29:40 -0500
Subject: [PATCH 398/994] Fix wrong GitCommandNotFound command from refresh

See #1809. The test extended in the previous commit now passes.
---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 24ba71b5f..1893c6b73 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -483,7 +483,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
             else:
                 # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
                 # None) we raise an exception.
-                raise GitCommandNotFound("git", err)
+                raise GitCommandNotFound(new_git, err)
 
         return has_git
 

From 3a34dee196c2cfb4873f161d17388847f9f1c1cc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 18:15:08 -0500
Subject: [PATCH 399/994] Also test refresh with an already-absolute bad path

---
 test/test_git.py | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index ea47abfae..45f32c85c 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -306,16 +306,23 @@ def test_cmd_override(self):
         ):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
-    def test_refresh_bad_git_path(self):
-        path = "yada"
-        escaped_abspath = re.escape(str(Path(path).absolute()))
-        expected_pattern = rf"\n[ \t]*cmdline: {escaped_abspath}\Z"
+    def test_refresh_bad_absolute_git_path(self):
+        absolute_path = str(Path("yada").absolute())
+        expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
         with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
-            refresh(path)
+            refresh(absolute_path)
 
-    def test_refresh_good_git_path(self):
-        path = shutil.which("git")
-        refresh(path)
+    def test_refresh_bad_relative_git_path(self):
+        relative_path = "yada"
+        absolute_path = str(Path(relative_path).absolute())
+        expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
+        with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+            refresh(relative_path)
+
+    def test_refresh_good_absolute_git_path(self):
+        absolute_path = shutil.which("git")
+        refresh(absolute_path)
+        self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.

From 5ad7cb2f4eeff030b27e0ece3a308464cad415b8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 18:46:48 -0500
Subject: [PATCH 400/994] Start work on making refresh tests restore state

+ Test a successful refresh with a relative path, which will be
  safer to do once the refresh tests restore changed state.

See #1811. This addresses it incompletely, because while it is
probably not necessary while running the test suite to preserve an
old False value of git.GIT_OK (most tests can't work if that
happens anyway), the value of Git.GIT_PYTHON_GIT_EXECUTABLE is not
the only other global state that effects the behavior of
subsequently run tests and that may be changed as a result of the
refresh tests.

1. After the git.refresh function calls git.cmd.Git.refresh, it
   calls git.remote.FetchInfo.refresh, which rebinds the
   git.remote.FetchInfo._flag_map attribute.

2. Future changes to git.cmd.Git.refresh may mutate other state in
   the Git class, and ideally the coupling would be loose enough
   that the refresh tests wouldn't have to be updated for that if
   the behavior being tested does not change.

3. Future changes to git.refresh may perform other refreshing
   actions, and ideally it would be easy (and obvious) what has to
   be done to patch it back. In particular, it will likely call
   another Git method that mutates class-wide state due to #1791,
   and for such state that is also of the Git class, ideally no
   further changes would have to be made to the code that restores
   state after the refresh tests.

If we assume git.refresh is working at least in the case that it is
called with no arguments, then the cleanup can just be a call to
git.refresh(). Otherwise, sufficiently general cleanup may be more
complicated.
---
 test/test_git.py | 41 +++++++++++++++++++++++++++++++++--------
 1 file changed, 33 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 45f32c85c..9b6059098 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -44,6 +44,15 @@ def _patch_out_env(name):
             os.environ[name] = old_value
 
 
+@contextlib.contextmanager
+def _restore_git_executable():
+    old_git_executable = Git.GIT_PYTHON_GIT_EXECUTABLE
+    try:
+        yield old_git_executable  # Let test code run that may rebind the attribute.
+    finally:
+        Git.GIT_PYTHON_GIT_EXECUTABLE = old_git_executable
+
+
 @ddt.ddt
 class TestGit(TestBase):
     @classmethod
@@ -309,20 +318,36 @@ def test_cmd_override(self):
     def test_refresh_bad_absolute_git_path(self):
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
-        with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
-            refresh(absolute_path)
+
+        with _restore_git_executable() as old_git_executable:
+            with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+                refresh(absolute_path)
+            self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
     def test_refresh_bad_relative_git_path(self):
-        relative_path = "yada"
-        absolute_path = str(Path(relative_path).absolute())
+        absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
-        with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
-            refresh(relative_path)
+
+        with _restore_git_executable() as old_git_executable:
+            with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+                refresh("yada")
+            self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
     def test_refresh_good_absolute_git_path(self):
         absolute_path = shutil.which("git")
-        refresh(absolute_path)
-        self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
+
+        with _restore_git_executable():
+            refresh(absolute_path)
+            self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
+
+    def test_refresh_good_relative_git_path(self):
+        absolute_path = shutil.which("git")
+        dirname, basename = osp.split(absolute_path)
+
+        with cwd(dirname):
+            with _restore_git_executable():
+                refresh(basename)
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.

From b3fc30eeb476e6d76a292b5a58788efa576fb9a1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 19:40:31 -0500
Subject: [PATCH 401/994] Re-refresh to restore state after refresh tests

For #1811. This is the simple way to do it. This relies on
git.refresh working when called when no arguments, so if that ever
breaks, then it may be difficult or inefficient to investigate. But
this is simpler than any alternatives that are equally or more
robust.

This is already better than the previous incomplete way that only
restored Git.GIT_PYTHON_GIT_EXECUTABLE (and nothing in FetchInfo).
Furthermore, because the tests already depend to a large extent on
git.refreh working when called with no arguments, as otherwise the
initial refresh may not have set Git.GIT_PYTHON_GIT_EXECUTABLE
usably, it might not be worthwhile to explore other approaches.
---
 test/test_git.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 9b6059098..080486a55 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -45,12 +45,12 @@ def _patch_out_env(name):
 
 
 @contextlib.contextmanager
-def _restore_git_executable():
+def _rollback_refresh():
     old_git_executable = Git.GIT_PYTHON_GIT_EXECUTABLE
     try:
-        yield old_git_executable  # Let test code run that may rebind the attribute.
+        yield old_git_executable  # Let test code run that may mutate class state.
     finally:
-        Git.GIT_PYTHON_GIT_EXECUTABLE = old_git_executable
+        refresh()
 
 
 @ddt.ddt
@@ -319,7 +319,7 @@ def test_refresh_bad_absolute_git_path(self):
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
 
-        with _restore_git_executable() as old_git_executable:
+        with _rollback_refresh() as old_git_executable:
             with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
                 refresh(absolute_path)
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
@@ -328,7 +328,7 @@ def test_refresh_bad_relative_git_path(self):
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
 
-        with _restore_git_executable() as old_git_executable:
+        with _rollback_refresh() as old_git_executable:
             with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
                 refresh("yada")
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
@@ -336,7 +336,7 @@ def test_refresh_bad_relative_git_path(self):
     def test_refresh_good_absolute_git_path(self):
         absolute_path = shutil.which("git")
 
-        with _restore_git_executable():
+        with _rollback_refresh():
             refresh(absolute_path)
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
@@ -345,7 +345,7 @@ def test_refresh_good_relative_git_path(self):
         dirname, basename = osp.split(absolute_path)
 
         with cwd(dirname):
-            with _restore_git_executable():
+            with _rollback_refresh():
                 refresh(basename)
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 

From ae28c986310703a83d0e2495e0e50530231040ee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 20:55:51 -0500
Subject: [PATCH 402/994] Refactor _rollback_refresh slightly for clarity

---
 test/test_git.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 080486a55..96730a7a5 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -46,9 +46,8 @@ def _patch_out_env(name):
 
 @contextlib.contextmanager
 def _rollback_refresh():
-    old_git_executable = Git.GIT_PYTHON_GIT_EXECUTABLE
     try:
-        yield old_git_executable  # Let test code run that may mutate class state.
+        yield Git.GIT_PYTHON_GIT_EXECUTABLE  # Provide the old value for convenience.
     finally:
         refresh()
 

From 3bb63f3766605a597fa1dbca2367cb6ffda4a3ba Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jan 2024 23:27:39 -0500
Subject: [PATCH 403/994] Condense FetchInfo.refresh using contextlib.suppress

I think this refactoring slightly improves readability.

+ Condense/sort/group imports (while adding contextlib import).

+ Minor style tweaks in FetchInfo.refresh comments.

+ Make a comment in Git.refresh clearer, building on revisions in:
  https://github.com/gitpython-developers/GitPython/pull/1810#discussion_r1464468620
---
 git/cmd.py    |  2 +-
 git/remote.py | 33 +++++++++++----------------------
 2 files changed, 12 insertions(+), 23 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 1893c6b73..b3bd48b81 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -478,7 +478,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 # We get here if this was the initial refresh and the refresh mode was
                 # not error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we
                 # discern the difference between the first refresh at import time
-                # and subsequent calls to refresh().
+                # and subsequent calls to git.refresh or this refresh method.
                 cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name
             else:
                 # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer
diff --git a/git/remote.py b/git/remote.py
index 98a421b3a..7ebe566b3 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -5,30 +5,24 @@
 
 """Module implementing a remote object allowing easy access to git remotes."""
 
+import contextlib
 import logging
 import re
 
-from git.cmd import handle_process_output, Git
+from git.cmd import Git, handle_process_output
 from git.compat import defenc, force_text
+from git.config import GitConfigParser, SectionConstraint, cp
 from git.exc import GitCommandError
+from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
 from git.util import (
-    LazyMixin,
-    IterableObj,
+    CallableRemoteProgress,
     IterableList,
+    IterableObj,
+    LazyMixin,
     RemoteProgress,
-    CallableRemoteProgress,
-)
-from git.util import (
     join_path,
 )
 
-from git.config import (
-    GitConfigParser,
-    SectionConstraint,
-    cp,
-)
-from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference
-
 # typing-------------------------------------------------------
 
 from typing import (
@@ -345,18 +339,13 @@ class FetchInfo(IterableObj):
     @classmethod
     def refresh(cls) -> Literal[True]:
         """This gets called by the refresh function (see the top level __init__)."""
-        # clear the old values in _flag_map
-        try:
+        # Clear the old values in _flag_map.
+        with contextlib.suppress(KeyError):
             del cls._flag_map["t"]
-        except KeyError:
-            pass
-
-        try:
+        with contextlib.suppress(KeyError):
             del cls._flag_map["-"]
-        except KeyError:
-            pass
 
-        # set the value given the git version
+        # Set the value given the git version.
         if Git().version_info[:2] >= (2, 10):
             cls._flag_map["t"] = cls.TAG_UPDATE
         else:

From 9b7e15f69186a61bc6413a6da0a1ea13776522bc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 25 Jan 2024 00:38:40 -0500
Subject: [PATCH 404/994] State what the refresh tests verify

This adds short docstrings to each of the four refresh tests to
briefly state the claims they verify.
---
 test/test_git.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/test/test_git.py b/test/test_git.py
index 96730a7a5..a9af0b04d 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -315,6 +315,7 @@ def test_cmd_override(self):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
     def test_refresh_bad_absolute_git_path(self):
+        """Bad absolute path arg is reported and not set."""
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
 
@@ -324,6 +325,7 @@ def test_refresh_bad_absolute_git_path(self):
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
     def test_refresh_bad_relative_git_path(self):
+        """Bad relative path arg is resolved to absolute path and reported, not set."""
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
 
@@ -333,6 +335,7 @@ def test_refresh_bad_relative_git_path(self):
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
     def test_refresh_good_absolute_git_path(self):
+        """Good absolute path arg is set."""
         absolute_path = shutil.which("git")
 
         with _rollback_refresh():
@@ -340,6 +343,7 @@ def test_refresh_good_absolute_git_path(self):
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
     def test_refresh_good_relative_git_path(self):
+        """Good relative path arg is resolved to absolute path and set."""
         absolute_path = shutil.which("git")
         dirname, basename = osp.split(absolute_path)
 

From 987dbf4a0f06c7933496739fbbcd083af4f25159 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 25 Jan 2024 21:19:13 -0500
Subject: [PATCH 405/994] Use consistent style for passing logger name

This has each module use `__name__` for the path to its own logger.
Previously, most modules did this several hard-coded their names in
calls to logging.getLogger.
---
 git/config.py                 | 2 +-
 git/objects/commit.py         | 2 +-
 git/objects/submodule/base.py | 2 +-
 git/objects/submodule/root.py | 2 +-
 git/remote.py                 | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/git/config.py b/git/config.py
index 2730ddaf3..48319e537 100644
--- a/git/config.py
+++ b/git/config.py
@@ -61,7 +61,7 @@
 __all__ = ("GitConfigParser", "SectionConstraint")
 
 
-log = logging.getLogger("git.config")
+log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 5a069848c..774e322fc 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -52,7 +52,7 @@
 
 # ------------------------------------------------------------------------
 
-log = logging.getLogger("git.objects.commit")
+log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 __all__ = ("Commit",)
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index c3d353141..585037653 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -56,7 +56,7 @@
 __all__ = ["Submodule", "UpdateProgress"]
 
 
-log = logging.getLogger("git.objects.submodule.base")
+log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index d9d9f6d24..db60abf8b 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -22,7 +22,7 @@
 
 __all__ = ["RootModule", "RootUpdateProgress"]
 
-log = logging.getLogger("git.objects.submodule.root")
+log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 
diff --git a/git/remote.py b/git/remote.py
index 98a421b3a..e33add1d1 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -65,7 +65,7 @@
 # -------------------------------------------------------------
 
 
-log = logging.getLogger("git.remote")
+log = logging.getLogger(__name__)
 log.addHandler(logging.NullHandler())
 
 

From 7bbccb42b0f40996f1ee1a821a6ac96726ab575d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 25 Jan 2024 23:46:53 -0500
Subject: [PATCH 406/994] Rename logger globals from log to _logger

These globals were nonpublic, because even though they were named
without leading underscores, they all appeared in modules in which
__all__ was defined and did not list them. (They remain nonpublic.)

This renaming is to avoid confusion between those log attributes
ang logging features of Git itself ("git log", "git reflog"), and
more importantly, with the related git.refs.log module, referred to
by the log attribute of the git.refs module, and the log attribute
of the git module (git/__init__.py has a * import from git.refs).
---
 git/cmd.py                    | 22 +++++++++++-----------
 git/config.py                 |  8 ++++----
 git/objects/commit.py         | 10 +++++-----
 git/objects/submodule/base.py | 16 ++++++++--------
 git/objects/submodule/root.py |  8 ++++----
 git/remote.py                 | 18 +++++++++---------
 git/repo/base.py              |  8 ++++----
 git/util.py                   |  8 ++++----
 test/lib/helper.py            | 14 +++++++-------
 test/test_git.py              |  4 ++--
 test/test_index.py            |  8 ++++----
 11 files changed, 62 insertions(+), 62 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 24ba71b5f..84f929ac6 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -81,8 +81,8 @@
     "strip_newline_in_stdout",
 }
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 __all__ = ("Git",)
 
@@ -146,7 +146,7 @@ def pump_stream(
                         handler(line)
 
         except Exception as ex:
-            log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
+            _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
             if "I/O operation on closed file" not in str(ex):
                 # Only reraise if the error was not due to the stream closing
                 raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
@@ -600,7 +600,7 @@ def _terminate(self) -> None:
                     self.status = self._status_code_if_terminate or proc.poll()
                     return
             except OSError as ex:
-                log.info("Ignored error after process had died: %r", ex)
+                _logger.info("Ignored error after process had died: %r", ex)
 
             # It can be that nothing really exists anymore...
             if os is None or getattr(os, "kill", None) is None:
@@ -613,7 +613,7 @@ def _terminate(self) -> None:
 
                 self.status = self._status_code_if_terminate or status
             except OSError as ex:
-                log.info("Ignored error after process had died: %r", ex)
+                _logger.info("Ignored error after process had died: %r", ex)
             # END exception handling
 
         def __del__(self) -> None:
@@ -654,7 +654,7 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte
 
             if status != 0:
                 errstr = read_all_from_possibly_closed_stream(p_stderr)
-                log.debug("AutoInterrupt wait stderr: %r" % (errstr,))
+                _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
                 raise GitCommandError(remove_password_if_present(self.args), status, errstr)
             return status
 
@@ -1018,7 +1018,7 @@ def execute(
         # Remove password for the command if present.
         redacted_command = remove_password_if_present(command)
         if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
-            log.info(" ".join(redacted_command))
+            _logger.info(" ".join(redacted_command))
 
         # Allow the user to have the command executed in their working dir.
         try:
@@ -1055,7 +1055,7 @@ def execute(
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
         if shell is None:
             shell = self.USE_SHELL
-        log.debug(
+        _logger.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,
             cwd,
@@ -1167,7 +1167,7 @@ def as_text(stdout_value: Union[bytes, str]) -> str:
             # END as_text
 
             if stderr_value:
-                log.info(
+                _logger.info(
                     "%s -> %d; stdout: '%s'; stderr: '%s'",
                     cmdstr,
                     status,
@@ -1175,9 +1175,9 @@ def as_text(stdout_value: Union[bytes, str]) -> str:
                     safe_decode(stderr_value),
                 )
             elif stdout_value:
-                log.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
+                _logger.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value))
             else:
-                log.info("%s -> %d", cmdstr, status)
+                _logger.info("%s -> %d", cmdstr, status)
         # END handle debug printing
 
         if with_exceptions and status != 0:
diff --git a/git/config.py b/git/config.py
index 48319e537..ba1666404 100644
--- a/git/config.py
+++ b/git/config.py
@@ -61,8 +61,8 @@
 __all__ = ("GitConfigParser", "SectionConstraint")
 
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 
 CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
@@ -412,7 +412,7 @@ def release(self) -> None:
         try:
             self.write()
         except IOError:
-            log.error("Exception during destruction of GitConfigParser", exc_info=True)
+            _logger.error("Exception during destruction of GitConfigParser", exc_info=True)
         except ReferenceError:
             # This happens in Python 3... and usually means that some state cannot be
             # written as the sections dict cannot be iterated. This usually happens when
@@ -712,7 +712,7 @@ def write(self) -> None:
         # END assert multiple files
 
         if self._has_includes():
-            log.debug(
+            _logger.debug(
                 "Skipping write-back of configuration file as include files were merged in."
                 + "Set merge_includes=False to prevent this."
             )
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 774e322fc..50e7f6d59 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -52,8 +52,8 @@
 
 # ------------------------------------------------------------------------
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 __all__ = ("Commit",)
 
@@ -767,7 +767,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                 self.author_tz_offset,
             ) = parse_actor_and_date(author_line.decode(self.encoding, "replace"))
         except UnicodeDecodeError:
-            log.error(
+            _logger.error(
                 "Failed to decode author line '%s' using encoding %s",
                 author_line,
                 self.encoding,
@@ -781,7 +781,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                 self.committer_tz_offset,
             ) = parse_actor_and_date(committer_line.decode(self.encoding, "replace"))
         except UnicodeDecodeError:
-            log.error(
+            _logger.error(
                 "Failed to decode committer line '%s' using encoding %s",
                 committer_line,
                 self.encoding,
@@ -795,7 +795,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
         try:
             self.message = self.message.decode(self.encoding, "replace")
         except UnicodeDecodeError:
-            log.error(
+            _logger.error(
                 "Failed to decode message '%s' using encoding %s",
                 self.message,
                 self.encoding,
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 585037653..d8011f14a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -56,8 +56,8 @@
 __all__ = ["Submodule", "UpdateProgress"]
 
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 
 class UpdateProgress(RemoteProgress):
@@ -731,7 +731,7 @@ def update(
                         )
                         mrepo.head.reference.set_tracking_branch(remote_branch)
                     except (IndexError, InvalidGitRepositoryError):
-                        log.warning("Failed to checkout tracking branch %s", self.branch_path)
+                        _logger.warning("Failed to checkout tracking branch %s", self.branch_path)
                     # END handle tracking branch
 
                     # NOTE: Have to write the repo config file as well, otherwise the
@@ -761,14 +761,14 @@ def update(
                         binsha = rcommit.binsha
                         hexsha = rcommit.hexsha
                     else:
-                        log.error(
+                        _logger.error(
                             "%s a tracking branch was not set for local branch '%s'",
                             msg_base,
                             mrepo.head.reference,
                         )
                     # END handle remote ref
                 else:
-                    log.error("%s there was no local tracking branch", msg_base)
+                    _logger.error("%s there was no local tracking branch", msg_base)
                 # END handle detached head
             # END handle to_latest_revision option
 
@@ -786,7 +786,7 @@ def update(
                         if force:
                             msg = "Will force checkout or reset on local branch that is possibly in the future of"
                             msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
-                            log.debug(msg)
+                            _logger.debug(msg)
                         else:
                             msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
                             msg %= (
@@ -794,7 +794,7 @@ def update(
                                 mrepo.head,
                                 mrepo,
                             )
-                            log.info(msg)
+                            _logger.info(msg)
                             may_reset = False
                         # END handle force
                     # END handle if we are in the future
@@ -834,7 +834,7 @@ def update(
         except Exception as err:
             if not keep_going:
                 raise
-            log.error(str(err))
+            _logger.error(str(err))
         # END handle keep_going
 
         # HANDLE RECURSION
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index db60abf8b..e48c0d633 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -22,8 +22,8 @@
 
 __all__ = ["RootModule", "RootUpdateProgress"]
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 
 class RootUpdateProgress(UpdateProgress):
@@ -321,7 +321,7 @@ def update(
                                     # this way, it will be checked out in the next step.
                                     # This will change the submodule relative to us, so
                                     # the user will be able to commit the change easily.
-                                    log.warning(
+                                    _logger.warning(
                                         "Current sha %s was not contained in the tracking\
              branch at the new remote, setting it the the remote's tracking branch",
                                         sm.hexsha,
@@ -393,7 +393,7 @@ def update(
         except Exception as err:
             if not keep_going:
                 raise
-            log.error(str(err))
+            _logger.error(str(err))
         # END handle keep_going
 
         # FINALLY UPDATE ALL ACTUAL SUBMODULES
diff --git a/git/remote.py b/git/remote.py
index e33add1d1..eb90fba47 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -65,8 +65,8 @@
 # -------------------------------------------------------------
 
 
-log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+_logger = logging.getLogger(__name__)
+_logger.addHandler(logging.NullHandler())
 
 
 __all__ = ("RemoteProgress", "PushInfo", "FetchInfo", "Remote")
@@ -857,7 +857,7 @@ def _get_fetch_info_from_stderr(
         stderr_text = progress.error_lines and "\n".join(progress.error_lines) or ""
         proc.wait(stderr=stderr_text)
         if stderr_text:
-            log.warning("Error lines received while fetching: %s", stderr_text)
+            _logger.warning("Error lines received while fetching: %s", stderr_text)
 
         for line in progress.other_lines:
             line = force_text(line)
@@ -878,9 +878,9 @@ def _get_fetch_info_from_stderr(
             msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n"
             msg += "Will ignore extra progress lines or fetch head lines."
             msg %= (l_fil, l_fhi)
-            log.debug(msg)
-            log.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8"))
-            log.debug(b"head info: " + str(fetch_head_info).encode("UTF-8"))
+            _logger.debug(msg)
+            _logger.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8"))
+            _logger.debug(b"head info: " + str(fetch_head_info).encode("UTF-8"))
             if l_fil < l_fhi:
                 fetch_head_info = fetch_head_info[:l_fil]
             else:
@@ -892,8 +892,8 @@ def _get_fetch_info_from_stderr(
             try:
                 output.append(FetchInfo._from_line(self.repo, err_line, fetch_line))
             except ValueError as exc:
-                log.debug("Caught error while parsing line: %s", exc)
-                log.warning("Git informed while fetching: %s", err_line.strip())
+                _logger.debug("Caught error while parsing line: %s", exc)
+                _logger.warning("Git informed while fetching: %s", err_line.strip())
         return output
 
     def _get_push_info(
@@ -935,7 +935,7 @@ def stdout_handler(line: str) -> None:
             if not output:
                 raise
             elif stderr_text:
-                log.warning("Error lines received while fetching: %s", stderr_text)
+                _logger.warning("Error lines received while fetching: %s", stderr_text)
                 output.error = e
 
         return output
diff --git a/git/repo/base.py b/git/repo/base.py
index c252ad1f3..f5069dbf5 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -89,7 +89,7 @@
 
 # -----------------------------------------------------------
 
-log = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
 
 __all__ = ("Repo",)
 
@@ -772,7 +772,7 @@ def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> boo
                 if object_info.type == object_type.encode():
                     return True
                 else:
-                    log.debug(
+                    _logger.debug(
                         "Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
                         object_info.type.decode(),
                         object_type,
@@ -781,7 +781,7 @@ def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> boo
             else:
                 return True
         except BadObject:
-            log.debug("Commit hash is invalid.")
+            _logger.debug("Commit hash is invalid.")
             return False
 
     def _get_daemon_export(self) -> bool:
@@ -1298,7 +1298,7 @@ def _clone(
             cmdline = getattr(proc, "args", "")
             cmdline = remove_password_if_present(cmdline)
 
-            log.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
+            _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
             finalize_process(proc, stderr=stderr)
 
         # Our git command could have a different working dir than our actual
diff --git a/git/util.py b/git/util.py
index 27ca06bcd..03d62ffc3 100644
--- a/git/util.py
+++ b/git/util.py
@@ -104,7 +104,7 @@
     "HIDE_WINDOWS_KNOWN_ERRORS",
 ]
 
-log = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
 
 
 def _read_win_env_flag(name: str, default: bool) -> bool:
@@ -124,7 +124,7 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
     except KeyError:
         return default
 
-    log.warning(
+    _logger.warning(
         "The %s environment variable is deprecated. Its effect has never been documented and changes without warning.",
         name,
     )
@@ -135,7 +135,7 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
         return False
     if adjusted_value in {"1", "true", "yes"}:
         return True
-    log.warning("%s has unrecognized value %r, treating as %r.", name, value, default)
+    _logger.warning("%s has unrecognized value %r, treating as %r.", name, value, default)
     return default
 
 
@@ -466,7 +466,7 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
             # retcode = process.poll()
             is_cygwin = "CYGWIN" in uname_out
         except Exception as ex:
-            log.debug("Failed checking if running in CYGWIN due to: %r", ex)
+            _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
         _is_cygwin_cache[git_executable] = is_cygwin
 
     return is_cygwin
diff --git a/test/lib/helper.py b/test/lib/helper.py
index 1fdc56fd1..e6784e51c 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -45,7 +45,7 @@
     "GIT_DAEMON_PORT",
 )
 
-log = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
 
 # { Routines
 
@@ -96,7 +96,7 @@ def wrapper(self, *args, **kwargs):
         try:
             return func(self, path, *args, **kwargs)
         except Exception:
-            log.info(
+            _logger.info(
                 "Test %s.%s failed, output is at %r\n",
                 type(self).__name__,
                 func.__name__,
@@ -147,7 +147,7 @@ def repo_creator(self):
             try:
                 return func(self, rw_repo)
             except:  # noqa: E722 B001
-                log.info("Keeping repo after failure: %s", repo_dir)
+                _logger.info("Keeping repo after failure: %s", repo_dir)
                 repo_dir = None
                 raise
             finally:
@@ -212,7 +212,7 @@ def git_daemon_launched(base_path, ip, port):
           and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
         """
         )
-        log.warning(msg, ex, ip, port, base_path, base_path, exc_info=1)
+        _logger.warning(msg, ex, ip, port, base_path, base_path, exc_info=1)
 
         yield  # OK, assume daemon started manually.
 
@@ -221,11 +221,11 @@ def git_daemon_launched(base_path, ip, port):
     finally:
         if gd:
             try:
-                log.debug("Killing git-daemon...")
+                _logger.debug("Killing git-daemon...")
                 gd.proc.kill()
             except Exception as ex:
                 # Either it has died (and we're here), or it won't die, again here...
-                log.debug("Hidden error while Killing git-daemon: %s", ex, exc_info=1)
+                _logger.debug("Hidden error while Killing git-daemon: %s", ex, exc_info=1)
 
 
 def with_rw_and_rw_remote_repo(working_tree_ref):
@@ -307,7 +307,7 @@ def remote_repo_creator(self):
                         try:
                             return func(self, rw_repo, rw_daemon_repo)
                         except:  # noqa: E722 B001
-                            log.info(
+                            _logger.info(
                                 "Keeping repos after failure: \n  rw_repo_dir: %s \n  rw_daemon_repo_dir: %s",
                                 rw_repo_dir,
                                 rw_daemon_repo_dir,
diff --git a/test/test_git.py b/test/test_git.py
index 3b9abc712..546416691 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -133,7 +133,7 @@ def test_it_uses_shell_or_not_as_specified(self, case):
     def test_it_logs_if_it_uses_a_shell(self, case):
         """``shell=`` in the log message agrees with what is passed to `Popen`."""
         value_in_call, value_from_class = case
-        with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
+        with self.assertLogs(cmd._logger, level=logging.DEBUG) as log_watcher:
             mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
         self._assert_logged_for_popen(log_watcher, "shell", mock_safer_popen.call_args.kwargs["shell"])
 
@@ -143,7 +143,7 @@ def test_it_logs_if_it_uses_a_shell(self, case):
     )
     def test_it_logs_istream_summary_for_stdin(self, case):
         expected_summary, istream_argument = case
-        with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
+        with self.assertLogs(cmd._logger, level=logging.DEBUG) as log_watcher:
             self.git.execute(["git", "version"], istream=istream_argument)
         self._assert_logged_for_popen(log_watcher, "stdin", expected_summary)
 
diff --git a/test/test_index.py b/test/test_index.py
index 22eac355b..f456f8be0 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -52,7 +52,7 @@
 
 HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
 
-log = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
 
 
 def _get_windows_ansi_encoding():
@@ -144,13 +144,13 @@ def check(cls):
         if process.returncode == 1 and re.search(r"\bhttps://aka.ms/wslstore\b", text):
             return cls.WslNoDistro(process, text)
         if process.returncode != 0:
-            log.error("Error running bash.exe to check WSL status: %s", text)
+            _logger.error("Error running bash.exe to check WSL status: %s", text)
             return cls.CheckError(process, text)
         if text == "0":
             return cls.Wsl()
         if text == "1":
             return cls.Native()
-        log.error("Strange output checking WSL status: %s", text)
+        _logger.error("Strange output checking WSL status: %s", text)
         return cls.CheckError(process, text)
 
     @staticmethod
@@ -174,7 +174,7 @@ def _decode(stdout):
         except UnicodeDecodeError:
             pass
         except LookupError as error:
-            log.warning("%s", str(error))  # Message already says "Unknown encoding:".
+            _logger.warning("%s", str(error))  # Message already says "Unknown encoding:".
 
         # Assume UTF-8. If invalid, substitute Unicode replacement characters.
         return stdout.decode("utf-8", errors="replace")

From 78a82b3cf94e4c2fcaabd69a3f5a57e77ef1d26b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 26 Jan 2024 00:49:39 -0500
Subject: [PATCH 407/994] Don't access logger in tests by nonpublic attribute

This changes a couple tests that access specific loggers to use the
logger name instead, as code outside GitPython should do.
---
 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 546416691..116a9f5df 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -133,7 +133,7 @@ def test_it_uses_shell_or_not_as_specified(self, case):
     def test_it_logs_if_it_uses_a_shell(self, case):
         """``shell=`` in the log message agrees with what is passed to `Popen`."""
         value_in_call, value_from_class = case
-        with self.assertLogs(cmd._logger, level=logging.DEBUG) as log_watcher:
+        with self.assertLogs(cmd.__name__, level=logging.DEBUG) as log_watcher:
             mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
         self._assert_logged_for_popen(log_watcher, "shell", mock_safer_popen.call_args.kwargs["shell"])
 
@@ -143,7 +143,7 @@ def test_it_logs_if_it_uses_a_shell(self, case):
     )
     def test_it_logs_istream_summary_for_stdin(self, case):
         expected_summary, istream_argument = case
-        with self.assertLogs(cmd._logger, level=logging.DEBUG) as log_watcher:
+        with self.assertLogs(cmd.__name__, level=logging.DEBUG) as log_watcher:
             self.git.execute(["git", "version"], istream=istream_argument)
         self._assert_logged_for_popen(log_watcher, "stdin", expected_summary)
 

From bc42ee53275e38fc71b2f9d7ae58086a832e29c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 26 Jan 2024 01:49:13 -0500
Subject: [PATCH 408/994] Don't add `NullHandler`s

This stops adding `NullHandler` instances to GitPython's loggers.
As noted in #1806, when they were added in #300 this prevented
errors when GitPython logged messages and logging was not enabled,
but since Python 3.2 there is a logger of last resort providing a
nicer default behavior of showing the messages. (They are still
shown with better formatting if logging is configured, even if
just done with logging.basicConfig(), so applications should still
typically configure logging.)
---
 git/cmd.py                    | 1 -
 git/config.py                 | 3 ---
 git/objects/commit.py         | 1 -
 git/objects/submodule/base.py | 4 +---
 git/objects/submodule/root.py | 1 -
 git/remote.py                 | 3 ---
 6 files changed, 1 insertion(+), 12 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 84f929ac6..7c9c89737 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -82,7 +82,6 @@
 }
 
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
 
 __all__ = ("Git",)
 
diff --git a/git/config.py b/git/config.py
index ba1666404..85f754197 100644
--- a/git/config.py
+++ b/git/config.py
@@ -60,10 +60,7 @@
 
 __all__ = ("GitConfigParser", "SectionConstraint")
 
-
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
-
 
 CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
 """The configuration level of a configuration file."""
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 50e7f6d59..caec1b6c4 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -53,7 +53,6 @@
 # ------------------------------------------------------------------------
 
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
 
 __all__ = ("Commit",)
 
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index d8011f14a..d08e967b8 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -40,6 +40,7 @@
 
 
 # typing ----------------------------------------------------------------------
+
 from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
 from typing import Any, Iterator, Union
 
@@ -50,14 +51,11 @@
     from git.repo import Repo
     from git.refs import Head
 
-
 # -----------------------------------------------------------------------------
 
 __all__ = ["Submodule", "UpdateProgress"]
 
-
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
 
 
 class UpdateProgress(RemoteProgress):
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index e48c0d633..dde384bbe 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -23,7 +23,6 @@
 __all__ = ["RootModule", "RootUpdateProgress"]
 
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
 
 
 class RootUpdateProgress(UpdateProgress):
diff --git a/git/remote.py b/git/remote.py
index eb90fba47..6fc7d1eb9 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -64,10 +64,7 @@
 
 # -------------------------------------------------------------
 
-
 _logger = logging.getLogger(__name__)
-_logger.addHandler(logging.NullHandler())
-
 
 __all__ = ("RemoteProgress", "PushInfo", "FetchInfo", "Remote")
 

From 73ebcfa54d923503d88078118c5b479c45920205 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 28 Jan 2024 09:33:31 +0800
Subject: [PATCH 409/994] Use Python 3.8 on Cygwin CI

This uses Python 3.8.16 (provided by the Cygwin package python38 at
version 3.8.16-1), to work around the problem that pip has begun to
block on some PyPI package downloads when Python 3.9.18 (provided
by the Cygwin package python39 at version 3.9.18-1) is used.

I also tried a bunch of other stuff, which is listed below and can
be examined in full detail, with all individual diffs and most CI
results, at https://github.com/EliahKagan/GitPython/pull/2.

* Try not installing/upgrading wheel for Cygwin CI

This is for a recent problem where "pip install -U" in the virtual
environment in a Cygwin test job seems to block indefinitely on
downloading the wheel package itself (not other packages' wheels).

* Try not upgrading/installing pip/setuptools either on Cygwin

* Try installing pytho39-wheel Cygwin package

Maybe this will overcome the next blockage, which is the codecov
PyPI package, downloading a .tar.gz file.

* Try upgrading wheel, but after upgrading pip

* Try always running pip on Cygwin as "python -m pip"

* Try using a venv on Cygwin

* Use "python -v -m pip" to see some of what's going on

* Undo venv; use "python -m pip -vvv" to see what's going on

* Undo all debugging changes except passing "-vvv"

* Try with "--no-cache-dir"

* Try with different tmp dir for pip runs

* Try with python39=3.9.16-1

* Try not upgrading setuptools

* Try not installing Cygwin python39-pip package

* Run pip freeze effectively

This doesn't fix the bigger issue, it just addresses something from
the last commit.

* Try not installing python39-virtualenv either

* Try giving IPv4 for files.pythonhosted.org in hosts file

* Try downloading wheel with wget

This is not a usable solution, but it is useful for troubleshooting.

* Try with python39-pip=23.0.1-1

And don't upgrade it or other PyPI packages.

* Pin pip with pip (Cygwin package doesn't pin)

This tries with an older pip, but if the problem is the build
rather than the version, then it would also help.

* Stop pinning; keep skipping -U for PyPA; instrument with -vvv

This won't fix it but is diagnostic, to reveal the URL for the
coverage package, so I can see what happens when that is installed
more manually.

* Try installing coverage[toml] separately

* Unset -vvv to see the bigger picture more easily

* Try killing pip after a timeout and rerunning it

* Use SIGKILL

* Increase timeout from 70 to 120 seconds per try

* Give each try a little more time than the last

Since it has to verify previous work.

* Tweak (re)try parameters

* Try Python 3.8
---
 .github/workflows/cygwin-test.yml | 21 ++++++++-------------
 1 file changed, 8 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index f3937d21e..84a7f6490 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Install Cygwin
       uses: cygwin/cygwin-install-action@v4
       with:
-        packages: python39 python39-pip python39-virtualenv git
+        packages: python38 python38-pip python38-virtualenv git
         add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
     - name: Arrange for verbose output
@@ -55,28 +55,23 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
-    - name: Ensure the "pip" command is available
-      run: |
-        # This is used unless, and before, an updated pip is installed.
-        ln -s pip3 /usr/bin/pip
-
     - name: Update PyPA packages
       run: |
-        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
+        # Get the latest pip, setuptools, and wheel.
+        python3.8 -m pip install -U pip setuptools wheel
 
     - name: Install project and test dependencies
       run: |
-        pip install ".[test]"
+        python3.8 -m pip install ".[test]"
 
     - name: Show version and platform information
       run: |
         uname -a
-        command -v git python
+        command -v git python3.8
         git version
-        python --version
-        python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
+        python3.8 --version
+        python3.8 -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
 
     - name: Test with pytest
       run: |
-        pytest --color=yes -p no:sugar --instafail -vv
+        python3.8 -m pytest --color=yes -p no:sugar --instafail -vv

From 8dc4cb02b5b9d01bd720dc46a9a847bdb906eae0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 28 Jan 2024 10:07:46 +0800
Subject: [PATCH 410/994] Use Python 3.9.16 on Cygwin CI

The latest currently packaged version of Python 3.9 for Cygwin is
3.9.18 (provided by the Cygwin package python39 at version
3.9.18-1). That version, at least as we are using it, has a problem
where pip has begun to block on some PyPI package downloads.

In 73ebcfa (EliahKagan#2), I worked around this problem by
downgrading the minor version of Python to 3.8. But it is better to
use 3.9 if we can, since it is currently the latest minor version
of Python in the Cygwin repositories, and also because (relating to
that) it is used more often, and thus probably used more often with
GitPython, than 3.8.

This upgrades Python on Cygwin but not all the way. It upgrades it
to the latest (or latest currently available) patch version of 3.9
packaged for Cygwin of those that strictly precede 3.9.18 where the
problem occurs. That version is 3.9.16, provided by the Cygwin
package python39 at version 3.9.16-1.

This version may eventually no longer be available for download
from Cygwin's repositories, so hopefully a real solution or better
workaround will be found by then, or perhaps a future update to the
package itself will fix the problem.

I also tried some more other stuff since finding 3.8 to work in
73ebcfa. Changes since then are listed below. They can be examined
in full detail, with individual diffs and CI results, at
https://github.com/EliahKagan/GitPython/pull/3.

* Try Python 3.9 with other details the same

Changing it to Python 3.8 worked, but I want to check that it was
actually the use of Python 3.8, rather than other seemingly small
changes made to support using Python 3.8, that made the difference.

* Revert "Try Python 3.9 with other details the same"

This reverts commit b55cbfbb31376e9da4ba6a64ac11cd791718ff8d.

* Try 3.9 again, with both python39=3.9.16-1 python39-pip=23.0.1-1

* Back to 3.8; try another GitHub Action

Python 3.8 worked with cygwin-install-action, but I want to make
the change to setup-cygwin by itself first before trying it with
3.9, in case I am using setup-cygwin incorrectly.

* Try 3.9 with this setup-cygwin action

* Try pinning with setup-cygwin

* Try not pinning, but no -U for PyPA, with setup-cygwin

Pinning and skipping -U for PyPA packages worked. Let's see if it
was really pinning that made the difference.

* Try pinning just python39-pip

Pinning works, and merely omitting the -U for PyPA package doesn't.
Examining the output of runs that used install-cygwin-action and
attemped pinning Cygwin package versions shows newer versions were
installed, whereas pinning is really happening with setup-cygwin.

This tries pinning just the Cygwin package for pip, rather than for
Python 3.9. I don't expect this to work.

* Try pinning just python39=3.9.16-1

And not pip, but this does not add back the -U for PyPA yet.

* Add back -U for PyPA packages

* Try pinning python39=3.9.16-1 with old action/everything

This is extremely unlikely to work, I just want to check.

* Try just setup-cygwin and pinning python39=3.9.16-1

That is, this puts back all the other stuff the way it was on the
main branch when the breakage occurred, besides changing from
cygwin-install-action to setup-cygwin to make pinning work and
using it to get version 3.9.16-1 of the Cygwin python39 package.
---
 .github/workflows/cygwin-test.yml | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 84a7f6490..61e6a3089 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -15,7 +15,7 @@ jobs:
 
     defaults:
       run:
-        shell: C:\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}"
+        shell: C:\tools\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}"
 
     steps:
     - name: Force LF line endings
@@ -27,11 +27,10 @@ jobs:
       with:
         fetch-depth: 0
 
-    - name: Install Cygwin
-      uses: cygwin/cygwin-install-action@v4
+    - name: Set up Cygwin
+      uses: egor-tensin/setup-cygwin@v4
       with:
-        packages: python38 python38-pip python38-virtualenv git
-        add-to-path: false  # No need to change $PATH outside the Cygwin environment.
+        packages: python39=3.9.16-1 python39-pip python39-virtualenv git
 
     - name: Arrange for verbose output
       run: |
@@ -55,23 +54,28 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
+    - name: Ensure the "pip" command is available
+      run: |
+        # This is used unless, and before, an updated pip is installed.
+        ln -s pip3 /usr/bin/pip
+
     - name: Update PyPA packages
       run: |
-        # Get the latest pip, setuptools, and wheel.
-        python3.8 -m pip install -U pip setuptools wheel
+        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
+        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
-        python3.8 -m pip install ".[test]"
+        pip install ".[test]"
 
     - name: Show version and platform information
       run: |
         uname -a
-        command -v git python3.8
+        command -v git python
         git version
-        python3.8 --version
-        python3.8 -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
+        python --version
+        python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
 
     - name: Test with pytest
       run: |
-        python3.8 -m pytest --color=yes -p no:sugar --instafail -vv
+        pytest --color=yes -p no:sugar --instafail -vv

From 8dc8eb9e76a3162636856fa31b71ba10b33705eb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 2 Feb 2024 01:06:26 -0500
Subject: [PATCH 411/994] Test established zero-argument refresh() behavior

This adds tests like the existing ones but for when git.refresh is
called with no arguments and the path is provided as the value of
the GIT_PYTHON_GIT_EXECUTABLE environment variable.

The preexisting tests, which this retains unchanged except with
slightly more specific names to avoid confusion with the new tests,
are of git.refresh(path).

One benefit of these tests is to make a subtle but important aspect
of the established behavior clear: relative paths are immediately
resolved when passed as a path argument, but they are kept relative
when given as the value of the environment variable.
---
 test/test_git.py | 69 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 64 insertions(+), 5 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index a1c6211db..1b630fee0 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -46,9 +46,24 @@ def _patch_out_env(name):
 
 @contextlib.contextmanager
 def _rollback_refresh():
+    old_git_executable = Git.GIT_PYTHON_GIT_EXECUTABLE
+
+    if old_git_executable is None:
+        raise RuntimeError("no executable string (need initial refresh before test)")
+
     try:
-        yield Git.GIT_PYTHON_GIT_EXECUTABLE  # Provide the old value for convenience.
+        yield old_git_executable  # Provide the old value for convenience.
     finally:
+        # The cleanup refresh should always raise an exception if it fails, since if it
+        # fails then previously discovered test results could be misleading and, more
+        # importantly, subsequent tests may be unable to run or give misleading results.
+        # So pre-set a non-None value, so that the cleanup will be a "second" refresh.
+        # This covers cases where a test has set it to None to test a "first" refresh.
+        Git.GIT_PYTHON_GIT_EXECUTABLE = Git.git_exec_name
+
+        # Do the cleanup refresh. This sets Git.GIT_PYTHON_GIT_EXECUTABLE to old_value
+        # in most cases. The reason to call it is to achieve other associated state
+        # changes as well, which include updating attributes of the FetchInfo class.
         refresh()
 
 
@@ -314,7 +329,51 @@ def test_cmd_override(self):
         ):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
-    def test_refresh_bad_absolute_git_path(self):
+    def test_refresh_from_bad_absolute_git_path_env(self):
+        """Bad absolute path from environment is reported and not set."""
+        absolute_path = str(Path("yada").absolute())
+        expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
+
+        with _rollback_refresh() as old_git_executable:
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": absolute_path}):
+                with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+                    refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
+
+    def test_refresh_from_bad_relative_git_path_env(self):
+        """Bad relative path from environment is kept relative and reported, not set."""
+        # Relative paths are not resolved when refresh() is called with no arguments, so
+        # use a string that's very unlikely to be a command name found in a path lookup.
+        relative_path = "yada-e47e70c6-acbf-40f8-ad65-13af93c2195b"
+        expected_pattern = rf"\n[ \t]*cmdline: {re.escape(relative_path)}\Z"
+
+        with _rollback_refresh() as old_git_executable:
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": relative_path}):
+                with self.assertRaisesRegex(GitCommandNotFound, expected_pattern):
+                    refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
+
+    def test_refresh_from_good_absolute_git_path_env(self):
+        """Good absolute path from environment is set."""
+        absolute_path = shutil.which("git")
+
+        with _rollback_refresh():
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": absolute_path}):
+                refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
+
+    def test_refresh_from_good_relative_git_path_env(self):
+        """Good relative path from environment is kept relative and set."""
+        with _rollback_refresh():
+            # Set a string that wouldn't work and isn't "git".
+            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = ""
+
+            # Now observe if setting the environment variable to "git" takes effect.
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": "git"}):
+                refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
+
+    def test_refresh_with_bad_absolute_git_path_arg(self):
         """Bad absolute path arg is reported and not set."""
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
@@ -324,7 +383,7 @@ def test_refresh_bad_absolute_git_path(self):
                 refresh(absolute_path)
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
-    def test_refresh_bad_relative_git_path(self):
+    def test_refresh_with_bad_relative_git_path_arg(self):
         """Bad relative path arg is resolved to absolute path and reported, not set."""
         absolute_path = str(Path("yada").absolute())
         expected_pattern = rf"\n[ \t]*cmdline: {re.escape(absolute_path)}\Z"
@@ -334,7 +393,7 @@ def test_refresh_bad_relative_git_path(self):
                 refresh("yada")
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, old_git_executable)
 
-    def test_refresh_good_absolute_git_path(self):
+    def test_refresh_with_good_absolute_git_path_arg(self):
         """Good absolute path arg is set."""
         absolute_path = shutil.which("git")
 
@@ -342,7 +401,7 @@ def test_refresh_good_absolute_git_path(self):
             refresh(absolute_path)
             self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
-    def test_refresh_good_relative_git_path(self):
+    def test_refresh_with_good_relative_git_path_arg(self):
         """Good relative path arg is resolved to absolute path and set."""
         absolute_path = shutil.which("git")
         dirname, basename = osp.split(absolute_path)

From 147e80b88999669d6e499556e7dc083bdf84c62e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 2 Feb 2024 05:31:50 -0500
Subject: [PATCH 412/994] Test current initial-refresh behavior

This adds tests of the initial refresh that is attempted
automatically on import. All the refresh tests prior to this point
test subsequent refreshes. Those tests are kept, and new ones are
added that simulate the condition of not having yet done the
initial refresh by setting Git.GIT_PYTHON_GIT_EXECUTABLE to None.

Some current behavior these tests assert may change for #1808.
---
 test/test_git.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 76 insertions(+), 1 deletion(-)

diff --git a/test/test_git.py b/test/test_git.py
index 1b630fee0..ab068e5c2 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -6,6 +6,7 @@
 import contextlib
 import gc
 import inspect
+import io
 import logging
 import os
 import os.path as osp
@@ -329,6 +330,80 @@ def test_cmd_override(self):
         ):
             self.assertRaises(GitCommandNotFound, self.git.version)
 
+    def test_git_exc_name_is_git(self):
+        self.assertEqual(self.git.git_exec_name, "git")
+
+    @ddt.data(("0",), ("q",), ("quiet",), ("s",), ("silence",), ("n",), ("none",))
+    def test_initial_refresh_from_bad_git_path_env_quiet(self, case):
+        """In "q" mode, bad initial path sets "git" and is quiet."""
+        (mode,) = case
+        set_vars = {
+            "GIT_PYTHON_GIT_EXECUTABLE": str(Path("yada").absolute()),  # Any bad path.
+            "GIT_PYTHON_REFRESH": mode,
+        }
+        with _rollback_refresh():
+            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+
+            with mock.patch.dict(os.environ, set_vars):
+                refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
+
+    @ddt.data(("1",), ("w",), ("warn",), ("warning",))
+    def test_initial_refresh_from_bad_git_path_env_warn(self, case):
+        """In "w" mode, bad initial path sets "git" and warns."""
+        (mode,) = case
+        env_vars = {
+            "GIT_PYTHON_GIT_EXECUTABLE": str(Path("yada").absolute()),  # Any bad path.
+            "GIT_PYTHON_REFRESH": mode,
+        }
+        with _rollback_refresh():
+            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+
+            with mock.patch.dict(os.environ, env_vars):
+                with contextlib.redirect_stdout(io.StringIO()) as out:
+                    refresh()
+                self.assertRegex(out.getvalue(), r"\AWARNING: Bad git executable.\n")
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
+
+    @ddt.data(("2",), ("r",), ("raise",), ("e",), ("error",))
+    def test_initial_refresh_from_bad_git_path_env_error(self, case):
+        """In "e" mode, bad initial path raises an exception."""
+        (mode,) = case
+        env_vars = {
+            "GIT_PYTHON_GIT_EXECUTABLE": str(Path("yada").absolute()),  # Any bad path.
+            "GIT_PYTHON_REFRESH": mode,
+        }
+        with _rollback_refresh():
+            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+
+            with mock.patch.dict(os.environ, env_vars):
+                with self.assertRaisesRegex(ImportError, r"\ABad git executable.\n"):
+                    refresh()
+
+    def test_initial_refresh_from_good_absolute_git_path_env(self):
+        """Good initial absolute path from environment is set."""
+        absolute_path = shutil.which("git")
+
+        with _rollback_refresh():
+            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": absolute_path}):
+                refresh()
+                self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
+
+    def test_initial_refresh_from_good_relative_git_path_env(self):
+        """Good initial relative path from environment is kept relative and set."""
+        with _rollback_refresh():
+            # Set the fallback to a string that wouldn't work and isn't "git", so we are
+            # more likely to detect if "git" is not set from the environment variable.
+            with mock.patch.object(type(self.git), "git_exec_name", ""):
+                type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+
+                # Now observe if setting the environment variable to "git" takes effect.
+                with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": "git"}):
+                    refresh()
+                    self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
+
     def test_refresh_from_bad_absolute_git_path_env(self):
         """Bad absolute path from environment is reported and not set."""
         absolute_path = str(Path("yada").absolute())
@@ -365,7 +440,7 @@ def test_refresh_from_good_absolute_git_path_env(self):
     def test_refresh_from_good_relative_git_path_env(self):
         """Good relative path from environment is kept relative and set."""
         with _rollback_refresh():
-            # Set a string that wouldn't work and isn't "git".
+            # Set as the executable name a string that wouldn't work and isn't "git".
             type(self.git).GIT_PYTHON_GIT_EXECUTABLE = ""
 
             # Now observe if setting the environment variable to "git" takes effect.

From ac50d8e40a9362073033f28f9a69bcd04c62f378 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 2 Feb 2024 05:48:15 -0500
Subject: [PATCH 413/994] Change warning refresh-mode tests to expect logging

This is instead of the current behavior writing the message to
stdout.

This commit does not change the behavior of the code under test,
but it changes tests to assert the following:

- "Bad git executable" messages are logged, at level CRITICAL.
- "log" (and "l") is recognized as another synonym of "warn".
- "silent" is recognized as a synonym of "quiet" (as "silence" is).

Although it is ambiguous whether this should be logged at level
ERROR or CRITICAL, because git.refresh is still expected to be
usable and can be called manually, not having a working git is a
condition in which GitPython, and any program that really relies on
its functionality, should be expected not work. That is the general
rationale for using CRIICAL here. There are also two specific
reasons:

- Existing messages GitPython logs as ERROR typically represent
  errors in individual operations on repositories, which could fail
  without indicating that GitPython's overall functionality is in
  an unusable state. Using the higher CRITICAL level for this
  situation makes sense for contrast.

- Prior to #1813, logging messsges emitted from GitPython modules,
  no matter the level, were suppressed when logging was not
  configured, but because this message was printed instead of
  being logged, it was still shown. Now that it is to be logged,
  there may be a benefit to have an easy way for someone to bring
  back a close approximation of the old behavior. Having this
  message be at a higher logging level makes that easier to do.
  (This is a less important reason, as that should rarely be done.)

test_initial_refresh_from_bad_git_path_env_warn is the main changed
test. All tests should pass again once code is changed for #1808.
---
 test/test_git.py | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index ab068e5c2..ea3d067ee 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -6,7 +6,6 @@
 import contextlib
 import gc
 import inspect
-import io
 import logging
 import os
 import os.path as osp
@@ -333,7 +332,7 @@ def test_cmd_override(self):
     def test_git_exc_name_is_git(self):
         self.assertEqual(self.git.git_exec_name, "git")
 
-    @ddt.data(("0",), ("q",), ("quiet",), ("s",), ("silence",), ("n",), ("none",))
+    @ddt.data(("0",), ("q",), ("quiet",), ("s",), ("silence",), ("silent",), ("n",), ("none",))
     def test_initial_refresh_from_bad_git_path_env_quiet(self, case):
         """In "q" mode, bad initial path sets "git" and is quiet."""
         (mode,) = case
@@ -348,9 +347,9 @@ def test_initial_refresh_from_bad_git_path_env_quiet(self, case):
                 refresh()
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
 
-    @ddt.data(("1",), ("w",), ("warn",), ("warning",))
+    @ddt.data(("1",), ("w",), ("warn",), ("warning",), ("l",), ("log",))
     def test_initial_refresh_from_bad_git_path_env_warn(self, case):
-        """In "w" mode, bad initial path sets "git" and warns."""
+        """In "w" mode, bad initial path sets "git" and warns, by logging."""
         (mode,) = case
         env_vars = {
             "GIT_PYTHON_GIT_EXECUTABLE": str(Path("yada").absolute()),  # Any bad path.
@@ -360,9 +359,11 @@ def test_initial_refresh_from_bad_git_path_env_warn(self, case):
             type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
             with mock.patch.dict(os.environ, env_vars):
-                with contextlib.redirect_stdout(io.StringIO()) as out:
+                with self.assertLogs(cmd.__name__, logging.CRITICAL) as ctx:
                     refresh()
-                self.assertRegex(out.getvalue(), r"\AWARNING: Bad git executable.\n")
+                self.assertEqual(len(ctx.records), 1)
+                message = ctx.records[0].getMessage()
+                self.assertRegex(message, r"\AWARNING: Bad git executable.\n")
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
 
     @ddt.data(("2",), ("r",), ("raise",), ("e",), ("error",))

From 3a6e3efb3592748768314969998604e136384622 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 26 Jan 2024 04:02:18 -0500
Subject: [PATCH 414/994] Have initial refresh use a logger to warn

For #1808.

See ac50d8e for details on the changes.
---
 git/cmd.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index a75d822fa..22b5ea99e 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -409,15 +409,15 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 # expect GIT_PYTHON_REFRESH to either be unset or be one of the
                 # following values:
                 #
-                #   0|q|quiet|s|silence|n|none
-                #   1|w|warn|warning
-                #   2|r|raise|e|error
+                #   0|q|quiet|s|silence|silent|n|none
+                #   1|w|warn|warning|l|log
+                #   2|r|raise|e|error|exception
 
                 mode = os.environ.get(cls._refresh_env_var, "raise").lower()
 
-                quiet = ["quiet", "q", "silence", "s", "none", "n", "0"]
-                warn = ["warn", "w", "warning", "1"]
-                error = ["error", "e", "raise", "r", "2"]
+                quiet = ["quiet", "q", "silence", "s", "silent", "none", "n", "0"]
+                warn = ["warn", "w", "warning", "log", "l", "1"]
+                error = ["error", "e", "exception", "raise", "r", "2"]
 
                 if mode in quiet:
                     pass
@@ -428,10 +428,10 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                         %s
                         All git commands will error until this is rectified.
 
-                        This initial warning can be silenced or aggravated in the future by setting the
+                        This initial message can be silenced or aggravated in the future by setting the
                         $%s environment variable. Use one of the following values:
-                            - %s: for no warning or exception
-                            - %s: for a printed warning
+                            - %s: for no message or exception
+                            - %s: for a warning message (logged at level CRITICAL, displayed by default)
                             - %s: for a raised exception
 
                         Example:
@@ -450,7 +450,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                     )
 
                     if mode in warn:
-                        print("WARNING: %s" % err)
+                        _logger.critical("WARNING: %s", err)
                     else:
                         raise ImportError(err)
                 else:
@@ -460,8 +460,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                         %s environment variable has been set but it has been set with an invalid value.
 
                         Use only the following values:
-                            - %s: for no warning or exception
-                            - %s: for a printed warning
+                            - %s: for no message or exception
+                            - %s: for a warning message (logged at level CRITICAL, displayed by default)
                             - %s: for a raised exception
                         """
                         )

From 8342f8200aad284249a690334ef5a9f61cfed114 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 5 Feb 2024 22:52:44 -0500
Subject: [PATCH 415/994] Test with M1 macOS CI runner

This adds macos-14 as a value for "os". The new macOS 14 runners
use Apple Silicon M1 systems (64-bit ARM CPUs), allowing GitPython
to be tested on ARM64 actions/python-versions builds of Python,
such as python-3.12.1-darwin-arm64.tar.gz.

https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/
https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/

This commit doesn't exclude any `os`/`python-version` combinations,
even though not all versions of Python that GitPython supports are
currently available via the setup-python (and 3.7 builds are very
unlikely ever to be available for macos-14). This is to verify that
the currently unsupported versions are 3.7, 3.8, 3.9, and no
others. The next commit will exclude the unavailable versions.
---
 .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 08ff4efdf..758b41427 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "macos-13", "windows-latest"]
+        os: ["ubuntu-latest", "macos-13", "macos-14", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false

From 9ad28c3d2dc7f7355836198a3848ff430aaaa382 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 5 Feb 2024 23:06:50 -0500
Subject: [PATCH 416/994] Exclude unavailable Python versions for M1 runner

Some versions of Python do not currently have builds available via
actions/setup-python for the Apple Silicon M1 (64-bit ARM) runner
used for macos-14. This excludes those.

- Python 3.7 is EoL and builds for it are no likely to be provided
  for newly available platforms on GitHub Actions.

- Python 3.8 and 3.9 are still supported by the Python Software
  Foundation. Builds for them are not currently avaialable on the
  GHA M1 runners, but it appears this may not be intentional. See
  https://github.com/actions/setup-python/issues/808. If those
  versions become available, then they can be reenabled.

- Later versions of Python are available.
---
 .github/workflows/pythonpackage.yml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 758b41427..595c031fc 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -15,6 +15,13 @@ jobs:
       matrix:
         os: ["ubuntu-latest", "macos-13", "macos-14", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        exclude:
+        - os: "macos-14"
+          python-version: "3.7"
+        - os: "macos-14"
+          python-version: "3.8"
+        - os: "macos-14"
+          python-version: "3.9"
         include:
         - experimental: false
 

From 85ef145b06e6eb164bc469f72b96a917c567c85a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 6 Feb 2024 01:01:50 -0500
Subject: [PATCH 417/994] Extend test_cmd_override to test exception's
 `command` attribute

---
 test/test_git.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index ea3d067ee..1148ccfd8 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -321,17 +321,17 @@ def test_version(self):
             self.assertIsInstance(n, int)
         # END verify number types
 
-    def test_cmd_override(self):
-        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_git_exc_name_is_git(self):
         self.assertEqual(self.git.git_exec_name, "git")
 
+    def test_cmd_override(self):
+        """Directly set bad GIT_PYTHON_GIT_EXECUTABLE causes git operations to raise."""
+        bad_path = osp.join("some", "path", "which", "doesn't", "exist", "gitbinary")
+        with mock.patch.object(type(self.git), "GIT_PYTHON_GIT_EXECUTABLE", bad_path):
+            with self.assertRaises(GitCommandNotFound) as ctx:
+                self.git.version()
+            self.assertEqual(ctx.exception.command, [bad_path, "version"])
+
     @ddt.data(("0",), ("q",), ("quiet",), ("s",), ("silence",), ("silent",), ("n",), ("none",))
     def test_initial_refresh_from_bad_git_path_env_quiet(self, case):
         """In "q" mode, bad initial path sets "git" and is quiet."""

From db361743a52455ce064d2fbcb766ec9979960d5a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 6 Feb 2024 01:05:07 -0500
Subject: [PATCH 418/994] Write Git instead of type(self.git)

Some tests in test_git.TestGit used type(self.git) while others
used Git. This changes them all to use Git. The distinction could
be relevant if self.git were set as a mock, but this is not being
done, and if it were, it is not obvious which would be preferred.
The intention of the existing tests is to test attributes of the
Git class, so this picks that simpler expression.
---
 test/test_git.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 1148ccfd8..b02a92b0f 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -327,7 +327,7 @@ def test_git_exc_name_is_git(self):
     def test_cmd_override(self):
         """Directly set bad GIT_PYTHON_GIT_EXECUTABLE causes git operations to raise."""
         bad_path = osp.join("some", "path", "which", "doesn't", "exist", "gitbinary")
-        with mock.patch.object(type(self.git), "GIT_PYTHON_GIT_EXECUTABLE", bad_path):
+        with mock.patch.object(Git, "GIT_PYTHON_GIT_EXECUTABLE", bad_path):
             with self.assertRaises(GitCommandNotFound) as ctx:
                 self.git.version()
             self.assertEqual(ctx.exception.command, [bad_path, "version"])
@@ -341,7 +341,7 @@ def test_initial_refresh_from_bad_git_path_env_quiet(self, case):
             "GIT_PYTHON_REFRESH": mode,
         }
         with _rollback_refresh():
-            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+            Git.GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
             with mock.patch.dict(os.environ, set_vars):
                 refresh()
@@ -356,7 +356,7 @@ def test_initial_refresh_from_bad_git_path_env_warn(self, case):
             "GIT_PYTHON_REFRESH": mode,
         }
         with _rollback_refresh():
-            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+            Git.GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
             with mock.patch.dict(os.environ, env_vars):
                 with self.assertLogs(cmd.__name__, logging.CRITICAL) as ctx:
@@ -375,7 +375,7 @@ def test_initial_refresh_from_bad_git_path_env_error(self, case):
             "GIT_PYTHON_REFRESH": mode,
         }
         with _rollback_refresh():
-            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+            Git.GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
             with mock.patch.dict(os.environ, env_vars):
                 with self.assertRaisesRegex(ImportError, r"\ABad git executable.\n"):
@@ -386,7 +386,7 @@ def test_initial_refresh_from_good_absolute_git_path_env(self):
         absolute_path = shutil.which("git")
 
         with _rollback_refresh():
-            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+            Git.GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
             with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": absolute_path}):
                 refresh()
@@ -397,8 +397,8 @@ def test_initial_refresh_from_good_relative_git_path_env(self):
         with _rollback_refresh():
             # Set the fallback to a string that wouldn't work and isn't "git", so we are
             # more likely to detect if "git" is not set from the environment variable.
-            with mock.patch.object(type(self.git), "git_exec_name", ""):
-                type(self.git).GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
+            with mock.patch.object(Git, "git_exec_name", ""):
+                Git.GIT_PYTHON_GIT_EXECUTABLE = None  # Simulate startup.
 
                 # Now observe if setting the environment variable to "git" takes effect.
                 with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": "git"}):
@@ -442,7 +442,7 @@ def test_refresh_from_good_relative_git_path_env(self):
         """Good relative path from environment is kept relative and set."""
         with _rollback_refresh():
             # Set as the executable name a string that wouldn't work and isn't "git".
-            type(self.git).GIT_PYTHON_GIT_EXECUTABLE = ""
+            Git.GIT_PYTHON_GIT_EXECUTABLE = ""
 
             # Now observe if setting the environment variable to "git" takes effect.
             with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": "git"}):

From 3250313d95e37040327c4d8620d03ec4ffc494df Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 6 Feb 2024 01:57:53 -0500
Subject: [PATCH 419/994] Test that extra prefix "WARNING:" is omitted

This changes test_initial_refresh_from_bad_git_path_env_warn to
assert a message with no added "WARNING:" prefix. As discussed
in #1815, the extra prefix is confusing with logging configured,
showing "CRITICAL:git.cmd:WARNING: Bad git executable." instead of
"CRITICAL:git.cmd: Bad git executable."
---
 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 b02a92b0f..f12428f9e 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -363,7 +363,7 @@ def test_initial_refresh_from_bad_git_path_env_warn(self, case):
                     refresh()
                 self.assertEqual(len(ctx.records), 1)
                 message = ctx.records[0].getMessage()
-                self.assertRegex(message, r"\AWARNING: Bad git executable.\n")
+                self.assertRegex(message, r"\ABad git executable.\n")
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, "git")
 
     @ddt.data(("2",), ("r",), ("raise",), ("e",), ("error",))

From 5faf62107a44c301e7d42a299926fc707f7299f2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 6 Feb 2024 02:04:27 -0500
Subject: [PATCH 420/994] Omit extra "WARNING:" prefix

As tested for in 3250313. This makes the test pass.
---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 22b5ea99e..f58e6df5b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -450,7 +450,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                     )
 
                     if mode in warn:
-                        _logger.critical("WARNING: %s", err)
+                        _logger.critical(err)
                     else:
                         raise ImportError(err)
                 else:

From 4b86993e029acf0bb7b36a4b5e40a588bdab6658 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 6 Feb 2024 02:07:14 -0500
Subject: [PATCH 421/994] Use same code style for all logging without
 placeholders

When logging with only a msg argument, it is a full literal message
rather than a format string (as it is when there are placeholders).
Thus both `...("%s", text)` and `...(text)`, where `...` is a
logging method or function, are equally good code styles, provided
`text` really is known to behave the same as `str(text)`.

The latter style, `...(text)`, was used in all logging calls, both
in the git module and in the test suite, except one. This changes
the one outlier from `...("%s", text)` to `...(text)` for stylistic
consistency and to avoid giving the false impression that there is
something special about that call.
---
 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 f456f8be0..ffe71450d 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -174,7 +174,7 @@ def _decode(stdout):
         except UnicodeDecodeError:
             pass
         except LookupError as error:
-            _logger.warning("%s", str(error))  # Message already says "Unknown encoding:".
+            _logger.warning(str(error))  # Message already says "Unknown encoding:".
 
         # Assume UTF-8. If invalid, substitute Unicode replacement characters.
         return stdout.decode("utf-8", errors="replace")

From 1ed6366df35246c87d9b464d6e9068444efdbddf Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 8 Feb 2024 09:10:26 +0100
Subject: [PATCH 422/994] Add note to clarify that `gitoxide` doensn't work in
 Python just yet.

Related to https://github.com/Byron/gitoxide/discussions/1279
---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 0e020a5fe..1137e894a 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@ probably the skills to scratch that itch of mine: implement `git` in a way that
 If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
 implementation of 'git' in [Rust](https://www.rust-lang.org).
 
+*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required)*
+
 ## GitPython
 
 GitPython is a python library used to interact with git repositories, high-level like git-porcelain,

From e75ea98e6c135c436e354537bf45f8192f3c2c63 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 12 Feb 2024 13:40:41 +0000
Subject: [PATCH 423/994] Bump pre-commit/action from 3.0.0 to 3.0.1

Bumps [pre-commit/action](https://github.com/pre-commit/action) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/pre-commit/action/releases)
- [Commits](https://github.com/pre-commit/action/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: pre-commit/action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .github/workflows/lint.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index f9c5b70b3..d805124fc 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,7 +13,7 @@ jobs:
       with:
         python-version: "3.x"
 
-    - uses: pre-commit/action@v3.0.0
+    - uses: pre-commit/action@v3.0.1
       with:
         extra_args: --all-files --hook-stage manual
       env:

From 7ba3fd2a4bd7a384d105ab0a1e4909940a92cda2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 12 Feb 2024 13:40:45 +0000
Subject: [PATCH 424/994] Bump Vampire/setup-wsl from 2.0.2 to 3.0.0

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 2.0.2 to 3.0.0.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v2.0.2...v3.0.0)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 595c031fc..6b89530c3 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -44,7 +44,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v2.0.2
+      uses: Vampire/setup-wsl@v3.0.0
       with:
         distribution: Debian
 

From 26711678ca02ada0dce4c8e1f008bdaa58dfcdc9 Mon Sep 17 00:00:00 2001
From: marcm-ml <88182556+marcm-ml@users.noreply.github.com>
Date: Thu, 15 Feb 2024 10:17:59 +0100
Subject: [PATCH 425/994] Remove deprecated section in README.md

Removed deprecated `How to verify a Realse`section in README.md
---
 README.md | 51 ---------------------------------------------------
 1 file changed, 51 deletions(-)

diff --git a/README.md b/README.md
index 1137e894a..7faeae23b 100644
--- a/README.md
+++ b/README.md
@@ -222,57 +222,6 @@ Please have a look at the [contributions file][contributing].
 6. Run `make release`.
 7. 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)
-
-Note that what follows is deprecated and future releases won't be signed anymore.
-More details about how it came to that can be found [in this issue](https://github.com/gitpython-developers/gitdb/issues/77).
-
-----
-
-Please only use releases from `pypi` as you can verify the respective source
-tarballs.
-
-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
-```
-
-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) <byronimo@gmail.com>" [ultimate]
-gpg:                 aka "Sebastian Thiel (In Rust I trust) <sebastian.thiel@icloud.com>" [ultimate]
-```
-
-You can verify that the keyid indeed matches the release-signature key provided in this
-repository by looking at the keys details:
-
-```bash
-gpg --list-packets ./release-verification-key.asc
-```
-
-You can verify that the commit adding it was also signed by it using:
-
-```bash
-git show --show-signature  ./release-verification-key.asc
-```
-
-If you would like to trust it permanently, you can import and sign it:
-
-```bash
-gpg --import ./release-verification-key.asc
-gpg --edit-key 4C08421980C9
-
-> sign
-> save
-```
-
 ### Projects using GitPython
 
 - [PyDriller](https://github.com/ishepard/pydriller)

From dd42e38f5201ff6858c6b787fa8dab16c6cbc7a9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Feb 2024 09:35:15 -0500
Subject: [PATCH 426/994] Keep temp files out of project dir and improve
 cleanup

This fixes a test bug in TestTree.test_tree_modifier_ordering
where:

- A `tmp` subdirectory of the directory the tests are run from
  (almost always the root of the GitPython source tree) was
  created, instead of creating it in /tmp or elsewhere configured.

- That directory was never deleted afterward, creating new
  untracked files in GitPython's own working tree, and also making
  it so running the tests twice would always fail the second time,
  unless a manual intervening deletion was performed. (This had not
  broken CI, since CI checks clone the repository anew each time.)

- The directory was changed into to set up the test, but not
  deterministically changed back out of. It would typically be
  exited, but this was not guaranteed to occur if some subprocess
  commands were to fail unexpectedly.

In addition to fixing all three aspects of that bug, this also:

- Remains in the temporary directory only as long as necessary to
  execute the subprocesses in it that produce the expected value
  for the test (including not entering it until running them).

- Deletes the temporary directory immediately after exiting it,
  since it is only used to produce a file list to compare to.

- Avoids interleaving file creation and git subprocess commands,
  since doing so made it harder to understand what was being set up
  and since the difference is not relevant to the test.

- Does the work in that temporary directory before performing any
  operations with the code under test, because it is conceptually
  an "arrange" step, and because doing so makes its limited purpose
  clearer on reading the tests.

- Some other refactoring to support the fixes and accompanying
  changes, including extracting the temporary directory logic to a
  helper method.

This deliberately does not change the order in which any files are
created. It also keeps the approach of using subprocess functions
directly to operate on the temporary git repository (and changing
directory while doing so, keeping each of the commands the same),
since there might be good reasons to do this, such as to make very
clear that the file order being obtained to compare to is really
coming from git itself. Even if this is to be changed, it seems
outside the scope of this bugfix.

The test still fails if the fix to git/objects/tree.py from 365d44f
(#1799) is absent. This was verified by running:

    git revert --no-commit 365d44f50a3d72d7ebfa063b142d2abd4082cfaa
    pytest --no-cov -vv test/test_tree.py
    git revert --abort
---
 test/test_tree.py | 54 ++++++++++++++++++++++++++++-------------------
 1 file changed, 32 insertions(+), 22 deletions(-)

diff --git a/test/test_tree.py b/test/test_tree.py
index 695cb803e..b44392e15 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -4,14 +4,15 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from io import BytesIO
+import os.path as osp
+from pathlib import Path
+import subprocess
+import tempfile
 
 from git.objects import Tree, Blob
+from git.util import cwd
 from test.lib import TestBase
 
-import os
-import os.path as osp
-import subprocess
-
 
 class TestTree(TestBase):
     def test_serializable(self):
@@ -42,29 +43,41 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
-    def test_tree_modifier_ordering(self):
-        def setup_git_repository_and_get_ordered_files():
-            os.mkdir("tmp")
-            os.chdir("tmp")
-            subprocess.run(["git", "init", "-q"], check=True)
-            os.mkdir("file")
-            for filename in [
+    @staticmethod
+    def _get_git_ordered_files():
+        """Get files as git orders them, to compare in test_tree_modifier_ordering."""
+        with tempfile.TemporaryDirectory() as tdir:
+            # Create directory contents.
+            Path(tdir, "file").mkdir()
+            for filename in (
                 "bin",
                 "bin.d",
                 "file.to",
                 "file.toml",
                 "file.toml.bin",
                 "file0",
-                "file/a",
-            ]:
-                open(filename, "a").close()
-
-            subprocess.run(["git", "add", "."], check=True)
-            subprocess.run(["git", "commit", "-m", "c1"], check=True)
-            tree_hash = subprocess.check_output(["git", "rev-parse", "HEAD^{tree}"]).decode().strip()
-            cat_file_output = subprocess.check_output(["git", "cat-file", "-p", tree_hash]).decode()
+            ):
+                Path(tdir, filename).touch()
+            Path(tdir, "file", "a").touch()
+
+            with cwd(tdir):
+                # Prepare the repository.
+                subprocess.run(["git", "init", "-q"], check=True)
+                subprocess.run(["git", "add", "."], check=True)
+                subprocess.run(["git", "commit", "-m", "c1"], check=True)
+
+                # Get git output from which an ordered file list can be parsed.
+                rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
+                tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
+                cat_file_command = ["git", "cat-file", "-p", tree_hash]
+                cat_file_output = subprocess.check_output(cat_file_command).decode()
+
             return [line.split()[-1] for line in cat_file_output.split("\n") if line]
 
+    def test_tree_modifier_ordering(self):
+        """TreeModifier.set_done() sorts files in the same order git does."""
+        git_file_names_in_order = self._get_git_ordered_files()
+
         hexsha = "6c1faef799095f3990e9970bc2cb10aa0221cf9c"
         roottree = self.rorepo.tree(hexsha)
         blob_mode = Tree.blob_id << 12
@@ -92,9 +105,6 @@ def names_in_mod_cache():
             here = file_names_in_order()
             return [e for e in a if e in here]
 
-        git_file_names_in_order = setup_git_repository_and_get_ordered_files()
-        os.chdir("..")
-
         mod.set_done()
         assert names_in_mod_cache() == git_file_names_in_order, "set_done() performs git-sorting"
 

From 90cf4d75c5ebcbdedae24a6cd26cc1e7dd3e3c08 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Feb 2024 10:36:26 -0500
Subject: [PATCH 427/994] Fix new PermissionError in Windows with Python 3.7

In Python 3.7, tempfile.TemporaryDirectory doesn't delete files on
cleanup whose permissions have to be changed to be deleted.

This uses `@with_rw_directory` instead, though that may be overkill
here.
---
 test/test_tree.py | 60 +++++++++++++++++++++++------------------------
 1 file changed, 29 insertions(+), 31 deletions(-)

diff --git a/test/test_tree.py b/test/test_tree.py
index b44392e15..0c06b950c 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -7,11 +7,10 @@
 import os.path as osp
 from pathlib import Path
 import subprocess
-import tempfile
 
 from git.objects import Tree, Blob
 from git.util import cwd
-from test.lib import TestBase
+from test.lib import TestBase, with_rw_directory
 
 
 class TestTree(TestBase):
@@ -43,36 +42,35 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
-    @staticmethod
-    def _get_git_ordered_files():
+    @with_rw_directory
+    def _get_git_ordered_files(self, rw_dir):
         """Get files as git orders them, to compare in test_tree_modifier_ordering."""
-        with tempfile.TemporaryDirectory() as tdir:
-            # Create directory contents.
-            Path(tdir, "file").mkdir()
-            for filename in (
-                "bin",
-                "bin.d",
-                "file.to",
-                "file.toml",
-                "file.toml.bin",
-                "file0",
-            ):
-                Path(tdir, filename).touch()
-            Path(tdir, "file", "a").touch()
-
-            with cwd(tdir):
-                # Prepare the repository.
-                subprocess.run(["git", "init", "-q"], check=True)
-                subprocess.run(["git", "add", "."], check=True)
-                subprocess.run(["git", "commit", "-m", "c1"], check=True)
-
-                # Get git output from which an ordered file list can be parsed.
-                rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
-                tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
-                cat_file_command = ["git", "cat-file", "-p", tree_hash]
-                cat_file_output = subprocess.check_output(cat_file_command).decode()
-
-            return [line.split()[-1] for line in cat_file_output.split("\n") if line]
+        # Create directory contents.
+        Path(rw_dir, "file").mkdir()
+        for filename in (
+            "bin",
+            "bin.d",
+            "file.to",
+            "file.toml",
+            "file.toml.bin",
+            "file0",
+        ):
+            Path(rw_dir, filename).touch()
+        Path(rw_dir, "file", "a").touch()
+
+        with cwd(rw_dir):
+            # Prepare the repository.
+            subprocess.run(["git", "init", "-q"], check=True)
+            subprocess.run(["git", "add", "."], check=True)
+            subprocess.run(["git", "commit", "-m", "c1"], check=True)
+
+            # Get git output from which an ordered file list can be parsed.
+            rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
+            tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
+            cat_file_command = ["git", "cat-file", "-p", tree_hash]
+            cat_file_output = subprocess.check_output(cat_file_command).decode()
+
+        return [line.split()[-1] for line in cat_file_output.split("\n") if line]
 
     def test_tree_modifier_ordering(self):
         """TreeModifier.set_done() sorts files in the same order git does."""

From 0114a997bf56eec86f217f99553c859613f1c9a4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Feb 2024 11:22:24 -0500
Subject: [PATCH 428/994] Use more ligtweight approach to guarantee deletion

This changes from `@with_rw_directory` back to TemporaryDirectory,
but calls git.util.rmtree on the repsitory's .git directory where
some read-only files otherwise cause TemporaryDirectory's cleanup
to raise PermissionError on Windows in Python 3.7.

This avoids the gc.collect that is known not to be necessary in
this speciifc situation, as well as the problem that, if operating
in the temporary directory did fail, then name of the helper would
be logged as the name of the test where the failure occurred. But
this has the disadvantage of making the helper more complex and
harder to understand. So this may not be the best approach either.
---
 test/test_tree.py | 65 +++++++++++++++++++++++++----------------------
 1 file changed, 35 insertions(+), 30 deletions(-)

diff --git a/test/test_tree.py b/test/test_tree.py
index 0c06b950c..371e3c625 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -7,10 +7,11 @@
 import os.path as osp
 from pathlib import Path
 import subprocess
+import tempfile
 
 from git.objects import Tree, Blob
-from git.util import cwd
-from test.lib import TestBase, with_rw_directory
+from git.util import cwd, rmtree
+from test.lib import TestBase
 
 
 class TestTree(TestBase):
@@ -42,35 +43,39 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
-    @with_rw_directory
-    def _get_git_ordered_files(self, rw_dir):
+    @staticmethod
+    def _get_git_ordered_files():
         """Get files as git orders them, to compare in test_tree_modifier_ordering."""
-        # Create directory contents.
-        Path(rw_dir, "file").mkdir()
-        for filename in (
-            "bin",
-            "bin.d",
-            "file.to",
-            "file.toml",
-            "file.toml.bin",
-            "file0",
-        ):
-            Path(rw_dir, filename).touch()
-        Path(rw_dir, "file", "a").touch()
-
-        with cwd(rw_dir):
-            # Prepare the repository.
-            subprocess.run(["git", "init", "-q"], check=True)
-            subprocess.run(["git", "add", "."], check=True)
-            subprocess.run(["git", "commit", "-m", "c1"], check=True)
-
-            # Get git output from which an ordered file list can be parsed.
-            rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
-            tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
-            cat_file_command = ["git", "cat-file", "-p", tree_hash]
-            cat_file_output = subprocess.check_output(cat_file_command).decode()
-
-        return [line.split()[-1] for line in cat_file_output.split("\n") if line]
+        with tempfile.TemporaryDirectory() as tdir:
+            # Create directory contents.
+            Path(tdir, "file").mkdir()
+            for filename in (
+                "bin",
+                "bin.d",
+                "file.to",
+                "file.toml",
+                "file.toml.bin",
+                "file0",
+            ):
+                Path(tdir, filename).touch()
+            Path(tdir, "file", "a").touch()
+
+            try:
+                with cwd(tdir):
+                    # Prepare the repository.
+                    subprocess.run(["git", "init", "-q"], check=True)
+                    subprocess.run(["git", "add", "."], check=True)
+                    subprocess.run(["git", "commit", "-m", "c1"], check=True)
+
+                    # Get git output from which an ordered file list can be parsed.
+                    rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
+                    tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
+                    cat_file_command = ["git", "cat-file", "-p", tree_hash]
+                    cat_file_output = subprocess.check_output(cat_file_command).decode()
+            finally:
+                rmtree(Path(tdir, ".git"))
+
+            return [line.split()[-1] for line in cat_file_output.split("\n") if line]
 
     def test_tree_modifier_ordering(self):
         """TreeModifier.set_done() sorts files in the same order git does."""

From b780a8c3a119c94b4c41f3c6f922ced686be212f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Feb 2024 11:47:08 -0500
Subject: [PATCH 429/994] Tweak `@with_rw_directory` and go back to using it

The situation with test_tree_modifier_ordering's helper, where it
makes sense to wrap the helper itself with `@with_rw_directory`
while still deleting the temporary directory as soon as the helper
itself finishes, in order to make clear that the test itself does
not use the temporary directory that the helper used, only occurs
in one place in GitPython's tests as far as I know.

In particular, all other places where `@with_rw_directory` was
actually in use are test methods, not helper methods, and the way
the decorator would have its wrapper log on failure documented the
decorated method as a test. Mainly for that reason, I had backed
away from using `@with_rw_directory` here.

But the test code seems much easier to understand when it is used
compared to other approaches. While it also has the disadvantage of
doing a gc.collect that is unnecessary here, this is not the only
place what that is done.

This commit brings back the test_tree_modifier_ordering helper
implementation that uses `@with_rw_directory`, while also modifying
the logging logic in `@with_rw_directory` slightly, so that when
the decorated method is not named as a test, it is referred to as a
"Helper" rather than a "Test" in the failure log message.
---
 test/lib/helper.py |  3 ++-
 test/test_tree.py  | 65 +++++++++++++++++++++-------------------------
 2 files changed, 32 insertions(+), 36 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index e6784e51c..f951a6a12 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -97,7 +97,8 @@ def wrapper(self, *args, **kwargs):
             return func(self, path, *args, **kwargs)
         except Exception:
             _logger.info(
-                "Test %s.%s failed, output is at %r\n",
+                "%s %s.%s failed, output is at %r\n",
+                "Test" if func.__name__.startswith("test_") else "Helper",
                 type(self).__name__,
                 func.__name__,
                 path,
diff --git a/test/test_tree.py b/test/test_tree.py
index 371e3c625..0c06b950c 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -7,11 +7,10 @@
 import os.path as osp
 from pathlib import Path
 import subprocess
-import tempfile
 
 from git.objects import Tree, Blob
-from git.util import cwd, rmtree
-from test.lib import TestBase
+from git.util import cwd
+from test.lib import TestBase, with_rw_directory
 
 
 class TestTree(TestBase):
@@ -43,39 +42,35 @@ def test_serializable(self):
             testtree._deserialize(stream)
         # END for each item in tree
 
-    @staticmethod
-    def _get_git_ordered_files():
+    @with_rw_directory
+    def _get_git_ordered_files(self, rw_dir):
         """Get files as git orders them, to compare in test_tree_modifier_ordering."""
-        with tempfile.TemporaryDirectory() as tdir:
-            # Create directory contents.
-            Path(tdir, "file").mkdir()
-            for filename in (
-                "bin",
-                "bin.d",
-                "file.to",
-                "file.toml",
-                "file.toml.bin",
-                "file0",
-            ):
-                Path(tdir, filename).touch()
-            Path(tdir, "file", "a").touch()
-
-            try:
-                with cwd(tdir):
-                    # Prepare the repository.
-                    subprocess.run(["git", "init", "-q"], check=True)
-                    subprocess.run(["git", "add", "."], check=True)
-                    subprocess.run(["git", "commit", "-m", "c1"], check=True)
-
-                    # Get git output from which an ordered file list can be parsed.
-                    rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
-                    tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
-                    cat_file_command = ["git", "cat-file", "-p", tree_hash]
-                    cat_file_output = subprocess.check_output(cat_file_command).decode()
-            finally:
-                rmtree(Path(tdir, ".git"))
-
-            return [line.split()[-1] for line in cat_file_output.split("\n") if line]
+        # Create directory contents.
+        Path(rw_dir, "file").mkdir()
+        for filename in (
+            "bin",
+            "bin.d",
+            "file.to",
+            "file.toml",
+            "file.toml.bin",
+            "file0",
+        ):
+            Path(rw_dir, filename).touch()
+        Path(rw_dir, "file", "a").touch()
+
+        with cwd(rw_dir):
+            # Prepare the repository.
+            subprocess.run(["git", "init", "-q"], check=True)
+            subprocess.run(["git", "add", "."], check=True)
+            subprocess.run(["git", "commit", "-m", "c1"], check=True)
+
+            # Get git output from which an ordered file list can be parsed.
+            rev_parse_command = ["git", "rev-parse", "HEAD^{tree}"]
+            tree_hash = subprocess.check_output(rev_parse_command).decode().strip()
+            cat_file_command = ["git", "cat-file", "-p", tree_hash]
+            cat_file_output = subprocess.check_output(cat_file_command).decode()
+
+        return [line.split()[-1] for line in cat_file_output.split("\n") if line]
 
     def test_tree_modifier_ordering(self):
         """TreeModifier.set_done() sorts files in the same order git does."""

From 1f37b482edbe4a7189e5898309a4094f3f4cf404 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 15 Feb 2024 21:11:38 +0100
Subject: [PATCH 430/994] prepare the next release

---
 VERSION                | 2 +-
 doc/source/changes.rst | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/VERSION b/VERSION
index 6c105e676..f69b7f25e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.41
+3.1.42
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index d7bf1731d..c75e5eded 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,12 @@
 Changelog
 =========
 
+3.1.42
+======
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.42
+
 3.1.41
 ======
 

From 44b856268d7c8259e4cce0d5ba560130201610c9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 8 Feb 2024 03:51:45 -0500
Subject: [PATCH 431/994] Test Alpine Linux on CI

With only one version of Python, currently 3.11.
---
 .github/workflows/alpine-test.yml | 56 +++++++++++++++++++++++++++++++
 1 file changed, 56 insertions(+)
 create mode 100644 .github/workflows/alpine-test.yml

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
new file mode 100644
index 000000000..991a8026b
--- /dev/null
+++ b/.github/workflows/alpine-test.yml
@@ -0,0 +1,56 @@
+name: test-alpine
+
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    container:
+      image: alpine:latest
+
+    defaults:
+      run:
+        shell: sh -exo pipefail {0}
+
+    steps:
+    - name: Install Alpine Linux packages
+      run: |
+        apk add git git-daemon python3 py3-pip
+
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+
+    - name: Prepare this repo for tests
+      run: |
+        ./init-tests-after-clone.sh
+
+    - name: Set git user identity and command aliases for the tests
+      run: |
+        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: |
+        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
+        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
+
+    - name: Install project and test dependencies
+      run: |
+        pip install ".[test]"
+
+    - name: Show version and platform information
+      run: |
+        uname -a
+        command -v git python
+        git version
+        python --version
+        python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
+
+    - name: Test with pytest
+      run: |
+        pytest --color=yes -p no:sugar --instafail -vv

From cefb53e32e814eb120b0e7d0405f7baa7bf8d8f1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 8 Feb 2024 04:12:37 -0500
Subject: [PATCH 432/994] Work around different ownership in container

This handles the "dubious ownership" error for the Alpine Linux
container using safe.directory as is done in the Cygwin job.

Another approach may be to actually use a limited user account in
the container, though, and that may be better, since I expect some
of the rmtree tests in test_util.py to fail due to the root user
being able to perform a delete operation the tests assume cannot be
done.
---
 .github/workflows/alpine-test.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 991a8026b..6de18d306 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -22,6 +22,10 @@ jobs:
       with:
         fetch-depth: 0
 
+    - name: Special configuration for Alpine Linux git
+      run: |
+        git config --global --add safe.directory "$(pwd)"
+
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From a45d0b07509b40ac0701ba72d9d892c46b9ec386 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 13 Feb 2024 18:31:24 -0500
Subject: [PATCH 433/994] Use venv on Alpine Linux

To overcome "This environment is externally managed" blocker.
---
 .github/workflows/alpine-test.yml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 6de18d306..46b5f139d 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -38,17 +38,24 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
+    - name: Create Python 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 $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
+        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
+        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -57,4 +64,5 @@ jobs:
 
     - name: Test with pytest
       run: |
+        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv

From 5de954a35d2953ca873eacaa0ce63525f7134914 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 09:21:38 -0500
Subject: [PATCH 434/994] Run tests as non-root user in Alpine Linux

- Add a note to the fixture in test_util.py that its ability to
  create files where rmtree will fail is contingent on not running
  as root (since root doesn't need to own a dir to delete from it).

- Create a non-root user in the container. Give it the same UID as
  owns the repository files that are shared with the container.
  Also create a group with the GID of the repository files that
  are shared with the container and add the user to the group,
  though that is less important. Actually creating the user ensures
  it has a home directory and may help some commands work. Passing
  `options: --user 1001` under `container:` will not work because,
  even if we didn't make the user, the `apk add` commands still
  need to run as root.

- Run all commands as the new non-root user, except for the system
  administration commands that install needed apk packages and set
  up the new non-root user account. To continue clearly expressing
  each step separately and have them automatically run in the
  container, this uses the hacky approach of having the default
  shell be a "sudo" command that runs the script step with "sh"
  (and passes the desired shell arguments).

- Preserve environment variables that may have been set by or for
  the GHA runner, in commands that run as the non-root user. That
  is, pass those through, while still removing/resetting others.
  If this is not done, then the variables such as `CI`, which the
  init-tests-after-clone.sh uses to proceed without interactive
  confirmation, will not be set, and that step will fail. However,
  I think it is also a good idea to do this, which is why I've
  included all the relevant variables and not just `CI`.

- Now that a non-root user is using "pip", stop using a venv, at
  least for now. The other test jobs don't use one, since the
  runners are isolated, and a container on a runner is even more
  isolated. But it may be best to bring the venv back, maybe even
  on the other test jobs, or alternatively to use "python -m pip"
  instead of "pip", to ensure expected version of pip is used.

- Don't add safe.directory inside the container, in the hope that
  this may not be necessary because the cloned repository files
  should have the same UID (and even GID) as the user using them.
  But I expect this may need to be put back; it seems to be needed
  separately from that, as actions/checkout automatically attempts
  it for the git command it finds and attempts to use.

This is not the only approach that could work. Another approach is
to make use of the container explicit in each step, rather than
using the `container` key. I think that would make the relationship
between the commands here and in other test workflows less apparent
and make the workflow a bit less clear, but it could also simplify
things. A third approach is to create an image with the needed apk
packages and user account, which switches to that user, by writing
a Dockerfile and building in image, producing it in a previous
job and sharing the image with the job that runs the tests so that
`container` can still be used. That might be ideal if it could be
done with upload-artifact and download-artifact, but I think
`container` only supports getting images from registries.
---
 .github/workflows/alpine-test.yml | 23 ++++++++---------------
 test/test_util.py                 |  4 +++-
 2 files changed, 11 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 46b5f139d..cc0db7e20 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -11,21 +11,22 @@ jobs:
 
     defaults:
       run:
-        shell: sh -exo pipefail {0}
+        shell: sudo -u runner sh -exo pipefail {0}
 
     steps:
-    - name: Install Alpine Linux packages
+    - name: Prepare Alpine Linux
       run: |
-        apk add git git-daemon python3 py3-pip
+        apk add sudo git git-daemon python3 py3-pip
+        echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
+        addgroup -g 127 docker
+        adduser -D -u 1001 runner
+        adduser runner docker
+      shell: sh -exo pipefail {0}  # Run this as root, not the "runner" user.
 
     - uses: actions/checkout@v4
       with:
         fetch-depth: 0
 
-    - name: Special configuration for Alpine Linux git
-      run: |
-        git config --global --add safe.directory "$(pwd)"
-
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh
@@ -38,24 +39,17 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
-    - name: Create Python 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 $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
-        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
-        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -64,5 +58,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv
diff --git a/test/test_util.py b/test/test_util.py
index f3769088c..65f77082d 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -51,7 +51,9 @@ def permission_error_tmpdir(tmp_path):
     # Set up PermissionError on Windows, where we can't delete read-only files.
     (td / "x").chmod(stat.S_IRUSR)
 
-    # Set up PermissionError on Unix, where we can't delete files in read-only directories.
+    # Set up PermissionError on Unix, where non-root users can't delete files in
+    # read-only directories. (Tests that rely on this and assert that rmtree raises
+    # PermissionError will fail if they are run as root.)
     td.chmod(stat.S_IRUSR | stat.S_IXUSR)
 
     yield td

From ab37ae7eb6d23e9d74178915ec9e072f67f0cfd1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 10:33:19 -0500
Subject: [PATCH 435/994] Add back safe.directory step

As anticipated, it is still needed.
---
 .github/workflows/alpine-test.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index cc0db7e20..fd085109d 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -27,6 +27,10 @@ jobs:
       with:
         fetch-depth: 0
 
+    - name: Special configuration for Alpine Linux git
+      run: |
+        git config --global --add safe.directory "$(pwd)"
+
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From 46e4234116949b820741322eaedaee4389390934 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 10:37:47 -0500
Subject: [PATCH 436/994] Debug ownership

---
 .github/workflows/alpine-test.yml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index fd085109d..a1327c1ce 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -27,6 +27,14 @@ jobs:
       with:
         fetch-depth: 0
 
+    - name: Debug ownership
+      run: |
+        printenv GITHUB_WORKSPACE
+        printenv RUNNER_WORKSPACE
+        whoami
+        id
+        ls -alR /__w
+
     - name: Special configuration for Alpine Linux git
       run: |
         git config --global --add safe.directory "$(pwd)"

From 20780cb98db6d30a42ef7432ed065ea92325588e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 13:03:07 -0500
Subject: [PATCH 437/994] Take ownership of cloned repository

---
 .github/workflows/alpine-test.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index a1327c1ce..33f7580bf 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -27,6 +27,11 @@ jobs:
       with:
         fetch-depth: 0
 
+    - name: Set workspace ownership
+      run: |
+        chown -R runner:docker -- "$GITHUB_WORKSPACE"
+      shell: sh -exo pipefail {0}  # Run this as root, not the "runner" user.
+
     - name: Debug ownership
       run: |
         printenv GITHUB_WORKSPACE

From b32932f63e013413fb5150d881b67707ee650f22 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 13:59:25 -0500
Subject: [PATCH 438/994] Bring back venv

The "error: externally-managed-environment" stoppage occurs even
when the Alpine Linux python command is run by a non-root user.
---
 .github/workflows/alpine-test.yml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 33f7580bf..cdc1bd5d8 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -56,17 +56,24 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
+    - name: Create Python 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 $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
+        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
+        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -75,4 +82,5 @@ jobs:
 
     - name: Test with pytest
       run: |
+        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv

From bad545ab36d37a1f89bb1dbb587130539498bc64 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 14:09:09 -0500
Subject: [PATCH 439/994] Re-remove safe.directory step

We chown the workspace, so this shouldn't be needed.

This commit also removes the "Debug ownership" step.
---
 .github/workflows/alpine-test.yml | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index cdc1bd5d8..7634ea1d8 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -32,18 +32,6 @@ jobs:
         chown -R runner:docker -- "$GITHUB_WORKSPACE"
       shell: sh -exo pipefail {0}  # Run this as root, not the "runner" user.
 
-    - name: Debug ownership
-      run: |
-        printenv GITHUB_WORKSPACE
-        printenv RUNNER_WORKSPACE
-        whoami
-        id
-        ls -alR /__w
-
-    - name: Special configuration for Alpine Linux git
-      run: |
-        git config --global --add safe.directory "$(pwd)"
-
     - name: Prepare this repo for tests
       run: |
         ./init-tests-after-clone.sh

From 4b427c994833e1a2781c028ac80a11e1a6a64997 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 14 Feb 2024 15:14:41 -0500
Subject: [PATCH 440/994] Factor venv activation into the venv creation step

---
 .github/workflows/alpine-test.yml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 7634ea1d8..2c1eed391 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -44,24 +44,23 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
-    - name: Create Python virtual environment
+    - name: Set up virtualenv
       run: |
         python -m venv .venv
+        . .venv/bin/activate
+        printf '%s=%s\n' 'PATH' "$PATH" 'VIRTUAL_ENV' "$VIRTUAL_ENV" >>"$GITHUB_ENV"
 
     - 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 $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
-        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
-        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -70,5 +69,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv

From 6074f4796659f099167539047528edaebe883fb1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Can=20Ta=C5=9Fl=C4=B1=C3=A7ukur?=
 <can.taslicukur@ozu.edu.tr>
Date: Mon, 19 Feb 2024 22:25:10 +0300
Subject: [PATCH 441/994] feat: :sparkles: added "--no-ext-diff" flag when
 create_patch=True to diff

---
 git/diff.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/git/diff.py b/git/diff.py
index aba1a1080..ae85e77ae 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -155,6 +155,8 @@ def diff(
 
         if create_patch:
             args.append("-p")
+            # we expect the default diff driver
+            args.append("--no-ext-diff")
         else:
             args.append("--raw")
             args.append("-z")

From e40fc2cd2677c8f7e7c067583df00572430442ec Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 00:41:41 -0500
Subject: [PATCH 442/994] Use TemporaryFile as a context manager

(This also uses assertRaises as a context manager, but that is
just for improved clarity.)
---
 test/test_git.py | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index f12428f9e..a00d2b950 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -260,13 +260,9 @@ def test_it_ignores_false_kwargs(self, git):
         self.assertTrue("pass_this_kwarg" not in git.call_args[1])
 
     def test_it_raises_proper_exception_with_output_stream(self):
-        tmp_file = TemporaryFile()
-        self.assertRaises(
-            GitCommandError,
-            self.git.checkout,
-            "non-existent-branch",
-            output_stream=tmp_file,
-        )
+        with TemporaryFile() as tmp_file:
+            with self.assertRaises(GitCommandError):
+                self.git.checkout("non-existent-branch", output_stream=tmp_file)
 
     def test_it_accepts_environment_variables(self):
         filename = fixture_path("ls_tree_empty")

From 1de2fdc5234e1d688386f3a345f9cf2f264ba5ea Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 01:34:08 -0500
Subject: [PATCH 443/994] Test that version_info caches

This begins to test the established version_info caching behavior.
---
 test/test_git.py | 32 ++++++++++++++++++++++++++++++--
 1 file changed, 30 insertions(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index a00d2b950..d8349e9be 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -14,7 +14,7 @@
 import shutil
 import subprocess
 import sys
-from tempfile import TemporaryFile
+import tempfile
 from unittest import skipUnless
 
 if sys.version_info >= (3, 8):
@@ -67,6 +67,24 @@ def _rollback_refresh():
         refresh()
 
 
+@contextlib.contextmanager
+def _fake_git():
+    fake_output = "git version 123.456.789 (fake)"
+
+    with tempfile.TemporaryDirectory() as tdir:
+        if os.name == "nt":
+            fake_git = Path(tdir, "fake-git.cmd")
+            script = f"@echo {fake_output}\n"
+            fake_git.write_text(script, encoding="utf-8")
+        else:
+            fake_git = Path(tdir, "fake-git")
+            script = f"#!/bin/sh\necho '{fake_output}'\n"
+            fake_git.write_text(script, encoding="utf-8")
+            fake_git.chmod(0o755)
+
+        yield str(fake_git.absolute())
+
+
 @ddt.ddt
 class TestGit(TestBase):
     @classmethod
@@ -260,7 +278,7 @@ def test_it_ignores_false_kwargs(self, git):
         self.assertTrue("pass_this_kwarg" not in git.call_args[1])
 
     def test_it_raises_proper_exception_with_output_stream(self):
-        with TemporaryFile() as tmp_file:
+        with tempfile.TemporaryFile() as tmp_file:
             with self.assertRaises(GitCommandError):
                 self.git.checkout("non-existent-branch", output_stream=tmp_file)
 
@@ -483,6 +501,16 @@ def test_refresh_with_good_relative_git_path_arg(self):
                 refresh(basename)
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
+    def test_version_info_is_cached(self):
+        with _rollback_refresh():
+            with _fake_git() as path:
+                new_git = Git()  # Not cached yet.
+                refresh(path)
+                version_info = new_git.version_info  # Caches the value.
+                self.assertEqual(version_info, (123, 456, 789))
+                os.remove(path)  # Arrange that reading a second time would fail.
+                self.assertEqual(new_git.version_info, version_info)  # Cached value.
+
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.
         git_version = self.git(version=True).NoOp()

From 3f107d59399538594fa98d5e3956e4cfe707494b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 01:35:31 -0500
Subject: [PATCH 444/994] Test that version_info caching is per instance

This independence is also established caching behavior for
version_info.
---
 test/test_git.py | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index d8349e9be..19e448f4a 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -28,6 +28,8 @@
 from git.util import cwd, finalize_process
 from test.lib import TestBase, fixture_path, with_rw_directory
 
+_FAKE_GIT_VERSION_INFO = (123, 456, 789)
+
 
 @contextlib.contextmanager
 def _patch_out_env(name):
@@ -69,7 +71,8 @@ def _rollback_refresh():
 
 @contextlib.contextmanager
 def _fake_git():
-    fake_output = "git version 123.456.789 (fake)"
+    fake_version = ".".join(str(field) for field in _FAKE_GIT_VERSION_INFO)
+    fake_output = f"git version {fake_version} (fake)"
 
     with tempfile.TemporaryDirectory() as tdir:
         if os.name == "nt":
@@ -506,10 +509,21 @@ def test_version_info_is_cached(self):
             with _fake_git() as path:
                 new_git = Git()  # Not cached yet.
                 refresh(path)
-                version_info = new_git.version_info  # Caches the value.
-                self.assertEqual(version_info, (123, 456, 789))
-                os.remove(path)  # Arrange that reading a second time would fail.
-                self.assertEqual(new_git.version_info, version_info)  # Cached value.
+                self.assertEqual(new_git.version_info, _FAKE_GIT_VERSION_INFO)
+                os.remove(path)  # Arrange that a second subprocess call would fail.
+                self.assertEqual(new_git.version_info, _FAKE_GIT_VERSION_INFO)
+
+    def test_version_info_cache_is_per_instance(self):
+        with _rollback_refresh():
+            with _fake_git() as path:
+                git1 = Git()
+                git2 = Git()
+                refresh(path)
+                git1.version_info
+                os.remove(path)  # Arrange that the second subprocess call will fail.
+                with self.assertRaises(GitCommandNotFound):
+                    git2.version_info
+                git1.version_info
 
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.

From 2eac36c951553e8554609fdad026e69e7e1e561e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 02:20:40 -0500
Subject: [PATCH 445/994] Have _fake_git fixture take version_info

This reorganization is to facilitate using two separate fake git
commands with different versions, in forthcoming tests.
---
 test/test_git.py | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 19e448f4a..7c433e910 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -28,8 +28,6 @@
 from git.util import cwd, finalize_process
 from test.lib import TestBase, fixture_path, with_rw_directory
 
-_FAKE_GIT_VERSION_INFO = (123, 456, 789)
-
 
 @contextlib.contextmanager
 def _patch_out_env(name):
@@ -70,8 +68,8 @@ def _rollback_refresh():
 
 
 @contextlib.contextmanager
-def _fake_git():
-    fake_version = ".".join(str(field) for field in _FAKE_GIT_VERSION_INFO)
+def _fake_git(*version_info):
+    fake_version = ".".join(map(str, version_info))
     fake_output = f"git version {fake_version} (fake)"
 
     with tempfile.TemporaryDirectory() as tdir:
@@ -505,17 +503,18 @@ def test_refresh_with_good_relative_git_path_arg(self):
                 self.assertEqual(self.git.GIT_PYTHON_GIT_EXECUTABLE, absolute_path)
 
     def test_version_info_is_cached(self):
+        fake_version_info = (123, 456, 789)
         with _rollback_refresh():
-            with _fake_git() as path:
+            with _fake_git(*fake_version_info) as path:
                 new_git = Git()  # Not cached yet.
                 refresh(path)
-                self.assertEqual(new_git.version_info, _FAKE_GIT_VERSION_INFO)
+                self.assertEqual(new_git.version_info, fake_version_info)
                 os.remove(path)  # Arrange that a second subprocess call would fail.
-                self.assertEqual(new_git.version_info, _FAKE_GIT_VERSION_INFO)
+                self.assertEqual(new_git.version_info, fake_version_info)
 
     def test_version_info_cache_is_per_instance(self):
         with _rollback_refresh():
-            with _fake_git() as path:
+            with _fake_git(123, 456, 789) as path:
                 git1 = Git()
                 git2 = Git()
                 refresh(path)

From 2d6311bf35ee1f1bf6a7347edc06327cfa18b118 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 22:56:23 -0500
Subject: [PATCH 446/994] Test version_info on unpickled Git instance

The new test doesn't pass yet, as #1836 is not yet fixed.
---
 test/test_git.py | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 7c433e910..0269bca4f 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -10,6 +10,7 @@
 import os
 import os.path as osp
 from pathlib import Path
+import pickle
 import re
 import shutil
 import subprocess
@@ -329,12 +330,20 @@ def test_persistent_cat_file_command(self):
         self.assertEqual(typename, typename_two)
         self.assertEqual(size, size_two)
 
-    def test_version(self):
+    def test_version_info(self):
+        """The version_info attribute is a tuple of ints."""
         v = self.git.version_info
         self.assertIsInstance(v, tuple)
         for n in v:
             self.assertIsInstance(n, int)
-        # END verify number types
+
+    def test_version_info_pickleable(self):
+        """The version_info attribute is usable on unpickled Git instances."""
+        deserialized = pickle.loads(pickle.dumps(self.git))
+        v = deserialized.version_info
+        self.assertIsInstance(v, tuple)
+        for n in v:
+            self.assertIsInstance(n, int)
 
     def test_git_exc_name_is_git(self):
         self.assertEqual(self.git.git_exec_name, "git")

From f699a387e19764e591dbca392035a5f9e457b06d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 23:20:44 -0500
Subject: [PATCH 447/994] Fix Git.version_info pickling

For #1836.

This uses a property with the same logic as before for version_info
caching, except that the _version_info backing attribute holds the
value None, rather than being unset, for the uncomputed state. This
rectifies the inconistency between the property's behavior and the
way the associated states are represented by its __setstate__ and
__getstate__ methods for pickling.

Because the Git class only used LazyMixin as an implementation
detail of the version_info attribute, it is removed as a subclass.
---
 git/cmd.py | 32 ++++++++++++++------------------
 1 file changed, 14 insertions(+), 18 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index f58e6df5b..e5edb4959 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -25,7 +25,6 @@
     UnsafeProtocolError,
 )
 from git.util import (
-    LazyMixin,
     cygpath,
     expand_path,
     is_cygwin_git,
@@ -287,7 +286,7 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 ## -- End Utilities -- @}
 
 
-class Git(LazyMixin):
+class Git:
     """The Git class manages communication with the Git binary.
 
     It provides a convenient interface to calling the Git binary, such as in::
@@ -784,6 +783,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         self._environment: Dict[str, str] = {}
 
         # Cached command slots
+        self._version_info: Union[Tuple[int, int, int, int], None] = None
         self.cat_file_header: Union[None, TBD] = None
         self.cat_file_all: Union[None, TBD] = None
 
@@ -795,8 +795,8 @@ def __getattr__(self, name: str) -> Any:
             Callable object that will execute call :meth:`_call_process` with
             your arguments.
         """
-        if name[0] == "_":
-            return LazyMixin.__getattr__(self, name)
+        if name.startswith("_"):
+            return super().__getattribute__(name)
         return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
 
     def set_persistent_git_options(self, **kwargs: Any) -> None:
@@ -811,20 +811,6 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
 
         self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
 
-    def _set_cache_(self, attr: str) -> None:
-        if attr == "_version_info":
-            # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
-            process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
-            version_numbers = process_version.split(" ")[2]
-
-            self._version_info = cast(
-                Tuple[int, int, int, int],
-                tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
-            )
-        else:
-            super()._set_cache_(attr)
-        # END handle version info
-
     @property
     def working_dir(self) -> Union[None, PathLike]:
         """:return: Git directory we are working on"""
@@ -838,6 +824,16 @@ def version_info(self) -> Tuple[int, int, int, int]:
 
             This value is generated on demand and is cached.
         """
+        if self._version_info is None:
+            # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
+            process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
+            version_numbers = process_version.split(" ")[2]
+
+            self._version_info = cast(
+                Tuple[int, int, int, int],
+                tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
+            )
+
         return self._version_info
 
     @overload

From 634b61812cd14a13cccb975d5d54d34001c56449 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 23:37:56 -0500
Subject: [PATCH 448/994] Use documented version_info in
 test_index_file_diffing

Instead of directly accessing the _version_info attribute.

This fixes a new test failure since the way the public version_info
property uses the _version_info backing attribute has changed, and
also shows the usage that is documented (accessing _version_info
was never guaranteed to work, and now no longer works as before).
---
 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 ffe71450d..fd62bb893 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -528,7 +528,7 @@ def test_index_file_diffing(self, rw_repo):
             index.checkout(test_file)
         except CheckoutError as e:
             # Detailed exceptions are only possible in older git versions.
-            if rw_repo.git._version_info < (2, 29):
+            if rw_repo.git.version_info < (2, 29):
                 self.assertEqual(len(e.failed_files), 1)
                 self.assertEqual(e.failed_files[0], osp.basename(test_file))
                 self.assertEqual(len(e.failed_files), len(e.failed_reasons))

From 626a550d22d91dc94b7cc0bfda3fef3ab7f10189 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 18:43:38 -0500
Subject: [PATCH 449/994] Test that refreshing invalidates cached version_info

Most of these don't pass yet, as such invalidation isn't
implemented yet (#1829).
---
 test/test_git.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 78 insertions(+)

diff --git a/test/test_git.py b/test/test_git.py
index 0269bca4f..9c18de77d 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -533,6 +533,84 @@ def test_version_info_cache_is_per_instance(self):
                     git2.version_info
                 git1.version_info
 
+    def test_successful_refresh_with_arg_invalidates_cached_version_info(self):
+        with _rollback_refresh():
+            with _fake_git(11, 111, 1) as path1:
+                with _fake_git(22, 222, 2) as path2:
+                    new_git = Git()
+                    refresh(path1)
+                    new_git.version_info
+                    refresh(path2)
+                    self.assertEqual(new_git.version_info, (22, 222, 2))
+
+    def test_failed_refresh_with_arg_does_not_invalidate_cached_version_info(self):
+        with _rollback_refresh():
+            with _fake_git(11, 111, 1) as path1:
+                with _fake_git(22, 222, 2) as path2:
+                    new_git = Git()
+                    refresh(path1)
+                    new_git.version_info
+                    os.remove(path1)  # Arrange that a repeat call for path1 would fail.
+                    os.remove(path2)  # Arrange that the new call for path2 will fail.
+                    with self.assertRaises(GitCommandNotFound):
+                        refresh(path2)
+                    self.assertEqual(new_git.version_info, (11, 111, 1))
+
+    def test_successful_refresh_with_same_arg_invalidates_cached_version_info(self):
+        """Changing git at the same path and refreshing affects version_info."""
+        with _rollback_refresh():
+            with _fake_git(11, 111, 1) as path1:
+                with _fake_git(22, 222, 2) as path2:
+                    new_git = Git()
+                    refresh(path1)
+                    new_git.version_info
+                    shutil.copy(path2, path1)
+                    refresh(path1)  # The fake git at path1 has a different version now.
+                    self.assertEqual(new_git.version_info, (22, 222, 2))
+
+    def test_successful_refresh_with_env_invalidates_cached_version_info(self):
+        with contextlib.ExitStack() as stack:
+            stack.enter_context(_rollback_refresh())
+            path1 = stack.enter_context(_fake_git(11, 111, 1))
+            path2 = stack.enter_context(_fake_git(22, 222, 2))
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": path1}):
+                new_git = Git()
+                refresh()
+                new_git.version_info
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": path2}):
+                refresh()
+                self.assertEqual(new_git.version_info, (22, 222, 2))
+
+    def test_failed_refresh_with_env_does_not_invalidate_cached_version_info(self):
+        with contextlib.ExitStack() as stack:
+            stack.enter_context(_rollback_refresh())
+            path1 = stack.enter_context(_fake_git(11, 111, 1))
+            path2 = stack.enter_context(_fake_git(22, 222, 2))
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": path1}):
+                new_git = Git()
+                refresh()
+                new_git.version_info
+            os.remove(path1)  # Arrange that a repeat call for path1 would fail.
+            os.remove(path2)  # Arrange that the new call for path2 will fail.
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": path2}):
+                with self.assertRaises(GitCommandNotFound):
+                    refresh(path2)
+                self.assertEqual(new_git.version_info, (11, 111, 1))
+
+    def test_successful_refresh_with_same_env_invalidates_cached_version_info(self):
+        """Changing git at the same path/command and refreshing affects version_info."""
+        with contextlib.ExitStack() as stack:
+            stack.enter_context(_rollback_refresh())
+            path1 = stack.enter_context(_fake_git(11, 111, 1))
+            path2 = stack.enter_context(_fake_git(22, 222, 2))
+            with mock.patch.dict(os.environ, {"GIT_PYTHON_GIT_EXECUTABLE": path1}):
+                new_git = Git()
+                refresh()
+                new_git.version_info
+                shutil.copy(path2, path1)
+                refresh()  # The fake git at path1 has a different version now.
+                self.assertEqual(new_git.version_info, (22, 222, 2))
+
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.
         git_version = self.git(version=True).NoOp()

From 9151c38b18474d6642857ece114a39e34fe7dd09 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 19:37:27 -0500
Subject: [PATCH 450/994] Test "installing" another git and refreshing

This is trickier to test than the other cases, but important enough
that I think it deserves its own test.
---
 test/test_git.py | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/test/test_git.py b/test/test_git.py
index 9c18de77d..10c51cd8f 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -24,6 +24,7 @@
     import mock  # To be able to examine call_args.kwargs on a mock.
 
 import ddt
+import pytest
 
 from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
 from git.util import cwd, finalize_process
@@ -611,6 +612,30 @@ def test_successful_refresh_with_same_env_invalidates_cached_version_info(self):
                 refresh()  # The fake git at path1 has a different version now.
                 self.assertEqual(new_git.version_info, (22, 222, 2))
 
+    @pytest.mark.xfail(
+        os.name == "nt",
+        reason="""Name "git" won't find .bat/.cmd (need shim, custom name, or shell)""",
+        raises=GitCommandNotFound,
+    )
+    def test_successful_refresh_in_default_scenario_invalidates_cached_version(self):
+        """Refreshing updates version after a filesystem change adds a git command."""
+        with contextlib.ExitStack() as stack:
+            stack.enter_context(_rollback_refresh())
+            path1 = Path(stack.enter_context(_fake_git(11, 111, 1)))
+            path2 = Path(stack.enter_context(_fake_git(22, 222, 2)))
+            sep = os.pathsep
+            new_path_var = f"{path1.parent}{sep}{path2.parent}{sep}{os.environ['PATH']}"
+            stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var}))
+            stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
+            new_git = Git()
+            path2.rename(path2.with_stem("git"))
+            refresh()
+            self.assertEqual(new_git.version_info, (22, 222, 2), 'before "downgrade"')
+            path1.rename(path1.with_stem("git"))
+            self.assertEqual(new_git.version_info, (22, 222, 2), "stale version")
+            refresh()
+            self.assertEqual(new_git.version_info, (11, 111, 1), "fresh version")
+
     def test_options_are_passed_to_git(self):
         # This works because any command after git --version is ignored.
         git_version = self.git(version=True).NoOp()

From c2d72ffde8b046d1f6d5314512fad24bd1b923eb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 19:40:26 -0500
Subject: [PATCH 451/994] Simplify patched PATH env var construction

This also makes the xfail marking for Windows correct.
---
 test/test_git.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 10c51cd8f..bf9e2373f 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -623,8 +623,7 @@ def test_successful_refresh_in_default_scenario_invalidates_cached_version(self)
             stack.enter_context(_rollback_refresh())
             path1 = Path(stack.enter_context(_fake_git(11, 111, 1)))
             path2 = Path(stack.enter_context(_fake_git(22, 222, 2)))
-            sep = os.pathsep
-            new_path_var = f"{path1.parent}{sep}{path2.parent}{sep}{os.environ['PATH']}"
+            new_path_var = f"{path1.parent}{os.pathsep}{path2.parent}"
             stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var}))
             stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
             new_git = Git()

From ee385bd8058473f4fdd06c2ba7f7b980a45e3ada Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 21 Feb 2024 20:44:25 -0500
Subject: [PATCH 452/994] Make "install" and refresh version_info test portable

This enables it to run on Windows, removing the xfail marking.

+ Rename the test.
+ Add comments and reformat.

The test still does not pass (on any platform) because it is one of
the tests of the invalidation feature that is not yet implemented.
---
 test/test_git.py | 26 +++++++++++++++++---------
 1 file changed, 17 insertions(+), 9 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index bf9e2373f..80316f8f8 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -24,7 +24,6 @@
     import mock  # To be able to examine call_args.kwargs on a mock.
 
 import ddt
-import pytest
 
 from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
 from git.util import cwd, finalize_process
@@ -612,25 +611,34 @@ def test_successful_refresh_with_same_env_invalidates_cached_version_info(self):
                 refresh()  # The fake git at path1 has a different version now.
                 self.assertEqual(new_git.version_info, (22, 222, 2))
 
-    @pytest.mark.xfail(
-        os.name == "nt",
-        reason="""Name "git" won't find .bat/.cmd (need shim, custom name, or shell)""",
-        raises=GitCommandNotFound,
-    )
-    def test_successful_refresh_in_default_scenario_invalidates_cached_version(self):
+    def test_successful_default_refresh_invalidates_cached_version_info(self):
         """Refreshing updates version after a filesystem change adds a git command."""
+        # The key assertion here is the last. The others mainly verify the test itself.
         with contextlib.ExitStack() as stack:
             stack.enter_context(_rollback_refresh())
+
             path1 = Path(stack.enter_context(_fake_git(11, 111, 1)))
             path2 = Path(stack.enter_context(_fake_git(22, 222, 2)))
+
             new_path_var = f"{path1.parent}{os.pathsep}{path2.parent}"
             stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var}))
             stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
+
+            if os.name == "nt":
+                # On Windows, use a shell so "git" finds "git.cmd". (In the infrequent
+                # case that this effect is desired in production code, it should not be
+                # done with this technique. USE_SHELL=True is less secure and reliable,
+                # as unintended shell expansions can occur, and is deprecated. Instead,
+                # use a custom command, by setting the GIT_PYTHON_GIT_EXECUTABLE
+                # environment variable to git.cmd or by passing git.cmd's full path to
+                # git.refresh. Or wrap the script with a .exe shim.
+                stack.enter_context(mock.patch.object(Git, "USE_SHELL", True))
+
             new_git = Git()
-            path2.rename(path2.with_stem("git"))
+            path2.rename(path2.with_stem("git"))  # "Install" git, "late" in the PATH.
             refresh()
             self.assertEqual(new_git.version_info, (22, 222, 2), 'before "downgrade"')
-            path1.rename(path1.with_stem("git"))
+            path1.rename(path1.with_stem("git"))  # "Install" another, higher priority.
             self.assertEqual(new_git.version_info, (22, 222, 2), "stale version")
             refresh()
             self.assertEqual(new_git.version_info, (11, 111, 1), "fresh version")

From 29406626f985c93404ef283f4e5d922e5bf96978 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 00:26:58 -0500
Subject: [PATCH 453/994] Test that version_info is not pickled

That is, that its value is not pickle serialized/deserialized, but
always recomputed with a subprocess call the first time it is
accessed even on a Git instance that is created by unpickling.
---
 test/test_git.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/test/test_git.py b/test/test_git.py
index 80316f8f8..cfccc0c48 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -533,6 +533,18 @@ def test_version_info_cache_is_per_instance(self):
                     git2.version_info
                 git1.version_info
 
+    def test_version_info_cache_is_not_pickled(self):
+        with _rollback_refresh():
+            with _fake_git(123, 456, 789) as path:
+                git1 = Git()
+                refresh(path)
+                git1.version_info
+                git2 = pickle.loads(pickle.dumps(git1))
+                os.remove(path)  # Arrange that the second subprocess call will fail.
+                with self.assertRaises(GitCommandNotFound):
+                    git2.version_info
+                git1.version_info
+
     def test_successful_refresh_with_arg_invalidates_cached_version_info(self):
         with _rollback_refresh():
             with _fake_git(11, 111, 1) as path1:

From e3e568760f7ee002921de292dd98564fdedf6126 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 01:34:32 -0500
Subject: [PATCH 454/994] Fix tests for Python <3.9 which lack Path.with_stem

---
 test/test_git.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index cfccc0c48..70c90c2bc 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -87,6 +87,13 @@ def _fake_git(*version_info):
         yield str(fake_git.absolute())
 
 
+def _rename_with_stem(path, new_stem):
+    if sys.version_info >= (3, 9):
+        path.rename(path.with_stem(new_stem))
+    else:
+        path.rename(path.with_name(new_stem + path.suffix))
+
+
 @ddt.ddt
 class TestGit(TestBase):
     @classmethod
@@ -647,10 +654,10 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
                 stack.enter_context(mock.patch.object(Git, "USE_SHELL", True))
 
             new_git = Git()
-            path2.rename(path2.with_stem("git"))  # "Install" git, "late" in the PATH.
+            _rename_with_stem(path2, "git")  # "Install" git, "late" in the PATH.
             refresh()
             self.assertEqual(new_git.version_info, (22, 222, 2), 'before "downgrade"')
-            path1.rename(path1.with_stem("git"))  # "Install" another, higher priority.
+            _rename_with_stem(path1, "git")  # "Install" another, higher priority.
             self.assertEqual(new_git.version_info, (22, 222, 2), "stale version")
             refresh()
             self.assertEqual(new_git.version_info, (11, 111, 1), "fresh version")

From 24b065e4920e11b10fb6230abcc137e2f49eb8f3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 01:11:41 -0500
Subject: [PATCH 455/994] Invalidate all cached version_info on refresh

For #1829.
---
 git/cmd.py | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index e5edb4959..c1b69da91 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -306,12 +306,18 @@ class Git:
         "cat_file_all",
         "cat_file_header",
         "_version_info",
+        "_version_info_token",
         "_git_options",
         "_persistent_git_options",
         "_environment",
     )
 
-    _excluded_ = ("cat_file_all", "cat_file_header", "_version_info")
+    _excluded_ = (
+        "cat_file_all",
+        "cat_file_header",
+        "_version_info",
+        "_version_info_token",
+    )
 
     re_unsafe_protocol = re.compile(r"(.+)::.+")
 
@@ -358,6 +364,8 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     the top level ``__init__``.
     """
 
+    _refresh_token = object()  # Since None would match an initial _version_info_token.
+
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         """This gets called by the refresh function (see the top level __init__)."""
@@ -370,7 +378,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
 
         # Keep track of the old and new git executable path.
         old_git = cls.GIT_PYTHON_GIT_EXECUTABLE
+        old_refresh_token = cls._refresh_token
         cls.GIT_PYTHON_GIT_EXECUTABLE = new_git
+        cls._refresh_token = object()
 
         # Test if the new git executable path is valid. A GitCommandNotFound error is
         # spawned by us. A PermissionError is spawned if the git executable cannot be
@@ -399,6 +409,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
 
             # Revert to whatever the old_git was.
             cls.GIT_PYTHON_GIT_EXECUTABLE = old_git
+            cls._refresh_token = old_refresh_token
 
             if old_git is None:
                 # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
@@ -782,8 +793,11 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         # Extra environment variables to pass to git commands
         self._environment: Dict[str, str] = {}
 
-        # Cached command slots
+        # Cached version slots
         self._version_info: Union[Tuple[int, int, int, int], None] = None
+        self._version_info_token: object = None
+
+        # Cached command slots
         self.cat_file_header: Union[None, TBD] = None
         self.cat_file_all: Union[None, TBD] = None
 
@@ -824,7 +838,11 @@ def version_info(self) -> Tuple[int, int, int, int]:
 
             This value is generated on demand and is cached.
         """
-        if self._version_info is None:
+        refresh_token = self._refresh_token  # Copy it, in case of a concurrent refresh.
+
+        # Ask git for its version if we haven't done so since the last refresh.
+        # (Refreshing is global, but version information caching is per-instance.)
+        if self._version_info_token is not refresh_token:
             # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
             process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
             version_numbers = process_version.split(" ")[2]
@@ -833,6 +851,7 @@ def version_info(self) -> Tuple[int, int, int, int]:
                 Tuple[int, int, int, int],
                 tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
             )
+            self._version_info_token = refresh_token
 
         return self._version_info
 

From 82b0a1e0681a662d5d418277292cf73fd26cae92 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 11:49:49 -0500
Subject: [PATCH 456/994] Clarify comments; add assertion (helps mypy)

---
 git/cmd.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index c1b69da91..9e5713956 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -838,10 +838,11 @@ def version_info(self) -> Tuple[int, int, int, int]:
 
             This value is generated on demand and is cached.
         """
-        refresh_token = self._refresh_token  # Copy it, in case of a concurrent refresh.
+        # Use a copy of this global state, in case of a concurrent refresh.
+        refresh_token = self._refresh_token
 
         # Ask git for its version if we haven't done so since the last refresh.
-        # (Refreshing is global, but version information caching is per-instance.)
+        # (Refreshing is global, but version_info caching is per-instance.)
         if self._version_info_token is not refresh_token:
             # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
             process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
@@ -853,6 +854,7 @@ def version_info(self) -> Tuple[int, int, int, int]:
             )
             self._version_info_token = refresh_token
 
+        assert self._version_info is not None, "Bug: token check should never let None through"
         return self._version_info
 
     @overload

From eb438ee63add2376e885890489cd5e7484cc8a9d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 12:29:55 -0500
Subject: [PATCH 457/994] Refactor and further clarify comments

+ Put assertion only on the branch where mypy benefits from it.
---
 git/cmd.py | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9e5713956..a1b3c9e45 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -833,28 +833,29 @@ def working_dir(self) -> Union[None, PathLike]:
     @property
     def version_info(self) -> Tuple[int, int, int, int]:
         """
-        :return: tuple(int, int, int, int) tuple with integers representing the major, minor
-            and additional version numbers as parsed from git version.
+        :return: tuple(int, int, int, int) tuple with integers representing the major,
+            minor and additional version numbers as parsed from git version.
 
             This value is generated on demand and is cached.
         """
-        # Use a copy of this global state, in case of a concurrent refresh.
-        refresh_token = self._refresh_token
-
-        # Ask git for its version if we haven't done so since the last refresh.
-        # (Refreshing is global, but version_info caching is per-instance.)
-        if self._version_info_token is not refresh_token:
-            # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
-            process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
-            version_numbers = process_version.split(" ")[2]
-
-            self._version_info = cast(
-                Tuple[int, int, int, int],
-                tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
-            )
-            self._version_info_token = refresh_token
+        # Refreshing is global, but version_info caching is per-instance.
+        refresh_token = self._refresh_token  # Copy token in case of concurrent refresh.
+
+        # Use the cached version if obtained after the most recent refresh.
+        if self._version_info_token is refresh_token:
+            assert self._version_info is not None, "Bug: corrupted token-check state"
+            return self._version_info
+
+        # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
+        process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
+        version_numbers = process_version.split(" ")[2]
+
+        self._version_info = cast(
+            Tuple[int, int, int, int],
+            tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
+        )
+        self._version_info_token = refresh_token
 
-        assert self._version_info is not None, "Bug: token check should never let None through"
         return self._version_info
 
     @overload

From dc6b90fa5a3617c490e95aeaf37fb80cfd17a215 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 15:20:23 -0500
Subject: [PATCH 458/994] Fix version_info type hint + more refactoring

This broadens the type annoation on the Git.version_info property,
for #1830.

It also revises the docstring for clarity, and continues
refactoring and comment changes (also for clarity).
---
 git/cmd.py | 22 ++++++++++------------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index a1b3c9e45..f534c6673 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -794,7 +794,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         self._environment: Dict[str, str] = {}
 
         # Cached version slots
-        self._version_info: Union[Tuple[int, int, int, int], None] = None
+        self._version_info: Union[Tuple[int, ...], None] = None
         self._version_info_token: object = None
 
         # Cached command slots
@@ -831,10 +831,10 @@ def working_dir(self) -> Union[None, PathLike]:
         return self._working_dir
 
     @property
-    def version_info(self) -> Tuple[int, int, int, int]:
+    def version_info(self) -> Tuple[int, ...]:
         """
-        :return: tuple(int, int, int, int) tuple with integers representing the major,
-            minor and additional version numbers as parsed from git version.
+        :return:  tuple with integers representing the major, minor and additional
+            version numbers as parsed from git version. Up to four fields are used.
 
             This value is generated on demand and is cached.
         """
@@ -846,16 +846,14 @@ def version_info(self) -> Tuple[int, int, int, int]:
             assert self._version_info is not None, "Bug: corrupted token-check state"
             return self._version_info
 
-        # We only use the first 4 numbers, as everything else could be strings in fact (on Windows).
-        process_version = self._call_process("version")  # Should be as default *args and **kwargs used.
-        version_numbers = process_version.split(" ")[2]
+        # Run "git version" and parse it.
+        process_version = self._call_process("version")
+        version_string = process_version.split(" ")[2]
+        version_fields = version_string.split(".")[:4]
+        self._version_info = tuple(int(n) for n in version_fields if n.isdigit())
 
-        self._version_info = cast(
-            Tuple[int, int, int, int],
-            tuple(int(n) for n in version_numbers.split(".")[:4] if n.isdigit()),
-        )
+        # This value will be considered valid until the next refresh.
         self._version_info_token = refresh_token
-
         return self._version_info
 
     @overload

From ac20325133649876749a12c2f611dbf66f5bc17c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 15:41:11 -0500
Subject: [PATCH 459/994] Test version_info parsing

This modifies two existing test cases to include an assertion about
the length. These test cases are retained as there is value in
testing on output from a real git command rather than only with
test doubles.

More importantly, this also adds a parameterized test method to
check parsing of:

- All numeric, shorter than the limit - all fields used.
- All numeric, at the limit - all fields used.
- All numeric, longer than the limit - extra fields dropped.
- Has unambiguous non-numeric - dropped from there on.
- Has ambiguous non-numeric, negative int - dropped from there on.
- Has ambiguous non-numeric, number+letter - dropped from there on.

The cases for parsing when a field is not numeric (or not fully or
unambiguously numeric) currently all fail, because the existing
logic drops intermediate non-numeric fields (#1833).

Parsing should instead stop at (or, *perhaps* in cases like "2a",
after) such fields. When the code is changed to stop at them
rather than dropping them and presenting the subsequent field as
though it were a previous field, these test cases should also pass.
---
 test/test_git.py | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/test/test_git.py b/test/test_git.py
index 70c90c2bc..97e21cad4 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -338,9 +338,10 @@ def test_persistent_cat_file_command(self):
         self.assertEqual(size, size_two)
 
     def test_version_info(self):
-        """The version_info attribute is a tuple of ints."""
+        """The version_info attribute is a tuple of up to four ints."""
         v = self.git.version_info
         self.assertIsInstance(v, tuple)
+        self.assertLessEqual(len(v), 4)
         for n in v:
             self.assertIsInstance(n, int)
 
@@ -349,9 +350,26 @@ def test_version_info_pickleable(self):
         deserialized = pickle.loads(pickle.dumps(self.git))
         v = deserialized.version_info
         self.assertIsInstance(v, tuple)
+        self.assertLessEqual(len(v), 4)
         for n in v:
             self.assertIsInstance(n, int)
 
+    @ddt.data(
+        (("123", "456", "789"), (123, 456, 789)),
+        (("12", "34", "56", "78"), (12, 34, 56, 78)),
+        (("12", "34", "56", "78", "90"), (12, 34, 56, 78)),
+        (("1", "2", "a", "3"), (1, 2)),
+        (("1", "-2", "3"), (1,)),
+        (("1", "2a", "3"), (1,)),  # Subject to change.
+    )
+    def test_version_info_is_leading_numbers(self, case):
+        fake_fields, expected_version_info = case
+        with _rollback_refresh():
+            with _fake_git(*fake_fields) as path:
+                refresh(path)
+                new_git = Git()
+                self.assertEqual(new_git.version_info, expected_version_info)
+
     def test_git_exc_name_is_git(self):
         self.assertEqual(self.git.git_exec_name, "git")
 

From 629fd87f46a91baccb298bef6fc34ac60ecc5727 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 22 Feb 2024 16:25:03 -0500
Subject: [PATCH 460/994] Fix how version_info omits non-numeric fields

For #1833.

This makes version_info parsing stop including fields after the
first non-numeric field, rather than skipping non-numeric fields
and including subsequent numeric fields that then wrongly appear to
have the original position and significance of the dropped field.

This actually stops at (rather than merely after) a non-numeric
field, i.e., potentially parseable fields that are not fully
numeric, such as "2a", are stopped at, rather than parsed as "2".
---
 git/cmd.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index f534c6673..bfc885222 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -8,6 +8,7 @@
 import re
 import contextlib
 import io
+import itertools
 import logging
 import os
 import signal
@@ -850,7 +851,8 @@ def version_info(self) -> Tuple[int, ...]:
         process_version = self._call_process("version")
         version_string = process_version.split(" ")[2]
         version_fields = version_string.split(".")[:4]
-        self._version_info = tuple(int(n) for n in version_fields if n.isdigit())
+        leading_numeric_fields = itertools.takewhile(str.isdigit, version_fields)
+        self._version_info = tuple(map(int, leading_numeric_fields))
 
         # This value will be considered valid until the next refresh.
         self._version_info_token = refresh_token

From d7b952e51b3d30e6cc95f322714661d8d2d8ee86 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 01:13:45 -0500
Subject: [PATCH 461/994] Document manual refresh path treatment

For #1831.
---
 git/__init__.py | 16 +++++++++++++++-
 git/cmd.py      | 41 ++++++++++++++++++++++++++++++++++++++---
 2 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index c6a52ef30..0ea7821ac 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -120,7 +120,21 @@
 
 
 def refresh(path: Optional[PathLike] = None) -> None:
-    """Convenience method for setting the git executable path."""
+    """Convenience method for setting the git executable path.
+
+    :param path: Optional path to the Git executable. If not absolute, it is resolved
+        immediately, relative to the current directory.
+
+    :note: The *path* parameter is usually omitted and cannot be used to specify a
+        custom command whose location is looked up in a path search on each call. See
+        :meth:`Git.refresh` for details on how to achieve this.
+
+    :note: This calls :meth:`Git.refresh` and sets other global configuration according
+        to the effect of doing so. As such, this function should usually be used instead
+        of using :meth:`Git.refresh` or :meth:`FetchInfo.refresh` directly.
+
+    :note: This function is called automatically, with no arguments, at import time.
+    """
     global GIT_OK
     GIT_OK = False
 
diff --git a/git/cmd.py b/git/cmd.py
index f58e6df5b..e7c12725d 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -355,13 +355,48 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     GIT_PYTHON_GIT_EXECUTABLE = None
     """Provide the full path to the git executable. Otherwise it assumes git is in the path.
 
-    Note that the git executable is actually found during the refresh step in
-    the top level ``__init__``.
+    :note: The git executable is actually found during the refresh step in
+        the top level :mod:`__init__`. It can also be changed by explicitly calling
+        :func:`git.refresh`.
     """
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
-        """This gets called by the refresh function (see the top level __init__)."""
+        """This gets called by the refresh function (see the top level __init__).
+
+        :param path: Optional path to the git executable. If not absolute, it is
+            resolved immediately, relative to the current directory. (See note below.)
+
+        :note: The top-level :func:`git.refresh` should be preferred because it calls
+            this method and may also update other state accordingly.
+
+        :note: There are three different ways to specify what command refreshing causes
+            to be uses for git:
+
+            1. Pass no *path* argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE``
+               environment variable. The command name ``git`` is used. It is looked up
+               in a path search by the system, in each command run (roughly similar to
+               how git is found when running ``git`` commands manually). This is usually
+               the desired behavior.
+
+            2. Pass no *path* argument but set the ``GIT_PYTHON_GIT_EXECUTABLE``
+               environment variable. The command given as the value of that variable is
+               used. This may be a simple command or an arbitrary path. It is looked up
+               in each command run. Setting ``GIT_PYTHON_GIT_EXECUTABLE`` to ``git`` has
+               the same effect as not setting it.
+
+            3. Pass a *path* argument. This path, if not absolute, it immediately
+               resolved, relative to the current directory. This resolution occurs at
+               the time of the refresh, and when git commands are run, they are run with
+               that actual path. If a *path* argument is passed, the
+               ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted.
+
+        :note: Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
+            attribute, which can be read on the :class:`Git` class or any of its
+            instances to check what command is used to run git. This attribute should
+            not be confused with the related ``GIT_PYTHON_GIT_EXECUTABLE`` environment
+            variable. The class attribute is set no matter how refreshing is performed.
+        """
         # Discern which path to refresh with.
         if path is not None:
             new_git = os.path.expanduser(path)

From e9da480a8cdecd28b2d5258c836f3c5aef190bde Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 01:15:58 -0500
Subject: [PATCH 462/994] Fix USE_SHELL docstring reStructuredText list
 formatting

---
 git/cmd.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/git/cmd.py b/git/cmd.py
index e7c12725d..e4054747b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -344,6 +344,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     for, which is not possible under most circumstances.
 
     See:
+
     - :meth:`Git.execute` (on the ``shell`` parameter).
     - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
     - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags

From 64ad585cfc749bb6615dcc3427ce99634f2dd882 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 14:31:15 -0500
Subject: [PATCH 463/994] Build online docs (RTD) with -W and dependencies

This configures Read the Docs builds to be more like local builds
in two ways:

- Pass "-W", as is done in a local build with "make -C doc html",
  so that if there are broken references, the build fails.

- Install dependencies. This configures the Python environment, via
  the python.install key, so that RTD builds install requirements.

More specifically on dependency installation, it does two things:

1. The equivalent of "pip install .", which installs the project
   and its dependencies (though not any extras). This includes the
   gitdb dependency, which is needed to import GitPython's git
   module to populate sections in the API Reference page (#1840).

2. The equivalent of "pip install -r doc/requirements.txt", which
   installs the additional Sphinx-related dependencies used when
   building documentation locally.

Installing Sphinx-related dependencies is useful for three reasons:

a. Least importantly, it should increase consistency between remote
   (RTD) and local documentation builds.

b. It may be needed to avoid warnings that are not being fixed at
   this time, while still allowing the build to succeed with the
   "-W" option (see above on that change) that causes failure for
   immediately addressable problems. The effect of newer versions
   of Sphinx carrying a few extra hard-to-fix warnings for
   GitPython is noted in #1802 (and is why they are not upgraded
   in #1803).

c. One of the documentation build dependencies listed in
   doc/requirements.txt is sphinx_rtd_theme. In 634151a (for #1794)
   the line specifying this theme was commented out, since it
   apparently broke in the build. This may allow it to be used
   again (or can be replaced with another custom theme if desired).

This also reenables the sphinx_rtd_theme theme disabled in 634151a.

Finally, this makes minor changes to .readthedocs.yml's comments
and formatting so the comments are accurate for GitPython details
and so the file is formatted in the same style as other YAML here.
---
 .readthedocs.yaml  | 22 ++++++++++++----------
 doc/source/conf.py |  2 +-
 2 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 3a73b5a3c..04275ce72 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -4,7 +4,7 @@
 # Required
 version: 2
 
-# Set the OS, Python version and other tools you might need
+# Set the OS, Python version and other tools you might need.
 build:
   os: ubuntu-22.04
   tools:
@@ -14,22 +14,24 @@ build:
     # rust: "1.70"
     # golang: "1.20"
 
-# Build documentation in the "docs/" directory with Sphinx
+# Build documentation in the "doc/" directory with Sphinx.
 sphinx:
   configuration: doc/source/conf.py
   # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
   # builder: "dirhtml"
   # Fail on all warnings to avoid broken references
-  # fail_on_warning: true
+  fail_on_warning: true
 
-# Optionally build your docs in additional formats such as PDF and ePub
+# Optionally build your docs in additional formats such as PDF and ePub.
 # formats:
-#   - pdf
-#   - epub
+# - pdf
+# - epub
 
 # Optional but recommended, declare the Python requirements required
-# to build your documentation
+# to build your documentation.
 # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
-# python:
-#   install:
-#     - requirements: docs/requirements.txt
+python:
+  install:
+  - method: pip
+    path: .
+  - requirements: doc/requirements.txt
diff --git a/doc/source/conf.py b/doc/source/conf.py
index ea7a28291..9c22ca06a 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -93,7 +93,7 @@
 # Options for HTML output
 # -----------------------
 
-# html_theme = "sphinx_rtd_theme"
+html_theme = "sphinx_rtd_theme"
 html_theme_options = {}
 
 # The name for this set of Sphinx documents.  If None, it defaults to

From 7d96a1aefd13be6526c8deb8f66e92ba08133dff Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 17:37:52 -0500
Subject: [PATCH 464/994] Fix ambiguous wording in Git.refresh docstring

---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7625357e8..44dfadd13 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -397,7 +397,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
             3. Pass a *path* argument. This path, if not absolute, it immediately
                resolved, relative to the current directory. This resolution occurs at
                the time of the refresh, and when git commands are run, they are run with
-               that actual path. If a *path* argument is passed, the
+               that previously resolved path. If a *path* argument is passed, the
                ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted.
 
         :note: Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class

From b8ebff877b3bad8730366e7e0b9a28ac6c0bb183 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 18:05:01 -0500
Subject: [PATCH 465/994] Suggest full-path refresh() in failure message

This does not suggest or recommend *preferring* to explicitly call
refresh() over the other other techniques, but it clarifies that
the use of refresh() being presented needs a path argument. It also
shows that path in the form of a full path, so users are less
likely to be misled into thinking a command name or other relative
path should be passed to refresh(), which should rarely be done
(since refresh(path) resolves path).
---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index 44dfadd13..5282acfdc 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -438,7 +438,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 The git executable must be specified in one of the following ways:
                     - be included in your $PATH
                     - be set via $%s
-                    - explicitly set via git.refresh()
+                    - explicitly set via git.refresh("/full/path/to/git")
                 """
                 )
                 % cls._git_exec_env_var

From f2e35e775c4346011b59bdbdb3d78cf388305ff3 Mon Sep 17 00:00:00 2001
From: Gaubbe <gabriel12125@gmail.com>
Date: Sat, 24 Feb 2024 15:58:35 -0500
Subject: [PATCH 466/994] fix: both blame methods accept None as a revision

---
 git/repo/base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index f5069dbf5..24f140f62 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -957,7 +957,7 @@ def active_branch(self) -> Head:
         # reveal_type(self.head.reference)  # => Reference
         return self.head.reference
 
-    def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
+    def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
         """Iterator for blame information for the given file at the given revision.
 
         Unlike :meth:`blame`, this does not return the actual file's contents, only a
@@ -1045,7 +1045,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat
 
     def blame(
         self,
-        rev: Union[str, HEAD],
+        rev: Union[str, HEAD, None],
         file: str,
         incremental: bool = False,
         rev_opts: Optional[List[str]] = None,

From 27d8a14904e0b0932c0d32c2080d647412dcd5e4 Mon Sep 17 00:00:00 2001
From: Gaubbe <gabriel12125@gmail.com>
Date: Sun, 25 Feb 2024 17:58:38 -0500
Subject: [PATCH 467/994] docs: updated explanation of the `rev` parameter for
 both blame methods

---
 git/repo/base.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index 24f140f62..bf7c2cc0d 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -963,7 +963,9 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
         Unlike :meth:`blame`, this does not return the actual file's contents, only a
         stream of :class:`BlameEntry` tuples.
 
-        :param rev: Revision specifier, see git-rev-parse for viable options.
+        :param rev: Revision specifier. If `None`, the blame will include all the latest
+            uncommitted changes. Otherwise, anything succesfully parsed by git-rev-parse
+            is a valid option.
 
         :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates
             the commit to blame for the line, and range indicates a span of line numbers
@@ -1053,7 +1055,9 @@ def blame(
     ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
         """The blame information for the given file at the given revision.
 
-        :param rev: Revision specifier, see git-rev-parse for viable options.
+        :param rev: Revision specifier. If `None`, the blame will include all the latest
+            uncommitted changes. Otherwise, anything succesfully parsed by git-rev-parse
+            is a valid option.
 
         :return:
             list: [git.Commit, list: [<line>]]

From 9b5531bc0e53e03acff70a8c8250115d2d3c9fa4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 11:34:08 -0500
Subject: [PATCH 468/994] Fix typos and further clarify Git.refresh docstring

This further improves the text previously introduced in #1829 and
improved in #1844.
---
 git/cmd.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 5282acfdc..75244536d 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -379,8 +379,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         :note: The top-level :func:`git.refresh` should be preferred because it calls
             this method and may also update other state accordingly.
 
-        :note: There are three different ways to specify what command refreshing causes
-            to be uses for git:
+        :note: There are three different ways to specify the command that refreshing
+            causes to be used for git:
 
             1. Pass no *path* argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE``
                environment variable. The command name ``git`` is used. It is looked up
@@ -394,9 +394,9 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                in each command run. Setting ``GIT_PYTHON_GIT_EXECUTABLE`` to ``git`` has
                the same effect as not setting it.
 
-            3. Pass a *path* argument. This path, if not absolute, it immediately
+            3. Pass a *path* argument. This path, if not absolute, is immediately
                resolved, relative to the current directory. This resolution occurs at
-               the time of the refresh, and when git commands are run, they are run with
+               the time of the refresh. When git commands are run, they are run using
                that previously resolved path. If a *path* argument is passed, the
                ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted.
 

From c0cd8a8df51224bb56235696e92c96608b51f9e7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 12:06:25 -0500
Subject: [PATCH 469/994] Clarify comment on shell case in _safer_popen_windows

---
 git/cmd.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 75244536d..9f886b53f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -250,9 +250,10 @@ def _safer_popen_windows(
     # When not using a shell, the current process does the search in a CreateProcessW
     # API call, so the variable must be set in our environment. With a shell, this is
     # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
-    # patched. If not, in the rare case the ComSpec environment variable is unset, the
-    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
-    # cases, as here, is simpler and protects against that. (The "1" can be any value.)
+    # patched. If that is unpatched, then in the rare case the ComSpec environment
+    # variable is unset, the search for the shell itself is unsafe. Setting
+    # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and
+    # protects against that. (As above, the "1" can be any value.)
     with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
         return Popen(
             command,

From afd943a96dbf1097546a6777d66827fa875db61c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 12:25:26 -0500
Subject: [PATCH 470/994] Tweak message about GIT_PYTHON_REFRESH for 80-column
 terminals

The fragment of the refresh failure message (which is often written
to a terminal) about the effect of setting GIT_PYTHON_REFRESH to
control refesh failure reporting had previously been formatted with
a maximum line length of 79 columns--in the message itself, not the
code for the message--so that it would fit in a traditional 80
column wide terminal. This remains one of the popular widths to set
for terminal windows in a GUI, so it seems worthwhile to preserve.

In 3a6e3ef (#1815), I had inadvertently made the line one character
too long for that; at 80 columns, it would cause the newline to be
written at the start of the next line, creating an unsightly extra
line break.

This is pretty minor, but what seems to me like an equally good
alternative wording avoids it, so this commit shortens the wording.
---
 git/cmd.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9f886b53f..a850956ec 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -479,7 +479,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                         This initial message can be silenced or aggravated in the future by setting the
                         $%s environment variable. Use one of the following values:
                             - %s: for no message or exception
-                            - %s: for a warning message (logged at level CRITICAL, displayed by default)
+                            - %s: for a warning message (logging level CRITICAL, displayed by default)
                             - %s: for a raised exception
 
                         Example:
@@ -509,7 +509,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
 
                         Use only the following values:
                             - %s: for no message or exception
-                            - %s: for a warning message (logged at level CRITICAL, displayed by default)
+                            - %s: for a warning message (logging level CRITICAL, displayed by default)
                             - %s: for a raised exception
                         """
                         )

From e08066cda1f5e76fb7a4ef795bfe435f88584e99 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 15:05:40 -0500
Subject: [PATCH 471/994] Revise docstrings in git.__init__ and git.cmd

Mainly for formatting.
---
 git/__init__.py |  18 ++--
 git/cmd.py      | 212 +++++++++++++++++++++++++++++-------------------
 2 files changed, 139 insertions(+), 91 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 0ea7821ac..bc97a6eff 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -122,18 +122,22 @@
 def refresh(path: Optional[PathLike] = None) -> None:
     """Convenience method for setting the git executable path.
 
-    :param path: Optional path to the Git executable. If not absolute, it is resolved
+    :param path:
+        Optional path to the Git executable. If not absolute, it is resolved
         immediately, relative to the current directory.
 
-    :note: The *path* parameter is usually omitted and cannot be used to specify a
-        custom command whose location is looked up in a path search on each call. See
+    :note:
+        The *path* parameter is usually omitted and cannot be used to specify a custom
+        command whose location is looked up in a path search on each call. See
         :meth:`Git.refresh` for details on how to achieve this.
 
-    :note: This calls :meth:`Git.refresh` and sets other global configuration according
-        to the effect of doing so. As such, this function should usually be used instead
-        of using :meth:`Git.refresh` or :meth:`FetchInfo.refresh` directly.
+    :note:
+        This calls :meth:`Git.refresh` and sets other global configuration according to
+        the effect of doing so. As such, this function should usually be used instead of
+        using :meth:`Git.refresh` or :meth:`FetchInfo.refresh` directly.
 
-    :note: This function is called automatically, with no arguments, at import time.
+    :note:
+        This function is called automatically, with no arguments, at import time.
     """
     global GIT_OK
     GIT_OK = False
diff --git a/git/cmd.py b/git/cmd.py
index a850956ec..1206641dc 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -111,17 +111,27 @@ def handle_process_output(
 
     This function returns once the finalizer returns.
 
-    :param process: :class:`subprocess.Popen` instance
-    :param stdout_handler: f(stdout_line_string), or None
-    :param stderr_handler: f(stderr_line_string), or None
-    :param finalizer: f(proc) - wait for proc to finish
+    :param process:
+        :class:`subprocess.Popen` instance
+
+    :param stdout_handler:
+        f(stdout_line_string), or None
+
+    :param stderr_handler:
+        f(stderr_line_string), or None
+
+    :param finalizer:
+        f(proc) - wait for proc to finish
+
     :param decode_streams:
-        Assume stdout/stderr streams are binary and decode them before pushing
-        their contents to handlers.
-        Set it to False if ``universal_newlines == True`` (then streams are in
-        text mode) or if decoding must happen later (i.e. for Diffs).
+        Assume stdout/stderr streams are binary and decode them before pushing their
+        contents to handlers.
+        Set this to False if ``universal_newlines == True`` (then streams are in text
+        mode) or if decoding must happen later (i.e. for :class:`git.diff.Diff`s).
+
     :param kill_after_timeout:
         float or None, Default = None
+
         To specify a timeout in seconds for the git command, after which the process
         should be killed.
     """
@@ -147,7 +157,7 @@ def pump_stream(
         except Exception as ex:
             _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
             if "I/O operation on closed file" not in str(ex):
-                # Only reraise if the error was not due to the stream closing
+                # Only reraise if the error was not due to the stream closing.
                 raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
         finally:
             stream.close()
@@ -196,11 +206,11 @@ def pump_stream(
                     "error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds"
                 )
                 if not decode_streams and isinstance(p_stderr, BinaryIO):
-                    #  Assume stderr_handler needs binary input.
+                    # Assume stderr_handler needs binary input.
                     error_str = cast(str, error_str)
                     error_str = error_str.encode()
-                # We ignore typing on the next line because mypy does not like
-                # the way we inferred that stderr takes str or bytes.
+                # We ignore typing on the next line because mypy does not like the way
+                # we inferred that stderr takes str or bytes.
                 stderr_handler(error_str)  # type: ignore
 
     if finalizer:
@@ -225,11 +235,13 @@ def _safer_popen_windows(
     the subprocess, which GitPython usually sets to a repository working tree, can
     itself be searched automatically by the shell. This wrapper covers all those cases.
 
-    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
+    :note:
+        This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
         environment variable during subprocess creation. It also takes care of passing
         Windows-specific process creation flags, but that is unrelated to path search.
 
-    :note: The current implementation contains a race condition on :attr:`os.environ`.
+    :note:
+        The current implementation contains a race condition on :attr:`os.environ`.
         GitPython isn't thread-safe, but a program using it on one thread should ideally
         be able to mutate :attr:`os.environ` on another, without unpredictable results.
         See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
@@ -297,10 +309,11 @@ class Git:
      g.init()                   # calls 'git init' program
      rval = g.ls_files()        # calls 'git ls-files' program
 
-    ``Debugging``
-        Set the GIT_PYTHON_TRACE environment variable print each invocation
-        of the command to stdout.
-        Set its value to 'full' to see details about the returned values.
+    Debugging:
+
+    * Set the ``GIT_PYTHON_TRACE`` environment variable print each invocation of the
+      command to stdout.
+    * Set its value to ``full`` to see details about the returned values.
     """
 
     __slots__ = (
@@ -361,10 +374,12 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     _refresh_env_var = "GIT_PYTHON_REFRESH"
 
     GIT_PYTHON_GIT_EXECUTABLE = None
-    """Provide the full path to the git executable. Otherwise it assumes git is in the path.
+    """Provide the full path to the git executable. Otherwise it assumes git is in the
+    executable search path.
 
-    :note: The git executable is actually found during the refresh step in
-        the top level :mod:`__init__`. It can also be changed by explicitly calling
+    :note:
+        The git executable is actually found during the refresh step in the top level
+        :mod:`__init__`. It can also be changed by explicitly calling
         :func:`git.refresh`.
     """
 
@@ -374,34 +389,38 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         """This gets called by the refresh function (see the top level __init__).
 
-        :param path: Optional path to the git executable. If not absolute, it is
-            resolved immediately, relative to the current directory. (See note below.)
+        :param path:
+            Optional path to the git executable. If not absolute, it is resolved
+            immediately, relative to the current directory. (See note below.)
 
-        :note: The top-level :func:`git.refresh` should be preferred because it calls
-            this method and may also update other state accordingly.
+        :note:
+            The top-level :func:`git.refresh` should be preferred because it calls this
+            method and may also update other state accordingly.
 
-        :note: There are three different ways to specify the command that refreshing
-            causes to be used for git:
+        :note:
+            There are three different ways to specify the command that refreshing causes
+            to be used for git:
 
-            1. Pass no *path* argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE``
+            1. Pass no `path` argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE``
                environment variable. The command name ``git`` is used. It is looked up
                in a path search by the system, in each command run (roughly similar to
                how git is found when running ``git`` commands manually). This is usually
                the desired behavior.
 
-            2. Pass no *path* argument but set the ``GIT_PYTHON_GIT_EXECUTABLE``
+            2. Pass no `path` argument but set the ``GIT_PYTHON_GIT_EXECUTABLE``
                environment variable. The command given as the value of that variable is
                used. This may be a simple command or an arbitrary path. It is looked up
                in each command run. Setting ``GIT_PYTHON_GIT_EXECUTABLE`` to ``git`` has
                the same effect as not setting it.
 
-            3. Pass a *path* argument. This path, if not absolute, is immediately
+            3. Pass a `path` argument. This path, if not absolute, is immediately
                resolved, relative to the current directory. This resolution occurs at
                the time of the refresh. When git commands are run, they are run using
-               that previously resolved path. If a *path* argument is passed, the
+               that previously resolved path. If a `path` argument is passed, the
                ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted.
 
-        :note: Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
+        :note:
+            Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
             attribute, which can be read on the :class:`Git` class or any of its
             instances to check what command is used to run git. This attribute should
             not be confused with the related ``GIT_PYTHON_GIT_EXECUTABLE`` environment
@@ -421,7 +440,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
         cls._refresh_token = object()
 
         # Test if the new git executable path is valid. A GitCommandNotFound error is
-        # spawned by us. A PermissionError is spawned if the git executable cannot be
+        # raised by us. A PermissionError is raised if the git executable cannot be
         # executed for whatever reason.
         has_git = False
         try:
@@ -453,7 +472,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only
                 # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value.
 
-                # Determine what the user wants to happen during the initial refresh we
+                # Determine what the user wants to happen during the initial refresh. We
                 # expect GIT_PYTHON_REFRESH to either be unset or be one of the
                 # following values:
                 #
@@ -550,7 +569,7 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str:
 
     @classmethod
     def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
-        """Remove any backslashes from urls to be written in config files.
+        """Remove any backslashes from URLs to be written in config files.
 
         Windows might create config files containing paths with backslashes,
         but git stops liking them as it will escape the backslashes. Hence we
@@ -572,9 +591,9 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
     def check_unsafe_protocols(cls, url: str) -> None:
         """Check for unsafe protocols.
 
-        Apart from the usual protocols (http, git, ssh),
-        Git allows "remote helpers" that have the form ``<transport>::<address>``.
-        One of these helpers (``ext::``) can be used to invoke any arbitrary command.
+        Apart from the usual protocols (http, git, ssh), Git allows "remote helpers"
+        that have the form ``<transport>::<address>``. One of these helpers (``ext::``)
+        can be used to invoke any arbitrary command.
 
         See:
 
@@ -592,11 +611,11 @@ def check_unsafe_protocols(cls, url: str) -> None:
     def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
         """Check for unsafe options.
 
-        Some options that are passed to `git <command>` can be used to execute
-        arbitrary commands, this are blocked by default.
+        Some options that are passed to ``git <command>`` can be used to execute
+        arbitrary commands. These are blocked by default.
         """
-        # 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".
+        # Options can be of the form `foo`, `--foo bar`, or `--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]
         for option in options:
             for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
@@ -673,9 +692,15 @@ def __getattr__(self, attr: str) -> Any:
         def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
             """Wait for the process and return its status code.
 
-            :param stderr: Previously read value of stderr, in case stderr is already closed.
-            :warn: May deadlock if output or error pipes are used and not handled separately.
-            :raise GitCommandError: If the return status is not 0.
+            :param stderr:
+                Previously read value of stderr, in case stderr is already closed.
+
+            :warn:
+                May deadlock if output or error pipes are used and not handled
+                separately.
+
+            :raise GitCommandError:
+                If the return status is not 0.
             """
             if stderr is None:
                 stderr_b = b""
@@ -725,8 +750,8 @@ def __init__(self, size: int, stream: IO[bytes]) -> None:
             self._size = size
             self._nbr = 0  # Number of bytes read.
 
-            # Special case: If the object is empty, has null bytes, get the
-            # final newline right away.
+            # Special case: If the object is empty, has null bytes, get the final
+            # newline right away.
             if size == 0:
                 stream.read(1)
             # END handle empty streams
@@ -745,7 +770,8 @@ def read(self, size: int = -1) -> bytes:
             data = self._stream.read(size)
             self._nbr += len(data)
 
-            # Check for depletion, read our final byte to make the stream usable by others.
+            # Check for depletion, read our final byte to make the stream usable by
+            # others.
             if self._size - self._nbr == 0:
                 self._stream.read(1)  # final newline
             # END finish reading
@@ -820,7 +846,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         :param working_dir:
            Git directory we should work in. If None, we always work in the current
            directory as returned by :func:`os.getcwd`.
-           It is meant to be the working tree directory if available, or the
+           This is meant to be the working tree directory if available, or the
            ``.git`` directory in case of bare repositories.
         """
         super().__init__()
@@ -844,8 +870,8 @@ def __getattr__(self, name: str) -> Any:
         an object.
 
         :return:
-            Callable object that will execute call :meth:`_call_process` with
-            your arguments.
+            Callable object that will execute call :meth:`_call_process` with your
+            arguments.
         """
         if name.startswith("_"):
             return super().__getattribute__(name)
@@ -857,8 +883,8 @@ def set_persistent_git_options(self, **kwargs: Any) -> None:
 
         :param kwargs:
             A dict of keyword arguments.
-            These arguments are passed as in :meth:`_call_process`, but will be
-            passed to the git command rather than the subcommand.
+            These arguments are passed as in :meth:`_call_process`, but will be passed
+            to the git command rather than the subcommand.
         """
 
         self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
@@ -872,7 +898,7 @@ def working_dir(self) -> Union[None, PathLike]:
     def version_info(self) -> Tuple[int, ...]:
         """
         :return:  tuple with integers representing the major, minor and additional
-            version numbers as parsed from git version. Up to four fields are used.
+            version numbers as parsed from ``git version``. Up to four fields are used.
 
             This value is generated on demand and is cached.
         """
@@ -1021,8 +1047,8 @@ def execute(
             If True, default True, we open stdout on the created process.
 
         :param universal_newlines:
-            if True, pipes will be opened as text, and lines are split at
-            all known line endings.
+            If True, pipes will be opened as text, and lines are split at all known line
+            endings.
 
         :param shell:
             Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
@@ -1057,6 +1083,7 @@ def execute(
             * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
 
             If output_stream is True, the stdout value will be your output stream:
+
             * output_stream if extended_output = False
             * tuple(int(status), output_stream, str(stderr)) if extended_output = True
 
@@ -1254,14 +1281,16 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
         values in a format that can be passed back into this function to revert the
         changes.
 
-        ``Examples``::
+        Examples::
 
             old_env = self.update_environment(PWD='/tmp')
             self.update_environment(**old_env)
 
-        :param kwargs: Environment variables to use for git processes
+        :param kwargs:
+            Environment variables to use for git processes
 
-        :return: Dict that maps environment variables to their old values
+        :return:
+            Dict that maps environment variables to their old values
         """
         old_env = {}
         for key, value in kwargs.items():
@@ -1280,12 +1309,13 @@ def custom_environment(self, **kwargs: Any) -> Iterator[None]:
         """A context manager around the above :meth:`update_environment` method to
         restore the environment back to its previous state after operation.
 
-        ``Examples``::
+        Examples::
 
             with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'):
                 repo.remotes.origin.fetch()
 
-        :param kwargs: See :meth:`update_environment`
+        :param kwargs:
+            See :meth:`update_environment`.
         """
         old_env = self.update_environment(**kwargs)
         try:
@@ -1310,7 +1340,7 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool
         return []
 
     def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]:
-        """Transform Python style kwargs into git command line options."""
+        """Transform Python-style kwargs into git command line options."""
         args = []
         for k, v in kwargs.items():
             if isinstance(v, (list, tuple)):
@@ -1339,7 +1369,8 @@ def __call__(self, **kwargs: Any) -> "Git":
             These arguments are passed as in :meth:`_call_process`, but will be
             passed to the git command rather than the subcommand.
 
-        ``Examples``::
+        Examples::
+
             git(work_tree='/tmp').difftool()
         """
         self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs)
@@ -1369,23 +1400,25 @@ def _call_process(
     def _call_process(
         self, method: str, *args: Any, **kwargs: Any
     ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
-        """Run the given git command with the specified arguments and return
-        the result as a string.
+        """Run the given git command with the specified arguments and return the result
+        as a string.
 
         :param method:
-            The command. Contained ``_`` characters will be converted to dashes,
-            such as in ``ls_files`` to call ``ls-files``.
+            The command. Contained ``_`` characters will be converted to hyphens, such
+            as in ``ls_files`` to call ``ls-files``.
 
         :param args:
             The list of arguments. If None is included, it will be pruned.
-            This allows your commands to call git more conveniently as None
-            is realized as non-existent.
+            This allows your commands to call git more conveniently, as None is realized
+            as non-existent.
 
         :param kwargs:
             Contains key-values for the following:
+
             - The :meth:`execute()` kwds, as listed in :var:`execute_kwargs`.
             - "Command options" to be converted by :meth:`transform_kwargs`.
             - The `'insert_kwargs_after'` key which its value must match one of ``*args``.
+
             It also contains any command options, to be appended after the matched arg.
 
         Examples::
@@ -1396,7 +1429,8 @@ def _call_process(
 
            git rev-list max-count 10 --header master
 
-        :return: Same as :meth:`execute`.
+        :return:
+            Same as :meth:`execute`.
             If no args are given, used :meth:`execute`'s default (especially
             ``as_process = False``, ``stdout_as_string = True``) and return str.
         """
@@ -1447,8 +1481,8 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
 
         :return: (hex_sha, type_string, size_as_int)
 
-        :raise ValueError: If the header contains indication for an error due to
-            incorrect input sha
+        :raise ValueError:
+            If the header contains indication for an error due to incorrect input sha
         """
         tokens = header_line.split()
         if len(tokens) != 3:
@@ -1504,22 +1538,27 @@ def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[st
             raise ValueError("cmd stdin was empty")
 
     def get_object_header(self, ref: str) -> Tuple[str, str, int]:
-        """Use this method to quickly examine the type and size of the object behind
-        the given ref.
+        """Use this method to quickly examine the type and size of the object behind the
+        given ref.
 
-        :note: The method will only suffer from the costs of command invocation
-            once and reuses the command in subsequent calls.
+        :note:
+            The method will only suffer from the costs of command invocation once and
+            reuses the command in subsequent calls.
 
-        :return: (hexsha, type_string, size_as_int)
+        :return:
+            (hexsha, type_string, size_as_int)
         """
         cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
         return self.__get_object_header(cmd, ref)
 
     def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
-        """As get_object_header, but returns object data as well.
+        """Similar to :meth:`get_object_header`, but returns object data as well.
+
+        :return:
+            (hexsha, type_string, size_as_int, data_string)
 
-        :return: (hexsha, type_string, size_as_int, data_string)
-        :note: Not threadsafe.
+        :note:
+            Not threadsafe.
         """
         hexsha, typename, size, stream = self.stream_object_data(ref)
         data = stream.read(size)
@@ -1527,10 +1566,14 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
         return (hexsha, typename, size, data)
 
     def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
-        """As get_object_header, but returns the data as a stream.
+        """Similar to :meth:`get_object_header`, but returns the data as a stream.
 
-        :return: (hexsha, type_string, size_as_int, stream)
-        :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!
+        :return:
+            (hexsha, type_string, size_as_int, stream)
+
+        :note:
+            This method is not threadsafe. You need one independent Command instance per
+            thread to be safe!
         """
         cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
         hexsha, typename, size = self.__get_object_header(cmd, ref)
@@ -1542,7 +1585,8 @@ def clear_cache(self) -> "Git":
 
         Currently persistent commands will be interrupted.
 
-        :return: self
+        :return:
+            self
         """
         for cmd in (self.cat_file_all, self.cat_file_header):
             if cmd:

From 8bb882ef914ac7e39cdc93547d012f8503730849 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 15:07:33 -0500
Subject: [PATCH 472/994] Fix concurrency note for stream_object_data

---
 git/cmd.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 1206641dc..eb6c235c7 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1572,8 +1572,8 @@ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileConte
             (hexsha, type_string, size_as_int, stream)
 
         :note:
-            This method is not threadsafe. You need one independent Command instance per
-            thread to be safe!
+            This method is not threadsafe. You need one independent :class:`Git`
+            instance per thread to be safe!
         """
         cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
         hexsha, typename, size = self.__get_object_header(cmd, ref)

From ba878ef09228176c17112f90434c685f5535a98a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 15:42:38 -0500
Subject: [PATCH 473/994] Reword partial_to_complete_sha_hex note

The git.db.partial_to_complete_sha_hex docstring refers to
"AmbiguousObjects", suggesting the existence of an AmbiguousObject
type (or an AmbiguousObjects type).

Since there appears to be no such type in GitPython (or in gitdb),
this seems to have been written in reference to the condition
expressed by the AmbiguousObjectName exception, which the note
says is not currently able to be raised (such that BadObject is
raised instead) in situations where it would apply.

Since the connection to that exeception is already clear from
context, this commit changes the wording to "ambiguous objects" to
avoid being misread as a reference to a Python class of ambiguous
objects.
---
 git/db.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/db.py b/git/db.py
index 03b631084..1531d663c 100644
--- a/git/db.py
+++ b/git/db.py
@@ -59,7 +59,7 @@ def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
         :raise BadObject:
 
         :note: Currently we only raise :class:`BadObject` as git does not communicate
-            AmbiguousObjects separately.
+            ambiguous objects separately.
         """
         try:
             hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)

From 39587475ff47ce3a7986f0a6f7cd29cbcdcf01bb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 16:39:33 -0500
Subject: [PATCH 474/994] Update CommandError._msg documentation

This converts it from a specially formatted comment to a docstring
(#1734), rewords for clarity, and removes the mention of unicode,
which appears to have been referring to the data type.

(GitPython no longer supports Python 2, and unicode is not a
separate type from str in Python 3, where instead str and bytes
are different types.)
---
 git/exc.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/git/exc.py b/git/exc.py
index 85113bc44..40fb8eea0 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -87,10 +87,13 @@ class CommandError(GitError):
         A non-empty list of argv comprising the command-line.
     """
 
-    #: A unicode print-format with 2 `%s for `<cmdline>` and the rest,
-    #:  e.g.
-    #:     "'%s' failed%s"
     _msg = "Cmd('%s') failed%s"
+    """Format string with 2 ``%s`` for ``<cmdline>`` and the rest.
+
+    For example: ``"'%s' failed%s"``
+
+    Subclasses may override this attribute, provided it is still in this form.
+    """
 
     def __init__(
         self,

From f56e1ac5c5a548d5f74b355f34643053f9b0eb6c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 17:00:02 -0500
Subject: [PATCH 475/994] Tweak code formatting in Remote._set_cache_

For slightly easier reading.
---
 git/remote.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/git/remote.py b/git/remote.py
index 3ccac59de..5ae361097 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -575,7 +575,10 @@ def _set_cache_(self, attr: str) -> None:
         if attr == "_config_reader":
             # NOTE: This is cached as __getattr__ is overridden to return remote config
             # values implicitly, such as in print(r.pushurl).
-            self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name())
+            self._config_reader = SectionConstraint(
+                self.repo.config_reader("repository"),
+                self._config_section_name(),
+            )
         else:
             super()._set_cache_(attr)
 

From fa471fe7b58085e9676c23a74443460a5c1d6ed6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 17:42:52 -0500
Subject: [PATCH 476/994] Fix up Remote.push docstring

- Replace the goo.gl web shortlink with a Sphinx reference that
  displays and also (in built documentation) becomes a hyperlink to
  the documentation on the method being referred to.

- Refer to a related class (which is in another module) as being
  in the module where it is defined, rather than in the top-level
  git module (where it also can be accessed because it is imported
  there, which is reasonable to do, but less clear documentation).

- Tweak punctuation so the effect of passing None is a bit clearer.
---
 git/remote.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 5ae361097..df809a9ac 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -1075,13 +1075,14 @@ def push(
         :param progress:
             Can take one of many value types:
 
-            * None to discard progress information.
+            * None, to discard progress information.
             * A function (callable) that is called with the progress information.
               Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
-              `Click here <http://goo.gl/NPa7st>`__ for a description of all arguments
-              given to the function.
-            * An instance of a class derived from :class:`git.RemoteProgress` that
-              overrides the :meth:`~git.RemoteProgress.update` method.
+              See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a
+              description of all arguments given to the function.
+            * An instance of a class derived from :class:`~git.util.RemoteProgress` that
+              overrides the
+              :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method.
 
         :note: No further progress information is returned after push returns.
 

From 1cd73ba118148e12b5dae7722efd5d86a640f64d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 19:27:43 -0500
Subject: [PATCH 477/994] Revise docstrings in second-level modules

Except for:

- git.cmd, where docstrings were revised in e08066c.

- git.types, where docstring changes may best be made together with
  changes to how imports are organized and documented, which seems
  not to be in the same scope as the changes in this commit.

This change, as well as those in e08066c, are largely along the
lines of #1725, with most revisions here being to docstrings and a
few being to comments.

The major differences between the kinds of docstring changes here
and those ind #1725 are that the changes here push somewhat harder
for consistency and apply some kinds of changes I was reluctant to
apply widely in #1725:

- Wrap all docstrings and comments to 88 columns, except for parts
  that are decisively clearer when not wrapped. Note that semi-
  paragraph changes represented as single newlines are still kept
  where meaningful, which is one reason this is not always the same
  effect as automatic wrapping would produce.

- Avoid code formatting (double backticks) for headings that
  precede sections and code blocks. This was done enough that it
  seems to have been intentional, but it doesn't really have the
  right semantics, and the documentation is currently rendering in
  such a way (including on readthedocs.org) where removing that
  formatting seems clearly better.

- References (single backticks with a role prefix) and code spans
  (double backticks) everywhere applicable, even in the first lines
  of docstrings.

- Single-backticks around parameter names, with no role prefix.
  These were mostly either formatted that way or emphasized (with
  asterisks). This is one of the rare cases that I have used single
  backticks without a role prefix, which ordinarily should be
  avoided, but to get a role for references to a function's
  parameters within that function, a plugin would be needed. In the
  rare case that one function's docstring refers to another
  function's parameters by names those are double-backticked as
  code spans (and where applicable the name of the referred-to
  function is single-backticked with the :func: or :meth: role).

- All sections, such as :param blah:, :note:, and :return:, now
  have a newline before any text in them. This was already often
  but far from always done, and the style was overall inconsistent.
  Of consistent approaches that are clear and easy to write, this
  is the simplest. It also seems to substantially improve
  readability, when taken together with...

- Sections are always separated by a blank line, even if they are
  very short.

- Essentially unlimited use of `~a.b.c`, where applicable, to refer
  and link to the documentation for a.b.c while showing the text
  "a" and revealing "a.b.c" on hover. I had previously somewhat
  limited my use of this tilde notation in case readers of the
  source code itself (where it is not rendered) weren't familiar
  with it, but at the cost of less consistency in when an entity
  was referred to. There remain a couple places in git.util where
  I do not do this because the explicit form `a <a.b.c>`, which is
  equivalent, lined things up better and was thus easier to read.

Those are the major differences between the approach taken here
and in #1725, but not necessarily most of the changes done here
(many of which are the same kinds of revisions as done there).

Note that this commit only modifies some git/*.py files, and there
are more git/**/*.py files that remain to be revised accordingly.
---
 git/compat.py |  15 +--
 git/config.py | 153 ++++++++++++++++++-----------
 git/db.py     |   9 +-
 git/diff.py   |  64 +++++++-----
 git/exc.py    |   9 +-
 git/remote.py | 262 ++++++++++++++++++++++++++++++++------------------
 git/util.py   | 188 ++++++++++++++++++++++--------------
 7 files changed, 439 insertions(+), 261 deletions(-)

diff --git a/git/compat.py b/git/compat.py
index 920e44b7f..3167cecdc 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -35,8 +35,9 @@
 :attr:`sys.platform` checks explicitly, especially in cases where it matters which is
 used.
 
-:note: ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To
-    detect Cygwin, use ``sys.platform == "cygwin"``.
+:note:
+    ``is_win`` is ``False`` on Cygwin, but is often wrongly assumed ``True``. To detect
+    Cygwin, use ``sys.platform == "cygwin"``.
 """
 
 is_posix = os.name == "posix"
@@ -46,9 +47,10 @@
 :attr:`sys.platform` checks explicitly, especially in cases where it matters which is
 used.
 
-:note: For POSIX systems, more detailed information is available in
-    :attr:`sys.platform`, while :attr:`os.name` is always ``"posix"`` on such systems,
-    including macOS (Darwin).
+:note:
+    For POSIX systems, more detailed information is available in :attr:`sys.platform`,
+    while :attr:`os.name` is always ``"posix"`` on such systems, including macOS
+    (Darwin).
 """
 
 is_darwin = sys.platform == "darwin"
@@ -57,7 +59,8 @@
 This is deprecated because it clearer to write out :attr:`os.name` or
 :attr:`sys.platform` checks explicitly.
 
-:note: For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
+:note:
+    For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
     ``sys.platform == "darwin"`.
 """
 
diff --git a/git/config.py b/git/config.py
index 85f754197..b5cff01cd 100644
--- a/git/config.py
+++ b/git/config.py
@@ -73,11 +73,13 @@
 
 
 class MetaParserBuilder(abc.ABCMeta):  # noqa: B024
-    """Utility class wrapping base-class methods into decorators that assure read-only properties."""
+    """Utility class wrapping base-class methods into decorators that assure read-only
+    properties."""
 
     def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder":
         """Equip all base-class methods with a needs_values decorator, and all non-const
-        methods with a set_dirty_and_flush_changes decorator in addition to that.
+        methods with a :func:`set_dirty_and_flush_changes` decorator in addition to
+        that.
         """
         kmm = "_mutating_methods_"
         if kmm in clsdict:
@@ -102,7 +104,8 @@ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParse
 
 
 def needs_values(func: Callable[..., _T]) -> Callable[..., _T]:
-    """Return a method for ensuring we read values (on demand) before we try to access them."""
+    """Return a method for ensuring we read values (on demand) before we try to access
+    them."""
 
     @wraps(func)
     def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
@@ -116,7 +119,8 @@ def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _
 def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]:
     """Return a method that checks whether given non constant function may be called.
 
-    If so, the instance will be set dirty. Additionally, we flush the changes right to disk.
+    If so, the instance will be set dirty. Additionally, we flush the changes right to
+    disk.
     """
 
     def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T:
@@ -136,7 +140,8 @@ class SectionConstraint(Generic[T_ConfigParser]):
 
     It supports all ConfigParser methods that operate on an option.
 
-    :note: If used as a context manager, will release the wrapped ConfigParser.
+    :note:
+        If used as a context manager, will release the wrapped ConfigParser.
     """
 
     __slots__ = ("_config", "_section_name")
@@ -171,8 +176,8 @@ def __getattr__(self, attr: str) -> Any:
         return super().__getattribute__(attr)
 
     def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any:
-        """Call the configuration at the given method which must take a section name
-        as first argument."""
+        """Call the configuration at the given method which must take a section name as
+        first argument."""
         return getattr(self._config, method)(self._section_name, *args, **kwargs)
 
     @property
@@ -254,7 +259,8 @@ def get_config_path(config_level: Lit_config_levels) -> str:
     elif config_level == "repository":
         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
+        # Should not reach here. Will raise ValueError if does. Static typing will warn
+        # about missing elifs.
         assert_never(  # type: ignore[unreachable]
             config_level,
             ValueError(f"Invalid configuration level: {config_level!r}"),
@@ -264,14 +270,15 @@ def get_config_path(config_level: Lit_config_levels) -> str:
 class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     """Implements specifics required to read git style configuration files.
 
-    This variation behaves much like the git.config command such that the configuration
-    will be read on demand based on the filepath given during initialization.
+    This variation behaves much like the ``git config`` command, such that the
+    configuration will be read on demand based on the filepath given during
+    initialization.
 
     The changes will automatically be written once the instance goes out of scope, but
     can be triggered manually as well.
 
-    The configuration file will be locked if you intend to change values preventing other
-    instances to write concurrently.
+    The configuration file will be locked if you intend to change values preventing
+    other instances to write concurrently.
 
     :note:
         The config is case-sensitive even when queried, hence section and option names
@@ -301,7 +308,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     del optvalueonly_source
 
     _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
-    """List of RawConfigParser methods able to change the instance."""
+    """Names of :class:`~configparser.RawConfigParser` methods able to change the
+    instance."""
 
     def __init__(
         self,
@@ -311,8 +319,8 @@ def __init__(
         config_level: Union[Lit_config_levels, None] = None,
         repo: Union["Repo", None] = None,
     ) -> None:
-        """Initialize a configuration reader to read the given file_or_files and to
-        possibly allow changes to it by setting read_only False.
+        """Initialize a configuration reader to read the given `file_or_files` and to
+        possibly allow changes to it by setting `read_only` False.
 
         :param file_or_files:
             A file path or file object, or a sequence of possibly more than one of them.
@@ -385,7 +393,7 @@ def _acquire_lock(self) -> None:
         # END read-only check
 
     def __del__(self) -> None:
-        """Write pending changes if required and release locks"""
+        """Write pending changes if required and release locks."""
         # NOTE: Only consistent in Python 2.
         self.release()
 
@@ -397,10 +405,12 @@ def __exit__(self, *args: Any) -> None:
         self.release()
 
     def release(self) -> None:
-        """Flush changes and release the configuration write lock. This instance must not be used anymore afterwards.
+        """Flush changes and release the configuration write lock. This instance must
+        not be used anymore afterwards.
 
-        In Python 3, it's required to explicitly release locks and flush changes, as __del__ is not called
-        deterministically anymore."""
+        In Python 3, it's required to explicitly release locks and flush changes, as
+        :meth:`__del__` is not called deterministically anymore.
+        """
         # Checking for the lock here makes sure we do not raise during write()
         # in case an invalid parser was created who could not get a lock.
         if self.read_only or (self._lock and not self._lock._has_lock()):
@@ -424,8 +434,9 @@ def optionxform(self, optionstr: str) -> str:
         return optionstr
 
     def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
-        """Originally a direct copy of the Python 2.4 version of RawConfigParser._read,
-        to ensure it uses ordered dicts.
+        """Originally a direct copy of the Python 2.4 version of
+        :meth:`RawConfigParser._read <configparser.RawConfigParser._read>`, to ensure it
+        uses ordered dicts.
 
         The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since
         Python 3.7. This has some other changes, especially that it ignores initial
@@ -525,7 +536,8 @@ def _has_includes(self) -> Union[bool, int]:
     def _included_paths(self) -> List[Tuple[str, str]]:
         """List all paths that must be included to configuration.
 
-        :return: The list of paths, where each path is a tuple of ``(option, value)``.
+        :return:
+            The list of paths, where each path is a tuple of ``(option, value)``.
         """
         paths = []
 
@@ -577,8 +589,11 @@ def read(self) -> None:  # type: ignore[override]
         This will ignore files that cannot be read, possibly leaving an empty
         configuration.
 
-        :return: Nothing
-        :raise IOError: If a file cannot be handled
+        :return:
+            Nothing
+
+        :raise IOError:
+            If a file cannot be handled
         """
         if self._is_initialized:
             return
@@ -591,7 +606,7 @@ def read(self) -> None:  # type: ignore[override]
         elif not isinstance(self._file_or_files, (tuple, list, Sequence)):
             # Could merge with above isinstance once runtime type known.
             files_to_read = [self._file_or_files]
-        else:  # for lists or tuples
+        else:  # For lists or tuples.
             files_to_read = list(self._file_or_files)
         # END ensure we have a copy of the paths to handle
 
@@ -603,7 +618,8 @@ def read(self) -> None:  # type: ignore[override]
 
             if hasattr(file_path, "seek"):
                 # Must be a file-object.
-                file_path = cast(IO[bytes], file_path)  # TODO: Replace with assert to narrow type, once sure.
+                # TODO: Replace cast with assert to narrow type, once sure.
+                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.
@@ -615,8 +631,8 @@ def read(self) -> None:  # type: ignore[override]
                 except IOError:
                     continue
 
-            # Read includes and append those that we didn't handle yet.
-            # We expect all paths to be normalized and absolute (and will ensure that is the case).
+            # Read includes and append those that we didn't handle yet. We expect all
+            # paths to be normalized and absolute (and will ensure that is the case).
             if self._has_includes():
                 for _, include_path in self._included_paths():
                     if include_path.startswith("~"):
@@ -695,8 +711,9 @@ def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]:
     def write(self) -> None:
         """Write changes to our file, if there are changes at all.
 
-        :raise IOError: If this is a read-only writer instance or if we could not obtain
-            a file lock"""
+        :raise IOError:
+            If this is a read-only writer instance or if we could not obtain a file lock
+        """
         self._assure_writable("write")
         if not self._dirty:
             return
@@ -740,7 +757,7 @@ def _assure_writable(self, method_name: str) -> None:
             raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
 
     def add_section(self, section: str) -> None:
-        """Assures added options will stay in order"""
+        """Assures added options will stay in order."""
         return super().add_section(section)
 
     @property
@@ -757,16 +774,18 @@ def get_value(
     ) -> Union[int, float, str, bool]:
         """Get an option's value.
 
-        If multiple values are specified for this option in the section, the
-        last one specified is returned.
+        If multiple values are specified for this option in the section, the last one
+        specified is returned.
 
         :param default:
-            If not None, the given default value will be returned in case
-            the option did not exist
+            If not None, the given default value will be returned in case the option did
+            not exist
 
-        :return: a properly typed value, either int, float or string
+        :return:
+            A properly typed value, either int, float or string
 
-        :raise TypeError: in case the value could not be understood
+        :raise TypeError:
+            In case the value could not be understood.
             Otherwise the exceptions known to the ConfigParser will be raised.
         """
         try:
@@ -790,12 +809,14 @@ def get_values(
         returned.
 
         :param default:
-            If not None, a list containing the given default value will be
-            returned in case the option did not exist
+            If not None, a list containing the given default value will be returned in
+            case the option did not exist.
 
-        :return: a list of properly typed values, either int, float or string
+        :return:
+            A list of properly typed values, either int, float or string
 
-        :raise TypeError: in case the value could not be understood
+        :raise TypeError:
+            In case the value could not be understood.
             Otherwise the exceptions known to the ConfigParser will be raised.
         """
         try:
@@ -849,11 +870,17 @@ def set_value(self, section: str, option: str, value: Union[str, bytes, int, flo
         This will create the section if required, and will not throw as opposed to the
         default ConfigParser 'set' method.
 
-        :param section: Name of the section in which the option resides or should reside
-        :param option: Name of the options whose value to set
-        :param value: Value to set the option to. It must be a string or convertible to
-            a string.
-        :return: This instance
+        :param section:
+            Name of the section in which the option resides or should reside.
+
+        :param option:
+            Name of the options whose value to set.
+
+        :param value:
+            Value to set the option to. It must be a string or convertible to a string.
+
+        :return:
+            This instance
         """
         if not self.has_section(section):
             self.add_section(section)
@@ -865,15 +892,22 @@ def set_value(self, section: str, option: str, value: Union[str, bytes, int, flo
     def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser":
         """Add a value for the given option in section.
 
-        This will create the section if required, and will not throw as opposed to the default
-        ConfigParser 'set' method. The value becomes the new value of the option as returned
-        by 'get_value', and appends to the list of values returned by 'get_values`'.
+        This will create the section if required, and will not throw as opposed to the
+        default ConfigParser 'set' method. The value becomes the new value of the option
+        as returned by 'get_value', and appends to the list of values returned by
+        'get_values'.
 
-        :param section: Name of the section in which the option resides or should reside
-        :param option: Name of the option
-        :param value: Value to add to option. It must be a string or convertible
-            to a string
-        :return: This instance
+        :param section:
+            Name of the section in which the option resides or should reside.
+
+        :param option:
+            Name of the option.
+
+        :param value:
+            Value to add to option. It must be a string or convertible to a string.
+
+        :return:
+            This instance
         """
         if not self.has_section(section):
             self.add_section(section)
@@ -883,8 +917,12 @@ def add_value(self, section: str, option: str, value: Union[str, bytes, int, flo
     def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
         """Rename the given section to new_name.
 
-        :raise ValueError: If ``section`` doesn't exist
-        :raise ValueError: If a section with ``new_name`` does already exist
+        :raise ValueError:
+            If:
+
+            * ``section`` doesn't exist.
+            * A section with ``new_name`` does already exist.
+
         :return: This instance
         """
         if not self.has_section(section):
@@ -898,6 +936,7 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
             new_section.setall(k, vs)
         # END for each value to copy
 
-        # This call writes back the changes, which is why we don't have the respective decorator.
+        # This call writes back the changes, which is why we don't have the respective
+        # decorator.
         self.remove_section(section)
         return self
diff --git a/git/db.py b/git/db.py
index 1531d663c..dff59f47a 100644
--- a/git/db.py
+++ b/git/db.py
@@ -31,8 +31,9 @@ class GitCmdObjectDB(LooseObjectDB):
 
     It will create objects only in the loose object database.
 
-    :note: For now, we use the git command to do all the lookup, just until we
-        have packs and the other implementations.
+    :note:
+        For now, we use the git command to do all the lookup, just until we have packs
+        and the other implementations.
     """
 
     def __init__(self, root_path: PathLike, git: "Git") -> None:
@@ -56,9 +57,11 @@ def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
         :return: Full binary 20 byte sha from the given partial hexsha
 
         :raise AmbiguousObjectName:
+
         :raise BadObject:
 
-        :note: Currently we only raise :class:`BadObject` as git does not communicate
+        :note:
+            Currently we only raise :class:`BadObject` as git does not communicate
             ambiguous objects separately.
         """
         try:
diff --git a/git/diff.py b/git/diff.py
index aba1a1080..7205136d0 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -84,8 +84,9 @@ class Diffable:
     compatible type.
 
     :note:
-        Subclasses require a repo member as it is the case for Object instances, for
-        practical reasons we do not derive from Object.
+        Subclasses require a repo member as it is the case for
+        :class:`~git.objects.base.Object` instances, for practical reasons we do not
+        derive from :class:`~git.objects.base.Object`.
     """
 
     __slots__ = ()
@@ -135,13 +136,13 @@ def diff(
             to be read and diffed.
 
         :param kwargs:
-            Additional arguments passed to git-diff, such as ``R=True`` to swap both
+            Additional arguments passed to ``git diff``, such as ``R=True`` to swap both
             sides of the diff.
 
         :return: git.DiffIndex
 
         :note:
-            On a bare repository, 'other' needs to be provided as
+            On a bare repository, `other` needs to be provided as
             :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or
             :class:`~git.objects.commit.Commit`, or a git command error will occur.
         """
@@ -183,7 +184,7 @@ def diff(
 
         args.insert(0, self)
 
-        # paths is list here, or None.
+        # paths is a list here, or None.
         if paths:
             args.append("--")
             args.extend(paths)
@@ -203,7 +204,7 @@ def diff(
 
 
 class DiffIndex(List[T_Diff]):
-    """An Index for diffs, allowing a list of Diffs to be queried by the diff
+    R"""An Index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
     properties.
 
     The class improves the diff handling convenience.
@@ -255,27 +256,27 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
 class Diff:
     """A Diff contains diff information between two Trees.
 
-    It contains two sides a and b of the diff, members are prefixed with
-    "a" and "b" respectively to indicate that.
+    It contains two sides a and b of the diff. Members are prefixed with "a" and "b"
+    respectively to indicate that.
 
     Diffs keep information about the changed blob objects, the file mode, renames,
     deletions and new files.
 
     There are a few cases where None has to be expected as member variable value:
 
-    ``New File``::
+    New File::
 
         a_mode is None
         a_blob is None
         a_path is None
 
-    ``Deleted File``::
+    Deleted File::
 
         b_mode is None
         b_blob is None
         b_path is None
 
-    ``Working Tree Blobs``
+    Working Tree Blobs:
 
         When comparing to working trees, the working tree blob will have a null hexsha
         as a corresponding object does not yet exist. The mode will be null as well.
@@ -469,7 +470,8 @@ def renamed(self) -> bool:
         """
         :return: True if the blob of our diff has been renamed
 
-        :note: This property is deprecated.
+        :note:
+            This property is deprecated.
             Please use the :attr:`renamed_file` property instead.
         """
         return self.renamed_file
@@ -494,11 +496,17 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m
 
     @classmethod
     def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex:
-        """Create a new DiffIndex from the given process output which must be in patch format.
+        """Create a new :class:`DiffIndex` from the given process output which must be
+        in patch format.
 
-        :param repo: The repository we are operating on
-        :param proc: ``git diff`` process to read from (supports :class:`Git.AutoInterrupt` wrapper)
-        :return: git.DiffIndex
+        :param repo: The repository we are operating on.
+
+        :param proc:
+            ``git diff`` process to read from
+            (supports :class:`Git.AutoInterrupt` wrapper).
+
+        :return:
+            :class:`DiffIndex`
         """
 
         # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
@@ -539,14 +547,14 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
             a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
             b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
 
-            # Our only means to find the actual text is to see what has not been matched by our regex,
-            # and then retro-actively assign it to our index.
+            # Our only means to find the actual text is to see what has not been matched
+            # by our regex, and then retro-actively assign it to our index.
             if previous_header is not None:
                 index[-1].diff = text[previous_header.end() : _header.start()]
             # END assign actual diff
 
-            # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid.
-            # We just use the one mode we should have parsed.
+            # Make sure the mode is set if the path is set. Otherwise the resulting blob
+            # is invalid. We just use the one mode we should have parsed.
             a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
             b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
             index.append(
@@ -610,7 +618,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
             rename_from = None
             rename_to = None
 
-            # NOTE: We cannot conclude from the existence of a blob to change type
+            # NOTE: We cannot conclude from the existence of a blob to change type,
             # as diffs with the working do not have blobs yet.
             if change_type == "D":
                 b_blob_id = None  # Optional[str]
@@ -654,11 +662,17 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
 
     @classmethod
     def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex":
-        """Create a new DiffIndex from the given process output which must be in raw format.
+        """Create a new :class:`DiffIndex` from the given process output which must be
+        in raw format.
 
-        :param repo: The repository we are operating on
-        :param proc: Process to read output from
-        :return: git.DiffIndex
+        :param repo:
+            The repository we are operating on.
+
+        :param proc:
+            Process to read output from.
+
+        :return:
+            :class:`DiffIndex`
         """
         # handles
         # :100644 100644 687099101... 37c5e30c8... M    .gitignore
diff --git a/git/exc.py b/git/exc.py
index 40fb8eea0..276d79e42 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -81,7 +81,8 @@ class UnsafeOptionError(GitError):
 
 
 class CommandError(GitError):
-    """Base class for exceptions thrown at every stage of `Popen()` execution.
+    """Base class for exceptions thrown at every stage of :class:`~subprocess.Popen`
+    execution.
 
     :param command:
         A non-empty list of argv comprising the command-line.
@@ -135,8 +136,8 @@ def __str__(self) -> str:
 
 
 class GitCommandNotFound(CommandError):
-    """Thrown if we cannot find the `git` executable in the PATH or at the path given by
-    the GIT_PYTHON_GIT_EXECUTABLE environment variable."""
+    """Thrown if we cannot find the `git` executable in the ``PATH`` or at the path
+    given by the ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable."""
 
     def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
         super().__init__(command, cause)
@@ -187,7 +188,7 @@ def __str__(self) -> str:
 
 
 class CacheError(GitError):
-    """Base for all errors related to the git index, which is called cache
+    """Base for all errors related to the git index, which is called "cache"
     internally."""
 
 
diff --git a/git/remote.py b/git/remote.py
index df809a9ac..5bee9edf5 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -70,13 +70,15 @@ def add_progress(
     git: Git,
     progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None],
 ) -> Any:
-    """Add the --progress flag to the given kwargs dict if supported by the
-    git command.
+    """Add the ``--progress`` flag to the given `kwargs` dict if supported by the git
+    command.
 
-    :note: If the actual progress in the given progress instance is not
-        given, we do not request any progress.
+    :note:
+        If the actual progress in the given progress instance is not given, we do not
+        request any progress.
 
-    :return: possibly altered kwargs
+    :return:
+        Possibly altered `kwargs`
     """
     if progress is not None:
         v = git.version_info[:2]
@@ -108,7 +110,8 @@ def to_progress_instance(progress: RemoteProgress) -> RemoteProgress:
 def to_progress_instance(
     progress: Union[Callable[..., Any], RemoteProgress, None]
 ) -> Union[RemoteProgress, CallableRemoteProgress]:
-    """Given the 'progress' return a suitable object derived from RemoteProgress."""
+    """Given the `progress` return a suitable object derived from
+    :class:`~git.util.RemoteProgress`."""
     # New API only needs progress as a function.
     if callable(progress):
         return CallableRemoteProgress(progress)
@@ -276,7 +279,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn:  # ->
 
 
 class PushInfoList(IterableList[PushInfo]):
-    """IterableList of PushInfo objects."""
+    """IterableList of :class:`PushInfo` objects."""
 
     def __new__(cls) -> "PushInfoList":
         return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
@@ -380,8 +383,8 @@ def commit(self) -> Commit_ish:
 
     @classmethod
     def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
-        """Parse information from the given line as returned by git-fetch -v
-        and return a new FetchInfo object representing this information.
+        """Parse information from the given line as returned by ``git-fetch -v`` and
+        return a new :class:`FetchInfo` object representing this information.
 
         We can handle a line as follows:
         "%c %-\\*s %-\\*s -> %s%s"
@@ -391,7 +394,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         + means success forcing update
         - means a tag was updated
         * means birth of new branch or tag
-        = means the head was up to date ( and not moved )
+        = means the head was up to date (and not moved)
         ' ' means a fast-forward
 
         fetch line is the corresponding line from FETCH_HEAD, like
@@ -455,15 +458,17 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         if remote_local_ref_str == "FETCH_HEAD":
             ref_type = SymbolicReference
         elif ref_type_name == "tag" or is_tag_operation:
-            # The ref_type_name can be branch, whereas we are still seeing a tag operation.
-            # It happens during testing, which is based on actual git operations.
+            # The ref_type_name can be branch, whereas we are still seeing a tag
+            # operation. It happens during testing, which is based on actual git
+            # operations.
             ref_type = TagReference
         elif ref_type_name in ("remote-tracking", "branch"):
-            # Note: remote-tracking is just the first part of the 'remote-tracking branch' token.
-            # We don't parse it correctly, but its enough to know what to do, and it's new in git 1.7something.
+            # Note: remote-tracking is just the first part of the
+            # 'remote-tracking branch' token. We don't parse it correctly, but it's
+            # enough to know what to do, and it's new in git 1.7something.
             ref_type = RemoteReference
         elif "/" in ref_type_name:
-            # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*',
+            # If the fetch spec look something like '+refs/pull/*:refs/heads/pull/*',
             # and is thus pretty much anything the user wants, we will have trouble
             # determining what's going on. For now, we assume the local ref is a Head.
             ref_type = Head
@@ -475,17 +480,19 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         if ref_type is SymbolicReference:
             remote_local_ref = ref_type(repo, "FETCH_HEAD")
         else:
-            # Determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories.
-            # It is not clear sometimes where exactly the item is, unless we have an absolute path as
-            # indicated by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is
-            # when it will have the 'tags/' subdirectory in its path.
-            # We don't want to test for actual existence, but try to figure everything out analytically.
+            # Determine prefix. Tags are usually pulled into refs/tags; they may have
+            # subdirectories. It is not clear sometimes where exactly the item is,
+            # unless we have an absolute path as indicated by the 'ref/' prefix.
+            # Otherwise even a tag could be in refs/remotes, which is when it will have
+            # the 'tags/' subdirectory in its path. We don't want to test for actual
+            # existence, but try to figure everything out analytically.
             ref_path: Optional[PathLike] = None
             remote_local_ref_str = remote_local_ref_str.strip()
 
             if remote_local_ref_str.startswith(Reference._common_path_default + "/"):
-                # Always use actual type if we get absolute paths.
-                # Will always be the case if something is fetched outside of refs/remotes (if its not a tag).
+                # Always use actual type if we get absolute paths. This will always be
+                # the case if something is fetched outside of refs/remotes (if its not a
+                # tag).
                 ref_path = remote_local_ref_str
                 if ref_type is not TagReference and not remote_local_ref_str.startswith(
                     RemoteReference._common_path_default + "/"
@@ -499,8 +506,8 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
                 ref_path = join_path(ref_type._common_path_default, remote_local_ref_str)
             # END obtain refpath
 
-            # Even though the path could be within the git conventions, we make
-            # sure we respect whatever the user wanted, and disabled path checking.
+            # Even though the path could be within the git conventions, we make sure we
+            # respect whatever the user wanted, and disabled path checking.
             remote_local_ref = ref_type(repo, ref_path, check_path=False)
         # END create ref instance
 
@@ -517,10 +524,11 @@ class Remote(LazyMixin, IterableObj):
     """Provides easy read and write access to a git remote.
 
     Everything not part of this interface is considered an option for the current
-    remote, allowing constructs like remote.pushurl to query the pushurl.
+    remote, allowing constructs like ``remote.pushurl`` to query the pushurl.
 
-    :note: When querying configuration, the configuration accessor will be cached
-        to speed up subsequent accesses.
+    :note:
+        When querying configuration, the configuration accessor will be cached to speed
+        up subsequent accesses.
     """
 
     __slots__ = ("repo", "name", "_config_reader")
@@ -547,21 +555,24 @@ class Remote(LazyMixin, IterableObj):
     def __init__(self, repo: "Repo", name: str) -> None:
         """Initialize a remote instance.
 
-        :param repo: The repository we are a remote of
-        :param name: The name of the remote, e.g. 'origin'
+        :param repo:
+            The repository we are a remote of.
+
+        :param name:
+            The name of the remote, e.g. 'origin'.
         """
         self.repo = repo
         self.name = name
         self.url: str
 
     def __getattr__(self, attr: str) -> Any:
-        """Allows to call this instance like
-        remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name."""
+        """Allows to call this instance like ``remote.special(*args, **kwargs)`` to
+        call ``git remote special self.name``."""
         if attr == "_config_reader":
             return super().__getattr__(attr)
 
-        # Sometimes, probably due to a bug in Python itself, we are being called
-        # even though a slot of the same name exists.
+        # Sometimes, probably due to a bug in Python itself, we are being called even
+        # though a slot of the same name exists.
         try:
             return self._config_reader.get(attr)
         except cp.NoOptionError:
@@ -599,7 +610,8 @@ def __hash__(self) -> int:
 
     def exists(self) -> bool:
         """
-        :return: True if this is a valid, existing remote.
+        :return:
+            True if this is a valid, existing remote.
             Valid remotes have an entry in the repository's configuration.
         """
         try:
@@ -627,14 +639,21 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote
     def set_url(
         self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
     ) -> "Remote":
-        """Configure URLs on current remote (cf command git remote set_url).
+        """Configure URLs on current remote (cf command ``git remote set-url``).
 
         This command manages URLs on the remote.
 
-        :param new_url: String being the URL to add as an extra remote URL
-        :param old_url: When set, replaces this URL with new_url for the remote
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :return: self
+        :param new_url:
+            String being the URL to add as an extra remote URL.
+
+        :param old_url:
+            When set, replaces this URL with `new_url` for the remote.
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :return:
+            self
         """
         if not allow_unsafe_protocols:
             Git.check_unsafe_protocols(new_url)
@@ -647,25 +666,33 @@ def set_url(
         return self
 
     def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
-        """Adds a new url on current remote (special case of git remote set_url).
+        """Adds a new url on current remote (special case of ``git remote set-url``).
 
         This command adds new URLs to a given remote, making it possible to have
         multiple URLs for a single remote.
 
-        :param url: String being the URL to add as an extra remote URL
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :return: self
+        :param url:
+            String being the URL to add as an extra remote URL.
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :return:
+            self
         """
         return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
 
     def delete_url(self, url: str, **kwargs: Any) -> "Remote":
-        """Deletes a new url on current remote (special case of git remote set_url)
+        """Deletes a new url on current remote (special case of ``git remote set-url``)
 
         This command deletes new URLs to a given remote, making it possible to have
         multiple URLs for a single remote.
 
-        :param url: String being the URL to delete from the remote
-        :return: self
+        :param url:
+            String being the URL to delete from the remote.
+
+        :return:
+            self
         """
         return self.set_url(url, delete=True)
 
@@ -706,9 +733,12 @@ def urls(self) -> Iterator[str]:
     def refs(self) -> IterableList[RemoteReference]:
         """
         :return:
-            IterableList of RemoteReference objects. It is prefixed, allowing
-            you to omit the remote path portion, e.g.::
-            remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')
+            :class:`~git.util.IterableList` of :class:`git.refs.remote.RemoteReference`
+            objects.
+
+            It is prefixed, allowing you to omit the remote path portion, e.g.::
+
+                remote.refs.master  # yields RemoteReference('/refs/remotes/origin/master')
         """
         out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name)
         out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name))
@@ -718,12 +748,13 @@ def refs(self) -> IterableList[RemoteReference]:
     def stale_refs(self) -> IterableList[Reference]:
         """
         :return:
-            IterableList RemoteReference objects that do not have a corresponding
-            head in the remote reference anymore as they have been deleted on the
-            remote side, but are still available locally.
+            :class:`~git.util.IterableList` of :class:`git.refs.remote.RemoteReference`
+            objects that do not have a corresponding head in the remote reference
+            anymore as they have been deleted on the remote side, but are still
+            available locally.
 
-            The IterableList is prefixed, hence the 'origin' must be omitted. See
-            'refs' property for an example.
+            The :class:`~git.util.IterableList` is prefixed, hence the 'origin' must be
+            omitted. See :attr:`refs` property for an example.
 
             To make things more complicated, it can be possible for the list to include
             other kinds of references, for example, tag references, if these are stale
@@ -752,13 +783,26 @@ def stale_refs(self) -> IterableList[Reference]:
     def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
         """Create a new remote to the given repository.
 
-        :param repo: Repository instance that is to receive the new remote
-        :param name: Desired name of the remote
-        :param url: URL which corresponds to the remote's name
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param kwargs: Additional arguments to be passed to the git-remote add command
-        :return: New Remote instance
-        :raise GitCommandError: in case an origin with that name already exists
+        :param repo:
+            Repository instance that is to receive the new remote.
+
+        :param name:
+            Desired name of the remote.
+
+        :param url:
+            URL which corresponds to the remote's name.
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :param kwargs:
+            Additional arguments to be passed to the ``git remote add`` command.
+
+        :return:
+            New :class:`Remote` instance
+
+        :raise GitCommandError:
+            In case an origin with that name already exists.
         """
         scmd = "add"
         kwargs["insert_kwargs_after"] = scmd
@@ -777,7 +821,8 @@ def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
     def remove(cls, repo: "Repo", name: str) -> str:
         """Remove the remote with the given name.
 
-        :return: The passed remote name to remove
+        :return:
+            The passed remote name to remove
         """
         repo.git.remote("rm", name)
         if isinstance(name, cls):
@@ -788,9 +833,10 @@ def remove(cls, repo: "Repo", name: str) -> str:
     rm = remove
 
     def rename(self, new_name: str) -> "Remote":
-        """Rename self to the given new_name.
+        """Rename self to the given `new_name`.
 
-        :return: self
+        :return:
+            self
         """
         if self.name == new_name:
             return self
@@ -802,12 +848,15 @@ def rename(self, new_name: str) -> "Remote":
         return self
 
     def update(self, **kwargs: Any) -> "Remote":
-        """Fetch all changes for this remote, including new branches which will
-        be forced in (in case your local remote branch is not part the new remote
-        branch's ancestry anymore).
+        """Fetch all changes for this remote, including new branches which will be
+        forced in (in case your local remote branch is not part the new remote branch's
+        ancestry anymore).
+
+        :param kwargs:
+            Additional arguments passed to ``git remote update``.
 
-        :param kwargs: Additional arguments passed to git-remote update
-        :return: self
+        :return:
+            self
         """
         scmd = "update"
         kwargs["insert_kwargs_after"] = scmd
@@ -966,9 +1015,9 @@ def fetch(
 
             Taken from the git manual, gitglossary(7).
 
-            Fetch supports multiple refspecs (as the
-            underlying git-fetch does) - supplying a list rather than a string
-            for 'refspec' will make use of this facility.
+            Fetch supports multiple refspecs (as the underlying git-fetch does) -
+            supplying a list rather than a string for 'refspec' will make use of this
+            facility.
 
         :param progress: See :meth:`push` method.
 
@@ -978,15 +1027,18 @@ def fetch(
             To specify a timeout in seconds for the git command, after which the process
             should be killed. It is set to None by default.
 
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
 
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
 
-        :param kwargs: Additional arguments to be passed to git-fetch.
+        :param kwargs:
+            Additional arguments to be passed to ``git fetch``.
 
         :return:
-            IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed
-            information about the fetch results
+            IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing
+            detailed information about the fetch results
 
         :note:
             As fetch does not provide progress information to non-ttys, we cannot make
@@ -1030,13 +1082,26 @@ def pull(
         """Pull changes from the given branch, being the same as a fetch followed
         by a merge of branch with your local branch.
 
-        :param refspec: See :meth:`fetch` method
-        :param progress: See :meth:`push` method
-        :param kill_after_timeout: See :meth:`fetch` method
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :param kwargs: Additional arguments to be passed to git-pull
-        :return: Please see :meth:`fetch` method
+        :param refspec:
+            See :meth:`fetch` method.
+
+        :param progress:
+            See :meth:`push` method.
+
+        :param kill_after_timeout:
+            See :meth:`fetch` method.
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
+
+        :param kwargs:
+            Additional arguments to be passed to ``git pull``.
+
+        :return:
+            Please see :meth:`fetch` method
         """
         if refspec is None:
             # No argument refspec, then ensure the repo's config has a fetch refspec.
@@ -1070,7 +1135,8 @@ def push(
     ) -> PushInfoList:
         """Push changes from source branch in refspec to target branch in refspec.
 
-        :param refspec: See :meth:`fetch` method.
+        :param refspec:
+            See :meth:`fetch` method.
 
         :param progress:
             Can take one of many value types:
@@ -1084,26 +1150,31 @@ def push(
               overrides the
               :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method.
 
-        :note: No further progress information is returned after push returns.
+        :note:
+            No further progress information is returned after push returns.
 
         :param kill_after_timeout:
             To specify a timeout in seconds for the git command, after which the process
             should be killed. It is set to None by default.
 
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
 
         :param allow_unsafe_options:
-            Allow unsafe options to be used, like --receive-pack.
+            Allow unsafe options to be used, like ``--receive-pack``.
 
-        :param kwargs: Additional arguments to be passed to git-push.
+        :param kwargs: Additional arguments to be passed to ``git push``.
 
         :return:
             A :class:`PushInfoList` object, where each list member represents an
             individual head which had been updated on the remote side.
+
             If the push contains rejected heads, these will have the
             :attr:`PushInfo.ERROR` bit set in their flags.
-            If the operation fails completely, the length of the returned PushInfoList
-            will be 0.
+
+            If the operation fails completely, the length of the returned
+            :class:`PushInfoList` will be 0.
+
             Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on
             any failure.
         """
@@ -1133,8 +1204,9 @@ def push(
     def config_reader(self) -> SectionConstraint[GitConfigParser]:
         """
         :return:
-            GitConfigParser compatible object able to read options for only our remote.
-            Hence you may simple type config.get("pushurl") to obtain the information.
+            :class:`~git.config.GitConfigParser` compatible object able to read options
+            for only our remote. Hence you may simply type ``config.get("pushurl")`` to
+            obtain the information.
         """
         return self._config_reader
 
@@ -1148,7 +1220,9 @@ def _clear_cache(self) -> None:
     @property
     def config_writer(self) -> SectionConstraint:
         """
-        :return: GitConfigParser compatible object able to write options for this remote.
+        :return:
+            :class:`~git.config.GitConfigParser`-compatible object able to write options
+            for this remote.
 
         :note:
             You can only own one writer at a time - delete it to release the
diff --git a/git/util.py b/git/util.py
index 03d62ffc3..30b78f7b2 100644
--- a/git/util.py
+++ b/git/util.py
@@ -111,10 +111,11 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
     """Read a boolean flag from an environment variable on Windows.
 
     :return:
-        On Windows, the flag, or the ``default`` value if absent or ambiguous.
-        On all other operating systems, ``False``.
+        On Windows, the flag, or the `default` value if absent or ambiguous.
+        On all other operating systems, False.
 
-    :note: This only accesses the environment on Windows.
+    :note:
+        This only accesses the environment on Windows.
     """
     if os.name != "nt":
         return False
@@ -151,8 +152,8 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
 
 
 def unbare_repo(func: Callable[..., T]) -> Callable[..., T]:
-    """Methods with this decorator raise :class:`.exc.InvalidGitRepositoryError` if they
-    encounter a bare repository."""
+    """Methods with this decorator raise
+    :class:`~git.exc.InvalidGitRepositoryError` if they encounter a bare repository."""
 
     from .exc import InvalidGitRepositoryError
 
@@ -206,7 +207,10 @@ def rmtree(path: PathLike) -> None:
     """
 
     def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
-        """Callback for :func:`shutil.rmtree`. Works either as ``onexc`` or ``onerror``."""
+        """Callback for :func:`shutil.rmtree`.
+
+        This works as either a ``onexc`` or ``onerror`` style callback.
+        """
         # Is the error an access error?
         os.chmod(path, stat.S_IWUSR)
 
@@ -228,7 +232,8 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
 
 
 def rmfile(path: PathLike) -> None:
-    """Ensure file deleted also on *Windows* where read-only files need special treatment."""
+    """Ensure file deleted also on *Windows* where read-only files need special
+    treatment."""
     if osp.isfile(path):
         if os.name == "nt":
             os.chmod(path, 0o777)
@@ -239,7 +244,8 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
     """Copy all data from the source stream into the destination stream in chunks
     of size chunk_size.
 
-    :return: Number of bytes written
+    :return:
+        Number of bytes written
     """
     br = 0
     while True:
@@ -473,12 +479,13 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
 
 
 def get_user_id() -> str:
-    """:return: string identifying the currently active system user as name@node"""
+    """:return: String identifying the currently active system user as ``name@node``"""
     return "%s@%s" % (getpass.getuser(), platform.node())
 
 
 def finalize_process(proc: Union[subprocess.Popen, "Git.AutoInterrupt"], **kwargs: Any) -> None:
-    """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly"""
+    """Wait for the process (clone, fetch, pull or push) and handle its errors
+    accordingly."""
     # TODO: No close proc-streams??
     proc.wait(**kwargs)
 
@@ -541,9 +548,9 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
 
 
 class RemoteProgress:
-    """
-    Handler providing an interface to parse progress information emitted by git-push
-    and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.
+    """Handler providing an interface to parse progress information emitted by
+    ``git push`` and ``git fetch`` and to dispatch callbacks allowing subclasses to
+    react to the progress.
     """
 
     _num_op_codes: int = 9
@@ -580,8 +587,8 @@ def __init__(self) -> None:
         self.other_lines: List[str] = []
 
     def _parse_progress_line(self, line: AnyStr) -> None:
-        """Parse progress information from the given line as retrieved by git-push
-        or git-fetch.
+        """Parse progress information from the given line as retrieved by ``git push``
+        or ``git fetch``.
 
         - Lines that do not contain progress info are stored in :attr:`other_lines`.
         - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``)
@@ -685,8 +692,8 @@ def _parse_progress_line(self, line: AnyStr) -> None:
     def new_message_handler(self) -> Callable[[str], None]:
         """
         :return:
-            A progress handler suitable for handle_process_output(), passing lines on to
-            this Progress handler in a suitable format
+            A progress handler suitable for :func:`~git.cmd.handle_process_output`,
+            passing lines on to this progress handler in a suitable format.
         """
 
         def handler(line: AnyStr) -> None:
@@ -712,25 +719,29 @@ def update(
         :param op_code:
             Integer allowing to be compared against Operation IDs and stage IDs.
 
-            Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation
-            ID as well as END. It may be that BEGIN and END are set at once in case only
-            one progress message was emitted due to the speed of the operation.
-            Between BEGIN and END, none of these flags will be set.
+            Stage IDs are :attr:`BEGIN` and :attr:`END`. :attr:`BEGIN` will only be set
+            once for each Operation ID as well as :attr:`END`. It may be that
+            :attr:`BEGIN` and :attr:`END` are set at once in case only one progress
+            message was emitted due to the speed of the operation. Between :attr:`BEGIN`
+            and :attr:`END`, none of these flags will be set.
 
-            Operation IDs are all held within the OP_MASK. Only one Operation ID will
-            be active per call.
+            Operation IDs are all held within the :attr:`OP_MASK`. Only one Operation ID
+            will be active per call.
 
-        :param cur_count: Current absolute count of items.
+        :param cur_count:
+            Current absolute count of items.
 
         :param max_count:
-            The maximum count of items we expect. It may be None in case there is
-            no maximum number of items or if it is (yet) unknown.
+            The maximum count of items we expect. It may be None in case there is no
+            maximum number of items or if it is (yet) unknown.
 
         :param message:
-            In case of the 'WRITING' operation, it contains the amount of bytes
+            In case of the :attr:`WRITING` operation, it contains the amount of bytes
             transferred. It may possibly be used for other purposes as well.
 
-        You may read the contents of the current line in ``self._cur_line``.
+        :note:
+            You may read the contents of the current line in
+            :attr:`self._cur_line <_cur_line>`.
         """
         pass
 
@@ -793,11 +804,13 @@ def __repr__(self) -> str:
     def _from_string(cls, string: str) -> "Actor":
         """Create an Actor from a string.
 
-        :param string: The string, which is expected to be in regular git format::
+        :param string:
+            The string, which is expected to be in regular git format::
 
-            John Doe <jdoe@example.com>
+                John Doe <jdoe@example.com>
 
-        :return: Actor
+        :return:
+            :class:`Actor`
         """
         m = cls.name_email_regex.search(string)
         if m:
@@ -857,18 +870,19 @@ def committer(cls, config_reader: Union[None, "GitConfigParser", "SectionConstra
         """
         :return: Actor instance corresponding to the configured committer. It behaves
             similar to the git implementation, such that the environment will override
-            configuration values of config_reader. If no value is set at all, it will be
-            generated.
+            configuration values of `config_reader`. If no value is set at all, it will
+            be generated.
 
-        :param config_reader: ConfigReader to use to retrieve the values from in case
-            they are not set in the environment.
+        :param config_reader:
+            ConfigReader to use to retrieve the values from in case they are not set in
+            the environment.
         """
         return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader)
 
     @classmethod
     def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor":
-        """Same as committer(), but defines the main author. It may be specified in the
-        environment, but defaults to the committer."""
+        """Same as :meth:`committer`, but defines the main author. It may be specified
+        in the environment, but defaults to the committer."""
         return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
 
 
@@ -877,7 +891,7 @@ class Stats:
     Represents stat information as presented by git at the end of a merge. It is
     created from the output of a diff operation.
 
-    ``Example``::
+    Example::
 
      c = Commit( sha1 )
      s = c.stats
@@ -907,9 +921,10 @@ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]):
 
     @classmethod
     def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
-        """Create a Stat object from output retrieved by git-diff.
+        """Create a :class:`Stats` object from output retrieved by ``git diff``.
 
-        :return: git.Stat
+        :return:
+            :class:`git.Stats`
         """
 
         hsh: HSH_TD = {
@@ -936,11 +951,12 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
 class IndexFileSHA1Writer:
     """Wrapper around a file-like object that remembers the SHA1 of
     the data written to it. It will write a sha when the stream is closed
-    or if the asked for explicitly using write_sha.
+    or if asked for explicitly using :meth:`write_sha`.
 
     Only useful to the index file.
 
-    :note: Based on the dulwich project.
+    :note:
+        Based on the dulwich project.
     """
 
     __slots__ = ("f", "sha1")
@@ -991,16 +1007,20 @@ def _lock_file_path(self) -> str:
 
     def _has_lock(self) -> bool:
         """
-        :return: True if we have a lock and if the lockfile still exists
+        :return:
+            True if we have a lock and if the lockfile still exists
 
-        :raise AssertionError: If our lock-file does not exist
+        :raise AssertionError:
+            If our lock-file does not exist.
         """
         return self._owns_lock
 
     def _obtain_lock_or_raise(self) -> None:
-        """Create a lock file as flag for other instances, mark our instance as lock-holder.
+        """Create a lock file as flag for other instances, mark our instance as
+        lock-holder.
 
-        :raise IOError: If a lock was already present or a lock file could not be written
+        :raise IOError:
+            If a lock was already present or a lock file could not be written.
         """
         if self._has_lock():
             return
@@ -1021,7 +1041,9 @@ def _obtain_lock_or_raise(self) -> None:
 
     def _obtain_lock(self) -> None:
         """The default implementation will raise if a lock cannot be obtained.
-        Subclasses may override this method to provide a different implementation."""
+
+        Subclasses may override this method to provide a different implementation.
+        """
         return self._obtain_lock_or_raise()
 
     def _release_lock(self) -> None:
@@ -1029,8 +1051,8 @@ def _release_lock(self) -> None:
         if not self._has_lock():
             return
 
-        # If someone removed our file beforehand, lets just flag this issue
-        # instead of failing, to make it more usable.
+        # If someone removed our file beforehand, lets just flag this issue instead of
+        # failing, to make it more usable.
         lfp = self._lock_file_path()
         try:
             rmfile(lfp)
@@ -1040,12 +1062,12 @@ def _release_lock(self) -> None:
 
 
 class BlockingLockFile(LockFile):
-    """The lock file will block until a lock could be obtained, or fail after
-    a specified timeout.
+    """The lock file will block until a lock could be obtained, or fail after a
+    specified timeout.
 
-    :note: If the directory containing the lock was removed, an exception will
-        be raised during the blocking period, preventing hangs as the lock
-        can never be obtained.
+    :note:
+        If the directory containing the lock was removed, an exception will be raised
+        during the blocking period, preventing hangs as the lock can never be obtained.
     """
 
     __slots__ = ("_check_interval", "_max_block_time")
@@ -1062,14 +1084,15 @@ def __init__(
             Period of time to sleep until the lock is checked the next time.
             By default, it waits a nearly unlimited time.
 
-        :param max_block_time_s: Maximum amount of seconds we may lock.
+        :param max_block_time_s:
+            Maximum amount of seconds we may lock.
         """
         super().__init__(file_path)
         self._check_interval = check_interval_s
         self._max_block_time = max_block_time_s
 
     def _obtain_lock(self) -> None:
-        """This method blocks until it obtained the lock, or raises IOError if
+        """This method blocks until it obtained the lock, or raises :class:`IOError` if
         it ran out of time or if the parent directory was not available anymore.
 
         If this method returns, you are guaranteed to own the lock.
@@ -1105,23 +1128,32 @@ def _obtain_lock(self) -> None:
 
 
 class IterableList(List[T_IterableObj]):
-    """
-    List of iterable objects allowing to query an object by id or by named index::
+    """List of iterable objects allowing to query an object by id or by named index::
 
      heads = repo.heads
      heads.master
      heads['master']
      heads[0]
 
-    Iterable parent objects = [Commit, SubModule, Reference, FetchInfo, PushInfo]
-    Iterable via inheritance = [Head, TagReference, RemoteReference]
+    Iterable parent objects:
+
+    * :class:`Commit <git.objects.Commit>`
+    * :class:`Submodule <git.objects.submodule.base.Submodule>`
+    * :class:`Reference <git.refs.reference.Reference>`
+    * :class:`FetchInfo <git.remote.FetchInfo>`
+    * :class:`PushInfo <git.remote.PushInfo>`
+
+    Iterable via inheritance:
 
-    It requires an id_attribute name to be set which will be queried from its
+    * :class:`Head <git.refs.head.Head>`
+    * :class:`TagReference <git.refs.tag.TagReference>`
+    * :class:`RemoteReference <git.refs.remote.RemoteReference>`
+
+    This requires an ``id_attribute`` name to be set which will be queried from its
     contained items to have a means for comparison.
 
-    A prefix can be specified which is to be used in case the id returned by the
-    items always contains a prefix that does not matter to the user, so it
-    can be left out.
+    A prefix can be specified which is to be used in case the id returned by the items
+    always contains a prefix that does not matter to the user, so it can be left out.
     """
 
     __slots__ = ("_id_attr", "_prefix")
@@ -1198,7 +1230,14 @@ class IterableObj(Protocol):
     """Defines an interface for iterable items, so there is a uniform way to retrieve
     and iterate items within the git repository.
 
-    Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]
+    Subclasses:
+
+    * :class:`Submodule <git.objects.submodule.base.Submodule>`
+    * :class:`Commit <git.objects.Commit>`
+    * :class:`Reference <git.refs.reference.Reference>`
+    * :class:`PushInfo <git.remote.PushInfo>`
+    * :class:`FetchInfo <git.remote.FetchInfo>`
+    * :class:`Remote <git.remote.Remote>`
     """
 
     __slots__ = ()
@@ -1211,11 +1250,12 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_Itera
         # Return-typed to be compatible with subtypes e.g. Remote.
         """Find (all) items of this type.
 
-        Subclasses can specify ``args`` and ``kwargs`` differently, and may use them for
+        Subclasses can specify `args` and `kwargs` differently, and may use them for
         filtering. However, when the method is called with no additional positional or
         keyword arguments, subclasses are obliged to to yield all items.
 
-        :return: Iterator yielding Items
+        :return:
+            :class:`~collections.abc.Iterator` yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
 
@@ -1225,11 +1265,13 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
 
         For more information about the arguments, see :meth:`iter_items`.
 
-        :note: Favor the :meth:`iter_items` method as it will avoid eagerly collecting
-            all items. When there are many items, that can slow performance and increase
+        :note:
+            Favor the :meth:`iter_items` method as it will avoid eagerly collecting all
+            items. When there are many items, that can slow performance and increase
             memory usage.
 
-        :return: list(Item,...) list of item instances
+        :return:
+            list(Item,...) list of item instances
         """
         out_list: IterableList = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))
@@ -1272,7 +1314,8 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
         See :meth:`IterableObj.iter_items` for details on usage.
 
-        :return: Iterator yielding Items
+        :return:
+            Iterator yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
 
@@ -1284,7 +1327,8 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any:
 
         See :meth:`IterableObj.list_items` for details on usage.
 
-        :return: list(Item,...) list of item instances
+        :return:
+            list(Item,...) list of item instances
         """
         out_list: Any = IterableList(cls._id_attribute_)
         out_list.extend(cls.iter_items(repo, *args, **kwargs))

From 29c63ac8d7c75e4863cd355610da3cabd48a421c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 21:16:49 -0500
Subject: [PATCH 478/994] Format first Git.execute overload stub like the
 others

This makes it easier to read along with, and compare to, the
other overloads.
---
 git/cmd.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index eb6c235c7..d3aa5f544 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -922,7 +922,12 @@ def version_info(self) -> Tuple[int, ...]:
         return self._version_info
 
     @overload
-    def execute(self, command: Union[str, Sequence[Any]], *, as_process: Literal[True]) -> "AutoInterrupt":
+    def execute(
+        self,
+        command: Union[str, Sequence[Any]],
+        *,
+        as_process: Literal[True],
+    ) -> "AutoInterrupt":
         ...
 
     @overload

From cd8a3123e322ea6166b9970a87f7cc2e892d2316 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 21:21:54 -0500
Subject: [PATCH 479/994] Show full-path refresh() in failure message
 differently

This presents it more symbolically, rather than in an example-like
style that could confuse. See discussion in #1844.
---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index d3aa5f544..760a9ff12 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -458,7 +458,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 The git executable must be specified in one of the following ways:
                     - be included in your $PATH
                     - be set via $%s
-                    - explicitly set via git.refresh("/full/path/to/git")
+                    - explicitly set via git.refresh(<full-path-to-git-executable>)
                 """
                 )
                 % cls._git_exec_env_var

From 8ec7e32032e0877144517bd15c0e9b6cdd283667 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 14:21:50 -0500
Subject: [PATCH 480/994] Revise docstrings within git.index

Along the same lines as e08066c and 1cd73ba.
---
 git/index/base.py | 531 ++++++++++++++++++++++++++--------------------
 git/index/fun.py  | 102 +++++----
 git/index/typ.py  |  13 +-
 git/index/util.py |  12 +-
 4 files changed, 378 insertions(+), 280 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 9d1db4e35..31186b203 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -101,10 +101,12 @@
 def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
     """Create a named temporary file git subprocesses can open, deleting it afterward.
 
-    :param directory: The directory in which the file is created.
+    :param directory:
+        The directory in which the file is created.
 
-    :return: A context manager object that creates the file and provides its name on
-        entry, and deletes it on exit.
+    :return:
+        A context manager object that creates the file and provides its name on entry,
+        and deletes it on exit.
     """
     if os.name == "nt":
         fd, name = tempfile.mkstemp(dir=directory)
@@ -123,21 +125,21 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
     git command function calls wherever possible.
 
     This provides custom merging facilities allowing to merge without actually changing
-    your index or your working tree. This way you can perform own test-merges based
-    on the index only without having to deal with the working copy. This is useful
-    in case of partial working trees.
+    your index or your working tree. This way you can perform own test-merges based on
+    the index only without having to deal with the working copy. This is useful in case
+    of partial working trees.
 
-    ``Entries``
+    Entries:
 
-    The index contains an entries dict whose keys are tuples of type IndexEntry
-    to facilitate access.
+        The index contains an entries dict whose keys are tuples of type
+        :class:`~git.index.typ.IndexEntry` to facilitate access.
 
-    You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
+        You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
 
-        index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
+            index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
 
-    Make sure you use index.write() once you are done manipulating the index directly
-    before operating on it using the git command.
+    Make sure you use :meth:`index.write() <write>` once you are done manipulating the
+    index directly before operating on it using the git command.
     """
 
     __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
@@ -149,9 +151,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
     """Flags for a submodule."""
 
     def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
-        """Initialize this Index instance, optionally from the given ``file_path``.
+        """Initialize this Index instance, optionally from the given `file_path`.
 
-        If no file_path is given, we will be created from the current index file.
+        If no `file_path` is given, we will be created from the current index file.
 
         If a stream is not given, the stream will be initialized from the current
         repository's index on demand.
@@ -230,25 +232,22 @@ def write(
         """Write the current state to our file path or to the given one.
 
         :param file_path:
-            If None, we will write to our stored file path from which we have
-            been initialized. Otherwise we write to the given file path.
-            Please note that this will change the file_path of this index to
-            the one you gave.
+            If None, we will write to our stored file path from which we have been
+            initialized. Otherwise we write to the given file path. Please note that
+            this will change the file_path of this index to the one you gave.
 
         :param ignore_extension_data:
-            If True, the TREE type extension data read in the index will not
-            be written to disk. NOTE that no extension data is actually written.
-            Use this if you have altered the index and
-            would like to use git-write-tree afterwards to create a tree
-            representing your written changes.
-            If this data is present in the written index, git-write-tree
-            will instead write the stored/cached tree.
-            Alternatively, use IndexFile.write_tree() to handle this case
-            automatically.
+            If True, the TREE type extension data read in the index will not be written
+            to disk. NOTE that no extension data is actually written.
+            Use this if you have altered the index and would like to use git-write-tree
+            afterwards to create a tree representing your written changes.
+            If this data is present in the written index, git-write-tree will instead
+            write the stored/cached tree.
+            Alternatively, use :meth:`write_tree` to handle this case automatically.
         """
         # Make sure we have our entries read before getting a write lock.
-        # Otherwise it would be done when streaming. This can happen if one
-        # doesn't change the index, but writes it right away.
+        # Otherwise it would be done when streaming.
+        # This can happen if one doesn't change the index, but writes it right away.
         self.entries
         lfd = LockedFD(file_path or self._file_path)
         stream = lfd.open(write=True, stream=True)
@@ -268,17 +267,17 @@ def write(
     @post_clear_cache
     @default_index
     def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
-        """Merge the given rhs treeish into the current index, possibly taking
+        """Merge the given `rhs` treeish into the current index, possibly taking
         a common base treeish into account.
 
-        As opposed to the :func:`IndexFile.from_tree` method, this allows you to use an
-        already existing tree as the left side of the merge.
+        As opposed to the :func:`from_tree` method, this allows you to use an already
+        existing tree as the left side of the merge.
 
         :param rhs:
             Treeish reference pointing to the 'other' side of the merge.
 
         :param base:
-            Optional treeish reference pointing to the common base of 'rhs' and this
+            Optional treeish reference pointing to the common base of `rhs` and this
             index which equals lhs.
 
         :return:
@@ -289,7 +288,7 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF
             If there is a merge conflict. The error will be raised at the first
             conflicting path. If you want to have proper merge resolution to be done by
             yourself, you have to commit the changed index (or make a valid tree from
-            it) and retry with a three-way index.from_tree call.
+            it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
         """
         # -i : ignore working tree status
         # --aggressive : handle more merge cases
@@ -308,15 +307,16 @@ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
 
         This method behaves like ``git-read-tree --aggressive`` when doing the merge.
 
-        :param repo: The repository treeish are located in.
+        :param repo:
+            The repository treeish are located in.
 
         :param tree_sha:
             20 byte or 40 byte tree sha or tree objects.
 
         :return:
-            New IndexFile instance. Its path will be undefined.
-            If you intend to write such a merged Index, supply an alternate file_path
-            to its 'write' method.
+            New :class:`IndexFile` instance. Its path will be undefined.
+            If you intend to write such a merged Index, supply an alternate
+            ``file_path`` to its :meth:`write` method.
         """
         tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
         base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
@@ -335,37 +335,40 @@ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
 
     @classmethod
     def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
-        """Merge the given treeish revisions into a new index which is returned.
+        R"""Merge the given treeish revisions into a new index which is returned.
         The original index will remain unaltered.
 
         :param repo:
             The repository treeish are located in.
 
         :param treeish:
-            One, two or three Tree Objects, Commits or 40 byte hexshas. The result
-            changes according to the amount of trees.
-            If 1 Tree is given, it will just be read into a new index
-            If 2 Trees are given, they will be merged into a new index using a
-            two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
-            one. It behaves like a fast-forward.
-            If 3 Trees are given, a 3-way merge will be performed with the first tree
-            being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree,
-            tree 3 is the 'other' one.
+            One, two or three :class:`~git.objects.tree.Tree` objects,
+            :class:`~git.objects.commit.Commit`\s or 40 byte hexshas.
+
+            The result changes according to the amount of trees:
+
+            1. If 1 Tree is given, it will just be read into a new index.
+            2. If 2 Trees are given, they will be merged into a new index using a two
+               way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
+               one. It behaves like a fast-forward.
+            3. If 3 Trees are given, a 3-way merge will be performed with the first tree
+               being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current'
+               tree, tree 3 is the 'other' one.
 
         :param kwargs:
-            Additional arguments passed to git-read-tree.
+            Additional arguments passed to ``git read-tree``.
 
         :return:
-            New IndexFile instance. It will point to a temporary index location which
-            does not exist anymore. If you intend to write such a merged Index, supply
-            an alternate file_path to its 'write' method.
+            New :class:`IndexFile` instance. It will point to a temporary index location
+            which does not exist anymore. If you intend to write such a merged Index,
+            supply an alternate ``file_path`` to its :meth:`write` method.
 
         :note:
-            In the three-way merge case, --aggressive will be specified to automatically
-            resolve more cases in a commonly correct manner. Specify trivial=True as kwarg
-            to override that.
+            In the three-way merge case, ``--aggressive`` will be specified to
+            automatically resolve more cases in a commonly correct manner. Specify
+            ``trivial=True`` as a keyword argument to override that.
 
-            As the underlying git-read-tree command takes into account the current
+            As the underlying ``git read-tree`` command takes into account the current
             index, it will be temporarily moved out of the way to prevent any unexpected
             interference.
         """
@@ -387,8 +390,8 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             arg_list.append("--index-output=%s" % tmp_index)
             arg_list.extend(treeish)
 
-            # Move the current index out of the way - otherwise the merge may fail
-            # as it considers existing entries. Moving it essentially clears the index.
+            # Move the 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 ensures the original file gets put back.
             with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
@@ -402,12 +405,13 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
 
     @unbare_repo
     def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
-        """Expand the directories in list of paths to the corresponding paths accordingly.
+        """Expand the directories in list of paths to the corresponding paths
+        accordingly.
 
         :note:
-            git will add items multiple times even if a glob overlapped
-            with manually specified paths or if paths where specified multiple
-            times - we respect that and do not prune.
+            git will add items multiple times even if a glob overlapped with manually
+            specified paths or if paths where specified multiple times - we respect that
+            and do not prune.
         """
 
         def raise_exc(e: Exception) -> NoReturn:
@@ -436,11 +440,11 @@ def raise_exc(e: Exception) -> NoReturn:
             if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
                 resolved_paths = glob.glob(abs_path)
                 # not abs_path in resolved_paths:
-                #   a glob() resolving to the same path we are feeding it with
-                #   is a glob() that failed to resolve. If we continued calling
-                #   ourselves we'd endlessly recurse. If the condition below
-                #   evaluates to true then we are likely dealing with a file
-                #   whose name contains wildcard characters.
+                #   A glob() resolving to the same path we are feeding it with is a
+                #   glob() that failed to resolve. If we continued calling ourselves
+                #   we'd endlessly recurse. If the condition below evaluates to true
+                #   then we are likely dealing with a file whose name contains wildcard
+                #   characters.
                 if abs_path not in resolved_paths:
                     for f in self._iter_expand_paths(glob.glob(abs_path)):
                         yield str(f).replace(rs, "")
@@ -468,22 +472,27 @@ def _write_path_to_stdin(
         fprogress: Callable[[PathLike, bool, PathLike], None],
         read_from_stdout: bool = True,
     ) -> Union[None, str]:
-        """Write path to proc.stdin and make sure it processes the item, including progress.
+        """Write path to ``proc.stdin`` and make sure it processes the item, including
+        progress.
 
-        :return: stdout string
+        :return:
+            stdout string
 
-        :param read_from_stdout: if True, proc.stdout will be read after the item
-            was sent to stdin. In that case, it will return None.
+        :param read_from_stdout:
+            If True, proc.stdout will be read after the item was sent to stdin. In that
+            case, it will return None.
 
-        :note: There is a bug in git-update-index that prevents it from sending
-            reports just in time. This is why we have a version that tries to
-            read stdout and one which doesn't. In fact, the stdout is not
-            important as the piped-in files are processed anyway and just in time.
+        :note:
+            There is a bug in git-update-index that prevents it from sending reports
+            just in time. This is why we have a version that tries to read stdout and
+            one which doesn't. In fact, the stdout is not important as the piped-in
+            files are processed anyway and just in time.
 
-        :note: Newlines are essential here, gits behaviour is somewhat inconsistent
-            on this depending on the version, hence we try our best to deal with
-            newlines carefully. Usually the last newline will not be sent, instead
-            we will close stdin to break the pipe.
+        :note:
+            Newlines are essential here, git's behaviour is somewhat inconsistent on
+            this depending on the version, hence we try our best to deal with newlines
+            carefully. Usually the last newline will not be sent, instead we will close
+            stdin to break the pipe.
         """
         fprogress(filepath, False, item)
         rval: Union[None, str] = None
@@ -506,12 +515,13 @@ def iter_blobs(
         self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True
     ) -> Iterator[Tuple[StageType, Blob]]:
         """
-        :return: Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob)
+        :return:
+            Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob).
 
         :param predicate:
             Function(t) returning True if tuple(stage, Blob) should be yielded by the
-            iterator. A default filter, the BlobFilter, allows you to yield blobs
-            only if they match a given list of paths.
+            iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you to
+            yield blobs only if they match a given list of paths.
         """
         for entry in self.entries.values():
             blob = entry.to_blob(self.repo)
@@ -524,14 +534,13 @@ def iter_blobs(
     def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
         """
         :return:
-            Dict(path : list( tuple( stage, Blob, ...))), being
-            a dictionary associating a path in the index with a list containing
-            sorted stage/blob pairs.
+            Dict(path : list( tuple( stage, Blob, ...))), being a dictionary associating
+            a path in the index with a list containing sorted stage/blob pairs.
 
         :note:
-            Blobs that have been removed in one side simply do not exist in the
-            given stage. I.e. a file removed on the 'other' branch whose entries
-            are at stage 3 will not have a stage 3 entry.
+            Blobs that have been removed in one side simply do not exist in the given
+            stage. That is, a file removed on the 'other' branch whose entries are at
+            stage 3 will not have a stage 3 entry.
         """
         is_unmerged_blob = lambda t: t[0] != 0
         path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
@@ -556,9 +565,11 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
         For each path there may only be one blob, otherwise a ValueError will be raised
         claiming the path is already at stage 0.
 
-        :raise ValueError: if one of the blobs already existed at stage 0
+        :raise ValueError:
+            If one of the blobs already existed at stage 0.
 
-        :return: self
+        :return:
+            self
 
         :note:
             You will have to write the index manually once you are done, i.e.
@@ -588,8 +599,9 @@ def update(self) -> "IndexFile":
         """Reread the contents of our index file, discarding all cached information
         we might have.
 
-        :note: This is a possibly dangerous operations as it will discard your changes
-            to index.entries.
+        :note:
+            This is a possibly dangerous operations as it will discard your changes to
+            :attr:`index.entries <entries>`.
 
         :return: self
         """
@@ -598,16 +610,19 @@ def update(self) -> "IndexFile":
         return self
 
     def write_tree(self) -> Tree:
-        """Write this index to a corresponding Tree object into the repository's
-        object database and return it.
+        """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
+        into the repository's object database and return it.
 
-        :return: Tree object representing this index.
+        :return:
+            :class:`~git.objects.tree.Tree` object representing this index.
 
-        :note: The tree will be written even if one or more objects the tree refers to
-            does not yet exist in the object database. This could happen if you added
-            Entries to the index directly.
+        :note:
+            The tree will be written even if one or more objects the tree refers to does
+            not yet exist in the object database. This could happen if you added entries
+            to the index directly.
 
-        :raise ValueError: if there are no entries in the cache
+        :raise ValueError:
+            If there are no entries in the cache.
 
         :raise UnmergedEntriesError:
         """
@@ -620,8 +635,8 @@ def write_tree(self) -> Tree:
         # Copy changed trees only.
         mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
 
-        # Note: Additional deserialization could be saved if write_tree_from_cache
-        # would return sorted tree entries.
+        # Note: Additional deserialization could be saved if write_tree_from_cache would
+        # return sorted tree entries.
         root_tree = Tree(self.repo, binsha, path="")
         root_tree._cache = tree_items
         return root_tree
@@ -639,8 +654,11 @@ def _process_diff_args(
 
     def _to_relative_path(self, path: PathLike) -> PathLike:
         """
-        :return: Version of path relative to our git directory or raise ValueError
-            if it is not within our git directory"""
+        :return: Version of path relative to our git directory or raise
+            :class:`ValueError` if it is not within our git directory.
+
+        :raise ValueError:
+        """
         if not osp.isabs(path):
             return path
         if self.repo.bare:
@@ -672,8 +690,12 @@ def _preprocess_add_items(
         return paths, entries
 
     def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
-        """Store file at filepath in the database and return the base index entry
-        Needs the git_working_dir decorator active ! This must be assured in the calling code"""
+        """Store file at filepath in the database and return the base index entry.
+
+        :note:
+            This needs the git_working_dir decorator active!
+            This must be ensured in the calling code.
+        """
         st = os.lstat(filepath)  # Handles non-symlinks as well.
         if S_ISLNK(st.st_mode):
             # In PY3, readlink is a string, but we need bytes.
@@ -744,8 +766,8 @@ def add(
         write: bool = True,
         write_extension_data: bool = False,
     ) -> List[BaseIndexEntry]:
-        """Add files from the working tree, specific blobs or BaseIndexEntries
-        to the index.
+        R"""Add files from the working tree, specific blobs, or
+        :class:`~git.index.typ.BaseIndexEntry`\s to the index.
 
         :param items:
             Multiple types of items are supported, types can be mixed within one call.
@@ -753,9 +775,10 @@ def add(
             relative or absolute.
 
             - path string
+
                 Strings denote a relative or absolute path into the repository pointing
-                to an existing file, e.g., CHANGES, lib/myfile.ext,
-                '/home/gitrepo/lib/myfile.ext'.
+                to an existing file, e.g., ``CHANGES``, `lib/myfile.ext``,
+                ``/home/gitrepo/lib/myfile.ext``.
 
                 Absolute paths must start with working tree directory of this index's
                 repository to be considered valid. For example, if it was initialized
@@ -769,11 +792,12 @@ def add(
                 directories like ``lib``, which will add all the files within the
                 directory and subdirectories.
 
-                This equals a straight git-add.
+                This equals a straight ``git add``.
 
                 They are added at stage 0.
 
-            - Blob or Submodule object
+            - :class:~`git.objects.blob.Blob` or :class:`~git.objects.submodule.base.Submodule` object
+
                 Blobs are added as they are assuming a valid mode is set.
 
                 The file they refer to may or may not exist in the file system, but must
@@ -787,53 +811,57 @@ def add(
                 except that the mode you have set will be kept. This allows you to
                 create symlinks by settings the mode respectively and writing the target
                 of the symlink directly into the file. This equals a default
-                Linux-Symlink which is not dereferenced automatically, except that it
+                Linux symlink which is not dereferenced automatically, except that it
                 can be created on filesystems not supporting it as well.
 
-                Please note that globs or directories are not allowed in Blob objects.
+                Please note that globs or directories are not allowed in
+                :class:~`git.objects.blob.Blob` objects.
 
                 They are added at stage 0.
 
-            - BaseIndexEntry or type
+            - :class:`~git.index.typ.BaseIndexEntry` or type
+
                 Handling equals the one of Blob objects, but the stage may be explicitly
                 set. Please note that Index Entries require binary sha's.
 
         :param force:
             **CURRENTLY INEFFECTIVE**
             If True, otherwise ignored or excluded files will be added anyway.
-            As opposed to the git-add command, we enable this flag by default
-            as the API user usually wants the item to be added even though
-            they might be excluded.
+            As opposed to the ``git add`` command, we enable this flag by default as the
+            API user usually wants the item to be added even though they might be
+            excluded.
 
         :param fprogress:
-            Function with signature f(path, done=False, item=item) called for each
-            path to be added, one time once it is about to be added where done==False
-            and once after it was added where done=True.
-            item is set to the actual item we handle, either a Path or a BaseIndexEntry
-            Please note that the processed path is not guaranteed to be present
-            in the index already as the index is currently being processed.
+            Function with signature ``f(path, done=False, item=item)`` called for each
+            path to be added, one time once it is about to be added where ``done=False``
+            and once after it was added where ``done=True``.
+            ``item`` is set to the actual item we handle, either a Path or a
+            :class:`~git.index.typ.BaseIndexEntry`.
+            Please note that the processed path is not guaranteed to be present in the
+            index already as the index is currently being processed.
 
         :param path_rewriter:
-            Function with signature (string) func(BaseIndexEntry) function returning a
-            path for each passed entry which is the path to be actually recorded for the
-            object created from entry.path. This allows you to write an index which is
-            not identical to the layout of the actual files on your hard-disk. If not
-            None and ``items`` contain plain paths, these paths will be converted to
-            Entries beforehand and passed to the path_rewriter. Please note that
-            entry.path is relative to the git repository.
+            Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
+            for each passed entry which is the path to be actually recorded for the
+            object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
+            This allows you to write an index which is not identical to the layout of
+            the actual files on your hard-disk. If not None and `items` contain plain
+            paths, these paths will be converted to Entries beforehand and passed to the
+            path_rewriter. Please note that ``entry.path`` is relative to the git
+            repository.
 
         :param write:
-            If True, the index will be written once it was altered. Otherwise
-            the changes only exist in memory and are not available to git commands.
+            If True, the index will be written once it was altered. Otherwise the
+            changes only exist in memory and are not available to git commands.
 
         :param write_extension_data:
             If True, extension data will be written back to the index. This can lead to
             issues in case it is containing the 'TREE' extension, which will cause the
-            `git commit` command to write an old tree, instead of a new one representing
-            the now changed index.
+            ``git commit`` command to write an old tree, instead of a new one
+            representing the now changed index.
 
             This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
-            `TREE` extension altogether. You should set it to True if you intend to use
+            'TREE' extension altogether. You should set it to True if you intend to use
             :meth:`IndexFile.commit` exclusively while maintaining support for
             third-party extensions. Besides that, you can usually safely ignore the
             built-in extensions when using GitPython on repositories that are not
@@ -843,18 +871,20 @@ def add(
             http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/technical/index-format.txt
 
         :return:
-            List(BaseIndexEntries) representing the entries just actually added.
+            List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
+            just actually added.
 
         :raise OSError:
-            If a supplied Path did not exist. Please note that BaseIndexEntry
-            Objects that do not have a null sha will be added even if their paths
-            do not exist.
+            If a supplied Path did not exist. Please note that
+            :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
+            will be added even if their paths do not exist.
         """
-        # Sort the entries into strings and Entries. Blobs are converted to entries automatically.
+        # Sort the entries into strings and Entries.
+        # Blobs are converted to entries automatically.
         # Paths can be git-added. For everything else we use git-update-index.
         paths, entries = self._preprocess_add_items(items)
         entries_added: List[BaseIndexEntry] = []
-        # This code needs a working tree, therefore we try not to run it unless required.
+        # This code needs a working tree, so we try not to run it unless required.
         # That way, we are OK on a bare repository as well.
         # If there are no paths, the rewriter has nothing to do either.
         if paths:
@@ -897,7 +927,8 @@ def handle_null_entries(self: "IndexFile") -> None:
             # END null_entry handling
 
             # REWRITE PATHS
-            # If we have to rewrite the entries, do so now, after we have generated all object sha's.
+            # If we have to rewrite the entries, do so now, after we have generated all
+            # object sha's.
             if path_rewriter:
                 for i, e in enumerate(entries):
                     entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
@@ -955,26 +986,28 @@ def remove(
         working_tree: bool = False,
         **kwargs: Any,
     ) -> List[str]:
-        """Remove the given items from the index and optionally from
-        the working tree as well.
+        R"""Remove the given items from the index and optionally from the working tree
+        as well.
 
         :param items:
             Multiple types of items are supported which may be be freely mixed.
 
             - path string
+
                 Remove the given path at all stages. If it is a directory, you must
-                specify the r=True keyword argument to remove all file entries
-                below it. If absolute paths are given, they will be converted
-                to a path relative to the git repository directory containing
-                the working tree
+                specify the ``r=True`` keyword argument to remove all file entries below
+                it. If absolute paths are given, they will be converted to a path
+                relative to the git repository directory containing the working tree
+
+                The path string may include globs, such as \*.c.
 
-                The path string may include globs, such as \\*.c.
+            - :class:~`git.objects.blob.Blob` object
 
-            - Blob Object
                 Only the path portion is used in this case.
 
-            - BaseIndexEntry or compatible type
-                The only relevant information here Yis the path. The stage is ignored.
+            - :class:`~git.index.typ.BaseIndexEntry` or compatible type
+
+                The only relevant information here is the path. The stage is ignored.
 
         :param working_tree:
             If True, the entry will also be removed from the working tree, physically
@@ -982,14 +1015,15 @@ def remove(
             in it.
 
         :param kwargs:
-            Additional keyword arguments to be passed to git-rm, such
-            as 'r' to allow recursive removal of
+            Additional keyword arguments to be passed to ``git rm``, such as ``r`` to
+            allow recursive removal.
 
         :return:
-            List(path_string, ...) list of repository relative paths that have
-            been removed effectively.
-            This is interesting to know in case you have provided a directory or
-            globs. Paths are relative to the repository.
+            List(path_string, ...) list of repository relative paths that have been
+            removed effectively.
+
+            This is interesting to know in case you have provided a directory or globs.
+            Paths are relative to the repository.
         """
         args = []
         if not working_tree:
@@ -1013,28 +1047,38 @@ def move(
         **kwargs: Any,
     ) -> List[Tuple[str, str]]:
         """Rename/move the items, whereas the last item is considered the destination of
-        the move operation. If the destination is a file, the first item (of two)
-        must be a file as well. If the destination is a directory, it may be preceded
-        by one or more directories or files.
+        the move operation.
+
+        If the destination is a file, the first item (of two) must be a file as well.
+
+        If the destination is a directory, it may be preceded by one or more directories
+        or files.
 
         The working tree will be affected in non-bare repositories.
 
-        :parma items:
+        :param items:
             Multiple types of items are supported, please see the :meth:`remove` method
             for reference.
+
         :param skip_errors:
-            If True, errors such as ones resulting from missing source files will
-            be skipped.
+            If True, errors such as ones resulting from missing source files will be
+            skipped.
+
         :param kwargs:
-            Additional arguments you would like to pass to git-mv, such as dry_run
-            or force.
+            Additional arguments you would like to pass to ``git mv``, such as
+            ``dry_run`` or ``force``.
 
-        :return: List(tuple(source_path_string, destination_path_string), ...)
-            A list of pairs, containing the source file moved as well as its
-            actual destination. Relative to the repository root.
+        :return:
+            List(tuple(source_path_string, destination_path_string), ...)
+
+            A list of pairs, containing the source file moved as well as its actual
+            destination. Relative to the repository root.
+
+        :raise ValueError:
+            If only one item was given.
 
-        :raise ValueError: If only one item was given
-        :raise GitCommandError: If git could not handle your request
+        :raise GitCommandError:
+            If git could not handle your request.
         """
         args = []
         if skip_errors:
@@ -1047,13 +1091,13 @@ def move(
         was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
         kwargs["dry_run"] = True
 
-        # First execute rename in dryrun so the command tells us what it actually does
+        # First execute rename in dry run so the command tells us what it actually does
         # (for later output).
         out = []
         mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
 
-        # Parse result - first 0:n/2 lines are 'checking ', the remaining ones
-        # are the 'renaming' ones which we parse.
+        # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
+        # 'renaming' ones which we parse.
         for ln in range(int(len(mvlines) / 2), len(mvlines)):
             tokens = mvlines[ln].split(" to ")
             assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
@@ -1066,7 +1110,7 @@ def move(
         # Either prepare for the real run, or output the dry-run result.
         if was_dry_run:
             return out
-        # END handle dryrun
+        # END handle dry run
 
         # Now apply the actual operation.
         kwargs.pop("dry_run")
@@ -1085,17 +1129,22 @@ def commit(
         commit_date: Union[datetime.datetime, str, None] = None,
         skip_hooks: bool = False,
     ) -> Commit:
-        """Commit the current default index file, creating a Commit object.
+        """Commit the current default index file, creating a
+        :class:`~git.objects.commit.Commit` object.
 
         For more information on the arguments, see
         :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
 
-        :note: If you have manually altered the :attr:`entries` member of this instance,
-               don't forget to :meth:`write` your changes to disk beforehand.
-               Passing ``skip_hooks=True`` is the equivalent of using ``-n``
-               or ``--no-verify`` on the command line.
+        :note:
+            If you have manually altered the :attr:`entries` member of this instance,
+            don't forget to :meth:`write` your changes to disk beforehand.
+
+        :note:
+            Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
+            ``--no-verify`` on the command line.
 
-        :return: :class:`Commit` object representing the new commit
+        :return:
+            :class:`~git.objects.commit.Commit` object representing the new commit
         """
         if not skip_hooks:
             run_commit_hook("pre-commit", self)
@@ -1160,44 +1209,49 @@ def checkout(
         """Check out the given paths or all files from the version known to the index
         into the working tree.
 
-        :note: Be sure you have written pending changes using the :meth:`write` method
-            in case you have altered the entries dictionary directly.
+        :note:
+            Be sure you have written pending changes using the :meth:`write` method in
+            case you have altered the entries dictionary directly.
 
         :param paths:
             If None, all paths in the index will be checked out. Otherwise an iterable
-            of relative or absolute paths or a single path pointing to files or directories
-            in the index is expected.
+            of relative or absolute paths or a single path pointing to files or
+            directories in the index is expected.
 
         :param force:
-            If True, existing files will be overwritten even if they contain local modifications.
-            If False, these will trigger a :class:`CheckoutError`.
+            If True, existing files will be overwritten even if they contain local
+            modifications.
+            If False, these will trigger a :class:`~git.exc.CheckoutError`.
 
         :param fprogress:
-            see :func:`IndexFile.add` for signature and explanation.
+            See :meth:`IndexFile.add` for signature and explanation.
+
             The provided progress information will contain None as path and item if no
-            explicit paths are given. Otherwise progress information will be send
-            prior and after a file has been checked out.
+            explicit paths are given. Otherwise progress information will be send prior
+            and after a file has been checked out.
 
         :param kwargs:
-            Additional arguments to be passed to git-checkout-index.
+            Additional arguments to be passed to ``git checkout-index``.
 
         :return:
-            iterable yielding paths to files which have been checked out and are
+            Iterable yielding paths to files which have been checked out and are
             guaranteed to match the version stored in the index.
 
-        :raise exc.CheckoutError:
-            If at least one file failed to be checked out. This is a summary,
-            hence it will checkout as many files as it can anyway.
-            If one of files or directories do not exist in the index
-            ( as opposed to the  original git command who ignores them ).
-            Raise GitCommandError if error lines could not be parsed - this truly is
-            an exceptional state.
-
-        .. note:: The checkout is limited to checking out the files in the
-            index. Files which are not in the index anymore and exist in
-            the working tree will not be deleted. This behaviour is fundamentally
-            different to *head.checkout*, i.e. if you want git-checkout like behaviour,
-            use head.checkout instead of index.checkout.
+        :raise git.exc.CheckoutError:
+            If at least one file failed to be checked out. This is a summary, hence it
+            will checkout as many files as it can anyway.
+            If one of files or directories do not exist in the index (as opposed to the
+            original git command, which ignores them).
+
+        :raise git.exc.GitCommandError:
+            If error lines could not be parsed - this truly is an exceptional state.
+
+        :note:
+            The checkout is limited to checking out the files in the index. Files which
+            are not in the index anymore and exist in the working tree will not be
+            deleted. This behaviour is fundamentally different to ``head.checkout``,
+            i.e. if you want ``git checkout`` like behaviour, use ``head.checkout``
+            instead of ``index.checkout``.
         """
         args = ["--index"]
         if force:
@@ -1276,9 +1330,9 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
             if isinstance(paths, str):
                 paths = [paths]
 
-            # Make sure we have our entries loaded before we start checkout_index,
-            # which will hold a lock on it. We try to get the lock as well during
-            # our entries initialization.
+            # Make sure we have our entries loaded before we start checkout_index, which
+            # will hold a lock on it. We try to get the lock as well during our entries
+            # initialization.
             self.entries
 
             args.append("--stdin")
@@ -1339,40 +1393,50 @@ def reset(
         head: bool = False,
         **kwargs: Any,
     ) -> "IndexFile":
-        """Reset the index to reflect the tree at the given commit. This will not
-        adjust our HEAD reference as opposed to HEAD.reset by default.
+        """Reset the index to reflect the tree at the given commit. This will not adjust
+        our ``HEAD`` reference by default, as opposed to
+        :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
 
         :param commit:
-            Revision, Reference or Commit specifying the commit we should represent.
-            If you want to specify a tree only, use IndexFile.from_tree and overwrite
-            the default index.
+            Revision, :class:`~git.refs.reference.Reference` or
+            :class:`~git.objects.commit.Commit` specifying the commit we should
+            represent.
+
+            If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
+            overwrite the default index.
 
         :param working_tree:
             If True, the files in the working tree will reflect the changed index.
-            If False, the working tree will not be touched
+            If False, the working tree will not be touched.
             Please note that changes to the working copy will be discarded without
-            warning !
+            warning!
 
         :param head:
             If True, the head will be set to the given commit. This is False by default,
-            but if True, this method behaves like HEAD.reset.
+            but if True, this method behaves like
+            :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
 
-        :param paths: if given as an iterable of absolute or repository-relative paths,
-            only these will be reset to their state at the given commit-ish.
+        :param paths:
+            If given as an iterable of absolute or repository-relative paths, only these
+            will be reset to their state at the given commit-ish.
             The paths need to exist at the commit, otherwise an exception will be
             raised.
 
         :param kwargs:
-            Additional keyword arguments passed to git-reset
+            Additional keyword arguments passed to ``git reset``.
 
-        .. note:: IndexFile.reset, as opposed to HEAD.reset, will not delete any files
-            in order to maintain a consistent working tree. Instead, it will just
-            check out the files according to their state in the index.
-            If you want git-reset like behaviour, use *HEAD.reset* instead.
+        :note:
+            :meth:`IndexFile.reset`, as opposed to
+            :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
+            order to maintain a consistent working tree. Instead, it will just check out
+            the files according to their state in the index.
+            If you want ``git reset``-like behaviour, use
+            :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
 
-        :return: self"""
-        # What we actually want to do is to merge the tree into our existing
-        # index, which is what git-read-tree does.
+        :return: self
+        """
+        # What we actually want to do is to merge the tree into our existing index,
+        # which is what git-read-tree does.
         new_inst = type(self).from_tree(self.repo, commit)
         if not paths:
             self.entries = new_inst.entries
@@ -1413,14 +1477,15 @@ def diff(
         create_patch: bool = False,
         **kwargs: Any,
     ) -> git_diff.DiffIndex:
-        """Diff this index against the working copy or a Tree or Commit object.
+        """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
+        or :class:`~git.objects.commit.Commit` object.
 
         For documentation of the parameters and return values, see
         :meth:`Diffable.diff <git.diff.Diffable.diff>`.
 
         :note:
-            Will only work with indices that represent the default git index as
-            they have not been initialized with a stream.
+            Will only work with indices that represent the default git index as they
+            have not been initialized with a stream.
         """
 
         # Only run if we are the default repository index.
@@ -1430,9 +1495,9 @@ def diff(
         if other is self.Index:
             return git_diff.DiffIndex()
 
-        # Index against anything but None is a reverse diff with the respective
-        # item. Handle existing -R flags properly. Transform strings to the object
-        # so that we can call diff on it.
+        # Index against anything but None is a reverse diff with the respective item.
+        # Handle existing -R flags properly.
+        # Transform strings to the object so that we can call diff on it.
         if isinstance(other, str):
             other = self.repo.rev_parse(other)
         # END object conversion
diff --git a/git/index/fun.py b/git/index/fun.py
index 580493f6d..785a1c748 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -1,7 +1,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Standalone functions to accompany the index implementation and make it more versatile."""
+"""Standalone functions to accompany the index implementation and make it more
+versatile."""
 
 from io import BytesIO
 import os
@@ -78,9 +79,15 @@ def _has_file_extension(path: str) -> str:
 def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     """Run the commit hook of the given name. Silently ignore hooks that do not exist.
 
-    :param name: name of hook, like 'pre-commit'
-    :param index: IndexFile instance
-    :param args: Arguments passed to hook file
+    :param name:
+        Name of hook, like ``pre-commit``.
+
+    :param index:
+        :class:`~git.index.base.IndexFile` instance.
+
+    :param args:
+        Arguments passed to hook file.
+
     :raises HookExecutionError:
     """
     hp = hook_path(name, index.repo.git_dir)
@@ -121,8 +128,8 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
 
 
 def stat_mode_to_index_mode(mode: int) -> int:
-    """Convert the given mode from a stat call to the corresponding index mode
-    and return it."""
+    """Convert the given mode from a stat call to the corresponding index mode and
+    return it."""
     if S_ISLNK(mode):  # symlinks
         return S_IFLNK
     if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK:  # submodules
@@ -138,16 +145,19 @@ def write_cache(
 ) -> None:
     """Write the cache represented by entries to a stream.
 
-    :param entries: **sorted** list of entries
+    :param entries:
+        **Sorted** list of entries.
 
-    :param stream: stream to wrap into the AdapterStreamCls - it is used for
-        final output.
+    :param stream:
+        Stream to wrap into the AdapterStreamCls - it is used for final output.
 
-    :param ShaStreamCls: Type to use when writing to the stream. It produces a sha
-        while writing to it, before the data is passed on to the wrapped stream
+    :param ShaStreamCls:
+        Type to use when writing to the stream. It produces a sha while writing to it,
+        before the data is passed on to the wrapped stream.
 
-    :param extension_data: any kind of data to write as a trailer, it must begin
-        a 4 byte identifier, followed by its size (4 bytes).
+    :param extension_data:
+        Any kind of data to write as a trailer, it must begin a 4 byte identifier,
+        followed by its size (4 bytes).
     """
     # Wrap the stream into a compatible writer.
     stream_sha = ShaStreamCls(stream)
@@ -197,7 +207,7 @@ def write_cache(
 
 
 def read_header(stream: IO[bytes]) -> Tuple[int, int]:
-    """Return tuple(version_long, num_entries) from the given stream"""
+    """Return tuple(version_long, num_entries) from the given stream."""
     type_id = stream.read(4)
     if type_id != b"DIRC":
         raise AssertionError("Invalid index file header: %r" % type_id)
@@ -210,9 +220,13 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
 
 
 def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]:
-    """:return: Key suitable to be used for the index.entries dictionary
+    """
+    :return:
+        Key suitable to be used for the :attr:`index.entries
+        <git.index.base.IndexFile.entries>` dictionary.
 
-    :param entry: One instance of type BaseIndexEntry or the path and the stage
+    :param entry:
+        One instance of type BaseIndexEntry or the path and the stage.
     """
 
     # def is_entry_key_tup(entry_key: Tuple) -> TypeGuard[Tuple[PathLike, int]]:
@@ -234,12 +248,14 @@ def read_cache(
 ) -> Tuple[int, Dict[Tuple[PathLike, int], "IndexEntry"], bytes, bytes]:
     """Read a cache file from the given stream.
 
-    :return: tuple(version, entries_dict, extension_data, content_sha)
+    :return:
+        tuple(version, entries_dict, extension_data, content_sha)
 
-      * version is the integer version number.
-      * entries dict is a dictionary which maps IndexEntry instances to a path at a stage.
-      * extension_data is '' or 4 bytes of type + 4 bytes of size + size bytes.
-      * content_sha is a 20 byte sha on all cache file contents.
+        * *version* is the integer version number.
+        * *entries_dict* is a dictionary which maps IndexEntry instances to a path at a
+          stage.
+        * *extension_data* is ``""`` or 4 bytes of type + 4 bytes of size + size bytes.
+        * *content_sha* is a 20 byte sha on all cache file contents.
     """
     version, num_entries = read_header(stream)
     count = 0
@@ -285,15 +301,25 @@ def read_cache(
 def write_tree_from_cache(
     entries: List[IndexEntry], odb: "GitCmdObjectDB", sl: slice, si: int = 0
 ) -> Tuple[bytes, List["TreeCacheTup"]]:
-    """Create a tree from the given sorted list of entries and put the respective
+    R"""Create a tree from the given sorted list of entries and put the respective
     trees into the given object database.
 
-    :param entries: **Sorted** list of IndexEntries
-    :param odb: Object database to store the trees in
-    :param si: Start index at which we should start creating subtrees
-    :param sl: Slice indicating the range we should process on the entries list
-    :return: tuple(binsha, list(tree_entry, ...)) a tuple of a sha and a list of
-        tree entries being a tuple of hexsha, mode, name
+    :param entries:
+        **Sorted** list of :class:`~git.index.typ.IndexEntry`\s.
+
+    :param odb:
+        Object database to store the trees in.
+
+    :param si:
+        Start index at which we should start creating subtrees.
+
+    :param sl:
+        Slice indicating the range we should process on the entries list.
+
+    :return:
+        tuple(binsha, list(tree_entry, ...))
+
+        A tuple of a sha and a list of tree entries being a tuple of hexsha, mode, name.
     """
     tree_items: List["TreeCacheTup"] = []
 
@@ -346,15 +372,17 @@ def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> Bas
 
 
 def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]:
-    """
-    :return: List of BaseIndexEntries representing the aggressive merge of the given
-        trees. All valid entries are on stage 0, whereas the conflicting ones are left
-        on stage 1, 2 or 3, whereas stage 1 corresponds to the common ancestor tree,
-        2 to our tree and 3 to 'their' tree.
-
-    :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas.
-        If 1 or two, the entries will effectively correspond to the last given tree.
-        If 3 are given, a 3 way merge is performed.
+    R"""
+    :return:
+        List of :class:`~git.index.typ.BaseIndexEntry`\s representing the aggressive
+        merge of the given trees. All valid entries are on stage 0, whereas the
+        conflicting ones are left on stage 1, 2 or 3, whereas stage 1 corresponds to the
+        common ancestor tree, 2 to our tree and 3 to 'their' tree.
+
+    :param tree_shas:
+        1, 2 or 3 trees as identified by their binary 20 byte shas. If 1 or two, the
+        entries will effectively correspond to the last given tree. If 3 are given, a 3
+        way merge is performed.
     """
     out: List[BaseIndexEntry] = []
 
diff --git a/git/index/typ.py b/git/index/typ.py
index 894e6f16d..dce67626f 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -58,7 +58,8 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
         blob_path: Path = blob_pathlike if isinstance(blob_pathlike, Path) else Path(blob_pathlike)
         for pathlike in self.paths:
             path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike)
-            # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no longer supported.
+            # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no
+            # longer supported.
             filter_parts: List[str] = path.parts
             blob_parts: List[str] = blob_path.parts
             if len(filter_parts) > len(blob_parts):
@@ -69,8 +70,11 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
 
 
 class BaseIndexEntryHelper(NamedTuple):
-    """Typed namedtuple to provide named attribute access for BaseIndexEntry.
-    Needed to allow overriding __new__ in child class to preserve backwards compat."""
+    """Typed named tuple to provide named attribute access for BaseIndexEntry.
+
+    This is needed to allow overriding ``__new__`` in child class to preserve backwards
+    compatibility.
+    """
 
     mode: int
     binsha: bytes
@@ -101,7 +105,8 @@ def __new__(
             Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
         ],
     ) -> "BaseIndexEntry":
-        """Override __new__ to allow construction from a tuple for backwards compatibility"""
+        """Override ``__new__`` to allow construction from a tuple for backwards
+        compatibility."""
         return super().__new__(cls, *inp_tuple)
 
     def __str__(self) -> str:
diff --git a/git/index/util.py b/git/index/util.py
index 125925783..8aedb7284 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -34,8 +34,8 @@
 
 
 class TemporaryFileSwap:
-    """Utility class moving a file to a temporary location within the same directory
-    and moving it back on to where on object deletion."""
+    """Utility class moving a file to a temporary location within the same directory and
+    moving it back on to where on object deletion."""
 
     __slots__ = ("file_path", "tmp_file_path")
 
@@ -66,12 +66,12 @@ def __exit__(
 
 def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
     """Decorator for functions that alter the index using the git command. This would
-    invalidate our possibly existing entries dictionary which is why it must be
-    deleted to allow it to be lazily reread later.
+    invalidate our possibly existing entries dictionary which is why it must be deleted
+    to allow it to be lazily reread later.
 
     :note:
-        This decorator will not be required once all functions are implemented
-        natively which in fact is possible, but probably not feasible performance wise.
+        This decorator will not be required once all functions are implemented natively
+        which in fact is possible, but probably not feasible performance wise.
     """
 
     @wraps(func)

From ca2ab6137b2cae27fb5c68dbd63c83bd8e9b429e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 14:25:24 -0500
Subject: [PATCH 481/994] Rewrite post_clear_cache note

So that it just describes the current situation, rather than
suggesting a future direction for IndexFile.

This is for #1847.
---
 git/index/util.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/index/util.py b/git/index/util.py
index 8aedb7284..347e8747f 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -70,8 +70,8 @@ def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
     to allow it to be lazily reread later.
 
     :note:
-        This decorator will not be required once all functions are implemented natively
-        which in fact is possible, but probably not feasible performance wise.
+        This decorator is required because not all functions related to
+        :class:`~git.index.base.IndexFile` are implemented natively.
     """
 
     @wraps(func)

From 37421e1b8f0618e1d4e5bd322aeb60d23b58e1b9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 15:06:19 -0500
Subject: [PATCH 482/994] Further revise post_clear_cache docstring

For #1847. This removes the note, and splits out some related
material from the docstring's top line into a second paragraph
for readability (since the first sentence of the top line was
complete, and described usage).
---
 git/index/util.py | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/git/index/util.py b/git/index/util.py
index 347e8747f..0bad11571 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -65,13 +65,10 @@ def __exit__(
 
 
 def post_clear_cache(func: Callable[..., _T]) -> Callable[..., _T]:
-    """Decorator for functions that alter the index using the git command. This would
-    invalidate our possibly existing entries dictionary which is why it must be deleted
-    to allow it to be lazily reread later.
+    """Decorator for functions that alter the index using the git command.
 
-    :note:
-        This decorator is required because not all functions related to
-        :class:`~git.index.base.IndexFile` are implemented natively.
+    When a git command alters the index, this invalidates our possibly existing entries
+    dictionary, which is why it must be deleted to allow it to be lazily reread later.
     """
 
     @wraps(func)

From ca32c226e9d52001e7f2feaa516bb2483210db82 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 15:49:04 -0500
Subject: [PATCH 483/994] Condense output_stream description in Git.execute
 docstring

For #1845.
---
 git/cmd.py | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 760a9ff12..cab089746 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1018,12 +1018,8 @@ def execute(
 
         :param output_stream:
             If set to a file-like object, data produced by the git command will be
-            output to the given stream directly.
-            This feature only has any effect if `as_process` is False. Processes will
-            always be created with a pipe due to issues with subprocess.
-            This merely is a workaround as data will be copied from the
-            output pipe to the given output stream directly.
-            Judging from the implementation, you shouldn't use this parameter!
+            copied to the given stream instead of being returned as a string.
+            This feature only has any effect if `as_process` is False.
 
         :param stdout_as_string:
             If False, the command's standard output will be bytes. Otherwise, it will be

From 3813bfbee34640da121209ecea0388b0cd1d39cf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 17:52:54 -0500
Subject: [PATCH 484/994] Clarify Submodule.branch_path documentation

This changes "Full (relative) path" to "Complete relative path" in
the wording used to describe the branch_path initializer parameter
and property of the Submodule class. This is to prevent confusion,
since "full path" means something like "absolute path" (and is now
used as such in error messages introduced since these docstrings
were last edited).
---
 git/objects/submodule/base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index d08e967b8..497e5a06a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -124,7 +124,7 @@ def __init__(
             the ``url`` parameter.
         :param parent_commit: See :meth:`set_parent_commit`.
         :param url: The URL to the remote repository which is the submodule.
-        :param branch_path: Full (relative) path to ref to checkout when cloning the
+        :param branch_path: Complete relative path to ref to checkout when cloning the
             remote repository.
         """
         super().__init__(repo, binsha, mode, path)
@@ -1322,7 +1322,7 @@ def branch(self) -> "Head":
     @property
     def branch_path(self) -> PathLike:
         """
-        :return: Full (relative) path as string to the branch we would checkout
+        :return: Complete relative path as string to the branch we would checkout
             from the remote and track
         """
         return self._branch_path

From 63c62ed801380be88ecdf7d686ddcc7cc13cdb29 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 19:43:59 -0500
Subject: [PATCH 485/994] Revise docstrings within git.objects.submodule

---
 git/objects/submodule/base.py |  82 ++++++++++++-------
 git/objects/submodule/root.py | 148 +++++++++++++++++++---------------
 git/objects/submodule/util.py |  15 ++--
 3 files changed, 147 insertions(+), 98 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 497e5a06a..58b474fdd 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -60,7 +60,8 @@
 
 class UpdateProgress(RemoteProgress):
     """Class providing detailed progress information to the caller who should
-    derive from it and implement the ``update(...)`` message."""
+    derive from it and implement the
+    :meth:`update(...) <git.util.RemoteProgress.update>` message."""
 
     CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
     _num_op_codes: int = RemoteProgress._num_op_codes + 3
@@ -76,8 +77,8 @@ class UpdateProgress(RemoteProgress):
 
 
 # IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
-# mechanism, which causes plenty of trouble if the only reason for packages and
-# modules is refactoring - subpackages shouldn't depend on parent packages.
+# mechanism, which causes plenty of trouble if the only reason for packages and modules
+# is refactoring - subpackages shouldn't depend on parent packages.
 class Submodule(IndexObject, TraversableIterableObj):
     """Implements access to a git submodule. They are special in that their sha
     represents a commit in the submodule's repository which is to be checked out
@@ -119,11 +120,19 @@ def __init__(
         We only document the parameters that differ from
         :class:`~git.objects.base.IndexObject`.
 
-        :param repo: Our parent repository.
-        :param binsha: Binary sha referring to a commit in the remote repository. See
-            the ``url`` parameter.
-        :param parent_commit: See :meth:`set_parent_commit`.
-        :param url: The URL to the remote repository which is the submodule.
+        :param repo:
+            Our parent repository.
+
+        :param binsha:
+            Binary sha referring to a commit in the remote repository.
+            See the `url` parameter.
+
+        :param parent_commit:
+            See :meth:`set_parent_commit`.
+
+        :param url:
+            The URL to the remote repository which is the submodule.
+
         :param branch_path: Complete relative path to ref to checkout when cloning the
             remote repository.
         """
@@ -1313,9 +1322,11 @@ def exists(self) -> bool:
     @property
     def branch(self) -> "Head":
         """
-        :return: The branch instance that we are to checkout
+        :return:
+            The branch instance that we are to checkout
 
-        :raise InvalidGitRepositoryError: If our module is not yet checked out
+        :raise InvalidGitRepositoryError:
+            If our module is not yet checked out.
         """
         return mkhead(self.module(), self._branch_path)
 
@@ -1329,7 +1340,10 @@ def branch_path(self) -> PathLike:
 
     @property
     def branch_name(self) -> str:
-        """:return: The name of the branch, which is the shortest possible branch name"""
+        """
+        :return:
+            The name of the branch, which is the shortest possible branch name
+        """
         # Use an instance method, for this we create a temporary Head instance
         # which uses a repository that is available at least (it makes no difference).
         return git.Head(self.repo, self._branch_path).name
@@ -1342,9 +1356,11 @@ def url(self) -> str:
     @property
     def parent_commit(self) -> "Commit_ish":
         """
-        :return: Commit instance with the tree containing the .gitmodules file
+        :return:
+            Commit instance with the tree containing the ``.gitmodules`` file
 
-        :note: Will always point to the current head's commit if it was not set explicitly.
+        :note:
+            Will always point to the current head's commit if it was not set explicitly.
         """
         if self._parent_commit is None:
             return self.repo.commit()
@@ -1353,33 +1369,40 @@ def parent_commit(self) -> "Commit_ish":
     @property
     def name(self) -> str:
         """
-        :return: The name of this submodule. It is used to identify it within the
-            .gitmodules file.
+        :return:
+            The name of this submodule. It is used to identify it within the
+            ``.gitmodules`` file.
 
-        :note: By default, this is the name is the path at which to find the submodule,
-            but in GitPython it should be a unique identifier similar to the identifiers
+        :note:
+            By default, this is the name is the path at which to find the submodule, but
+            in GitPython it should be a unique identifier similar to the identifiers
             used for remotes, which allows to change the path of the submodule easily.
         """
         return self._name
 
     def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
         """
-        :return: ConfigReader instance which allows you to query the configuration
-            values of this submodule, as provided by the .gitmodules file.
+        :return:
+            ConfigReader instance which allows you to query the configuration values of
+            this submodule, as provided by the ``.gitmodules`` file.
 
-        :note: The config reader will actually read the data directly from the
-            repository and thus does not need nor care about your working tree.
+        :note:
+            The config reader will actually read the data directly from the repository
+            and thus does not need nor care about your working tree.
 
-        :note: Should be cached by the caller and only kept as long as needed.
+        :note:
+            Should be cached by the caller and only kept as long as needed.
 
-        :raise IOError: If the .gitmodules file/blob could not be read.
+        :raise IOError:
+            If the ``.gitmodules`` file/blob could not be read.
         """
         return self._config_parser_constrained(read_only=True)
 
     def children(self) -> IterableList["Submodule"]:
         """
-        :return: IterableList(Submodule, ...) an iterable list of submodules instances
-            which are children of this submodule or 0 if the submodule is not checked out.
+        :return:
+            IterableList(Submodule, ...) An iterable list of submodules instances which
+            are children of this submodule or 0 if the submodule is not checked out.
         """
         return self._get_intermediate_items(self)
 
@@ -1395,7 +1418,10 @@ def iter_items(
         *Args: Any,
         **kwargs: Any,
     ) -> Iterator["Submodule"]:
-        """:return: Iterator yielding Submodule instances available in the given repository"""
+        """
+        :return:
+            Iterator yielding Submodule instances available in the given repository
+        """
         try:
             pc = repo.commit(parent_commit)  # Parent commit instance
             parser = cls._config_parser(repo, pc, read_only=True)
@@ -1423,8 +1449,8 @@ def iter_items(
                     entry = index.entries[index.entry_key(p, 0)]
                     sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
                 except KeyError:
-                    # The submodule doesn't exist, probably it wasn't
-                    # removed from the .gitmodules file.
+                    # The submodule doesn't exist, probably it wasn't removed from the
+                    # .gitmodules file.
                     continue
                 # END handle keyerror
             # END handle critical error
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index dde384bbe..8ef874f90 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -26,7 +26,8 @@
 
 
 class RootUpdateProgress(UpdateProgress):
-    """Utility class which adds more opcodes to the UpdateProgress."""
+    """Utility class which adds more opcodes to
+    :class:`~git.objects.submodule.base.UpdateProgress`."""
 
     REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
         1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
@@ -91,43 +92,59 @@ def update(
         This method behaves smartly by determining changes of the path of a submodules
         repository, next to changes to the to-be-checked-out commit or the branch to be
         checked out. This works if the submodules ID does not change.
+
         Additionally it will detect addition and removal of submodules, which will be
         handled gracefully.
 
-        :param previous_commit: If set to a commit-ish, the commit we should use as the
-            previous commit the HEAD pointed to before it was set to the commit it
-            points to now.
-            If None, it defaults to ``HEAD@{1}`` otherwise
-        :param recursive: if True, the children of submodules will be updated as well
-            using the same technique.
-        :param force_remove: If submodules have been deleted, they will be forcibly
-            removed. Otherwise the update may fail if a submodule's repository cannot be
-            deleted as changes have been made to it.
+        :param previous_commit:
+            If set to a commit-ish, the commit we should use as the previous commit the
+            HEAD pointed to before it was set to the commit it points to now.
+            If None, it defaults to ``HEAD@{1}`` otherwise.
+
+        :param recursive:
+            If True, the children of submodules will be updated as well using the same
+            technique.
+
+        :param force_remove:
+            If submodules have been deleted, they will be forcibly removed. Otherwise
+            the update may fail if a submodule's repository cannot be deleted as changes
+            have been made to it.
             (See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
             for more information.)
-        :param init: If we encounter a new module which would need to be initialized,
-            then do it.
-        :param to_latest_revision: If True, instead of checking out the revision pointed
-            to by this submodule's sha, the checked out tracking branch will be merged
-            with the latest remote branch fetched from the repository's origin.
+
+        :param init:
+            If we encounter a new module which would need to be initialized, then do it.
+
+        :param to_latest_revision:
+            If True, instead of checking out the revision pointed to by this submodule's
+            sha, the checked out tracking branch will be merged with the latest remote
+            branch fetched from the repository's origin.
             Unless `force_reset` is specified, a local tracking branch will never be
             reset into its past, therefore the remote branch must be in the future for
             this to have an effect.
-        :param force_reset: If True, submodules may checkout or reset their branch even
-            if the repository has pending changes that would be overwritten, or if the
-            local tracking branch is in the future of the remote tracking branch and
-            would be reset into its past.
-        :param progress: :class:`RootUpdateProgress` instance or None if no progress
-            should be sent.
-        :param dry_run: If True, operations will not actually be performed. Progress
-            messages will change accordingly to indicate the WOULD DO state of the
-            operation.
-        :param keep_going: If True, we will ignore but log all errors, and keep going
-            recursively. Unless `dry_run` is set as well, `keep_going` could cause
+
+        :param force_reset:
+            If True, submodules may checkout or reset their branch even if the
+            repository has pending changes that would be overwritten, or if the local
+            tracking branch is in the future of the remote tracking branch and would be
+            reset into its past.
+
+        :param progress:
+            :class:`RootUpdateProgress` instance or None if no progress should be sent.
+
+        :param dry_run:
+            If True, operations will not actually be performed. Progress messages will
+            change accordingly to indicate the WOULD DO state of the operation.
+
+        :param keep_going:
+            If True, we will ignore but log all errors, and keep going recursively.
+            Unless `dry_run` is set as well, `keep_going` could cause
             subsequent/inherited errors you wouldn't see otherwise.
             In conjunction with `dry_run`, this can be useful to anticipate all errors
             when updating submodules.
-        :return: self
+
+        :return:
+            self
         """
         if self.repo.bare:
             raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
@@ -234,13 +251,14 @@ def update(
                     ###################
                     if sm.url != psm.url:
                         # Add the new remote, remove the old one.
-                        # This way, if the url just changes, the commits will not
-                        # have to be re-retrieved.
+                        # This way, if the url just changes, the commits will not have
+                        # to be re-retrieved.
                         nn = "__new_origin__"
                         smm = sm.module()
                         rmts = smm.remotes
 
-                        # Don't do anything if we already have the url we search in place.
+                        # Don't do anything if we already have the url we search in
+                        # place.
                         if len([r for r in rmts if r.url == sm.url]) == 0:
                             progress.update(
                                 BEGIN | URLCHANGE,
@@ -272,17 +290,17 @@ def update(
                                     # END if urls match
                                 # END for each remote
 
-                                # If we didn't find a matching remote, but have exactly one,
-                                # we can safely use this one.
+                                # If we didn't find a matching remote, but have exactly
+                                # one, we can safely use this one.
                                 if rmt_for_deletion is None:
                                     if len(rmts) == 1:
                                         rmt_for_deletion = rmts[0]
                                     else:
-                                        # If we have not found any remote with the original URL
-                                        # we may not have a name. This is a special case,
-                                        # and its okay to fail here.
-                                        # Alternatively we could just generate a unique name and
-                                        # leave all existing ones in place.
+                                        # If we have not found any remote with the
+                                        # original URL we may not have a name. This is a
+                                        # special case, and its okay to fail here.
+                                        # Alternatively we could just generate a unique
+                                        # name and leave all existing ones in place.
                                         raise InvalidGitRepositoryError(
                                             "Couldn't find original remote-repo at url %r" % psm.url
                                         )
@@ -292,19 +310,19 @@ def update(
                                 orig_name = rmt_for_deletion.name
                                 smm.delete_remote(rmt_for_deletion)
                                 # NOTE: Currently we leave tags from the deleted remotes
-                                # as well as separate tracking branches in the possibly totally
-                                # changed repository (someone could have changed the url to
-                                # another project). At some point, one might want to clean
-                                # it up, but the danger is high to remove stuff the user
-                                # has added explicitly.
+                                # as well as separate tracking branches in the possibly
+                                # totally changed repository (someone could have changed
+                                # the url to another project). At some point, one might
+                                # want to clean it up, but the danger is high to remove
+                                # stuff the user has added explicitly.
 
                                 # Rename the new remote back to what it was.
                                 smr.rename(orig_name)
 
-                                # Early on, we verified that the our current tracking branch
-                                # exists in the remote. Now we have to ensure that the
-                                # sha we point to is still contained in the new remote
-                                # tracking branch.
+                                # Early on, we verified that the our current tracking
+                                # branch exists in the remote. Now we have to ensure
+                                # that the sha we point to is still contained in the new
+                                # remote tracking branch.
                                 smsha = sm.binsha
                                 found = False
                                 rref = smr.refs[self.branch_name]
@@ -316,10 +334,11 @@ def update(
                                 # END for each commit
 
                                 if not found:
-                                    # Adjust our internal binsha to use the one of the remote
-                                    # this way, it will be checked out in the next step.
-                                    # This will change the submodule relative to us, so
-                                    # the user will be able to commit the change easily.
+                                    # Adjust our internal binsha to use the one of the
+                                    # remote this way, it will be checked out in the
+                                    # next step. This will change the submodule relative
+                                    # to us, so the user will be able to commit the
+                                    # change easily.
                                     _logger.warning(
                                         "Current sha %s was not contained in the tracking\
              branch at the new remote, setting it the the remote's tracking branch",
@@ -328,7 +347,8 @@ def update(
                                     sm.binsha = rref.commit.binsha
                                 # END reset binsha
 
-                                # NOTE: All checkout is performed by the base implementation of update.
+                                # NOTE: All checkout is performed by the base
+                                # implementation of update.
                             # END handle dry_run
                             progress.update(
                                 END | URLCHANGE,
@@ -342,7 +362,8 @@ def update(
                     # HANDLE PATH CHANGES
                     #####################
                     if sm.branch_path != psm.branch_path:
-                        # Finally, create a new tracking branch which tracks the new remote branch.
+                        # Finally, create a new tracking branch which tracks the new
+                        # remote branch.
                         progress.update(
                             BEGIN | BRANCHCHANGE,
                             i,
@@ -354,8 +375,8 @@ def update(
                         if not dry_run:
                             smm = sm.module()
                             smmr = smm.remotes
-                            # As the branch might not exist yet, we will have to fetch all remotes
-                            # to be sure...
+                            # As the branch might not exist yet, we will have to fetch
+                            # all remotes to be sure...
                             for remote in smmr:
                                 remote.fetch(progress=progress)
                             # END for each remote
@@ -372,11 +393,12 @@ def update(
                             # END ensure tracking branch exists
 
                             tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
-                            # NOTE: All head-resetting is done in the base implementation of update
-                            # but we will have to checkout the new branch here. As it still points
-                            # to the currently checked out commit, we don't do any harm.
-                            # As we don't want to update working-tree or index, changing the ref is
-                            # all there is to do.
+                            # NOTE: All head-resetting is done in the base
+                            # implementation of update but we will have to checkout the
+                            # new branch here. As it still points to the currently
+                            # checked out commit, we don't do any harm.
+                            # As we don't want to update working-tree or index, changing
+                            # the ref is all there is to do.
                             smm.head.reference = tbr
                         # END handle dry_run
 
@@ -409,10 +431,10 @@ def update(
                 keep_going=keep_going,
             )
 
-            # Update recursively depth first - question is which inconsistent
-            # state will be better in case it fails somewhere. Defective branch
-            # or defective depth. The RootSubmodule type will never process itself,
-            # which was done in the previous expression.
+            # Update recursively depth first - question is which inconsistent state will
+            # be better in case it fails somewhere. Defective branch or defective depth.
+            # The RootSubmodule type will never process itself, which was done in the
+            # previous expression.
             if recursive:
                 # The module would exist by now if we are not in dry_run mode.
                 if sm.module_exists():
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index f8265798d..91e18a26b 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -35,7 +35,7 @@
 
 
 def sm_section(name: str) -> str:
-    """:return: Section title used in .gitmodules configuration file"""
+    """:return: Section title used in ``.gitmodules`` configuration file"""
     return f'submodule "{name}"'
 
 
@@ -51,7 +51,8 @@ def mkhead(repo: "Repo", path: PathLike) -> "Head":
 
 
 def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
-    """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError."""
+    """Find the remote branch matching the name of the given branch or raise
+    :class:`git.exc.InvalidGitRepositoryError`."""
     for remote in remotes:
         try:
             return remote.refs[branch_name]
@@ -69,11 +70,11 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "
 
 
 class SubmoduleConfigParser(GitConfigParser):
-    """Catches calls to _write, and updates the .gitmodules blob in the index
+    """Catches calls to _write, and updates the ``.gitmodules`` blob in the index
     with the new data, if we have written into a stream.
 
-    Otherwise it would add the local file to the index to make it correspond
-    with the working tree. Additionally, the cache must be cleared.
+    Otherwise it would add the local file to the index to make it correspond with the
+    working tree. Additionally, the cache must be cleared.
 
     Please note that no mutating method will work in bare mode.
     """
@@ -86,8 +87,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
 
     # { Interface
     def set_submodule(self, submodule: "Submodule") -> None:
-        """Set this instance's submodule. It must be called before
-        the first write operation begins."""
+        """Set this instance's submodule. It must be called before the first write
+        operation begins."""
         self._smref = weakref.ref(submodule)
 
     def flush_to_index(self) -> None:

From 115451d4605b880e26c59bb415b539cdce984b64 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 20:13:16 -0500
Subject: [PATCH 486/994] Change _write to write in SubmoduleConfigParser
 docstring

Since it appears to refer to the write method, which it overrides,
rather that the nonpublic _write method (which is not overridden,
and whose direct calls are not affected).
---
 git/objects/submodule/util.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index 91e18a26b..b02da501e 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -70,8 +70,9 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "
 
 
 class SubmoduleConfigParser(GitConfigParser):
-    """Catches calls to _write, and updates the ``.gitmodules`` blob in the index
-    with the new data, if we have written into a stream.
+    """Catches calls to :meth:`~git.config.GitConfigParser.write`, and updates the
+    ``.gitmodules`` blob in the index with the new data, if we have written into a
+    stream.
 
     Otherwise it would add the local file to the index to make it correspond with the
     working tree. Additionally, the cache must be cleared.

From 521948989134c8e0ab7d4c1063780a0799f4dbc8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 20:58:21 -0500
Subject: [PATCH 487/994] Fix IndexObject.abspath docstring formatting

The original problem where the backslash wasn't included in the
docstring at all was fixed in 7dd2095 (#1725), but the backslash
still did not appear in rendered Sphinx documentation, because it
was also treated as a reStructuredText metacharacter.

Although that can be addressed by adding a further backslash to
escape it, the effect is ambiguous when the docstring is read in
the code. So this just encloses it in a double-backticked code
span instead, which is a somewhat clearer way to show it anyway.
---
 git/objects/base.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 934fb40bc..938dd826c 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -226,7 +226,8 @@ def abspath(self) -> PathLike:
             Absolute path to this index object in the file system (as opposed to the
             :attr:`path` field which is a path relative to the git repository).
 
-            The returned path will be native to the system and contains '\' on Windows.
+            The returned path will be native to the system and contains ``\`` on
+            Windows.
         """
         if self.repo.working_tree_dir is not None:
             return join_path_native(self.repo.working_tree_dir, self.path)

From c06dfd9bdc076e44df436836914d07a1f9f695c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 26 Feb 2024 21:49:29 -0500
Subject: [PATCH 488/994] Fix parameter names in TagObject.__init__

---
 git/objects/tag.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/objects/tag.py b/git/objects/tag.py
index f7aaaf477..f874f5bdf 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -59,8 +59,8 @@ def __init__(
         :param tagged_date: int_seconds_since_epoch
             The :class:`DateTime` of the tag creation.
             Use :func:`time.gmtime` to convert it into a different format.
-        :param tagged_tz_offset: int_seconds_west_of_utc
-            The timezone that the authored_date is in, in a format similar
+        :param tagger_tz_offset: int_seconds_west_of_utc
+            The timezone that the tagged_date is in, in a format similar
             to :attr:`time.altzone`.
         """
         super().__init__(repo, binsha)

From ae37a4a699608c6bc55e023547c8daa473477920 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 13:21:46 -0500
Subject: [PATCH 489/994] Revise docstrings within git.objects

But not within git.objects.submodule, which was done in 63c62ed.
---
 git/objects/__init__.py |   4 +-
 git/objects/base.py     |  71 +++++++----
 git/objects/blob.py     |   3 +-
 git/objects/commit.py   | 252 ++++++++++++++++++++++++----------------
 git/objects/fun.py      |  69 ++++++-----
 git/objects/tag.py      |  28 +++--
 git/objects/tree.py     |  88 ++++++++------
 git/objects/util.py     | 167 +++++++++++++++-----------
 8 files changed, 415 insertions(+), 267 deletions(-)

diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index fc8d8fbb2..1061ec874 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -14,8 +14,8 @@
 from .tag import *  # noqa: F403
 from .tree import *  # noqa: F403
 
-# Fix import dependency - add IndexObject to the util module, so that it can be
-# imported by the submodule.base.
+# Fix import dependency - add IndexObject to the util module, so that it can be imported
+# by the submodule.base.
 smutil.IndexObject = IndexObject  # type: ignore[attr-defined]  # noqa: F405
 smutil.Object = Object  # type: ignore[attr-defined]  # noqa: F405
 del smutil
diff --git a/git/objects/base.py b/git/objects/base.py
index 938dd826c..df56a4bac 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -37,7 +37,9 @@
 
 
 class Object(LazyMixin):
-    """An Object which may be Blobs, Trees, Commits and Tags."""
+    """An Object which may be :class:`~git.objects.blob.Blob`,
+    :class:`~git.objects.tree.Tree`, :class:`~git.objects.commit.Commit` or
+    `~git.objects.tag.TagObject`."""
 
     NULL_HEX_SHA = "0" * 40
     NULL_BIN_SHA = b"\0" * 20
@@ -55,11 +57,14 @@ class Object(LazyMixin):
 
     def __init__(self, repo: "Repo", binsha: bytes):
         """Initialize an object by identifying it by its binary sha.
+
         All keyword arguments will be set on demand if None.
 
-        :param repo: repository this object is located in
+        :param repo:
+            Repository this object is located in.
 
-        :param binsha: 20 byte SHA1
+        :param binsha:
+            20 byte SHA1
         """
         super().__init__()
         self.repo = repo
@@ -72,24 +77,28 @@ def __init__(self, repo: "Repo", binsha: bytes):
     @classmethod
     def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
         """
-        :return: New :class:`Object`` instance of a type appropriate to the object type
-            behind `id`. The id of the newly created object will be a binsha even though
-            the input id may have been a Reference or Rev-Spec.
+        :return:
+            New :class:`Object` instance of a type appropriate to the object type behind
+            `id`. The id of the newly created object will be a binsha even though the
+            input id may have been a `~git.refs.reference.Reference` or rev-spec.
 
-        :param id: reference, rev-spec, or hexsha
+        :param id:
+            :class:`~git.refs.reference.Reference`, rev-spec, or hexsha
 
-        :note: This cannot be a ``__new__`` method as it would always call
-            :meth:`__init__` with the input id which is not necessarily a binsha.
+        :note:
+            This cannot be a ``__new__`` method as it would always call :meth:`__init__`
+            with the input id which is not necessarily a binsha.
         """
         return repo.rev_parse(str(id))
 
     @classmethod
     def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
         """
-        :return: new object instance of a type appropriate to represent the given
-            binary sha1
+        :return:
+            New object instance of a type appropriate to represent the given binary sha1
 
-        :param sha1: 20 byte binary sha1
+        :param sha1:
+            20 byte binary sha1
         """
         if sha1 == cls.NULL_BIN_SHA:
             # The NULL binsha is always the root commit.
@@ -126,11 +135,11 @@ def __hash__(self) -> int:
         return hash(self.binsha)
 
     def __str__(self) -> str:
-        """:return: string of our SHA1 as understood by all git commands"""
+        """:return: String of our SHA1 as understood by all git commands"""
         return self.hexsha
 
     def __repr__(self) -> str:
-        """:return: string with pythonic representation of our object"""
+        """:return: String with pythonic representation of our object"""
         return '<git.%s "%s">' % (self.__class__.__name__, self.hexsha)
 
     @property
@@ -142,17 +151,22 @@ def hexsha(self) -> str:
     @property
     def data_stream(self) -> "OStream":
         """
-        :return: File Object compatible stream to the uncompressed raw data of the object
+        :return:
+            File-object compatible stream to the uncompressed raw data of the object
 
-        :note: Returned streams must be read in order.
+        :note:
+            Returned streams must be read in order.
         """
         return self.repo.odb.stream(self.binsha)
 
     def stream_data(self, ostream: "OStream") -> "Object":
         """Write our data directly to the given output stream.
 
-        :param ostream: File object compatible stream object.
-        :return: self
+        :param ostream:
+            File-object compatible stream object.
+
+        :return:
+            self
         """
         istream = self.repo.odb.stream(self.binsha)
         stream_copy(istream, ostream)
@@ -160,8 +174,9 @@ def stream_data(self, ostream: "OStream") -> "Object":
 
 
 class IndexObject(Object):
-    """Base for all objects that can be part of the index file, namely Tree, Blob and
-    SubModule objects."""
+    """Base for all objects that can be part of the index file, namely
+    :class:`~git.objects.tree.Tree`, :class:`~git.objects.blob.Blob` and
+    :class:`~git.objects.submodule.base.Submodule` objects."""
 
     __slots__ = ("path", "mode")
 
@@ -175,16 +190,22 @@ def __init__(
         mode: Union[None, int] = None,
         path: Union[None, PathLike] = None,
     ) -> None:
-        """Initialize a newly instanced IndexObject.
+        """Initialize a newly instanced :class:`IndexObject`.
+
+        :param repo:
+            The :class:`~git.repo.base.Repo` we are located in.
+
+        :param binsha:
+            20 byte sha1.
 
-        :param repo: The :class:`~git.repo.base.Repo` we are located in.
-        :param binsha: 20 byte sha1.
         :param mode:
             The stat compatible file mode as int, use the :mod:`stat` module to evaluate
             the information.
+
         :param path:
             The path to the file in the file system, relative to the git repository
             root, like ``file.ext`` or ``folder/other.ext``.
+
         :note:
             Path may not be set if the index object has been created directly, as it
             cannot be retrieved without knowing the parent tree.
@@ -198,8 +219,8 @@ def __init__(
     def __hash__(self) -> int:
         """
         :return:
-            Hash of our path as index items are uniquely identifiable by path, not
-            by their data!
+            Hash of our path as index items are uniquely identifiable by path, not by
+            their data!
         """
         return hash(self.path)
 
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 6d7e859af..50c12f0d7 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -29,7 +29,8 @@ def mime_type(self) -> str:
         """
         :return: String describing the mime type of this file (based on the filename)
 
-        :note: Defaults to 'text/plain' in case the actual file type is unknown.
+        :note:
+            Defaults to ``text/plain`` in case the actual file type is unknown.
         """
         guesses = None
         if self.path:
diff --git a/git/objects/commit.py b/git/objects/commit.py
index caec1b6c4..e6aa7ac39 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -58,11 +58,11 @@
 
 
 class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
+    """Wraps a git commit object.
 
-    """Wraps a git Commit object.
-
-    This class will act lazily on some of its attributes and will query the
-    value on demand only if it involves calling the git binary."""
+    This class will act lazily on some of its attributes and will query the value on
+    demand only if it involves calling the git binary.
+    """
 
     # ENVIRONMENT VARIABLES
     # Read when creating new commits.
@@ -108,41 +108,55 @@ def __init__(
         encoding: Union[str, None] = None,
         gpgsig: Union[str, None] = None,
     ) -> None:
-        """Instantiate a new Commit. All keyword arguments taking None as default will
-        be implicitly set on first query.
+        R"""Instantiate a new :class:`Commit`. All keyword arguments taking None as
+        default will be implicitly set on first query.
+
+        :param binsha:
+            20 byte sha1
 
-        :param binsha: 20 byte sha1
         :param parents: tuple(Commit, ...)
-            A tuple of commit ids or actual Commits
-        :param tree: Tree object
-        :param author: Actor
+            A tuple of commit ids or actual :class:`Commit`\s.
+
+        :param tree:
+            A :class:`~git.objects.tree.Tree` object.
+
+        :param author: :class:`~git.util.Actor`
             The author Actor object
+
         :param authored_date: int_seconds_since_epoch
-            The authored DateTime - use time.gmtime() to convert it into a
-            different format
+            The authored DateTime - use :func:`time.gmtime` to convert it into a
+            different format.
+
         :param author_tz_offset: int_seconds_west_of_utc
-            The timezone that the authored_date is in
-        :param committer: Actor
-            The committer string
+            The timezone that the `authored_date` is in.
+
+        :param committer: :class:`~git.util.Actor`
+            The committer string.
+
         :param committed_date: int_seconds_since_epoch
-            The committed DateTime - use time.gmtime() to convert it into a
-            different format
+            The committed DateTime - use :func:`time.gmtime` to convert it into a
+            different format.
+
         :param committer_tz_offset: int_seconds_west_of_utc
-            The timezone that the committed_date is in
+            The timezone that the `committed_date` is in.
+
         :param message: string
-            The commit message
+            The commit message.
+
         :param encoding: string
-            Encoding of the message, defaults to UTF-8
+            Encoding of the message, defaults to UTF-8.
+
         :param parents:
-            List or tuple of Commit objects which are our parent(s) in the commit
-            dependency graph
+            List or tuple of :class:`Commit` objects which are our parent(s) in the
+            commit dependency graph.
 
-        :return: git.Commit
+        :return:
+            :class:`Commit`
 
         :note:
-            Timezone information is in the same format and in the same sign
-            as what time.altzone returns. The sign is inverted compared to git's
-            UTC timezone.
+            Timezone information is in the same format and in the same sign as what
+            :func:`time.altzone` returns. The sign is inverted compared to git's UTC
+            timezone.
         """
         super().__init__(repo, binsha)
         self.binsha = binsha
@@ -179,8 +193,11 @@ def _get_intermediate_items(cls, commit: "Commit") -> Tuple["Commit", ...]:
     def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes:
         """Calculate the sha of a commit.
 
-        :param repo: Repo object the commit should be part of
-        :param commit: Commit object for which to generate the sha
+        :param repo:
+            :class:`~git.repo.base.Repo` object the commit should be part of.
+
+        :param commit:
+            :class:`Commit` object for which to generate the sha.
         """
 
         stream = BytesIO()
@@ -194,8 +211,8 @@ def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes:
     def replace(self, **kwargs: Any) -> "Commit":
         """Create new commit object from existing commit object.
 
-        Any values provided as keyword arguments will replace the
-        corresponding attribute in the new object.
+        Any values provided as keyword arguments will replace the corresponding
+        attribute in the new object.
         """
 
         attrs = {k: getattr(self, k) for k in self.__slots__}
@@ -239,17 +256,18 @@ def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any)
         """Count the number of commits reachable from this commit.
 
         :param paths:
-            An optional path or a list of paths restricting the return value
-            to commits actually containing the paths.
+            An optional path or a list of paths restricting the return value to commits
+            actually containing the paths.
 
         :param kwargs:
-            Additional options to be passed to git-rev-list. They must not alter
-            the output style of the command, or parsing will yield incorrect results.
+            Additional options to be passed to ``git rev-list``. They must not alter the
+            output style of the command, or parsing will yield incorrect results.
 
-        :return: An int defining the number of reachable commits
+        :return:
+            An int defining the number of reachable commits
         """
-        # Yes, it makes a difference whether empty paths are given or not in our case
-        # as the empty paths version will ignore merge commits for some reason.
+        # Yes, it makes a difference whether empty paths are given or not in our case as
+        # the empty paths version will ignore merge commits for some reason.
         if paths:
             return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines())
         return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines())
@@ -258,8 +276,11 @@ def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any)
     def name_rev(self) -> str:
         """
         :return:
-            String describing the commits hex sha based on the closest Reference.
-            Mostly useful for UI purposes
+            String describing the commits hex sha based on the closest
+            `~git.refs.reference.Reference`.
+
+        :note:
+            Mostly useful for UI purposes.
         """
         return self.repo.git.name_rev(self)
 
@@ -271,24 +292,27 @@ def iter_items(
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator["Commit"]:
-        """Find all commits matching the given criteria.
+        R"""Find all commits matching the given criteria.
 
-        :param repo: The Repo
+        :param repo:
+            The Repo
 
-        :param rev: Revision specifier, see git-rev-parse for viable options.
+        :param rev:
+            Revision specifier, see git-rev-parse for viable options.
 
         :param paths:
-            An optional path or list of paths, if set only Commits that include the path
-            or paths will be considered.
+            An optional path or list of paths. If set only :class:`Commit`\s that
+            include the path or paths will be considered.
 
         :param kwargs:
             Optional keyword arguments to ``git rev-list`` where:
 
-            * ``max_count`` is the maximum number of commits to fetch
-            * ``skip`` is the number of commits to skip
-            * ``since`` all commits since e.g. '1970-01-01'
+            * ``max_count`` is the maximum number of commits to fetch.
+            * ``skip`` is the number of commits to skip.
+            * ``since`` all commits since e.g. '1970-01-01'.
 
-        :return: Iterator yielding :class:`Commit` items.
+        :return:
+            Iterator yielding :class:`Commit` items.
         """
         if "pretty" in kwargs:
             raise ValueError("--pretty cannot be used as parsing expects single sha's only")
@@ -313,13 +337,17 @@ def iter_items(
         return cls._iter_from_process_or_stream(repo, proc)
 
     def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> Iterator["Commit"]:
-        """Iterate _all_ parents of this commit.
+        R"""Iterate _all_ parents of this commit.
 
         :param paths:
-            Optional path or list of paths limiting the Commits to those that
-            contain at least one of the paths
-        :param kwargs: All arguments allowed by git-rev-list
-        :return: Iterator yielding Commit objects which are parents of self
+            Optional path or list of paths limiting the :class:`Commit`\s to those that
+            contain at least one of the paths.
+
+        :param kwargs:
+            All arguments allowed by ``git rev-list``.
+
+        :return:
+            Iterator yielding :class:`Commit` objects which are parents of ``self``
         """
         # skip ourselves
         skip = kwargs.get("skip", 1)
@@ -334,7 +362,8 @@ def stats(self) -> Stats:
         """Create a git stat from changes between this commit and its first parent
         or from all changes done if this is the very first commit.
 
-        :return: git.Stats
+        :return:
+            :class:`Stats`
         """
         if not self.parents:
             text = self.repo.git.diff_tree(self.hexsha, "--", numstat=True, no_renames=True, root=True)
@@ -364,11 +393,11 @@ def trailers(self) -> Dict[str, 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
-        e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers).
+        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.
+        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::
 
@@ -382,7 +411,6 @@ def trailers_list(self) -> List[Tuple[str, str]]:
             key1: value1.2
             key2 :    value 2 with inner spaces
 
-
         Returned list will look like this::
 
             [
@@ -391,7 +419,6 @@ def trailers_list(self) -> List[Tuple[str, str]]:
                 ("key2", "value 2 with inner spaces"),
             ]
 
-
         :return:
             List containing key-value tuples of whitespace stripped trailer information.
         """
@@ -414,12 +441,12 @@ def trailers_list(self) -> List[Tuple[str, str]]:
     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).
+        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. The key value pairs are stripped of
-        leading and trailing whitespaces before they get saved into a dictionary.
+        This functions calls ``git interpret-trailers --parse`` onto the message to
+        extract the trailer information. The key value pairs are stripped of leading and
+        trailing whitespaces before they get saved into a dictionary.
 
         Valid message with trailer::
 
@@ -433,7 +460,6 @@ def trailers_dict(self) -> Dict[str, List[str]]:
             key1: value1.2
             key2 :    value 2 with inner spaces
 
-
         Returned dictionary will look like this::
 
             {
@@ -443,8 +469,8 @@ def trailers_dict(self) -> Dict[str, List[str]]:
 
 
         :return:
-            Dictionary containing whitespace stripped trailer information.
-            Mapping trailer keys to a list of their corresponding values.
+            Dictionary containing whitespace stripped trailer information, mapping
+            trailer keys to a list of their corresponding values.
         """
         d = defaultdict(list)
         for key, val in self.trailers_list:
@@ -453,13 +479,16 @@ def trailers_dict(self) -> Dict[str, List[str]]:
 
     @classmethod
     def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:
-        """Parse out commit information into a list of Commit objects.
+        """Parse out commit information into a list of :class:`Commit` objects.
 
         We expect one-line per commit, and parse the actual commit information directly
         from our lighting fast object database.
 
-        :param proc: git-rev-list process instance - one sha per line
-        :return: iterator supplying :class:`Commit` objects
+        :param proc:
+            ``git rev-list`` process instance - one sha per line.
+
+        :return:
+            Iterator supplying :class:`Commit` objects
         """
 
         # def is_proc(inp) -> TypeGuard[Popen]:
@@ -491,8 +520,8 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen,
             yield cls(repo, hex_to_bin(hexsha))
         # END for each line in stream
 
-        # TODO: Review this - it seems process handling got a bit out of control
-        # due to many developers trying to fix the open file handles issue.
+        # TODO: Review this - it seems process handling got a bit out of control due to
+        # many developers trying to fix the open file handles issue.
         if hasattr(proc_or_stream, "wait"):
             proc_or_stream = cast(Popen, proc_or_stream)
             finalize_process(proc_or_stream)
@@ -512,33 +541,49 @@ def create_from_tree(
     ) -> "Commit":
         """Commit the given tree, creating a commit object.
 
-        :param repo: Repo object the commit should be part of
-        :param tree: Tree object or hex or bin sha. The tree of the new commit.
-        :param message: Commit message. It may be an empty string if no message is
-            provided. It will be converted to a string, in any case.
+        :param repo:
+            :class:`~git.repo.base.Repo` object the commit should be part of.
+
+        :param tree:
+            :class:`~git.objects.tree.Tree` object or hex or bin sha.
+            The tree of the new commit.
+
+        :param message:
+            Commit message. It may be an empty string if no message is provided. It will
+            be converted to a string, in any case.
+
         :param parent_commits:
-            Optional :class:`Commit` objects to use as parents for the new commit.
-            If empty list, the commit will have no parents at all and become
-            a root commit.
-            If None, the current head commit will be the parent of the
-            new commit object.
+            Optional :class:`Commit` objects to use as parents for the new commit. If
+            empty list, the commit will have no parents at all and become a root commit.
+            If None, the current head commit will be the parent of the new commit
+            object.
+
         :param head:
             If True, the HEAD will be advanced to the new commit automatically.
             Otherwise the HEAD will remain pointing on the previous commit. This could
             lead to undesired results when diffing files.
-        :param author: The name of the author, optional. If unset, the repository
-            configuration is used to obtain this value.
-        :param committer: The name of the committer, optional. If unset, the
-            repository configuration is used to obtain this value.
-        :param author_date: The timestamp for the author field.
-        :param commit_date: The timestamp for the committer field.
 
-        :return: Commit object representing the new commit.
+        :param author:
+            The name of the author, optional.
+            If unset, the repository configuration is used to obtain this value.
+
+        :param committer:
+            The name of the committer, optional.
+            If unset, the repository configuration is used to obtain this value.
+
+        :param author_date:
+            The timestamp for the author field.
+
+        :param commit_date:
+            The timestamp for the committer field.
+
+        :return:
+            :class:`Commit` object representing the new commit.
 
         :note:
-            Additional information about the committer and Author are taken from the
-            environment or from the git configuration, see git-commit-tree for
-            more information.
+            Additional information about the committer and author are taken from the
+            environment or from the git configuration. See git-commit-tree for more
+            information.
         """
         if parent_commits is None:
             try:
@@ -595,8 +640,8 @@ def create_from_tree(
         if not isinstance(conf_encoding, str):
             raise TypeError("conf_encoding could not be coerced to str")
 
-        # If the tree is no object, make sure we create one - otherwise
-        # the created commit object is invalid.
+        # If the tree is no object, make sure we create one - otherwise the created
+        # commit object is invalid.
         if isinstance(tree, str):
             tree = repo.tree(tree)
         # END tree conversion
@@ -620,8 +665,8 @@ def create_from_tree(
         new_commit.binsha = cls._calculate_sha_(repo, new_commit)
 
         if head:
-            # Need late import here, importing git at the very beginning throws
-            # as well...
+            # Need late import here, importing git at the very beginning throws as
+            # well...
             import git.refs
 
             try:
@@ -718,7 +763,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
         # END for each parent line
         self.parents = tuple(self.parents)
 
-        # We don't know actual author encoding before we have parsed it, so keep the lines around.
+        # We don't know actual author encoding before we have parsed it, so keep the
+        # lines around.
         author_line = next_line
         committer_line = readline()
 
@@ -730,7 +776,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
                 next_line = readline()
         # END skip mergetags
 
-        # Now we can have the encoding line, or an empty line followed by the optional message.
+        # Now we can have the encoding line, or an empty line followed by the optional
+        # message.
         self.encoding = self.default_encoding
         self.gpgsig = ""
 
@@ -808,12 +855,13 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
 
     @property
     def co_authors(self) -> List[Actor]:
-        """
-        Search the commit message for any co-authors of this commit.
+        """Search the commit message for any co-authors of this commit.
 
-        Details on co-authors: https://github.blog/2018-01-29-commit-together-with-co-authors/
+        Details on co-authors:
+        https://github.blog/2018-01-29-commit-together-with-co-authors/
 
-        :return: List of co-authors for this commit (as Actor objects).
+        :return:
+            List of co-authors for this commit (as :class:`~git.util.Actor` objects).
         """
         co_authors = []
 
diff --git a/git/objects/fun.py b/git/objects/fun.py
index bf6bc21d6..d349737de 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -40,10 +40,13 @@
 
 
 def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
-    """Write the given list of entries into a stream using its write method.
+    """Write the given list of entries into a stream using its ``write`` method.
 
-    :param entries: **sorted** list of tuples with (binsha, mode, name)
-    :param write: write method which takes a data string
+    :param entries:
+        **Sorted** list of tuples with (binsha, mode, name).
+
+    :param write:
+        A ``write`` method which takes a data string.
     """
     ord_zero = ord("0")
     bit_mask = 7  # 3 bits set.
@@ -59,11 +62,11 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer
             mode_str = mode_str[1:]
         # END save a byte
 
-        # Here it comes: If the name is actually unicode, the replacement below
-        # will not work as the binsha is not part of the ascii unicode encoding -
-        # hence we must convert to an UTF-8 string for it to work properly.
-        # According to my tests, this is exactly what git does, that is it just
-        # takes the input literally, which appears to be UTF-8 on linux.
+        # Here it comes: If the name is actually unicode, the replacement below will not
+        # work as the binsha is not part of the ascii unicode encoding - hence we must
+        # convert to an UTF-8 string for it to work properly. According to my tests,
+        # this is exactly what git does, that is it just takes the input literally,
+        # which appears to be UTF-8 on linux.
         if isinstance(name, str):
             name_bytes = name.encode(defenc)
         else:
@@ -73,10 +76,15 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer
 
 
 def tree_entries_from_data(data: bytes) -> List[EntryTup]:
-    """Reads the binary representation of a tree and returns tuples of Tree items
+    """Read the binary representation of a tree and returns tuples of
+    :class:`~git.objects.tree.Tree` items.
+
+    :param data:
+        Data block with tree data (as bytes).
 
-    :param data: data block with tree data (as bytes)
-    :return: list(tuple(binsha, mode, tree_relative_path), ...)"""
+    :return:
+        list(tuple(binsha, mode, tree_relative_path), ...)
+    """
     ord_zero = ord("0")
     space_ord = ord(" ")
     len_data = len(data)
@@ -89,8 +97,8 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
         # Some git versions truncate the leading 0, some don't.
         # The type will be extracted from the mode later.
         while data[i] != space_ord:
-            # Move existing mode integer up one level being 3 bits
-            # and add the actual ordinal value of the character.
+            # Move existing mode integer up one level being 3 bits and add the actual
+            # ordinal value of the character.
             mode = (mode << 3) + (data[i] - ord_zero)
             i += 1
         # END while reading mode
@@ -122,8 +130,8 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
 def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone:
     """Return data entry matching the given name and tree mode or None.
 
-    Before the item is returned, the respective data item is set None in the
-    tree_data list to mark it done.
+    Before the item is returned, the respective data item is set None in the `tree_data`
+    list to mark it done.
     """
 
     try:
@@ -165,6 +173,7 @@ def traverse_trees_recursive(
 ) -> List[Tuple[EntryTupOrNone, ...]]:
     """
     :return: list of list with entries according to the given binary tree-shas.
+
         The result is encoded in a list
         of n tuple|None per blob/commit, (n == len(tree_shas)), where:
 
@@ -172,16 +181,19 @@ def traverse_trees_recursive(
         * [1] == mode as int
         * [2] == path relative to working tree root
 
-        The entry tuple is None if the respective blob/commit did not
-        exist in the given tree.
+        The entry tuple is None if the respective blob/commit did not exist in the given
+        tree.
 
-    :param tree_shas: iterable of shas pointing to trees. All trees must
-        be on the same level. A tree-sha may be None in which case None.
+    :param tree_shas:
+        Iterable of shas pointing to trees. All trees must be on the same level. A
+        tree-sha may be None in which case None.
 
-    :param path_prefix: a prefix to be added to the returned paths on this level,
-        set it '' for the first iteration.
+    :param path_prefix:
+        A prefix to be added to the returned paths on this level.
+        Set it ``""`` for the first iteration.
 
-    :note: The ordering of the returned items will be partially lost.
+    :note:
+        The ordering of the returned items will be partially lost.
     """
     trees_data: List[List[EntryTupOrNone]] = []
 
@@ -198,8 +210,8 @@ def traverse_trees_recursive(
 
     out: List[Tuple[EntryTupOrNone, ...]] = []
 
-    # Find all matching entries and recursively process them together if the match
-    # is a tree. If the match is a non-tree item, put it into the result.
+    # Find all matching entries and recursively process them together if the match 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):
@@ -213,8 +225,8 @@ def traverse_trees_recursive(
             is_dir = S_ISDIR(mode)  # Type mode bits
 
             # Find this item in all other tree data items.
-            # Wrap around, but stop one before our current index, hence
-            # ti+nt, not ti+1+nt.
+            # Wrap around, but stop one before our current index, hence ti+nt, not
+            # ti+1+nt.
             for tio in range(ti + 1, ti + nt):
                 tio = tio % nt
                 entries[tio] = _find_by_name(trees_data[tio], name, is_dir, ii)
@@ -245,7 +257,7 @@ def traverse_trees_recursive(
 
 def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]:
     """
-    :return: list of entries of the tree pointed to by the binary tree_sha.
+    :return: list of entries of the tree pointed to by the binary `tree_sha`.
 
         An entry has the following format:
 
@@ -253,7 +265,8 @@ def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix:
         * [1] mode as int
         * [2] path relative to the repository
 
-    :param path_prefix: Prefix to prepend to the front of all returned paths.
+    :param path_prefix:
+        Prefix to prepend to the front of all returned paths.
     """
     entries = []
     data = tree_entries_from_data(odb.stream(tree_sha).read())
diff --git a/git/objects/tag.py b/git/objects/tag.py
index f874f5bdf..211c84ac7 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -25,7 +25,8 @@
 
 
 class TagObject(base.Object):
-    """Non-lightweight tag carrying additional information about an object we are pointing to."""
+    """Non-lightweight tag carrying additional information about an object we are
+    pointing to."""
 
     type: Literal["tag"] = "tag"
 
@@ -51,17 +52,28 @@ def __init__(
     ) -> None:  # @ReservedAssignment
         """Initialize a tag object with additional data.
 
-        :param repo: Repository this object is located in
-        :param binsha: 20 byte SHA1
-        :param object: Object instance of object we are pointing to
-        :param tag: Name of this tag
-        :param tagger: Actor identifying the tagger
+        :param repo:
+            Repository this object is located in.
+
+        :param binsha:
+            20 byte SHA1.
+
+        :param object:
+            :class:`~git.objects.base.Object` instance of object we are pointing to.
+
+        :param tag:
+            Name of this tag.
+
+        :param tagger:
+            :class:`~git.util.Actor` identifying the tagger.
+
         :param tagged_date: int_seconds_since_epoch
             The :class:`DateTime` of the tag creation.
             Use :func:`time.gmtime` to convert it into a different format.
+
         :param tagger_tz_offset: int_seconds_west_of_utc
-            The timezone that the tagged_date is in, in a format similar
-            to :attr:`time.altzone`.
+            The timezone that the `tagged_date` is in, in a format similar to
+            :attr:`time.altzone`.
         """
         super().__init__(repo, binsha)
         if object is not None:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 450973280..da682b8cd 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -54,10 +54,12 @@
 
 
 class TreeModifier:
-    """A utility class providing methods to alter the underlying cache in a list-like fashion.
+    """A utility class providing methods to alter the underlying cache in a list-like
+    fashion.
 
-    Once all adjustments are complete, the _cache, which really is a reference to
-    the cache of a tree, will be sorted. This ensures it will be in a serializable state.
+    Once all adjustments are complete, the :attr:`_cache`, which really is a reference
+    to the cache of a tree, will be sorted. This ensures it will be in a serializable
+    state.
     """
 
     __slots__ = ("_cache",)
@@ -78,10 +80,11 @@ def _index_by_name(self, name: str) -> int:
     def set_done(self) -> "TreeModifier":
         """Call this method once you are done modifying the tree information.
 
-        This may be called several times, but be aware that each call will cause
-        a sort operation.
+        This may be called several times, but be aware that each call will cause a sort
+        operation.
 
-        :return self:
+        :return:
+            self
         """
         self._cache.sort(key=lambda x: (x[2] + "/") if x[1] == Tree.tree_id << 12 else x[2])
         return self
@@ -93,17 +96,21 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
         """Add the given item to the tree.
 
         If an item with the given name already exists, nothing will be done, but a
-        ValueError will be raised if the sha and mode of the existing item do not match
-        the one you add, unless force is True
+        :class:`ValueError` will be raised if the sha and mode of the existing item do
+        not match the one you add, unless `force` is True.
 
-        :param sha: The 20 or 40 byte sha of the item to add
+        :param sha:
+            The 20 or 40 byte sha of the item to add.
 
-        :param mode: int representing the stat compatible mode of the item
+        :param mode:
+            int representing the stat compatible mode of the item.
 
-        :param force: If True, an item with your name and information will overwrite any
-            existing item with the same name, no matter which information it has
+        :param force:
+            If True, an item with your name and information will overwrite any existing
+            item with the same name, no matter which information it has.
 
-        :return: self
+        :return:
+            self
         """
         if "/" in name:
             raise ValueError("Name must not contain '/' characters")
@@ -136,7 +143,8 @@ def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None:
 
         For more information on the parameters, see :meth:`add`.
 
-        :param binsha: 20 byte binary sha
+        :param binsha:
+            20 byte binary sha.
         """
         assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str)
         tree_cache = (binsha, mode, name)
@@ -153,15 +161,14 @@ def __delitem__(self, name: str) -> None:
 
 
 class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
-    """Tree objects represent an ordered list of Blobs and other Trees.
+    R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
+    other :class:`~git.objects.tree.Tree`\s.
 
-    ``Tree as a list``::
+    Tree as a list::
 
-        Access a specific blob using the
-        tree['filename'] notation.
+        Access a specific blob using the ``tree['filename']`` notation.
 
-        You may as well access by index
-        blob = tree[0]
+        You may likewise access by index, like ``blob = tree[0]``.
     """
 
     type: Literal["tree"] = "tree"
@@ -223,8 +230,12 @@ def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[
     def join(self, file: str) -> IndexObjUnion:
         """Find the named object in this tree's contents.
 
-        :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule``
-        :raise KeyError: if given file or tree does not exist in tree
+        :return:
+            :class:`~git.objects.blob.Blob`, :class:`~git.objects.tree.Tree`,
+            or :class:`~git.objects.submodule.base.Submodule`
+
+        :raise KeyError:
+            If the given file or tree does not exist in tree.
         """
         msg = "Blob or Tree named %r not found"
         if "/" in file:
@@ -275,9 +286,13 @@ def blobs(self) -> List[Blob]:
     @property
     def cache(self) -> TreeModifier:
         """
-        :return: An object allowing to modify the internal cache. This can be used
-            to change the tree's contents. When done, make sure you call ``set_done``
-            on the tree modifier, or serialization behaviour will be incorrect.
+        :return:
+            An object allowing modification of the internal cache. This can be used to
+            change the tree's contents. When done, make sure you call
+            :meth:`~TreeModifier.set_done` on the tree modifier, or serialization
+            behaviour will be incorrect.
+
+        :note:
             See :class:`TreeModifier` for more information on how to alter the cache.
         """
         return TreeModifier(self._cache)
@@ -292,12 +307,13 @@ def traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]:
-        """For documentation, see util.Traversable._traverse().
+        """For documentation, see
+        `Traversable._traverse() <git.objects.util.Traversable._traverse>`.
 
-        Trees are set to ``visit_once = False`` to gain more performance in the traversal.
+        Trees are set to ``visit_once = False`` to gain more performance in the
+        traversal.
         """
 
-        # """
         # # To typecheck instead of using cast.
         # import itertools
         # def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]:
@@ -306,7 +322,8 @@ def traverse(
         # ret = super().traverse(predicate, prune, depth, branch_first, visit_once, ignore_self)
         # ret_tup = itertools.tee(ret, 2)
         # assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}"
-        # return ret_tup[0]"""
+        # return ret_tup[0]
+
         return cast(
             Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
             super()._traverse(
@@ -321,8 +338,10 @@ def traverse(
 
     def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion]:
         """
-        :return: IterableList with the results of the traversal as produced by
-            traverse()
+        :return:
+            :class:`~git.util.IterableList`IterableList with the results of the
+            traversal as produced by :meth:`traverse`
+
             Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
         """
         return super()._list_traverse(*args, **kwargs)
@@ -375,9 +394,10 @@ def __reversed__(self) -> Iterator[IndexObjUnion]:
     def _serialize(self, stream: "BytesIO") -> "Tree":
         """Serialize this tree into the stream. Assumes sorted tree data.
 
-        .. note:: We will assume our tree data to be in a sorted state. If this is not
-            the case, serialization will not generate a correct tree representation as
-            these are assumed to be sorted by algorithms.
+        :note:
+            We will assume our tree data to be in a sorted state. If this is not the
+            case, serialization will not generate a correct tree representation as these
+            are assumed to be sorted by algorithms.
         """
         tree_to_stream(self._cache, stream.write)
         return self
diff --git a/git/objects/util.py b/git/objects/util.py
index b25a3f5ff..7ad20d66e 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -88,14 +88,16 @@ class TraverseNT(NamedTuple):
 
 
 def mode_str_to_int(modestr: Union[bytes, str]) -> int:
-    """
+    """Convert mode bits from an octal mode string to an integer mode for git.
+
     :param modestr:
-        String like 755 or 644 or 100644 - only the last 6 chars will be used.
+        String like ``755`` or ``644`` or ``100644`` - only the last 6 chars will be
+        used.
 
     :return:
-        String identifying a mode compatible to the mode methods ids of the
-        stat module regarding the rwx permissions for user, group and other,
-        special flags and file system flags, such as whether it is a symlink.
+        String identifying a mode compatible to the mode methods ids of the stat module
+        regarding the rwx permissions for user, group and other, special flags and file
+        system flags, such as whether it is a symlink.
     """
     mode = 0
     for iteration, char in enumerate(reversed(modestr[-6:])):
@@ -108,13 +110,17 @@ def mode_str_to_int(modestr: Union[bytes, str]) -> int:
 def get_object_type_by_name(
     object_type_name: bytes,
 ) -> Union[Type["Commit"], Type["TagObject"], Type["Tree"], Type["Blob"]]:
-    """
-    :return: A type suitable to handle the given object type name.
-        Use the type to create new instances.
+    """Retrieve the Python class GitPython uses to represent a kind of Git object.
+
+    :return:
+        A type suitable to handle the given as `object_type_name`.
+        This type can be called create new instances.
 
-    :param object_type_name: Member of TYPES
+    :param object_type_name:
+        Member of :attr:`Object.TYPES <git.objects.base.Object.TYPES>`.
 
-    :raise ValueError: If object_type_name is unknown
+    :raise ValueError:
+        If `object_type_name` is unknown.
     """
     if object_type_name == b"commit":
         from . import commit
@@ -137,10 +143,11 @@ def get_object_type_by_name(
 
 
 def utctz_to_altz(utctz: str) -> int:
-    """Convert a git timezone offset into a timezone offset west of
-    UTC in seconds (compatible with :attr:`time.altzone`).
+    """Convert a git timezone offset into a timezone offset west of UTC in seconds
+    (compatible with :attr:`time.altzone`).
 
-    :param utctz: git utc timezone string, e.g. +0200
+    :param utctz:
+        git utc timezone string, e.g. +0200
     """
     int_utctz = int(utctz)
     seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60
@@ -148,9 +155,11 @@ def utctz_to_altz(utctz: str) -> int:
 
 
 def altz_to_utctz_str(altz: float) -> str:
-    """Convert a timezone offset west of UTC in seconds into a Git timezone offset string.
+    """Convert a timezone offset west of UTC in seconds into a Git timezone offset
+    string.
 
-    :param altz: Timezone offset in seconds west of UTC
+    :param altz:
+        Timezone offset in seconds west of UTC.
     """
     hours = abs(altz) // 3600
     minutes = (abs(altz) % 3600) // 60
@@ -160,9 +169,11 @@ def altz_to_utctz_str(altz: float) -> str:
 
 def verify_utctz(offset: str) -> str:
     """
-    :raise ValueError: If offset is incorrect
+    :raise ValueError:
+        If `offset` is incorrect.
 
-    :return: offset
+    :return:
+        `offset`
     """
     fmt_exc = ValueError("Invalid timezone offset format: %s" % offset)
     if len(offset) != 5:
@@ -197,7 +208,8 @@ def dst(self, dt: Union[datetime, None]) -> timedelta:
 
 
 def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
-    """Convert a timestamp + tz_offset into an aware datetime instance."""
+    """Convert a `timestamp` + `tz_offset` into an aware :class:`~datetime.datetime`
+    instance."""
     utc_dt = datetime.fromtimestamp(timestamp, utc)
     try:
         local_dt = utc_dt.astimezone(tzoffset(tz_offset))
@@ -207,20 +219,22 @@ def from_timestamp(timestamp: float, tz_offset: float) -> datetime:
 
 
 def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
-    """
-    Parse the given date as one of the following:
+    """Parse the given date as one of the following:
 
         * Aware datetime instance
         * Git internal format: timestamp offset
-        * RFC 2822: Thu, 07 Apr 2005 22:13:13 +0200.
-        * ISO 8601 2005-04-07T22:13:13
+        * RFC 2822: ``Thu, 07 Apr 2005 22:13:13 +0200``
+        * ISO 8601: ``2005-04-07T22:13:13``
             The T can be a space as well.
 
-    :return: Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch
+    :return:
+        Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch
 
-    :raise ValueError: If the format could not be understood
+    :raise ValueError:
+        If the format could not be understood.
 
-    :note: Date can also be YYYY.MM.DD, MM/DD/YYYY and DD.MM.YYYY.
+    :note:
+        Date can also be ``YYYY.MM.DD``, ``MM/DD/YYYY`` and ``DD.MM.YYYY``.
     """
     if isinstance(string_date, datetime):
         if string_date.tzinfo:
@@ -314,7 +328,8 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
 
         author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
 
-    :return: [Actor, int_seconds_since_epoch, int_timezone_offset]
+    :return:
+        [Actor, int_seconds_since_epoch, int_timezone_offset]
     """
     actor, epoch, offset = "", "0", "0"
     m = _re_actor_epoch.search(line)
@@ -336,8 +351,8 @@ class ProcessStreamAdapter:
     """Class wiring all calls to the contained Process instance.
 
     Use this type to hide the underlying process to provide access only to a specified
-    stream. The process is usually wrapped into an AutoInterrupt class to kill
-    it if the instance goes out of scope.
+    stream. The process is usually wrapped into an :class:`~git.cmd.Git.AutoInterrupt`
+    class to kill it if the instance goes out of scope.
     """
 
     __slots__ = ("_proc", "_stream")
@@ -352,14 +367,18 @@ def __getattr__(self, attr: str) -> Any:
 
 @runtime_checkable
 class Traversable(Protocol):
-    """Simple interface to perform depth-first or breadth-first traversals
-    in one direction.
+    """Simple interface to perform depth-first or breadth-first traversals in one
+    direction.
 
     Subclasses only need to implement one function.
 
-    Instances of the Subclass must be hashable.
+    Instances of the subclass must be hashable.
+
+    Defined subclasses:
 
-    Defined subclasses = [Commit, Tree, SubModule]
+    * :class:`Commit <git.objects.Commit>`
+    * :class:`Tree <git.objects.tree.Tree>`
+    * :class:`Submodule <git.objects.submodule.base.Submodule>`
     """
 
     __slots__ = ()
@@ -368,7 +387,7 @@ class Traversable(Protocol):
     @abstractmethod
     def _get_intermediate_items(cls, item: Any) -> Sequence["Traversable"]:
         """
-        Returns:
+        :return:
             Tuple of items connected to the given item.
             Must be implemented in subclass.
 
@@ -399,20 +418,23 @@ def _list_traverse(
     ) -> IterableList[Union["Commit", "Submodule", "Tree", "Blob"]]:
         """Traverse self and collect all items found.
 
-        :return: IterableList with the results of the traversal as produced by
-            :meth:`traverse`::
+        :return: :class:`~git.util.IterableList` with the results of the traversal as
+            produced by :meth:`traverse`::
 
-                Commit -> IterableList['Commit']
-                Submodule ->  IterableList['Submodule']
-                Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
+                Commit -> IterableList["Commit"]
+                Submodule ->  IterableList["Submodule"]
+                Tree -> IterableList[Union["Submodule", "Tree", "Blob"]]
         """
         # Commit and Submodule have id.__attribute__ as IterableObj.
         # Tree has id.__attribute__ inherited from IndexObject.
         if isinstance(self, Has_id_attribute):
             id = self._id_attribute_
         else:
-            id = ""  # Shouldn't reach here, unless Traversable subclass created with no _id_attribute_.
-            # Could add _id_attribute_ to Traversable, or make all Traversable also Iterable?
+            # Shouldn't reach here, unless Traversable subclass created with no
+            # _id_attribute_.
+            id = ""
+            # Could add _id_attribute_ to Traversable, or make all Traversable also
+            # Iterable?
 
         if not as_edge:
             out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
@@ -453,46 +475,49 @@ def _traverse(
     ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
         """Iterator yielding items found when traversing self.
 
-        :param predicate: f(i,d) returns False if item i at depth d should not be
-            included in the result.
+        :param predicate:
+            A function ``f(i,d)`` that returns False if item i at depth ``d`` should not
+            be included in the result.
 
         :param prune:
-            f(i,d) return True if the search should stop at item i at depth d. Item i
-            will not be returned.
+            A function ``f(i,d)`` that returns True if the search should stop at item
+            ``i`` at depth ``d``. Item ``i`` will not be returned.
 
         :param depth:
             Defines at which level the iteration should not go deeper if -1, there is no
             limit if 0, you would effectively only get self, the root of the iteration
-            i.e. if 1, you would only get the first level of predecessors/successors
+            i.e. if 1, you would only get the first level of predecessors/successors.
 
         :param branch_first:
-            if True, items will be returned branch first, otherwise depth first
+            If True, items will be returned branch first, otherwise depth first.
 
         :param visit_once:
-            if True, items will only be returned once, although they might be
+            If True, items will only be returned once, although they might be
             encountered several times. Loops are prevented that way.
 
         :param ignore_self:
-            if True, self will be ignored and automatically pruned from the result.
-            Otherwise it will be the first item to be returned. If as_edge is True, the
+            If True, self will be ignored and automatically pruned from the result.
+            Otherwise it will be the first item to be returned. If `as_edge` is True, the
             source of the first edge is None
 
         :param as_edge:
-            if True, return a pair of items, first being the source, second the
+            If True, return a pair of items, first being the source, second the
             destination, i.e. tuple(src, dest) with the edge spanning from source to
-            destination
+            destination.
 
-        :return: Iterator yielding items found when traversing self::
+        :return:
+            Iterator yielding items found when traversing self::
 
-                Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]]
-                Submodule -> Iterator[Submodule, Tuple[Submodule, Submodule]]
-                Tree -> Iterator[Union[Blob, Tree, Submodule,
-                                        Tuple[Union[Submodule, Tree], Union[Blob, Tree, Submodule]]]
+                Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] Submodule ->
+                Iterator[Submodule, Tuple[Submodule, Submodule]] Tree ->
+                Iterator[Union[Blob, Tree, Submodule,
+                                        Tuple[Union[Submodule, Tree], Union[Blob, Tree,
+                                        Submodule]]]
 
-                ignore_self=True is_edge=True -> Iterator[item]
-                ignore_self=True is_edge=False --> Iterator[item]
-                ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]]
-                ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]
+                ignore_self=True is_edge=True -> Iterator[item] ignore_self=True
+                is_edge=False --> Iterator[item] ignore_self=False is_edge=True ->
+                Iterator[item] | Iterator[Tuple[src, item]] ignore_self=False
+                is_edge=False -> Iterator[Tuple[src, item]]
         """
 
         visited = set()
@@ -526,7 +551,9 @@ def addToStack(
                 visited.add(item)
 
             rval: Union[TraversedTup, "Traversable", "Blob"]
-            if as_edge:  # If as_edge return (src, item) unless rrc is None (e.g. for first item).
+            if as_edge:
+                # If as_edge return (src, item) unless rrc is None
+                # (e.g. for first item).
                 rval = (src, item)
             else:
                 rval = item
@@ -549,7 +576,8 @@ def addToStack(
 
 @runtime_checkable
 class Serializable(Protocol):
-    """Defines methods to serialize and deserialize objects from and into a data stream."""
+    """Defines methods to serialize and deserialize objects from and into a data
+    stream."""
 
     __slots__ = ()
 
@@ -557,11 +585,14 @@ class Serializable(Protocol):
     def _serialize(self, stream: "BytesIO") -> "Serializable":
         """Serialize the data of this object into the given data stream.
 
-        :note: A serialized object would :meth:`_deserialize` into the same object.
+        :note:
+            A serialized object would :meth:`_deserialize` into the same object.
 
-        :param stream: a file-like object
+        :param stream:
+            A file-like object.
 
-        :return: self
+        :return:
+            self
         """
         raise NotImplementedError("To be implemented in subclass")
 
@@ -569,9 +600,11 @@ def _serialize(self, stream: "BytesIO") -> "Serializable":
     def _deserialize(self, stream: "BytesIO") -> "Serializable":
         """Deserialize all information regarding this object from the stream.
 
-        :param stream: a file-like object
+        :param stream:
+            A file-like object.
 
-        :return: self
+        :return:
+            self
         """
         raise NotImplementedError("To be implemented in subclass")
 

From 37011bf873b93ad38ab184b3885df66072692a12 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 13:25:05 -0500
Subject: [PATCH 490/994] Fix backslash formatting in git.util docstrings

As in 5219489.
---
 git/util.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/util.py b/git/util.py
index 30b78f7b2..4126c6c73 100644
--- a/git/util.py
+++ b/git/util.py
@@ -260,7 +260,7 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
 
 def join_path(a: PathLike, *p: PathLike) -> PathLike:
     R"""Join path tokens together similar to osp.join, but always use
-    '/' instead of possibly '\' on Windows."""
+    ``/`` instead of possibly ``\`` on Windows."""
     path = str(a)
     for b in p:
         b = str(b)
@@ -300,7 +300,7 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
     R"""Like join_path, but makes sure an OS native path is returned.
 
     This is only needed to play it safe on Windows and to ensure nice paths that only
-    use '\'.
+    use ``\``.
     """
     return to_native_path(join_path(a, *p))
 

From d9fb2f4c8f59f6074493d780a4ba62e89fa9f5d6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 13:36:49 -0500
Subject: [PATCH 491/994] Further git.util docstring revisions

That I had missed in 1cd73ba.
---
 git/util.py | 52 +++++++++++++++++++++++++++-------------------------
 1 file changed, 27 insertions(+), 25 deletions(-)

diff --git a/git/util.py b/git/util.py
index 4126c6c73..72861ef84 100644
--- a/git/util.py
+++ b/git/util.py
@@ -201,9 +201,9 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]:
 def rmtree(path: PathLike) -> None:
     """Remove the given directory tree recursively.
 
-    :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files
-        that couldn't be deleted are read-only. Windows will not remove them in that
-        case.
+    :note:
+        We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that
+        couldn't be deleted are read-only. Windows will not remove them in that case.
     """
 
     def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
@@ -241,8 +241,8 @@ def rmfile(path: PathLike) -> None:
 
 
 def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * 1024) -> int:
-    """Copy all data from the source stream into the destination stream in chunks
-    of size chunk_size.
+    """Copy all data from the `source` stream into the `destination` stream in chunks
+    of size `chunk_size`.
 
     :return:
         Number of bytes written
@@ -259,8 +259,8 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
 
 
 def join_path(a: PathLike, *p: PathLike) -> PathLike:
-    R"""Join path tokens together similar to osp.join, but always use
-    ``/`` instead of possibly ``\`` on Windows."""
+    R"""Join path tokens together similar to osp.join, but always use ``/`` instead of
+    possibly ``\`` on Windows."""
     path = str(a)
     for b in p:
         b = str(b)
@@ -297,7 +297,7 @@ def to_native_path_linux(path: PathLike) -> str:
 
 
 def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
-    R"""Like join_path, but makes sure an OS native path is returned.
+    R"""Like :func:`join_path`, but makes sure an OS native path is returned.
 
     This is only needed to play it safe on Windows and to ensure nice paths that only
     use ``\``.
@@ -308,10 +308,12 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
 def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
     """Make sure that the directory pointed to by path exists.
 
-    :param is_file: If True, ``path`` is assumed to be a file and handled correctly.
+    :param is_file:
+        If True, `path` is assumed to be a file and handled correctly.
         Otherwise it must be a directory.
 
-    :return: True if the directory was created, False if it already existed.
+    :return:
+        True if the directory was created, False if it already existed.
     """
     if is_file:
         path = osp.dirname(path)
@@ -339,7 +341,8 @@ def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
     :func:`is_cygwin_git`. When a search following all shell rules is needed,
     :func:`shutil.which` can be used instead.
 
-    :note: Neither this function nor :func:`shutil.which` will predict the effect of an
+    :note:
+        Neither this function nor :func:`shutil.which` will predict the effect of an
         executable search on a native Windows system due to a :class:`subprocess.Popen`
         call without ``shell=True``, because shell and non-shell executable search on
         Windows differ considerably.
@@ -550,8 +553,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
 class RemoteProgress:
     """Handler providing an interface to parse progress information emitted by
     ``git push`` and ``git fetch`` and to dispatch callbacks allowing subclasses to
-    react to the progress.
-    """
+    react to the progress."""
 
     _num_op_codes: int = 9
     (
@@ -761,8 +763,8 @@ def update(self, *args: Any, **kwargs: Any) -> None:
 
 class Actor:
     """Actors hold information about a person acting on the repository. They
-    can be committers and authors or anything with a name and an email as
-    mentioned in the git log entries."""
+    can be committers and authors or anything with a name and an email as mentioned in
+    the git log entries."""
 
     # PRECOMPILED REGEX
     name_only_regex = re.compile(r"<(.*)>")
@@ -802,7 +804,7 @@ def __repr__(self) -> str:
 
     @classmethod
     def _from_string(cls, string: str) -> "Actor":
-        """Create an Actor from a string.
+        """Create an :class:`Actor` from a string.
 
         :param string:
             The string, which is expected to be in regular git format::
@@ -868,10 +870,11 @@ def default_name() -> str:
     @classmethod
     def committer(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor":
         """
-        :return: Actor instance corresponding to the configured committer. It behaves
-            similar to the git implementation, such that the environment will override
-            configuration values of `config_reader`. If no value is set at all, it will
-            be generated.
+        :return:
+            :class:`Actor` instance corresponding to the configured committer. It
+            behaves similar to the git implementation, such that the environment will
+            override configuration values of `config_reader`. If no value is set at all,
+            it will be generated.
 
         :param config_reader:
             ConfigReader to use to retrieve the values from in case they are not set in
@@ -887,8 +890,7 @@ def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint
 
 
 class Stats:
-    """
-    Represents stat information as presented by git at the end of a merge. It is
+    """Represents stat information as presented by git at the end of a merge. It is
     created from the output of a diff operation.
 
     Example::
@@ -949,9 +951,9 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
 
 
 class IndexFileSHA1Writer:
-    """Wrapper around a file-like object that remembers the SHA1 of
-    the data written to it. It will write a sha when the stream is closed
-    or if asked for explicitly using :meth:`write_sha`.
+    """Wrapper around a file-like object that remembers the SHA1 of the data written to
+    it. It will write a sha when the stream is closed or if asked for explicitly using
+    :meth:`write_sha`.
 
     Only useful to the index file.
 

From d1d18c230b8853712cc44e0609b54cf55f09cafa Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 14:34:40 -0500
Subject: [PATCH 492/994] Revise docstrings within git.refs

---
 git/refs/head.py      |  72 +++++++-------
 git/refs/log.py       | 110 ++++++++++++++--------
 git/refs/reference.py |  38 +++++---
 git/refs/remote.py    |  16 ++--
 git/refs/symbolic.py  | 213 +++++++++++++++++++++++++-----------------
 git/refs/tag.py       |  20 ++--
 6 files changed, 283 insertions(+), 186 deletions(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index ebc71eb96..a051748ba 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -52,8 +52,9 @@ def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME):
 
     def orig_head(self) -> SymbolicReference:
         """
-        :return: SymbolicReference pointing at the ORIG_HEAD, which is maintained
-            to contain the previous value of HEAD.
+        :return:
+            :class:`~git.refs.symbolic.SymbolicReference` pointing at the ORIG_HEAD,
+            which is maintained to contain the previous value of HEAD.
         """
         return SymbolicReference(self.repo, self._ORIG_HEAD_NAME)
 
@@ -66,16 +67,16 @@ def reset(
         **kwargs: Any,
     ) -> "HEAD":
         """Reset our HEAD to the given commit optionally synchronizing
-        the index and working tree. The reference we refer to will be set to
-        commit as well.
+        the index and working tree. The reference we refer to will be set to commit as
+        well.
 
         :param commit:
-            Commit object, Reference Object or string identifying a revision we
-            should reset HEAD to.
+            :class:`~git.objects.commit.Commit`, :class:`~git.refs.reference.Reference`,
+            or string identifying a revision we should reset HEAD to.
 
         :param index:
-            If True, the index will be set to match the given commit. Otherwise
-            it will not be touched.
+            If True, the index will be set to match the given commit.
+            Otherwise it will not be touched.
 
         :param working_tree:
             If True, the working tree will be forcefully adjusted to match the given
@@ -87,7 +88,7 @@ def reset(
             that are to be reset. This allows to partially reset individual files.
 
         :param kwargs:
-            Additional arguments passed to git-reset.
+            Additional arguments passed to ``git reset``.
 
         :return: self
         """
@@ -123,8 +124,8 @@ def reset(
 
 
 class Head(Reference):
-    """A Head is a named reference to a Commit. Every Head instance contains a name
-    and a Commit object.
+    """A Head is a named reference to a :class:`~git.objects.commit.Commit`. Every Head
+    instance contains a name and a :class:`~git.objects.commit.Commit` object.
 
     Examples::
 
@@ -150,9 +151,8 @@ def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, *
         """Delete the given heads.
 
         :param force:
-            If True, the heads will be deleted even if they are not yet merged into
-            the main development stream.
-            Default False
+            If True, the heads will be deleted even if they are not yet merged into the
+            main development stream. Default False.
         """
         flag = "-d"
         if force:
@@ -163,9 +163,11 @@ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None])
         """Configure this branch to track the given remote reference. This will
         alter this branch's configuration accordingly.
 
-        :param remote_reference: The remote reference to track or None to untrack
-            any references.
-        :return: self
+        :param remote_reference:
+            The remote reference to track or None to untrack any references.
+
+        :return:
+            self
         """
         from .remote import RemoteReference
 
@@ -190,8 +192,10 @@ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None])
 
     def tracking_branch(self) -> Union["RemoteReference", None]:
         """
-        :return: The remote_reference we are tracking, or None if we are
-            not a tracking branch."""
+        :return:
+            The remote reference we are tracking, or None if we are not a tracking
+            branch.
+        """
         from .remote import RemoteReference
 
         reader = self.config_reader()
@@ -211,16 +215,18 @@ def rename(self, new_path: PathLike, force: bool = False) -> "Head":
         """Rename self to a new path.
 
         :param new_path:
-            Either a simple name or a path, i.e. new_name or features/new_name.
-            The prefix refs/heads is implied.
+            Either a simple name or a path, e.g. ``new_name`` or ``features/new_name``.
+            The prefix ``refs/heads`` is implied.
 
         :param force:
             If True, the rename will succeed even if a head with the target name
             already exists.
 
-        :return: self
+        :return:
+            self
 
-        :note: Respects the ref log as git commands are used.
+        :note:
+            Respects the ref log as git commands are used.
         """
         flag = "-m"
         if force:
@@ -247,15 +253,15 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
             ``b="new_branch"`` to create a new branch at the given spot.
 
         :return:
-            The active branch after the checkout operation, usually self unless
-            a new branch has been created.
+            The active branch after the checkout operation, usually self unless a new
+            branch has been created.
             If there is no active branch, as the HEAD is now detached, the HEAD
             reference will be returned instead.
 
         :note:
-            By default it is only allowed to checkout heads - everything else
-            will leave the HEAD detached which is allowed and possible, but remains
-            a special state that some tools might not be able to handle.
+            By default it is only allowed to checkout heads - everything else will leave
+            the HEAD detached which is allowed and possible, but remains a special state
+            that some tools might not be able to handle.
         """
         kwargs["f"] = force
         if kwargs["f"] is False:
@@ -279,15 +285,17 @@ def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]:
 
     def config_reader(self) -> SectionConstraint[GitConfigParser]:
         """
-        :return: A configuration parser instance constrained to only read
-            this instance's values.
+        :return:
+            A configuration parser instance constrained to only read this instance's
+            values.
         """
         return self._config_parser(read_only=True)
 
     def config_writer(self) -> SectionConstraint[GitConfigParser]:
         """
-        :return: A configuration writer instance with read-and write access
-            to options of this head.
+        :return:
+            A configuration writer instance with read-and write access to options of
+            this head.
         """
         return self._config_parser(read_only=False)
 
diff --git a/git/refs/log.py b/git/refs/log.py
index e45798d8a..260f2fff5 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -44,6 +44,7 @@ class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
     """Named tuple allowing easy access to the revlog data fields."""
 
     _re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
+
     __slots__ = ()
 
     def __repr__(self) -> str:
@@ -81,7 +82,7 @@ def actor(self) -> Actor:
 
     @property
     def time(self) -> Tuple[int, int]:
-        """time as tuple:
+        """Time as tuple:
 
         * [0] = ``int(time)``
         * [1] = ``int(timezone_offset)`` in :attr:`time.altzone` format
@@ -113,9 +114,11 @@ def new(
     def from_line(cls, line: bytes) -> "RefLogEntry":
         """:return: New RefLogEntry instance from the given revlog line.
 
-        :param line: Line bytes without trailing newline
+        :param line:
+            Line bytes without trailing newline
 
-        :raise ValueError: If `line` could not be parsed
+        :raise ValueError:
+            If `line` could not be parsed.
         """
         line_str = line.decode(defenc)
         fields = line_str.split("\t", 1)
@@ -147,9 +150,9 @@ def from_line(cls, line: bytes) -> "RefLogEntry":
 
 
 class RefLog(List[RefLogEntry], Serializable):
-    """A reflog contains RefLogEntrys, each of which defines a certain state
-    of the head in question. Custom query methods allow to retrieve log entries
-    by date or by other criteria.
+    R"""A reflog contains :class:`RefLogEntry`\s, each of which defines a certain state
+    of the head in question. Custom query methods allow to retrieve log entries by date
+    or by other criteria.
 
     Reflog entries are ordered. The first added entry is first in the list. The last
     entry, i.e. the last change of the head or reference, is last in the list.
@@ -163,8 +166,8 @@ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog":
 
     def __init__(self, filepath: Union[PathLike, None] = None):
         """Initialize this instance with an optional filepath, from which we will
-        initialize our data. The path is also used to write changes back using
-        the write() method."""
+        initialize our data. The path is also used to write changes back using the
+        :meth:`write` method."""
         self._path = filepath
         if filepath is not None:
             self._read_from_file()
@@ -189,31 +192,40 @@ def _read_from_file(self) -> None:
     @classmethod
     def from_file(cls, filepath: PathLike) -> "RefLog":
         """
-        :return: A new RefLog instance containing all entries from the reflog
-            at the given filepath
-        :param filepath: Path to reflog
-        :raise ValueError: If the file could not be read or was corrupted in some way
+        :return:
+            A new :class:`RefLog` instance containing all entries from the reflog at the
+            given `filepath`.
+
+        :param filepath:
+            Path to reflog.
+
+        :raise ValueError:
+            If the file could not be read or was corrupted in some way.
         """
         return cls(filepath)
 
     @classmethod
     def path(cls, ref: "SymbolicReference") -> str:
         """
-        :return: String to absolute path at which the reflog of the given ref
-            instance would be found. The path is not guaranteed to point to a valid
-            file though.
-        :param ref: SymbolicReference instance
+        :return:
+            String to absolute path at which the reflog of the given ref instance would
+            be found. The path is not guaranteed to point to a valid file though.
+
+        :param ref:
+            :class:`~git.refs.symbolic.SymbolicReference` instance
         """
         return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path))
 
     @classmethod
     def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEntry]:
         """
-        :return: Iterator yielding RefLogEntry instances, one for each line read
+        :return:
+            Iterator yielding :class:`RefLogEntry` instances, one for each line read
             from the given stream.
 
-        :param stream: File-like object containing the revlog in its native format
-            or string instance pointing to a file to read.
+        :param stream:
+            File-like object containing the revlog in its native format or string
+            instance pointing to a file to read.
         """
         new_entry = RefLogEntry.from_line
         if isinstance(stream, str):
@@ -233,18 +245,23 @@ def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEnt
     @classmethod
     def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry":
         """
-        :return: RefLogEntry at the given index.
+        :return:
+            :class:`RefLogEntry` at the given index.
 
-        :param filepath: Full path to the index file from which to read the entry.
+        :param filepath:
+            Full path to the index file from which to read the entry.
 
-        :param index: Python list compatible index, i.e. it may be negative to
-            specify an entry counted from the end of the list.
+        :param index:
+            Python list compatible index, i.e. it may be negative to specify an entry
+            counted from the end of the list.
 
-        :raise IndexError: If the entry didn't exist.
+        :raise IndexError:
+            If the entry didn't exist.
 
-        .. note:: This method is faster as it only parses the entry at index, skipping
-            all other lines. Nonetheless, the whole file has to be read if
-            the index is negative.
+        :note:
+            This method is faster as it only parses the entry at index, skipping all
+            other lines. Nonetheless, the whole file has to be read if the index is
+            negative.
         """
         with open(filepath, "rb") as fp:
             if index < 0:
@@ -264,7 +281,8 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry":
     def to_file(self, filepath: PathLike) -> None:
         """Write the contents of the reflog instance to a file at the given filepath.
 
-        :param filepath: Path to file, parent directories are assumed to exist.
+        :param filepath:
+            Path to file. Parent directories are assumed to exist.
         """
         lfd = LockedFD(filepath)
         assure_directory_exists(filepath, is_file=True)
@@ -290,19 +308,32 @@ def append_entry(
     ) -> "RefLogEntry":
         """Append a new log entry to the revlog at filepath.
 
-        :param config_reader: Configuration reader of the repository - used to obtain
-            user information. May also be an Actor instance identifying the committer
+        :param config_reader:
+            Configuration reader of the repository - used to obtain user information.
+            May also be an :class:`~git.util.Actor` instance identifying the committer
             directly or None.
-        :param filepath: Full path to the log file.
-        :param oldbinsha: Binary sha of the previous commit.
-        :param newbinsha: Binary sha of the current commit.
-        :param message: Message describing the change to the reference.
-        :param write: If True, the changes will be written right away. Otherwise the
-            change will not be written.
 
-        :return: RefLogEntry objects which was appended to the log.
+        :param filepath:
+            Full path to the log file.
+
+        :param oldbinsha:
+            Binary sha of the previous commit.
+
+        :param newbinsha:
+            Binary sha of the current commit.
+
+        :param message:
+            Message describing the change to the reference.
+
+        :param write:
+            If True, the changes will be written right away.
+            Otherwise the change will not be written.
+
+        :return:
+            :class:`RefLogEntry` objects which was appended to the log.
 
-        :note: As we are append-only, concurrent access is not a problem as we do not
+        :note:
+            As we are append-only, concurrent access is not a problem as we do not
             interfere with readers.
         """
 
@@ -340,7 +371,8 @@ def append_entry(
     def write(self) -> "RefLog":
         """Write this instance's data to the file we are originating from.
 
-        :return: self
+        :return:
+            self
         """
         if self._path is None:
             raise ValueError("Instance was not initialized with a path, use to_file(...) instead")
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 20d42472d..32547278f 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -25,7 +25,8 @@
 
 
 def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
-    """A decorator raising a TypeError if we are not a valid remote, based on the path."""
+    """A decorator raising :class:`TypeError` if we are not a valid remote, based on the
+    path."""
 
     def wrapper(self: T_References, *args: Any) -> _T:
         if not self.is_remote():
@@ -56,11 +57,16 @@ class Reference(SymbolicReference, LazyMixin, IterableObj):
     def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> None:
         """Initialize this instance.
 
-        :param repo: Our parent repository.
-        :param path: Path relative to the .git/ directory pointing to the ref in
-            question, e.g. ``refs/heads/master``.
-        :param check_path: If False, you can provide any path. Otherwise the path must
-            start with the default path prefix of this type.
+        :param repo:
+            Our parent repository.
+
+        :param path:
+            Path relative to the ``.git/`` directory pointing to the ref in question,
+            e.g. ``refs/heads/master``.
+
+        :param check_path:
+            If False, you can provide any path.
+            Otherwise the path must start with the default path prefix of this type.
         """
         if check_path and not str(path).startswith(self._common_path_default + "/"):
             raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
@@ -80,7 +86,8 @@ def set_object(
     ) -> "Reference":
         """Special version which checks if the head-log needs an update as well.
 
-        :return: self
+        :return:
+            self
         """
         oldbinsha = None
         if logmsg is not None:
@@ -115,7 +122,10 @@ def set_object(
 
     @property
     def name(self) -> str:
-        """:return: (shortest) Name of this reference - it may contain path components"""
+        """
+        :return:
+            (shortest) Name of this reference - it may contain path components
+        """
         # The first two path tokens can be removed as they are
         # refs/heads or refs/tags or refs/remotes.
         tokens = self.path.split("/")
@@ -144,8 +154,8 @@ def iter_items(
     def remote_name(self) -> str:
         """
         :return:
-            Name of the remote we are a reference of, such as 'origin' for a reference
-            named 'origin/master'.
+            Name of the remote we are a reference of, such as ``origin`` for a reference
+            named ``origin/master``.
         """
         tokens = self.path.split("/")
         # /refs/remotes/<remote name>/<branch_name>
@@ -155,10 +165,12 @@ def remote_name(self) -> str:
     @require_remote_ref_path
     def remote_head(self) -> str:
         """
-        :return: Name of the remote head itself, e.g. master.
+        :return:
+            Name of the remote head itself, e.g. ``master``.
 
-        :note: The returned name is usually not qualified enough to uniquely identify
-            a branch.
+        :note:
+            The returned name is usually not qualified enough to uniquely identify a
+            branch.
         """
         tokens = self.path.split("/")
         return "/".join(tokens[3:])
diff --git a/git/refs/remote.py b/git/refs/remote.py
index 59d02a755..c2c2c1aac 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -47,10 +47,10 @@ def iter_items(
         # super is Reference
         return super().iter_items(repo, common_path)
 
-    # The Head implementation of delete also accepts strs, but this
-    # implementation does not.  mypy doesn't have a way of representing
-    # tightening the types of arguments in subclasses and recommends Any or
-    # "type: ignore".  (See https://github.com/python/typing/issues/241)
+    # The Head implementation of delete also accepts strs, but this implementation does
+    # not. mypy doesn't have a way of representing tightening the types of arguments in
+    # subclasses and recommends Any or "type: ignore".
+    # (See: https://github.com/python/typing/issues/241)
     @classmethod
     def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore
         """Delete the given remote references.
@@ -60,9 +60,9 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:
             should not narrow the signature.
         """
         repo.git.branch("-d", "-r", *refs)
-        # The official deletion method will ignore remote symbolic refs - these
-        # are generally ignored in the refs/ folder. We don't though
-        # and delete remainders manually.
+        # The official deletion method will ignore remote symbolic refs - these are
+        # generally ignored in the refs/ folder. We don't though and delete remainders
+        # manually.
         for ref in refs:
             try:
                 os.remove(os.path.join(repo.common_dir, ref.path))
@@ -76,5 +76,5 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:
 
     @classmethod
     def create(cls, *args: Any, **kwargs: Any) -> NoReturn:
-        """Raises TypeError. Defined so the create method is disabled."""
+        """Raise :class:`TypeError`. Defined so the ``create`` method is disabled."""
         raise TypeError("Cannot explicitly create remote references")
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 31f959ac2..6b9fd9ab7 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -100,8 +100,8 @@ def __hash__(self) -> int:
     def name(self) -> str:
         """
         :return:
-            In case of symbolic references, the shortest assumable name
-            is the path itself.
+            In case of symbolic references, the shortest assumable name is the path
+            itself.
         """
         return str(self.path)
 
@@ -118,7 +118,8 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
         """Return an iterator yielding pairs of sha1/path pairs (as strings)
         for the corresponding refs.
 
-        :note: The packed refs file will be kept open as long as we iterate.
+        :note:
+            The packed refs file will be kept open as long as we iterate.
         """
         try:
             with open(cls._get_packed_refs_path(repo), "rt", encoding="UTF-8") as fp:
@@ -155,10 +156,12 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
     @classmethod
     def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> str:
         """
-        :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all
-            intermediate references as required
+        :return:
+            hexsha stored in the reference at the given `ref_path`, recursively
+            dereferencing all intermediate references as required
 
-        :param repo: The repository containing the reference at ref_path
+        :param repo:
+            The repository containing the reference at `ref_path`.
         """
 
         while True:
@@ -221,8 +224,9 @@ def _get_ref_info_helper(
         cls, repo: "Repo", ref_path: Union[PathLike, None]
     ) -> Union[Tuple[str, None], Tuple[None, str]]:
         """
-        :return: (str(sha), str(target_ref_path)) if available, the sha the file at
-            rela_path points to, or None.
+        :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.
         """
@@ -276,8 +280,8 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T
     def _get_object(self) -> Commit_ish:
         """
         :return:
-            The object our ref currently refers to. Refs can be cached, they will
-            always point to the actual object as it gets re-created on each query.
+            The object our ref currently refers to. Refs can be cached, they will always
+            point to the actual object as it gets re-created on each query.
         """
         # We have to be dynamic here as we may be a tag which can point to anything.
         # Our path will be resolved to the hexsha which will be used accordingly.
@@ -305,11 +309,15 @@ def set_commit(
         commit: Union[Commit, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
-        """As set_object, but restricts the type of object to be a Commit.
+        """Like :meth:`set_object`, but restricts the type of object to be a
+        :class:`~git.objects.commit.Commit`.
+
+        :raise ValueError:
+            If `commit` is not a :class:`~git.objects.commit.Commit` object or doesn't
+            point to a commit.
 
-        :raise ValueError: If commit is not a :class:`~git.objects.commit.Commit` object
-            or doesn't point to a commit
-        :return: self
+        :return:
+            self
         """
         # Check the type - assume the best if it is a base-string.
         invalid_type = False
@@ -339,18 +347,25 @@ def set_object(
         object: Union[Commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
-        """Set the object we point to, possibly dereference our symbolic reference first.
-        If the reference does not exist, it will be created.
-
-        :param object: A refspec, a :class:`SymbolicReference` or an
-            :class:`~git.objects.base.Object` instance.
-            :class:`SymbolicReference` instances will be dereferenced beforehand to
-            obtain the object they point to.
-        :param logmsg: If not None, the message will be used in the reflog entry to be
-            written. Otherwise the reflog is not altered.
-        :note: Plain :class:`SymbolicReference` instances may not actually point to
-            objects by convention.
-        :return: self
+        """Set the object we point to, possibly dereference our symbolic reference
+        first. If the reference does not exist, it will be created.
+
+        :param object:
+            A refspec, a :class:`SymbolicReference` or an
+            :class:`~git.objects.base.Object` instance. :class:`SymbolicReference`
+            instances will be dereferenced beforehand to obtain the object they point
+            to.
+
+        :param logmsg:
+            If not None, the message will be used in the reflog entry to be written.
+            Otherwise the reflog is not altered.
+
+        :note:
+            Plain :class:`SymbolicReference` instances may not actually point to objects
+            by convention.
+
+        :return:
+            self
         """
         if isinstance(object, SymbolicReference):
             object = object.object  # @ReservedAssignment
@@ -374,10 +389,13 @@ def set_object(
 
     def _get_reference(self) -> "SymbolicReference":
         """
-        :return: Reference Object we point to
+        :return:
+            :class:`~git.refs.reference.Reference` object we point to
 
-        :raise TypeError: If this symbolic reference is detached, hence it doesn't point
-            to a reference, but to a commit"""
+        :raise TypeError:
+            If this symbolic reference is detached, hence it doesn't point to a
+            reference, but to a commit.
+        """
         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))
@@ -388,10 +406,13 @@ def set_reference(
         ref: Union[Commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
-        """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference.
-        Otherwise an Object, given as Object instance or refspec, is assumed and if valid,
-        will be set which effectively detaches the reference if it was a purely
-        symbolic one.
+        """Set ourselves to the given `ref`.
+
+        It will stay a symbol if the ref is a :class:`~git.refs.reference.Reference`.
+
+        Otherwise an Object, given as :class:`~git.objects.base.Object` instance or
+        refspec, is assumed and if valid, will be set which effectively detaches the
+        reference if it was a purely symbolic one.
 
         :param ref:
             A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object`
@@ -399,15 +420,18 @@ def set_reference(
             :class:`SymbolicReference` instance, we will point to it. Everything else is
             dereferenced to obtain the actual object.
 
-        :param logmsg: If set to a string, the message will be used in the reflog.
+        :param logmsg:
+            If set to a string, the message will be used in the reflog.
             Otherwise, a reflog entry is not written for the changed reference.
             The previous commit of the entry will be the commit we point to now.
 
             See also: :meth:`log_append`
 
-        :return: self
+        :return:
+            self
 
-        :note: This symbolic reference will not be dereferenced. For that, see
+        :note:
+            This symbolic reference will not be dereferenced. For that, see
             :meth:`set_object`.
         """
         write_value = None
@@ -467,8 +491,8 @@ def set_reference(
     def is_valid(self) -> bool:
         """
         :return:
-            True if the reference is valid, hence it can be read and points to
-            a valid object or reference.
+            True if the reference is valid, hence it can be read and points to a valid
+            object or reference.
         """
         try:
             self.object
@@ -492,11 +516,13 @@ def is_detached(self) -> bool:
 
     def log(self) -> "RefLog":
         """
-        :return: RefLog for this reference. Its last entry reflects the latest change
-            applied to this reference.
+        :return:
+            :class:`~git.refs.log.RefLog` for this reference.
+            Its last entry reflects the latest change applied to this reference.
 
-        .. note:: As the log is parsed every time, its recommended to cache it for use
-            instead of calling this method repeatedly. It should be considered read-only.
+        :note:
+            As the log is parsed every time, its recommended to cache it for use instead
+            of calling this method repeatedly. It should be considered read-only.
         """
         return RefLog.from_file(RefLog.path(self))
 
@@ -508,11 +534,17 @@ def log_append(
     ) -> "RefLogEntry":
         """Append a logentry to the logfile of this ref.
 
-        :param oldbinsha: Binary sha this ref used to point to.
-        :param message: A message describing the change.
-        :param newbinsha: The sha the ref points to now. If None, our current commit sha
-            will be used.
-        :return: The added :class:`~git.refs.log.RefLogEntry` instance.
+        :param oldbinsha:
+            Binary sha this ref used to point to.
+
+        :param message:
+            A message describing the change.
+
+        :param newbinsha:
+            The sha the ref points to now. If None, our current commit sha will be used.
+
+        :return:
+            The added :class:`~git.refs.log.RefLogEntry` instance.
         """
         # NOTE: We use the committer of the currently active commit - this should be
         # correct to allow overriding the committer on a per-commit level.
@@ -532,20 +564,24 @@ def log_append(
 
     def log_entry(self, index: int) -> "RefLogEntry":
         """
-        :return: RefLogEntry at the given index
+        :return:
+            RefLogEntry at the given index
 
-        :param index: Python list compatible positive or negative index
+        :param index:
+            Python list compatible positive or negative index.
 
-        .. note:: This method must read part of the reflog during execution, hence
-            it should be used sparingly, or only if you need just one index.
-            In that case, it will be faster than the :meth:`log` method.
+        :note:
+            This method must read part of the reflog during execution, hence it should
+            be used sparingly, or only if you need just one index. In that case, it will
+            be faster than the :meth:`log` method.
         """
         return RefLog.entry_at(RefLog.path(self), index)
 
     @classmethod
     def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
         """
-        :return: string with a full repository-relative path which can be used to initialize
+        :return:
+            String with a full repository-relative path which can be used to initialize
             a Reference instance, for instance by using
             :meth:`Reference.from_path <git.refs.reference.Reference.from_path>`.
         """
@@ -566,8 +602,8 @@ def delete(cls, repo: "Repo", path: PathLike) -> None:
             Repository to delete the reference from.
 
         :param path:
-            Short or full path pointing to the reference, e.g. ``refs/myreference``
-            or just ``myreference``, hence ``refs/`` is implied.
+            Short or full path pointing to the reference, e.g. ``refs/myreference`` or
+            just ``myreference``, hence ``refs/`` is implied.
             Alternatively the symbolic reference to be deleted.
         """
         full_ref_path = cls.to_full_path(path)
@@ -586,10 +622,10 @@ def delete(cls, repo: "Repo", path: PathLike) -> None:
                         line = line_bytes.decode(defenc)
                         _, _, line_ref = line.partition(" ")
                         line_ref = line_ref.strip()
-                        # Keep line if it is a comment or if the ref to delete is not
-                        # in the line.
-                        # If we deleted the last line and this one is a tag-reference object,
-                        # we drop it as well.
+                        # Keep line if it is a comment or if the ref to delete is not in
+                        # the line.
+                        # If we deleted the last line and this one is a tag-reference
+                        # object, we drop it as well.
                         if (line.startswith("#") or full_ref_path != line_ref) and (
                             not dropped_last_line or dropped_last_line and not line.startswith("^")
                         ):
@@ -604,8 +640,8 @@ def delete(cls, repo: "Repo", path: PathLike) -> None:
 
                 # Write the new lines.
                 if made_change:
-                    # Binary writing is required, otherwise Windows will
-                    # open the file in text mode and change LF to CRLF!
+                    # Binary writing is required, otherwise Windows will open the file
+                    # in text mode and change LF to CRLF!
                     with open(pack_file_path, "wb") as fd:
                         fd.writelines(line.encode(defenc) for line in new_lines)
 
@@ -630,10 +666,9 @@ def _create(
     ) -> T_References:
         """Internal method used to create a new symbolic reference.
 
-        If `resolve` is False, the reference will be taken as is, creating
-        a proper symbolic reference. Otherwise it will be resolved to the
-        corresponding object and a detached symbolic reference will be created
-        instead.
+        If `resolve` is False, the reference will be taken as is, creating a proper
+        symbolic reference. Otherwise it will be resolved to the corresponding object
+        and a detached symbolic reference will be created instead.
         """
         git_dir = _git_dir(repo, path)
         full_ref_path = cls.to_full_path(path)
@@ -679,28 +714,30 @@ def create(
             Repository to create the reference in.
 
         :param path:
-            Full path at which the new symbolic reference is supposed to be
-            created at, e.g. ``NEW_HEAD`` or ``symrefs/my_new_symref``.
+            Full path at which the new symbolic reference is supposed to be created at,
+            e.g. ``NEW_HEAD`` or ``symrefs/my_new_symref``.
 
         :param reference:
             The reference which the new symbolic reference should point to.
             If it is a commit-ish, the symbolic ref will be detached.
 
         :param force:
-            If True, force creation even if a symbolic reference with that name already exists.
-            Raise :class:`OSError` otherwise.
+            If True, force creation even if a symbolic reference with that name already
+            exists. Raise :class:`OSError` otherwise.
 
         :param logmsg:
-            If not None, the message to append to the reflog. Otherwise no reflog
-            entry is written.
+            If not None, the message to append to the reflog.
+            If None, no reflog entry is written.
 
-        :return: Newly created symbolic Reference
+        :return:
+            Newly created symbolic reference
 
         :raise OSError:
-            If a (Symbolic)Reference with the same name but different contents
-            already exists.
+            If a (Symbolic)Reference with the same name but different contents already
+            exists.
 
-        :note: This does not alter the current HEAD, index or working tree.
+        :note:
+            This does not alter the current HEAD, index or working tree.
         """
         return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg)
 
@@ -708,17 +745,20 @@ def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference"
         """Rename self to a new path.
 
         :param new_path:
-            Either a simple name or a full path, e.g. ``new_name`` or ``features/new_name``.
+            Either a simple name or a full path, e.g. ``new_name`` or
+            ``features/new_name``.
             The prefix ``refs/`` is implied for references and will be set as needed.
             In case this is a symbolic ref, there is no implied prefix.
 
         :param force:
-            If True, the rename will succeed even if a head with the target name
-            already exists. It will be overwritten in that case.
+            If True, the rename will succeed even if a head with the target name already
+            exists. It will be overwritten in that case.
 
-        :return: self
+        :return:
+            self
 
-        :raise OSError: If a file at path but with different contents already exists.
+        :raise OSError:
+            If a file at path but with different contents already exists.
         """
         new_path = self.to_full_path(new_path)
         if self.path == new_path:
@@ -801,7 +841,8 @@ def iter_items(
     ) -> Iterator[T_References]:
         """Find all refs in the repository.
 
-        :param repo: is the Repo
+        :param repo:
+            The :class:`~git.repo.base.Repo`.
 
         :param common_path:
             Optional keyword argument to the path which is to be shared by all returned
@@ -821,13 +862,13 @@ def iter_items(
 
     @classmethod
     def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_References:
-        """
-        Make a symbolic reference from a path.
+        """Make a symbolic reference from a path.
 
-        :param path: Full ``.git``-directory-relative path name to the Reference to
-            instantiate.
+        :param path:
+            Full ``.git``-directory-relative path name to the Reference to instantiate.
 
-        :note: Use :meth:`to_full_path` if you only have a partial path of a known
+        :note:
+            Use :meth:`to_full_path` if you only have a partial path of a known
             Reference type.
 
         :return:
diff --git a/git/refs/tag.py b/git/refs/tag.py
index a59a51337..7edd1d12a 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -43,7 +43,8 @@ class TagReference(Reference):
     def commit(self) -> "Commit":  # type: ignore[override]  # LazyMixin has unrelated commit method
         """:return: Commit object the tag ref points to
 
-        :raise ValueError: If the tag points to a tree or blob
+        :raise ValueError:
+            If the tag points to a tree or blob.
         """
         obj = self.object
         while obj.type != "commit":
@@ -63,8 +64,9 @@ def commit(self) -> "Commit":  # type: ignore[override]  # LazyMixin has unrelat
     @property
     def tag(self) -> Union["TagObject", None]:
         """
-        :return: Tag object this tag ref points to or None in case
-            we are a lightweight tag"""
+        :return:
+            Tag object this tag ref points to or None in case we are a lightweight tag
+        """
         obj = self.object
         if obj.type == "tag":
             return obj
@@ -97,21 +99,23 @@ def create(
 
         :param logmsg:
             If not None, the message will be used in your tag object. This will also
-            create an additional tag object that allows to obtain that information, e.g.::
+            create an additional tag object that allows to obtain that information,
+            e.g.::
 
                 tagref.tag.message
 
         :param message:
-            Synonym for the `logmsg` parameter.
-            Included for backwards compatibility. `logmsg` takes precedence if both are passed.
+            Synonym for the `logmsg` parameter. Included for backwards compatibility.
+            `logmsg` takes precedence if both are passed.
 
         :param force:
             If True, force creation of a tag even though that tag already exists.
 
         :param kwargs:
-            Additional keyword arguments to be passed to git-tag.
+            Additional keyword arguments to be passed to ``git tag``.
 
-        :return: A new TagReference.
+        :return:
+            A new :class:`TagReference`.
         """
         if "ref" in kwargs and kwargs["ref"]:
             reference = kwargs["ref"]

From 5d6c86a47cb72d7ca80d122bc0bb899eb0637b19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Can=20Ta=C5=9Fl=C4=B1=C3=A7ukur?=
 <can.taslicukur@ozu.edu.tr>
Date: Wed, 28 Feb 2024 00:24:59 +0300
Subject: [PATCH 493/994] test: :white_check_mark: Added test for external diff
 engine and removed comment

---
 git/diff.py       |  1 -
 test/test_diff.py | 42 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/git/diff.py b/git/diff.py
index ae85e77ae..6d4474d3e 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -155,7 +155,6 @@ def diff(
 
         if create_patch:
             args.append("-p")
-            # we expect the default diff driver
             args.append("--no-ext-diff")
         else:
             args.append("--raw")
diff --git a/test/test_diff.py b/test/test_diff.py
index 87f92f5d1..e3d0b8e5c 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -473,3 +473,45 @@ 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)
+
+    @with_rw_directory
+    def test_diff_patch_with_external_engine(self, rw_dir):
+        repo = Repo.init(rw_dir)
+        gitignore = osp.join(rw_dir, ".gitignore")
+
+        # First commit
+        with open(gitignore, "w") as f:
+            f.write("first_line\n")
+        repo.git.add(".gitignore")
+        repo.index.commit("first commit")
+
+        # Adding second line and committing
+        with open(gitignore, "a") as f:
+            f.write("second_line\n")
+        repo.git.add(".gitignore")
+        repo.index.commit("second commit")
+
+        # Adding third line and staging
+        with open(gitignore, "a") as f:
+            f.write("third_line\n")
+        repo.git.add(".gitignore")
+
+        # Adding fourth line
+        with open(gitignore, "a") as f:
+            f.write("fourth_line\n")
+
+        # Set the external diff engine
+        with repo.config_writer(config_level="repository") as writer:
+            writer.set_value("diff", "external", "bogus_diff_engine")
+
+        head_against_head = repo.head.commit.diff("HEAD^", create_patch=True)
+        self.assertEqual(len(head_against_head), 1)
+        head_against_index = repo.head.commit.diff(create_patch=True)
+        self.assertEqual(len(head_against_index), 1)
+        head_against_working_tree = repo.head.commit.diff(None, create_patch=True)
+        self.assertEqual(len(head_against_working_tree), 1)
+
+        index_against_head = repo.index.diff("HEAD", create_patch=True)
+        self.assertEqual(len(index_against_head), 1)
+        index_against_working_tree = repo.index.diff(None, create_patch=True)
+        self.assertEqual(len(index_against_working_tree), 1)

From 4f6736995cf9816773757b9a6808d6eaabd08fd4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 18:04:12 -0500
Subject: [PATCH 494/994] Fix backslashes in Repo.__init__ docstring

The example code block included a Windows-style path string written
with doubled backslashes, but the docstring itself was not a raw
string literal, so these collapsed into single backslashes. This
makes the whole docstring an R-string, which is sufficient to solve
that problem (since the backslashes are in a code block, Sphinx
does not itself treat them as metacharacters).

In addition, this changes the Windows-style path to be an R-string
rather than using doubled backslashes. This is just to improve
clarity (and remind readers they can use an R-string for Windows
paths), and does not affect correctness.
---
 git/repo/base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index bf7c2cc0d..c840e8a30 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -168,7 +168,7 @@ def __init__(
         search_parent_directories: bool = False,
         expand_vars: bool = True,
     ) -> None:
-        """Create a new Repo instance.
+        R"""Create a new Repo instance.
 
         :param path:
             The path to either the root git directory or the bare git repo::
@@ -177,7 +177,7 @@ def __init__(
                 repo = Repo("/Users/mtrier/Development/git-python.git")
                 repo = Repo("~/Development/git-python.git")
                 repo = Repo("$REPOSITORIES/Development/git-python.git")
-                repo = Repo("C:\\Users\\mtrier\\Development\\git-python\\.git")
+                repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
 
             - In *Cygwin*, path may be a ``cygdrive/...`` prefixed path.
             - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also

From 0c8ca1a9bdc7852e0e8b86923765c8cf812154e6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 18:45:26 -0500
Subject: [PATCH 495/994] Fix Repo.iter_commits docstring about return type

It had said it returned a list of Commit objects, but it returns an
iterator of Commit objects.
---
 git/repo/base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index c840e8a30..598921ca9 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -692,7 +692,7 @@ def iter_commits(
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator[Commit]:
-        """A list of Commit objects representing the history of a given ref/commit.
+        """An iterator of Commit objects representing the history of a given ref/commit.
 
         :param rev:
             Revision specifier, see git-rev-parse for viable options.
@@ -708,7 +708,7 @@ def iter_commits(
         :note: To receive only commits between two named revisions, use the
             ``"revA...revB"`` revision specifier.
 
-        :return: ``git.Commit[]``
+        :return: Iterator of ``git.Commit``
         """
         if rev is None:
             rev = self.head.commit

From b2b6f7c83cafd69ee683dfc2546a98c8699d0a6a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 19:54:09 -0500
Subject: [PATCH 496/994] Revise docstrings within git.repo

---
 git/repo/__init__.py |   2 +-
 git/repo/base.py     | 479 ++++++++++++++++++++++++++-----------------
 git/repo/fun.py      | 120 ++++++-----
 3 files changed, 366 insertions(+), 235 deletions(-)

diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index a63d77878..50a8c6f86 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -1,6 +1,6 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Initialize the Repo package."""
+"""Initialize the repo package."""
 
 from .base import Repo as Repo  # noqa: F401
diff --git a/git/repo/base.py b/git/repo/base.py
index 598921ca9..afbc1d75d 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -103,26 +103,34 @@ class BlameEntry(NamedTuple):
 
 class Repo:
     """Represents a git repository and allows you to query references,
-    gather commit information, generate diffs, create and clone repositories query
+    gather commit information, generate diffs, create and clone repositories, and query
     the log.
 
     The following attributes are worth using:
 
-    'working_dir' is the working directory of the git command, which is the working tree
-    directory if available or the .git directory in case of bare repositories
+    * :attr:`working_dir` is the working directory of the git command, which is the
+      working tree directory if available or the ``.git`` directory in case of bare
+      repositories.
 
-    'working_tree_dir' is the working tree directory, but will return None
-    if we are a bare repository.
+    * :attr:`working_tree_dir` is the working tree directory, but will return None if we
+      are a bare repository.
 
-    'git_dir' is the .git repository directory, which is always set.
+    * :attr:`git_dir` is the ``.git`` repository directory, which is always set.
     """
 
     DAEMON_EXPORT_FILE = "git-daemon-export-ok"
 
-    git = cast("Git", None)  # Must exist, or  __del__  will fail in case we raise on `__init__()`.
+    # Must exist, or  __del__  will fail in case we raise on `__init__()`.
+    git = cast("Git", None)
+
     working_dir: PathLike
+    """The working directory of the git command."""
+
     _working_tree_dir: Optional[PathLike] = None
+
     git_dir: PathLike
+    """The ``.git`` repository directory."""
+
     _common_dir: PathLike = ""
 
     # Precompiled regex
@@ -168,7 +176,7 @@ def __init__(
         search_parent_directories: bool = False,
         expand_vars: bool = True,
     ) -> None:
-        R"""Create a new Repo instance.
+        R"""Create a new :class:`Repo` instance.
 
         :param path:
             The path to either the root git directory or the bare git repo::
@@ -179,14 +187,14 @@ def __init__(
                 repo = Repo("$REPOSITORIES/Development/git-python.git")
                 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
 
-            - In *Cygwin*, path may be a ``cygdrive/...`` prefixed path.
-            - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also
-              evals to false, the current-directory is used.
+            - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
+            - If `path` is None or an empty string, :envvar:`GIT_DIR` is used. If that
+              environment variable is absent or empty, the current directory is used.
 
         :param odbt:
-            Object DataBase type - a type which is constructed by providing
-            the directory containing the database objects, i.e. .git/objects. It will
-            be used to access all object data
+            Object DataBase type - a type which is constructed by providing the
+            directory containing the database objects, i.e. ``.git/objects``. It will be
+            used to access all object data
 
         :param search_parent_directories:
             If True, all parent directories will be searched for a valid repo as well.
@@ -195,18 +203,20 @@ def __init__(
             GitPython, which is considered a bug though.
 
         :raise InvalidGitRepositoryError:
+
         :raise NoSuchPathError:
 
-        :return: git.Repo
+        :return:
+            git.Repo
         """
 
         epath = path or os.getenv("GIT_DIR")
         if not epath:
             epath = os.getcwd()
         if Git.is_cygwin():
-            # Given how the tests are written, this seems more likely to catch
-            # Cygwin git used from Windows than Windows git used from Cygwin.
-            # Therefore changing to Cygwin-style paths is the relevant operation.
+            # Given how the tests are written, this seems more likely to catch Cygwin
+            # git used from Windows than Windows git used from Cygwin. Therefore
+            # changing to Cygwin-style paths is the relevant operation.
             epath = cygpath(epath)
 
         epath = epath or path or os.getcwd()
@@ -223,25 +233,26 @@ def __init__(
             if not os.path.exists(epath):
                 raise NoSuchPathError(epath)
 
-        ## Walk up the path to find the `.git` dir.
-        #
+        # Walk up the path to find the `.git` dir.
         curpath = epath
         git_dir = None
         while curpath:
             # ABOUT osp.NORMPATH
-            # It's important to normalize the paths, as submodules will otherwise initialize their
-            # repo instances with paths that depend on path-portions that will not exist after being
-            # removed. It's just cleaner.
+            # It's important to normalize the paths, as submodules will otherwise
+            # initialize their repo instances with paths that depend on path-portions
+            # that will not exist after being removed. It's just cleaner.
             if is_git_dir(curpath):
                 git_dir = curpath
                 # from man git-config : core.worktree
-                # Set the path to the root of the working tree. If GIT_COMMON_DIR environment
-                # variable is set, core.worktree is ignored and not used for determining the
-                # root of working tree. This can be overridden by the GIT_WORK_TREE environment
-                # variable. The value can be an absolute path or relative to the path to the .git
-                # directory, which is either specified by GIT_DIR, or automatically discovered.
-                # If GIT_DIR is specified but none of GIT_WORK_TREE and core.worktree is specified,
-                # the current working directory is regarded as the top level of your working tree.
+                # Set the path to the root of the working tree. If GIT_COMMON_DIR
+                # environment variable is set, core.worktree is ignored and not used for
+                # determining the root of working tree. This can be overridden by the
+                # GIT_WORK_TREE environment variable. The value can be an absolute path
+                # or relative to the path to the .git directory, which is either
+                # specified by GIT_DIR, or automatically discovered. If GIT_DIR is
+                # specified but none of GIT_WORK_TREE and core.worktree is specified,
+                # the current working directory is regarded as the top level of your
+                # working tree.
                 self._working_tree_dir = os.path.dirname(git_dir)
                 if os.environ.get("GIT_COMMON_DIR") is None:
                     gitconf = self._config_reader("repository", git_dir)
@@ -359,7 +370,8 @@ def _set_description(self, descr: str) -> None:
     @property
     def working_tree_dir(self) -> Optional[PathLike]:
         """
-        :return: The working tree directory of our git repository.
+        :return:
+            The working tree directory of our git repository.
             If this is a bare repository, None is returned.
         """
         return self._working_tree_dir
@@ -367,8 +379,9 @@ def working_tree_dir(self) -> Optional[PathLike]:
     @property
     def common_dir(self) -> PathLike:
         """
-        :return: The git dir that holds everything except possibly HEAD,
-            FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
+        :return:
+            The git dir that holds everything except possibly HEAD, FETCH_HEAD,
+            ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
         """
         return self._common_dir or self.git_dir
 
@@ -379,17 +392,21 @@ def bare(self) -> bool:
 
     @property
     def heads(self) -> "IterableList[Head]":
-        """A list of ``Head`` objects representing the branch heads in this repo.
+        """A list of :class:`~git.refs.head.Head` objects representing the branch heads
+        in this repo.
 
-        :return: ``git.IterableList(Head, ...)``
+        :return:
+            ``git.IterableList(Head, ...)``
         """
         return Head.list_items(self)
 
     @property
     def references(self) -> "IterableList[Reference]":
-        """A list of Reference objects representing tags, heads and remote references.
+        """A list of :class:`~git.refs.reference.Reference` objects representing tags,
+        heads and remote references.
 
-        :return: ``git.IterableList(Reference, ...)``
+        :return:
+            ``git.IterableList(Reference, ...)``
         """
         return Reference.list_items(self)
 
@@ -402,10 +419,11 @@ def references(self) -> "IterableList[Reference]":
     @property
     def index(self) -> "IndexFile":
         """
-        :return: A :class:`~git.index.base.IndexFile` representing this repository's
-            index.
+        :return:
+            A :class:`~git.index.base.IndexFile` representing this repository's index.
 
-        :note: This property can be expensive, as the returned
+        :note:
+            This property can be expensive, as the returned
             :class:`~git.index.base.IndexFile` will be reinitialized.
             It is recommended to reuse the object.
         """
@@ -413,21 +431,27 @@ def index(self) -> "IndexFile":
 
     @property
     def head(self) -> "HEAD":
-        """:return: HEAD Object pointing to the current head reference"""
+        """
+        :return:
+            :class:`~git.refs.head.HEAD` object pointing to the current head reference
+        """
         return HEAD(self, "HEAD")
 
     @property
     def remotes(self) -> "IterableList[Remote]":
-        """A list of Remote objects allowing to access and manipulate remotes.
+        """A list of :class:`~git.remote.Remote` objects allowing to access and
+        manipulate remotes.
 
-        :return: ``git.IterableList(Remote, ...)``
+        :return:
+            ``git.IterableList(Remote, ...)``
         """
         return Remote.list_items(self)
 
     def remote(self, name: str = "origin") -> "Remote":
-        """:return: Remote with the specified name
+        """:return: The remote with the specified name
 
-        :raise ValueError: If no remote with such a name exists
+        :raise ValueError
+            If no remote with such a name exists.
         """
         r = Remote(self, name)
         if not r.exists():
@@ -439,15 +463,17 @@ def remote(self, name: str = "origin") -> "Remote":
     @property
     def submodules(self) -> "IterableList[Submodule]":
         """
-        :return: git.IterableList(Submodule, ...) of direct submodules
-            available from the current head
+        :return:
+            git.IterableList(Submodule, ...) of direct submodules available from the
+            current head
         """
         return Submodule.list_items(self)
 
     def submodule(self, name: str) -> "Submodule":
-        """:return: Submodule with the given name
+        """:return: The submodule with the given name
 
-        :raise ValueError: If no such submodule exists
+        :raise ValueError:
+            If no such submodule exists.
         """
         try:
             return self.submodules[name]
@@ -458,10 +484,12 @@ def submodule(self, name: str) -> "Submodule":
     def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
         """Create a new submodule.
 
-        :note: See the documentation of Submodule.add for a description of the
-            applicable parameters.
+        :note:
+            For a description of the applicable parameters, see the documentation of
+            :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`.
 
-        :return: The created submodules.
+        :return:
+            The created submodule.
         """
         return Submodule.add(self, *args, **kwargs)
 
@@ -477,7 +505,8 @@ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
         """Update the submodules, keeping the repository consistent as it will
         take the previous state into consideration.
 
-        :note: For more information, please see the documentation of
+        :note:
+            For more information, please see the documentation of
             :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
         """
         return RootModule(self).update(*args, **kwargs)
@@ -486,16 +515,22 @@ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
 
     @property
     def tags(self) -> "IterableList[TagReference]":
-        """A list of ``Tag`` objects that are available in this repo.
+        """A list of :class:`~git.refs.tag.TagReference` objects that are available in
+        this repo.
 
-        :return: ``git.IterableList(TagReference, ...)``
+        :return:
+            ``git.IterableList(TagReference, ...)``
         """
         return TagReference.list_items(self)
 
     def tag(self, path: PathLike) -> TagReference:
-        """:return: TagReference Object, reference pointing to a Commit or Tag
+        """
+        :return:
+            :class:`~git.refs.tag.TagReference` object, reference pointing to a
+            :class:`~git.objects.commit.Commit` or tag
 
-        :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5
+        :param path:
+            Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``.
         """
         full_path = self._to_full_tag_path(path)
         return TagReference(self, full_path)
@@ -522,14 +557,16 @@ def create_head(
         :note: For more documentation, please see the
             :meth:`Head.create <git.refs.head.Head.create>` method.
 
-        :return: Newly created :class:`~git.refs.head.Head` Reference
+        :return:
+            Newly created :class:`~git.refs.head.Head` Reference.
         """
         return Head.create(self, path, commit, logmsg, force)
 
     def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
         """Delete the given heads.
 
-        :param kwargs: Additional keyword arguments to be passed to git-branch
+        :param kwargs:
+            Additional keyword arguments to be passed to ``git branch``.
         """
         return Head.delete(self, *heads, **kwargs)
 
@@ -543,10 +580,12 @@ def create_tag(
     ) -> TagReference:
         """Create a new tag reference.
 
-        :note: For more documentation, please see the
+        :note:
+            For more documentation, please see the
             :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
 
-        :return: :class:`~git.refs.tag.TagReference` object
+        :return:
+            :class:`~git.refs.tag.TagReference` object
         """
         return TagReference.create(self, path, ref, message, force, **kwargs)
 
@@ -560,7 +599,8 @@ def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
         For more information, please see the documentation of the
         :meth:`Remote.create <git.remote.Remote.create>` method.
 
-        :return: :class:`~git.remote.Remote` reference
+        :return:
+            :class:`~git.remote.Remote` reference
         """
         return Remote.create(self, name, url, **kwargs)
 
@@ -612,7 +652,8 @@ def config_reader(
             applicable levels will be used. Specify a level in case you know which file
             you wish to read to prevent reading multiple files.
 
-        :note: On Windows, system configuration cannot currently be read as the path is
+        :note:
+            On Windows, system configuration cannot currently be read as the path is
             unknown, instead the global path will be used.
         """
         return self._config_reader(config_level=config_level)
@@ -650,37 +691,43 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo
         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.
+        """The :class:`~git.objects.commit.Commit` object for the specified revision.
+
+        :param rev:
+            Revision specifier, see ``git rev-parse`` for viable options.
 
-        :param rev: revision specifier, see git-rev-parse for viable options.
-        :return: :class:`git.Commit <git.objects.commit.Commit>`
+        :return:
+            :class:`~git.objects.commit.Commit`
         """
         if rev is None:
             return self.head.commit
         return self.rev_parse(str(rev) + "^0")
 
     def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
-        """:return: Iterator yielding Tree objects
+        """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects
 
-        :note: Accepts all arguments known to the :meth:`iter_commits` method.
+        :note:
+            Accepts all arguments known to the :meth:`iter_commits` method.
         """
         return (c.tree for c in self.iter_commits(*args, **kwargs))
 
     def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
-        """The Tree object for the given tree-ish revision.
+        """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision.
 
         Examples::
 
               repo.tree(repo.heads[0])
 
-        :param rev: is a revision pointing to a Treeish (being a commit or tree)
+        :param rev:
+            A revision pointing to a Treeish (being a commit or tree).
 
-        :return: ``git.Tree``
+        :return:
+            :class:`~git.objects.tree.Tree`
 
         :note:
-            If you need a non-root level tree, find it by iterating the root tree. Otherwise
-            it cannot know about its path relative to the repository root and subsequent
-            operations might have unexpected results.
+            If you need a non-root level tree, find it by iterating the root tree.
+            Otherwise it cannot know about its path relative to the repository root and
+            subsequent operations might have unexpected results.
         """
         if rev is None:
             return self.head.commit.tree
@@ -692,23 +739,27 @@ def iter_commits(
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator[Commit]:
-        """An iterator of Commit objects representing the history of a given ref/commit.
+        """An iterator of :class:`~git.objects.commit.Commit` objects representing the
+        history of a given ref/commit.
 
         :param rev:
-            Revision specifier, see git-rev-parse for viable options.
+            Revision specifier, see ``git rev-parse`` for viable options.
             If None, the active branch will be used.
 
         :param paths:
-            An optional path or a list of paths; if set only commits that include the
-            path or paths will be returned
+            An optional path or a list of paths. If set, only commits that include the
+            path or paths will be returned.
 
         :param kwargs:
-            Arguments to be passed to git-rev-list - common ones are max_count and skip.
+            Arguments to be passed to ``git rev-list``.
+            Common ones are ``max_count`` and ``skip``.
 
-        :note: To receive only commits between two named revisions, use the
+        :note:
+            To receive only commits between two named revisions, use the
             ``"revA...revB"`` revision specifier.
 
-        :return: Iterator of ``git.Commit``
+        :return:
+            Iterator of  :class:`~git.objects.commit.Commit` objects
         """
         if rev is None:
             rev = self.head.commit
@@ -716,16 +767,25 @@ def iter_commits(
         return Commit.iter_items(self, rev, paths, **kwargs)
 
     def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
-        """Find the closest common ancestor for the given revision (Commits, Tags, References, etc.).
+        R"""Find the closest common ancestor for the given revision
+        (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
+        :class:`git.refs.reference.Reference`\s, etc.).
+
+        :param rev:
+            At least two revs to find the common ancestor for.
+
+        :param kwargs:
+            Additional arguments to be passed to the ``repo.git.merge_base()`` command
+            which does all the work.
 
-        :param rev: At least two revs to find the common ancestor for.
-        :param kwargs: Additional arguments to be passed to the
-            ``repo.git.merge_base()`` command which does all the work.
-        :return: A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
+        :return:
+            A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
             not passed as a keyword argument, the list will have at max one
             :class:`~git.objects.commit.Commit`, or is empty if no common merge base
             exists.
-        :raises ValueError: If not at least two revs are provided.
+
+        :raises ValueError:
+            If fewer than two revisions are provided.
         """
         if len(rev) < 2:
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
@@ -752,9 +812,14 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
     def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool:
         """Check if a commit is an ancestor of another.
 
-        :param ancestor_rev: Rev which should be an ancestor
-        :param rev: Rev to test against ancestor_rev
-        :return: ``True``, ancestor_rev is an ancestor to rev.
+        :param ancestor_rev:
+            Rev which should be an ancestor.
+
+        :param rev:
+            Rev to test against `ancestor_rev`.
+
+        :return:
+            True if `ancestor_rev` is an ancestor to `rev`.
         """
         try:
             self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
@@ -809,7 +874,8 @@ def _set_daemon_export(self, value: object) -> None:
     def _get_alternates(self) -> List[str]:
         """The list of alternates for this repo from which objects can be retrieved.
 
-        :return: List of strings being pathnames of alternates
+        :return:
+            List of strings being pathnames of alternates
         """
         if self.git_dir:
             alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
@@ -821,17 +887,17 @@ def _get_alternates(self) -> List[str]:
         return []
 
     def _set_alternates(self, alts: List[str]) -> None:
-        """Sets the alternates.
+        """Set the alternates.
 
         :param alts:
-            is the array of string paths representing the alternates at which
-            git should look for objects, i.e. /home/user/repo/.git/objects
+            The array of string paths representing the alternates at which git should
+            look for objects, i.e. ``/home/user/repo/.git/objects``.
 
         :raise NoSuchPathError:
 
         :note:
-            The method does not check for the existence of the paths in alts
-            as the caller is responsible.
+            The method does not check for the existence of the paths in `alts`, as the
+            caller is responsible.
         """
         alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
         if not alts:
@@ -857,9 +923,9 @@ def is_dirty(
     ) -> bool:
         """
         :return:
-            ``True`` if the repository is considered dirty. By default it will react
-            like a git-status without untracked files, hence it is dirty if the
-            index or the working copy have changes.
+            True if the repository is considered dirty. By default it will react like a
+            git-status without untracked files, hence it is dirty if the index or the
+            working copy have changes.
         """
         if self._bare:
             # Bare repositories with no associated working directory are
@@ -894,11 +960,12 @@ def untracked_files(self) -> List[str]:
         :return:
             list(str,...)
 
-            Files currently untracked as they have not been staged yet. Paths
-            are relative to the current working directory of the git command.
+            Files currently untracked as they have not been staged yet. Paths are
+            relative to the current working directory of the git command.
 
         :note:
             Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
+
         :note:
             This property is expensive, as no cache is involved. To process the result,
             please consider caching it yourself.
@@ -926,23 +993,25 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
         return untracked_files
 
     def ignored(self, *paths: PathLike) -> List[str]:
-        """Checks if paths are ignored via .gitignore.
+        """Checks if paths are ignored via ``.gitignore``.
 
         This does so using the ``git check-ignore`` method.
 
-        :param paths: List of paths to check whether they are ignored or not
+        :param paths:
+            List of paths to check whether they are ignored or not.
 
-        :return: Subset of those paths which are ignored
+        :return:
+            Subset of those paths which are ignored
         """
         try:
             proc: str = self.git.check_ignore(*paths)
         except GitCommandError as err:
-            # If return code is 1, this means none of the items in *paths
-            # are ignored by Git, so return an empty list.  Raise the
-            # exception on all other return codes.
             if err.status == 1:
+                # If return code is 1, this means none of the items in *paths are
+                # ignored by Git, so return an empty list.
                 return []
             else:
+                # Raise the exception on all other return codes.
                 raise
 
         return proc.replace("\\\\", "\\").replace('"', "").split("\n")
@@ -951,8 +1020,11 @@ def ignored(self, *paths: PathLike) -> List[str]:
     def active_branch(self) -> Head:
         """The name of the currently active branch.
 
-        :raises	TypeError: If HEAD is detached
-        :return: Head to the active branch
+        :raises	TypeError:
+            If HEAD is detached.
+
+        :return:
+            Head to the active branch
         """
         # reveal_type(self.head.reference)  # => Reference
         return self.head.reference
@@ -963,13 +1035,15 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
         Unlike :meth:`blame`, this does not return the actual file's contents, only a
         stream of :class:`BlameEntry` tuples.
 
-        :param rev: Revision specifier. If `None`, the blame will include all the latest
-            uncommitted changes. Otherwise, anything succesfully parsed by git-rev-parse
-            is a valid option.
+        :param rev:
+            Revision specifier. If None, the blame will include all the latest
+            uncommitted changes. Otherwise, anything successfully parsed by ``git
+            rev-parse`` is a valid option.
 
-        :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates
-            the commit to blame for the line, and range indicates a span of line numbers
-            in the resulting file.
+        :return:
+            Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
+            commit to blame for the line, and range indicates a span of line numbers in
+            the resulting file.
 
         If you combine all line number ranges outputted by this command, you should get
         a continuous range spanning all line numbers in the file.
@@ -981,7 +1055,8 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
         stream = (line for line in data.split(b"\n") if line)
         while True:
             try:
-                line = next(stream)  # When exhausted, causes a StopIteration, terminating this function.
+                # When exhausted, causes a StopIteration, terminating this function.
+                line = next(stream)
             except StopIteration:
                 return
             split_line = line.split()
@@ -990,8 +1065,8 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
             num_lines = int(num_lines_b)
             orig_lineno = int(orig_lineno_b)
             if hexsha not in commits:
-                # Now read the next few lines and build up a dict of properties
-                # for this commit.
+                # Now read the next few lines and build up a dict of properties for this
+                # commit.
                 props: Dict[bytes, bytes] = {}
                 while True:
                     try:
@@ -999,8 +1074,8 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
                     except StopIteration:
                         return
                     if line == b"boundary":
-                        # "boundary" indicates a root commit and occurs
-                        # instead of the "previous" tag.
+                        # "boundary" indicates a root commit and occurs instead of the
+                        # "previous" tag.
                         continue
 
                     tag, value = line.split(b" ", 1)
@@ -1026,11 +1101,12 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
                 )
                 commits[hexsha] = c
             else:
-                # Discard all lines until we find "filename" which is
-                # guaranteed to be the last line.
+                # Discard all lines until we find "filename" which is guaranteed to be
+                # the last line.
                 while True:
                     try:
-                        line = next(stream)  # Will fail if we reach the EOF unexpectedly.
+                        # Will fail if we reach the EOF unexpectedly.
+                        line = next(stream)
                     except StopIteration:
                         return
                     tag, value = line.split(b" ", 1)
@@ -1055,16 +1131,18 @@ def blame(
     ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
         """The blame information for the given file at the given revision.
 
-        :param rev: Revision specifier. If `None`, the blame will include all the latest
-            uncommitted changes. Otherwise, anything succesfully parsed by git-rev-parse
-            is a valid option.
+        :param rev:
+            Revision specifier. If None, the blame will include all the latest
+            uncommitted changes. Otherwise, anything successfully parsed by
+            git-rev-parse is a valid option.
 
         :return:
             list: [git.Commit, list: [<line>]]
 
-            A list of lists associating a Commit object with a list of lines that
-            changed within the given commit. The Commit objects will be given in order
-            of appearance.
+            A list of lists associating a :class:`~git.objects.commit.Commit` object
+            with a list of lines that changed within the given commit. The
+            :class:`~git.objects.commit.Commit` objects will be given in order of
+            appearance.
         """
         if incremental:
             return self.blame_incremental(rev, file, **kwargs)
@@ -1096,10 +1174,11 @@ class InfoTD(TypedDict, total=False):
                 parts = []
                 is_binary = True
             else:
-                # As we don't have an idea when the binary data ends, as it could contain multiple newlines
-                # in the process. So we rely on being able to decode to tell us what it is.
-                # This can absolutely fail even on text files, but even if it does, we should be fine treating it
-                # as binary instead.
+                # As we don't have an idea when the binary data ends, as it could
+                # contain multiple newlines in the process. So we rely on being able to
+                # decode to tell us what it is. This can absolutely fail even on text
+                # files, but even if it does, we should be fine treating it as binary
+                # instead.
                 parts = self.re_whitespace.split(line_str, 1)
                 firstpart = parts[0]
                 is_binary = False
@@ -1180,10 +1259,12 @@ class InfoTD(TypedDict, total=False):
                                     line = line_str
                                 else:
                                     line = line_bytes
-                                    # NOTE: We are actually parsing lines out of binary data, which can lead to the
-                                    # binary being split up along the newline separator. We will append this to the
-                                    # blame we are currently looking at, even though it should be concatenated with
-                                    # the last line we have seen.
+                                    # NOTE: We are actually parsing lines out of binary
+                                    # data, which can lead to the binary being split up
+                                    # along the newline separator. We will append this
+                                    # to the blame we are currently looking at, even
+                                    # though it should be concatenated with the last
+                                    # line we have seen.
                                 blames[-1][1].append(line)
 
                             info = {"id": sha}
@@ -1205,28 +1286,30 @@ def init(
         """Initialize a git repository at the given path if specified.
 
         :param path:
-            The full path to the repo (traditionally ends with /<name>.git) or None in
-            which case the repository will be created in the current working directory
+            The full path to the repo (traditionally ends with ``/<name>.git``).
+            Or None, in which case the repository will be created in the current working
+            directory.
 
         :param mkdir:
-            If specified, will create the repository directory if it doesn't
-            already exist. Creates the directory with a mode=0755.
+            If specified, will create the repository directory if it doesn't already
+            exist. Creates the directory with a mode=0755.
             Only effective if a path is explicitly given.
 
         :param odbt:
-            Object DataBase type - a type which is constructed by providing
-            the directory containing the database objects, i.e. .git/objects.
-            It will be used to access all object data.
+            Object DataBase type - a type which is constructed by providing the
+            directory containing the database objects, i.e. ``.git/objects``. It will be
+            used to access all object data.
 
         :param expand_vars:
-            If specified, environment variables will not be escaped. This
-            can lead to information disclosure, allowing attackers to
-            access the contents of environment variables.
+            If specified, environment variables will not be escaped. This can lead to
+            information disclosure, allowing attackers to access the contents of
+            environment variables.
 
         :param kwargs:
-            Keyword arguments serving as additional options to the git-init command.
+            Keyword arguments serving as additional options to the ``git init`` command.
 
-        :return: ``git.Repo`` (the newly created repo)
+        :return:
+            :class:`Repo` (the newly created repo)
         """
         if path:
             path = expand_path(path, expand_vars)
@@ -1253,7 +1336,7 @@ def _clone(
     ) -> "Repo":
         odbt = kwargs.pop("odbt", odb_default_type)
 
-        # When pathlib.Path or other classbased path is passed
+        # When pathlib.Path or other class-based path is passed
         if not isinstance(path, str):
             path = str(path)
 
@@ -1315,11 +1398,10 @@ def _clone(
         # Retain env values that were passed to _clone().
         repo.git.update_environment(**git.environment())
 
-        # Adjust remotes - there may be operating systems which use backslashes,
-        # These might be given as initial paths, but when handling the config file
-        # that contains the remote from which we were clones, git stops liking it
-        # as it will escape the backslashes. Hence we undo the escaping just to be
-        # sure.
+        # Adjust remotes - there may be operating systems which use backslashes, These
+        # might be given as initial paths, but when handling the config file that
+        # contains the remote from which we were clones, git stops liking it as it will
+        # escape the backslashes. Hence we undo the escaping just to be sure.
         if repo.remotes:
             with repo.remotes[0].config_writer as writer:
                 writer.set_value("url", Git.polish_url(repo.remotes[0].url))
@@ -1337,21 +1419,38 @@ def clone(
     ) -> "Repo":
         """Create a clone from this repository.
 
-        :param path: The full path of the new repo (traditionally ends with
-            ``./<name>.git``).
-        :param progress: See :meth:`git.remote.Remote.push`.
-        :param multi_options: A list of Clone options that can be provided multiple times.
+        :param path:
+            The full path of the new repo (traditionally ends with ``./<name>.git``).
+
+        :param progress:
+            See :meth:`Remote.push <git.remote.Remote.push>`.
+
+        :param multi_options:
+            A list of ``git clone`` options that can be provided multiple times.
+
             One option per list item which is passed exactly as specified to clone.
-            For example: ['--config core.filemode=false', '--config core.ignorecase',
-            '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
+            For example::
+
+                [
+                    "--config core.filemode=false",
+                    "--config core.ignorecase",
+                    "--recurse-submodule=repo1_path",
+                    "--recurse-submodule=repo2_path",
+                ]
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
+
         :param kwargs:
             * odbt = ObjectDatabase Type, allowing to determine the object database
               implementation used by the returned Repo instance.
-            * All remaining keyword arguments are given to the git-clone command.
+            * All remaining keyword arguments are given to the ``git clone`` command.
 
-        :return: :class:`Repo` (the newly cloned repo)
+        :return:
+            :class:`Repo` (the newly cloned repo)
         """
         return self._clone(
             self.git,
@@ -1379,29 +1478,38 @@ def clone_from(
     ) -> "Repo":
         """Create a clone from the given URL.
 
-        :param url: Valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
+        :param url:
+            Valid git url, see:
+            http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
 
-        :param to_path: Path to which the repository should be cloned to.
+        :param to_path:
+            Path to which the repository should be cloned to.
 
-        :param progress: See :meth:`git.remote.Remote.push`.
+        :param progress:
+            See :meth:`Remote.push <git.remote.Remote.push>`.
 
-        :param env: Optional dictionary containing the desired environment variables.
+        :param env:
+            Optional dictionary containing the desired environment variables.
 
-            Note: Provided variables will be used to update the execution
-            environment for `git`. If some variable is not specified in `env`
-            and is defined in `os.environ`, value from `os.environ` will be used.
-            If you want to unset some variable, consider providing empty string
-            as its value.
+            Note: Provided variables will be used to update the execution environment
+            for ``git``. If some variable is not specified in `env` and is defined in
+            :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want
+            to unset some variable, consider providing empty string as its value.
 
-        :param multi_options: See :meth:`clone` method.
+        :param multi_options:
+            See the :meth:`clone` method.
 
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
 
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack.
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
 
-        :param kwargs: See the :meth:`clone` method.
+        :param kwargs:
+            See the :meth:`clone` method.
 
-        :return: :class:`Repo` instance pointing to the cloned directory.
+        :return:
+            :class:`Repo` instance pointing to the cloned directory.
         """
         git = cls.GitCommandWrapperType(os.getcwd())
         if env is not None:
@@ -1459,10 +1567,14 @@ def archive(
 
     def has_separate_working_tree(self) -> bool:
         """
-        :return: True if our git_dir is not at the root of our working_tree_dir, but a .git file with a
-            platform agnositic symbolic link. Our git_dir will be wherever the .git file points to.
+        :return:
+            True if our :attr:`git_dir` is not at the root of our
+            :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic
+            symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points
+            to.
 
-        :note: bare repositories will always return False here
+        :note:
+            Bare repositories will always return False here.
         """
         if self.bare:
             return False
@@ -1479,7 +1591,8 @@ def __repr__(self) -> str:
 
     def currently_rebasing_on(self) -> Commit | None:
         """
-        :return: The commit which is currently being replayed while rebasing.
+        :return:
+            The commit which is currently being replayed while rebasing.
 
             None if we are not currently rebasing.
         """
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 63bcfdfc7..1a53a6825 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -57,13 +57,13 @@ def touch(filename: str) -> str:
 
 
 def is_git_dir(d: "PathLike") -> bool:
-    """This is taken from the git setup.c:is_git_directory
-    function.
+    """This is taken from the git setup.c:is_git_directory function.
 
-    @throws WorkTreeRepositoryUnsupported if it sees a worktree directory. It's quite hacky to do that here,
-            but at least clearly indicates that we don't support it.
-            There is the unlikely danger to throw if we see directories which just look like a worktree dir,
-            but are none."""
+    :raises WorkTreeRepositoryUnsupported:
+        If it sees a worktree directory. It's quite hacky to do that here, but at least
+        clearly indicates that we don't support it. There is the unlikely danger to
+        throw if we see directories which just look like a worktree dir, but are none.
+    """
     if osp.isdir(d):
         if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
             osp.join(d, "refs")
@@ -107,15 +107,15 @@ def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
         with open(d) as fp:
             content = fp.read().rstrip()
     except IOError:
-        # it's probably not a file
+        # It's probably not a file.
         pass
     else:
         if content.startswith("gitdir: "):
             path = content[8:]
 
             if Git.is_cygwin():
-                ## Cygwin creates submodules prefixed with `/cygdrive/...` suffixes.
-                # Cygwin git understands Cygwin paths much better than Windows ones
+                # Cygwin creates submodules prefixed with `/cygdrive/...` suffixes.
+                # Cygwin git understands Cygwin paths much better than Windows ones.
                 # Also the Cygwin tests are assuming Cygwin paths.
                 path = cygpath(path)
             if not osp.isabs(path):
@@ -126,9 +126,14 @@ def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
 
 
 def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
-    """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha
-        or None if no candidate could be found.
-    :param hexsha: hexsha with less than 40 byte"""
+    """
+    :return:
+        Long hexadecimal sha1 from the given less than 40 byte hexsha or None if no
+        candidate could be found.
+
+    :param hexsha:
+        hexsha with less than 40 bytes.
+    """
     try:
         return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha))
     except BadObject:
@@ -140,25 +145,29 @@ def name_to_object(
     repo: "Repo", name: str, return_ref: bool = False
 ) -> Union[SymbolicReference, "Commit", "TagObject", "Blob", "Tree"]:
     """
-    :return: object specified by the given name, hexshas ( short and long )
-        as well as references are supported
-    :param return_ref: if name specifies a reference, we will return the reference
-        instead of the object. Otherwise it will raise BadObject or BadName
+    :return:
+        Object specified by the given name - hexshas (short and long) as well as
+        references are supported.
+
+    :param return_ref:
+        If True, and name specifies a reference, we will return the reference
+        instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` o
+        `~gitdb.exc.BadName`.
     """
     hexsha: Union[None, str, bytes] = None
 
-    # is it a hexsha ? Try the most common ones, which is 7 to 40
+    # Is it a hexsha? Try the most common ones, which is 7 to 40.
     if repo.re_hexsha_shortened.match(name):
         if len(name) != 40:
-            # find long sha for short sha
+            # Find long sha for short sha.
             hexsha = short_to_long(repo.odb, name)
         else:
             hexsha = name
         # END handle short shas
     # END find sha if it matches
 
-    # if we couldn't find an object for what seemed to be a short hexsha
-    # try to find it as reference anyway, it could be named 'aaa' for instance
+    # If we couldn't find an object for what seemed to be a short hexsha, try to find it
+    # as reference anyway, it could be named 'aaa' for instance.
     if hexsha is None:
         for base in (
             "%s",
@@ -179,12 +188,12 @@ def name_to_object(
         # END for each base
     # END handle hexsha
 
-    # didn't find any ref, this is an error
+    # Didn't find any ref, this is an error.
     if return_ref:
         raise BadObject("Couldn't find reference named %r" % name)
     # END handle return ref
 
-    # tried everything ? fail
+    # Tried everything ? fail.
     if hexsha is None:
         raise BadName(name)
     # END assert hexsha was found
@@ -216,17 +225,27 @@ def to_commit(obj: Object) -> Union["Commit", "TagObject"]:
 
 def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
     """
-    :return: Object at the given revision, either Commit, Tag, Tree or Blob
-    :param rev: git-rev-parse compatible revision specification as string, please see
-        http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html
-        for details
-    :raise BadObject: if the given revision could not be found
-    :raise ValueError: If rev couldn't be parsed
-    :raise IndexError: If invalid reflog index is specified"""
-
-    # colon search mode ?
+    :return:
+        `~git.objects.base.Object` at the given revision, either
+        `~git.objects.commit.Commit`, `~git.refs.tag.Tag`, `~git.objects.tree.Tree` or
+        `~git.objects.blob.Blob`.
+
+    :param rev:
+        ``git rev-parse``-compatible revision specification as string. Please see
+        http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html for details.
+
+    :raise BadObject:
+        If the given revision could not be found.
+
+    :raise ValueError:
+        If `rev` couldn't be parsed.
+
+    :raise IndexError:
+        If an invalid reflog index is specified.
+    """
+    # Are we in colon search mode?
     if rev.startswith(":/"):
-        # colon search mode
+        # Colon search mode
         raise NotImplementedError("commit by message search ( regex )")
     # END handle search
 
@@ -245,7 +264,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         token = rev[start]
 
         if obj is None:
-            # token is a rev name
+            # token is a rev name.
             if start == 0:
                 ref = repo.head.ref
             else:
@@ -265,29 +284,29 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
 
         start += 1
 
-        # try to parse {type}
+        # Try to parse {type}.
         if start < lr and rev[start] == "{":
             end = rev.find("}", start)
             if end == -1:
                 raise ValueError("Missing closing brace to define type in %s" % rev)
-            output_type = rev[start + 1 : end]  # exclude brace
+            output_type = rev[start + 1 : end]  # Exclude brace.
 
-            # handle type
+            # Handle type.
             if output_type == "commit":
-                pass  # default
+                pass  # Default.
             elif output_type == "tree":
                 try:
                     obj = cast(Commit_ish, obj)
                     obj = to_commit(obj).tree
                 except (AttributeError, ValueError):
-                    pass  # error raised later
+                    pass  # Error raised later.
                 # END exception handling
             elif output_type in ("", "blob"):
                 obj = cast("TagObject", obj)
                 if obj and obj.type == "tag":
                     obj = deref_tag(obj)
                 else:
-                    # cannot do anything for non-tags
+                    # Cannot do anything for non-tags.
                     pass
                 # END handle tag
             elif token == "@":
@@ -295,11 +314,10 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 assert ref is not None, "Require Reference to access reflog"
                 revlog_index = None
                 try:
-                    # transform reversed index into the format of our revlog
+                    # Transform reversed index into the format of our revlog.
                     revlog_index = -(int(output_type) + 1)
                 except ValueError as e:
-                    # TODO: Try to parse the other date options, using parse_date
-                    # maybe
+                    # TODO: Try to parse the other date options, using parse_date maybe.
                     raise NotImplementedError("Support for additional @{...} modes not implemented") from e
                 # END handle revlog index
 
@@ -311,23 +329,24 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
 
                 obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
 
-                # make it pass the following checks
+                # Make it pass the following checks.
                 output_type = ""
             else:
                 raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
             # END handle output type
 
-            # empty output types don't require any specific type, its just about dereferencing tags
+            # Empty output types don't require any specific type, its just about
+            # dereferencing tags.
             if output_type and obj and obj.type != output_type:
                 raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type))
             # END verify output type
 
-            start = end + 1  # skip brace
+            start = end + 1  # Skip brace.
             parsed_to = start
             continue
         # END parse type
 
-        # try to parse a number
+        # Try to parse a number.
         num = 0
         if token != ":":
             found_digit = False
@@ -341,15 +360,14 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 # END handle number
             # END number parse loop
 
-            # no explicit number given, 1 is the default
-            # It could be 0 though
+            # No explicit number given, 1 is the default. It could be 0 though.
             if not found_digit:
                 num = 1
             # END set default num
         # END number parsing only if non-blob mode
 
         parsed_to = start
-        # handle hierarchy walk
+        # Handle hierarchy walk.
         try:
             obj = cast(Commit_ish, obj)
             if token == "~":
@@ -359,7 +377,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 # END for each history item to walk
             elif token == "^":
                 obj = to_commit(obj)
-                # must be n'th parent
+                # Must be n'th parent.
                 if num:
                     obj = obj.parents[num - 1]
             elif token == ":":
@@ -378,7 +396,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         # END exception handling
     # END parse loop
 
-    # still no obj ? Its probably a simple name
+    # Still no obj? It's probably a simple name.
     if obj is None:
         obj = cast(Commit_ish, name_to_object(repo, rev))
         parsed_to = lr

From c67b2e2a05be13c70a72147b9553348cdc5217a9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 19:55:36 -0500
Subject: [PATCH 497/994] Adjust spacing in colon seach mode
 NotImplementedError

This is a (very) minor improvement that uses the more common
convention of not padding parentheses on the inside with spaces in
prose, in an exception message.
---
 git/repo/fun.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/repo/fun.py b/git/repo/fun.py
index 1a53a6825..b080cac5b 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -246,7 +246,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
     # Are we in colon search mode?
     if rev.startswith(":/"):
         # Colon search mode
-        raise NotImplementedError("commit by message search ( regex )")
+        raise NotImplementedError("commit by message search (regex)")
     # END handle search
 
     obj: Union[Commit_ish, "Reference", None] = None

From 5ee87441a5c936a55c69e310e4c8812b635cbdf0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 20:10:33 -0500
Subject: [PATCH 498/994] Update git source link in Repo.merge_base comment

And link to a specific tag (the most recent stable version tag) so
the the hyperlink won't break in the future (as long as GitHub URLs
keep working).
---
 git/repo/base.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index afbc1d75d..a0266f090 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -798,8 +798,8 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
             if err.status == 128:
                 raise
             # END handle invalid rev
-            # Status code 1 is returned if there is no merge-base
-            # (see https://github.com/git/git/blob/master/builtin/merge-base.c#L16)
+            # Status code 1 is returned if there is no merge-base.
+            # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19)
             return res
         # END exception handling
 

From c8b6cf0cd96e6c0129aea563cb5092d6658f4424 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 20:32:14 -0500
Subject: [PATCH 499/994] Update comment about improving expand_path overloads

---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 72861ef84..4929affeb 100644
--- a/git/util.py
+++ b/git/util.py
@@ -500,7 +500,7 @@ def expand_path(p: None, expand_vars: bool = ...) -> None:
 
 @overload
 def expand_path(p: PathLike, expand_vars: bool = ...) -> str:
-    # improve these overloads when 3.5 dropped
+    # TODO: Support for Python 3.5 has been dropped, so these overloads can be improved.
     ...
 
 

From bcc0c27482a46ff030cbddf75feac070f169263e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 20:38:15 -0500
Subject: [PATCH 500/994] Fix recent inconsistency, using :raise:, not :raises:

GitPython used :raise: consistently before, but I recently
introduced some occurrences of :raises: by accident. This changes
those to :raise:.
---
 git/index/fun.py | 2 +-
 git/repo/base.py | 2 +-
 git/repo/fun.py  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/index/fun.py b/git/index/fun.py
index 785a1c748..a60a3b809 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -88,7 +88,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     :param args:
         Arguments passed to hook file.
 
-    :raises HookExecutionError:
+    :raise HookExecutionError:
     """
     hp = hook_path(name, index.repo.git_dir)
     if not os.access(hp, os.X_OK):
diff --git a/git/repo/base.py b/git/repo/base.py
index a0266f090..1e58a24d0 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -784,7 +784,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
             :class:`~git.objects.commit.Commit`, or is empty if no common merge base
             exists.
 
-        :raises ValueError:
+        :raise ValueError:
             If fewer than two revisions are provided.
         """
         if len(rev) < 2:
diff --git a/git/repo/fun.py b/git/repo/fun.py
index b080cac5b..fa2cebb67 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -59,7 +59,7 @@ def touch(filename: str) -> str:
 def is_git_dir(d: "PathLike") -> bool:
     """This is taken from the git setup.c:is_git_directory function.
 
-    :raises WorkTreeRepositoryUnsupported:
+    :raise WorkTreeRepositoryUnsupported:
         If it sees a worktree directory. It's quite hacky to do that here, but at least
         clearly indicates that we don't support it. There is the unlikely danger to
         throw if we see directories which just look like a worktree dir, but are none.

From 0231b745b0005cb795a1a08493de018590daadef Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 21:09:57 -0500
Subject: [PATCH 501/994] Further revise docstrings in
 git.objects.submodule.base

I had missed a lot of stuff there in 63c62ed.
---
 git/objects/submodule/base.py | 447 ++++++++++++++++++++++------------
 1 file changed, 289 insertions(+), 158 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 58b474fdd..70a021c54 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -133,8 +133,9 @@ def __init__(
         :param url:
             The URL to the remote repository which is the submodule.
 
-        :param branch_path: Complete relative path to ref to checkout when cloning the
-            remote repository.
+        :param branch_path:
+            Complete relative path to ref to checkout when cloning the remote
+            repository.
         """
         super().__init__(repo, binsha, mode, path)
         self.size = 0
@@ -214,10 +215,12 @@ def _config_parser(
         cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool
     ) -> SubmoduleConfigParser:
         """
-        :return: Config Parser constrained to our submodule in read or write mode
+        :return:
+            Config parser constrained to our submodule in read or write mode
 
-        :raise IOError: If the .gitmodules file cannot be found, either locally or in
-            the repository at the given parent commit. Otherwise the exception would be
+        :raise IOError:
+            If the ``.gitmodules`` file cannot be found, either locally or in the
+            repository at the given parent commit. Otherwise the exception would be
             delayed until the first access of the config parser.
         """
         parent_matches_head = True
@@ -225,7 +228,8 @@ def _config_parser(
             try:
                 parent_matches_head = repo.head.commit == parent_commit
             except ValueError:
-                # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref.
+                # We are most likely in an empty repository, so the HEAD doesn't point
+                # to a valid ref.
                 pass
         # END handle parent_commit
         fp_module: Union[str, BytesIO]
@@ -260,13 +264,17 @@ def _clear_cache(self) -> None:
 
     @classmethod
     def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
-        """:return: Configuration file as BytesIO - we only access it through the respective blob's data"""
+        """
+        :return:
+            Configuration file as BytesIO - we only access it through the respective
+            blob's data
+        """
         sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
         sio.name = cls.k_modules_file
         return sio
 
     def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
-        """:return: Config Parser constrained to our submodule in read or write mode"""
+        """:return: Config parser constrained to our submodule in read or write mode"""
         try:
             pc: Union["Commit_ish", None] = self.parent_commit
         except ValueError:
@@ -296,14 +304,29 @@ def _clone_repo(
         **kwargs: Any,
     ) -> "Repo":
         """
-        :return: Repo instance of newly cloned repository
-        :param repo: Our parent repository
-        :param url: URL to clone from
-        :param path: Repository - relative path to the submodule checkout location
-        :param name: Canonical name of the submodule
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :param kwargs: Additional arguments given to git.clone
+        :return:
+            :class:`~git.repo.base.Repo` instance of newly cloned repository.
+
+        :param repo:
+            Our parent repository.
+
+        :param url:
+            URL to clone from.
+
+        :param path:
+            Repository-relative path to the submodule checkout location.
+
+        :param name:
+            Canonical name of the submodule.
+
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
+
+        :param kwargs:
+            Additional arguments given to ``git clone``
         """
         module_abspath = cls._module_abspath(repo, path, name)
         module_checkout_path = module_abspath
@@ -328,8 +351,11 @@ def _clone_repo(
 
     @classmethod
     def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
-        """:return: a path guaranteed  to be relative to the given parent - repository
-        :raise ValueError: if path is not contained in the parent repository's working tree"""
+        """:return: a path guaranteed to be relative to the given parent repository
+
+        :raise ValueError:
+            If path is not contained in the parent repository's working tree
+        """
         path = to_native_path_linux(path)
         if path.endswith("/"):
             path = path[:-1]
@@ -352,16 +378,26 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
 
     @classmethod
     def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
-        """Write a .git file containing a(preferably) relative path to the actual git module repository.
+        """Write a .git file containing a (preferably) relative path to the actual git
+        module repository.
+
+        It is an error if the `module_abspath` cannot be made into a relative path,
+        relative to the `working_tree_dir`.
+
+        :note:
+            This will overwrite existing files!
+
+        :note:
+            As we rewrite both the git file as well as the module configuration, we
+            might fail on the configuration and will not roll back changes done to the
+            git file. This should be a non-issue, but may easily be fixed if it becomes
+            one.
 
-        It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir
+        :param working_tree_dir:
+            Directory to write the ``.git`` file into.
 
-        :note: This will overwrite existing files!
-        :note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration
-            and will not roll back changes done to the git file. This should be a non - issue, but may easily be fixed
-            if it becomes one.
-        :param working_tree_dir: Directory to write the .git file into
-        :param module_abspath: Absolute path to the bare repository
+        :param module_abspath:
+            Absolute path to the bare repository.
         """
         git_file = osp.join(working_tree_dir, ".git")
         rela_path = osp.relpath(module_abspath, start=working_tree_dir)
@@ -395,54 +431,77 @@ def add(
         allow_unsafe_protocols: bool = False,
     ) -> "Submodule":
         """Add a new submodule to the given repository. This will alter the index
-        as well as the .gitmodules file, but will not create a new commit.
+        as well as the ``.gitmodules`` file, but will not create a new commit.
         If the submodule already exists, no matter if the configuration differs
         from the one provided, the existing submodule will be returned.
 
-        :param repo: Repository instance which should receive the submodule.
-        :param name: The name/identifier for the submodule.
-        :param path: Repository-relative or absolute path at which the submodule
-            should be located.
+        :param repo:
+            Repository instance which should receive the submodule.
+
+        :param name:
+            The name/identifier for the submodule.
+
+        :param path:
+            Repository-relative or absolute path at which the submodule should be
+            located.
             It will be created as required during the repository initialization.
-        :param url: git-clone compatible URL, see git-clone reference for more information.
-            If None, the repository is assumed to exist, and the url of the first
-            remote is taken instead. This is useful if you want to make an existing
-            repository a submodule of another one.
-        :param branch: name of branch at which the submodule should (later) be checked out.
-            The given branch must exist in the remote repository, and will be checked
-            out locally as a tracking branch.
-            It will only be written into the configuration if it not None, which is
-            when the checked out branch will be the one the remote HEAD pointed to.
-            The result you get in these situation is somewhat fuzzy, and it is recommended
-            to specify at least 'master' here.
-            Examples are 'master' or 'feature/new'.
-        :param no_checkout: If True, and if the repository has to be cloned manually,
-            no checkout will be performed.
-        :param depth: Create a shallow clone with a history truncated to the
-            specified number of commits.
-        :param env: Optional dictionary containing the desired environment variables.
+
+        :param url:
+            git-clone compatible URL, see git-clone reference for more information.
+            If None, the repository is assumed to exist, and the url of the first remote
+            is taken instead. This is useful if you want to make an existing repository
+            a submodule of another one.
+
+        :param branch:
+            Name of branch at which the submodule should (later) be checked out. The
+            given branch must exist in the remote repository, and will be checked out
+            locally as a tracking branch.
+            It will only be written into the configuration if it not None, which is when
+            the checked out branch will be the one the remote HEAD pointed to.
+            The result you get in these situation is somewhat fuzzy, and it is
+            recommended to specify at least ``master`` here.
+            Examples are ``master`` or ``feature/new``.
+
+        :param no_checkout:
+            If True, and if the repository has to be cloned manually, no checkout will
+            be performed.
+
+        :param depth:
+            Create a shallow clone with a history truncated to the specified number of
+            commits.
+
+        :param env:
+            Optional dictionary containing the desired environment variables.
+
             Note: Provided variables will be used to update the execution environment
             for ``git``. If some variable is not specified in `env` and is defined in
             attr:`os.environ`, the value from attr:`os.environ` will be used. If you
             want to unset some variable, consider providing an empty string as its
             value.
+
         :param clone_multi_options: A list of Clone options. Please see
             :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
-        :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext.
-        :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack
-        :return: The newly created submodule instance.
-        :note: Works atomically, such that no change will be done if the repository
-            update fails for instance.
-        """
 
+        :param allow_unsafe_protocols:
+            Allow unsafe protocols to be used, like ``ext``.
+
+        :param allow_unsafe_options:
+            Allow unsafe options to be used, like ``--upload-pack``.
+
+        :return:
+            The newly created :class:`Submodule` instance.
+
+        :note:
+            Works atomically, such that no change will be done if, for example, the
+            repository update fails.
+        """
         if repo.bare:
             raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
         # END handle bare repos
 
         path = cls._to_relative_path(repo, path)
 
-        # Ensure we never put backslashes into the URL, as some operating systems
-        # like it...
+        # Ensure we never put backslashes into the URL, as might happen on Windows.
         if url is not None:
             url = to_native_path_linux(url)
         # END ensure URL correctness
@@ -569,24 +628,29 @@ def update(
         allow_unsafe_options: bool = False,
         allow_unsafe_protocols: bool = False,
     ) -> "Submodule":
-        """Update the repository of this submodule to point to the checkout
-        we point at with the binsha of this instance.
+        """Update the repository of this submodule to point to the checkout we point at
+        with the binsha of this instance.
 
         :param recursive:
             If True, we will operate recursively and update child modules as well.
+
         :param init:
             If True, the module repository will be cloned into place if necessary.
+
         :param to_latest_revision:
             If True, the submodule's sha will be ignored during checkout. Instead, the
             remote will be fetched, and the local tracking branch updated. This only
             works if we have a local tracking branch, which is the case if the remote
             repository had a master branch, or of the 'branch' option was specified for
             this submodule and the branch existed remotely.
+
         :param progress:
-            UpdateProgress instance or None if no progress should be shown.
+            :class:`UpdateProgress` instance, or None if no progress should be shown.
+
         :param dry_run:
             If True, the operation will only be simulated, but not performed.
             All performed operations are read-only.
+
         :param force:
             If True, we may reset heads even if the repository in question is dirty.
             Additionally we will be allowed to set a tracking branch which is ahead of
@@ -594,31 +658,40 @@ def update(
             This will essentially 'forget' commits.
             If False, local tracking branches that are in the future of their respective
             remote branches will simply not be moved.
+
         :param keep_going:
             If True, we will ignore but log all errors, and keep going recursively.
-            Unless dry_run is set as well, keep_going could cause subsequent / inherited
-            errors you wouldn't see otherwise.
-            In conjunction with dry_run, it can be useful to anticipate all errors when
-            updating submodules.
+            Unless `dry_run` is set as well, `keep_going` could cause
+            subsequent/inherited errors you wouldn't see otherwise.
+            In conjunction with `dry_run`, it can be useful to anticipate all errors
+            when updating submodules.
+
         :param env: Optional dictionary containing the desired environment variables.
             Note: Provided variables will be used to update the execution environment
             for ``git``. If some variable is not specified in `env` and is defined in
             attr:`os.environ`, value from attr:`os.environ` will be used.
             If you want to unset some variable, consider providing the empty string as
             its value.
+
         :param clone_multi_options:
-            List of Clone options.
+            List of ``git clone`` options.
             Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
             They only take effect with the `init` option.
+
         :param allow_unsafe_protocols:
-            Allow unsafe protocols to be used, like ext.
+            Allow unsafe protocols to be used, like ``ext``.
+
         :param allow_unsafe_options:
-            Allow unsafe options to be used, like --upload-pack.
+            Allow unsafe options to be used, like ``--upload-pack``.
 
-        :note: Does nothing in bare repositories.
-        :note: This method is definitely not atomic if `recursive` is True.
+        :note:
+            Does nothing in bare repositories.
 
-        :return: self
+        :note:
+            This method is definitely not atomic if `recursive` is True.
+
+        :return:
+            self
         """
         if self.repo.bare:
             return self
@@ -742,9 +815,10 @@ def update(
                     # END handle tracking branch
 
                     # NOTE: Have to write the repo config file as well, otherwise the
-                    # default implementation will be offended and not update the repository.
-                    # Maybe this is a good way to ensure it doesn't get into our way, but
-                    # we want to stay backwards compatible too... It's so redundant!
+                    # default implementation will be offended and not update the
+                    # repository. Maybe this is a good way to ensure it doesn't get into
+                    # our way, but we want to stay backwards compatible too... It's so
+                    # redundant!
                     with self.repo.config_writer() as writer:
                         writer.set_value(sm_section(self.name), "url", self.url)
                 # END handle dry_run
@@ -755,7 +829,8 @@ def update(
             binsha = self.binsha
             hexsha = self.hexsha
             if mrepo is not None:
-                # mrepo is only set if we are not in dry-run mode or if the module existed.
+                # mrepo is only set if we are not in dry-run mode or if the module
+                # existed.
                 is_detached = mrepo.head.is_detached
             # END handle dry_run
 
@@ -782,10 +857,12 @@ def update(
             # Update the working tree.
             # Handles dry_run.
             if mrepo is not None and mrepo.head.commit.binsha != binsha:
-                # We must ensure that our destination sha (the one to point to) is in the future of our current head.
-                # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed.
-                # We also handle the case that history has been rewritten, leaving no merge-base. In that case
-                # we behave conservatively, protecting possible changes the user had done.
+                # We must ensure that our destination sha (the one to point to) is in
+                # the future of our current head. Otherwise, we will reset changes that
+                # might have been done on the submodule, but were not yet pushed. We
+                # also handle the case that history has been rewritten, leaving no
+                # merge-base. In that case we behave conservatively, protecting possible
+                # changes the user had done.
                 may_reset = True
                 if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
                     base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
@@ -822,10 +899,10 @@ def update(
 
                 if not dry_run and may_reset:
                     if is_detached:
-                        # NOTE: For now we force. The user is not supposed to change detached
-                        # submodules anyway. Maybe at some point this becomes an option, to
-                        # properly handle user modifications - see below for future options
-                        # regarding rebase and merge.
+                        # NOTE: For now we force. The user is not supposed to change
+                        # detached submodules anyway. Maybe at some point this becomes
+                        # an option, to properly handle user modifications - see below
+                        # for future options regarding rebase and merge.
                         mrepo.git.checkout(hexsha, force=force)
                     else:
                         mrepo.head.reset(hexsha, index=True, working_tree=True)
@@ -871,19 +948,30 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
         the repository at our current path, changing the configuration, as well as
         adjusting our index entry accordingly.
 
-        :param module_path: The path to which to move our module in the parent
-            repository's working tree, given as repository - relative or absolute path.
-            Intermediate directories will be created accordingly. If the path already
-            exists, it must be empty. Trailing (back)slashes are removed automatically.
-        :param configuration: If True, the configuration will be adjusted to let
-            the submodule point to the given path.
-        :param module: If True, the repository managed by this submodule
-            will be moved as well. If False, we don't move the submodule's checkout,
-            which may leave the parent repository in an inconsistent state.
-        :return: self
-        :raise ValueError: If the module path existed and was not empty, or was a file.
-        :note: Currently the method is not atomic, and it could leave the repository
-            in an inconsistent state if a sub-step fails for some reason.
+        :param module_path:
+            The path to which to move our module in the parent repository's working
+            tree, given as repository - relative or absolute path. Intermediate
+            directories will be created accordingly. If the path already exists, it must
+            be empty. Trailing (back)slashes are removed automatically.
+
+        :param configuration:
+            If True, the configuration will be adjusted to let the submodule point to
+            the given path.
+
+        :param module:
+            If True, the repository managed by this submodule will be moved as well. If
+            False, we don't move the submodule's checkout, which may leave the parent
+            repository in an inconsistent state.
+
+        :return:
+            self
+
+        :raise ValueError:
+            If the module path existed and was not empty, or was a file.
+
+        :note:
+            Currently the method is not atomic, and it could leave the repository in an
+            inconsistent state if a sub-step fails for some reason.
         """
         if module + configuration < 1:
             raise ValueError("You must specify to move at least the module or the configuration of the submodule")
@@ -940,8 +1028,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             # END handle git file rewrite
         # END move physical module
 
-        # Rename the index entry - we have to manipulate the index directly as
-        # git-mv cannot be used on submodules... yeah.
+        # Rename the index entry - we have to manipulate the index directly as git-mv
+        # cannot be used on submodules... yeah.
         previous_sm_path = self.path
         try:
             if configuration:
@@ -967,7 +1055,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
             raise
         # END handle undo rename
 
-        # Auto-rename submodule if its name was 'default', that is, the checkout directory.
+        # Auto-rename submodule if its name was 'default', that is, the checkout
+        # directory.
         if previous_sm_path == self.name:
             self.rename(module_checkout_path)
 
@@ -982,30 +1071,46 @@ def remove(
         dry_run: bool = False,
     ) -> "Submodule":
         """Remove this submodule from the repository. This will remove our entry
-        from the .gitmodules file and the entry in the .git/config file.
-
-        :param module: If True, the checked out module we point to will be deleted as
-            well. If that module is currently on a commit outside any branch in the
-            remote, or if it is ahead of its tracking branch, or if there are modified
-            or untracked files in its working tree, then the removal will fail. In case
-            the removal of the repository fails for these reasons, the submodule status
-            will not have been altered.
+        from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
+
+        :param module:
+            If True, the checked out module we point to will be deleted as well. If that
+            module is currently on a commit outside any branch in the remote, or if it
+            is ahead of its tracking branch, or if there are modified or untracked files
+            in its working tree, then the removal will fail. In case the removal of the
+            repository fails for these reasons, the submodule status will not have been
+            altered.
             If this submodule has child modules of its own, these will be deleted prior
             to touching the direct submodule.
-        :param force: Enforces the deletion of the module even though it contains
-            modifications. This basically enforces a brute-force file system based
-            deletion.
-        :param configuration: If True, the submodule is deleted from the configuration,
-            otherwise it isn't. Although this should be enabled most of the time, this
-            flag enables you to safely delete the repository of your submodule.
-        :param dry_run: If True, we will not actually do anything, but throw the errors
-            we would usually throw.
-        :return: self
-        :note: Doesn't work in bare repositories.
-        :note: Doesn't work atomically, as failure to remove any part of the submodule
-            will leave an inconsistent state.
-        :raise InvalidGitRepositoryError: Thrown if the repository cannot be deleted.
-        :raise OSError: If directories or files could not be removed.
+
+        :param force:
+            Enforces the deletion of the module even though it contains modifications.
+            This basically enforces a brute-force file system based deletion.
+
+        :param configuration:
+            If True, the submodule is deleted from the configuration, otherwise it
+            isn't. Although this should be enabled most of the time, this flag enables
+            you to safely delete the repository of your submodule.
+
+        :param dry_run:
+            If True, we will not actually do anything, but throw the errors we would
+            usually throw.
+
+        :return:
+            self
+
+        :note:
+            Doesn't work in bare repositories.
+
+        :note:
+            Doesn't work atomically, as failure to remove any part of the submodule will
+            leave an inconsistent state.
+
+        :raise InvalidGitRepositoryError:
+            Thrown if the repository cannot be deleted.
+
+        :raise OSError:
+            If directories or files could not be removed.
         """
         if not (module or configuration):
             raise ValueError("Need to specify to delete at least the module, or the configuration")
@@ -1019,8 +1124,9 @@ def remove(
             del csm
 
         if configuration and not dry_run and nc > 0:
-            # Ensure we don't leave the parent repository in a dirty state, and commit our changes.
-            # It's important for recursive, unforced, deletions to work as expected.
+            # Ensure we don't leave the parent repository in a dirty state, and commit
+            # our changes. It's important for recursive, unforced, deletions to work as
+            # expected.
             self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
         # END handle recursion
 
@@ -1031,8 +1137,9 @@ def remove(
             git_dir = mod.git_dir
             if force:
                 # Take the fast lane and just delete everything in our module path.
-                # TODO: If we run into permission problems, we have a highly inconsistent
-                # state. Delete the .git folders last, start with the submodules first.
+                # TODO: If we run into permission problems, we have a highly
+                # inconsistent state. Delete the .git folders last, start with the
+                # submodules first.
                 mp = self.abspath
                 method: Union[None, Callable[[PathLike], None]] = None
                 if osp.islink(mp):
@@ -1129,18 +1236,24 @@ def remove(
 
     def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) -> "Submodule":
         """Set this instance to use the given commit whose tree is supposed to
-        contain the .gitmodules blob.
+        contain the ``.gitmodules`` blob.
 
         :param commit:
             Commit-ish reference pointing at the root_tree, or None to always point to
             the most recent commit
+
         :param check:
-            If True, relatively expensive checks will be performed to verify
-            validity of the submodule.
-        :raise ValueError: If the commit's tree didn't contain the .gitmodules blob.
+            If True, relatively expensive checks will be performed to verify validity of
+            the submodule.
+
+        :raise ValueError:
+            If the commit's tree didn't contain the ``.gitmodules`` blob.
+
         :raise ValueError:
             If the parent commit didn't store this submodule under the current path.
-        :return: self
+
+        :return:
+            self
         """
         if commit is None:
             self._parent_commit = None
@@ -1179,21 +1292,28 @@ def config_writer(
         self, index: Union["IndexFile", None] = None, write: bool = True
     ) -> SectionConstraint["SubmoduleConfigParser"]:
         """
-        :return: A config writer instance allowing you to read and write the data
-            belonging to this submodule into the .gitmodules file.
+        :return:
+            A config writer instance allowing you to read and write the data belonging
+            to this submodule into the ``.gitmodules`` file.
 
-        :param index: If not None, an IndexFile instance which should be written.
+        :param index:
+            If not None, an IndexFile instance which should be written.
             Defaults to the index of the Submodule's parent repository.
-        :param write: If True, the index will be written each time a configuration
-            value changes.
 
-        :note: The parameters allow for a more efficient writing of the index,
-            as you can pass in a modified index on your own, prevent automatic writing,
-            and write yourself once the whole operation is complete.
+        :param write:
+            If True, the index will be written each time a configuration value changes.
+
+        :note:
+            The parameters allow for a more efficient writing of the index, as you can
+            pass in a modified index on your own, prevent automatic writing, and write
+            yourself once the whole operation is complete.
+
+        :raise ValueError:
+            If trying to get a writer on a parent_commit which does not match the
+            current head commit.
 
-        :raise ValueError: If trying to get a writer on a parent_commit which does not
-            match the current head commit.
-        :raise IOError: If the .gitmodules file/blob could not be read
+        :raise IOError:
+            If the ``.gitmodules`` file/blob could not be read.
         """
         writer = self._config_parser_constrained(read_only=False)
         if index is not None:
@@ -1208,22 +1328,24 @@ def rename(self, new_name: str) -> "Submodule":
         :note:
             This method takes care of renaming the submodule in various places, such as:
 
-            * $parent_git_dir / config
-            * $working_tree_dir / .gitmodules
+            * ``$parent_git_dir / config``
+            * ``$working_tree_dir / .gitmodules``
             * (git >= v1.8.0: move submodule repository to new name)
 
-        As .gitmodules will be changed, you would need to make a commit afterwards. The
-        changed .gitmodules file will already be added to the index.
+        As ``.gitmodules`` will be changed, you would need to make a commit afterwards.
+        The changed ``.gitmodules`` file will already be added to the index.
 
-        :return: This submodule instance
+        :return:
+            This :class:`Submodule` instance
         """
         if self.name == new_name:
             return self
 
         # .git/config
         with self.repo.config_writer() as pw:
-            # As we ourselves didn't write anything about submodules into the parent .git/config,
-            # we will not require it to exist, and just ignore missing entries.
+            # As we ourselves didn't write anything about submodules into the parent
+            # .git/config, we will not require it to exist, and just ignore missing
+            # entries.
             if pw.has_section(sm_section(self.name)):
                 pw.rename_section(sm_section(self.name), sm_section(new_name))
 
@@ -1260,8 +1382,9 @@ def module(self) -> "Repo":
         """
         :return: Repo instance initialized from the repository at our submodule path
 
-        :raise InvalidGitRepositoryError: If a repository was not available. This could
-            also mean that it was not yet initialized.
+        :raise InvalidGitRepositoryError:
+            If a repository was not available.
+            This could also mean that it was not yet initialized.
         """
         module_checkout_abspath = self.abspath
         try:
@@ -1276,7 +1399,11 @@ def module(self) -> "Repo":
         # END handle exceptions
 
     def module_exists(self) -> bool:
-        """:return: True if our module exists and is a valid git repository. See module() method."""
+        """
+        :return:
+            True if our module exists and is a valid git repository.
+            See the :meth:`module` method.
+        """
         try:
             self.module()
             return True
@@ -1286,9 +1413,10 @@ def module_exists(self) -> bool:
 
     def exists(self) -> bool:
         """
-        :return: True if the submodule exists, False otherwise. Please note that
-            a submodule may exist (in the .gitmodules file) even though its module
-            doesn't exist on disk.
+        :return:
+            True if the submodule exists, False otherwise.
+            Please note that a submodule may exist (in the ``.gitmodules`` file) even
+            though its module doesn't exist on disk.
         """
         # Keep attributes for later, and restore them if we have no valid data.
         # This way we do not actually alter the state of the object.
@@ -1299,7 +1427,8 @@ def exists(self) -> bool:
                     loc[attr] = getattr(self, attr)
                 # END if we have the attribute cache
             except (cp.NoSectionError, ValueError):
-                # On PY3, this can happen apparently... don't know why this doesn't happen on PY2.
+                # On PY3, this can happen apparently... don't know why this doesn't
+                # happen on PY2.
                 pass
         # END for each attr
         self._clear_cache()
@@ -1333,8 +1462,9 @@ def branch(self) -> "Head":
     @property
     def branch_path(self) -> PathLike:
         """
-        :return: Complete relative path as string to the branch we would checkout
-            from the remote and track
+        :return:
+            Complete relative path as string to the branch we would checkout from the
+            remote and track
         """
         return self._branch_path
 
@@ -1344,8 +1474,8 @@ def branch_name(self) -> str:
         :return:
             The name of the branch, which is the shortest possible branch name
         """
-        # Use an instance method, for this we create a temporary Head instance
-        # which uses a repository that is available at least (it makes no difference).
+        # Use an instance method, for this we create a temporary Head instance which
+        # uses a repository that is available at least (it makes no difference).
         return git.Head(self.repo, self._branch_path).name
 
     @property
@@ -1420,7 +1550,8 @@ def iter_items(
     ) -> Iterator["Submodule"]:
         """
         :return:
-            Iterator yielding Submodule instances available in the given repository
+            Iterator yielding :class:`Submodule` instances available in the given
+            repository
         """
         try:
             pc = repo.commit(parent_commit)  # Parent commit instance

From 8344f442b4c002ec7f0a351c6a61d6d9ca084bce Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 21:19:11 -0500
Subject: [PATCH 502/994] Revise Repo.archive docstring

I had missed this in b2b6f7c.
---
 git/repo/base.py | 27 +++++++++++++++++----------
 1 file changed, 17 insertions(+), 10 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index 1e58a24d0..71468bebc 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1535,22 +1535,29 @@ def archive(
     ) -> Repo:
         """Archive the tree at the given revision.
 
-        :param ostream: file compatible stream object to which the archive will be written as bytes.
+        :param ostream:
+            File-compatible stream object to which the archive will be written as bytes.
 
-        :param treeish: is the treeish name/id, defaults to active branch.
+        :param treeish:
+            The treeish name/id, defaults to active branch.
 
-        :param prefix: is the optional prefix to prepend to each filename in the archive.
+        :param prefix:
+            The optional prefix to prepend to each filename in the archive.
 
-        :param kwargs: Additional arguments passed to git-archive:
+        :param kwargs:
+            Additional arguments passed to ``git archive``:
 
-            * Use the 'format' argument to define the kind of format. Use
-              specialized ostreams to write any format supported by python.
-            * You may specify the special **path** keyword, which may either be a repository-relative
-              path to a directory or file to place into the archive, or a list or tuple of multiple paths.
+            * Use the 'format' argument to define the kind of format. Use specialized
+              ostreams to write any format supported by Python.
+            * You may specify the special **path** keyword, which may either be a
+              repository-relative path to a directory or file to place into the archive,
+              or a list or tuple of multiple paths.
 
-        :raise GitCommandError: If something went wrong.
+        :raise GitCommandError:
+            If something went wrong.
 
-        :return: self
+        :return:
+            self
         """
         if treeish is None:
             treeish = self.head.commit

From 432ec72b759289cd8b8967847dd8bafcd6b78915 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 21:22:50 -0500
Subject: [PATCH 503/994] Fix another :raises: to :raise:

Missed in bcc0c27.
---
 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 71468bebc..19e951567 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1020,7 +1020,7 @@ def ignored(self, *paths: PathLike) -> List[str]:
     def active_branch(self) -> Head:
         """The name of the currently active branch.
 
-        :raises	TypeError:
+        :raise TypeError:
             If HEAD is detached.
 
         :return:

From 5ca584444b1fe00d5c52341cd267588e64174d7f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 21:26:45 -0500
Subject: [PATCH 504/994] Fully qualify non-builtin exceptions in :raise:

This expands exceptions from git.exc and gitdb.exc in :raise:
sections of docstrings, to include those qualifiers.

This serves a few purposes. The main benefits are:

- Immediately clarify which exceptions are not built-in ones, as
  not all readers are necessarily familiar with all built-in
  exceptions.

- Distinguish exceptions defined in GitPython (in git.exc) from
  those defined in gitdb (in gitdb.exc). Although the gitdb
  exceptions GitPython uses are imported into git.exc and listed
  in __all__, they are still not introduced in GitPython, which is
  relevant for (a) preventing confusion, so developers don't think
  they accidentally imported a different same-named exception,
  (b) understanding why they do not have documentation in the
  generated GitPython docs.

It also has these minor benefits:

- May help sphinx-autodoc find them. In practice I think this is
  unlikely to be necessary.

- Most other references, including references to classes, within
  docstrings in the git module, are now fully qualified, when not
  defined/introduced in the same module. So this is consistent with
  that. This consistency might be more than a minor benefit if
  there were a committment to keep most such refernces fully
  qualified, but this really should not be a goal: especially where
  Sphinx autodoc is guaranteed to be able to resolve them,
  shortening (in some cases, re-shortening) the references in the
  future may lead to better docstring readability.
---
 git/cmd.py                    |  4 ++--
 git/db.py                     |  8 ++++----
 git/index/base.py             |  6 +++---
 git/index/fun.py              |  2 +-
 git/objects/submodule/base.py |  6 +++---
 git/remote.py                 |  2 +-
 git/repo/base.py              | 10 +++++-----
 git/repo/fun.py               |  4 ++--
 8 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index cab089746..997f9d41e 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -699,7 +699,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
                 May deadlock if output or error pipes are used and not handled
                 separately.
 
-            :raise GitCommandError:
+            :raise git.exc.GitCommandError:
                 If the return status is not 0.
             """
             if stderr is None:
@@ -1091,7 +1091,7 @@ def execute(
             Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
             output regardless of system language.
 
-        :raise GitCommandError:
+        :raise git.exc.GitCommandError:
 
         :note:
            If you add additional keyword arguments to the signature of this method,
diff --git a/git/db.py b/git/db.py
index dff59f47a..eb7f758da 100644
--- a/git/db.py
+++ b/git/db.py
@@ -56,13 +56,13 @@ def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
         """
         :return: Full binary 20 byte sha from the given partial hexsha
 
-        :raise AmbiguousObjectName:
+        :raise gitdb.exc.AmbiguousObjectName:
 
-        :raise BadObject:
+        :raise gitdb.exc.BadObject:
 
         :note:
-            Currently we only raise :class:`BadObject` as git does not communicate
-            ambiguous objects separately.
+            Currently we only raise :class:`~gitdb.exc.BadObject` as git does not
+            communicate ambiguous objects separately.
         """
         try:
             hexsha, _typename, _size = self._git.get_object_header(partial_hexsha)
diff --git a/git/index/base.py b/git/index/base.py
index 31186b203..5c738cdf2 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -284,7 +284,7 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF
             self (containing the merge and possibly unmerged entries in case of
             conflicts)
 
-        :raise GitCommandError:
+        :raise git.exc.GitCommandError:
             If there is a merge conflict. The error will be raised at the first
             conflicting path. If you want to have proper merge resolution to be done by
             yourself, you have to commit the changed index (or make a valid tree from
@@ -624,7 +624,7 @@ def write_tree(self) -> Tree:
         :raise ValueError:
             If there are no entries in the cache.
 
-        :raise UnmergedEntriesError:
+        :raise git.exc.UnmergedEntriesError:
         """
         # We obtain no lock as we just flush our contents to disk as tree.
         # If we are a new index, the entries access will load our data accordingly.
@@ -1077,7 +1077,7 @@ def move(
         :raise ValueError:
             If only one item was given.
 
-        :raise GitCommandError:
+        :raise git.exc.GitCommandError:
             If git could not handle your request.
         """
         args = []
diff --git a/git/index/fun.py b/git/index/fun.py
index a60a3b809..1d4ca9b93 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -88,7 +88,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     :param args:
         Arguments passed to hook file.
 
-    :raise HookExecutionError:
+    :raise git.exc.HookExecutionError:
     """
     hp = hook_path(name, index.repo.git_dir)
     if not os.access(hp, os.X_OK):
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 70a021c54..159403f24 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1106,7 +1106,7 @@ def remove(
             Doesn't work atomically, as failure to remove any part of the submodule will
             leave an inconsistent state.
 
-        :raise InvalidGitRepositoryError:
+        :raise git.exc.InvalidGitRepositoryError:
             Thrown if the repository cannot be deleted.
 
         :raise OSError:
@@ -1382,7 +1382,7 @@ def module(self) -> "Repo":
         """
         :return: Repo instance initialized from the repository at our submodule path
 
-        :raise InvalidGitRepositoryError:
+        :raise git.exc.InvalidGitRepositoryError:
             If a repository was not available.
             This could also mean that it was not yet initialized.
         """
@@ -1454,7 +1454,7 @@ def branch(self) -> "Head":
         :return:
             The branch instance that we are to checkout
 
-        :raise InvalidGitRepositoryError:
+        :raise git.exc.InvalidGitRepositoryError:
             If our module is not yet checked out.
         """
         return mkhead(self.module(), self._branch_path)
diff --git a/git/remote.py b/git/remote.py
index 5bee9edf5..249a6b7bf 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -801,7 +801,7 @@ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool
         :return:
             New :class:`Remote` instance
 
-        :raise GitCommandError:
+        :raise git.exc.GitCommandError:
             In case an origin with that name already exists.
         """
         scmd = "add"
diff --git a/git/repo/base.py b/git/repo/base.py
index 19e951567..7b3e514c1 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -202,12 +202,12 @@ def __init__(
             Please note that this was the default behaviour in older versions of
             GitPython, which is considered a bug though.
 
-        :raise InvalidGitRepositoryError:
+        :raise git.exc.InvalidGitRepositoryError:
 
-        :raise NoSuchPathError:
+        :raise git.exc.NoSuchPathError:
 
         :return:
-            git.Repo
+            :class:`Repo`
         """
 
         epath = path or os.getenv("GIT_DIR")
@@ -893,7 +893,7 @@ def _set_alternates(self, alts: List[str]) -> None:
             The array of string paths representing the alternates at which git should
             look for objects, i.e. ``/home/user/repo/.git/objects``.
 
-        :raise NoSuchPathError:
+        :raise git.exc.NoSuchPathError:
 
         :note:
             The method does not check for the existence of the paths in `alts`, as the
@@ -1553,7 +1553,7 @@ def archive(
               repository-relative path to a directory or file to place into the archive,
               or a list or tuple of multiple paths.
 
-        :raise GitCommandError:
+        :raise git.exc.GitCommandError:
             If something went wrong.
 
         :return:
diff --git a/git/repo/fun.py b/git/repo/fun.py
index fa2cebb67..4936f1773 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -59,7 +59,7 @@ def touch(filename: str) -> str:
 def is_git_dir(d: "PathLike") -> bool:
     """This is taken from the git setup.c:is_git_directory function.
 
-    :raise WorkTreeRepositoryUnsupported:
+    :raise git.exc.WorkTreeRepositoryUnsupported:
         If it sees a worktree directory. It's quite hacky to do that here, but at least
         clearly indicates that we don't support it. There is the unlikely danger to
         throw if we see directories which just look like a worktree dir, but are none.
@@ -234,7 +234,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         ``git rev-parse``-compatible revision specification as string. Please see
         http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html for details.
 
-    :raise BadObject:
+    :raise gitdb.exc.BadObject:
         If the given revision could not be found.
 
     :raise ValueError:

From e6768ec9a06c295808ea1b4b7ed91e57fb4a2bf3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 27 Feb 2024 21:50:38 -0500
Subject: [PATCH 505/994] Improve Git.execute docstring formatting re:
 max_chunk_size

---
 git/cmd.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 997f9d41e..4bd53ea0f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1067,9 +1067,9 @@ def execute(
             A dictionary of environment variables to be passed to :class:`subprocess.Popen`.
 
         :param max_chunk_size:
-            Maximum number of bytes in one chunk of data passed to the output_stream in
-            one invocation of write() method. If the given number is not positive then
-            the default value is used.
+            Maximum number of bytes in one chunk of data passed to the `output_stream`
+            in one invocation of its ``write()`` method. If the given number is not
+            positive then the default value is used.
 
         :param strip_newline_in_stdout:
             Whether to strip the trailing ``\n`` of the command stdout.

From cd61eb4f7a5c9cd1c09b362335577d87a441e2c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 17:23:04 -0500
Subject: [PATCH 506/994] Further revise docstrings in second-level modules

Some stuff unintentionally missed in 1cd73ba and some subsequent
commits, plus a few further things intentionally omitted there,
such as converting True, False, and None literals, to double
backticked form (rather than bare or single-backticked).
---
 git/__init__.py |   9 +++--
 git/cmd.py      | 103 +++++++++++++++++++++++++-----------------------
 git/config.py   |  46 +++++++++++----------
 git/diff.py     |  17 ++++----
 git/exc.py      |   4 +-
 git/remote.py   |  37 ++++++++---------
 git/util.py     |   8 ++--
 7 files changed, 119 insertions(+), 105 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index bc97a6eff..6fc6110f4 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -129,12 +129,13 @@ def refresh(path: Optional[PathLike] = None) -> None:
     :note:
         The *path* parameter is usually omitted and cannot be used to specify a custom
         command whose location is looked up in a path search on each call. See
-        :meth:`Git.refresh` for details on how to achieve this.
+        :meth:`Git.refresh <git.cmd.Git.refresh>` for details on how to achieve this.
 
     :note:
-        This calls :meth:`Git.refresh` and sets other global configuration according to
-        the effect of doing so. As such, this function should usually be used instead of
-        using :meth:`Git.refresh` or :meth:`FetchInfo.refresh` directly.
+        This calls :meth:`Git.refresh <git.cmd.Git.refresh>` and sets other global
+        configuration according to the effect of doing so. As such, this function should
+        usually be used instead of using :meth:`Git.refresh <git.cmd.Git.refresh>` or
+        :meth:`FetchInfo.refresh <git.remote.FetchInfo.refresh>` directly.
 
     :note:
         This function is called automatically, with no arguments, at import time.
diff --git a/git/cmd.py b/git/cmd.py
index 4bd53ea0f..d0c76eafe 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -112,25 +112,25 @@ def handle_process_output(
     This function returns once the finalizer returns.
 
     :param process:
-        :class:`subprocess.Popen` instance
+        :class:`subprocess.Popen` instance.
 
     :param stdout_handler:
-        f(stdout_line_string), or None
+        f(stdout_line_string), or ``None``.
 
     :param stderr_handler:
-        f(stderr_line_string), or None
+        f(stderr_line_string), or ``None``.
 
     :param finalizer:
-        f(proc) - wait for proc to finish
+        f(proc) - wait for proc to finish.
 
     :param decode_streams:
         Assume stdout/stderr streams are binary and decode them before pushing their
         contents to handlers.
-        Set this to False if ``universal_newlines == True`` (then streams are in text
-        mode) or if decoding must happen later (i.e. for :class:`git.diff.Diff`s).
+        Set this to ``False`` if ``universal_newlines == True`` (then streams are in
+        text mode) or if decoding must happen later (i.e. for :class:`~git.diff.Diff`s).
 
     :param kill_after_timeout:
-        float or None, Default = None
+        :class:`float` or ``None``, Default = ``None``
 
         To specify a timeout in seconds for the git command, after which the process
         should be killed.
@@ -236,7 +236,7 @@ def _safer_popen_windows(
     itself be searched automatically by the shell. This wrapper covers all those cases.
 
     :note:
-        This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
+        This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath`
         environment variable during subprocess creation. It also takes care of passing
         Windows-specific process creation flags, but that is unrelated to path search.
 
@@ -311,8 +311,8 @@ class Git:
 
     Debugging:
 
-    * Set the ``GIT_PYTHON_TRACE`` environment variable print each invocation of the
-      command to stdout.
+    * Set the :envvar:`GIT_PYTHON_TRACE` environment variable to print each invocation
+      of the command to stdout.
     * Set its value to ``full`` to see details about the returned values.
     """
 
@@ -351,7 +351,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     """Enables debugging of GitPython's git commands."""
 
     USE_SHELL = False
-    """Deprecated. If set to True, a shell will be used when executing git commands.
+    """Deprecated. If set to ``True``, a shell will be used when executing git commands.
 
     Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
     in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
@@ -401,30 +401,32 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
             There are three different ways to specify the command that refreshing causes
             to be used for git:
 
-            1. Pass no `path` argument and do not set the ``GIT_PYTHON_GIT_EXECUTABLE``
-               environment variable. The command name ``git`` is used. It is looked up
-               in a path search by the system, in each command run (roughly similar to
-               how git is found when running ``git`` commands manually). This is usually
-               the desired behavior.
+            1. Pass no `path` argument and do not set the
+               :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable. The command
+               name ``git`` is used. It is looked up in a path search by the system, in
+               each command run (roughly similar to how git is found when running
+               ``git`` commands manually). This is usually the desired behavior.
 
-            2. Pass no `path` argument but set the ``GIT_PYTHON_GIT_EXECUTABLE``
+            2. Pass no `path` argument but set the :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
                environment variable. The command given as the value of that variable is
                used. This may be a simple command or an arbitrary path. It is looked up
-               in each command run. Setting ``GIT_PYTHON_GIT_EXECUTABLE`` to ``git`` has
-               the same effect as not setting it.
+               in each command run. Setting :envvar:`GIT_PYTHON_GIT_EXECUTABLE` to
+               ``git`` has the same effect as not setting it.
 
             3. Pass a `path` argument. This path, if not absolute, is immediately
                resolved, relative to the current directory. This resolution occurs at
                the time of the refresh. When git commands are run, they are run using
                that previously resolved path. If a `path` argument is passed, the
-               ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable is not consulted.
+               :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable is not
+               consulted.
 
         :note:
             Refreshing always sets the :attr:`Git.GIT_PYTHON_GIT_EXECUTABLE` class
             attribute, which can be read on the :class:`Git` class or any of its
             instances to check what command is used to run git. This attribute should
-            not be confused with the related ``GIT_PYTHON_GIT_EXECUTABLE`` environment
-            variable. The class attribute is set no matter how refreshing is performed.
+            not be confused with the related :envvar:`GIT_PYTHON_GIT_EXECUTABLE`
+            environment variable. The class attribute is set no matter how refreshing is
+            performed.
         """
         # Discern which path to refresh with.
         if path is not None:
@@ -571,9 +573,9 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str:
     def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
         """Remove any backslashes from URLs to be written in config files.
 
-        Windows might create config files containing paths with backslashes,
-        but git stops liking them as it will escape the backslashes. Hence we
-        undo the escaping just to be sure.
+        Windows might create config files containing paths with backslashes, but git
+        stops liking them as it will escape the backslashes. Hence we undo the escaping
+        just to be sure.
         """
         if is_cygwin is None:
             is_cygwin = cls.is_cygwin()
@@ -638,8 +640,8 @@ class AutoInterrupt:
 
         __slots__ = ("proc", "args", "status")
 
-        # If this is non-zero it will override any status code during
-        # _terminate, used to prevent race conditions in testing.
+        # If this is non-zero it will override any status code during _terminate, used
+        # to prevent race conditions in testing.
         _status_code_if_terminate: int = 0
 
         def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
@@ -844,7 +846,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         """Initialize this instance with:
 
         :param working_dir:
-           Git directory we should work in. If None, we always work in the current
+           Git directory we should work in. If ``None``, we always work in the current
            directory as returned by :func:`os.getcwd`.
            This is meant to be the working tree directory if available, or the
            ``.git`` directory in case of bare repositories.
@@ -1022,8 +1024,8 @@ def execute(
             This feature only has any effect if `as_process` is False.
 
         :param stdout_as_string:
-            If False, the command's standard output will be bytes. Otherwise, it will be
-            decoded into a string using the default encoding (usually UTF-8).
+            If ``False``, the command's standard output will be bytes. Otherwise, it
+            will be decoded into a string using the default encoding (usually UTF-8).
             The latter can fail, if the output contains binary data.
 
         :param kill_after_timeout:
@@ -1045,15 +1047,16 @@ def execute(
                is manually removed.
 
         :param with_stdout:
-            If True, default True, we open stdout on the created process.
+            If ``True``, default ``True``, we open stdout on the created process.
 
         :param universal_newlines:
-            If True, pipes will be opened as text, and lines are split at all known line
-            endings.
+            If ``True``, pipes will be opened as text, and lines are split at all known
+            line endings.
 
         :param shell:
-            Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
-            If this is not `None`, it overrides :attr:`USE_SHELL`.
+            Whether to invoke commands through a shell
+            (see :class:`Popen(..., shell=True) <subprocess.Popen>`).
+            If this is not ``None``, it overrides :attr:`USE_SHELL`.
 
             Passing ``shell=True`` to this or any other GitPython function should be
             avoided, as it is unsafe under most circumstances. This is because it is
@@ -1064,7 +1067,8 @@ def execute(
             issues.
 
         :param env:
-            A dictionary of environment variables to be passed to :class:`subprocess.Popen`.
+            A dictionary of environment variables to be passed to
+            :class:`subprocess.Popen`.
 
         :param max_chunk_size:
             Maximum number of bytes in one chunk of data passed to the `output_stream`
@@ -1083,7 +1087,7 @@ def execute(
             * str(output) if extended_output = False (Default)
             * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
 
-            If output_stream is True, the stdout value will be your output stream:
+            If `output_stream` is ``True``, the stdout value will be your output stream:
 
             * output_stream if extended_output = False
             * tuple(int(status), output_stream, str(stderr)) if extended_output = True
@@ -1288,7 +1292,7 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]:
             self.update_environment(**old_env)
 
         :param kwargs:
-            Environment variables to use for git processes
+            Environment variables to use for git processes.
 
         :return:
             Dict that maps environment variables to their old values
@@ -1367,8 +1371,8 @@ def __call__(self, **kwargs: Any) -> "Git":
 
         :param kwargs:
             A dict of keyword arguments.
-            These arguments are passed as in :meth:`_call_process`, but will be
-            passed to the git command rather than the subcommand.
+            These arguments are passed as in :meth:`_call_process`, but will be passed
+            to the git command rather than the subcommand.
 
         Examples::
 
@@ -1409,16 +1413,16 @@ def _call_process(
             as in ``ls_files`` to call ``ls-files``.
 
         :param args:
-            The list of arguments. If None is included, it will be pruned.
-            This allows your commands to call git more conveniently, as None is realized
-            as non-existent.
+            The list of arguments. If ``None`` is included, it will be pruned.
+            This allows your commands to call git more conveniently, as ``None`` is
+            realized as non-existent.
 
         :param kwargs:
             Contains key-values for the following:
 
             - The :meth:`execute()` kwds, as listed in :var:`execute_kwargs`.
             - "Command options" to be converted by :meth:`transform_kwargs`.
-            - The `'insert_kwargs_after'` key which its value must match one of ``*args``.
+            - The ``insert_kwargs_after`` key which its value must match one of ``*args``.
 
             It also contains any command options, to be appended after the matched arg.
 
@@ -1431,9 +1435,9 @@ def _call_process(
            git rev-list max-count 10 --header master
 
         :return:
-            Same as :meth:`execute`.
-            If no args are given, used :meth:`execute`'s default (especially
-            ``as_process = False``, ``stdout_as_string = True``) and return str.
+            Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
+            default (especially ``as_process = False``, ``stdout_as_string = True``) and
+            return :class:`str`.
         """
         # Handle optional arguments prior to calling transform_kwargs.
         # Otherwise these'll end up in args, which is bad.
@@ -1480,10 +1484,11 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
         :param header_line:
             <hex_sha> type_string size_as_int
 
-        :return: (hex_sha, type_string, size_as_int)
+        :return:
+            (hex_sha, type_string, size_as_int)
 
         :raise ValueError:
-            If the header contains indication for an error due to incorrect input sha
+            If the header contains indication for an error due to incorrect input sha.
         """
         tokens = header_line.split()
         if len(tokens) != 3:
diff --git a/git/config.py b/git/config.py
index b5cff01cd..ff5c9d564 100644
--- a/git/config.py
+++ b/git/config.py
@@ -186,8 +186,8 @@ def config(self) -> T_ConfigParser:
         return self._config
 
     def release(self) -> None:
-        """Equivalent to GitConfigParser.release(), which is called on our underlying
-        parser instance."""
+        """Equivalent to :meth:`GitConfigParser.release`, which is called on our
+        underlying parser instance."""
         return self._config.release()
 
     def __enter__(self) -> "SectionConstraint[T_ConfigParser]":
@@ -292,7 +292,7 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     t_lock = LockFile
     """The lock type determines the type of lock to use in new configuration readers.
 
-    They must be compatible to the LockFile interface.
+    They must be compatible to the :class:`~git.util.LockFile` interface.
     A suitable alternative would be the :class:`~git.util.BlockingLockFile`.
     """
 
@@ -326,14 +326,15 @@ def __init__(
             A file path or file object, or a sequence of possibly more than one of them.
 
         :param read_only:
-            If True, the ConfigParser may only read the data, but not change it.
-            If False, only a single file path or file object may be given. We will write
-            back the changes when they happen, or when the ConfigParser is released.
-            This will not happen if other configuration files have been included.
+            If ``True``, the ConfigParser may only read the data, but not change it.
+            If ``False``, only a single file path or file object may be given. We will
+            write back the changes when they happen, or when the ConfigParser is
+            released. This will not happen if other configuration files have been
+            included.
 
         :param merge_includes:
-            If True, we will read files mentioned in ``[include]`` sections and merge
-            their contents into ours. This makes it impossible to write back an
+            If ``True``, we will read files mentioned in ``[include]`` sections and
+            merge their contents into ours. This makes it impossible to write back an
             individual configuration file. Thus, if you want to modify a single
             configuration file, turn this off to leave the original dataset unaltered
             when reading it.
@@ -347,7 +348,8 @@ def __init__(
         self._defaults: _OMD
         self._sections: _OMD  # type: ignore  # mypy/typeshed bug?
 
-        # Used in Python 3. Needs to stay in sync with sections for underlying implementation to work.
+        # Used in Python 3. Needs to stay in sync with sections for underlying
+        # implementation to work.
         if not hasattr(self, "_proxies"):
             self._proxies = self._dict()
 
@@ -593,7 +595,7 @@ def read(self) -> None:  # type: ignore[override]
             Nothing
 
         :raise IOError:
-            If a file cannot be handled
+            If a file cannot be handled.
         """
         if self._is_initialized:
             return
@@ -712,7 +714,8 @@ def write(self) -> None:
         """Write changes to our file, if there are changes at all.
 
         :raise IOError:
-            If this is a read-only writer instance or if we could not obtain a file lock
+            If this is a read-only writer instance or if we could not obtain a file
+            lock.
         """
         self._assure_writable("write")
         if not self._dirty:
@@ -778,8 +781,8 @@ def get_value(
         specified is returned.
 
         :param default:
-            If not None, the given default value will be returned in case the option did
-            not exist
+            If not ``None``, the given default value will be returned in case the option
+            did not exist.
 
         :return:
             A properly typed value, either int, float or string
@@ -809,8 +812,8 @@ def get_values(
         returned.
 
         :param default:
-            If not None, a list containing the given default value will be returned in
-            case the option did not exist.
+            If not ``None``, a list containing the given default value will be returned
+            in case the option did not exist.
 
         :return:
             A list of properly typed values, either int, float or string
@@ -868,7 +871,7 @@ def set_value(self, section: str, option: str, value: Union[str, bytes, int, flo
         """Set the given option in section to the given value.
 
         This will create the section if required, and will not throw as opposed to the
-        default ConfigParser 'set' method.
+        default ConfigParser ``set`` method.
 
         :param section:
             Name of the section in which the option resides or should reside.
@@ -893,9 +896,9 @@ def add_value(self, section: str, option: str, value: Union[str, bytes, int, flo
         """Add a value for the given option in section.
 
         This will create the section if required, and will not throw as opposed to the
-        default ConfigParser 'set' method. The value becomes the new value of the option
-        as returned by 'get_value', and appends to the list of values returned by
-        'get_values'.
+        default ConfigParser ``set`` method. The value becomes the new value of the
+        option as returned by :meth:`get_value`, and appends to the list of values
+        returned by :meth:`get_values`.
 
         :param section:
             Name of the section in which the option resides or should reside.
@@ -923,7 +926,8 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
             * ``section`` doesn't exist.
             * A section with ``new_name`` does already exist.
 
-        :return: This instance
+        :return:
+            This instance
         """
         if not self.has_section(section):
             raise ValueError("Source section '%s' doesn't exist" % section)
diff --git a/git/diff.py b/git/diff.py
index 7205136d0..3c760651b 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -118,7 +118,7 @@ def diff(
         :param other:
             This the item to compare us with.
 
-            * If None, we will be compared to the working tree.
+            * If ``None``, we will be compared to the working tree.
             * If :class:`~git.index.base.Treeish`, it will be compared against the
               respective tree.
             * If :class:`~Diffable.Index`, it will be compared against the index.
@@ -131,7 +131,7 @@ def diff(
             include at least one of the given path or paths.
 
         :param create_patch:
-            If True, the returned :class:`Diff` contains a detailed patch that if
+            If ``True``, the returned :class:`Diff` contains a detailed patch that if
             applied makes the self to other. Patches are somewhat costly as blobs have
             to be read and diffed.
 
@@ -139,7 +139,8 @@ def diff(
             Additional arguments passed to ``git diff``, such as ``R=True`` to swap both
             sides of the diff.
 
-        :return: git.DiffIndex
+        :return:
+            :class:`DiffIndex`
 
         :note:
             On a bare repository, `other` needs to be provided as
@@ -262,7 +263,7 @@ class Diff:
     Diffs keep information about the changed blob objects, the file mode, renames,
     deletions and new files.
 
-    There are a few cases where None has to be expected as member variable value:
+    There are a few cases where ``None`` has to be expected as member variable value:
 
     New File::
 
@@ -468,7 +469,8 @@ def rename_to(self) -> Optional[str]:
     @property
     def renamed(self) -> bool:
         """
-        :return: True if the blob of our diff has been renamed
+        :return:
+            ``True`` if the blob of our diff has been renamed
 
         :note:
             This property is deprecated.
@@ -499,11 +501,12 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
         """Create a new :class:`DiffIndex` from the given process output which must be
         in patch format.
 
-        :param repo: The repository we are operating on.
+        :param repo:
+            The repository we are operating on.
 
         :param proc:
             ``git diff`` process to read from
-            (supports :class:`Git.AutoInterrupt` wrapper).
+            (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
 
         :return:
             :class:`DiffIndex`
diff --git a/git/exc.py b/git/exc.py
index 276d79e42..9f6462b39 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -136,8 +136,8 @@ def __str__(self) -> str:
 
 
 class GitCommandNotFound(CommandError):
-    """Thrown if we cannot find the `git` executable in the ``PATH`` or at the path
-    given by the ``GIT_PYTHON_GIT_EXECUTABLE`` environment variable."""
+    """Thrown if we cannot find the ``git`` executable in the :envvar:`PATH` or at the
+    path given by the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable."""
 
     def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None:
         super().__init__(command, cause)
diff --git a/git/remote.py b/git/remote.py
index 249a6b7bf..265f1901f 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -125,8 +125,7 @@ def to_progress_instance(
 
 
 class PushInfo(IterableObj):
-    """
-    Carries information about the result of a push operation of a single head::
+    """Carries information about the result of a push operation of a single head::
 
         info = remote.push()[0]
         info.flags          # bitflags providing more information about the result
@@ -222,8 +221,8 @@ def remote_ref(self) -> Union[RemoteReference, TagReference]:
 
     @classmethod
     def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
-        """Create a new PushInfo instance as parsed from line which is expected to be like
-        refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
+        """Create a new :class:`PushInfo` instance as parsed from line which is expected
+        to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes."""
         control_character, from_to, summary = line.split("\t", 3)
         flags = 0
 
@@ -279,7 +278,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn:  # ->
 
 
 class PushInfoList(IterableList[PushInfo]):
-    """IterableList of :class:`PushInfo` objects."""
+    """:class:`~git.util.IterableList` of :class:`PushInfo` objects."""
 
     def __new__(cls) -> "PushInfoList":
         return cast(PushInfoList, IterableList.__new__(cls, "push_infos"))
@@ -295,8 +294,7 @@ def raise_if_error(self) -> None:
 
 
 class FetchInfo(IterableObj):
-    """
-    Carries information about the results of a fetch operation of a single head::
+    """Carries information about the results of a fetch operation of a single head::
 
      info = remote.fetch()[0]
      info.ref           # Symbolic Reference or RemoteReference to the changed
@@ -559,7 +557,7 @@ def __init__(self, repo: "Repo", name: str) -> None:
             The repository we are a remote of.
 
         :param name:
-            The name of the remote, e.g. 'origin'.
+            The name of the remote, e.g. ``origin``.
         """
         self.repo = repo
         self.name = name
@@ -611,7 +609,7 @@ def __hash__(self) -> int:
     def exists(self) -> bool:
         """
         :return:
-            True if this is a valid, existing remote.
+            ``True`` if this is a valid, existing remote.
             Valid remotes have an entry in the repository's configuration.
         """
         try:
@@ -683,7 +681,7 @@ def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any)
         return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
 
     def delete_url(self, url: str, **kwargs: Any) -> "Remote":
-        """Deletes a new url on current remote (special case of ``git remote set-url``)
+        """Deletes a new url on current remote (special case of ``git remote set-url``).
 
         This command deletes new URLs to a given remote, making it possible to have
         multiple URLs for a single remote.
@@ -733,7 +731,7 @@ def urls(self) -> Iterator[str]:
     def refs(self) -> IterableList[RemoteReference]:
         """
         :return:
-            :class:`~git.util.IterableList` of :class:`git.refs.remote.RemoteReference`
+            :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
             objects.
 
             It is prefixed, allowing you to omit the remote path portion, e.g.::
@@ -748,7 +746,7 @@ def refs(self) -> IterableList[RemoteReference]:
     def stale_refs(self) -> IterableList[Reference]:
         """
         :return:
-            :class:`~git.util.IterableList` of :class:`git.refs.remote.RemoteReference`
+            :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference`
             objects that do not have a corresponding head in the remote reference
             anymore as they have been deleted on the remote side, but are still
             available locally.
@@ -1019,13 +1017,15 @@ def fetch(
             supplying a list rather than a string for 'refspec' will make use of this
             facility.
 
-        :param progress: See :meth:`push` method.
+        :param progress:
+            See :meth:`push` method.
 
-        :param verbose: Boolean for verbose output.
+        :param verbose:
+            Boolean for verbose output.
 
         :param kill_after_timeout:
             To specify a timeout in seconds for the git command, after which the process
-            should be killed. It is set to None by default.
+            should be killed. It is set to ``None`` by default.
 
         :param allow_unsafe_protocols:
             Allow unsafe protocols to be used, like ``ext``.
@@ -1101,7 +1101,7 @@ def pull(
             Additional arguments to be passed to ``git pull``.
 
         :return:
-            Please see :meth:`fetch` method
+            Please see :meth:`fetch` method.
         """
         if refspec is None:
             # No argument refspec, then ensure the repo's config has a fetch refspec.
@@ -1141,7 +1141,7 @@ def push(
         :param progress:
             Can take one of many value types:
 
-            * None, to discard progress information.
+            * ``None``, to discard progress information.
             * A function (callable) that is called with the progress information.
               Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
               See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a
@@ -1163,7 +1163,8 @@ def push(
         :param allow_unsafe_options:
             Allow unsafe options to be used, like ``--receive-pack``.
 
-        :param kwargs: Additional arguments to be passed to ``git push``.
+        :param kwargs:
+            Additional arguments to be passed to ``git push``.
 
         :return:
             A :class:`PushInfoList` object, where each list member represents an
diff --git a/git/util.py b/git/util.py
index 4929affeb..e7dd94a08 100644
--- a/git/util.py
+++ b/git/util.py
@@ -112,7 +112,7 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
 
     :return:
         On Windows, the flag, or the `default` value if absent or ambiguous.
-        On all other operating systems, False.
+        On all other operating systems, ``False``.
 
     :note:
         This only accesses the environment on Windows.
@@ -309,11 +309,11 @@ def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
     """Make sure that the directory pointed to by path exists.
 
     :param is_file:
-        If True, `path` is assumed to be a file and handled correctly.
+        If ``True``, `path` is assumed to be a file and handled correctly.
         Otherwise it must be a directory.
 
     :return:
-        True if the directory was created, False if it already existed.
+        ``True`` if the directory was created, ``False`` if it already existed.
     """
     if is_file:
         path = osp.dirname(path)
@@ -734,7 +734,7 @@ def update(
             Current absolute count of items.
 
         :param max_count:
-            The maximum count of items we expect. It may be None in case there is no
+            The maximum count of items we expect. It may be ``None`` in case there is no
             maximum number of items or if it is (yet) unknown.
 
         :param message:

From fc1762b94ec9c8e22e0eb3b817604d15153f01b2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 17:26:38 -0500
Subject: [PATCH 507/994] Undo a couple minor black-incompatible changes

Two of the docstrings in git.remote need to have a newline before
the conceptually "first" line in order for black to accept the
indentation of the subsequent text, which appears intentional and
works fine as reStructuredText. Otherwise black dedents the code
block, which affects rendering and is also less readable.
---
 git/remote.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 265f1901f..3d5b8fdc4 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -125,7 +125,8 @@ def to_progress_instance(
 
 
 class PushInfo(IterableObj):
-    """Carries information about the result of a push operation of a single head::
+    """
+    Carries information about the result of a push operation of a single head::
 
         info = remote.push()[0]
         info.flags          # bitflags providing more information about the result
@@ -294,7 +295,8 @@ def raise_if_error(self) -> None:
 
 
 class FetchInfo(IterableObj):
-    """Carries information about the results of a fetch operation of a single head::
+    """
+    Carries information about the results of a fetch operation of a single head::
 
      info = remote.fetch()[0]
      info.ref           # Symbolic Reference or RemoteReference to the changed

From 1b25a13a36ec641ffd63a6f1a97c3bdf8f6d28de Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 17:30:46 -0500
Subject: [PATCH 508/994] Further revise docstrings within git.index

---
 git/index/base.py | 147 ++++++++++++++++++++++++----------------------
 git/index/fun.py  |   4 +-
 git/index/typ.py  |  36 +++++++-----
 3 files changed, 99 insertions(+), 88 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 5c738cdf2..249144d54 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -3,8 +3,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Module containing IndexFile, an Index implementation facilitating all kinds of index
-manipulations such as querying and merging."""
+"""Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
+of index manipulations such as querying and merging."""
 
 import contextlib
 import datetime
@@ -125,9 +125,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
     git command function calls wherever possible.
 
     This provides custom merging facilities allowing to merge without actually changing
-    your index or your working tree. This way you can perform own test-merges based on
-    the index only without having to deal with the working copy. This is useful in case
-    of partial working trees.
+    your index or your working tree. This way you can perform your own test merges based
+    on the index only without having to deal with the working copy. This is useful in
+    case of partial working trees.
 
     Entries:
 
@@ -211,7 +211,7 @@ def _deserialize(self, stream: IO) -> "IndexFile":
         return self
 
     def _entries_sorted(self) -> List[IndexEntry]:
-        """:return: list of entries, in a sorted fashion, first by path, then by stage"""
+        """:return: List of entries, in a sorted fashion, first by path, then by stage"""
         return sorted(self.entries.values(), key=lambda e: (e.path, e.stage))
 
     def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile":
@@ -232,17 +232,17 @@ def write(
         """Write the current state to our file path or to the given one.
 
         :param file_path:
-            If None, we will write to our stored file path from which we have been
+            If ``None``, we will write to our stored file path from which we have been
             initialized. Otherwise we write to the given file path. Please note that
-            this will change the file_path of this index to the one you gave.
+            this will change the `file_path` of this index to the one you gave.
 
         :param ignore_extension_data:
-            If True, the TREE type extension data read in the index will not be written
-            to disk. NOTE that no extension data is actually written.
-            Use this if you have altered the index and would like to use git-write-tree
-            afterwards to create a tree representing your written changes.
-            If this data is present in the written index, git-write-tree will instead
-            write the stored/cached tree.
+            If ``True``, the TREE type extension data read in the index will not be
+            written to disk. NOTE that no extension data is actually written.
+            Use this if you have altered the index and would like to use
+            ``git write-tree`` afterwards to create a tree representing your written
+            changes. If this data is present in the written index, ``git write-tree``
+            will instead write the stored/cached tree.
             Alternatively, use :meth:`write_tree` to handle this case automatically.
         """
         # Make sure we have our entries read before getting a write lock.
@@ -479,8 +479,8 @@ def _write_path_to_stdin(
             stdout string
 
         :param read_from_stdout:
-            If True, proc.stdout will be read after the item was sent to stdin. In that
-            case, it will return None.
+            If ``True``, ``proc.stdout`` will be read after the item was sent to stdin.
+            In that case, it will return ``None``.
 
         :note:
             There is a bug in git-update-index that prevents it from sending reports
@@ -516,12 +516,13 @@ def iter_blobs(
     ) -> Iterator[Tuple[StageType, Blob]]:
         """
         :return:
-            Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob).
+            Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and
+            stages, tuple(stage, Blob).
 
         :param predicate:
-            Function(t) returning True if tuple(stage, Blob) should be yielded by the
-            iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you to
-            yield blobs only if they match a given list of paths.
+            Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
+            the iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you
+            to yield blobs only if they match a given list of paths.
         """
         for entry in self.entries.values():
             blob = entry.to_blob(self.repo)
@@ -534,8 +535,8 @@ def iter_blobs(
     def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
         """
         :return:
-            Dict(path : list( tuple( stage, Blob, ...))), being a dictionary associating
-            a path in the index with a list containing sorted stage/blob pairs.
+            Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a
+            path in the index with a list containing sorted stage/blob pairs.
 
         :note:
             Blobs that have been removed in one side simply do not exist in the given
@@ -562,8 +563,8 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
         This will effectively remove the index entries of the respective path at all
         non-null stages and add the given blob as new stage null blob.
 
-        For each path there may only be one blob, otherwise a ValueError will be raised
-        claiming the path is already at stage 0.
+        For each path there may only be one blob, otherwise a :class:`ValueError` will
+        be raised claiming the path is already at stage 0.
 
         :raise ValueError:
             If one of the blobs already existed at stage 0.
@@ -603,7 +604,8 @@ def update(self) -> "IndexFile":
             This is a possibly dangerous operations as it will discard your changes to
             :attr:`index.entries <entries>`.
 
-        :return: self
+        :return:
+            self
         """
         self._delete_entries_cache()
         # Allows to lazily reread on demand.
@@ -654,8 +656,9 @@ def _process_diff_args(
 
     def _to_relative_path(self, path: PathLike) -> PathLike:
         """
-        :return: Version of path relative to our git directory or raise
-            :class:`ValueError` if it is not within our git directory.
+        :return:
+            Version of path relative to our git directory or raise :class:`ValueError`
+            if it is not within our git directory.
 
         :raise ValueError:
         """
@@ -693,7 +696,7 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry
         """Store file at filepath in the database and return the base index entry.
 
         :note:
-            This needs the git_working_dir decorator active!
+            This needs the :func:`~git.index.util.git_working_dir` decorator active!
             This must be ensured in the calling code.
         """
         st = os.lstat(filepath)  # Handles non-symlinks as well.
@@ -810,24 +813,25 @@ def add(
                 The handling now very much equals the way string paths are processed,
                 except that the mode you have set will be kept. This allows you to
                 create symlinks by settings the mode respectively and writing the target
-                of the symlink directly into the file. This equals a default
-                Linux symlink which is not dereferenced automatically, except that it
-                can be created on filesystems not supporting it as well.
+                of the symlink directly into the file. This equals a default Linux
+                symlink which is not dereferenced automatically, except that it can be
+                created on filesystems not supporting it as well.
 
                 Please note that globs or directories are not allowed in
-                :class:~`git.objects.blob.Blob` objects.
+                :class:`~git.objects.blob.Blob` objects.
 
                 They are added at stage 0.
 
             - :class:`~git.index.typ.BaseIndexEntry` or type
 
-                Handling equals the one of Blob objects, but the stage may be explicitly
-                set. Please note that Index Entries require binary sha's.
+                Handling equals the one of :class:~`git.objects.blob.Blob` objects, but
+                the stage may be explicitly set. Please note that Index Entries require
+                binary sha's.
 
         :param force:
             **CURRENTLY INEFFECTIVE**
-            If True, otherwise ignored or excluded files will be added anyway.
-            As opposed to the ``git add`` command, we enable this flag by default as the
+            If ``True``, otherwise ignored or excluded files will be added anyway. As
+            opposed to the ``git add`` command, we enable this flag by default as the
             API user usually wants the item to be added even though they might be
             excluded.
 
@@ -835,8 +839,10 @@ def add(
             Function with signature ``f(path, done=False, item=item)`` called for each
             path to be added, one time once it is about to be added where ``done=False``
             and once after it was added where ``done=True``.
-            ``item`` is set to the actual item we handle, either a Path or a
+
+            ``item`` is set to the actual item we handle, either a path or a
             :class:`~git.index.typ.BaseIndexEntry`.
+
             Please note that the processed path is not guaranteed to be present in the
             index already as the index is currently being processed.
 
@@ -845,24 +851,24 @@ def add(
             for each passed entry which is the path to be actually recorded for the
             object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
             This allows you to write an index which is not identical to the layout of
-            the actual files on your hard-disk. If not None and `items` contain plain
-            paths, these paths will be converted to Entries beforehand and passed to the
-            path_rewriter. Please note that ``entry.path`` is relative to the git
+            the actual files on your hard-disk. If not ``None`` and `items` contain
+            plain paths, these paths will be converted to Entries beforehand and passed
+            to the path_rewriter. Please note that ``entry.path`` is relative to the git
             repository.
 
         :param write:
-            If True, the index will be written once it was altered. Otherwise the
+            If ``True``, the index will be written once it was altered. Otherwise the
             changes only exist in memory and are not available to git commands.
 
         :param write_extension_data:
-            If True, extension data will be written back to the index. This can lead to
-            issues in case it is containing the 'TREE' extension, which will cause the
-            ``git commit`` command to write an old tree, instead of a new one
+            If ``True``, extension data will be written back to the index. This can lead
+            to issues in case it is containing the 'TREE' extension, which will cause
+            the ``git commit`` command to write an old tree, instead of a new one
             representing the now changed index.
 
             This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
-            'TREE' extension altogether. You should set it to True if you intend to use
-            :meth:`IndexFile.commit` exclusively while maintaining support for
+            'TREE' extension altogether. You should set it to ``True`` if you intend to
+            use :meth:`IndexFile.commit` exclusively while maintaining support for
             third-party extensions. Besides that, you can usually safely ignore the
             built-in extensions when using GitPython on repositories that are not
             handled manually at all.
@@ -875,7 +881,7 @@ def add(
             just actually added.
 
         :raise OSError:
-            If a supplied Path did not exist. Please note that
+            If a supplied path did not exist. Please note that
             :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
             will be added even if their paths do not exist.
         """
@@ -999,7 +1005,7 @@ def remove(
                 it. If absolute paths are given, they will be converted to a path
                 relative to the git repository directory containing the working tree
 
-                The path string may include globs, such as \*.c.
+                The path string may include globs, such as ``*.c``.
 
             - :class:~`git.objects.blob.Blob` object
 
@@ -1010,9 +1016,9 @@ def remove(
                 The only relevant information here is the path. The stage is ignored.
 
         :param working_tree:
-            If True, the entry will also be removed from the working tree, physically
-            removing the respective file. This may fail if there are uncommitted changes
-            in it.
+            If ``True``, the entry will also be removed from the working tree,
+            physically removing the respective file. This may fail if there are
+            uncommitted changes in it.
 
         :param kwargs:
             Additional keyword arguments to be passed to ``git rm``, such as ``r`` to
@@ -1061,7 +1067,7 @@ def move(
             for reference.
 
         :param skip_errors:
-            If True, errors such as ones resulting from missing source files will be
+            If ``True``, errors such as ones resulting from missing source files will be
             skipped.
 
         :param kwargs:
@@ -1214,21 +1220,21 @@ def checkout(
             case you have altered the entries dictionary directly.
 
         :param paths:
-            If None, all paths in the index will be checked out. Otherwise an iterable
-            of relative or absolute paths or a single path pointing to files or
-            directories in the index is expected.
+            If ``None``, all paths in the index will be checked out.
+            Otherwise an iterable of relative or absolute paths or a single path
+            pointing to files or directories in the index is expected.
 
         :param force:
-            If True, existing files will be overwritten even if they contain local
+            If ``True``, existing files will be overwritten even if they contain local
             modifications.
-            If False, these will trigger a :class:`~git.exc.CheckoutError`.
+            If ``False``, these will trigger a :class:`~git.exc.CheckoutError`.
 
         :param fprogress:
             See :meth:`IndexFile.add` for signature and explanation.
 
-            The provided progress information will contain None as path and item if no
-            explicit paths are given. Otherwise progress information will be send prior
-            and after a file has been checked out.
+            The provided progress information will contain ``None`` as path and item if
+            no explicit paths are given. Otherwise progress information will be send
+            prior and after a file has been checked out.
 
         :param kwargs:
             Additional arguments to be passed to ``git checkout-index``.
@@ -1238,10 +1244,10 @@ def checkout(
             guaranteed to match the version stored in the index.
 
         :raise git.exc.CheckoutError:
-            If at least one file failed to be checked out. This is a summary, hence it
-            will checkout as many files as it can anyway.
-            If one of files or directories do not exist in the index (as opposed to the
-            original git command, which ignores them).
+            * If at least one file failed to be checked out. This is a summary, hence it
+              will checkout as many files as it can anyway.
+            * If one of files or directories do not exist in the index (as opposed to
+              the original git command, which ignores them).
 
         :raise git.exc.GitCommandError:
             If error lines could not be parsed - this truly is an exceptional state.
@@ -1394,7 +1400,7 @@ def reset(
         **kwargs: Any,
     ) -> "IndexFile":
         """Reset the index to reflect the tree at the given commit. This will not adjust
-        our ``HEAD`` reference by default, as opposed to
+        our HEAD reference by default, as opposed to
         :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
 
         :param commit:
@@ -1406,14 +1412,14 @@ def reset(
             overwrite the default index.
 
         :param working_tree:
-            If True, the files in the working tree will reflect the changed index.
-            If False, the working tree will not be touched.
+            If ``True``, the files in the working tree will reflect the changed index.
+            If ``False``, the working tree will not be touched.
             Please note that changes to the working copy will be discarded without
             warning!
 
         :param head:
-            If True, the head will be set to the given commit. This is False by default,
-            but if True, this method behaves like
+            If ``True``, the head will be set to the given commit. This is ``False`` by
+            default, but if ``True``, this method behaves like
             :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
 
         :param paths:
@@ -1433,7 +1439,8 @@ def reset(
             If you want ``git reset``-like behaviour, use
             :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
 
-        :return: self
+        :return:
+            self
         """
         # What we actually want to do is to merge the tree into our existing index,
         # which is what git-read-tree does.
diff --git a/git/index/fun.py b/git/index/fun.py
index 1d4ca9b93..58335739e 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -222,8 +222,8 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
 def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]:
     """
     :return:
-        Key suitable to be used for the :attr:`index.entries
-        <git.index.base.IndexFile.entries>` dictionary.
+        Key suitable to be used for the
+        :attr:`index.entries <git.index.base.IndexFile.entries>` dictionary.
 
     :param entry:
         One instance of type BaseIndexEntry or the path and the stage.
diff --git a/git/index/typ.py b/git/index/typ.py
index dce67626f..a7d2ad47a 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -36,9 +36,9 @@
 
 
 class BlobFilter:
-    """
-    Predicate to be used by iter_blobs allowing to filter only return blobs which
-    match the given list of directories or files.
+    """Predicate to be used by
+    :meth:`IndexFile.iter_blobs <git.index.base.IndexFile.iter_blobs>` allowing to
+    filter only return blobs which match the given list of directories or files.
 
     The given paths are given relative to the repository.
     """
@@ -48,8 +48,8 @@ class BlobFilter:
     def __init__(self, paths: Sequence[PathLike]) -> None:
         """
         :param paths:
-            Tuple or list of paths which are either pointing to directories or
-            to files relative to the current repository
+            Tuple or list of paths which are either pointing to directories or to files
+            relative to the current repository.
         """
         self.paths = paths
 
@@ -70,7 +70,7 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
 
 
 class BaseIndexEntryHelper(NamedTuple):
-    """Typed named tuple to provide named attribute access for BaseIndexEntry.
+    """Typed named tuple to provide named attribute access for :class:`BaseIndexEntry`.
 
     This is needed to allow overriding ``__new__`` in child class to preserve backwards
     compatibility.
@@ -90,12 +90,12 @@ class BaseIndexEntryHelper(NamedTuple):
 
 
 class BaseIndexEntry(BaseIndexEntryHelper):
-    """Small brother of an index entry which can be created to describe changes
+    R"""Small brother of an index entry which can be created to describe changes
     done to the index in which case plenty of additional information is not required.
 
-    As the first 4 data members match exactly to the IndexEntry type, methods
-    expecting a BaseIndexEntry can also handle full IndexEntries even if they
-    use numeric indices for performance reasons.
+    As the first 4 data members match exactly to the :class:`IndexEntry` type, methods
+    expecting a :class:`BaseIndexEntry` can also handle full :class:`IndexEntry`\s even
+    if they use numeric indices for performance reasons.
     """
 
     def __new__(
@@ -127,9 +127,11 @@ def stage(self) -> int:
             * 0 = default stage
             * 1 = stage before a merge or common ancestor entry in case of a 3 way merge
             * 2 = stage of entries from the 'left' side of the merge
-            * 3 = stage of entries from the right side of the merge
+            * 3 = stage of entries from the 'right' side of the merge
 
-        :note: For more information, see http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
+        :note:
+            For more information, see:
+            http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
         """
         return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
 
@@ -144,7 +146,8 @@ def to_blob(self, repo: "Repo") -> Blob:
 
 
 class IndexEntry(BaseIndexEntry):
-    """Allows convenient access to IndexEntry data without completely unpacking it.
+    """Allows convenient access to index entry data as defined in
+    :class:`BaseIndexEntry` without completely unpacking it.
 
     Attributes usually accessed often are cached in the tuple whereas others are
     unpacked on demand.
@@ -163,17 +166,18 @@ def ctime(self) -> Tuple[int, int]:
 
     @property
     def mtime(self) -> Tuple[int, int]:
-        """See ctime property, but returns modification time."""
+        """See :attr:`ctime` property, but returns modification time."""
         return cast(Tuple[int, int], unpack(">LL", self.mtime_bytes))
 
     @classmethod
     def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry":
         """
         :return:
-            Minimal entry as created from the given BaseIndexEntry instance.
+            Minimal entry as created from the given :class:`BaseIndexEntry` instance.
             Missing values will be set to null-like values.
 
-        :param base: Instance of type :class:`BaseIndexEntry`
+        :param base:
+            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))

From 08a80aa1d91af4745dd369db7ad48a23df7145a9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 17:49:11 -0500
Subject: [PATCH 509/994] Further revise docstrings within
 git.objects.submodule

---
 git/objects/submodule/base.py | 128 +++++++++++++++++++---------------
 git/objects/submodule/root.py |  28 ++++----
 git/objects/submodule/util.py |   2 +-
 3 files changed, 86 insertions(+), 72 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 159403f24..dc1deee36 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -266,8 +266,8 @@ def _clear_cache(self) -> None:
     def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
         """
         :return:
-            Configuration file as BytesIO - we only access it through the respective
-            blob's data
+            Configuration file as :class:`~io.BytesIO` - we only access it through the
+            respective blob's data
         """
         sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
         sio.name = cls.k_modules_file
@@ -354,7 +354,7 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
         """:return: a path guaranteed to be relative to the given parent repository
 
         :raise ValueError:
-            If path is not contained in the parent repository's working tree
+            If path is not contained in the parent repository's working tree.
         """
         path = to_native_path_linux(path)
         if path.endswith("/"):
@@ -378,8 +378,8 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
 
     @classmethod
     def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
-        """Write a .git file containing a (preferably) relative path to the actual git
-        module repository.
+        """Write a ``.git`` file containing a (preferably) relative path to the actual
+        git module repository.
 
         It is an error if the `module_abspath` cannot be made into a relative path,
         relative to the `working_tree_dir`.
@@ -430,10 +430,10 @@ def add(
         allow_unsafe_options: bool = False,
         allow_unsafe_protocols: bool = False,
     ) -> "Submodule":
-        """Add a new submodule to the given repository. This will alter the index
-        as well as the ``.gitmodules`` file, but will not create a new commit.
-        If the submodule already exists, no matter if the configuration differs
-        from the one provided, the existing submodule will be returned.
+        """Add a new submodule to the given repository. This will alter the index as
+        well as the ``.gitmodules`` file, but will not create a new commit. If the
+        submodule already exists, no matter if the configuration differs from the one
+        provided, the existing submodule will be returned.
 
         :param repo:
             Repository instance which should receive the submodule.
@@ -448,23 +448,23 @@ def add(
 
         :param url:
             git-clone compatible URL, see git-clone reference for more information.
-            If None, the repository is assumed to exist, and the url of the first remote
-            is taken instead. This is useful if you want to make an existing repository
-            a submodule of another one.
+            If ``None```, the repository is assumed to exist, and the url of the first
+            remote is taken instead. This is useful if you want to make an existing
+            repository a submodule of another one.
 
         :param branch:
             Name of branch at which the submodule should (later) be checked out. The
             given branch must exist in the remote repository, and will be checked out
             locally as a tracking branch.
-            It will only be written into the configuration if it not None, which is when
-            the checked out branch will be the one the remote HEAD pointed to.
+            It will only be written into the configuration if it not ``None``, which is
+            when the checked out branch will be the one the remote HEAD pointed to.
             The result you get in these situation is somewhat fuzzy, and it is
             recommended to specify at least ``master`` here.
             Examples are ``master`` or ``feature/new``.
 
         :param no_checkout:
-            If True, and if the repository has to be cloned manually, no checkout will
-            be performed.
+            If ``True``, and if the repository has to be cloned manually, no checkout
+            will be performed.
 
         :param depth:
             Create a shallow clone with a history truncated to the specified number of
@@ -479,7 +479,8 @@ def add(
             want to unset some variable, consider providing an empty string as its
             value.
 
-        :param clone_multi_options: A list of Clone options. Please see
+        :param clone_multi_options:
+            A list of clone options. Please see
             :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
 
         :param allow_unsafe_protocols:
@@ -632,44 +633,49 @@ def update(
         with the binsha of this instance.
 
         :param recursive:
-            If True, we will operate recursively and update child modules as well.
+            If ``True``, we will operate recursively and update child modules as well.
 
         :param init:
-            If True, the module repository will be cloned into place if necessary.
+            If ``True``, the module repository will be cloned into place if necessary.
 
         :param to_latest_revision:
-            If True, the submodule's sha will be ignored during checkout. Instead, the
-            remote will be fetched, and the local tracking branch updated. This only
+            If ``True``, the submodule's sha will be ignored during checkout. Instead,
+            the remote will be fetched, and the local tracking branch updated. This only
             works if we have a local tracking branch, which is the case if the remote
-            repository had a master branch, or of the 'branch' option was specified for
-            this submodule and the branch existed remotely.
+            repository had a master branch, or of the ``branch`` option was specified
+            for this submodule and the branch existed remotely.
 
         :param progress:
-            :class:`UpdateProgress` instance, or None if no progress should be shown.
+            :class:`UpdateProgress` instance, or ``None`` if no progress should be
+            shown.
 
         :param dry_run:
-            If True, the operation will only be simulated, but not performed.
+            If ``True``, the operation will only be simulated, but not performed.
             All performed operations are read-only.
 
         :param force:
-            If True, we may reset heads even if the repository in question is dirty.
+            If ``True``, we may reset heads even if the repository in question is dirty.
             Additionally we will be allowed to set a tracking branch which is ahead of
             its remote branch back into the past or the location of the remote branch.
             This will essentially 'forget' commits.
-            If False, local tracking branches that are in the future of their respective
-            remote branches will simply not be moved.
+
+            If ``False``, local tracking branches that are in the future of their
+            respective remote branches will simply not be moved.
 
         :param keep_going:
-            If True, we will ignore but log all errors, and keep going recursively.
+            If ``True``, we will ignore but log all errors, and keep going recursively.
             Unless `dry_run` is set as well, `keep_going` could cause
             subsequent/inherited errors you wouldn't see otherwise.
             In conjunction with `dry_run`, it can be useful to anticipate all errors
             when updating submodules.
 
-        :param env: Optional dictionary containing the desired environment variables.
+        :param env:
+            Optional dictionary containing the desired environment variables.
+
             Note: Provided variables will be used to update the execution environment
             for ``git``. If some variable is not specified in `env` and is defined in
             attr:`os.environ`, value from attr:`os.environ` will be used.
+
             If you want to unset some variable, consider providing the empty string as
             its value.
 
@@ -688,7 +694,7 @@ def update(
             Does nothing in bare repositories.
 
         :note:
-            This method is definitely not atomic if `recursive` is True.
+            This method is definitely not atomic if `recursive` is ``True``.
 
         :return:
             self
@@ -950,18 +956,18 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
 
         :param module_path:
             The path to which to move our module in the parent repository's working
-            tree, given as repository - relative or absolute path. Intermediate
+            tree, given as repository-relative or absolute path. Intermediate
             directories will be created accordingly. If the path already exists, it must
             be empty. Trailing (back)slashes are removed automatically.
 
         :param configuration:
-            If True, the configuration will be adjusted to let the submodule point to
-            the given path.
+            If ``True``, the configuration will be adjusted to let the submodule point
+            to the given path.
 
         :param module:
-            If True, the repository managed by this submodule will be moved as well. If
-            False, we don't move the submodule's checkout, which may leave the parent
-            repository in an inconsistent state.
+            If ``True``, the repository managed by this submodule will be moved as well.
+            If ``False``, we don't move the submodule's checkout, which may leave the
+            parent repository in an inconsistent state.
 
         :return:
             self
@@ -1074,12 +1080,13 @@ def remove(
         from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
 
         :param module:
-            If True, the checked out module we point to will be deleted as well. If that
-            module is currently on a commit outside any branch in the remote, or if it
-            is ahead of its tracking branch, or if there are modified or untracked files
-            in its working tree, then the removal will fail. In case the removal of the
-            repository fails for these reasons, the submodule status will not have been
-            altered.
+            If ``True``, the checked out module we point to will be deleted as well. If
+            that module is currently on a commit outside any branch in the remote, or if
+            it is ahead of its tracking branch, or if there are modified or untracked
+            files in its working tree, then the removal will fail. In case the removal
+            of the repository fails for these reasons, the submodule status will not
+            have been altered.
+
             If this submodule has child modules of its own, these will be deleted prior
             to touching the direct submodule.
 
@@ -1088,12 +1095,12 @@ def remove(
             This basically enforces a brute-force file system based deletion.
 
         :param configuration:
-            If True, the submodule is deleted from the configuration, otherwise it
+            If ``True``, the submodule is deleted from the configuration, otherwise it
             isn't. Although this should be enabled most of the time, this flag enables
             you to safely delete the repository of your submodule.
 
         :param dry_run:
-            If True, we will not actually do anything, but throw the errors we would
+            If ``True``, we will not actually do anything, but throw the errors we would
             usually throw.
 
         :return:
@@ -1239,12 +1246,12 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True)
         contain the ``.gitmodules`` blob.
 
         :param commit:
-            Commit-ish reference pointing at the root_tree, or None to always point to
-            the most recent commit
+            Commit-ish reference pointing at the root_tree, or ``None`` to always point
+            to the most recent commit.
 
         :param check:
-            If True, relatively expensive checks will be performed to verify validity of
-            the submodule.
+            If ``True``, relatively expensive checks will be performed to verify
+            validity of the submodule.
 
         :raise ValueError:
             If the commit's tree didn't contain the ``.gitmodules`` blob.
@@ -1297,8 +1304,9 @@ def config_writer(
             to this submodule into the ``.gitmodules`` file.
 
         :param index:
-            If not None, an IndexFile instance which should be written.
-            Defaults to the index of the Submodule's parent repository.
+            If not ``None``, an :class:`~git.index.base.IndexFile` instance which should
+            be written. Defaults to the index of the :class:`Submodule`'s parent
+            repository.
 
         :param write:
             If True, the index will be written each time a configuration value changes.
@@ -1380,7 +1388,9 @@ def rename(self, new_name: str) -> "Submodule":
     @unbare_repo
     def module(self) -> "Repo":
         """
-        :return: Repo instance initialized from the repository at our submodule path
+        :return:
+            :class:`~git.repo.base.Repo` instance initialized from the repository at our
+            submodule path
 
         :raise git.exc.InvalidGitRepositoryError:
             If a repository was not available.
@@ -1401,7 +1411,7 @@ def module(self) -> "Repo":
     def module_exists(self) -> bool:
         """
         :return:
-            True if our module exists and is a valid git repository.
+            ``True`` if our module exists and is a valid git repository.
             See the :meth:`module` method.
         """
         try:
@@ -1414,7 +1424,7 @@ def module_exists(self) -> bool:
     def exists(self) -> bool:
         """
         :return:
-            True if the submodule exists, False otherwise.
+            ``True`` if the submodule exists, ``False`` otherwise.
             Please note that a submodule may exist (in the ``.gitmodules`` file) even
             though its module doesn't exist on disk.
         """
@@ -1480,14 +1490,15 @@ def branch_name(self) -> str:
 
     @property
     def url(self) -> str:
-        """:return: The url to the repository which our module - repository refers to"""
+        """:return: The url to the repository our submodule's repository refers to"""
         return self._url
 
     @property
     def parent_commit(self) -> "Commit_ish":
         """
         :return:
-            Commit instance with the tree containing the ``.gitmodules`` file
+            :class:`~git.objects.commit.Commit` instance with the tree containing the
+            ``.gitmodules`` file
 
         :note:
             Will always point to the current head's commit if it was not set explicitly.
@@ -1531,8 +1542,9 @@ def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
     def children(self) -> IterableList["Submodule"]:
         """
         :return:
-            IterableList(Submodule, ...) An iterable list of submodules instances which
-            are children of this submodule or 0 if the submodule is not checked out.
+            IterableList(Submodule, ...) An iterable list of :class:`Submodule`
+            instances which are children of this submodule or 0 if the submodule is not
+            checked out.
         """
         return self._get_intermediate_items(self)
 
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 8ef874f90..ac0f7ad94 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -89,9 +89,9 @@ def update(
     ) -> "RootModule":
         """Update the submodules of this repository to the current HEAD commit.
 
-        This method behaves smartly by determining changes of the path of a submodules
+        This method behaves smartly by determining changes of the path of a submodule's
         repository, next to changes to the to-be-checked-out commit or the branch to be
-        checked out. This works if the submodules ID does not change.
+        checked out. This works if the submodule's ID does not change.
 
         Additionally it will detect addition and removal of submodules, which will be
         handled gracefully.
@@ -99,11 +99,11 @@ def update(
         :param previous_commit:
             If set to a commit-ish, the commit we should use as the previous commit the
             HEAD pointed to before it was set to the commit it points to now.
-            If None, it defaults to ``HEAD@{1}`` otherwise.
+            If ``None``, it defaults to ``HEAD@{1}`` otherwise.
 
         :param recursive:
-            If True, the children of submodules will be updated as well using the same
-            technique.
+            If ``True``, the children of submodules will be updated as well using the
+            same technique.
 
         :param force_remove:
             If submodules have been deleted, they will be forcibly removed. Otherwise
@@ -116,28 +116,30 @@ def update(
             If we encounter a new module which would need to be initialized, then do it.
 
         :param to_latest_revision:
-            If True, instead of checking out the revision pointed to by this submodule's
-            sha, the checked out tracking branch will be merged with the latest remote
-            branch fetched from the repository's origin.
+            If ``True``, instead of checking out the revision pointed to by this
+            submodule's sha, the checked out tracking branch will be merged with the
+            latest remote branch fetched from the repository's origin.
+
             Unless `force_reset` is specified, a local tracking branch will never be
             reset into its past, therefore the remote branch must be in the future for
             this to have an effect.
 
         :param force_reset:
-            If True, submodules may checkout or reset their branch even if the
+            If ``True``, submodules may checkout or reset their branch even if the
             repository has pending changes that would be overwritten, or if the local
             tracking branch is in the future of the remote tracking branch and would be
             reset into its past.
 
         :param progress:
-            :class:`RootUpdateProgress` instance or None if no progress should be sent.
+            :class:`RootUpdateProgress` instance, or ``None`` if no progress should be
+            sent.
 
         :param dry_run:
-            If True, operations will not actually be performed. Progress messages will
-            change accordingly to indicate the WOULD DO state of the operation.
+            If ``True``, operations will not actually be performed. Progress messages
+            will change accordingly to indicate the WOULD DO state of the operation.
 
         :param keep_going:
-            If True, we will ignore but log all errors, and keep going recursively.
+            If ``True``, we will ignore but log all errors, and keep going recursively.
             Unless `dry_run` is set as well, `keep_going` could cause
             subsequent/inherited errors you wouldn't see otherwise.
             In conjunction with `dry_run`, this can be useful to anticipate all errors
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index b02da501e..e5af5eb31 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -52,7 +52,7 @@ def mkhead(repo: "Repo", path: PathLike) -> "Head":
 
 def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
     """Find the remote branch matching the name of the given branch or raise
-    :class:`git.exc.InvalidGitRepositoryError`."""
+    :class:`~git.exc.InvalidGitRepositoryError`."""
     for remote in remotes:
         try:
             return remote.refs[branch_name]

From bc48d264df657f3d5593760365427467980b1ef9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 18:46:22 -0500
Subject: [PATCH 510/994] Further revise other docstrings within git.objects

---
 git/objects/base.py   | 12 ++++++------
 git/objects/blob.py   |  3 ++-
 git/objects/commit.py | 28 ++++++++++++++--------------
 git/objects/fun.py    | 14 ++++++++------
 git/objects/tree.py   | 23 +++++++++++------------
 git/objects/util.py   | 37 +++++++++++++++++++------------------
 6 files changed, 60 insertions(+), 57 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index df56a4bac..5cee8e405 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -58,7 +58,7 @@ class Object(LazyMixin):
     def __init__(self, repo: "Repo", binsha: bytes):
         """Initialize an object by identifying it by its binary sha.
 
-        All keyword arguments will be set on demand if None.
+        All keyword arguments will be set on demand if ``None``.
 
         :param repo:
             Repository this object is located in.
@@ -83,7 +83,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
             input id may have been a `~git.refs.reference.Reference` or rev-spec.
 
         :param id:
-            :class:`~git.refs.reference.Reference`, rev-spec, or hexsha
+            :class:`~git.refs.reference.Reference`, rev-spec, or hexsha.
 
         :note:
             This cannot be a ``__new__`` method as it would always call :meth:`__init__`
@@ -119,13 +119,13 @@ def _set_cache_(self, attr: str) -> None:
             super()._set_cache_(attr)
 
     def __eq__(self, other: Any) -> bool:
-        """:return: True if the objects have the same SHA1"""
+        """:return: ``True`` if the objects have the same SHA1"""
         if not hasattr(other, "binsha"):
             return False
         return self.binsha == other.binsha
 
     def __ne__(self, other: Any) -> bool:
-        """:return: True if the objects do not have the same SHA1"""
+        """:return: ``True`` if the objects do not have the same SHA1"""
         if not hasattr(other, "binsha"):
             return True
         return self.binsha != other.binsha
@@ -199,8 +199,8 @@ def __init__(
             20 byte sha1.
 
         :param mode:
-            The stat compatible file mode as int, use the :mod:`stat` module to evaluate
-            the information.
+            The stat-compatible file mode as :class:`int`.
+            Use the :mod:`stat` module to evaluate the information.
 
         :param path:
             The path to the file in the file system, relative to the git repository
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 50c12f0d7..253ceccb5 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -27,7 +27,8 @@ class Blob(base.IndexObject):
     @property
     def mime_type(self) -> str:
         """
-        :return: String describing the mime type of this file (based on the filename)
+        :return:
+            String describing the mime type of this file (based on the filename)
 
         :note:
             Defaults to ``text/plain`` in case the actual file type is unknown.
diff --git a/git/objects/commit.py b/git/objects/commit.py
index e6aa7ac39..b5b1c540d 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -108,11 +108,11 @@ def __init__(
         encoding: Union[str, None] = None,
         gpgsig: Union[str, None] = None,
     ) -> None:
-        R"""Instantiate a new :class:`Commit`. All keyword arguments taking None as
+        R"""Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
         default will be implicitly set on first query.
 
         :param binsha:
-            20 byte sha1
+            20 byte sha1.
 
         :param parents: tuple(Commit, ...)
             A tuple of commit ids or actual :class:`Commit`\s.
@@ -120,8 +120,8 @@ def __init__(
         :param tree:
             A :class:`~git.objects.tree.Tree` object.
 
-        :param author: :class:`~git.util.Actor`
-            The author Actor object
+        :param author:
+            The author :class:`~git.util.Actor` object.
 
         :param authored_date: int_seconds_since_epoch
             The authored DateTime - use :func:`time.gmtime` to convert it into a
@@ -130,8 +130,8 @@ def __init__(
         :param author_tz_offset: int_seconds_west_of_utc
             The timezone that the `authored_date` is in.
 
-        :param committer: :class:`~git.util.Actor`
-            The committer string.
+        :param committer:
+            The committer string, as an :class:`~git.util.Actor` object.
 
         :param committed_date: int_seconds_since_epoch
             The committed DateTime - use :func:`time.gmtime` to convert it into a
@@ -209,7 +209,7 @@ def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes:
         return istream.binsha
 
     def replace(self, **kwargs: Any) -> "Commit":
-        """Create new commit object from existing commit object.
+        """Create new commit object from an existing commit object.
 
         Any values provided as keyword arguments will replace the corresponding
         attribute in the new object.
@@ -295,7 +295,7 @@ def iter_items(
         R"""Find all commits matching the given criteria.
 
         :param repo:
-            The Repo
+            The :class:`~git.repo.base.Repo`.
 
         :param rev:
             Revision specifier, see git-rev-parse for viable options.
@@ -378,7 +378,7 @@ def stats(self) -> Stats:
 
     @property
     def trailers(self) -> Dict[str, str]:
-        """Get the trailers of the message as a dictionary
+        """Deprecated. Get the trailers of the message as a dictionary.
 
         :note: This property is deprecated, please use either :attr:`trailers_list` or
             :attr:`trailers_dict``.
@@ -396,7 +396,7 @@ def trailers_list(self) -> List[Tuple[str, str]]:
         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
+        This function 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::
@@ -444,7 +444,7 @@ def trailers_dict(self) -> Dict[str, List[str]]:
         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
+        This function calls ``git interpret-trailers --parse`` onto the message to
         extract the trailer information. The key value pairs are stripped of leading and
         trailing whitespaces before they get saved into a dictionary.
 
@@ -481,7 +481,7 @@ def trailers_dict(self) -> Dict[str, List[str]]:
     def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]:
         """Parse out commit information into a list of :class:`Commit` objects.
 
-        We expect one-line per commit, and parse the actual commit information directly
+        We expect one line per commit, and parse the actual commit information directly
         from our lighting fast object database.
 
         :param proc:
@@ -555,11 +555,11 @@ def create_from_tree(
         :param parent_commits:
             Optional :class:`Commit` objects to use as parents for the new commit. If
             empty list, the commit will have no parents at all and become a root commit.
-            If None, the current head commit will be the parent of the new commit
+            If ``None``, the current head commit will be the parent of the new commit
             object.
 
         :param head:
-            If True, the HEAD will be advanced to the new commit automatically.
+            If ``True``, the HEAD will be advanced to the new commit automatically.
             Otherwise the HEAD will remain pointing on the previous commit. This could
             lead to undesired results when diffing files.
 
diff --git a/git/objects/fun.py b/git/objects/fun.py
index d349737de..ad5bc9a88 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -128,7 +128,7 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
 
 
 def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone:
-    """Return data entry matching the given name and tree mode or None.
+    """Return data entry matching the given name and tree mode or ``None``.
 
     Before the item is returned, the respective data item is set None in the `tree_data`
     list to mark it done.
@@ -172,7 +172,8 @@ def traverse_trees_recursive(
     odb: "GitCmdObjectDB", tree_shas: Sequence[Union[bytes, None]], path_prefix: str
 ) -> List[Tuple[EntryTupOrNone, ...]]:
     """
-    :return: list of list with entries according to the given binary tree-shas.
+    :return:
+        List of list with entries according to the given binary tree-shas.
 
         The result is encoded in a list
         of n tuple|None per blob/commit, (n == len(tree_shas)), where:
@@ -181,12 +182,12 @@ def traverse_trees_recursive(
         * [1] == mode as int
         * [2] == path relative to working tree root
 
-        The entry tuple is None if the respective blob/commit did not exist in the given
-        tree.
+        The entry tuple is ``None`` if the respective blob/commit did not exist in the
+        given tree.
 
     :param tree_shas:
         Iterable of shas pointing to trees. All trees must be on the same level. A
-        tree-sha may be None in which case None.
+        tree-sha may be ``None`` in which case ``None``.
 
     :param path_prefix:
         A prefix to be added to the returned paths on this level.
@@ -257,7 +258,8 @@ def traverse_trees_recursive(
 
 def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]:
     """
-    :return: list of entries of the tree pointed to by the binary `tree_sha`.
+    :return:
+        List of entries of the tree pointed to by the binary `tree_sha`.
 
         An entry has the following format:
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index da682b8cd..69cd6ef3f 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -97,17 +97,17 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
 
         If an item with the given name already exists, nothing will be done, but a
         :class:`ValueError` will be raised if the sha and mode of the existing item do
-        not match the one you add, unless `force` is True.
+        not match the one you add, unless `force` is ``True``.
 
         :param sha:
             The 20 or 40 byte sha of the item to add.
 
         :param mode:
-            int representing the stat compatible mode of the item.
+            :class:`int` representing the stat-compatible mode of the item.
 
         :param force:
-            If True, an item with your name and information will overwrite any existing
-            item with the same name, no matter which information it has.
+            If ``True``, an item with your name and information will overwrite any
+            existing item with the same name, no matter which information it has.
 
         :return:
             self
@@ -164,11 +164,10 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
     other :class:`~git.objects.tree.Tree`\s.
 
-    Tree as a list::
+    Tree as a list:
 
-        Access a specific blob using the ``tree['filename']`` notation.
-
-        You may likewise access by index, like ``blob = tree[0]``.
+    * Access a specific blob using the ``tree['filename']`` notation.
+    * You may likewise access by index, like ``blob = tree[0]``.
     """
 
     type: Literal["tree"] = "tree"
@@ -235,7 +234,7 @@ def join(self, file: str) -> IndexObjUnion:
             or :class:`~git.objects.submodule.base.Submodule`
 
         :raise KeyError:
-            If the given file or tree does not exist in tree.
+            If the given file or tree does not exist in this tree.
         """
         msg = "Blob or Tree named %r not found"
         if "/" in file:
@@ -275,12 +274,12 @@ def __truediv__(self, file: str) -> IndexObjUnion:
 
     @property
     def trees(self) -> List["Tree"]:
-        """:return: list(Tree, ...) list of trees directly below this tree"""
+        """:return: list(Tree, ...) List of trees directly below this tree"""
         return [i for i in self if i.type == "tree"]
 
     @property
     def blobs(self) -> List[Blob]:
-        """:return: list(Blob, ...) list of blobs directly below this tree"""
+        """:return: list(Blob, ...) List of blobs directly below this tree"""
         return [i for i in self if i.type == "blob"]
 
     @property
@@ -342,7 +341,7 @@ def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion
             :class:`~git.util.IterableList`IterableList with the results of the
             traversal as produced by :meth:`traverse`
 
-            Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']]
+            Tree -> IterableList[Union[Submodule, Tree, Blob]]
         """
         return super()._list_traverse(*args, **kwargs)
 
diff --git a/git/objects/util.py b/git/objects/util.py
index 7ad20d66e..6f46bab18 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -95,9 +95,9 @@ def mode_str_to_int(modestr: Union[bytes, str]) -> int:
         used.
 
     :return:
-        String identifying a mode compatible to the mode methods ids of the stat module
-        regarding the rwx permissions for user, group and other, special flags and file
-        system flags, such as whether it is a symlink.
+        String identifying a mode compatible to the mode methods ids of the :mod:`stat`
+        module regarding the rwx permissions for user, group and other, special flags
+        and file system flags, such as whether it is a symlink.
     """
     mode = 0
     for iteration, char in enumerate(reversed(modestr[-6:])):
@@ -401,7 +401,7 @@ class Tree::       (cls, Tree) -> Tuple[Tree, ...]
     def list_traverse(self, *args: Any, **kwargs: Any) -> Any:
         """Traverse self and collect all items found.
 
-        Calling this directly only the abstract base class, including via a ``super()``
+        Calling this directly on the abstract base class, including via a ``super()``
         proxy, is deprecated. Only overridden implementations should be called.
         """
         warnings.warn(
@@ -418,12 +418,13 @@ def _list_traverse(
     ) -> IterableList[Union["Commit", "Submodule", "Tree", "Blob"]]:
         """Traverse self and collect all items found.
 
-        :return: :class:`~git.util.IterableList` with the results of the traversal as
+        :return:
+            :class:`~git.util.IterableList` with the results of the traversal as
             produced by :meth:`traverse`::
 
-                Commit -> IterableList["Commit"]
-                Submodule ->  IterableList["Submodule"]
-                Tree -> IterableList[Union["Submodule", "Tree", "Blob"]]
+                Commit -> IterableList[Commit]
+                Submodule ->  IterableList[Submodule]
+                Tree -> IterableList[Union[Submodule, Tree, Blob]]
         """
         # Commit and Submodule have id.__attribute__ as IterableObj.
         # Tree has id.__attribute__ inherited from IndexObject.
@@ -476,12 +477,12 @@ def _traverse(
         """Iterator yielding items found when traversing self.
 
         :param predicate:
-            A function ``f(i,d)`` that returns False if item i at depth ``d`` should not
-            be included in the result.
+            A function ``f(i,d)`` that returns ``False`` if item i at depth ``d`` should
+            not be included in the result.
 
         :param prune:
-            A function ``f(i,d)`` that returns True if the search should stop at item
-            ``i`` at depth ``d``. Item ``i`` will not be returned.
+            A function ``f(i,d)`` that returns ``True`` if the search should stop at
+            item ``i`` at depth ``d``. Item ``i`` will not be returned.
 
         :param depth:
             Defines at which level the iteration should not go deeper if -1, there is no
@@ -489,19 +490,19 @@ def _traverse(
             i.e. if 1, you would only get the first level of predecessors/successors.
 
         :param branch_first:
-            If True, items will be returned branch first, otherwise depth first.
+            If ``True``, items will be returned branch first, otherwise depth first.
 
         :param visit_once:
-            If True, items will only be returned once, although they might be
+            If ``True``, items will only be returned once, although they might be
             encountered several times. Loops are prevented that way.
 
         :param ignore_self:
-            If True, self will be ignored and automatically pruned from the result.
-            Otherwise it will be the first item to be returned. If `as_edge` is True, the
-            source of the first edge is None
+            If ``True``, self will be ignored and automatically pruned from the result.
+            Otherwise it will be the first item to be returned. If `as_edge` is
+            ``True``, the source of the first edge is ``None``.
 
         :param as_edge:
-            If True, return a pair of items, first being the source, second the
+            If ``True``, return a pair of items, first being the source, second the
             destination, i.e. tuple(src, dest) with the edge spanning from source to
             destination.
 

From 30f7da508154df8b968f171b789c366c52276cee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 18:50:46 -0500
Subject: [PATCH 511/994] Fix erroneous reference to DateTime "class"

DateTime is not a class here, and the parameter is expected as int.
This fixes a documentation bug I introduced in cd16a35 (#1725).
---
 git/objects/tag.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/tag.py b/git/objects/tag.py
index 211c84ac7..357c8ff7e 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -68,7 +68,7 @@ def __init__(
             :class:`~git.util.Actor` identifying the tagger.
 
         :param tagged_date: int_seconds_since_epoch
-            The :class:`DateTime` of the tag creation.
+            The DateTime of the tag creation.
             Use :func:`time.gmtime` to convert it into a different format.
 
         :param tagger_tz_offset: int_seconds_west_of_utc

From 61269970f2b88ba008e5b8c5e4d5e4d2975d82d7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 19:06:07 -0500
Subject: [PATCH 512/994] Improve docstrings about tags

- Fix long-outdated information in the git.objects.tag docstring
  that seems to have been left over from when there was no such
  separate module. This module has only a tag-related class, not
  all classes that derive from git.object.base.Object.

- Expand the git.objects.tag docstring to clarify what the module
  provides, add a git.refs.tag module docstring explaining what
  that provides, and cross-reference them with an explanation of
  how they differ (mentioning how this relates to git concepts).

- Expand thie git.object.tag.TagObject class docstring to include
  the term "annotated" (retaining "non-lightweight" in parentheses).
---
 git/objects/tag.py | 10 +++++++---
 git/refs/tag.py    |  7 +++++++
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/git/objects/tag.py b/git/objects/tag.py
index 357c8ff7e..7589a5be1 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -3,7 +3,11 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Object-based types."""
+"""Provides an :class:`git.objects.base.Object`-based type for annotated tags.
+
+This defines the :class:`TagReference` class, which represents annotated tags.
+For lightweight tags, see the :mod:`git.refs.tag` module.
+"""
 
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
@@ -25,8 +29,8 @@
 
 
 class TagObject(base.Object):
-    """Non-lightweight tag carrying additional information about an object we are
-    pointing to."""
+    """Annotated (i.e. non-lightweight) tag carrying additional information about an
+    object we are pointing to."""
 
     type: Literal["tag"] = "tag"
 
diff --git a/git/refs/tag.py b/git/refs/tag.py
index 7edd1d12a..99c4bf443 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -1,6 +1,13 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+"""Provides a :class:`~git.refs.reference.Reference`-based type for lightweight tags.
+
+This defines the :class:`TagReference` class (and its alias :class:`Tag`), which
+represents lightweight tags. For annotated tags (which are git objects), see the
+:mod:`git.objects.tag` module.
+"""
+
 from .reference import Reference
 
 __all__ = ["TagReference", "Tag"]

From 110706e2f49eb90b29ceb3057340437d3d93245f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 19:25:50 -0500
Subject: [PATCH 513/994] Fix param name in TagRefernece docstring and add info

The "reference" parameter was listed as "ref" in the docstring.
This fixes that, slightly expands its description, and also adds
documentation for the "repo" parameter.
---
 git/refs/tag.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/git/refs/tag.py b/git/refs/tag.py
index 99c4bf443..89a82d1be 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -72,7 +72,8 @@ def commit(self) -> "Commit":  # type: ignore[override]  # LazyMixin has unrelat
     def tag(self) -> Union["TagObject", None]:
         """
         :return:
-            Tag object this tag ref points to or None in case we are a lightweight tag
+            Tag object this tag ref points to, or ``None`` in case we are a lightweight
+            tag
         """
         obj = self.object
         if obj.type == "tag":
@@ -96,13 +97,17 @@ def create(
     ) -> "TagReference":
         """Create a new tag reference.
 
+        :param repo:
+            The :class:`~git.repo.base.Repo` to create the tag in.
+
         :param path:
             The name of the tag, e.g. ``1.0`` or ``releases/1.0``.
             The prefix ``refs/tags`` is implied.
 
-        :param ref:
+        :param reference:
             A reference to the :class:`~git.objects.base.Object` you want to tag.
-            The Object can be a commit, tree or blob.
+            The referenced object can be a :class:`~git.objects.commit.Commit`,
+            :class:`~git.objects.tree.Tree`, or :class:`~git.objects.blob.Blob`.
 
         :param logmsg:
             If not None, the message will be used in your tag object. This will also

From b0e5bffef39ae6064bb2a1909bc0b536244b4b4c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 19:29:29 -0500
Subject: [PATCH 514/994] Undo some expansion of "reference" parameter

This keeps most of the changes from the previous commit, but it
undoes the expansion of the git object types a lightweight tag can
refer to into GitPython git.objects.base.Object-based types, which
seems more misleading than helpful because an uncareful reading
could lead to an incorrect belief that the corresponding argument
should or could be an object of such types.
---
 git/refs/tag.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/git/refs/tag.py b/git/refs/tag.py
index 89a82d1be..fd2dee131 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -106,8 +106,7 @@ def create(
 
         :param reference:
             A reference to the :class:`~git.objects.base.Object` you want to tag.
-            The referenced object can be a :class:`~git.objects.commit.Commit`,
-            :class:`~git.objects.tree.Tree`, or :class:`~git.objects.blob.Blob`.
+            The referenced object can be a commit, tree, or blob.
 
         :param logmsg:
             If not None, the message will be used in your tag object. This will also

From a5a1b2c541072ee73b5c295581f678acb7c91c2c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 19:34:11 -0500
Subject: [PATCH 515/994] Add a bit of missing docstring formatting

---
 git/refs/tag.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/refs/tag.py b/git/refs/tag.py
index fd2dee131..6a6dad09a 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -109,7 +109,7 @@ def create(
             The referenced object can be a commit, tree, or blob.
 
         :param logmsg:
-            If not None, the message will be used in your tag object. This will also
+            If not ``None``, the message will be used in your tag object. This will also
             create an additional tag object that allows to obtain that information,
             e.g.::
 
@@ -120,7 +120,7 @@ def create(
             `logmsg` takes precedence if both are passed.
 
         :param force:
-            If True, force creation of a tag even though that tag already exists.
+            If ``True``, force creation of a tag even though that tag already exists.
 
         :param kwargs:
             Additional keyword arguments to be passed to ``git tag``.

From 018ebaf72d2d96b2ca98a46748e857abb829a9d4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 19:53:46 -0500
Subject: [PATCH 516/994] Further revise docstrings within git.repo

---
 git/repo/base.py | 74 ++++++++++++++++++++++++++----------------------
 git/repo/fun.py  |  8 +++---
 2 files changed, 44 insertions(+), 38 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index 7b3e514c1..4745a2411 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -102,9 +102,9 @@ class BlameEntry(NamedTuple):
 
 
 class Repo:
-    """Represents a git repository and allows you to query references,
-    gather commit information, generate diffs, create and clone repositories, and query
-    the log.
+    """Represents a git repository and allows you to query references, father
+    commit information, generate diffs, create and clone repositories, and query the
+    log.
 
     The following attributes are worth using:
 
@@ -188,16 +188,18 @@ def __init__(
                 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
 
             - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
-            - If `path` is None or an empty string, :envvar:`GIT_DIR` is used. If that
-              environment variable is absent or empty, the current directory is used.
+            - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If
+              that environment variable is absent or empty, the current directory is
+              used.
 
         :param odbt:
             Object DataBase type - a type which is constructed by providing the
             directory containing the database objects, i.e. ``.git/objects``. It will be
-            used to access all object data
+            used to access all object data.
 
         :param search_parent_directories:
-            If True, all parent directories will be searched for a valid repo as well.
+            If ``True``, all parent directories will be searched for a valid repo as
+            well.
 
             Please note that this was the default behaviour in older versions of
             GitPython, which is considered a bug though.
@@ -372,7 +374,7 @@ def working_tree_dir(self) -> Optional[PathLike]:
         """
         :return:
             The working tree directory of our git repository.
-            If this is a bare repository, None is returned.
+            If this is a bare repository, ``None`` is returned.
         """
         return self._working_tree_dir
 
@@ -387,7 +389,7 @@ def common_dir(self) -> PathLike:
 
     @property
     def bare(self) -> bool:
-        """:return: True if the repository is bare"""
+        """:return: ``True`` if the repository is bare"""
         return self._bare
 
     @property
@@ -450,7 +452,7 @@ def remotes(self) -> "IterableList[Remote]":
     def remote(self, name: str = "origin") -> "Remote":
         """:return: The remote with the specified name
 
-        :raise ValueError
+        :raise ValueError:
             If no remote with such a name exists.
         """
         r = Remote(self, name)
@@ -494,10 +496,13 @@ def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
         return Submodule.add(self, *args, **kwargs)
 
     def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
-        """An iterator yielding Submodule instances, see Traversable interface
-        for a description of args and kwargs.
+        """An iterator yielding Submodule instances.
 
-        :return: Iterator
+        See the `~git.objects.util.Traversable` interface for a description of `args`
+        and `kwargs`.
+
+        :return:
+            Iterator
         """
         return RootModule(self).traverse(*args, **kwargs)
 
@@ -554,7 +559,8 @@ def create_head(
     ) -> "Head":
         """Create a new head within the repository.
 
-        :note: For more documentation, please see the
+        :note:
+            For more documentation, please see the
             :meth:`Head.create <git.refs.head.Head.create>` method.
 
         :return:
@@ -648,7 +654,7 @@ def config_reader(
             configuration files.
 
         :param config_level:
-            For possible values, see the :meth:`config_writer` method. If None, all
+            For possible values, see the :meth:`config_writer` method. If ``None``, all
             applicable levels will be used. Specify a level in case you know which file
             you wish to read to prevent reading multiple files.
 
@@ -744,7 +750,7 @@ def iter_commits(
 
         :param rev:
             Revision specifier, see ``git rev-parse`` for viable options.
-            If None, the active branch will be used.
+            If ``None``, the active branch will be used.
 
         :param paths:
             An optional path or a list of paths. If set, only commits that include the
@@ -759,7 +765,7 @@ def iter_commits(
             ``"revA...revB"`` revision specifier.
 
         :return:
-            Iterator of  :class:`~git.objects.commit.Commit` objects
+            Iterator of :class:`~git.objects.commit.Commit` objects
         """
         if rev is None:
             rev = self.head.commit
@@ -819,7 +825,7 @@ def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool:
             Rev to test against `ancestor_rev`.
 
         :return:
-            True if `ancestor_rev` is an ancestor to `rev`.
+            ``True`` if `ancestor_rev` is an ancestor to `rev`.
         """
         try:
             self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
@@ -923,9 +929,9 @@ def is_dirty(
     ) -> bool:
         """
         :return:
-            True if the repository is considered dirty. By default it will react like a
-            git-status without untracked files, hence it is dirty if the index or the
-            working copy have changes.
+            ``True`` if the repository is considered dirty. By default it will react
+            like a git-status without untracked files, hence it is dirty if the index or
+            the working copy have changes.
         """
         if self._bare:
             # Bare repositories with no associated working directory are
@@ -1036,9 +1042,9 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
         stream of :class:`BlameEntry` tuples.
 
         :param rev:
-            Revision specifier. If None, the blame will include all the latest
-            uncommitted changes. Otherwise, anything successfully parsed by ``git
-            rev-parse`` is a valid option.
+            Revision specifier. If ``None``, the blame will include all the latest
+            uncommitted changes. Otherwise, anything successfully parsed by
+            ``git rev-parse`` is a valid option.
 
         :return:
             Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
@@ -1132,9 +1138,9 @@ def blame(
         """The blame information for the given file at the given revision.
 
         :param rev:
-            Revision specifier. If None, the blame will include all the latest
+            Revision specifier. If ``None``, the blame will include all the latest
             uncommitted changes. Otherwise, anything successfully parsed by
-            git-rev-parse is a valid option.
+            ``git rev-parse`` is a valid option.
 
         :return:
             list: [git.Commit, list: [<line>]]
@@ -1286,9 +1292,9 @@ def init(
         """Initialize a git repository at the given path if specified.
 
         :param path:
-            The full path to the repo (traditionally ends with ``/<name>.git``).
-            Or None, in which case the repository will be created in the current working
-            directory.
+            The full path to the repo (traditionally ends with ``/<name>.git``). Or
+            ``None``, in which case the repository will be created in the current
+            working directory.
 
         :param mkdir:
             If specified, will create the repository directory if it doesn't already
@@ -1445,7 +1451,7 @@ def clone(
             Allow unsafe options to be used, like ``--upload-pack``.
 
         :param kwargs:
-            * odbt = ObjectDatabase Type, allowing to determine the object database
+            * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
               implementation used by the returned Repo instance.
             * All remaining keyword arguments are given to the ``git clone`` command.
 
@@ -1547,9 +1553,9 @@ def archive(
         :param kwargs:
             Additional arguments passed to ``git archive``:
 
-            * Use the 'format' argument to define the kind of format. Use specialized
+            * Use the ``format`` argument to define the kind of format. Use specialized
               ostreams to write any format supported by Python.
-            * You may specify the special **path** keyword, which may either be a
+            * You may specify the special ``path`` keyword, which may either be a
               repository-relative path to a directory or file to place into the archive,
               or a list or tuple of multiple paths.
 
@@ -1581,7 +1587,7 @@ def has_separate_working_tree(self) -> bool:
             to.
 
         :note:
-            Bare repositories will always return False here.
+            Bare repositories will always return ``False`` here.
         """
         if self.bare:
             return False
@@ -1601,7 +1607,7 @@ def currently_rebasing_on(self) -> Commit | None:
         :return:
             The commit which is currently being replayed while rebasing.
 
-            None if we are not currently rebasing.
+            ``None`` if we are not currently rebasing.
         """
         if self.git_dir:
             rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 4936f1773..e9fad2c46 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -128,7 +128,7 @@ def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
 def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
     """
     :return:
-        Long hexadecimal sha1 from the given less than 40 byte hexsha or None if no
+        Long hexadecimal sha1 from the given less than 40 byte hexsha, or ``None`` if no
         candidate could be found.
 
     :param hexsha:
@@ -150,7 +150,7 @@ def name_to_object(
         references are supported.
 
     :param return_ref:
-        If True, and name specifies a reference, we will return the reference
+        If ``True``, and name specifies a reference, we will return the reference
         instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` o
         `~gitdb.exc.BadName`.
     """
@@ -202,7 +202,7 @@ def name_to_object(
 
 
 def deref_tag(tag: "Tag") -> "TagObject":
-    """Recursively dereference a tag and return the resulting object"""
+    """Recursively dereference a tag and return the resulting object."""
     while True:
         try:
             tag = tag.object
@@ -213,7 +213,7 @@ def deref_tag(tag: "Tag") -> "TagObject":
 
 
 def to_commit(obj: Object) -> Union["Commit", "TagObject"]:
-    """Convert the given object to a commit if possible and return it"""
+    """Convert the given object to a commit if possible and return it."""
     if obj.type == "tag":
         obj = deref_tag(obj)
 

From 608147ec7fa240686cc88612e1962c18fc6e8549 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 20:07:13 -0500
Subject: [PATCH 517/994] Better explain conditional cleanup in
 test_base_object

This expands the comment added in 41fac85 (#1770) to make more
clear that this particular cleanup is deliberately done only when
the operation was successful (and why).
---
 test/test_base.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/test/test_base.py b/test/test_base.py
index 74f342071..693c97f20 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -72,7 +72,10 @@ def test_base_object(self):
                 self.assertEqual(item, item.stream_data(tmpfile))
                 tmpfile.seek(0)
                 self.assertEqual(tmpfile.read(), data)
-            os.remove(tmpfile.name)  # Do it this way so we can inspect the file on failure.
+
+            # Remove the file this way, instead of with a context manager or "finally",
+            # so it is only removed on success, and we can inspect the file on failure.
+            os.remove(tmpfile.name)
         # END for each object type to create
 
         # Each has a unique sha.

From 5cf5b6049f4e4cc83b08c1b2f36aaf0ad48b67f1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 20:48:52 -0500
Subject: [PATCH 518/994] Revise test suite docstrings and comments

In a more limited way than in the git/ code.

Docstring revisions are, in spirit, along the same lines as those
in the git package, but much more conservative, because the tests'
docstrings are not rendered into Sphinx documentation and there is
no current plan to do so (so the tradeoffs are different, with even
a tiny decrease in clarity when read in the code being a reason to
avoid changes that use Sphinx roles more robustly or that would
improve hypothetical rendered documentation), and because the test
suite uses docstrings more sparingly and the existing docstrings
were mostly already clear and easy to read.

This wraps commnts to 88 columns as most comments now are in the
git package, except it avoids doing so when doing so would make
anything even slightly less clear, and where it would require
significant further style or spacing changes for it to remain
obvious (even before reading a comment) what the comment applies
to, and in most portions of tutorial-generating test case code
(where, although clarity would improve when reading the tests, it
might sometimes decrease in the generated documentation).
---
 test/lib/helper.py               |  13 ++--
 test/performance/test_streams.py |   4 +-
 test/test_base.py                |   4 +-
 test/test_clone.py               |   4 +-
 test/test_commit.py              |  16 +++--
 test/test_config.py              |  12 ++--
 test/test_diff.py                |  23 ++++---
 test/test_docs.py                |  26 ++++----
 test/test_fun.py                 |   3 +-
 test/test_git.py                 |  22 +++---
 test/test_index.py               |  49 +++++++-------
 test/test_installation.py        |   6 +-
 test/test_refs.py                |  50 +++++++++-----
 test/test_remote.py              |  39 +++++------
 test/test_repo.py                |  34 +++++-----
 test/test_submodule.py           | 111 ++++++++++++++++++-------------
 test/test_util.py                |  13 ++--
 17 files changed, 243 insertions(+), 186 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index f951a6a12..26469ed5d 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -346,17 +346,20 @@ class TestBase(TestCase):
     """Base class providing default functionality to all tests such as:
 
     - Utility functions provided by the TestCase base of the unittest method such as::
+
         self.fail("todo")
         self.assertRaises(...)
 
     - Class level repository which is considered read-only as it is shared among
       all test cases in your type.
+
       Access it using::
-      self.rorepo  # 'ro' stands for read-only
+
+        self.rorepo  # 'ro' stands for read-only
 
       The rorepo is in fact your current project's git repo. If you refer to specific
-      shas for your objects, be sure you choose some that are part of the immutable portion
-      of the project history (so that tests don't fail for others).
+      shas for your objects, be sure you choose some that are part of the immutable
+      portion of the project history (so that tests don't fail for others).
     """
 
     def _small_repo_url(self):
@@ -383,8 +386,8 @@ def tearDownClass(cls):
 
     def _make_file(self, rela_path, data, repo=None):
         """
-        Create a file at the given path relative to our repository, filled
-        with the given data.
+        Create a file at the given path relative to our repository, filled with the
+        given data.
 
         :return: An absolute path to the created file.
         """
diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py
index ba5cbe415..56b5274ec 100644
--- a/test/performance/test_streams.py
+++ b/test/performance/test_streams.py
@@ -25,8 +25,8 @@ class TestObjDBPerformance(TestBigRepoR):
 
     @with_rw_repo("HEAD", bare=True)
     def test_large_data_streaming(self, rwrepo):
-        # TODO: This part overlaps with the same file in gitdb.test.performance.test_stream.
-        # It should be shared if possible.
+        # TODO: This part overlaps with the same file in
+        # gitdb.test.performance.test_stream. It should be shared if possible.
         ldb = LooseObjectDB(osp.join(rwrepo.git_dir, "objects"))
 
         for randomize in range(2):
diff --git a/test/test_base.py b/test/test_base.py
index 693c97f20..ef7486e86 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -135,8 +135,8 @@ def test_add_unicode(self, rw_repo):
             # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
             # Therefore, it must be added using the Python implementation.
             rw_repo.index.add([file_path])
-            # However, when the test winds down, rmtree fails to delete this file, which is recognized
-            # as ??? only.
+            # However, when the test winds down, rmtree fails to delete this file, which
+            # is recognized as ??? only.
         else:
             # On POSIX, we can just add Unicode files without problems.
             rw_repo.git.add(rw_repo.working_dir)
diff --git a/test/test_clone.py b/test/test_clone.py
index dcab7ad6f..be2e6b19b 100644
--- a/test/test_clone.py
+++ b/test/test_clone.py
@@ -19,8 +19,8 @@ def test_checkout_in_non_empty_dir(self, rw_dir):
         garbage_file = non_empty_dir / "not-empty"
         garbage_file.write_text("Garbage!")
 
-        # Verify that cloning into the non-empty dir fails while complaining about
-        # the target directory not being empty/non-existent.
+        # Verify that cloning into the non-empty dir fails while complaining about the
+        # target directory not being empty/non-existent.
         try:
             self.rorepo.clone(non_empty_dir)
         except git.GitCommandError as exc:
diff --git a/test/test_commit.py b/test/test_commit.py
index fddb91c17..5571b9ecb 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -27,8 +27,8 @@
 
 class TestCommitSerialization(TestBase):
     def assert_commit_serialization(self, rwrepo, commit_id, print_performance_info=False):
-        """Traverse all commits in the history of commit identified by commit_id and check
-        if the serialization works.
+        """Traverse all commits in the history of commit identified by commit_id and
+        check if the serialization works.
 
         :param print_performance_info: If True, we will show how fast we are.
         """
@@ -317,8 +317,9 @@ def test_count(self):
         self.assertEqual(self.rorepo.tag("refs/tags/0.1.5").commit.count(), 143)
 
     def test_list(self):
-        # This doesn't work anymore, as we will either attempt getattr with bytes, or compare 20 byte string
-        # with actual 20 byte bytes. This usage makes no sense anyway.
+        # This doesn't work anymore, as we will either attempt getattr with bytes, or
+        # compare 20 byte string with actual 20 byte bytes. This usage makes no sense
+        # anyway.
         assert isinstance(
             Commit.list_items(self.rorepo, "0.1.5", max_count=5)["5117c9c8a4d3af19a9958677e45cda9269de1541"],
             Commit,
@@ -383,8 +384,8 @@ def test_serialization_unicode_support(self):
 
         self.assertEqual(cmt.author.name, ncmt.author.name)
         self.assertEqual(cmt.message, ncmt.message)
-        # Actually, it can't be printed in a shell as repr wants to have ascii only
-        # it appears.
+        # Actually, it can't be printed in a shell as repr wants to have ascii only it
+        # appears.
         cmt.author.__repr__()
 
     def test_invalid_commit(self):
@@ -498,7 +499,8 @@ def test_trailers(self):
         KEY_2 = "Key"
         VALUE_2 = "Value with inner spaces"
 
-        # Check that the following trailer example is extracted from multiple msg variations.
+        # Check that 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",
diff --git a/test/test_config.py b/test/test_config.py
index f493c5672..4843d91eb 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -41,7 +41,7 @@ def _to_memcache(self, file_path):
         return sio
 
     def test_read_write(self):
-        # writer must create the exact same file as the one read before
+        # The writer must create the exact same file as the one read before.
         for filename in ("git_config", "git_config_global"):
             file_obj = self._to_memcache(fixture_path(filename))
             with GitConfigParser(file_obj, read_only=False) as w_config:
@@ -56,7 +56,8 @@ def test_read_write(self):
                     self._to_memcache(fixture_path(filename)).getvalue(),
                 )
 
-                # Creating an additional config writer must fail due to exclusive access.
+                # Creating an additional config writer must fail due to exclusive
+                # access.
                 with self.assertRaises(IOError):
                     GitConfigParser(file_obj, read_only=False)
 
@@ -91,8 +92,8 @@ def test_includes_order(self):
             r_config.read()  # Enforce reading.
             # Simple inclusions, again checking them taking precedence.
             assert r_config.get_value("sec", "var0") == "value0_included"
-            # This one should take the git_config_global value since included
-            # values must be considered as soon as they get them.
+            # This one should take the git_config_global value since included values
+            # must be considered as soon as they get them.
             assert r_config.get_value("diff", "tool") == "meld"
             try:
                 # FIXME: Split this assertion out somehow and mark it xfail (or fix it).
@@ -109,7 +110,8 @@ def test_lock_reentry(self, rw_dir):
         # Entering again locks the file again...
         with gcp as cw:
             cw.set_value("include", "some_other_value", "b")
-            # ...so creating an additional config writer must fail due to exclusive access.
+            # ...so creating an additional config writer must fail due to exclusive
+            # access.
             with self.assertRaises(IOError):
                 GitConfigParser(fpl, read_only=False)
         # but work when the lock is removed
diff --git a/test/test_diff.py b/test/test_diff.py
index 87f92f5d1..cdd473f7f 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -271,18 +271,18 @@ def test_diff_unsafe_paths(self):
         self.assertEqual(res[10].b_rawpath, b"path/\x80-invalid-unicode-path.txt")
 
         # The "Moves"
-        # NOTE: The path prefixes a/ and b/ here are legit!  We're actually
-        # verifying that it's not "a/a/" that shows up, see the fixture data.
-        self.assertEqual(res[11].a_path, "a/with spaces")  # NOTE: path a/ here legit!
-        self.assertEqual(res[11].b_path, "b/with some spaces")  # NOTE: path b/ here legit!
+        # NOTE: The path prefixes "a/" and "b/" here are legit! We're actually verifying
+        # that it's not "a/a/" that shows up; see the fixture data.
+        self.assertEqual(res[11].a_path, "a/with spaces")  # NOTE: path "a/"" legit!
+        self.assertEqual(res[11].b_path, "b/with some spaces")  # NOTE: path "b/"" legit!
         self.assertEqual(res[12].a_path, "a/ending in a space ")
         self.assertEqual(res[12].b_path, "b/ending with space ")
         self.assertEqual(res[13].a_path, 'a/"with-quotes"')
         self.assertEqual(res[13].b_path, 'b/"with even more quotes"')
 
     def test_diff_patch_format(self):
-        # Test all of the 'old' format diffs for completeness - it should at least
-        # be able to deal with it.
+        # Test all of the 'old' format diffs for completeness - it should at least be
+        # able to deal with it.
         fixtures = (
             "diff_2",
             "diff_2f",
@@ -345,8 +345,9 @@ def test_diff_submodule(self):
         repo.create_tag("2")
 
         diff = repo.commit("1").diff(repo.commit("2"))[0]
-        # If diff is unable to find the commit hashes (looks in wrong repo) the *_blob.size
-        # property will be a string containing exception text, an int indicates success.
+        # If diff is unable to find the commit hashes (looks in wrong repo) the
+        # *_blob.size property will be a string containing exception text, an int
+        # indicates success.
         self.assertIsInstance(diff.a_blob.size, int)
         self.assertIsInstance(diff.b_blob.size, int)
 
@@ -392,9 +393,9 @@ def test_diff_interface(self):
             # END for each other side
         # END for each commit
 
-        # Assert that we could always find at least one instance of the members we
-        # can iterate in the diff index - if not this indicates its not working correctly
-        # or our test does not span the whole range of possibilities.
+        # Assert that we could always find at least one instance of the members we can
+        # iterate in the diff index - if not this indicates its not working correctly or
+        # our test does not span the whole range of possibilities.
         for key, value in assertion_map.items():
             self.assertIsNotNone(value, "Did not find diff for %s" % key)
         # END for each iteration type
diff --git a/test/test_docs.py b/test/test_docs.py
index 48922eba8..409f66bb3 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -19,8 +19,9 @@ class Tutorials(TestBase):
     def tearDown(self):
         gc.collect()
 
-    # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last call to it via
-    # git.objects.submodule.base.Submodule.remove (at "handle separate bare repository"), line 1062.
+    # ACTUALLY skipped by git.util.rmtree (in local onerror function), from the last
+    # call to it via git.objects.submodule.base.Submodule.remove
+    # (at "handle separate bare repository"), line 1062.
     #
     # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
     #         "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
@@ -31,8 +32,8 @@ def test_init_repo_object(self, rw_dir):
         from git import Repo
 
         # rorepo is a Repo instance pointing to the git-python repository.
-        # For all you know, the first argument to Repo is a path to the repository
-        # you want to work with.
+        # For all you know, the first argument to Repo is a path to the repository you
+        # want to work with.
         repo = Repo(self.rorepo.working_tree_dir)
         assert not repo.bare
         # ![1-test_init_repo_object]
@@ -149,8 +150,8 @@ def update(self, op_code, cur_count, max_count=None, message=""):
         assert origin.exists()
         for fetch_info in origin.fetch(progress=MyProgressPrinter()):
             print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit))
-        # Create a local branch at the latest fetched master. We specify the name statically, but you have all
-        # information to do it programmatically as well.
+        # Create a local branch at the latest fetched master. We specify the name
+        # statically, but you have all information to do it programmatically as well.
         bare_master = bare_repo.create_head("master", origin.refs.master)
         bare_repo.head.set_reference(bare_master)
         assert not bare_repo.delete_remote(origin).exists()
@@ -188,9 +189,9 @@ def update(self, op_code, cur_count, max_count=None, message=""):
         # submodules
 
         # [14-test_init_repo_object]
-        # Create a new submodule and check it out on the spot, setup to track master branch of `bare_repo`.
-        # As our GitPython repository has submodules already that point to GitHub, make sure we don't
-        # interact with them.
+        # Create a new submodule and check it out on the spot, setup to track master
+        # branch of `bare_repo`. As our GitPython repository has submodules already that
+        # point to GitHub, make sure we don't interact with them.
         for sm in cloned_repo.submodules:
             assert not sm.remove().exists()  # after removal, the sm doesn't exist anymore
         sm = cloned_repo.create_submodule("mysubrepo", "path/to/subrepo", url=bare_repo.git_dir, branch="master")
@@ -424,8 +425,8 @@ def test_references_and_objects(self, rw_dir):
         with origin.config_writer as cw:
             cw.set("pushurl", "other_url")
 
-        # Please note that in Python 2, writing origin.config_writer.set(...) is totally safe.
-        # In py3 __del__ calls can be delayed, thus not writing changes in time.
+        # Please note that in Python 2, writing origin.config_writer.set(...) is totally
+        # safe. In py3 __del__ calls can be delayed, thus not writing changes in time.
         # ![26-test_references_and_objects]
 
         # [27-test_references_and_objects]
@@ -462,7 +463,8 @@ def test_references_and_objects(self, rw_dir):
         # ![29-test_references_and_objects]
 
         # [30-test_references_and_objects]
-        # Check out the branch using git-checkout. It will fail as the working tree appears dirty.
+        # Check out the branch using git-checkout.
+        # It will fail as the working tree appears dirty.
         self.assertRaises(git.GitCommandError, repo.heads.master.checkout)
         repo.heads.past_branch.checkout()
         # ![30-test_references_and_objects]
diff --git a/test/test_fun.py b/test/test_fun.py
index 566bc9aae..2d30d355a 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -35,7 +35,8 @@ def _assert_index_entries(self, entries, trees):
         # END assert entry matches fully
 
     def test_aggressive_tree_merge(self):
-        # Head tree with additions, removals and modification compared to its predecessor.
+        # Head tree with additions, removals and modification compared to its
+        # predecessor.
         odb = self.rorepo.odb
         HC = self.rorepo.commit("6c1faef799095f3990e9970bc2cb10aa0221cf9c")
         H = HC.tree
diff --git a/test/test_git.py b/test/test_git.py
index 97e21cad4..e1a8bda5e 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -253,21 +253,27 @@ def test_it_avoids_upcasing_unrelated_environment_variable_names(self):
         if old_name == old_name.upper():
             raise RuntimeError("test bug or strange locale: old_name invariant under upcasing")
 
-        # 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.
+        # 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. 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).
+        # 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"),  # Contains steps 3 and 4.
             self.rorepo.working_dir,
             old_name,
         ]
-        pair_text = subprocess.check_output(cmdline, shell=False, text=True)  # Run steps 3 and 4.
+
+        # Run steps 3 and 4.
+        pair_text = subprocess.check_output(cmdline, shell=False, text=True)
 
         new_name = pair_text.split("=")[0]
         self.assertEqual(new_name, old_name)
@@ -668,7 +674,7 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
                 # as unintended shell expansions can occur, and is deprecated. Instead,
                 # use a custom command, by setting the GIT_PYTHON_GIT_EXECUTABLE
                 # environment variable to git.cmd or by passing git.cmd's full path to
-                # git.refresh. Or wrap the script with a .exe shim.
+                # git.refresh. Or wrap the script with a .exe shim.)
                 stack.enter_context(mock.patch.object(Git, "USE_SHELL", True))
 
             new_git = Git()
diff --git a/test/test_index.py b/test/test_index.py
index fd62bb893..fa64b82a2 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -219,8 +219,7 @@ def _fprogress(self, path, done, item):
         self._fprogress_map[path] = curval + 1
 
     def _fprogress_add(self, path, done, item):
-        """Called as progress func - we keep track of the proper
-        call order"""
+        """Called as progress func - we keep track of the proper call order."""
         assert item is not None
         self._fprogress(path, done, item)
 
@@ -385,17 +384,17 @@ def test_index_merge_tree(self, rw_repo):
 
         # FAKE MERGE
         #############
-        # Add a change with a NULL sha that should conflict with next_commit. We
-        # pretend there was a change, but we do not even bother adding a proper
-        # sha for it (which makes things faster of course).
+        # Add a change with a NULL sha that should conflict with next_commit. We pretend
+        # there was a change, but we do not even bother adding a proper sha for it
+        # (which makes things faster of course).
         manifest_fake_entry = BaseIndexEntry((manifest_entry[0], b"\0" * 20, 0, manifest_entry[3]))
         # Try write flag.
         self._assert_entries(rw_repo.index.add([manifest_fake_entry], write=False))
-        # Add actually resolves the null-hex-sha for us as a feature, but we can
-        # edit the index manually.
+        # Add actually resolves the null-hex-sha for us as a feature, but we can edit
+        # the index manually.
         assert rw_repo.index.entries[manifest_key].binsha != Object.NULL_BIN_SHA
-        # We must operate on the same index for this! It's a bit problematic as
-        # it might confuse people.
+        # We must operate on the same index for this! It's a bit problematic as it might
+        # confuse people.
         index = rw_repo.index
         index.entries[manifest_key] = IndexEntry.from_base(manifest_fake_entry)
         index.write()
@@ -404,19 +403,20 @@ def test_index_merge_tree(self, rw_repo):
         # Write an unchanged index (just for the fun of it).
         rw_repo.index.write()
 
-        # A three way merge would result in a conflict and fails as the command will
-        # not overwrite any entries in our index and hence leave them unmerged. This is
+        # A three way merge would result in a conflict and fails as the command will not
+        # overwrite any entries in our index and hence leave them unmerged. This is
         # mainly a protection feature as the current index is not yet in a tree.
         self.assertRaises(GitCommandError, index.merge_tree, next_commit, base=parent_commit)
 
-        # The only way to get the merged entries is to safe the current index away into a tree,
-        # which is like a temporary commit for us. This fails as well as the NULL sha does not
-        # have a corresponding object.
+        # The only way to get the merged entries is to safe the current index away into
+        # a tree, which is like a temporary commit for us. This fails as well as the
+        # NULL sha does not have a corresponding object.
         # NOTE: missing_ok is not a kwarg anymore, missing_ok is always true.
         # self.assertRaises(GitCommandError, index.write_tree)
 
-        # If missing objects are okay, this would work though (they are always okay now).
-        # As we can't read back the tree with NULL_SHA, we rather set it to something else.
+        # If missing objects are okay, this would work though (they are always okay
+        # now). As we can't read back the tree with NULL_SHA, we rather set it to
+        # something else.
         index.entries[manifest_key] = IndexEntry(manifest_entry[:1] + (hex_to_bin("f" * 40),) + manifest_entry[2:])
         tree = index.write_tree()
 
@@ -428,7 +428,7 @@ def test_index_merge_tree(self, rw_repo):
 
     @with_rw_repo("0.1.6")
     def test_index_file_diffing(self, rw_repo):
-        # Default Index instance points to our index.
+        # Default IndexFile instance points to our index.
         index = IndexFile(rw_repo)
         assert index.path is not None
         assert len(index.entries)
@@ -439,8 +439,8 @@ def test_index_file_diffing(self, rw_repo):
         # Could sha it, or check stats.
 
         # Test diff.
-        # Resetting the head will leave the index in a different state, and the
-        # diff will yield a few changes.
+        # Resetting the head will leave the index in a different state, and the diff
+        # will yield a few changes.
         cur_head_commit = rw_repo.head.reference.commit
         rw_repo.head.reset("HEAD~6", index=True, working_tree=False)
 
@@ -956,10 +956,10 @@ def test_index_new(self):
 
     @with_rw_repo("HEAD", bare=True)
     def test_index_bare_add(self, rw_bare_repo):
-        # Something is wrong after cloning to a bare repo, reading the
-        # property rw_bare_repo.working_tree_dir will return '/tmp'
-        # instead of throwing the Exception we are expecting. This is
-        # a quick hack to make this test fail when expected.
+        # Something is wrong after cloning to a bare repo, reading the property
+        # rw_bare_repo.working_tree_dir will return '/tmp' instead of throwing the
+        # Exception we are expecting. This is a quick hack to make this test fail when
+        # expected.
         assert rw_bare_repo.working_tree_dir is None
         assert rw_bare_repo.bare
         contents = b"This is a BytesIO file"
@@ -984,7 +984,8 @@ def test_index_bare_add(self, rw_bare_repo):
 
     @with_rw_directory
     def test_add_utf8P_path(self, rw_dir):
-        # NOTE: fp is not a Unicode object in Python 2 (which is the source of the problem).
+        # NOTE: fp is not a Unicode object in Python 2
+        # (which is the source of the problem).
         fp = osp.join(rw_dir, "ø.txt")
         with open(fp, "wb") as fs:
             fs.write("content of ø".encode("utf-8"))
diff --git a/test/test_installation.py b/test/test_installation.py
index 15ed5b13b..ae6472e98 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -46,9 +46,9 @@ def test_installation(self, rw_dir):
             msg=result.stderr or result.stdout or "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.
+        # 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,
diff --git a/test/test_refs.py b/test/test_refs.py
index 2656ceab1..28db70c6e 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -245,8 +245,8 @@ def test_head_reset(self, rw_repo):
         cur_head.reset(new_head_commit)
         rw_repo.index.checkout(["lib"], force=True)
 
-        # Now that we have a write write repo, change the HEAD reference - it's
-        # like "git-reset --soft".
+        # Now that we have a write write repo, change the HEAD reference - it's like
+        # "git-reset --soft".
         heads = rw_repo.heads
         assert heads
         for head in heads:
@@ -349,8 +349,8 @@ def test_head_reset(self, rw_repo):
         for remote in remotes:
             refs = remote.refs
 
-            # If a HEAD exists, it must be deleted first. Otherwise it might
-            # end up pointing to an invalid ref it the ref was deleted before.
+            # If a HEAD exists, it must be deleted first. Otherwise it might end up
+            # pointing to an invalid ref it the ref was deleted before.
             remote_head_name = "HEAD"
             if remote_head_name in refs:
                 RemoteReference.delete(rw_repo, refs[remote_head_name])
@@ -383,7 +383,7 @@ def test_head_reset(self, rw_repo):
         # Setting a non-commit as commit fails, but succeeds as object.
         head_tree = head.commit.tree
         self.assertRaises(ValueError, setattr, head, "commit", head_tree)
-        assert head.commit == old_commit  # and the ref did not change
+        assert head.commit == old_commit  # And the ref did not change.
         # We allow heads to point to any object.
         head.object = head_tree
         assert head.object == head_tree
@@ -492,8 +492,8 @@ def test_head_reset(self, rw_repo):
         # Would raise if the symref wouldn't have been deleted (probably).
         symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference)
 
-        # Test symbolic references which are not at default locations like HEAD
-        # or FETCH_HEAD - they may also be at spots in refs of course.
+        # Test symbolic references which are not at default locations like HEAD or
+        # FETCH_HEAD - they may also be at spots in refs of course.
         symbol_ref_path = "refs/symbol_ref"
         symref = SymbolicReference(rw_repo, symbol_ref_path)
         assert symref.path == symbol_ref_path
@@ -525,14 +525,13 @@ def test_head_reset(self, rw_repo):
         assert active_branch in heads
         assert rw_repo.tags
 
-        # We should be able to iterate all symbolic refs as well - in that case
-        # we should expect only symbolic references to be returned.
+        # We should be able to iterate all symbolic refs as well - in that case we
+        # should expect only symbolic references to be returned.
         for symref in SymbolicReference.iter_items(rw_repo):
             assert not symref.is_detached
 
-        # When iterating references, we can get references and symrefs
-        # when deleting all refs, I'd expect them to be gone! Even from
-        # the packed ones.
+        # When iterating references, we can get references and symrefs when deleting all
+        # refs, I'd expect them to be gone! Even from the packed ones.
         # For this to work, we must not be on any branch.
         rw_repo.head.reference = rw_repo.head.commit
         deleted_refs = set()
@@ -577,9 +576,9 @@ def test_head_reset(self, rw_repo):
             self.assertRaises(ValueError, setattr, ref, "commit", "nonsense")
             assert not ref.is_valid()
 
-            # I am sure I had my reason to make it a class method at first, but
-            # now it doesn't make so much sense anymore, want an instance method as well
-            # See http://byronimo.lighthouseapp.com/projects/51787-gitpython/tickets/27
+            # I am sure I had my reason to make it a class method at first, but now it
+            # doesn't make so much sense anymore, want an instance method as well. See:
+            # http://byronimo.lighthouseapp.com/projects/51787-gitpython/tickets/27
             Reference.delete(ref.repo, ref.path)
             assert not ref.is_valid()
 
@@ -619,8 +618,8 @@ def test_reflog(self):
 
     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.
+        # 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:
@@ -630,37 +629,52 @@ def test_refs_outside_repo(self):
             self.assertRaises(BadName, self.rorepo.commit, f"../../{ref_file_name}")
 
     def test_validity_ref_names(self):
+        """Ensure ref names are checked for validity.
+
+        This is based on the rules specified in:
+        https://git-scm.com/docs/git-check-ref-format/#_description
+        """
         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")
diff --git a/test/test_remote.py b/test/test_remote.py
index df6034326..c0bd11f80 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -294,11 +294,11 @@ def get_info(res, remote, name):
 
         # Provoke to receive actual objects to see what kind of output we have to
         # expect. For that we need a remote transport protocol.
-        # Create a new UN-shared repo and fetch into it after we pushed a change
-        # to the shared repo.
+        # Create a new UN-shared repo and fetch into it after we pushed a change to the
+        # shared repo.
         other_repo_dir = tempfile.mktemp("other_repo")
-        # Must clone with a local path for the repo implementation not to freak out
-        # as it wants local paths only (which I can understand).
+        # Must clone with a local path for the repo implementation not to freak out as
+        # it wants local paths only (which I can understand).
         other_repo = remote_repo.clone(other_repo_dir, shared=False)
         remote_repo_url = osp.basename(remote_repo.git_dir)  # git-daemon runs with appropriate `--base-path`.
         remote_repo_url = Git.polish_url("git://localhost:%s/%s" % (GIT_DAEMON_PORT, remote_repo_url))
@@ -317,10 +317,10 @@ def get_info(res, remote, name):
             self._commit_random_file(rw_repo)
             remote.push(rw_repo.head.reference)
 
-            # Here I would expect to see remote-information about packing
-            # objects and so on. Unfortunately, this does not happen
-            # if we are redirecting the output - git explicitly checks for this
-            # and only provides progress information to ttys.
+            # Here I would expect to see remote-information about packing objects and so
+            # on. Unfortunately, this does not happen if we are redirecting the output -
+            # git explicitly checks for this and only provides progress information to
+            # ttys.
             res = fetch_and_test(other_origin)
         finally:
             rmtree(other_repo_dir)
@@ -333,8 +333,8 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
         try:
             lhead.reference = rw_repo.heads.master
         except AttributeError:
-            # If the author is on a non-master branch, the clones might not have
-            # a local master yet. We simply create it.
+            # If the author is on a non-master branch, the clones might not have a local
+            # master yet. We simply create it.
             lhead.reference = rw_repo.create_head("master")
         # END master handling
         lhead.reset(remote.refs.master, working_tree=True)
@@ -488,8 +488,8 @@ def test_base(self, rw_repo, remote_repo):
             self._assert_push_and_pull(remote, rw_repo, remote_repo)
 
             # FETCH TESTING
-            # Only for remotes - local cases are the same or less complicated
-            # as additional progress information will never be emitted.
+            # Only for remotes - local cases are the same or less complicated as
+            # additional progress information will never be emitted.
             if remote.name == "daemon_origin":
                 self._do_test_fetch(remote, rw_repo, remote_repo, kill_after_timeout=10.0)
                 ran_fetch_test = True
@@ -508,7 +508,8 @@ def test_base(self, rw_repo, remote_repo):
         # Verify we can handle prunes when fetching.
         # stderr lines look like this:  x [deleted]         (none)     -> origin/experiment-2012
         # These should just be skipped.
-        # If we don't have a manual checkout, we can't actually assume there are any non-master branches.
+        # If we don't have a manual checkout, we can't actually assume there are any
+        # non-master branches.
         remote_repo.create_head("myone_for_deletion")
         # Get the branch - to be pruned later
         origin.fetch()
@@ -812,8 +813,8 @@ def test_fetch_unsafe_url_allowed(self, rw_repo):
                 "fd::17/foo",
             ]
             for url in urls:
-                # The URL will be allowed into the command, but the command will
-                # fail since we don't have that protocol enabled in the Git config file.
+                # The URL will be allowed into the command, but the command will fail
+                # since we don't have that protocol enabled in the Git config file.
                 with self.assertRaises(GitCommandError):
                     remote.fetch(url, allow_unsafe_protocols=True)
                 assert not tmp_file.exists()
@@ -880,8 +881,8 @@ def test_pull_unsafe_url_allowed(self, rw_repo):
                 "fd::17/foo",
             ]
             for url in urls:
-                # The URL will be allowed into the command, but the command will
-                # fail since we don't have that protocol enabled in the Git config file.
+                # The URL will be allowed into the command, but the command will fail
+                # since we don't have that protocol enabled in the Git config file.
                 with self.assertRaises(GitCommandError):
                     remote.pull(url, allow_unsafe_protocols=True)
                 assert not tmp_file.exists()
@@ -948,8 +949,8 @@ def test_push_unsafe_url_allowed(self, rw_repo):
                 "fd::17/foo",
             ]
             for url in urls:
-                # The URL will be allowed into the command, but the command will
-                # fail since we don't have that protocol enabled in the Git config file.
+                # The URL will be allowed into the command, but the command will fail
+                # since we don't have that protocol enabled in the Git config file.
                 with self.assertRaises(GitCommandError):
                     remote.push(url, allow_unsafe_protocols=True)
                 assert not tmp_file.exists()
diff --git a/test/test_repo.py b/test/test_repo.py
index 465bb2574..30a44b6c1 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -543,8 +543,8 @@ def test_init(self):
                 try:
                     rmtree(clone_path)
                 except OSError:
-                    # When relative paths are used, the clone may actually be inside
-                    # of the parent directory.
+                    # When relative paths are used, the clone may actually be inside of
+                    # the parent directory.
                     pass
                 # END exception handling
 
@@ -556,8 +556,8 @@ def test_init(self):
                 try:
                     rmtree(clone_path)
                 except OSError:
-                    # When relative paths are used, the clone may actually be inside
-                    # of the parent directory.
+                    # When relative paths are used, the clone may actually be inside of
+                    # the parent directory.
                     pass
                 # END exception handling
 
@@ -832,8 +832,8 @@ def test_config_level_paths(self):
             assert self.rorepo._get_config_path(config_level)
 
     def test_creation_deletion(self):
-        # Just a very quick test to assure it generally works. There are
-        # specialized cases in the test_refs module.
+        # Just a very quick test to assure it generally works. There are specialized
+        # cases in the test_refs module.
         head = self.rorepo.create_head("new_head", "HEAD~1")
         self.rorepo.delete_head(head)
 
@@ -1027,7 +1027,8 @@ def test_rev_parse(self):
                     num_resolved += 1
                 except (BadName, BadObject):
                     print("failed on %s" % path_section)
-                    # This is fine if we have something like 112, which belongs to remotes/rname/merge-requests/112.
+                    # This is fine if we have something like 112, which belongs to
+                    # remotes/rname/merge-requests/112.
                 # END exception handling
             # END for each token
             if ref_no == 3 - 1:
@@ -1149,7 +1150,7 @@ def test_submodule_update(self, rwrepo):
         )
         self.assertIsInstance(sm, Submodule)
 
-        # NOTE: the rest of this functionality is tested in test_submodule.
+        # NOTE: The rest of this functionality is tested in test_submodule.
 
     @with_rw_repo("HEAD")
     def test_git_file(self, rwrepo):
@@ -1178,8 +1179,9 @@ def last_commit(repo, rev, path):
         # This is based on this comment:
         # https://github.com/gitpython-developers/GitPython/issues/60#issuecomment-23558741
         # And we expect to set max handles to a low value, like 64.
-        # You should set ulimit -n X, see .travis.yml
-        # The loops below would easily create 500 handles if these would leak (4 pipes + multiple mapped files).
+        # You should set ulimit -n X. See .travis.yml.
+        # The loops below would easily create 500 handles if these would leak
+        # (4 pipes + multiple mapped files).
         for _ in range(64):
             for repo_type in (GitCmdObjectDB, GitDB):
                 repo = Repo(self.rorepo.working_tree_dir, odbt=repo_type)
@@ -1200,8 +1202,8 @@ def test_empty_repo(self, rw_dir):
         self.assertEqual(r.active_branch.name, "master")
         assert not r.active_branch.is_valid(), "Branch is yet to be born"
 
-        # Actually, when trying to create a new branch without a commit, git itself fails.
-        # We should, however, not fail ungracefully.
+        # Actually, when trying to create a new branch without a commit, git itself
+        # fails. We should, however, not fail ungracefully.
         self.assertRaises(BadName, r.create_head, "foo")
         self.assertRaises(BadName, r.create_head, "master")
         # It's expected to not be able to access a tree
@@ -1315,13 +1317,13 @@ def test_git_work_tree_dotgit(self, rw_dir):
         repo = Repo(worktree_path)
         self.assertIsInstance(repo, Repo)
 
-        # This ensures we're able to actually read the refs in the tree, which
-        # means we can read commondir correctly.
+        # This ensures we're able to actually read the refs in the tree, which means we
+        # can read commondir correctly.
         commit = repo.head.commit
         self.assertIsInstance(commit, Object)
 
-        # This ensures we can read the remotes, which confirms we're reading
-        # the config correctly.
+        # This ensures we can read the remotes, which confirms we're reading the config
+        # correctly.
         origin = repo.remotes.origin
         self.assertIsInstance(origin, Remote)
 
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 993f6b57e..68164729b 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -94,13 +94,15 @@ def _do_base_tests(self, rwrepo):
         # The module is not checked-out yet.
         self.assertRaises(InvalidGitRepositoryError, sm.module)
 
-        # ...which is why we can't get the branch either - it points into the module() repository.
+        # ...which is why we can't get the branch either - it points into the module()
+        # repository.
         self.assertRaises(InvalidGitRepositoryError, getattr, sm, "branch")
 
         # branch_path works, as it's just a string.
         assert isinstance(sm.branch_path, str)
 
-        # Some commits earlier we still have a submodule, but it's at a different commit.
+        # Some commits earlier we still have a submodule, but it's at a different
+        # commit.
         smold = next(Submodule.iter_items(rwrepo, self.k_subm_changed))
         assert smold.binsha != sm.binsha
         assert smold != sm  # the name changed
@@ -141,11 +143,12 @@ def _do_base_tests(self, rwrepo):
         smold.set_parent_commit(self.k_subm_changed + "~1")
         assert smold.binsha != sm.binsha
 
-        # Raises if the sm didn't exist in new parent - it keeps its
-        # parent_commit unchanged.
+        # Raises if the sm didn't exist in new parent - it keeps its parent_commit
+        # unchanged.
         self.assertRaises(ValueError, smold.set_parent_commit, self.k_no_subm_tag)
 
-        # TEST TODO: If a path is in the .gitmodules file, but not in the index, it raises.
+        # TODO: Test that, if a path is in the .gitmodules file, but not in the index,
+        # then it raises.
 
         # TEST UPDATE
         ##############
@@ -196,8 +199,8 @@ def _do_base_tests(self, rwrepo):
 
             # INTERLEAVE ADD TEST
             #####################
-            # url must match the one in the existing repository (if submodule name suggests a new one)
-            # or we raise.
+            # url must match the one in the existing repository (if submodule name
+            # suggests a new one) or we raise.
             self.assertRaises(
                 ValueError,
                 Submodule.add,
@@ -228,7 +231,8 @@ def _do_base_tests(self, rwrepo):
             assert not csm.module_exists()
             csm_repopath = csm.path
 
-            # Adjust the path of the submodules module to point to the local destination.
+            # Adjust the path of the submodules module to point to the local
+            # destination.
             new_csmclone_path = Git.polish_url(osp.join(self.rorepo.working_tree_dir, sm.path, csm.path))
             with csm.config_writer() as writer:
                 writer.set_value("url", new_csmclone_path)
@@ -249,7 +253,8 @@ def _do_base_tests(self, rwrepo):
             # This flushed in a sub-submodule.
             assert len(list(rwrepo.iter_submodules())) == 2
 
-            # Reset both heads to the previous version, verify that to_latest_revision works.
+            # Reset both heads to the previous version, verify that to_latest_revision
+            # works.
             smods = (sm.module(), csm.module())
             for repo in smods:
                 repo.head.reset("HEAD~2", working_tree=1)
@@ -296,8 +301,8 @@ def _do_base_tests(self, rwrepo):
             # Must delete something.
             self.assertRaises(ValueError, csm.remove, module=False, configuration=False)
 
-            # module() is supposed to point to gitdb, which has a child-submodule whose URL is still pointing
-            # to GitHub. To save time, we will change it to:
+            # module() is supposed to point to gitdb, which has a child-submodule whose
+            # URL is still pointing to GitHub. To save time, we will change it to:
             csm.set_parent_commit(csm.repo.head.commit)
             with csm.config_writer() as cw:
                 cw.set_value("url", self._small_repo_url())
@@ -399,8 +404,8 @@ def _do_base_tests(self, rwrepo):
             rwrepo.index.commit("my submod commit")
             assert len(rwrepo.submodules) == 2
 
-            # Needs update, as the head changed. It thinks it's in the history
-            # of the repo otherwise.
+            # Needs update, as the head changed.
+            # It thinks it's in the history of the repo otherwise.
             nsm.set_parent_commit(rwrepo.head.commit)
             osm.set_parent_commit(rwrepo.head.commit)
 
@@ -434,7 +439,8 @@ def _do_base_tests(self, rwrepo):
 
             # REMOVE 'EM ALL
             ################
-            # If a submodule's repo has no remotes, it can't be added without an explicit url.
+            # If a submodule's repo has no remotes, it can't be added without an
+            # explicit url.
             osmod = osm.module()
 
             osm.remove(module=False)
@@ -510,7 +516,8 @@ def test_root_module(self, rwrepo):
 
         # TEST UPDATE
         #############
-        # Set up a commit that removes existing, adds new and modifies existing submodules.
+        # Set up a commit that removes existing, adds new and modifies existing
+        # submodules.
         rm = RootModule(rwrepo)
         assert len(rm.children()) == 1
 
@@ -534,13 +541,15 @@ def test_root_module(self, rwrepo):
         sm.update(recursive=False)
         assert sm.module_exists()
         with sm.config_writer() as writer:
-            writer.set_value("path", fp)  # Change path to something with prefix AFTER url change.
+            # Change path to something with prefix AFTER url change.
+            writer.set_value("path", fp)
 
-        # Update doesn't fail, because list_items ignores the wrong path in such situations.
+        # Update doesn't fail, because list_items ignores the wrong path in such
+        # situations.
         rm.update(recursive=False)
 
-        # Move it properly - doesn't work as it its path currently points to an indexentry
-        # which doesn't exist (move it to some path, it doesn't matter here).
+        # Move it properly - doesn't work as it its path currently points to an
+        # indexentry which doesn't exist (move it to some path, it doesn't matter here).
         self.assertRaises(InvalidGitRepositoryError, sm.move, pp)
         # Reset the path(cache) to where it was, now it works.
         sm.path = prep
@@ -588,23 +597,27 @@ def test_root_module(self, rwrepo):
         rm.update(recursive=False, dry_run=True, force_remove=True)
         assert osp.isdir(smp)
 
-        # When removing submodules, we may get new commits as nested submodules are auto-committing changes
-        # to allow deletions without force, as the index would be dirty otherwise.
+        # When removing submodules, we may get new commits as nested submodules are
+        # auto-committing changes to allow deletions without force, as the index would
+        # be dirty otherwise.
         # QUESTION: Why does this seem to work in test_git_submodule_compatibility() ?
         self.assertRaises(InvalidGitRepositoryError, rm.update, recursive=False, force_remove=False)
         rm.update(recursive=False, force_remove=True)
         assert not osp.isdir(smp)
 
-        # 'Apply work' to the nested submodule and ensure this is not removed/altered during updates
-        # Need to commit first, otherwise submodule.update wouldn't have a reason to change the head.
+        # 'Apply work' to the nested submodule and ensure this is not removed/altered
+        # during updates. We need to commit first, otherwise submodule.update wouldn't
+        # have a reason to change the head.
         touch(osp.join(nsm.module().working_tree_dir, "new-file"))
-        # We cannot expect is_dirty to even run as we wouldn't reset a head to the same location.
+        # We cannot expect is_dirty to even run as we wouldn't reset a head to the same
+        # location.
         assert nsm.module().head.commit.hexsha == nsm.hexsha
         nsm.module().index.add([nsm])
         nsm.module().index.commit("added new file")
         rm.update(recursive=False, dry_run=True, progress=prog)  # Would not change head, and thus doesn't fail.
-        # Everything we can do from now on will trigger the 'future' check, so no is_dirty() check will even run.
-        # This would only run if our local branch is in the past and we have uncommitted changes.
+        # Everything we can do from now on will trigger the 'future' check, so no
+        # is_dirty() check will even run. This would only run if our local branch is in
+        # the past and we have uncommitted changes.
 
         prev_commit = nsm.module().head.commit
         rm.update(recursive=False, dry_run=False, progress=prog)
@@ -616,8 +629,8 @@ def test_root_module(self, rwrepo):
 
         # Change url...
         # =============
-        # ...to the first repository. This way we have a fast checkout, and a completely different
-        # repository at the different url.
+        # ...to the first repository. This way we have a fast checkout, and a completely
+        # different repository at the different url.
         nsm.set_parent_commit(csmremoved)
         nsmurl = Git.polish_url(osp.join(self.rorepo.working_tree_dir, rsmsp[0]))
         with nsm.config_writer() as writer:
@@ -637,16 +650,15 @@ def test_root_module(self, rwrepo):
         assert len(rwrepo.submodules) == 1
         assert not rwrepo.submodules[0].children()[0].module_exists(), "nested submodule should not be checked out"
 
-        # Add the submodule's changed commit to the index, which is what the
-        # user would do.
-        # Beforehand, update our instance's binsha with the new one.
+        # Add the submodule's changed commit to the index, which is what the user would
+        # do. Beforehand, update our instance's binsha with the new one.
         nsm.binsha = nsm.module().head.commit.binsha
         rwrepo.index.add([nsm])
 
         # Change branch.
         # ==============
-        # We only have one branch, so we switch to a virtual one, and back
-        # to the current one to trigger the difference.
+        # We only have one branch, so we switch to a virtual one, and back to the
+        # current one to trigger the difference.
         cur_branch = nsm.branch
         nsmm = nsm.module()
         prev_commit = nsmm.head.commit
@@ -808,8 +820,8 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):
         smm.git.add(Git.polish_url(fp))
         smm.git.commit(m="new file added")
 
-        # Submodules are retrieved from the current commit's tree, therefore we can't really get a new submodule
-        # object pointing to the new submodule commit.
+        # Submodules are retrieved from the current commit's tree, therefore we can't
+        # really get a new submodule object pointing to the new submodule commit.
         sm_too = parent.submodules["module_moved"]
         assert parent.head.commit.tree[sm.path].binsha == sm.binsha
         assert sm_too.binsha == sm.binsha, "cached submodule should point to the same commit as updated one"
@@ -848,8 +860,9 @@ def assert_exists(sm, value=True):
 
         # END assert_exists
 
-        # As git is backwards compatible itself, it would still recognize what we do here... unless we really
-        # muss it up. That's the only reason why the test is still here...
+        # As git is backwards compatible itself, it would still recognize what we do
+        # here... unless we really muss it up. That's the only reason why the test is
+        # still here...
         assert len(parent.git.submodule().splitlines()) == 1
 
         module_repo_path = osp.join(sm.module().working_tree_dir, ".git")
@@ -885,7 +898,8 @@ def assert_exists(sm, value=True):
         assert_exists(csm)
 
         # Rename nested submodule.
-        # This name would move itself one level deeper - needs special handling internally.
+        # This name would move itself one level deeper - needs special handling
+        # internally.
         new_name = csm.name + "/mine"
         assert csm.rename(new_name).name == new_name
         assert_exists(csm)
@@ -1011,13 +1025,15 @@ def test_branch_renames(self, rw_dir):
         sm_source_repo.index.add([new_file])
         sm.repo.index.commit("added new file")
 
-        # Change designated submodule checkout branch to the new upstream feature branch.
+        # Change designated submodule checkout branch to the new upstream feature
+        # branch.
         with sm.config_writer() as smcw:
             smcw.set_value("branch", sm_fb.name)
         assert sm.repo.is_dirty(index=True, working_tree=False)
         sm.repo.index.commit("changed submodule branch to '%s'" % sm_fb)
 
-        # Verify submodule update with feature branch that leaves currently checked out branch in it's past.
+        # Verify submodule update with feature branch that leaves currently checked out
+        # branch in it's past.
         sm_mod = sm.module()
         prev_commit = sm_mod.commit()
         assert sm_mod.head.ref.name == "master"
@@ -1029,7 +1045,8 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.head.ref.name == sm_fb.name
         assert sm_mod.commit() == sm_fb.commit
 
-        # Create new branch which is in our past, and thus seemingly unrelated to the currently checked out one.
+        # Create new branch which is in our past, and thus seemingly unrelated to the
+        # currently checked out one.
         # To make it even 'harder', we shall fork and create a new commit.
         sm_pfb = sm_source_repo.create_head("past-feature", commit="HEAD~20")
         sm_pfb.checkout()
@@ -1043,8 +1060,8 @@ def test_branch_renames(self, rw_dir):
 
         # Test submodule updates - must fail if submodule is dirty.
         touch(osp.join(sm_mod.working_tree_dir, "unstaged file"))
-        # This doesn't fail as our own submodule binsha didn't change, and the reset is only triggered if
-        # to_latest_revision is True.
+        # This doesn't fail as our own submodule binsha didn't change, and the reset is
+        # only triggered if to_latest_revision is True.
         parent_repo.submodule_update(to_latest_revision=False)
         assert sm_mod.head.ref.name == sm_pfb.name, "should have been switched to past head"
         assert sm_mod.commit() == sm_fb.commit, "Head wasn't reset"
@@ -1184,8 +1201,8 @@ def test_submodule_add_unsafe_url_allowed(self, rw_repo):
                 "fd::/foo",
             ]
             for url in urls:
-                # The URL will be allowed into the command, but the command will
-                # fail since we don't have that protocol enabled in the Git config file.
+                # The URL will be allowed into the command, but the command will fail
+                # since we don't have that protocol enabled in the Git config file.
                 with self.assertRaises(GitCommandError):
                     Submodule.add(rw_repo, "new", "new", url, allow_unsafe_protocols=True)
                 assert not tmp_file.exists()
@@ -1269,8 +1286,8 @@ def test_submodule_update_unsafe_url_allowed(self, rw_repo):
             ]
             for url in urls:
                 submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=url)
-                # The URL will be allowed into the command, but the command will
-                # fail since we don't have that protocol enabled in the Git config file.
+                # The URL will be allowed into the command, but the command will fail
+                # since we don't have that protocol enabled in the Git config file.
                 with self.assertRaises(GitCommandError):
                     submodule.update(allow_unsafe_protocols=True)
                 assert not tmp_file.exists()
diff --git a/test/test_util.py b/test/test_util.py
index 65f77082d..824b3ab3d 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -153,7 +153,8 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         reason="PermissionError is only ever wrapped on Windows",
     )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
-        """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
+        """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is
+        true."""
         self._patch_for_wrapping_test(mocker, True)
 
         with pytest.raises(SkipTest):
@@ -171,7 +172,8 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         ],
     )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
-        """rmtree does not wrap PermissionError on non-Windows systems or when HIDE_WINDOWS_KNOWN_ERRORS is false."""
+        """rmtree does not wrap PermissionError on non-Windows systems or when
+        HIDE_WINDOWS_KNOWN_ERRORS is false."""
         self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
 
         with pytest.raises(PermissionError):
@@ -182,7 +184,9 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
 
     @pytest.mark.parametrize("hide_windows_known_errors", [False, True])
     def test_does_not_wrap_other_errors(self, tmp_path, mocker, hide_windows_known_errors):
-        file_not_found_tmpdir = tmp_path / "testdir"  # It is deliberately never created.
+        # The file is deliberately never created.
+        file_not_found_tmpdir = tmp_path / "testdir"
+
         self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
 
         with pytest.raises(FileNotFoundError):
@@ -502,7 +506,8 @@ def test_actor_get_uid_laziness_called(self, mock_get_uid):
         committer = Actor.committer(None)
         author = Actor.author(None)
         # We can't test with `self.rorepo.config_reader()` here, as the UUID laziness
-        # depends on whether the user running the test has their global user.name config set.
+        # depends on whether the user running the test has their global user.name config
+        # set.
         self.assertEqual(committer.name, "user")
         self.assertTrue(committer.email.startswith("user@"))
         self.assertEqual(author.name, "user")

From 4b04d8a33371d655a3cb5416ba18215fa08e0edd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 21:34:30 -0500
Subject: [PATCH 519/994] Better clarify Submodule.branch_path documentation

This revisits the changes in 3813bfb and takes a different
approach, using the phrase "full repository-relative path" that is
used elsewhere in the documentation and seems clear in context.

Although this brings back the potential confusion around having the
terms "full" and "relative" used together to describe a path, this
may no longer be an issue now that the phrase "repository-relative"
is used here as it is elsewhere.

(In particular, see the SymbolicReference.to_full_path docstring.)
---
 git/objects/submodule/base.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index dc1deee36..6379d1500 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -134,7 +134,7 @@ def __init__(
             The URL to the remote repository which is the submodule.
 
         :param branch_path:
-            Complete relative path to ref to checkout when cloning the remote
+            Full repository-relative path to ref to checkout when cloning the remote
             repository.
         """
         super().__init__(repo, binsha, mode, path)
@@ -1473,8 +1473,8 @@ def branch(self) -> "Head":
     def branch_path(self) -> PathLike:
         """
         :return:
-            Complete relative path as string to the branch we would checkout from the
-            remote and track
+            Full repository-relative path as string to the branch we would checkout from
+            the remote and track
         """
         return self._branch_path
 

From 254c82a00a3a6d141cfb6df7fdb8f33a23d4ebcb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 21:58:22 -0500
Subject: [PATCH 520/994] More docstring revisions within git.refs

---
 git/refs/head.py      | 32 +++++++++++++++----------------
 git/refs/log.py       |  8 ++++----
 git/refs/reference.py |  7 ++++---
 git/refs/remote.py    |  2 +-
 git/refs/symbolic.py  | 44 +++++++++++++++++++++----------------------
 5 files changed, 47 insertions(+), 46 deletions(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index a051748ba..d546cb4ef 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -36,8 +36,8 @@ def strip_quotes(string: str) -> str:
 
 
 class HEAD(SymbolicReference):
-    """Special case of a SymbolicReference representing the repository's
-    HEAD reference."""
+    """Special case of a SymbolicReference representing the repository's HEAD
+    reference."""
 
     _HEAD_NAME = "HEAD"
     _ORIG_HEAD_NAME = "ORIG_HEAD"
@@ -66,22 +66,21 @@ def reset(
         paths: Union[PathLike, Sequence[PathLike], None] = None,
         **kwargs: Any,
     ) -> "HEAD":
-        """Reset our HEAD to the given commit optionally synchronizing
-        the index and working tree. The reference we refer to will be set to commit as
-        well.
+        """Reset our HEAD to the given commit optionally synchronizing the index and
+        working tree. The reference we refer to will be set to commit as well.
 
         :param commit:
             :class:`~git.objects.commit.Commit`, :class:`~git.refs.reference.Reference`,
             or string identifying a revision we should reset HEAD to.
 
         :param index:
-            If True, the index will be set to match the given commit.
+            If ``True``, the index will be set to match the given commit.
             Otherwise it will not be touched.
 
         :param working_tree:
-            If True, the working tree will be forcefully adjusted to match the given
+            If ``True``, the working tree will be forcefully adjusted to match the given
             commit, possibly overwriting uncommitted changes without warning.
-            If `working_tree` is True, `index` must be True as well.
+            If `working_tree` is ``True``, `index` must be ``True`` as well.
 
         :param paths:
             Single path or list of paths relative to the git root directory
@@ -90,7 +89,8 @@ def reset(
         :param kwargs:
             Additional arguments passed to ``git reset``.
 
-        :return: self
+        :return:
+            self
         """
         mode: Union[str, None]
         mode = "--soft"
@@ -151,8 +151,8 @@ def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, *
         """Delete the given heads.
 
         :param force:
-            If True, the heads will be deleted even if they are not yet merged into the
-            main development stream. Default False.
+            If ``True``, the heads will be deleted even if they are not yet merged into
+            the main development stream. Default ``False``.
         """
         flag = "-d"
         if force:
@@ -193,7 +193,7 @@ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None])
     def tracking_branch(self) -> Union["RemoteReference", None]:
         """
         :return:
-            The remote reference we are tracking, or None if we are not a tracking
+            The remote reference we are tracking, or ``None`` if we are not a tracking
             branch.
         """
         from .remote import RemoteReference
@@ -219,14 +219,14 @@ def rename(self, new_path: PathLike, force: bool = False) -> "Head":
             The prefix ``refs/heads`` is implied.
 
         :param force:
-            If True, the rename will succeed even if a head with the target name
+            If ``True``, the rename will succeed even if a head with the target name
             already exists.
 
         :return:
             self
 
         :note:
-            Respects the ref log as git commands are used.
+            Respects the ref log, as git commands are used.
         """
         flag = "-m"
         if force:
@@ -244,8 +244,8 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
         The command will fail if changed working tree files would be overwritten.
 
         :param force:
-            If True, changes to the index and the working tree will be discarded.
-            If False, :class:`~git.exc.GitCommandError` will be raised in that
+            If ``True``, changes to the index and the working tree will be discarded.
+            If ``False``, :class:`~git.exc.GitCommandError` will be raised in that
             situation.
 
         :param kwargs:
diff --git a/git/refs/log.py b/git/refs/log.py
index 260f2fff5..7aefeb4e6 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -104,7 +104,7 @@ def new(
         tz_offset: int,
         message: str,
     ) -> "RefLogEntry":  # skipcq: PYL-W0621
-        """:return: New instance of a RefLogEntry"""
+        """:return: New instance of a :class:`RefLogEntry`"""
         if not isinstance(actor, Actor):
             raise ValueError("Need actor instance, got %s" % actor)
         # END check types
@@ -112,7 +112,7 @@ def new(
 
     @classmethod
     def from_line(cls, line: bytes) -> "RefLogEntry":
-        """:return: New RefLogEntry instance from the given revlog line.
+        """:return: New :class:`RefLogEntry` instance from the given revlog line.
 
         :param line:
             Line bytes without trailing newline
@@ -311,7 +311,7 @@ def append_entry(
         :param config_reader:
             Configuration reader of the repository - used to obtain user information.
             May also be an :class:`~git.util.Actor` instance identifying the committer
-            directly or None.
+            directly or ``None``.
 
         :param filepath:
             Full path to the log file.
@@ -326,7 +326,7 @@ def append_entry(
             Message describing the change to the reference.
 
         :param write:
-            If True, the changes will be written right away.
+            If ``True``, the changes will be written right away.
             Otherwise the change will not be written.
 
         :return:
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 32547278f..d1a9f3c54 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -65,7 +65,7 @@ def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> Non
             e.g. ``refs/heads/master``.
 
         :param check_path:
-            If False, you can provide any path.
+            If ``False``, you can provide any path.
             Otherwise the path must start with the default path prefix of this type.
         """
         if check_path and not str(path).startswith(self._common_path_default + "/"):
@@ -141,8 +141,9 @@ def iter_items(
         *args: Any,
         **kwargs: Any,
     ) -> Iterator[T_References]:
-        """Equivalent to SymbolicReference.iter_items, but will return non-detached
-        references as well."""
+        """Equivalent to
+        :meth:`SymbolicReference.iter_items <git.refs.symbolic.SymbolicReference.iter_items>`,
+        but will return non-detached references as well."""
         return cls._iter_items(repo, common_path)
 
     # }END interface
diff --git a/git/refs/remote.py b/git/refs/remote.py
index c2c2c1aac..bb2a4e438 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -56,7 +56,7 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:
         """Delete the given remote references.
 
         :note:
-            kwargs are given for comparability with the base class method as we
+            `kwargs` are given for comparability with the base class method as we
             should not narrow the signature.
         """
         repo.git.branch("-d", "-r", *refs)
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 6b9fd9ab7..70af4615d 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -63,7 +63,7 @@ class SymbolicReference:
     This does not point to a specific commit, but to another
     :class:`~git.refs.head.Head`, which itself specifies a commit.
 
-    A typical example for a symbolic reference is ``HEAD``.
+    A typical example for a symbolic reference is :class:`~git.refs.head.HEAD`.
     """
 
     __slots__ = ("repo", "path")
@@ -115,8 +115,8 @@ def _get_packed_refs_path(cls, repo: "Repo") -> str:
 
     @classmethod
     def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
-        """Return an iterator yielding pairs of sha1/path pairs (as strings)
-        for the corresponding refs.
+        """Return an iterator yielding pairs of sha1/path pairs (as strings) for the
+        corresponding refs.
 
         :note:
             The packed refs file will be kept open as long as we iterate.
@@ -226,9 +226,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.
+            points to, or ``None``.
 
-            target_ref_path is the reference we point to, or None.
+            target_ref_path is the reference we point to, or ``None``.
         """
         if ref_path:
             cls._check_ref_name_valid(ref_path)
@@ -273,7 +273,7 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T
         :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.
+            target_ref_path is the reference we point to, or ``None``.
         """
         return cls._get_ref_info_helper(repo, ref_path)
 
@@ -313,7 +313,7 @@ def set_commit(
         :class:`~git.objects.commit.Commit`.
 
         :raise ValueError:
-            If `commit` is not a :class:`~git.objects.commit.Commit` object or doesn't
+            If `commit` is not a :class:`~git.objects.commit.Commit` object, nor does it
             point to a commit.
 
         :return:
@@ -357,7 +357,7 @@ def set_object(
             to.
 
         :param logmsg:
-            If not None, the message will be used in the reflog entry to be written.
+            If not ``None``, the message will be used in the reflog entry to be written.
             Otherwise the reflog is not altered.
 
         :note:
@@ -491,8 +491,8 @@ def set_reference(
     def is_valid(self) -> bool:
         """
         :return:
-            True if the reference is valid, hence it can be read and points to a valid
-            object or reference.
+            ``True`` if the reference is valid, hence it can be read and points to a
+            valid object or reference.
         """
         try:
             self.object
@@ -505,7 +505,7 @@ def is_valid(self) -> bool:
     def is_detached(self) -> bool:
         """
         :return:
-            True if we are a detached reference, hence we point to a specific commit
+            ``True`` if we are a detached reference, hence we point to a specific commit
             instead to another reference.
         """
         try:
@@ -565,7 +565,7 @@ def log_append(
     def log_entry(self, index: int) -> "RefLogEntry":
         """
         :return:
-            RefLogEntry at the given index
+            :class:`~git.refs.log.RefLogEntry` at the given index
 
         :param index:
             Python list compatible positive or negative index.
@@ -582,7 +582,7 @@ def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
         """
         :return:
             String with a full repository-relative path which can be used to initialize
-            a Reference instance, for instance by using
+            a :class:`~git.refs.reference.Reference` instance, for instance by using
             :meth:`Reference.from_path <git.refs.reference.Reference.from_path>`.
         """
         if isinstance(path, SymbolicReference):
@@ -666,7 +666,7 @@ def _create(
     ) -> T_References:
         """Internal method used to create a new symbolic reference.
 
-        If `resolve` is False, the reference will be taken as is, creating a proper
+        If `resolve` is ``False``, the reference will be taken as is, creating a proper
         symbolic reference. Otherwise it will be resolved to the corresponding object
         and a detached symbolic reference will be created instead.
         """
@@ -722,12 +722,12 @@ def create(
             If it is a commit-ish, the symbolic ref will be detached.
 
         :param force:
-            If True, force creation even if a symbolic reference with that name already
-            exists. Raise :class:`OSError` otherwise.
+            If ``True``, force creation even if a symbolic reference with that name
+            already exists. Raise :class:`OSError` otherwise.
 
         :param logmsg:
-            If not None, the message to append to the reflog.
-            If None, no reflog entry is written.
+            If not ``None``, the message to append to the reflog.
+            If ``None``, no reflog entry is written.
 
         :return:
             Newly created symbolic reference
@@ -751,8 +751,8 @@ def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference"
             In case this is a symbolic ref, there is no implied prefix.
 
         :param force:
-            If True, the rename will succeed even if a head with the target name already
-            exists. It will be overwritten in that case.
+            If ``True``, the rename will succeed even if a head with the target name
+            already exists. It will be overwritten in that case.
 
         :return:
             self
@@ -847,8 +847,8 @@ def iter_items(
         :param common_path:
             Optional keyword argument to the path which is to be shared by all returned
             Ref objects.
-            Defaults to class specific portion if None, ensuring that only refs suitable
-            for the actual class are returned.
+            Defaults to class specific portion if ``None``, ensuring that only refs
+            suitable for the actual class are returned.
 
         :return:
             A list of :class:`SymbolicReference`, each guaranteed to be a symbolic ref

From 679d2e87b4a4c758a1059854f03fb6b4b2fd28d3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 22:01:26 -0500
Subject: [PATCH 521/994] Fix exception type in require_remote_ref_path
 docstring

The require_remote_ref_path decorator has always used ValueError
(ever since its introduction in a92ab80) but was documented as
using TypeError.

ValueError is the correct exception to raise here, since this is
not any kind of type error or related condition. So the bug wasn't
in the function's behavior, but instead in the way that behavior
was documented. (The bug was fairly minor, since the function is
not listed in __all__.)
---
 git/refs/reference.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/refs/reference.py b/git/refs/reference.py
index d1a9f3c54..a7b545fed 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -25,7 +25,7 @@
 
 
 def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
-    """A decorator raising :class:`TypeError` if we are not a valid remote, based on the
+    """A decorator raising :class:`ValueError` if we are not a valid remote, based on the
     path."""
 
     def wrapper(self: T_References, *args: Any) -> _T:

From ee0301ab69e4f2af1248596a9efb897893c54da1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 23:13:54 -0500
Subject: [PATCH 522/994] More docstring revisions in second-level modules and
 git.__init__

---
 git/__init__.py |  2 +-
 git/cmd.py      | 36 +++++++++++++++++++-----------------
 git/config.py   | 11 ++++-------
 git/db.py       |  3 ++-
 git/diff.py     | 17 +++++++++--------
 git/remote.py   | 39 +++++++++++++++++++++------------------
 git/util.py     |  6 +++---
 7 files changed, 59 insertions(+), 55 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 6fc6110f4..ed8a88d4b 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -127,7 +127,7 @@ def refresh(path: Optional[PathLike] = None) -> None:
         immediately, relative to the current directory.
 
     :note:
-        The *path* parameter is usually omitted and cannot be used to specify a custom
+        The `path` parameter is usually omitted and cannot be used to specify a custom
         command whose location is looked up in a path search on each call. See
         :meth:`Git.refresh <git.cmd.Git.refresh>` for details on how to achieve this.
 
diff --git a/git/cmd.py b/git/cmd.py
index d0c76eafe..949814765 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -106,7 +106,7 @@ def handle_process_output(
     decode_streams: bool = True,
     kill_after_timeout: Union[None, float] = None,
 ) -> None:
-    """Register for notifications to learn that process output is ready to read, and
+    R"""Register for notifications to learn that process output is ready to read, and
     dispatch lines to the respective line handlers.
 
     This function returns once the finalizer returns.
@@ -126,8 +126,11 @@ def handle_process_output(
     :param decode_streams:
         Assume stdout/stderr streams are binary and decode them before pushing their
         contents to handlers.
-        Set this to ``False`` if ``universal_newlines == True`` (then streams are in
-        text mode) or if decoding must happen later (i.e. for :class:`~git.diff.Diff`s).
+
+        This defaults to ``True``. Set it to ``False``:
+
+        - if ``universal_newlines == True``, as then streams are in text mode, or
+        - if decoding must happen later, such as for :class:`~git.diff.Diff`\s.
 
     :param kill_after_timeout:
         :class:`float` or ``None``, Default = ``None``
@@ -379,15 +382,14 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     :note:
         The git executable is actually found during the refresh step in the top level
-        :mod:`__init__`. It can also be changed by explicitly calling
-        :func:`git.refresh`.
+        ``__init__``. It can also be changed by explicitly calling :func:`git.refresh`.
     """
 
     _refresh_token = object()  # Since None would match an initial _version_info_token.
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
-        """This gets called by the refresh function (see the top level __init__).
+        """This gets called by the refresh function (see the top level ``__init__``).
 
         :param path:
             Optional path to the git executable. If not absolute, it is resolved
@@ -868,8 +870,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         self.cat_file_all: Union[None, TBD] = None
 
     def __getattr__(self, name: str) -> Any:
-        """A convenience method as it allows to call the command as if it was
-        an object.
+        """A convenience method as it allows to call the command as if it was an object.
 
         :return:
             Callable object that will execute call :meth:`_call_process` with your
@@ -899,7 +900,7 @@ def working_dir(self) -> Union[None, PathLike]:
     @property
     def version_info(self) -> Tuple[int, ...]:
         """
-        :return:  tuple with integers representing the major, minor and additional
+        :return: Tuple with integers representing the major, minor and additional
             version numbers as parsed from ``git version``. Up to four fields are used.
 
             This value is generated on demand and is cached.
@@ -1021,7 +1022,7 @@ def execute(
         :param output_stream:
             If set to a file-like object, data produced by the git command will be
             copied to the given stream instead of being returned as a string.
-            This feature only has any effect if `as_process` is False.
+            This feature only has any effect if `as_process` is ``False``.
 
         :param stdout_as_string:
             If ``False``, the command's standard output will be bytes. Otherwise, it
@@ -1030,10 +1031,10 @@ def execute(
 
         :param kill_after_timeout:
             Specifies a timeout in seconds for the git command, after which the process
-            should be killed. This will have no effect if `as_process` is set to True.
-            It is set to None by default and will let the process run until the timeout
-            is explicitly specified. Uses of this feature should be carefully
-            considered, due to the following limitations:
+            should be killed. This will have no effect if `as_process` is set to
+            ``True``. It is set to ``None`` by default and will let the process run
+            until the timeout is explicitly specified. Uses of this feature should be
+            carefully considered, due to the following limitations:
 
             1. This feature is not supported at all on Windows.
             2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
@@ -1099,7 +1100,7 @@ def execute(
 
         :note:
            If you add additional keyword arguments to the signature of this method,
-           you must update the execute_kwargs tuple housed in this module.
+           you must update the ``execute_kwargs`` variable housed in this module.
         """
         # Remove password for the command if present.
         redacted_command = remove_password_if_present(command)
@@ -1420,9 +1421,10 @@ def _call_process(
         :param kwargs:
             Contains key-values for the following:
 
-            - The :meth:`execute()` kwds, as listed in :var:`execute_kwargs`.
+            - The :meth:`execute()` kwds, as listed in ``execute_kwargs``.
             - "Command options" to be converted by :meth:`transform_kwargs`.
-            - The ``insert_kwargs_after`` key which its value must match one of ``*args``.
+            - The ``insert_kwargs_after`` key which its value must match one of
+              ``*args``.
 
             It also contains any command options, to be appended after the matched arg.
 
diff --git a/git/config.py b/git/config.py
index ff5c9d564..b358ee417 100644
--- a/git/config.py
+++ b/git/config.py
@@ -411,7 +411,7 @@ def release(self) -> None:
         not be used anymore afterwards.
 
         In Python 3, it's required to explicitly release locks and flush changes, as
-        :meth:`__del__` is not called deterministically anymore.
+        ``__del__`` is not called deterministically anymore.
         """
         # Checking for the lock here makes sure we do not raise during write()
         # in case an invalid parser was created who could not get a lock.
@@ -539,7 +539,7 @@ def _included_paths(self) -> List[Tuple[str, str]]:
         """List all paths that must be included to configuration.
 
         :return:
-            The list of paths, where each path is a tuple of ``(option, value)``.
+            The list of paths, where each path is a tuple of (option, value).
         """
         paths = []
 
@@ -591,9 +591,6 @@ def read(self) -> None:  # type: ignore[override]
         This will ignore files that cannot be read, possibly leaving an empty
         configuration.
 
-        :return:
-            Nothing
-
         :raise IOError:
             If a file cannot be handled.
         """
@@ -765,7 +762,7 @@ def add_section(self, section: str) -> None:
 
     @property
     def read_only(self) -> bool:
-        """:return: True if this instance may change the configuration file"""
+        """:return: ``True`` if this instance may change the configuration file"""
         return self._read_only
 
     # FIXME: Figure out if default or return type can really include bool.
@@ -918,7 +915,7 @@ def add_value(self, section: str, option: str, value: Union[str, bytes, int, flo
         return self
 
     def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
-        """Rename the given section to new_name.
+        """Rename the given section to `new_name`.
 
         :raise ValueError:
             If:
diff --git a/git/db.py b/git/db.py
index eb7f758da..3f496a979 100644
--- a/git/db.py
+++ b/git/db.py
@@ -54,7 +54,8 @@ def stream(self, binsha: bytes) -> OStream:
 
     def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
         """
-        :return: Full binary 20 byte sha from the given partial hexsha
+        :return:
+            Full binary 20 byte sha from the given partial hexsha
 
         :raise gitdb.exc.AmbiguousObjectName:
 
diff --git a/git/diff.py b/git/diff.py
index 3c760651b..adddaa7f6 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -121,9 +121,9 @@ def diff(
             * If ``None``, we will be compared to the working tree.
             * If :class:`~git.index.base.Treeish`, it will be compared against the
               respective tree.
-            * If :class:`~Diffable.Index`, it will be compared against the index.
+            * If :class:`Diffable.Index`, it will be compared against the index.
             * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
-            * It defaults to :class:`~Diffable.Index` so that the method will not by
+            * It defaults to :class:`Diffable.Index` so that the method will not by
               default fail on bare repositories.
 
         :param paths:
@@ -280,11 +280,11 @@ class Diff:
     Working Tree Blobs:
 
         When comparing to working trees, the working tree blob will have a null hexsha
-        as a corresponding object does not yet exist. The mode will be null as well.
-        The path will be available, though.
+        as a corresponding object does not yet exist. The mode will be null as well. The
+        path will be available, though.
 
-        If it is listed in a diff, the working tree version of the file must
-        differ from the version in the index or tree, and hence has been modified.
+        If it is listed in a diff, the working tree version of the file must differ from
+        the version in the index or tree, and hence has been modified.
     """
 
     # Precompiled regex.
@@ -468,7 +468,8 @@ def rename_to(self) -> Optional[str]:
 
     @property
     def renamed(self) -> bool:
-        """
+        """Deprecated, use :attr:`renamed_file` instead.
+
         :return:
             ``True`` if the blob of our diff has been renamed
 
@@ -480,7 +481,7 @@ def renamed(self) -> bool:
 
     @property
     def renamed_file(self) -> bool:
-        """:return: True if the blob of our diff has been renamed"""
+        """:return: ``True`` if the blob of our diff has been renamed"""
         return self.rename_from != self.rename_to
 
     @classmethod
diff --git a/git/remote.py b/git/remote.py
index 3d5b8fdc4..6ce720ee3 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -338,7 +338,7 @@ class FetchInfo(IterableObj):
 
     @classmethod
     def refresh(cls) -> Literal[True]:
-        """This gets called by the refresh function (see the top level __init__)."""
+        """This gets called by the refresh function (see the top level ``__init__``)."""
         # Clear the old values in _flag_map.
         with contextlib.suppress(KeyError):
             del cls._flag_map["t"]
@@ -386,19 +386,22 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
         """Parse information from the given line as returned by ``git-fetch -v`` and
         return a new :class:`FetchInfo` object representing this information.
 
-        We can handle a line as follows:
-        "%c %-\\*s %-\\*s -> %s%s"
+        We can handle a line as follows::
 
-        Where c is either ' ', !, +, -, \\*, or =
-        ! means error
-        + means success forcing update
-        - means a tag was updated
-        * means birth of new branch or tag
-        = means the head was up to date (and not moved)
-        ' ' means a fast-forward
+            %c %-*s %-*s -> %s%s
 
-        fetch line is the corresponding line from FETCH_HEAD, like
-        acb0fa8b94ef421ad60c8507b634759a472cd56c    not-for-merge   branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
+        Where ``c`` is either a space, ``!``, ``+``, ``-``, ``*``, or ``=``:
+
+        - '!' means error
+        - '+' means success forcing update
+        - '-' means a tag was updated
+        - '*' means birth of new branch or tag
+        - '=' means the head was up to date (and not moved)
+        - ' ' means a fast-forward
+
+        `fetch_line` is the corresponding line from FETCH_HEAD, like::
+
+            acb0fa8b94ef421ad60c8507b634759a472cd56c    not-for-merge   branch '0.1.7RC' of /tmp/tmpya0vairemote_repo
         """
         match = cls._re_fetch_result.match(line)
         if match is None:
@@ -625,7 +628,7 @@ def exists(self) -> bool:
 
     @classmethod
     def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]:
-        """:return: Iterator yielding Remote objects of the given repository"""
+        """:return: Iterator yielding :class:`Remote` objects of the given repository"""
         for section in repo.config_reader("repository").sections():
             if not section.startswith("remote "):
                 continue
@@ -639,7 +642,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote
     def set_url(
         self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
     ) -> "Remote":
-        """Configure URLs on current remote (cf command ``git remote set-url``).
+        """Configure URLs on current remote (cf. command ``git remote set-url``).
 
         This command manages URLs on the remote.
 
@@ -1020,7 +1023,7 @@ def fetch(
             facility.
 
         :param progress:
-            See :meth:`push` method.
+            See the :meth:`push` method.
 
         :param verbose:
             Boolean for verbose output.
@@ -1081,8 +1084,8 @@ def pull(
         allow_unsafe_options: bool = False,
         **kwargs: Any,
     ) -> IterableList[FetchInfo]:
-        """Pull changes from the given branch, being the same as a fetch followed
-        by a merge of branch with your local branch.
+        """Pull changes from the given branch, being the same as a fetch followed by a
+        merge of branch with your local branch.
 
         :param refspec:
             See :meth:`fetch` method.
@@ -1157,7 +1160,7 @@ def push(
 
         :param kill_after_timeout:
             To specify a timeout in seconds for the git command, after which the process
-            should be killed. It is set to None by default.
+            should be killed. It is set to ``None`` by default.
 
         :param allow_unsafe_protocols:
             Allow unsafe protocols to be used, like ``ext``.
diff --git a/git/util.py b/git/util.py
index e7dd94a08..bab765a09 100644
--- a/git/util.py
+++ b/git/util.py
@@ -762,9 +762,9 @@ def update(self, *args: Any, **kwargs: Any) -> None:
 
 
 class Actor:
-    """Actors hold information about a person acting on the repository. They
-    can be committers and authors or anything with a name and an email as mentioned in
-    the git log entries."""
+    """Actors hold information about a person acting on the repository. They can be
+    committers and authors or anything with a name and an email as mentioned in the git
+    log entries."""
 
     # PRECOMPILED REGEX
     name_only_regex = re.compile(r"<(.*)>")

From 231c3ef758a004bb51fae2868cb59742069c7dde Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 23:23:45 -0500
Subject: [PATCH 523/994] More docstring revisions within git.repo

---
 git/repo/base.py | 15 +++++++--------
 git/repo/fun.py  |  2 +-
 2 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index 4745a2411..a54591746 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -102,9 +102,8 @@ class BlameEntry(NamedTuple):
 
 
 class Repo:
-    """Represents a git repository and allows you to query references, father
-    commit information, generate diffs, create and clone repositories, and query the
-    log.
+    """Represents a git repository and allows you to query references, create commit
+    information, generate diffs, create and clone repositories, and query the log.
 
     The following attributes are worth using:
 
@@ -112,8 +111,8 @@ class Repo:
       working tree directory if available or the ``.git`` directory in case of bare
       repositories.
 
-    * :attr:`working_tree_dir` is the working tree directory, but will return None if we
-      are a bare repository.
+    * :attr:`working_tree_dir` is the working tree directory, but will return ``None``
+      if we are a bare repository.
 
     * :attr:`git_dir` is the ``.git`` repository directory, which is always set.
     """
@@ -596,7 +595,7 @@ def create_tag(
         return TagReference.create(self, path, ref, message, force, **kwargs)
 
     def delete_tag(self, *tags: TagReference) -> None:
-        """Delete the given tag references"""
+        """Delete the given tag references."""
         return TagReference.delete(self, *tags)
 
     def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
@@ -775,7 +774,7 @@ def iter_commits(
     def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
         R"""Find the closest common ancestor for the given revision
         (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
-        :class:`git.refs.reference.Reference`\s, etc.).
+        :class:`~git.refs.reference.Reference`\s, etc.).
 
         :param rev:
             At least two revs to find the common ancestor for.
@@ -1030,7 +1029,7 @@ def active_branch(self) -> Head:
             If HEAD is detached.
 
         :return:
-            Head to the active branch
+            :class:`~git.refs.head.Head` to the active branch
         """
         # reveal_type(self.head.reference)  # => Reference
         return self.head.reference
diff --git a/git/repo/fun.py b/git/repo/fun.py
index e9fad2c46..e3c69c68c 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -151,7 +151,7 @@ def name_to_object(
 
     :param return_ref:
         If ``True``, and name specifies a reference, we will return the reference
-        instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` o
+        instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` or
         `~gitdb.exc.BadName`.
     """
     hexsha: Union[None, str, bytes] = None

From e166a0a7b9dc2643c686e0a5b365e22d0fa88ae7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 23:45:33 -0500
Subject: [PATCH 524/994] More docstring revisions within git.objects

---
 git/objects/base.py   |  2 +-
 git/objects/commit.py | 15 ++++++++-------
 git/objects/fun.py    |  4 ++--
 git/objects/tag.py    |  2 +-
 git/objects/tree.py   | 10 +++++-----
 git/objects/util.py   | 15 ++++++++-------
 6 files changed, 25 insertions(+), 23 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 5cee8e405..e78420e8a 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -98,7 +98,7 @@ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
             New object instance of a type appropriate to represent the given binary sha1
 
         :param sha1:
-            20 byte binary sha1
+            20 byte binary sha1.
         """
         if sha1 == cls.NULL_BIN_SHA:
             # The NULL binsha is always the root commit.
diff --git a/git/objects/commit.py b/git/objects/commit.py
index b5b1c540d..06ab0898b 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -298,7 +298,7 @@ def iter_items(
             The :class:`~git.repo.base.Repo`.
 
         :param rev:
-            Revision specifier, see git-rev-parse for viable options.
+            Revision specifier. See git-rev-parse for viable options.
 
         :param paths:
             An optional path or list of paths. If set only :class:`Commit`\s that
@@ -309,7 +309,7 @@ def iter_items(
 
             * ``max_count`` is the maximum number of commits to fetch.
             * ``skip`` is the number of commits to skip.
-            * ``since`` all commits since e.g. '1970-01-01'.
+            * ``since`` selects all commits since some date, e.g. ``"1970-01-01"``.
 
         :return:
             Iterator yielding :class:`Commit` items.
@@ -380,12 +380,13 @@ def stats(self) -> Stats:
     def trailers(self) -> Dict[str, str]:
         """Deprecated. Get the trailers of the message as a dictionary.
 
-        :note: This property is deprecated, please use either :attr:`trailers_list` or
-            :attr:`trailers_dict``.
+        :note:
+            This property is deprecated, please use either :attr:`trailers_list` or
+            :attr:`trailers_dict`.
 
         :return:
-            Dictionary containing whitespace stripped trailer information. Only contains
-            the latest instance of each trailer key.
+            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()}
 
@@ -539,7 +540,7 @@ def create_from_tree(
         author_date: Union[None, str, datetime.datetime] = None,
         commit_date: Union[None, str, datetime.datetime] = None,
     ) -> "Commit":
-        """Commit the given tree, creating a commit object.
+        """Commit the given tree, creating a :class:`Commit` object.
 
         :param repo:
             :class:`~git.repo.base.Repo` object the commit should be part of.
diff --git a/git/objects/fun.py b/git/objects/fun.py
index ad5bc9a88..22b99cb6b 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -186,8 +186,8 @@ def traverse_trees_recursive(
         given tree.
 
     :param tree_shas:
-        Iterable of shas pointing to trees. All trees must be on the same level. A
-        tree-sha may be ``None`` in which case ``None``.
+        Iterable of shas pointing to trees. All trees must be on the same level.
+        A tree-sha may be ``None``, in which case ``None``.
 
     :param path_prefix:
         A prefix to be added to the returned paths on this level.
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 7589a5be1..f455c55fc 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Provides an :class:`git.objects.base.Object`-based type for annotated tags.
+"""Provides an :class:`~git.objects.base.Object`-based type for annotated tags.
 
 This defines the :class:`TagReference` class, which represents annotated tags.
 For lightweight tags, see the :mod:`git.refs.tag` module.
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 69cd6ef3f..a506bba7d 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -166,7 +166,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
 
     Tree as a list:
 
-    * Access a specific blob using the ``tree['filename']`` notation.
+    * Access a specific blob using the ``tree["filename"]`` notation.
     * You may likewise access by index, like ``blob = tree[0]``.
     """
 
@@ -215,8 +215,8 @@ def _set_cache_(self, attr: str) -> None:
         # END handle attribute
 
     def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]:
-        """Iterable yields tuples of (binsha, mode, name), which will be converted
-        to the respective object representation.
+        """Iterable yields tuples of (binsha, mode, name), which will be converted to
+        the respective object representation.
         """
         for binsha, mode, name in iterable:
             path = join_path(self.path, name)
@@ -338,8 +338,8 @@ def traverse(
     def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList[IndexObjUnion]:
         """
         :return:
-            :class:`~git.util.IterableList`IterableList with the results of the
-            traversal as produced by :meth:`traverse`
+            :class:`~git.util.IterableList` with the results of the traversal as
+            produced by :meth:`traverse`
 
             Tree -> IterableList[Union[Submodule, Tree, Blob]]
         """
diff --git a/git/objects/util.py b/git/objects/util.py
index 6f46bab18..6f4e7d087 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -474,7 +474,7 @@ def _traverse(
         ignore_self: int = 1,
         as_edge: bool = False,
     ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]:
-        """Iterator yielding items found when traversing self.
+        """Iterator yielding items found when traversing `self`.
 
         :param predicate:
             A function ``f(i,d)`` that returns ``False`` if item i at depth ``d`` should
@@ -485,9 +485,10 @@ def _traverse(
             item ``i`` at depth ``d``. Item ``i`` will not be returned.
 
         :param depth:
-            Defines at which level the iteration should not go deeper if -1, there is no
-            limit if 0, you would effectively only get self, the root of the iteration
-            i.e. if 1, you would only get the first level of predecessors/successors.
+            Defines at which level the iteration should not go deeper if -1. There is no
+            limit if 0, you would effectively only get `self`, the root of the
+            iteration. If 1, you would only get the first level of
+            predecessors/successors.
 
         :param branch_first:
             If ``True``, items will be returned branch first, otherwise depth first.
@@ -497,8 +498,8 @@ def _traverse(
             encountered several times. Loops are prevented that way.
 
         :param ignore_self:
-            If ``True``, self will be ignored and automatically pruned from the result.
-            Otherwise it will be the first item to be returned. If `as_edge` is
+            If ``True``, `self` will be ignored and automatically pruned from the
+            result. Otherwise it will be the first item to be returned. If `as_edge` is
             ``True``, the source of the first edge is ``None``.
 
         :param as_edge:
@@ -507,7 +508,7 @@ def _traverse(
             destination.
 
         :return:
-            Iterator yielding items found when traversing self::
+            Iterator yielding items found when traversing `self`::
 
                 Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] Submodule ->
                 Iterator[Submodule, Tuple[Submodule, Submodule]] Tree ->

From ffeb7e76c54c3e8af1e94e116f3a1ef714b36e19 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 28 Feb 2024 23:57:35 -0500
Subject: [PATCH 525/994] More docstring revisions in
 git.objects.submodule.base

---
 git/objects/submodule/base.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 6379d1500..cdd7c8e1b 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -172,7 +172,7 @@ def _set_cache_(self, attr: str) -> None:
 
     @classmethod
     def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]:
-        """:return: all the submodules of our module repository"""
+        """:return: All the submodules of our module repository"""
         try:
             return cls.list_items(item.module())
         except InvalidGitRepositoryError:
@@ -326,7 +326,7 @@ def _clone_repo(
             Allow unsafe options to be used, like ``--upload-pack``.
 
         :param kwargs:
-            Additional arguments given to ``git clone``
+            Additional arguments given to ``git clone``.
         """
         module_abspath = cls._module_abspath(repo, path, name)
         module_checkout_path = module_abspath
@@ -351,7 +351,7 @@ def _clone_repo(
 
     @classmethod
     def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
-        """:return: a path guaranteed to be relative to the given parent repository
+        """:return: A path guaranteed to be relative to the given parent repository
 
         :raise ValueError:
             If path is not contained in the parent repository's working tree.
@@ -642,7 +642,7 @@ def update(
             If ``True``, the submodule's sha will be ignored during checkout. Instead,
             the remote will be fetched, and the local tracking branch updated. This only
             works if we have a local tracking branch, which is the case if the remote
-            repository had a master branch, or of the ``branch`` option was specified
+            repository had a master branch, or if the ``branch`` option was specified
             for this submodule and the branch existed remotely.
 
         :param progress:
@@ -1309,7 +1309,7 @@ def config_writer(
             repository.
 
         :param write:
-            If True, the index will be written each time a configuration value changes.
+            If ``True``, the index will be written each time a configuration value changes.
 
         :note:
             The parameters allow for a more efficient writing of the index, as you can

From ec9395526719a87127efa9e8b942a05a4238aa25 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 29 Feb 2024 00:18:48 -0500
Subject: [PATCH 526/994] Further refine some docstring revisions

---
 git/cmd.py  | 16 +++++++++-------
 git/diff.py |  4 ++--
 2 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 949814765..6574cbb34 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -127,10 +127,10 @@ def handle_process_output(
         Assume stdout/stderr streams are binary and decode them before pushing their
         contents to handlers.
 
-        This defaults to ``True``. Set it to ``False``:
+        This defaults to ``True``. Set it to ``False`` if:
 
-        - if ``universal_newlines == True``, as then streams are in text mode, or
-        - if decoding must happen later, such as for :class:`~git.diff.Diff`\s.
+        - ``universal_newlines == True``, as then streams are in text mode, or
+        - decoding must happen later, such as for :class:`~git.diff.Diff`\s.
 
     :param kill_after_timeout:
         :class:`float` or ``None``, Default = ``None``
@@ -1085,13 +1085,15 @@ def execute(
             specify may not be the same ones.
 
         :return:
-            * str(output) if extended_output = False (Default)
-            * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
+            * str(output), if `extended_output` is ``False`` (Default)
+            * tuple(int(status), str(stdout), str(stderr)),
+              if `extended_output` is ``True``
 
             If `output_stream` is ``True``, the stdout value will be your output stream:
 
-            * output_stream if extended_output = False
-            * tuple(int(status), output_stream, str(stderr)) if extended_output = True
+            * output_stream, if `extended_output` is ``False``
+            * tuple(int(status), output_stream, str(stderr)),
+              if `extended_output` is ``True``
 
             Note that git is executed with ``LC_MESSAGES="C"`` to ensure consistent
             output regardless of system language.
diff --git a/git/diff.py b/git/diff.py
index adddaa7f6..a89ca9880 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -112,8 +112,8 @@ def diff(
         create_patch: bool = False,
         **kwargs: Any,
     ) -> "DiffIndex":
-        """Create diffs between two items being trees, trees and index or an
-        index and the working tree. Detects renames automatically.
+        """Create diffs between two items being trees, trees and index or an index and
+        the working tree. Detects renames automatically.
 
         :param other:
             This the item to compare us with.

From 63983c2b9206a4a599112aa6c302a143f4661799 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 29 Feb 2024 00:41:10 -0500
Subject: [PATCH 527/994] Remove note in GitCmdObjectDB docstring

For #1849.
---
 git/db.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/git/db.py b/git/db.py
index 3f496a979..bf0de40de 100644
--- a/git/db.py
+++ b/git/db.py
@@ -30,10 +30,6 @@ class GitCmdObjectDB(LooseObjectDB):
     objects, pack files and an alternates file.
 
     It will create objects only in the loose object database.
-
-    :note:
-        For now, we use the git command to do all the lookup, just until we have packs
-        and the other implementations.
     """
 
     def __init__(self, root_path: PathLike, git: "Git") -> None:

From f43292e4e4860c856de0ac52ef4a326aacff6579 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 29 Feb 2024 00:51:12 -0500
Subject: [PATCH 528/994] Somewhat improve _get_ref_info{,_helper} docstrings

+ Add a missing :class: reference in _get_commit docstring.
---
 git/refs/symbolic.py | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 70af4615d..142952e06 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -225,10 +225,10 @@ def _get_ref_info_helper(
     ) -> Union[Tuple[str, None], Tuple[None, str]]:
         """
         :return:
-            (str(sha), str(target_ref_path)) if available, the sha the file at rela_path
-            points to, or ``None``.
+            *(str(sha), str(target_ref_path))*, where:
 
-            target_ref_path is the reference we point to, or ``None``.
+            * *sha* is of the file at rela_path points to if available, or ``None``.
+            * *target_ref_path* is the reference we point to, or ``None``.
         """
         if ref_path:
             cls._check_ref_name_valid(ref_path)
@@ -270,10 +270,11 @@ def _get_ref_info_helper(
     @classmethod
     def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[Tuple[str, None], Tuple[None, str]]:
         """
-        :return: (str(sha), str(target_ref_path)) if available, the sha the file at
-            rela_path points to, or None.
+        :return:
+            *(str(sha), str(target_ref_path))*, where:
 
-            target_ref_path is the reference we point to, or ``None``.
+            * *sha* is of the file at rela_path points to if available, or ``None``.
+            * *target_ref_path* is the reference we point to, or ``None``.
         """
         return cls._get_ref_info_helper(repo, ref_path)
 
@@ -290,9 +291,9 @@ def _get_object(self) -> Commit_ish:
     def _get_commit(self) -> "Commit":
         """
         :return:
-            Commit object we point to. This works for detached and non-detached
-            :class:`SymbolicReference` instances. The symbolic reference will be
-            dereferenced recursively.
+            :class:`~git.objects.commit.Commit` object we point to. This works for
+            detached and non-detached :class:`SymbolicReference` instances. The symbolic
+            reference will be dereferenced recursively.
         """
         obj = self._get_object()
         if obj.type == "tag":

From 37c93de3d31aabf32f0032a2a49a9fee285fa6e8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 29 Feb 2024 03:44:44 -0500
Subject: [PATCH 529/994] A couple more small docstring refinements

- Single vs. double backtick error/inconsistency in one place
  (parameter names were in in doubled backticks, now single)

- Undo making "Iterator" in the phrase "Iterator yielding" a
  reference to collections.abc.Iterator, which is unnecessary and
  not done anywhere else.
---
 git/config.py | 4 ++--
 git/util.py   | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/config.py b/git/config.py
index b358ee417..2164f67dc 100644
--- a/git/config.py
+++ b/git/config.py
@@ -920,8 +920,8 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser":
         :raise ValueError:
             If:
 
-            * ``section`` doesn't exist.
-            * A section with ``new_name`` does already exist.
+            * `section` doesn't exist.
+            * A section with `new_name` does already exist.
 
         :return:
             This instance
diff --git a/git/util.py b/git/util.py
index bab765a09..52f9dbdad 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1257,7 +1257,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_Itera
         keyword arguments, subclasses are obliged to to yield all items.
 
         :return:
-            :class:`~collections.abc.Iterator` yielding Items
+            Iterator yielding Items
         """
         raise NotImplementedError("To be implemented by Subclass")
 

From 0d6c68af35d1d4a9da95b189db1da68fdcee40b8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Mar 2024 14:04:54 -0500
Subject: [PATCH 530/994] Fix ref to git.refresh in refresh methods' docstrings

---
 git/cmd.py    | 4 +++-
 git/remote.py | 5 ++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 6574cbb34..6e71e37fd 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -389,7 +389,9 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
-        """This gets called by the refresh function (see the top level ``__init__``).
+        """This gets called by the :func:`git.refresh` function.
+
+        See the top level ``__init__.py``.
 
         :param path:
             Optional path to the git executable. If not absolute, it is resolved
diff --git a/git/remote.py b/git/remote.py
index 6ce720ee3..c1b2c11f7 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -338,7 +338,10 @@ class FetchInfo(IterableObj):
 
     @classmethod
     def refresh(cls) -> Literal[True]:
-        """This gets called by the refresh function (see the top level ``__init__``)."""
+        """This gets called by the :func:`git.refresh` function.
+
+        See the top level ``__init__.py``.
+        """
         # Clear the old values in _flag_map.
         with contextlib.suppress(KeyError):
             del cls._flag_map["t"]

From 65657421014ebcf89e29734ca2383346063c8363 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Mar 2024 14:21:35 -0500
Subject: [PATCH 531/994] Further expand refresh methods' docstrings

---
 git/cmd.py    | 7 +++++--
 git/remote.py | 5 +++--
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 6e71e37fd..4fd6dc7cd 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -389,9 +389,12 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     @classmethod
     def refresh(cls, path: Union[None, PathLike] = None) -> bool:
-        """This gets called by the :func:`git.refresh` function.
+        """Update information about the git executable :class:`Git` objects will use.
 
-        See the top level ``__init__.py``.
+        Called by the :func:`git.refresh` function in the top level ``__init__``.
+
+        This gets called by the :func:`git.refresh` function in the top-level
+        ``__init__``.
 
         :param path:
             Optional path to the git executable. If not absolute, it is resolved
diff --git a/git/remote.py b/git/remote.py
index c1b2c11f7..2c671582b 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -338,9 +338,10 @@ class FetchInfo(IterableObj):
 
     @classmethod
     def refresh(cls) -> Literal[True]:
-        """This gets called by the :func:`git.refresh` function.
+        """Update information about which ``git fetch`` flags are supported by the git
+        executable being used.
 
-        See the top level ``__init__.py``.
+        Called by the :func:`git.refresh` function in the top level ``__init__``.
         """
         # Clear the old values in _flag_map.
         with contextlib.suppress(KeyError):

From c0a27c027d2e04e3379f68f9e3299f30730559c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Mar 2024 14:41:01 -0500
Subject: [PATCH 532/994] Better document overrides in GitCmdObjectDB

---
 git/db.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/git/db.py b/git/db.py
index bf0de40de..15e791e6c 100644
--- a/git/db.py
+++ b/git/db.py
@@ -38,11 +38,12 @@ def __init__(self, root_path: PathLike, git: "Git") -> None:
         self._git = git
 
     def info(self, binsha: bytes) -> OInfo:
+        """Get a git object header (using git itself)."""
         hexsha, typename, size = self._git.get_object_header(bin_to_hex(binsha))
         return OInfo(hex_to_bin(hexsha), typename, size)
 
     def stream(self, binsha: bytes) -> OStream:
-        """For now, all lookup is done by git itself"""
+        """Get git object data as a stream supporting ``read()`` (using git itself)."""
         hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha))
         return OStream(hex_to_bin(hexsha), typename, size, stream)
 

From 5ca0fbae5c8f1ae112ddd0d4647d3399fe610501 Mon Sep 17 00:00:00 2001
From: jcole-crowdstrike <jcole-crowdstrike@>
Date: Fri, 1 Mar 2024 11:55:32 -0800
Subject: [PATCH 533/994] Updating regex pattern to handle unicode whitespaces.

Replacing the \s whitespace characters with normal spaces (" ") to prevent breaking on unicode whitespace characters (e.g., NBSP). Without this, if a branch name contains a unicode whitespace character that falls under \s, then the branch name will be truncated.
---
 git/remote.py       |  2 +-
 test/test_remote.py | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/git/remote.py b/git/remote.py
index 6ce720ee3..6e841949f 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -325,7 +325,7 @@ class FetchInfo(IterableObj):
         ERROR,
     ) = [1 << x for x in range(8)]
 
-    _re_fetch_result = re.compile(r"^\s*(.) (\[[\w\s\.$@]+\]|[\w\.$@]+)\s+(.+) -> ([^\s]+)(    \(.*\)?$)?")
+    _re_fetch_result = re.compile(r"^ *(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)(    \(.*\)?$)?")
 
     _flag_map: Dict[flagKeyLiteral, int] = {
         "!": ERROR,
diff --git a/test/test_remote.py b/test/test_remote.py
index c0bd11f80..63eababac 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -1002,6 +1002,21 @@ def test_push_unsafe_options_allowed(self, rw_repo):
                 assert tmp_file.exists()
                 tmp_file.unlink()
 
+    @with_rw_and_rw_remote_repo("0.1.6")
+    def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo):
+        # Create branch with a name containing a NBSP
+        bad_branch_name = f"branch_with_{chr(160)}_nbsp"
+        Head.create(remote_repo, bad_branch_name)
+
+        # Fetch and get branches
+        remote = rw_repo.remote("origin")
+        branches = remote.fetch()
+
+        # Test for truncated branch name in branches
+        assert f"origin/{bad_branch_name}" in [b.name for b in branches]
+
+        # Cleanup branch
+        Head.delete(remote_repo, bad_branch_name)
 
 class TestTimeouts(TestBase):
     @with_rw_repo("HEAD", bare=False)

From 827e986ec686b168e65f3bbad526d6a863191031 Mon Sep 17 00:00:00 2001
From: jcole-crowdstrike <jcole-crowdstrike@>
Date: Fri, 1 Mar 2024 13:51:33 -0800
Subject: [PATCH 534/994] Updating spacing per linting test.

Adding a second blank line after the newly added test case.
---
 test/test_remote.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/test_remote.py b/test/test_remote.py
index 63eababac..35af8172d 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -1018,6 +1018,7 @@ def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo):
         # Cleanup branch
         Head.delete(remote_repo, bad_branch_name)
 
+
 class TestTimeouts(TestBase):
     @with_rw_repo("HEAD", bare=False)
     def test_timeout_funcs(self, repo):

From fc59e5eafde730c9380266335edb278a23630ee8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 1 Mar 2024 18:59:39 -0500
Subject: [PATCH 535/994] Improve stream_object_data and _parse_object_header
 docstrings

This slightly adjusts working and formatting of these methods of
the Git class, for clarity.
---
 git/cmd.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 4fd6dc7cd..41b88bd54 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1491,7 +1491,9 @@ def _call_process(
     def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]:
         """
         :param header_line:
-            <hex_sha> type_string size_as_int
+            A line of the form::
+
+                <hex_sha> type_string size_as_int
 
         :return:
             (hex_sha, type_string, size_as_int)
@@ -1581,7 +1583,7 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]:
         return (hexsha, typename, size, data)
 
     def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]:
-        """Similar to :meth:`get_object_header`, but returns the data as a stream.
+        """Similar to :meth:`get_object_data`, but returns the data as a stream.
 
         :return:
             (hexsha, type_string, size_as_int, stream)

From 7044ff6b9c5381c5506fd1f2a71546641ed259f7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 01:48:30 -0500
Subject: [PATCH 536/994] Include top-level git.refresh in API Reference

For #1854.
---
 doc/source/reference.rst | 39 +++++++++++++++++++++------------------
 1 file changed, 21 insertions(+), 18 deletions(-)

diff --git a/doc/source/reference.rst b/doc/source/reference.rst
index 68a7f0ba4..6ee25c04d 100644
--- a/doc/source/reference.rst
+++ b/doc/source/reference.rst
@@ -3,13 +3,16 @@
 API Reference
 =============
 
-Version
--------
+Top-Level
+---------
 
 .. py:data:: git.__version__
 
    Current GitPython version.
 
+.. automodule:: git
+   :members: refresh
+
 Objects.Base
 ------------
 
@@ -17,7 +20,7 @@ Objects.Base
    :members:
    :undoc-members:
    :special-members:
- 
+
 Objects.Blob
 ------------
 
@@ -25,7 +28,7 @@ Objects.Blob
    :members:
    :undoc-members:
    :special-members:
-   
+
 Objects.Commit
 --------------
 
@@ -33,7 +36,7 @@ Objects.Commit
    :members:
    :undoc-members:
    :special-members:
-   
+
 Objects.Tag
 -----------
 
@@ -73,7 +76,7 @@ Objects.Submodule.root
    :members:
    :undoc-members:
    :special-members:
-   
+
 Objects.Submodule.util
 ----------------------
 
@@ -81,7 +84,7 @@ Objects.Submodule.util
    :members:
    :undoc-members:
    :special-members:
-   
+
 Objects.Util
 -------------
 
@@ -105,7 +108,7 @@ Index.Functions
    :members:
    :undoc-members:
    :special-members:
-   
+
 Index.Types
 -----------
 
@@ -113,7 +116,7 @@ Index.Types
    :members:
    :undoc-members:
    :special-members:
-   
+
 Index.Util
 -------------
 
@@ -121,7 +124,7 @@ Index.Util
    :members:
    :undoc-members:
    :special-members:
-   
+
 GitCmd
 ------
 
@@ -137,7 +140,7 @@ Config
    :members:
    :undoc-members:
    :special-members:
-   
+
 Diff
 ----
 
@@ -154,7 +157,7 @@ Exceptions
    :undoc-members:
    :special-members:
 
- 
+
 Refs.symbolic
 -------------
 
@@ -162,7 +165,7 @@ Refs.symbolic
    :members:
    :undoc-members:
    :special-members:
-   
+
 Refs.reference
 --------------
 
@@ -178,7 +181,7 @@ Refs.head
    :members:
    :undoc-members:
    :special-members:
-   
+
 Refs.tag
 ------------
 
@@ -186,7 +189,7 @@ Refs.tag
    :members:
    :undoc-members:
    :special-members:
-   
+
 Refs.remote
 ------------
 
@@ -194,7 +197,7 @@ Refs.remote
    :members:
    :undoc-members:
    :special-members:
-   
+
 Refs.log
 ------------
 
@@ -202,7 +205,7 @@ Refs.log
    :members:
    :undoc-members:
    :special-members:
-   
+
 Remote
 ------
 
@@ -218,7 +221,7 @@ Repo.Base
    :members:
    :undoc-members:
    :special-members:
-   
+
 Repo.Functions
 --------------
 

From 24160d1c7217622ec9b4de49ee2bf710e385ba0e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 03:11:15 -0500
Subject: [PATCH 537/994] Add git.compat, git.db, and git.types in API
 Reference

For #1854 (along with 7044ff6).
---
 doc/source/reference.rst | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/doc/source/reference.rst b/doc/source/reference.rst
index 6ee25c04d..13dd38d02 100644
--- a/doc/source/reference.rst
+++ b/doc/source/reference.rst
@@ -230,6 +230,30 @@ Repo.Functions
    :undoc-members:
    :special-members:
 
+Compat
+------
+
+.. automodule:: git.compat
+   :members:
+   :undoc-members:
+   :special-members:
+
+DB
+--
+
+.. automodule:: git.db
+   :members:
+   :undoc-members:
+   :special-members:
+
+Types
+-----
+
+.. automodule:: git.types
+   :members:
+   :undoc-members:
+   :special-members:
+
 Util
 ----
 

From d1da48d512a9f68ced06e47998295c6b910eb640 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 03:14:03 -0500
Subject: [PATCH 538/994] Fix unterminated double-backtick in a git.compat
 docstring

Adding git.compat in reference.rst revealed this (and makes this
fix necessary for the documentation to build).
---
 git/compat.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/compat.py b/git/compat.py
index 3167cecdc..7753fe8b2 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -61,7 +61,7 @@
 
 :note:
     For macOS (Darwin), ``os.name == "posix"`` as in other Unix-like systems, while
-    ``sys.platform == "darwin"`.
+    ``sys.platform == "darwin"``.
 """
 
 defenc = sys.getfilesystemencoding()

From c4a6618eefcc5c2610bb0e247e02d188ac10663f Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sat, 2 Mar 2024 11:14:03 +0100
Subject: [PATCH 539/994] Remove duplicate information in docstring

---
 git/cmd.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 41b88bd54..c7cec48d7 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -393,9 +393,6 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
 
         Called by the :func:`git.refresh` function in the top level ``__init__``.
 
-        This gets called by the :func:`git.refresh` function in the top-level
-        ``__init__``.
-
         :param path:
             Optional path to the git executable. If not absolute, it is resolved
             immediately, relative to the current directory. (See note below.)

From 5253b8d6910cf2defea946955f86ea80ffcac8a2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 03:46:00 -0500
Subject: [PATCH 540/994] Restore building of documentation downloads

Although documentation resumed being built in 634151a, and the RTD
theme and API Reference section were restored in 64ad585 (#1843),
documentation for download did not resume being built, with 3.1.37
(not 3.1.42) being the latest version listed here:

    https://readthedocs.org/projects/gitpython/downloads/

Three kinds of downloadable documentation are supported -- PDF,
ePub, and HTML -- and all three were previously being built but
all have stopped.

This attempts to resume building all three, using `all` as the
value of the `formats` key in .readthedocs.yml.

A string value of `all` currently should have the same effect as a
sequence value of the strings `htmlzip`, `pdf`, and `epub`. (In the
future, `all` may build more formats.) See:

- https://docs.readthedocs.io/en/stable/downloadable-documentation.html
- https://docs.readthedocs.io/en/stable/config-file/v2.html#formats
---
 .readthedocs.yaml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 04275ce72..0b83e20ea 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -23,9 +23,7 @@ sphinx:
   fail_on_warning: true
 
 # Optionally build your docs in additional formats such as PDF and ePub.
-# formats:
-# - pdf
-# - epub
+formats: all
 
 # Optional but recommended, declare the Python requirements required
 # to build your documentation.

From f83b056e6d89c45df8f3fb57fa3e0c6dbc16772b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 15:29:12 -0500
Subject: [PATCH 541/994] Revise assert_never

This revises the docstring of git.types.assert_never to more
clearly express its semantics and usage, to use the type names used
in the typing module, typing_extensions package, and mypy. This
expands some parts when doing so seemed to benefit clarity.

The docstring had previously said AssertionError was raised, but
the code raised ValueError. For now I've adjusted the docstring to
be accurate to the code's behavior, but maybe this should change.

I also removed the part about the mypy error being on variable
creation, since I am not clear on what that was referring to, and
if it means the first name binding operation for a variable in its
scope, then I am not sure why that would be where an error message
would be expected when using assert_never.

Finally, this slightly adjusts the message:

- "Literal" is decapitalized, to decrease confusion if the default
  message is used when matching on something not a typing.Literal.

- The exception message prints the unexpected value's repr rather
  than its str, since the repr, when different from the str, is
  usually the representation more useful for debugging.
---
 git/types.py | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/git/types.py b/git/types.py
index efb393471..0bd723176 100644
--- a/git/types.py
+++ b/git/types.py
@@ -72,19 +72,27 @@
 
 
 def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None:
-    """For use in exhaustive checking of literal or Enum in if/else chain.
+    """For use in exhaustive checking of a literal or enum in if/else chains.
 
-    Should only be reached if all members not handled OR attempt to pass non-members through chain.
+    A call to this function should only be reached if not all members are handled, or if
+    an attempt is made to pass non-members through the chain.
 
-    If all members handled, type is Empty. Otherwise, will cause mypy error.
+    :param inp:
+        If all members are handled, the argument for `inp` will have the
+        :class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will
+        mismatch and cause a mypy error.
 
-    If non-members given, should cause mypy error at variable creation.
+    :param raise_error:
+        If ``True``, will also raise :class:`ValueError` with a general "unhandled
+        literal" message, or the exception object passed as `exc`.
 
-    If raise_error is True, will also raise AssertionError or the Exception passed to exc.
+    :param exc:
+        It not ``None``, this should be an already-constructed exception object, to be
+        raised if `raise_error` is ``True``.
     """
     if raise_error:
         if exc is None:
-            raise ValueError(f"An unhandled Literal ({inp}) in an if/else chain was found")
+            raise ValueError(f"An unhandled literal ({inp!r}) in an if/else chain was found")
         else:
             raise exc
 

From 01cc8e28ae1677286e32f9f00764dbefe83c3787 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 21:40:30 -0500
Subject: [PATCH 542/994] Fix unnecessarily long reference in Tree docstrings

There is no advantage to fully qualifying names in the same module
as the entity being documented, but I had accidentally done that.
---
 git/objects/tree.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index a506bba7d..a3bd7181c 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -162,7 +162,7 @@ def __delitem__(self, name: str) -> None:
 
 class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
-    other :class:`~git.objects.tree.Tree`\s.
+    other :class:`Tree`\s.
 
     Tree as a list:
 
@@ -230,8 +230,8 @@ def join(self, file: str) -> IndexObjUnion:
         """Find the named object in this tree's contents.
 
         :return:
-            :class:`~git.objects.blob.Blob`, :class:`~git.objects.tree.Tree`,
-            or :class:`~git.objects.submodule.base.Submodule`
+            :class:`~git.objects.blob.Blob`, :class:`Tree`, or
+            :class:`~git.objects.submodule.base.Submodule`
 
         :raise KeyError:
             If the given file or tree does not exist in this tree.

From 6f3a20f1b651a75d38366f177708a96dd78d157e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 21:45:42 -0500
Subject: [PATCH 543/994] Change how tree[subscript] is introduced

This modifies the Tree docstring to replace the phrase "Tree as a
list" with a phrase that:

- Encompasses the use where subscripts are not integers or slices.
- Is more precise in its terminology.

The former advantage--not leaving out the case where one can pass
a string--is the rationale.
---
 git/objects/tree.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index a3bd7181c..0960215aa 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -164,7 +164,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
     other :class:`Tree`\s.
 
-    Tree as a list:
+    Subscripting is supported, as with a mapping or sequence:
 
     * Access a specific blob using the ``tree["filename"]`` notation.
     * You may likewise access by index, like ``blob = tree[0]``.

From 85889cde01292c2c4b041eb031f645a5fdf62f27 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 21:50:23 -0500
Subject: [PATCH 544/994] Refine how tree[subscript] is introduced

This drops the newly introduced precision in Python terminology,
likening usage to that of a list or dict (experienced Python users
will already know about sequences and mappings, and inexperienced
users will, with these terms, be able to understand what is said).

Note that this does not undo the broader effect of the previous
commit, which is to make clear that subscripting a tree supports
both list-like and dict-like usage, not just list-like usage.
---
 git/objects/tree.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index 0960215aa..8e7ef6ae6 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -164,7 +164,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
     other :class:`Tree`\s.
 
-    Subscripting is supported, as with a mapping or sequence:
+    Subscripting is supported, as with a list or dict:
 
     * Access a specific blob using the ``tree["filename"]`` notation.
     * You may likewise access by index, like ``blob = tree[0]``.

From 9e470839fe0a082cfdb61ba18f31e5e18cd345ad Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 23:39:29 -0500
Subject: [PATCH 545/994] Start adding docstrings to types in git.types

---
 git/types.py | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/git/types.py b/git/types.py
index 0bd723176..0080e8911 100644
--- a/git/types.py
+++ b/git/types.py
@@ -39,6 +39,7 @@
 #     from typing_extensions import TypeGuard  # noqa: F401
 
 PathLike = Union[str, "os.PathLike[str]"]
+"""A :class:`str` (Unicode) based file or directory path."""
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -47,6 +48,8 @@
     # from git.refs import SymbolicReference
 
 TBD = Any
+"""Alias of :class:`~typing.Any`, when a type hint is meant to become more specific."""
+
 _T = TypeVar("_T")
 
 Tree_ish = Union["Commit", "Tree"]
@@ -56,17 +59,27 @@
 # Config_levels ---------------------------------------------------------
 
 Lit_config_levels = Literal["system", "global", "user", "repository"]
+"""Type of literal strings naming git configuration levels.
 
-# Progress parameter type alias -----------------------------------------
+Such a string identifies what level, or scope, a git configuration variables is in.
+"""
 
-CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]
+ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
+"""Static type of a tuple of the four strings representing configuration levels."""
 
 # def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]:
 #     # return inp in get_args(Lit_config_level)  # only py >= 3.8
 #     return inp in ("system", "user", "global", "repository")
 
+# Progress parameter type alias -----------------------------------------
 
-ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
+CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]
+"""General type of a progress reporter for cloning.
+
+This is the type of a function or other callable that reports the progress of a clone,
+when passed as a ``progress`` argument to :meth:`Repo.clone <git.repo.base.Repo.clone>`
+or :meth:`Repo.clone_from <git.repo.base.Repo.clone_from>`.
+"""
 
 # -----------------------------------------------------------------------------------
 

From 3bd8177f07e724edfb780c8b19149fd19577cb88 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 2 Mar 2024 23:45:42 -0500
Subject: [PATCH 546/994] Document Tree_ish, Commit_ish, and related types

The Commit_ish docstring may require substantive revision. The
follow claims about the reasion for the design of Commit_ish should
be checked, and if false removed or fixed, and if true then
possibly clarified or otherwise refined:

- "often usable where a commit-ish is expected" (both as for
  whether it is a reasonable generalization and for whether it is
  actually part of the reason Commit_ish includes Blob and Tree)

- "It is done for practical reasons including backward
  compatibility." (for whether that is why it was done is or kept)
---
 git/types.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/git/types.py b/git/types.py
index 0080e8911..497d5fa45 100644
--- a/git/types.py
+++ b/git/types.py
@@ -53,8 +53,58 @@
 _T = TypeVar("_T")
 
 Tree_ish = Union["Commit", "Tree"]
+"""Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish.
+
+See gitglossary(7) on "tree-ish": https://git-scm.com/docs/gitglossary#def_tree-ish
+
+:note:
+    This union comprises **only** the :class:`~git.objects.commit.Commit` and
+    :class:`~git.objects.tree.Tree` classes, **all** of whose instances are tree-ish.
+    This is done because of the way GitPython uses it as a static type annotation.
+
+    :class:`~git.objects.tag.TagObject`, some but not all of whose instances are
+    tree-ish (those representing git tag objects that ultimately resolve to a tree or
+    commit), is not covered as part of this union type.
+"""
+
 Commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
+"""Union of the :class:`~git.objects.base.Object`-based types that represent kinds of
+git objects. This union is often usable where a commit-ish is expected, but is not
+actually limited to types representing commit-ish git objects.
+
+See gitglossary(7) on:
+
+* "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish
+* "object type": https://git-scm.com/docs/gitglossary#def_object_type
+
+:note:
+    This union comprises **more** classes than those whose instances really represent
+    commit-ish git objects:
+
+    * A :class:`~git.objects.commit.Commit` is of course always
+      commit-ish, and a :class:`~git.objects.tag.TagObject` is commit-ish if, when
+      peeled (recursively followed), a :class:`~git.objects.commit.Commit` is obtained.
+    * However, :class:`~git.objects.blob.Blob` and :class:`~git.objects.tree.Tree` are
+      also included, and they represent git objects that are never really commit-ish.
+
+    This is an inversion of the situation with :class:`Tree_ish`, which is narrower than
+    all tree-ish objects. It is done for practical reasons including backward
+    compatibility.
+"""
+
 Lit_commit_ish = Literal["commit", "tag", "blob", "tree"]
+"""Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes.
+
+See :class:`Object.type <git.objects.base.Object.type>`.
+
+:note:
+    See also :class:`Commit_ish`, a union of the the :class:`~git.objects.base.Object`
+    subtypes associated with these literal strings.
+
+:note:
+    As noted in :class:`Commit_ish`, this is not limited to types of git objects that
+    are actually commit-ish.
+"""
 
 # Config_levels ---------------------------------------------------------
 

From f3b9a695a066360c5dc0ce0209d8bd4a57126930 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 3 Mar 2024 00:06:09 -0500
Subject: [PATCH 547/994] Expand docs of classes representing Git objects

This expands the docstrings of the Object base class, as well as
the four leaf subclasses that represent git objects (Blob, Commit,
TagObject, and Tree) to clarify those classes' relationship to git
objects in general and specific types of git objects, to each
other, and where applicable to types defined in git.types.

This includes links to specific sections of the gitglossary(7)
manpage (as hosted online), where directly applicable.
---
 git/objects/base.py   | 53 ++++++++++++++++++++++++++++++++++++++-----
 git/objects/blob.py   |  5 +++-
 git/objects/commit.py |  8 +++++--
 git/objects/tag.py    |  6 ++++-
 git/objects/tree.py   |  3 +++
 5 files changed, 65 insertions(+), 10 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index e78420e8a..c1df9e756 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -37,9 +37,33 @@
 
 
 class Object(LazyMixin):
-    """An Object which may be :class:`~git.objects.blob.Blob`,
-    :class:`~git.objects.tree.Tree`, :class:`~git.objects.commit.Commit` or
-    `~git.objects.tag.TagObject`."""
+    """Base class for classes representing kinds of git objects.
+
+    The following leaf classes represent specific kinds of git objects:
+
+    * :class:`Blob <git.objects.blob.Blob>`
+    * :class:`Tree <git.objects.tree.Tree>`
+    * :class:`Commit <git.objects.commit.Commit>`
+    * :class:`TagObject <git.objects.tag.TagObject>`
+
+    See gitglossary(7) on:
+
+    * "object": https://git-scm.com/docs/gitglossary#def_object
+    * "object type": https://git-scm.com/docs/gitglossary#def_object_type
+    * "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+    * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
+    * "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
+    * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
+
+    :note:
+        See the :class:`git.types.Commit_ish` union type.
+
+    :note:
+        :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
+        rooted at this :class:`Object` class, even though submodules are not really a
+        kind of git object.
+
+    """
 
     NULL_HEX_SHA = "0" * 40
     NULL_BIN_SHA = b"\0" * 20
@@ -54,6 +78,20 @@ class Object(LazyMixin):
     __slots__ = ("repo", "binsha", "size")
 
     type: Union[Lit_commit_ish, None] = None
+    """String identifying (a concrete :class:`Object` subtype for) a kind of git object.
+
+    The subtypes that this may name correspond to the kinds of git objects that exist,
+    i.e., the objects that may be present in a git repository.
+
+    :note:
+        Most subclasses represent specific types of git objects and override this class
+        attribute accordingly. This attribute is ``None`` in the :class:`Object` base
+        class, as well as the :class:`IndexObject` intermediate subclass, but never
+        ``None`` in concrete leaf subclasses representing specific kinds of git objects.
+
+    :note:
+        See also :class:`~git.types.Commit_ish`.
+    """
 
     def __init__(self, repo: "Repo", binsha: bytes):
         """Initialize an object by identifying it by its binary sha.
@@ -174,9 +212,12 @@ def stream_data(self, ostream: "OStream") -> "Object":
 
 
 class IndexObject(Object):
-    """Base for all objects that can be part of the index file, namely
-    :class:`~git.objects.tree.Tree`, :class:`~git.objects.blob.Blob` and
-    :class:`~git.objects.submodule.base.Submodule` objects."""
+    """Base for all objects that can be part of the index file.
+
+    The classes representing kinds of git objects that can be part of the index file are
+    :class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition,
+    :class:`~git.objects.submodule.base.Submodule` is also a subclass.
+    """
 
     __slots__ = ("path", "mode")
 
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 253ceccb5..b37570c3a 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -12,7 +12,10 @@
 
 
 class Blob(base.IndexObject):
-    """A Blob encapsulates a git blob object."""
+    """A Blob encapsulates a git blob object.
+
+    See gitglossary(7) on "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+    """
 
     DEFAULT_MIME_TYPE = "text/plain"
     type: Literal["blob"] = "blob"
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 06ab0898b..3246d9a36 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -60,8 +60,12 @@
 class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     """Wraps a git commit object.
 
-    This class will act lazily on some of its attributes and will query the value on
-    demand only if it involves calling the git binary.
+    See gitglossary(7) on "commit object":
+    https://git-scm.com/docs/gitglossary#def_commit_object
+
+    :note:
+        This class will act lazily on some of its attributes and will query the value on
+        demand only if it involves calling the git binary.
     """
 
     # ENVIRONMENT VARIABLES
diff --git a/git/objects/tag.py b/git/objects/tag.py
index f455c55fc..49fb97e37 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -30,7 +30,11 @@
 
 class TagObject(base.Object):
     """Annotated (i.e. non-lightweight) tag carrying additional information about an
-    object we are pointing to."""
+    object we are pointing to.
+
+    See gitglossary(7) on "tag object":
+    https://git-scm.com/docs/gitglossary#def_tag_object
+    """
 
     type: Literal["tag"] = "tag"
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 8e7ef6ae6..995c9a663 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -164,6 +164,9 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
     other :class:`Tree`\s.
 
+    See gitglossary(7) on "tree object":
+    https://git-scm.com/docs/gitglossary#def_tree_object
+
     Subscripting is supported, as with a list or dict:
 
     * Access a specific blob using the ``tree["filename"]`` notation.

From 2af7640345697888cce7c6eae7ab247aed2e497f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 3 Mar 2024 00:17:07 -0500
Subject: [PATCH 548/994] Do a bit of tidying related to unused names

In git.types.

- Remove commented-out import of SymbolicReference, which is not
  used anywhere, nor mentioned in commented-out code.

- Add a docstring to _T to note that it is used within GitPython.
  (Otherwise it looks left over, as no code in git.types uses it.)
---
 git/types.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/git/types.py b/git/types.py
index 497d5fa45..714a084ef 100644
--- a/git/types.py
+++ b/git/types.py
@@ -45,12 +45,11 @@
     from git.repo import Repo
     from git.objects import Commit, Tree, TagObject, Blob
 
-    # from git.refs import SymbolicReference
-
 TBD = Any
 """Alias of :class:`~typing.Any`, when a type hint is meant to become more specific."""
 
 _T = TypeVar("_T")
+"""Type variable used internally in GitPython."""
 
 Tree_ish = Union["Commit", "Tree"]
 """Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish.

From 2aa053e0d1eead06dce588265fdc8c0a1f29f7cd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 3 Mar 2024 01:11:22 -0500
Subject: [PATCH 549/994] Add docstrings to TypedDicts in git.types

---
 git/types.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/git/types.py b/git/types.py
index 714a084ef..b888cea51 100644
--- a/git/types.py
+++ b/git/types.py
@@ -160,12 +160,24 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception,
 
 
 class Files_TD(TypedDict):
+    """Dictionary with stat counts for the diff of a particular file.
+
+    For the :class:`~git.util.Stats.files` attribute of :class:`~git.util.Stats`
+    objects.
+    """
+
     insertions: int
     deletions: int
     lines: int
 
 
 class Total_TD(TypedDict):
+    """Dictionary with total stats from any number of files.
+
+    For the :class:`~git.util.Stats.total` attribute of :class:`~git.util.Stats`
+    objects.
+    """
+
     insertions: int
     deletions: int
     lines: int
@@ -173,6 +185,8 @@ class Total_TD(TypedDict):
 
 
 class HSH_TD(TypedDict):
+    """Dictionary carrying the same information as a :class:`~git.util.Stats` object."""
+
     total: Total_TD
     files: Dict[PathLike, Files_TD]
 

From 15d50dee206c6e834ad9bea53ab182de381d7d41 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 10:34:44 -0500
Subject: [PATCH 550/994] Revise a couple new docstrings for clarity

---
 git/types.py | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/git/types.py b/git/types.py
index b888cea51..cb29be478 100644
--- a/git/types.py
+++ b/git/types.py
@@ -80,9 +80,9 @@
     This union comprises **more** classes than those whose instances really represent
     commit-ish git objects:
 
-    * A :class:`~git.objects.commit.Commit` is of course always
-      commit-ish, and a :class:`~git.objects.tag.TagObject` is commit-ish if, when
-      peeled (recursively followed), a :class:`~git.objects.commit.Commit` is obtained.
+    * A :class:`~git.objects.commit.Commit` is of course always commit-ish, and a
+      :class:`~git.objects.tag.TagObject` is commit-ish if, when peeled (recursively
+      followed), a :class:`~git.objects.commit.Commit` is obtained.
     * However, :class:`~git.objects.blob.Blob` and :class:`~git.objects.tree.Tree` are
       also included, and they represent git objects that are never really commit-ish.
 
@@ -92,9 +92,10 @@
 """
 
 Lit_commit_ish = Literal["commit", "tag", "blob", "tree"]
-"""Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes.
+"""Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes
+representing kinds of git objects.
 
-See :class:`Object.type <git.objects.base.Object.type>`.
+See the :class:`Object.type <git.objects.base.Object.type>` attribute.
 
 :note:
     See also :class:`Commit_ish`, a union of the the :class:`~git.objects.base.Object`
@@ -110,7 +111,7 @@
 Lit_config_levels = Literal["system", "global", "user", "repository"]
 """Type of literal strings naming git configuration levels.
 
-Such a string identifies what level, or scope, a git configuration variables is in.
+Such a string identifies what level, or scope, a git configuration variable is in.
 """
 
 ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]

From 716670326d5fba5356628408237dfb7786d5d228 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 10:40:26 -0500
Subject: [PATCH 551/994] Fix possible inaccuracy in Lit_config_levels
 docstring

These are not necessarily what one means by the "scope" of a
configuration variable, in part because of the nature of the subtle
distinction between "user" and "global", and in part because of
other issues such as how setting a variable for a single command
with "-c" has a scope which is not listed.

This also brings the docstring somewhat more in line with how these
values are documented elsewhere in GitPython.
---
 git/types.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/types.py b/git/types.py
index cb29be478..9cd38a4f4 100644
--- a/git/types.py
+++ b/git/types.py
@@ -111,7 +111,7 @@
 Lit_config_levels = Literal["system", "global", "user", "repository"]
 """Type of literal strings naming git configuration levels.
 
-Such a string identifies what level, or scope, a git configuration variable is in.
+These strings relate to which file a git configuration variable is in.
 """
 
 ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]

From 1530fd2b8137bb7c93206cdb373374db46c884c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 10:52:05 -0500
Subject: [PATCH 552/994] Use phrases like "git object type" where applicable

In some recently introduced or expanded docstrings, I had overused
phrases like "kind of git object" with the hope of avoiding
confusion with the meanings of "type" relevant to Python (i.e.,
"class" or "static type"). But this made the relationship to git's
own notion of "object type" less clear than it could be, especially
in docstrings that also included links to the gitglossary(7) entry
for "object type" (because those links' relevance was less clear).

This dials back the use of "kind" to where I am more confident that
it is clarifying or at least not confusing.
---
 git/objects/base.py | 13 +++++++------
 git/types.py        |  8 ++++----
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index c1df9e756..0496b18cd 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -37,7 +37,7 @@
 
 
 class Object(LazyMixin):
-    """Base class for classes representing kinds of git objects.
+    """Base class for classes representing git object types.
 
     The following leaf classes represent specific kinds of git objects:
 
@@ -61,7 +61,7 @@ class Object(LazyMixin):
     :note:
         :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
         rooted at this :class:`Object` class, even though submodules are not really a
-        kind of git object.
+        type of git object.
 
     """
 
@@ -78,7 +78,7 @@ class Object(LazyMixin):
     __slots__ = ("repo", "binsha", "size")
 
     type: Union[Lit_commit_ish, None] = None
-    """String identifying (a concrete :class:`Object` subtype for) a kind of git object.
+    """String identifying (a concrete :class:`Object` subtype for) a git object type.
 
     The subtypes that this may name correspond to the kinds of git objects that exist,
     i.e., the objects that may be present in a git repository.
@@ -87,7 +87,7 @@ class Object(LazyMixin):
         Most subclasses represent specific types of git objects and override this class
         attribute accordingly. This attribute is ``None`` in the :class:`Object` base
         class, as well as the :class:`IndexObject` intermediate subclass, but never
-        ``None`` in concrete leaf subclasses representing specific kinds of git objects.
+        ``None`` in concrete leaf subclasses representing specific git object types.
 
     :note:
         See also :class:`~git.types.Commit_ish`.
@@ -214,9 +214,10 @@ def stream_data(self, ostream: "OStream") -> "Object":
 class IndexObject(Object):
     """Base for all objects that can be part of the index file.
 
-    The classes representing kinds of git objects that can be part of the index file are
+    The classes representing git object types that can be part of the index file are
     :class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition,
-    :class:`~git.objects.submodule.base.Submodule` is also a subclass.
+    :class:`~git.objects.submodule.base.Submodule`, which is not really a git object
+    type but can be part of an index file, is also a subclass.
     """
 
     __slots__ = ("path", "mode")
diff --git a/git/types.py b/git/types.py
index 9cd38a4f4..e8e8dd3e4 100644
--- a/git/types.py
+++ b/git/types.py
@@ -67,9 +67,9 @@
 """
 
 Commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
-"""Union of the :class:`~git.objects.base.Object`-based types that represent kinds of
-git objects. This union is often usable where a commit-ish is expected, but is not
-actually limited to types representing commit-ish git objects.
+"""Union of the :class:`~git.objects.base.Object`-based types that represent git object
+types. This union is often usable where a commit-ish is expected, but is not actually
+limited to types representing commit-ish git objects.
 
 See gitglossary(7) on:
 
@@ -93,7 +93,7 @@
 
 Lit_commit_ish = Literal["commit", "tag", "blob", "tree"]
 """Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes
-representing kinds of git objects.
+representing git object types.
 
 See the :class:`Object.type <git.objects.base.Object.type>` attribute.
 

From 2e02b09f1d9aeda2db1fe2340c131bb42ab87cb9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 11:47:53 -0500
Subject: [PATCH 553/994] Add docstrings to protocols in git.types

---
 git/types.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/git/types.py b/git/types.py
index e8e8dd3e4..9159195cd 100644
--- a/git/types.py
+++ b/git/types.py
@@ -194,9 +194,13 @@ class HSH_TD(TypedDict):
 
 @runtime_checkable
 class Has_Repo(Protocol):
+    """Protocol for having a :attr:`repo` attribute, the repository to operate on."""
+
     repo: "Repo"
 
 
 @runtime_checkable
 class Has_id_attribute(Protocol):
+    """Protocol for having :attr:`_id_attribute_` used in iteration and traversal."""
+
     _id_attribute_: str

From 012d710c18ee59bafa9565f9d9ca5609a88d21f8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 11:50:41 -0500
Subject: [PATCH 554/994] Move our PathLike below even TYPE_CHECKING imports

The benefits are that putting imports before newly introduced
names (other than names like __all__) is recommended by PEP-8, and
that git.types.PathLike is not equivalent to os.PathLike and
introducing it after all imports may help avoid obscuring this.
---
 git/types.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/types.py b/git/types.py
index 9159195cd..efcd2a7aa 100644
--- a/git/types.py
+++ b/git/types.py
@@ -38,13 +38,13 @@
 # else:
 #     from typing_extensions import TypeGuard  # noqa: F401
 
-PathLike = Union[str, "os.PathLike[str]"]
-"""A :class:`str` (Unicode) based file or directory path."""
-
 if TYPE_CHECKING:
     from git.repo import Repo
     from git.objects import Commit, Tree, TagObject, Blob
 
+PathLike = Union[str, "os.PathLike[str]"]
+"""A :class:`str` (Unicode) based file or directory path."""
+
 TBD = Any
 """Alias of :class:`~typing.Any`, when a type hint is meant to become more specific."""
 

From a06f1fc9834f8fc1feffd0249ef38c1cca302945 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 11:53:16 -0500
Subject: [PATCH 555/994] Remove commented-out is_config_level function

And the commented-out imports that had been solely to support it.
---
 git/types.py | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/git/types.py b/git/types.py
index efcd2a7aa..7e1cceaa8 100644
--- a/git/types.py
+++ b/git/types.py
@@ -33,11 +33,6 @@
         runtime_checkable,
     )
 
-# if sys.version_info >= (3, 10):
-#     from typing import TypeGuard  # noqa: F401
-# else:
-#     from typing_extensions import TypeGuard  # noqa: F401
-
 if TYPE_CHECKING:
     from git.repo import Repo
     from git.objects import Commit, Tree, TagObject, Blob
@@ -117,10 +112,6 @@
 ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]]
 """Static type of a tuple of the four strings representing configuration levels."""
 
-# def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]:
-#     # return inp in get_args(Lit_config_level)  # only py >= 3.8
-#     return inp in ("system", "user", "global", "repository")
-
 # Progress parameter type alias -----------------------------------------
 
 CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]

From c93e431e73b896515999b6dbb4cf61ac6931fd37 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 12:00:59 -0500
Subject: [PATCH 556/994] Expand git.compat docstring

To make clear that code outside GitPython would not typically
benefit from using anything in that module.

See #1854 for context.
---
 git/compat.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/git/compat.py b/git/compat.py
index 7753fe8b2..43b50b287 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -3,7 +3,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Utilities to help provide compatibility with Python 3."""
+"""Utilities to help provide compatibility with Python 3.
+
+This module exists for historical reasons. Code outside GitPython may make use of public
+members of this module, but is unlikely to benefit from doing so. GitPython continues to
+use some of these utilities, in some cases for compatibility across different platforms.
+"""
 
 import locale
 import os

From 29443ce94b3a687a5ab85edadafc231f8e5455ee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 14:57:01 -0500
Subject: [PATCH 557/994] Add a cationary note about Object vs. object

See also f78587f (#1725).
---
 git/objects/base.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/git/objects/base.py b/git/objects/base.py
index 0496b18cd..cb660b347 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -63,6 +63,9 @@ class Object(LazyMixin):
         rooted at this :class:`Object` class, even though submodules are not really a
         type of git object.
 
+    :note:
+        This :class:`Object` class should not be confused with :class:`object` (the root
+        of the class hierarchy in Python).
     """
 
     NULL_HEX_SHA = "0" * 40

From b6e3ad2f9cf00e066ac40ec472143ec99b2f90db Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 15:40:46 -0500
Subject: [PATCH 558/994] Don't bind unused _assertion_msg_format

The _assertion_msg_format module attribute (global variable) of
git.objects.base was formerly used in an assertion check that has
since been commented out but not completely removed.

It may be that both it and the commented-out code that uses it
should simply be removed (they will be in the git history, after
all), but this change just brings them in line by also commenting
out the variable.
---
 git/objects/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index cb660b347..c1f2b0e63 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -31,7 +31,7 @@
 # --------------------------------------------------------------------------
 
 
-_assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r"
+# _assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r"
 
 __all__ = ("Object", "IndexObject")
 

From 9f226fc47683038acd3304dd7d59be18984d3737 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 19:38:10 -0500
Subject: [PATCH 559/994] Remove unneeded annotation on __slots__ variable

The correct type is inferred, and no other __slots__ variable in
the codebase has an annotation.
---
 git/cmd.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index c7cec48d7..8550830aa 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -749,7 +749,7 @@ class CatFileContentStream:
         rest to ensure the underlying stream continues to work.
         """
 
-        __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size")
+        __slots__ = ("_stream", "_nbr", "_size")
 
         def __init__(self, size: int, stream: IO[bytes]) -> None:
             self._stream = stream

From e984bfebd9f1934b9810ac6cccb1bb803c64dd21 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 20:02:47 -0500
Subject: [PATCH 560/994] Fix some underindented portions of docstrings

A few docstrings had parts that were meant to be indented the
usual four spaces beyond the surrounding indentation, but were
indented only three spaces beyond it instead.
---
 git/cmd.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 8550830aa..3d5992dbd 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -850,10 +850,10 @@ def __init__(self, working_dir: Union[None, PathLike] = None):
         """Initialize this instance with:
 
         :param working_dir:
-           Git directory we should work in. If ``None``, we always work in the current
-           directory as returned by :func:`os.getcwd`.
-           This is meant to be the working tree directory if available, or the
-           ``.git`` directory in case of bare repositories.
+            Git directory we should work in. If ``None``, we always work in the current
+            directory as returned by :func:`os.getcwd`.
+            This is meant to be the working tree directory if available, or the
+            ``.git`` directory in case of bare repositories.
         """
         super().__init__()
         self._working_dir = expand_path(working_dir)
@@ -1103,8 +1103,8 @@ def execute(
         :raise git.exc.GitCommandError:
 
         :note:
-           If you add additional keyword arguments to the signature of this method,
-           you must update the ``execute_kwargs`` variable housed in this module.
+            If you add additional keyword arguments to the signature of this method, you
+            must update the ``execute_kwargs`` variable housed in this module.
         """
         # Remove password for the command if present.
         redacted_command = remove_password_if_present(command)
@@ -1438,7 +1438,7 @@ def _call_process(
 
         turns into::
 
-           git rev-list max-count 10 --header master
+            git rev-list max-count 10 --header master
 
         :return:
             Same as :meth:`execute`. If no args are given, used :meth:`execute`'s

From 5d7e55b8dd2abaacd5397860e6efd7bd4215fe12 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 20:11:40 -0500
Subject: [PATCH 561/994] Add return-type annotation on __init__ methods

Although sometimes found unintuitive, the reutrn type of a class's
__init__ method is best annotated as None, as in other functions
that return implicitly or by operand-less return statements.

Note that this is only a minor improvement, effectively just a
style fix, because mypy treats __init__ specially and, *when at
least one parameter is annotated*, its return type is implicitly
None rather than implicitly Any like other functions. All the
__init__ methods modified here did already have one or more
annotated parameters.

However, having __init__ methods without the return type makes it
easier to introduce a bug where an __init__ method with no
parameters besides self -- which itself should almost always be
unannotated and is properly inferred -- is written and the return
annotation needed to get mypy to regard it as statically typed,
so it checks it at all, is omitted. (It is also inelegant when
one considers the meaning of __init__ and the distinction between
it and __new__.)

This commit does not add any return type annotations to functions
in the test suite, since the test suite doesn't currently use
static typing.

Further reading:

- https://peps.python.org/pep-0484/#the-meaning-of-annotations
- https://github.com/python/mypy/issues/604
- https://github.com/gitpython-developers/GitPython/pull/1755#issuecomment-1837419098
---
 git/cmd.py                    | 2 +-
 git/objects/base.py           | 2 +-
 git/objects/submodule/root.py | 2 +-
 git/refs/head.py              | 2 +-
 git/refs/log.py               | 2 +-
 git/refs/symbolic.py          | 2 +-
 git/util.py                   | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 3d5992dbd..731d0fab8 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -846,7 +846,7 @@ def __del__(self) -> None:
                 self._stream.read(bytes_left + 1)
             # END handle incomplete read
 
-    def __init__(self, working_dir: Union[None, PathLike] = None):
+    def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
         """Initialize this instance with:
 
         :param working_dir:
diff --git a/git/objects/base.py b/git/objects/base.py
index e78420e8a..2b8dd0ff6 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -55,7 +55,7 @@ class Object(LazyMixin):
 
     type: Union[Lit_commit_ish, None] = None
 
-    def __init__(self, repo: "Repo", binsha: bytes):
+    def __init__(self, repo: "Repo", binsha: bytes) -> None:
         """Initialize an object by identifying it by its binary sha.
 
         All keyword arguments will be set on demand if ``None``.
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index ac0f7ad94..3268d73a4 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -56,7 +56,7 @@ class RootModule(Submodule):
 
     k_root_name = "__ROOT__"
 
-    def __init__(self, repo: "Repo"):
+    def __init__(self, repo: "Repo") -> None:
         # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
         super().__init__(
             repo,
diff --git a/git/refs/head.py b/git/refs/head.py
index d546cb4ef..f6020f461 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -44,7 +44,7 @@ class HEAD(SymbolicReference):
 
     __slots__ = ()
 
-    def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME):
+    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))
         super().__init__(repo, path)
diff --git a/git/refs/log.py b/git/refs/log.py
index 7aefeb4e6..f98f56f11 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -164,7 +164,7 @@ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog":
         inst = super().__new__(cls)
         return inst
 
-    def __init__(self, filepath: Union[PathLike, None] = None):
+    def __init__(self, filepath: Union[PathLike, None] = None) -> None:
         """Initialize this instance with an optional filepath, from which we will
         initialize our data. The path is also used to write changes back using the
         :meth:`write` method."""
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 142952e06..16aada0a7 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -74,7 +74,7 @@ class SymbolicReference:
     _remote_common_path_default = "refs/remotes"
     _id_attribute_ = "name"
 
-    def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False):
+    def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False) -> None:
         self.repo = repo
         self.path = path
 
diff --git a/git/util.py b/git/util.py
index 52f9dbdad..d272dc53c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -917,7 +917,7 @@ class Stats:
 
     __slots__ = ("total", "files")
 
-    def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]):
+    def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]) -> None:
         self.total = total
         self.files = files
 

From 4810491cb9d30d05e3ed73e55fe803fcf7ae0b53 Mon Sep 17 00:00:00 2001
From: jcole-crowdstrike <jcole-crowdstrike@>
Date: Mon, 4 Mar 2024 20:33:24 -0800
Subject: [PATCH 562/994] Fixing Windows encoding issue.

Stdout is being encoded as Windows-1251 instead of UTF-8 due to passing
universal_newlines as True to subprocess.
---
 git/remote.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 6e841949f..6cb33bede 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -891,7 +891,7 @@ def _get_fetch_info_from_stderr(
             None,
             progress_handler,
             finalizer=None,
-            decode_streams=False,
+            decode_streams=True,
             kill_after_timeout=kill_after_timeout,
         )
 
@@ -1068,7 +1068,7 @@ def fetch(
             Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
 
         proc = self.repo.git.fetch(
-            "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
+            "--", self, *args, as_process=True, with_stdout=False, v=verbose, **kwargs
         )
         res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
         if hasattr(self.repo.odb, "update_cache"):

From b5d91987a2ad160ab5ca7e5e5ca95bf332536549 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 6 Mar 2024 14:09:38 -0500
Subject: [PATCH 563/994] Remove commented-out code

---
 git/objects/base.py | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index c1f2b0e63..215557db5 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -30,9 +30,6 @@
 
 # --------------------------------------------------------------------------
 
-
-# _assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r"
-
 __all__ = ("Object", "IndexObject")
 
 
@@ -154,8 +151,7 @@ def _set_cache_(self, attr: str) -> None:
         """Retrieve object information."""
         if attr == "size":
             oinfo = self.repo.odb.info(self.binsha)
-            self.size = oinfo.size  # type:  int
-            # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type)
+            self.size = oinfo.size  # type: int
         else:
             super()._set_cache_(attr)
 

From 2212ac98912b66eb848cd60e7f5c43067dc0c1d7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 00:44:36 -0500
Subject: [PATCH 564/994] Fix Sphinx reference that rendered overly long

---
 git/objects/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 215557db5..df9b2d9f1 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -53,7 +53,7 @@ class Object(LazyMixin):
     * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
 
     :note:
-        See the :class:`git.types.Commit_ish` union type.
+        See the :class:`~git.types.Commit_ish` union type.
 
     :note:
         :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy

From 3c5ca52b2462964e1d478ec796dd7c5dd8bb2f68 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 00:44:59 -0500
Subject: [PATCH 565/994] Simplify _safer_popen_windows "if shell" logic

This fixes a static typing error reported by mypy.

Using a separate variable, as I had done originally, does not seem
to be clearer than rebinding the parameter, and in this case the
code is simpler and can be checked by mypy without needing another
explicit type annotation to be added. This fixes one mypy error.

This also adds a comment to make clearer why the mapping passed in
is copied when modifications are needed, rather than mutated.
---
 git/cmd.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index c7cec48d7..6354da666 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -257,10 +257,9 @@ def _safer_popen_windows(
     # When using a shell, the shell is the direct subprocess, so the variable must be
     # set in its environment, to affect its search behavior. (The "1" can be any value.)
     if shell:
-        safer_env = {} if env is None else dict(env)
-        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
-    else:
-        safer_env = env
+        # The original may be immutable or reused by the caller. Make changes in a copy.
+        env = {} if env is None else dict(env)
+        env["NoDefaultCurrentDirectoryInExePath"] = "1"
 
     # When not using a shell, the current process does the search in a CreateProcessW
     # API call, so the variable must be set in our environment. With a shell, this is
@@ -273,7 +272,7 @@ def _safer_popen_windows(
         return Popen(
             command,
             shell=shell,
-            env=safer_env,
+            env=env,
             creationflags=creationflags,
             **kwargs,
         )

From 43b7f8a3eb299c550674c6f95c9919019dc5a194 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 10:31:31 -0500
Subject: [PATCH 566/994] Annotate safer_popen broad enough for all platforms

This fixes another static typing error reported by mypy.

(The annotation could be made more specific in the future by making
a custom protocol for it, which may or may not be worthwhile, given
that `**kwargs: Any` would still have to be present after whatever
typed keyword arguments the protocol's `__call__` method listed,
since some callers intentionally forward arbitrary extra keyword
arguments through safer_popen to Popen.)
---
 git/cmd.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/git/cmd.py b/git/cmd.py
index 6354da666..c37d28988 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -278,6 +278,8 @@ def _safer_popen_windows(
         )
 
 
+safer_popen: Callable[..., Popen]
+
 if os.name == "nt":
     safer_popen = _safer_popen_windows
 else:

From dc95a768b3381cf5a68fc9ad4e8dc4bba37b0ca0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 11:34:11 -0500
Subject: [PATCH 567/994] Fix mypy error with creationflags in subprocess
 module

On Windows, Python's subprocess module contains constants useful to
pass to the `creationflags` parameter of subprocess.Popen. These
are absent on other platforms, where they are not meaningful.

The code was already safe at runtime from any AttributeError
related to these constants, because they were only used in
git.cmd._safer_popen_windows, which was defined on Windows.

But in #1792 I did not write the code in a way where mypy can
verify its correctness. So if a regression of the kind mypy can in
principle catch were to occur, it would be harder to notice it.

This refactors the code, keeping the same behavior but expressing
it in a way mypy can understand. This consists of two changes:

1. Only define _safer_popen_windows when the platform is Windows,
   placing it in the first branch of the `if` statement.

   This is needed because mypy will not take the only current call
   to that nonpublic function being on Windows as sufficient
   evidence that the platform is always Windows when it is run.

2. Determine the platform, for this purpose, using sys.platform
   instead of os.name.

   These are far from equivalent in general (see the deprecation
   rationale for is_<platform> in #1732, revised in a0fa2bd
   in #1787). However, in Python 3 (GitPython no longer supports
   Python 2), in the specific case of Windows, we have a choice of
   which to use, as both `sys.platform == "win32"` and
   `os.name == "nt"`.

   os.name is "nt" on native Windows, and "posix" on Cygwin.
   sys.platform is "win32" on native Windows (including 64-bit
   systems with 64-bit Python builds), and "cygwin" on Cygwin. See:
   https://docs.python.org/3/library/sys.html#sys.platform

   This is needed because the type stubs for the subprocess module
   use this sys.platform check (rather than an os.name check) to
   determine if the platform is Windows for the purpose of deciding
   which constants to say the subprocess module defines.

I have verified that neither of these changes is enough by itself.
---
 git/cmd.py | 113 +++++++++++++++++++++++++++--------------------------
 1 file changed, 57 insertions(+), 56 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index c37d28988..04037c06f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -14,6 +14,7 @@
 import signal
 from subprocess import Popen, PIPE, DEVNULL
 import subprocess
+import sys
 import threading
 from textwrap import dedent
 
@@ -220,67 +221,67 @@ def pump_stream(
         finalizer(process)
 
 
-def _safer_popen_windows(
-    command: Union[str, Sequence[Any]],
-    *,
-    shell: bool = False,
-    env: Optional[Mapping[str, str]] = None,
-    **kwargs: Any,
-) -> Popen:
-    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
-
-    This avoids an untrusted search path condition where a file like ``git.exe`` in a
-    malicious repository would be run when GitPython operates on the repository. The
-    process using GitPython may have an untrusted repository's working tree as its
-    current working directory. Some operations may temporarily change to that directory
-    before running a subprocess. In addition, while by default GitPython does not run
-    external commands with a shell, it can be made to do so, in which case the CWD of
-    the subprocess, which GitPython usually sets to a repository working tree, can
-    itself be searched automatically by the shell. This wrapper covers all those cases.
+safer_popen: Callable[..., Popen]
 
-    :note:
-        This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath`
-        environment variable during subprocess creation. It also takes care of passing
-        Windows-specific process creation flags, but that is unrelated to path search.
+if sys.platform == "win32":
 
-    :note:
-        The current implementation contains a race condition on :attr:`os.environ`.
-        GitPython isn't thread-safe, but a program using it on one thread should ideally
-        be able to mutate :attr:`os.environ` on another, without unpredictable results.
-        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
-    """
-    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
-    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
-    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
-    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
-
-    # When using a shell, the shell is the direct subprocess, so the variable must be
-    # set in its environment, to affect its search behavior. (The "1" can be any value.)
-    if shell:
-        # The original may be immutable or reused by the caller. Make changes in a copy.
-        env = {} if env is None else dict(env)
-        env["NoDefaultCurrentDirectoryInExePath"] = "1"
-
-    # When not using a shell, the current process does the search in a CreateProcessW
-    # API call, so the variable must be set in our environment. With a shell, this is
-    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
-    # patched. If that is unpatched, then in the rare case the ComSpec environment
-    # variable is unset, the search for the shell itself is unsafe. Setting
-    # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and
-    # protects against that. (As above, the "1" can be any value.)
-    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
-        return Popen(
-            command,
-            shell=shell,
-            env=env,
-            creationflags=creationflags,
-            **kwargs,
-        )
+    def _safer_popen_windows(
+        command: Union[str, Sequence[Any]],
+        *,
+        shell: bool = False,
+        env: Optional[Mapping[str, str]] = None,
+        **kwargs: Any,
+    ) -> Popen:
+        """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
+
+        This avoids an untrusted search path condition where a file like ``git.exe`` in a
+        malicious repository would be run when GitPython operates on the repository. The
+        process using GitPython may have an untrusted repository's working tree as its
+        current working directory. Some operations may temporarily change to that directory
+        before running a subprocess. In addition, while by default GitPython does not run
+        external commands with a shell, it can be made to do so, in which case the CWD of
+        the subprocess, which GitPython usually sets to a repository working tree, can
+        itself be searched automatically by the shell. This wrapper covers all those cases.
 
+        :note:
+            This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath`
+            environment variable during subprocess creation. It also takes care of passing
+            Windows-specific process creation flags, but that is unrelated to path search.
 
-safer_popen: Callable[..., Popen]
+        :note:
+            The current implementation contains a race condition on :attr:`os.environ`.
+            GitPython isn't thread-safe, but a program using it on one thread should ideally
+            be able to mutate :attr:`os.environ` on another, without unpredictable results.
+            See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
+        """
+        # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
+        # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+        # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
+        creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+
+        # When using a shell, the shell is the direct subprocess, so the variable must be
+        # set in its environment, to affect its search behavior. (The "1" can be any value.)
+        if shell:
+            # The original may be immutable or reused by the caller. Make changes in a copy.
+            env = {} if env is None else dict(env)
+            env["NoDefaultCurrentDirectoryInExePath"] = "1"
+
+        # When not using a shell, the current process does the search in a CreateProcessW
+        # API call, so the variable must be set in our environment. With a shell, this is
+        # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
+        # patched. If that is unpatched, then in the rare case the ComSpec environment
+        # variable is unset, the search for the shell itself is unsafe. Setting
+        # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and
+        # protects against that. (As above, the "1" can be any value.)
+        with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
+            return Popen(
+                command,
+                shell=shell,
+                env=env,
+                creationflags=creationflags,
+                **kwargs,
+            )
 
-if os.name == "nt":
     safer_popen = _safer_popen_windows
 else:
     safer_popen = Popen

From 4191f7d598206ee829376e1a2a4e95ea571de500 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 14:29:48 -0500
Subject: [PATCH 568/994] Refactor kill_after_timeout logic so mypy can check
 it

This changes how Git.execute defines the kill_process callback and
how it performs checks, fixing two mypy errors on Windows about how
the signal module doesn't have SIGKILL. In doing so, it also
eliminates the need for the assertion added for safety and clarity
in 2f017ac (#1761), since now kill_process is only defined if it is
to be used (which is also guarded by a platform check, needed by
mypy).

As in dc95a76 before this, part of the change here is to replace
some os.named-based checks with sys.platform-based checks, which is
safe because, when one is specifically checking only for the
distinction between native Windows and all other systems, one can
use either approach. (See dc95a76 for more details on that.)
---
 git/cmd.py | 67 ++++++++++++++++++++++++++++--------------------------
 1 file changed, 35 insertions(+), 32 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 04037c06f..0adc8cf35 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1134,7 +1134,7 @@ def execute(
         if inline_env is not None:
             env.update(inline_env)
 
-        if os.name == "nt":
+        if sys.platform == "win32":
             cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
                 raise GitCommandError(
@@ -1179,35 +1179,38 @@ def execute(
         if as_process:
             return self.AutoInterrupt(proc, command)
 
-        def kill_process(pid: int) -> None:
-            """Callback to kill a process."""
-            if os.name == "nt":
-                raise AssertionError("Bug: This callback would be ineffective and unsafe on Windows, stopping.")
-            p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
-            child_pids = []
-            if p.stdout is not None:
-                for line in p.stdout:
-                    if len(line.split()) > 0:
-                        local_pid = (line.split())[0]
-                        if local_pid.isdigit():
-                            child_pids.append(int(local_pid))
-            try:
-                os.kill(pid, signal.SIGKILL)
-                for child_pid in child_pids:
-                    try:
-                        os.kill(child_pid, signal.SIGKILL)
-                    except OSError:
-                        pass
-                kill_check.set()  # Tell the main routine that the process was killed.
-            except OSError:
-                # It is possible that the process gets completed in the duration after
-                # timeout happens and before we try to kill the process.
-                pass
-            return
-
-        # END kill_process
-
-        if kill_after_timeout is not None:
+        if sys.platform != "win32" and kill_after_timeout is not None:
+
+            def kill_process(pid: int) -> None:
+                """Callback to kill a process.
+
+                This callback implementation would be ineffective and unsafe on Windows.
+                """
+                p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
+                child_pids = []
+                if p.stdout is not None:
+                    for line in p.stdout:
+                        if len(line.split()) > 0:
+                            local_pid = (line.split())[0]
+                            if local_pid.isdigit():
+                                child_pids.append(int(local_pid))
+                try:
+                    os.kill(pid, signal.SIGKILL)
+                    for child_pid in child_pids:
+                        try:
+                            os.kill(child_pid, signal.SIGKILL)
+                        except OSError:
+                            pass
+                    # Tell the main routine that the process was killed.
+                    kill_check.set()
+                except OSError:
+                    # It is possible that the process gets completed in the duration
+                    # after timeout happens and before we try to kill the process.
+                    pass
+                return
+
+            # END kill_process
+
             kill_check = threading.Event()
             watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
 
@@ -1218,10 +1221,10 @@ def kill_process(pid: int) -> None:
         newline = "\n" if universal_newlines else b"\n"
         try:
             if output_stream is None:
-                if kill_after_timeout is not None:
+                if sys.platform != "win32" and kill_after_timeout is not None:
                     watchdog.start()
                 stdout_value, stderr_value = proc.communicate()
-                if kill_after_timeout is not None:
+                if sys.platform != "win32" and kill_after_timeout is not None:
                     watchdog.cancel()
                     if kill_check.is_set():
                         stderr_value = 'Timeout: the command "%s" did not complete in %d ' "secs." % (

From 1ef336514a3513c7723a85ea295202560e7f40f0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 15:36:28 -0500
Subject: [PATCH 569/994] Factor communicate and watchdog logic to helper

The goal is to improve readability by not repeating the platform
and kill_after_timeout check three times.
---
 git/cmd.py | 32 ++++++++++++++++++--------------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 0adc8cf35..2ce690879 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1135,12 +1135,12 @@ def execute(
             env.update(inline_env)
 
         if sys.platform == "win32":
-            cmd_not_found_exception = OSError
             if kill_after_timeout is not None:
                 raise GitCommandError(
                     redacted_command,
                     '"kill_after_timeout" feature is not supported on Windows.',
                 )
+            cmd_not_found_exception = OSError
         else:
             cmd_not_found_exception = FileNotFoundError
         # END handle
@@ -1209,10 +1209,25 @@ def kill_process(pid: int) -> None:
                     pass
                 return
 
-            # END kill_process
+            def communicate() -> Tuple[AnyStr, AnyStr]:
+                watchdog.start()
+                out, err = proc.communicate()
+                watchdog.cancel()
+                if kill_check.is_set():
+                    err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
+                        " ".join(redacted_command),
+                        kill_after_timeout,
+                    )
+                    if not universal_newlines:
+                        err = err.encode(defenc)
+                return out, err
+
+            # END helpers
 
             kill_check = threading.Event()
             watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
+        else:
+            communicate = proc.communicate
 
         # Wait for the process to return.
         status = 0
@@ -1221,18 +1236,7 @@ def kill_process(pid: int) -> None:
         newline = "\n" if universal_newlines else b"\n"
         try:
             if output_stream is None:
-                if sys.platform != "win32" and kill_after_timeout is not None:
-                    watchdog.start()
-                stdout_value, stderr_value = proc.communicate()
-                if sys.platform != "win32" and kill_after_timeout is not None:
-                    watchdog.cancel()
-                    if kill_check.is_set():
-                        stderr_value = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
-                            " ".join(redacted_command),
-                            kill_after_timeout,
-                        )
-                        if not universal_newlines:
-                            stderr_value = stderr_value.encode(defenc)
+                stdout_value, stderr_value = communicate()
                 # Strip trailing "\n".
                 if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore
                     stdout_value = stdout_value[:-1]

From 4083dd896ad53f84e96f447b58cdf9acc1b02db3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 16:14:39 -0500
Subject: [PATCH 570/994] Fix new mypy confusion about kill_after_timeout type

The refactoring in 1ef3365 before this added a new mypy error on
non-Windows platforms, where mypy failed to infer that the type of
kill_after_timeout was `float` (rather than `float | None`) at the
point in the code where it was used as a captured variable in the
newly introduced communicate() helper.

This was even though the variable is never rebound there or in the
enclosing scope that introduced it. So introducing and using a new
variable that holds the same reference, which is sufficient to fix
the problem and is the approach taken here, is not a behavioral
change.
---
 git/cmd.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 2ce690879..9fd2c532d 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1180,6 +1180,8 @@ def execute(
             return self.AutoInterrupt(proc, command)
 
         if sys.platform != "win32" and kill_after_timeout is not None:
+            # Help mypy figure out this is not None even when used inside communicate().
+            timeout = kill_after_timeout
 
             def kill_process(pid: int) -> None:
                 """Callback to kill a process.
@@ -1216,7 +1218,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
                 if kill_check.is_set():
                     err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
                         " ".join(redacted_command),
-                        kill_after_timeout,
+                        timeout,
                     )
                     if not universal_newlines:
                         err = err.encode(defenc)
@@ -1225,7 +1227,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
             # END helpers
 
             kill_check = threading.Event()
-            watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,))
+            watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
         else:
             communicate = proc.communicate
 

From 3aeef466dea47d088fc7356b87589ee550c2030a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 17:24:56 -0500
Subject: [PATCH 571/994] Fix how Diffable annotates expected repo attribute

The Diffable class's essential method, diff, requires self to have
a repo attribute, which is outside Diffable's own responsibility to
provide. This was already documented in the Diffable docstring, but
the technique used to prevent mypy from treating accesses to that
attribute as errors had several disadvantages:

- Annotating `self.repo` is ambiguous, since typically *names* are
  annotated. Although mypy treated this afterwards, even in code
  appearing outside the implementation of Diffable.diff, to mean
  that Diffable instances have repo attributes, it is not clear how
  other type checkers, or possibly future version of mypy, would or
  should interpret this. It is also not obvious from reading the
  code that it should have an effect on static analysis of code
  outside the diff method body, but this can be verified by
  temporarily placing this code (unindented) after, and outside,
  the Diffable class:

      from git.types import Has_Repo

      def f(x: Has_Repo) -> None:
          pass

      f(Diffable())

  This produces no new type errors from mypy, but if the annotated
  conditional attribute rebinding statement is commented out, then
  one of the type errors is that `f(Diffable())` passes an argument
  of the wrong type to `f`.

- The annotation was on an attribute binding operation (assigning
  to `self.repo`), which mypy accurately reported as an error
  because the Diffable class had no `repo` instance attribute
  indicated in the code elsewhere and is a slotted class whose
  instances have no instance dictionaries to allow that actual
  binding operation to succeed. In addition to being an incorrect
  annotation, this had the effect of decreasing the number of
  related mypy errors only to one, instead of the hoped-for zero.

- The rebinding of self.repo to itself was written in way that, if
  the statement were executed, rebinding would actually occur when
  the instance has a repo attribute. (See below for why this does
  not actually happen.) But this depends on the presence of a repo
  attribute to read from and then write to, so any change that
  allows mypy to infer that it is okay should also be enough to
  avoid the original mypy errors at issue in the first place.

- If the statement were executed, it would fail at runtime if the
  instance had a repo attribute that were read-only. But there is
  no reason a subclass or sibling class used to provide the repo
  attribute should be unable to do this, such as by defining repo
  as a property with only a getter.

- The condition that guarded the self.repo (re)binding operation
  was unintentionally incorrect in a way that caused it to be
  entirely unrelated to the git.typing.Has_Repo protocol it tried
  to refer to. The check was `hasattr(self, "Has_Repo")`, but the
  goal is not to check for an attribute named `Has_Repo`, but to
  check for an attribute named `repo`.

  With `Has_Repo` imported, the different condition
  `isinstance(self, Has_Repo)` would do that (the protocol is
  runtime-checkable). Or the condition `hasattr(self, "repo")`
  could be used. Instead, it worked the same as under the change:

      - if hasattr(self, "Has_Repo"):
      + if hasattr(self, "Any_Other_Nonexistent_Attribute"):

  The way it avoided a runtime error (and provably so, to mypy) was
  that it was always False, and mypy simply treated the annotation
  as usable information on both branches.

This commit removes that code and instead adds a class-level
annotation for a `repo` instance attribute, with no assignments.
That eliminates the mypy error for the attempt to conditionally
annotate `self.repo`, while also keeping the self.repo accesses
from becoming static type errors again.

As before, mypy treats Diffable as a static subclass of Has_Repo,
but if an instance `x` of Diffable lacked a `repo` attribute (which
would lead to a runtime error, as before, in realistic use, when
`diff` were called on it), then `isinstance(x, Has_Repo)` will
evaluate to `False`. Because the repo member of Has_Repo is not
written as a method, it is a runtime error to use issubclass to
check if a class is a subclass of it (even though isinstance does
work), so adding this annotation to the Diffable class does not
affect that either.

Some behavior of Has_Repo may not be as expected, because of the
different semantics of (a) the static type system checked by mypy
and other static type checkers and (b) the actual dynamic type
system of Python enforced at runtime by the implementation.
Furthermore, this protocol is not used anywhere in GitPython, and
since this commit, there is no attempt to use or reference it.
Because it is public in the public git.types module, it should not
be immediately removed, but it may make sense to deprecate it.
However, this commit does not make that change.
---
 git/diff.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 966b73154..16e6be151 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -84,13 +84,16 @@ class Diffable:
     compatible type.
 
     :note:
-        Subclasses require a repo member as it is the case for
-        :class:`~git.objects.base.Object` instances, for practical reasons we do not
+        Subclasses require a repo member, as it is the case for
+        :class:`~git.objects.base.Object` instances. For practical reasons we do not
         derive from :class:`~git.objects.base.Object`.
     """
 
     __slots__ = ()
 
+    repo: "Repo"
+    """Repository to operate on. Must be provided by subclass or sibling class."""
+
     class Index:
         """Stand-in indicating you want to diff against the index."""
 
@@ -169,9 +172,6 @@ def diff(
         if paths is not None and not isinstance(paths, (tuple, list)):
             paths = [paths]
 
-        if hasattr(self, "Has_Repo"):
-            self.repo: "Repo" = self.repo
-
         diff_cmd = self.repo.git.diff
         if other is self.Index:
             args.insert(0, "--cached")

From f1cc1fe7c9497c97716af4c3071effaa37b89e79 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 20:17:55 -0500
Subject: [PATCH 572/994] Fix how HEAD annotates inherited commit property

HEAD inherits its commit attribute from SymbolicReference, which
defines it as a property using an explicit function call to
`property`, rather than using it as a decorator. Due at least to
https://github.com/python/mypy/issues/16827, mypy has trouble with
this. Currently that assignment in SymbolicReference is marked as
type[ignore], which suppresses a type error (or something mypy
regards as one) in the call itself. Even without that type comment,
the type of the SymboilicReference instance attribute produced by
the property is inferred as Any. This commit does not change that.

This commit replaces the HEAD class's partially successful attempt
to annotate `commit` as `self.commit` in `__init__` with a fully
recognized annotation for `commit` in the class scope (which is
interpreted as an instance attribute).

Merely removing the annotation in `__init__` is sufficient to make
the mypy error for it go away, but this causes the inferred type of
`x.commit` when `x` is a `HEAD` instance to be inferred as Any
rather than the desired specific type Commit. That is why this also
adds an annotation to the class to achieve that without causing its
own mypy error as the old one did.

Once the SymbolicReference.commit property has a more specific
static type, however that is to be achieved, there will be no need
for the annotation added in the HEAD class body here. (This differs
from the situation in 3aeef46 before this, where Diffable does not
inherit any `repo` attribute--and is intended not to--and therefore
had to introduce an annotation for it, for mypy to accept it.)
---
 git/refs/head.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index d546cb4ef..4da614abd 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -44,11 +44,13 @@ 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):
         if path != self._HEAD_NAME:
             raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path))
         super().__init__(repo, path)
-        self.commit: "Commit"
 
     def orig_head(self) -> SymbolicReference:
         """

From e133018954cc38c09206de0236a281385f98e072 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 21:14:09 -0500
Subject: [PATCH 573/994] Broaden cygpath parameter annotation

To fix a mypy error in Repo.__init__ where the epath variable,
which can sometimes be a os.PathLike[str], does not match the str
annotation for cygpath's path parameter, even though cygwin
immediately calls str on that parameter.

Pulling most of cygpath out into a new helper function is to help
mypy correctly infer that the type of path is still compatible with
the return type of str, when it is used in the return statement.

I'm not sure this change is really the best solution at this time,
because while it fixes the mypy error (without creating a new one),
and cygpath is not listed in __all__, the docstring of advises to
call Git.polish_url instead of cygpath. That method is public,
annotates its parameter as str, and should not have its annotation
broadened without considernig if it makes sense to do so or if its
docstring needs to be updated.

So either the cygpath docstring should be updated or this change
should be undone and a different approach used to fix the type
error in Repo.__init__.
---
 git/util.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/git/util.py b/git/util.py
index 52f9dbdad..c07338582 100644
--- a/git/util.py
+++ b/git/util.py
@@ -404,9 +404,7 @@ def _cygexpath(drive: Optional[str], path: str) -> str:
 )
 
 
-def cygpath(path: str) -> str:
-    """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
-    path = str(path)  # Ensure is str and not AnyPath.
+def _cygpath(path: str) -> str:
     # Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs?
     if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")):
         for regex, parser, recurse in _cygpath_parsers:
@@ -422,6 +420,11 @@ def cygpath(path: str) -> str:
     return path
 
 
+def cygpath(path: PathLike) -> str:
+    """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
+    return _cygpath(str(path))  # Ensure is str and not AnyPath.
+
+
 _decygpath_regex = re.compile(r"(?:/proc)?/cygdrive/(\w)(/.*)?")
 
 

From c34a466bb3c6b74ee2335bf8cb6511b9b17bdce5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 21:41:13 -0500
Subject: [PATCH 574/994] Have Repo.__init__ convert epath to str first instead

In e133018 before this, I had noticed that Repo.__init__ implicitly
relied on the implementation detail of cygpath that it converts its
argument to str immediately. That detail was not reflected in the
parameter type annotation, which could've been broader, so I had
fixed the mypy error in Repo.__init__ by broadening cygpath's
parameter type annotation.

This undoes that change, putting the cygpath annotations back as
before, and fixes the mypy error in another way, by having
Repo.__init__ pass `str(epath)` instead of `epath` to cygpath.
The reason is the concern noted in the e133018 commit message,
that broadening the annotation made the way cygpath documents its
relationshp to Git.polish_url no longer correct, and that it is not
clear that the str(path) step in cygpath really ought to be made
more than an implementation detail (which if done may require some
documentation to be changed).

Instances of str are usually direct instances (rather than of a
subclass of str), in which case an extra call to str will (at least
in CPython, and probably in all implementations) just return the
same str object that was passed in. The performance penalty of this
extra call to str should therefore be extremely small.
---
 git/repo/base.py | 2 +-
 git/util.py      | 9 +++------
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index a54591746..39a80b449 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -218,7 +218,7 @@ def __init__(
             # Given how the tests are written, this seems more likely to catch Cygwin
             # git used from Windows than Windows git used from Cygwin. Therefore
             # changing to Cygwin-style paths is the relevant operation.
-            epath = cygpath(epath)
+            epath = cygpath(str(epath))
 
         epath = epath or path or os.getcwd()
         if not isinstance(epath, str):
diff --git a/git/util.py b/git/util.py
index c07338582..52f9dbdad 100644
--- a/git/util.py
+++ b/git/util.py
@@ -404,7 +404,9 @@ def _cygexpath(drive: Optional[str], path: str) -> str:
 )
 
 
-def _cygpath(path: str) -> str:
+def cygpath(path: str) -> str:
+    """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
+    path = str(path)  # Ensure is str and not AnyPath.
     # Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs?
     if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")):
         for regex, parser, recurse in _cygpath_parsers:
@@ -420,11 +422,6 @@ def _cygpath(path: str) -> str:
     return path
 
 
-def cygpath(path: PathLike) -> str:
-    """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
-    return _cygpath(str(path))  # Ensure is str and not AnyPath.
-
-
 _decygpath_regex = re.compile(r"(?:/proc)?/cygdrive/(\w)(/.*)?")
 
 

From 4dfd48098c9c2ecf60ff29102c7c211311f8e93a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 7 Mar 2024 22:04:12 -0500
Subject: [PATCH 575/994] Fix how Remote annotates dynamic config-backed url
 attribute

This fixes a mypy error by replacing an annotation on `self.url`,
which is a mypy error, by adding an annotation at the class level
for a `url` instance attribute.

This adds a corresponding brief docstring for it as well, which may
slightly improve usability, but the main impact is one less mypy
error.
---
 git/remote.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/git/remote.py b/git/remote.py
index 2c671582b..2bd674199 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -559,6 +559,9 @@ class Remote(LazyMixin, IterableObj):
         "--exec",
     ]
 
+    url: str  # Obtained dynamically from _config_reader. See __getattr__ below.
+    """The URL configured for the remote."""
+
     def __init__(self, repo: "Repo", name: str) -> None:
         """Initialize a remote instance.
 
@@ -570,7 +573,6 @@ def __init__(self, repo: "Repo", name: str) -> None:
         """
         self.repo = repo
         self.name = name
-        self.url: str
 
     def __getattr__(self, attr: str) -> Any:
         """Allows to call this instance like ``remote.special(*args, **kwargs)`` to

From e4fd2e343b4e7ece9df1269da9802b965ede4c51 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 09:23:37 -0500
Subject: [PATCH 576/994] Drop wrong variable annotations in
 BlobFilter.__call__

This fixes two mypy errors by removing the annotations of the
filter_parts and blob_parts local variables as lists. They only
need to be sequences (more precisely, they only need to be sized
and iterable, they don't even need to support subscripting in the
way __call__ uses them), and mypy is able to infer their type.
---
 git/index/typ.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/index/typ.py b/git/index/typ.py
index a7d2ad47a..c247fab99 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -12,7 +12,7 @@
 
 # typing ----------------------------------------------------------------------
 
-from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast, List
+from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
 
 from git.types import PathLike
 
@@ -60,8 +60,8 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool:
             path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike)
             # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no
             # longer supported.
-            filter_parts: List[str] = path.parts
-            blob_parts: List[str] = blob_path.parts
+            filter_parts = path.parts
+            blob_parts = blob_path.parts
             if len(filter_parts) > len(blob_parts):
                 continue
             if all(i == j for i, j in zip(filter_parts, blob_parts)):

From 94344b49b6dc38067cbb9aa2063043ac8e0a37eb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 11:05:28 -0500
Subject: [PATCH 577/994] Clarify CallableProgress vs. CallableRemoteProgress

This corrects an overstatement in the git.types.CallableProgress
docstring, which was introduced recently in 9e47083, and in which
I had erroneously claimed that it was the most general type of
object passed as a progress reporter for cloning.

There are some non-None non-callable types that can also be
passed and that are not encompassed by git.types.CallableProgress,
somewhat confusingly including CallableRemoteProgress (which like
RemoteProgress is not callable; rather, it wraps a callable and
forwards progress information to it).

In addition, None can be passed, and while
git.types.CallableProgress does encompass it, it is not callable.
(This is minor by comparison and I just added a brief note for it.)

This also further expands the git.types.CallableProgress docstring,
as well as the git.util.CallableRemoteProgress docstring, to
clarify the distinction between them, as well as what "Callable"
really signifies for CallableRemoteProgress (that it wraps and
forwards to a callable, rather than itself being callable).
---
 git/types.py | 16 +++++++++++++++-
 git/util.py  |  9 ++++++++-
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/git/types.py b/git/types.py
index 7e1cceaa8..8f71d079a 100644
--- a/git/types.py
+++ b/git/types.py
@@ -115,11 +115,25 @@
 # Progress parameter type alias -----------------------------------------
 
 CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]]
-"""General type of a progress reporter for cloning.
+"""General type of a function or other callable used as a progress reporter for cloning.
 
 This is the type of a function or other callable that reports the progress of a clone,
 when passed as a ``progress`` argument to :meth:`Repo.clone <git.repo.base.Repo.clone>`
 or :meth:`Repo.clone_from <git.repo.base.Repo.clone_from>`.
+
+:note:
+    Those :meth:`~git.repo.base.Repo.clone` and :meth:`~git.repo.base.Repo.clone_from`
+    methods also accept :meth:`~git.util.RemoteProgress` instances, including instances
+    of its :meth:`~git.util.CallableRemoteProgress` subclass.
+
+:note:
+    Unlike objects that match this type, :meth:`~git.util.RemoteProgress` instances are
+    not directly callable, not even when they are instances of
+    :meth:`~git.util.CallableRemoteProgress`, which wraps a callable and forwards
+    information to it but is not itself callable.
+
+:note:
+    This type also allows ``None``, for cloning without reporting progress.
 """
 
 # -----------------------------------------------------------------------------------
diff --git a/git/util.py b/git/util.py
index 52f9dbdad..880d6625a 100644
--- a/git/util.py
+++ b/git/util.py
@@ -749,7 +749,14 @@ def update(
 
 
 class CallableRemoteProgress(RemoteProgress):
-    """An implementation forwarding updates to any callable."""
+    """A :class:`RemoteProgress` implementation forwarding updates to any callable.
+
+    :note:
+        Like direct instances of :class:`RemoteProgress`, instances of this
+        :class:`CallableRemoteProgress` class are not themselves directly callable.
+        Rather, instances of this class wrap a callable and forward to it. This should
+        therefore not be confused with :class:`git.types.CallableProgress`.
+    """
 
     __slots__ = ("_callable",)
 

From 8e8b87a573dd7ca27d3556788c0da20faa803cfd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 14:06:37 -0500
Subject: [PATCH 578/994] Fix RootModule.update `ignore[override]` suppression

It was on the `previous_commit` parameter, and ineffective. It may
be that the original intent of the suppression was to suppress only
incompatible override type errors due to the addition of that
parameter, but there are other paramters that also mismatch by
having a different name or by being absent even though present in
the overridden base-class method. Furthermore, even coresponding
parameters mismatch in position due to the insertion of the
`previous_commit` parameter, so even if there is some way to do a
more fine-grained suppression than applying `ignore[override]` to
the entire method signature, that would be insufficient here.

This fixes one mypy error. It does so by causing it to be suppressed
effectively, not by fixing the underlying issue, which may not be
fixable since it would entail a breaking change to fix. However,
this does not introduce any new suppressions, just fixes an existing
suppression so it is effective (and probably so it operates as
intended, though maybe it is slightly stronger than intended as
discussed above).
---
 git/objects/submodule/root.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index ac0f7ad94..d0d1818a9 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -75,9 +75,9 @@ def _clear_cache(self) -> None:
 
     # { Interface
 
-    def update(
+    def update(  # type: ignore[override]
         self,
-        previous_commit: Union[Commit_ish, None] = None,  # type: ignore[override]
+        previous_commit: Union[Commit_ish, None] = None,
         recursive: bool = True,
         force_remove: bool = False,
         init: bool = True,

From 938f2725eb137492a459b4210ae58565e26e4fd5 Mon Sep 17 00:00:00 2001
From: jcole-crowdstrike <jcole-crowdstrike@>
Date: Tue, 5 Mar 2024 07:37:36 -0800
Subject: [PATCH 579/994] Setting universal_newlines=False explicitly in
 fetch() and pull().

This fix avoids encoding problems on Windows, as was done for fetch() in the previous commit. However, here it is being made more explicit and adds the same fix for the pull function.
---
 git/remote.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 6cb33bede..bbe1aea97 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -1068,7 +1068,7 @@ def fetch(
             Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
 
         proc = self.repo.git.fetch(
-            "--", self, *args, as_process=True, with_stdout=False, v=verbose, **kwargs
+            "--", self, *args, as_process=True, with_stdout=False, universal_newlines=False, v=verbose, **kwargs
         )
         res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
         if hasattr(self.repo.odb, "update_cache"):
@@ -1122,7 +1122,7 @@ def pull(
             Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
 
         proc = self.repo.git.pull(
-            "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
+            "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=False, v=True, **kwargs
         )
         res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
         if hasattr(self.repo.odb, "update_cache"):

From 8b8c76a707155e32e7538136053ff00c3231cc92 Mon Sep 17 00:00:00 2001
From: jcole-crowdstrike <jcole-crowdstrike@>
Date: Fri, 8 Mar 2024 12:45:33 -0800
Subject: [PATCH 580/994] Fixing stripping issue causing passing tests to be
 interpreted as failures

Removing this block of code that strips away nonprintable ASCII characters, as it is causing some tests to fail inappropriately.

Successful tests are identified by the suffix ", done", which is being removed from some messages due to this aforementioned block of code. The codeblock probably intends to just strip away traililng non-ASCII characters from strings, but does not break, and so will instead continue to iterate through the string and then strip away both printable/nonprintable ASCII; this is causing the loss of the ", done" because anytime a nonprintable character is anywhere but the end of the string then the entire section of string after the character is truncated.

Looking back through commits, this codeblock was added in 882ebb153e14488b275e374ccebcdda1dea22dd7 as a workaround after the addition of universal_newlines. Seeing as we have removed universal_newlines in the previous commit, we can also remove this codeblock.

In future, if something likw this is needed, then maybe a join that concatenates all printable ASCII characters would serve better (e.g., "".join(b for b in line if b >= 32)), instead of trying to cut out the nonprintable chars.
---
 git/util.py | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/git/util.py b/git/util.py
index 52f9dbdad..63c9b1e99 100644
--- a/git/util.py
+++ b/git/util.py
@@ -611,20 +611,6 @@ def _parse_progress_line(self, line: AnyStr) -> None:
             self.error_lines.append(self._cur_line)
             return
 
-        # Find escape characters and cut them away - regex will not work with
-        # them as they are non-ASCII. As git might expect a tty, it will send them.
-        last_valid_index = None
-        for i, c in enumerate(reversed(line_str)):
-            if ord(c) < 32:
-                # its a slice index
-                last_valid_index = -i - 1
-            # END character was non-ASCII
-        # END for each character in line
-        if last_valid_index is not None:
-            line_str = line_str[:last_valid_index]
-        # END cut away invalid part
-        line_str = line_str.rstrip()
-
         cur_count, max_count = None, None
         match = self.re_op_relative.match(line_str)
         if match is None:

From 1cdec7afcd8e4d69dbb9f8e596282c6ddb6414be Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 17:12:28 -0500
Subject: [PATCH 581/994] Fix wrong class name in git.objects.tag docstring

In 6126997 (#1850), I had meant to have the git.objects.tag module
docstring say that the module defined the TagObject class, to help
ensure no one would confuse this with the TagReference class. But I
instead had it wrongly say it defined the TagReference class! This
fixes that.
---
 git/objects/tag.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/tag.py b/git/objects/tag.py
index 49fb97e37..fd3cdd630 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -5,7 +5,7 @@
 
 """Provides an :class:`~git.objects.base.Object`-based type for annotated tags.
 
-This defines the :class:`TagReference` class, which represents annotated tags.
+This defines the :class:`TagObject` class, which represents annotated tags.
 For lightweight tags, see the :mod:`git.refs.tag` module.
 """
 

From ed6ead969914597a56aca752aadd044a276f1137 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 17:37:00 -0500
Subject: [PATCH 582/994] Correct and clarify Diffable.diff docstring

In cd16a35 (#1725), I had taken "Treeish" to mean the type of that
exact name, git.index.base.Treeish. But that type is only used
within the git.index package (actually only in git.index.base
itself). It is also nonpublic: git.index.base.__all__ exists and
does not list it.

So most likely this was not intended in the git.diff.Diffable.diff
docstring. Even if intended, it does not appear accurate, since the
git.index.base.Treeish union includes bytes, and the logic in
Diffable.diff and its helpers does not appear to accommodate bytes.

A closer type is the public git.types.Tree_ish union, which is
narrower than git.index.base.Treeish, including neither str nor
bytes. However, it does not include str, and Diffable.diff does
accept str to specify a tree-ish for diff-ing. It may be that
"Treeish" in the pre-#1725 docstring was capitalized for some
reason other than to identify a type defined in GitPython's code.

For now, I've changed it to refer to git.types.Tree_ish, but also
explicitly documented that a string can be used to specify a
tree-ish -- which is independently valuable, since previously the
effect of passing a str instance to the diff method was not stated
anywhere in the method docstring. To clarify further, I included a
link to tree-ish in gitglossary(7) as well.

In addition, the original wording before cd16a35 had included
"(type)", which I had erroneously assumed was just meant to state
that it is a type (i.e. a class), so I had wrongly removed it
without replacing it with anything when making it into a reference
to a type. But it was really an attempt to clarify that
Diffable.Index should be used directly, rather than an instance of
it. That is in effect the opposite of merely pointing out that it
is a class; it is to express that it should be used in a way that
does not depend in any way on it being a class. This commit has the
docstring explicitly state that.
---
 git/diff.py | 23 ++++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 16e6be151..ec7fe7755 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -122,12 +122,21 @@ def diff(
             This the item to compare us with.
 
             * If ``None``, we will be compared to the working tree.
-            * If :class:`~git.index.base.Treeish`, it will be compared against the
-              respective tree.
-            * If :class:`Diffable.Index`, it will be compared against the index.
-            * If :attr:`git.NULL_TREE`, it will compare against the empty tree.
-            * It defaults to :class:`Diffable.Index` so that the method will not by
-              default fail on bare repositories.
+
+            * If :class:`~git.types.Tree_ish`, it will be compared against the
+              respective tree. (See https://git-scm.com/docs/gitglossary#def_tree-ish.)
+              This can also be passed as a string.
+
+            * If :class:`Diffable.Index`, it will be compared against the index. Use the
+              type object :class:`Index` itself, without attempting to instantiate it.
+              (That is, you should treat :class:`Index` as an opqaue constant. Don't
+              rely on it being a class or even callable.)
+
+            * If :attr:`git.NULL_TREE <NULL_TREE>`, it will compare against the empty
+              tree.
+
+            This parameter defaults to :class:`Diffable.Index` (rather than ``None``) so
+            that the method will not by default fail on bare repositories.
 
         :param paths:
             This a list of paths or a single path to limit the diff to. It will only
@@ -143,7 +152,7 @@ def diff(
             sides of the diff.
 
         :return:
-            :class:`DiffIndex`
+            A :class:`DiffIndex` representing the computed diff.
 
         :note:
             On a bare repository, `other` needs to be provided as

From 0e1df29c171e8d6d7dd62f5840d88a9bfd0ef3ab Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 8 Mar 2024 23:27:58 -0500
Subject: [PATCH 583/994] Start fixing diff and _process_diff_args type
 annotations

This fixes three mypy errors by modifying the Diffable.diff,
Diffable._process_diff_args, and its IndexFile._process_diff_args
override. This change, so far, adds no suppressions, and even
removes some preexisting suppressions that were ineffective because
of how they were written (not being on the first line of the def).

However, this is not a complete fix of those annotations
themselves. This is because although the `other` parameter to
Diffable.diff and Diffable._process_diff_args does not appear
intended to have been as broad as including object in the union had
the effect of making it -- any union with object as an alternative
is equivalent to object itself -- it should nonetheless be broader
than the changes here make it.

Once that is fixed, it may not be possible to maintain compliance
with the Liskov substitution principle, in which case a suppression,
corresponding to one of those that was removed but fixed so it has
an effect, may need to be introduced, since actually fixing the LSP
violation would be a breaking change.

Specifically, as seen in 797e962 and 09053c5 in #1285, the object
alternative was originally intended not to indicate that any object
is allowed, but instead to allow the NULL_TREE constant, which was
(and currently remains) implemented as a direct instance of object.

The type of NULL_TREE is not the core problem. It can be fixed to
allow a narrowly scoped annotation. One way is by making NULL_TREE
an enumeration constant and annotating `Literal[NULL_TREE]`.

Rather, the problem is that the IndexFile.diff override really does
not accept NULL_TREE. It may not be feasible to modify it to accept
it. Even if that is to be done, it should be for runtime
correctness, and likely have a test case added for it, and may not
be suitable for inclusion alongside these static typing fixes.

So when the base class method's annotation is broadened to add such
a literal or equivalent, the override's annotation in the derived
class may not reasonably be able to change accordingly, as LSP
would dictate.

Part of the change here adds a missing os.PathLike[str] alternative
to _process_diff_args. For now I've opted to include both str
(already present) and os.PathLike[str] separately, rather than
consolidating them into PathLike (in GitPython, PathLike is
git.types.PathLike, which is Union[str, os.PathLike[str]]). The
reason is that, while some str arguments may be paths, others may
be options.

However, that stylistic choice may not be justified, since it is at
odds with some existing uses of PathLike in GitPython to cover both
str as a path and str as a non-path, including in the
implementation of Diffable.diff itself. So those may be
consolidated in a forthcoming commit.
---
 git/diff.py       | 12 +++++++-----
 git/index/base.py |  8 ++++----
 2 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index ec7fe7755..cf86e95fb 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import os
 import re
 
 from git.cmd import handle_process_output
@@ -98,8 +99,9 @@ class Index:
         """Stand-in indicating you want to diff against the index."""
 
     def _process_diff_args(
-        self, args: List[Union[str, "Diffable", Type["Diffable.Index"], object]]
-    ) -> List[Union[str, "Diffable", Type["Diffable.Index"], object]]:
+        self,
+        args: List[Union[str, os.PathLike[str], "Diffable", Type["Index"]]],
+    ) -> List[Union[str, os.PathLike[str], "Diffable", Type["Index"]]]:
         """
         :return:
             Possibly altered version of the given args list.
@@ -110,7 +112,7 @@ def _process_diff_args(
 
     def diff(
         self,
-        other: Union[Type["Index"], "Tree", "Commit", None, str, object] = Index,
+        other: Union[Type["Index"], "Tree", "Commit", str, None] = Index,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
@@ -159,7 +161,7 @@ def diff(
             :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or
             :class:`~git.objects.commit.Commit`, or a git command error will occur.
         """
-        args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = []
+        args: List[Union[PathLike, Diffable, Type["Diffable.Index"]]] = []
         args.append("--abbrev=40")  # We need full shas.
         args.append("--full-index")  # Get full index paths, not only filenames.
 
@@ -195,7 +197,7 @@ def diff(
 
         args.insert(0, self)
 
-        # paths is a list here, or None.
+        # paths is a list or tuple here, or None.
         if paths:
             args.append("--")
             args.extend(paths)
diff --git a/git/index/base.py b/git/index/base.py
index 249144d54..6fb5abdcb 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -644,9 +644,9 @@ def write_tree(self) -> Tree:
         return root_tree
 
     def _process_diff_args(
-        self,  # type: ignore[override]
-        args: List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
-    ) -> List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
+        self,
+        args: List[Union[str, os.PathLike[str], "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
+    ) -> List[Union[str, os.PathLike[str], "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
         try:
             args.pop(args.index(self))
         except IndexError:
@@ -1478,7 +1478,7 @@ def reset(
 
     # @ default_index, breaks typing for some reason, copied into function
     def diff(
-        self,  # type: ignore[override]
+        self,
         other: Union[Type["git_diff.Diffable.Index"], "Tree", "Commit", str, None] = git_diff.Diffable.Index,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,

From 62c0823da472a4f9c714827291d15a6f36abebda Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 00:09:45 -0500
Subject: [PATCH 584/994] Consolidate str and os.PathLike[str] (use GitPython's
 PathLike)

Where they had been introduced and written separately in 0e1df29
before this.

The main reason is that it would have to be quoted as a string
for compatibility with Python 3.8 and lower, since os.PathLike
only defined __class_getitem__ in 3.8 and later. (The definition
in git.types already does that.) In addition, the benefit of
keeping them separate was negligible, as mentioned in 0e1df29.
---
 git/diff.py       | 5 ++---
 git/index/base.py | 4 ++--
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index cf86e95fb..04e38d209 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,7 +3,6 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import os
 import re
 
 from git.cmd import handle_process_output
@@ -100,8 +99,8 @@ class Index:
 
     def _process_diff_args(
         self,
-        args: List[Union[str, os.PathLike[str], "Diffable", Type["Index"]]],
-    ) -> List[Union[str, os.PathLike[str], "Diffable", Type["Index"]]]:
+        args: List[Union[PathLike, "Diffable", Type["Index"]]],
+    ) -> List[Union[PathLike, "Diffable", Type["Index"]]]:
         """
         :return:
             Possibly altered version of the given args list.
diff --git a/git/index/base.py b/git/index/base.py
index 6fb5abdcb..357de6bd9 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -645,8 +645,8 @@ def write_tree(self) -> Tree:
 
     def _process_diff_args(
         self,
-        args: List[Union[str, os.PathLike[str], "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
-    ) -> List[Union[str, os.PathLike[str], "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
+        args: List[Union[PathLike, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
+    ) -> List[Union[PathLike, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
         try:
             args.pop(args.index(self))
         except IndexError:

From 7204cc106cc055b5bdd841dcc2a1c4c18e60fcf3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 10:34:59 -0500
Subject: [PATCH 585/994] Further clarify Diffable.diff docstring

Along the lines of ed6ead9.
---
 git/diff.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 04e38d209..4f4cab048 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -157,8 +157,9 @@ def diff(
 
         :note:
             On a bare repository, `other` needs to be provided as
-            :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or
-            :class:`~git.objects.commit.Commit`, or a git command error will occur.
+            :class:`~Diffable.Index`, or as an instance of
+            :class:`~git.objects.tree.Tree` or :class:`~git.objects.commit.Commit`, or a
+            git command error will occur.
         """
         args: List[Union[PathLike, Diffable, Type["Diffable.Index"]]] = []
         args.append("--abbrev=40")  # We need full shas.

From 2f5e258d2b232685993c589b51445e51b0c2a185 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 09:58:52 -0500
Subject: [PATCH 586/994] Annotate _process_diff_args without Diffable.Index

This removes the static type of Diffable's nested Index class,
Type["Index"], from the Diffable._process_diff_args method, and its
override IndexFile._process_diff_args. Further changes related to
this remain necessary, and at this time, this adds one mypy error.

The _process_diff_args methods did not handle Diffable.Index. The
base class method would pass it through, but it would not be usable
when then passed to "git diff". Instead, it is handled with correct
behavior at runtime in Diffable.diff and its IndexFile.diff
override, which handle it themselves and ensure it is never passed
to any _process_diff_args implementation.

That was already the case. The change here is mostly to type hints,
removing it from the _process_diff_args annotations, since those
methods have never actually worked with it and it probably wouldn't
make sense for them to handle it. However, this does also attempt
to help mypy figure out that Diffable.Index cannot end up as an
element of its local variable `args`. This attempt is unsuccessful.

The problem with the `args` local variable may be the reason
including Index in the parameter type annotations appeared correct
or necessary before. The issue is that Diffable.Index, even though
it ought to be used as an opaque constant, is a class. Its static
type is Type[Diffable.Index], but mypy is unable to infer that
there are no other objects of that static type, because if
Diffable.Index were subclassed, and the type object produced from
doing so (i.e. the subclass itself) were passed as an `other`
argument to the diff method, then the `other is Diffable.Index`
condition would evaluate to False.

Therefore to solve this problem it should be sufficient to decorate
the Diffable.Index class as `@final`. This works for pyright (and
thus also pylance), but it does not work for mypy (even in 1.9.0).
So that still has to be solved, and (other than via suppressions)
it may be necessary to make Diffable.Index an enumeration constant
rather than a class, so it can be annotated as a literal.
---
 git/diff.py       | 11 ++++++-----
 git/index/base.py |  5 ++---
 git/types.py      |  2 ++
 3 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 4f4cab048..91dd45704 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -28,7 +28,7 @@
     TYPE_CHECKING,
     cast,
 )
-from git.types import PathLike, Literal
+from git.types import Literal, PathLike, final
 
 if TYPE_CHECKING:
     from .objects.tree import Tree
@@ -94,13 +94,14 @@ class Diffable:
     repo: "Repo"
     """Repository to operate on. Must be provided by subclass or sibling class."""
 
+    @final
     class Index:
         """Stand-in indicating you want to diff against the index."""
 
     def _process_diff_args(
         self,
-        args: List[Union[PathLike, "Diffable", Type["Index"]]],
-    ) -> List[Union[PathLike, "Diffable", Type["Index"]]]:
+        args: List[Union[PathLike, "Diffable"]],
+    ) -> List[Union[PathLike, "Diffable"]]:
         """
         :return:
             Possibly altered version of the given args list.
@@ -161,7 +162,7 @@ def diff(
             :class:`~git.objects.tree.Tree` or :class:`~git.objects.commit.Commit`, or a
             git command error will occur.
         """
-        args: List[Union[PathLike, Diffable, Type["Diffable.Index"]]] = []
+        args: List[Union[PathLike, Diffable]] = []
         args.append("--abbrev=40")  # We need full shas.
         args.append("--full-index")  # Get full index paths, not only filenames.
 
@@ -184,7 +185,7 @@ def diff(
             paths = [paths]
 
         diff_cmd = self.repo.git.diff
-        if other is self.Index:
+        if other is Diffable.Index:
             args.insert(0, "--cached")
         elif other is NULL_TREE:
             args.insert(0, "-r")  # Recursive diff-tree.
diff --git a/git/index/base.py b/git/index/base.py
index 357de6bd9..591f8c6f6 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -645,8 +645,8 @@ def write_tree(self) -> Tree:
 
     def _process_diff_args(
         self,
-        args: List[Union[PathLike, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]],
-    ) -> List[Union[PathLike, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]:
+        args: List[Union[PathLike, "git_diff.Diffable"]],
+    ) -> List[Union[PathLike, "git_diff.Diffable"]]:
         try:
             args.pop(args.index(self))
         except IndexError:
@@ -1494,7 +1494,6 @@ def diff(
             Will only work with indices that represent the default git index as they
             have not been initialized with a stream.
         """
-
         # Only run if we are the default repository index.
         if self._file_path != self._index_path():
             raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
diff --git a/git/types.py b/git/types.py
index 8f71d079a..652a73940 100644
--- a/git/types.py
+++ b/git/types.py
@@ -22,6 +22,7 @@
         TypedDict,
         Protocol,
         SupportsIndex as SupportsIndex,
+        final,
         runtime_checkable,
     )
 else:
@@ -30,6 +31,7 @@
         SupportsIndex as SupportsIndex,
         TypedDict,
         Protocol,
+        final,
         runtime_checkable,
     )
 

From 65863a28c2bfebe084e1c86d409fbf68b0b33a76 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 13:34:55 -0500
Subject: [PATCH 587/994] Make NULL_TREE and Index precisely annotatable

This creates a git.diff.DiffConstants enumeration and makes the
git.diff.NULL_TREE and git.diff.Diffable.Index objects constants in
it. This allows them (including as an alternative in a union) to be
annotated as literals:

- Literal[DiffConstants.NULL_TREE]
- Literal[DIffConstants.INDEX]

Although the enumeration type must unfortunately be included in the
annotations as shown above (at least mypy requires this), using the
objects/values themselves does not require any code to change. So
this shouldn't break anything at runtime for code using GitPython,
unless it has relied on NULL_TREE being a direct instance of object,
or relied on Diffable.Index (all useful uses of which were also as
an opaque constant) being defined as a class.

More specifically, all the ways that NULL_TREE and Index could be
*accessed* before are still working, because:

- NULL_TREE is aliased at module level, where it was before.
  It is also aliased at class level in Diffable, for consistency.

- INDEX is aliased at class level in Diffable, as Index, where it
  was before. This way, Diffable.Index can still be used. It is
  also aliased, in the same scope, as INDEX. Because it is, and in
  effect has always been, a constant, the new INDEX spelling is
  preferable. But there is no major disadvantage of the old Index
  spelling, so in docstrings I have made no effort at this time to
  discourage its use. (If GitPython ever uses all-caps identifiers
  strictly as constants, then the clarity benefit of ensuring only
  the INDEX version is used will be greater, and then it might make
  sense to deprecate Index. However, this seems unlikely to happen
  as it would be a breaking change, due to the way functions like
  git.reset rebind Git.GIT_PYTHON_GIT_EXECUTABLE.) INDEX is also
  aliased at module level, for consistency.

- NULL_TREE is still included in git.diff.__all__, causing it to be
  recognized as public in git.diff and also to be accessible as an
  attribute of the top-level git module (which currently uses
  wildcard imports). For consistency, I have also included INDEX in
  __all__.

- Because there is a benefit to being able to freely referene the
  new DiffConstants enumeration in annotations (though in practice
  this may mostly be to implementers of new Diffable subclasses), I
  have also included DiffConstants in __all__. In addition, the
  name DiffConstants (rather than, e.g., Constants) should avoid
  confusion even if it ends up in another scope unexpectedly.

To avoid a situation where users/developers may erroneously think
these aliases are different from each other, I have documented the
situation in the docstrings for each, referring to the others.
(Sphinx does not automatically use the original docstring for an
aliased name introduced in this way, and there is also arguably a
clarity benefit to their differing wording, such as how each refers
*only* to the others.) Other docstings are also updated.

This commit completes the change begun in 2f5e258 before this,
resolving the one mypy error it added. But this does not complete
the larger change begun in 0e1df29:

- One of the effects of this change is to make it possible to
  annotate precisely for NULL_TREE, either by using
  Literal[DiffConstants.NULL_TREE] or consolidating it with the
  Literal[DiffConstants.INDEX] alternative by including
  DiffConstants in the union instead of either/both of them.

- But that is not yet done here, and when it is done for
  Diffable.diff, the LSP issue in IndexFile.diff, which does not
  currently accommodate NULL_TREE, will resurface (or, rather, be
  rightly revealed by mypy, in a way that is specifically clear).
---
 git/diff.py       | 102 +++++++++++++++++++++++++++++++++++-----------
 git/index/base.py |   5 +--
 git/types.py      |   2 -
 3 files changed, 81 insertions(+), 28 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index 91dd45704..cebc1b3ed 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,6 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import enum
 import re
 
 from git.cmd import handle_process_output
@@ -22,13 +23,12 @@
     Match,
     Optional,
     Tuple,
-    Type,
     TypeVar,
     Union,
     TYPE_CHECKING,
     cast,
 )
-from git.types import Literal, PathLike, final
+from git.types import Literal, PathLike
 
 if TYPE_CHECKING:
     from .objects.tree import Tree
@@ -48,10 +48,55 @@
 # ------------------------------------------------------------------------
 
 
-__all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE")
+__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff")
 
-NULL_TREE = object()
-"""Special object to compare against the empty tree in diffs."""
+
+@enum.unique
+class DiffConstants(enum.Enum):
+    """Special objects for :meth:`Diffable.diff`.
+
+    See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
+    values including these.
+
+    :note:
+        These constants are also available as attributes of the :mod:`git.diff` module,
+        the :class:`Diffable` class and its subclasses and instances, and the top-level
+        :mod:`git` module.
+    """
+
+    NULL_TREE = enum.auto()
+    """Stand-in indicating you want to compare against the empty tree in diffs.
+
+    Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
+    :const:`Diffable.NULL_TREE`.
+    """
+
+    INDEX = enum.auto()
+    """Stand-in indicating you want to diff against the index.
+
+    Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
+    :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
+    kept for backward compatibility and made an alias of this, so it may still be used.
+    """
+
+
+NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
+"""Stand-in indicating you want to compare against the empty tree in diffs.
+
+See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
+
+This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
+:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
+"""
+
+INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
+"""Stand-in indicating you want to diff against the index.
+
+See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
+
+This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
+:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
+"""
 
 _octal_byte_re = re.compile(rb"\\([0-9]{3})")
 
@@ -84,7 +129,7 @@ class Diffable:
     compatible type.
 
     :note:
-        Subclasses require a repo member, as it is the case for
+        Subclasses require a :attr:`repo` member, as it is the case for
         :class:`~git.objects.base.Object` instances. For practical reasons we do not
         derive from :class:`~git.objects.base.Object`.
     """
@@ -94,9 +139,25 @@ class Diffable:
     repo: "Repo"
     """Repository to operate on. Must be provided by subclass or sibling class."""
 
-    @final
-    class Index:
-        """Stand-in indicating you want to diff against the index."""
+    NULL_TREE = NULL_TREE
+    """Stand-in indicating you want to compare against the empty tree in diffs.
+
+    See the :meth:`diff` method, which accepts this as a value of its ``other``
+    parameter.
+
+    This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
+    :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
+    """
+
+    INDEX = Index = INDEX
+    """Stand-in indicating you want to diff against the index.
+
+    See the :meth:`diff` method, which accepts this as a value of its ``other``
+    parameter.
+
+    This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
+    :const:`git.INDEX` and :const:`git.diff.INDEX`.
+    """
 
     def _process_diff_args(
         self,
@@ -112,7 +173,7 @@ def _process_diff_args(
 
     def diff(
         self,
-        other: Union[Type["Index"], "Tree", "Commit", str, None] = Index,
+        other: Union[Literal[DiffConstants.INDEX], "Tree", "Commit", str, None] = INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
@@ -125,20 +186,15 @@ def diff(
 
             * If ``None``, we will be compared to the working tree.
 
-            * If :class:`~git.types.Tree_ish`, it will be compared against the
-              respective tree. (See https://git-scm.com/docs/gitglossary#def_tree-ish.)
-              This can also be passed as a string.
+            * If a :class:`~git.types.Tree_ish` or string, it will be compared against
+              the respective tree.
 
-            * If :class:`Diffable.Index`, it will be compared against the index. Use the
-              type object :class:`Index` itself, without attempting to instantiate it.
-              (That is, you should treat :class:`Index` as an opqaue constant. Don't
-              rely on it being a class or even callable.)
+            * If :const:`INDEX`, it will be compared against the index.
 
-            * If :attr:`git.NULL_TREE <NULL_TREE>`, it will compare against the empty
-              tree.
+            * If :const:`NULL_TREE`, it will compare against the empty tree.
 
-            This parameter defaults to :class:`Diffable.Index` (rather than ``None``) so
-            that the method will not by default fail on bare repositories.
+            This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
+            method will not by default fail on bare repositories.
 
         :param paths:
             This a list of paths or a single path to limit the diff to. It will only
@@ -185,7 +241,7 @@ def diff(
             paths = [paths]
 
         diff_cmd = self.repo.git.diff
-        if other is Diffable.Index:
+        if other is INDEX:
             args.insert(0, "--cached")
         elif other is NULL_TREE:
             args.insert(0, "-r")  # Recursive diff-tree.
@@ -218,7 +274,7 @@ def diff(
 
 
 class DiffIndex(List[T_Diff]):
-    R"""An Index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
+    R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
     properties.
 
     The class improves the diff handling convenience.
diff --git a/git/index/base.py b/git/index/base.py
index 591f8c6f6..48e843822 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -76,11 +76,10 @@
     Sequence,
     TYPE_CHECKING,
     Tuple,
-    Type,
     Union,
 )
 
-from git.types import Commit_ish, PathLike
+from git.types import Commit_ish, Literal, PathLike
 
 if TYPE_CHECKING:
     from subprocess import Popen
@@ -1479,7 +1478,7 @@ def reset(
     # @ default_index, breaks typing for some reason, copied into function
     def diff(
         self,
-        other: Union[Type["git_diff.Diffable.Index"], "Tree", "Commit", str, None] = git_diff.Diffable.Index,
+        other: Union[Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None] = git_diff.INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
diff --git a/git/types.py b/git/types.py
index 652a73940..8f71d079a 100644
--- a/git/types.py
+++ b/git/types.py
@@ -22,7 +22,6 @@
         TypedDict,
         Protocol,
         SupportsIndex as SupportsIndex,
-        final,
         runtime_checkable,
     )
 else:
@@ -31,7 +30,6 @@
         SupportsIndex as SupportsIndex,
         TypedDict,
         Protocol,
-        final,
         runtime_checkable,
     )
 

From c9952e14b51140e7307a031149fbe15ab5911abd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 14:49:46 -0500
Subject: [PATCH 588/994] Fix Sphinx references; give Diffable.Index a
 docstring

---
 git/diff.py | 27 +++++++++++++++++++++------
 1 file changed, 21 insertions(+), 6 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index cebc1b3ed..e185b4cb3 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -149,14 +149,30 @@ class Diffable:
     :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
     """
 
-    INDEX = Index = INDEX
+    INDEX = INDEX
     """Stand-in indicating you want to diff against the index.
 
     See the :meth:`diff` method, which accepts this as a value of its ``other``
     parameter.
 
     This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
-    :const:`git.INDEX` and :const:`git.diff.INDEX`.
+    :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
+    which is kept for backward compatibility (it is now defined an alias of this).
+    """
+
+    Index = INDEX
+    """Stand-in indicating you want to diff against the index
+    (same as :const:`~Diffable.INDEX`).
+
+    This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
+    :const:`~Diffable.INDEX` and :meth:`diff` for details.
+
+    :note:
+        Although always meant for use as an opaque constant, this was formerly defined
+        as a class. Its usage is unchanged, but static type annotations that attempt
+        to permit only this object must be changed to avoid new mypy errors. This was
+        previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
+        It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
     """
 
     def _process_diff_args(
@@ -213,10 +229,9 @@ def diff(
             A :class:`DiffIndex` representing the computed diff.
 
         :note:
-            On a bare repository, `other` needs to be provided as
-            :class:`~Diffable.Index`, or as an instance of
-            :class:`~git.objects.tree.Tree` or :class:`~git.objects.commit.Commit`, or a
-            git command error will occur.
+            On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
+            an instance of :class:`~git.objects.tree.Tree` or
+            :class:`~git.objects.commit.Commit`, or a git command error will occur.
         """
         args: List[Union[PathLike, Diffable]] = []
         args.append("--abbrev=40")  # We need full shas.

From b8a25dfbe1c7a3811549ab2fd4e10482af70f752 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 15:10:58 -0500
Subject: [PATCH 589/994] Modify annotations to accommodate NULL_TREE

This finishes the main changes being done at this time to the
annotations of diff-related methods that were started in 0e1df29.

As expected and noted in previous commits, having Diffable.diff
permit NULL_TREE entails a violation of the Liskov substitution
principle in the overridden method IndexFile.diff unless a similar
change were also made there, which could not be done correctly
without modifying its behavior to actually accept NULL_TREE (it
does not contain code to cover it, and raises ane exception if it
is passed). I am unsure if that should ultimately be done, but even
if so, it seems to me to be beyond the scope of the typing changes
being done here.

This therefore applies a suppression there. The suppression is
specific to that one parameter. The long-standing comment atop the
IndexFile.diff method, which reads as vague today, is replaced with
a specific FIXME comment describing the situation where the method
refers to the base-class docstring for documentation of its
parameters yet doesn't accept NULL_TREE (and notes the mypy error).

In effect this is really restoring and fixing the suppression that
was present before 0e1df29 rather than adding a "new" one, but at
that time the base-class parameter type was much broader since it
was a union with object as one of its alternatives, so the
situation was much less clear.
---
 git/diff.py       | 2 +-
 git/index/base.py | 7 +++++--
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index e185b4cb3..06935f87e 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -189,7 +189,7 @@ def _process_diff_args(
 
     def diff(
         self,
-        other: Union[Literal[DiffConstants.INDEX], "Tree", "Commit", str, None] = INDEX,
+        other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
diff --git a/git/index/base.py b/git/index/base.py
index 48e843822..dd58200ed 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1475,10 +1475,13 @@ def reset(
 
         return self
 
-    # @ default_index, breaks typing for some reason, copied into function
+    # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
+    # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
     def diff(
         self,
-        other: Union[Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None] = git_diff.INDEX,
+        other: Union[  # type: ignore[override]
+            Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None
+        ] = git_diff.INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,

From e49327dbe5ecd792d3c354155b6f186b31409c33 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 19:51:39 -0500
Subject: [PATCH 590/994] Add refresh to top-level __all__

This makes the git.refresh function unambiguously public.

git.refresh was already public in the sense that it was explicitly
documented as appropriate to call from code outside GitPython.
However, it had not been included in git.__all__.

Because __all__ existed but omitted "refresh", git.refresh had
appeared non-public to automated tools.

This also does some cleanup:

- It removes a comment that showed how git.__all__ had been defined
  dynamically before #1659, since with the addition of "refresh",
  git.__all__ no longer contains exactly the same elements as that
  technique produced (as it examined the module's contents prior to
  running the def statement that bound the name "refresh").

- With that comment removed, it is no longer necessary to define
  __all__ below the imports to show what the dynamic techinque had
  operated on. So this moves it up above them in accordance with
  PEP-8.
---
 git/__init__.py | 63 ++++++++++++++++++++++++-------------------------
 1 file changed, 31 insertions(+), 32 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index ed8a88d4b..ac211f9b9 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -5,38 +5,6 @@
 
 # @PydevCodeAnalysisIgnore
 
-__version__ = "git"
-
-from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
-
-from gitdb.util import to_hex_sha
-from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
-from git.types import PathLike
-
-try:
-    from git.compat import safe_decode  # @NoMove @IgnorePep8
-    from git.config import GitConfigParser  # @NoMove @IgnorePep8
-    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.cmd import Git  # @NoMove @IgnorePep8
-    from git.repo import Repo  # @NoMove @IgnorePep8
-    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.util import (  # @NoMove @IgnorePep8
-        LockFile,
-        BlockingLockFile,
-        Stats,
-        Actor,
-        remove_password_if_present,
-        rmtree,
-    )
-except GitError as _exc:  # noqa: F405
-    raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
-
-# __all__ must be statically defined by py.typed support
-# __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
 __all__ = [  # noqa: F405
     "Actor",
     "AmbiguousObjectName",
@@ -109,12 +77,43 @@
     "UnsupportedOperation",
     "UpdateProgress",
     "WorkTreeRepositoryUnsupported",
+    "refresh",
     "remove_password_if_present",
     "rmtree",
     "safe_decode",
     "to_hex_sha",
 ]
 
+__version__ = "git"
+
+from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
+
+from gitdb.util import to_hex_sha
+from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
+from git.types import PathLike
+
+try:
+    from git.compat import safe_decode  # @NoMove @IgnorePep8
+    from git.config import GitConfigParser  # @NoMove @IgnorePep8
+    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.cmd import Git  # @NoMove @IgnorePep8
+    from git.repo import Repo  # @NoMove @IgnorePep8
+    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
+    from git.util import (  # @NoMove @IgnorePep8
+        LockFile,
+        BlockingLockFile,
+        Stats,
+        Actor,
+        remove_password_if_present,
+        rmtree,
+    )
+except GitError as _exc:  # noqa: F405
+    raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
+
 # { Initialize git executable path
 GIT_OK = None
 

From c8ad3a36d39adddb36f62722678cdada49f94708 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 23 Feb 2024 22:07:24 -0500
Subject: [PATCH 591/994] Deprecate public access to typing imports in git

This adds comments to entries in git.__all__ for each of the
entries that come from the standard library typing module, noting
them as deprecated.

These imports were included in __all__ inadvertently due to the
way __all__ was dynamically constructed, and placed in __all__
explicitly when __all__ became static in #1659. They are there for
backward compatibility, in case some code relies on them being
there. But a module is unlikely to rely intentionally on the git
module providing them, since they are not conceptually related to
GitPython.

`from git import *` should not typically be used, since wildcard
imports are not generally recommended, as discussed in PEP-8. But
if someone does choose to use it, they would probably benefit less
from DeprecationWarning being issued for each of those names than
they would usually benefit from DeprecationWarning. This could lead
to developers deciding not to enable DeprecationWarning when it may
otherwise be useful. For this reason, no attempt is currently made
to issue DeprecationWarning when those names are accessed as
attributes of the git module.
---
 git/__init__.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index ac211f9b9..d6a247de0 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -38,13 +38,13 @@
     "IndexObject",
     "InvalidDBRoot",
     "InvalidGitRepositoryError",
-    "List",
+    "List",  # Deprecated - import this from `typing` instead.
     "LockFile",
     "NULL_TREE",
     "NoSuchPathError",
     "ODBError",
     "Object",
-    "Optional",
+    "Optional",  # Deprecated - import this from `typing` instead.
     "ParseError",
     "PathLike",
     "PushInfo",
@@ -58,19 +58,19 @@
     "RepositoryDirtyError",
     "RootModule",
     "RootUpdateProgress",
-    "Sequence",
+    "Sequence",  # Deprecated - import this from `typing` instead.
     "StageType",
     "Stats",
     "Submodule",
     "SymbolicReference",
-    "TYPE_CHECKING",
+    "TYPE_CHECKING",  # Deprecated - import this from `typing` instead.
     "Tag",
     "TagObject",
     "TagReference",
     "Tree",
     "TreeModifier",
-    "Tuple",
-    "Union",
+    "Tuple",  # Deprecated - import this from `typing` instead.
+    "Union",  # Deprecated - import this from `typing` instead.
     "UnmergedEntriesError",
     "UnsafeOptionError",
     "UnsafeProtocolError",

From 3c8cbe90abb0f8b38bf53b111fac3e1d1e0a8de3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 4 Mar 2024 22:52:16 -0500
Subject: [PATCH 592/994] Mention collections.abc for Sequence

In the deprecation comment for importing Sequence from the top
level git module. Although the git module imports it from typing,
collections.abc.Sequence supports __class_getitem__ since Python
3.9 (and typing.Sequence is actually itself deprecated since then).
---
 git/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/__init__.py b/git/__init__.py
index d6a247de0..24b8054f0 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -58,7 +58,7 @@
     "RepositoryDirtyError",
     "RootModule",
     "RootUpdateProgress",
-    "Sequence",  # Deprecated - import this from `typing` instead.
+    "Sequence",  # Deprecated - import from `typing`, or `collections.abc` in 3.9+.
     "StageType",
     "Stats",
     "Submodule",

From 87b314ee1374e0c295ba65248b2c69c6c15378c7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 16:24:05 -0500
Subject: [PATCH 593/994] Add INDEX and DiffConstants to git.__all__

This adds the names of git.diff.INDEX and git.diff.DiffConstants to
the top-level __all__.

In particular, the recently introduced INDEX constant has the same
standing as NULL_TREE, which was (rightly) already listed, and this
change makes true the claims in some docstrings in the git.diff
module that say that INDEX is accessible (among other ways) as
git.INDEX.
---
 git/__init__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/git/__init__.py b/git/__init__.py
index 24b8054f0..026dc1461 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -20,6 +20,7 @@
     "CommandError",
     "Commit",
     "Diff",
+    "DiffConstants",
     "DiffIndex",
     "Diffable",
     "FetchInfo",
@@ -33,6 +34,7 @@
     "HEAD",
     "Head",
     "HookExecutionError",
+    "INDEX",
     "IndexEntry",
     "IndexFile",
     "IndexObject",

From 9ed904cf6d3c36192161cded9615fefc2bf18bf7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 16:47:11 -0500
Subject: [PATCH 594/994] Adjust mypy options to work well with mypy 1.9.0

mypy 1.9.0 was very recently released. This makes two changes,
in light of that, while avoiding breaking earlier versions:

- Change the Python version mypy assumes from 3.7 to 3.8 as
  configiured in pyproject.toml, because starting in 1.9.0 mypy no
  longer supports type checking for Python 3.7.

  It makes sense to keep having a low version checked on local runs
  of mypy (when not further overridden with `--python-version`).
  Since the CI mypy steps currently are written not to fail their
  jobs if they fail (and they do fail, as there remain some mypy
  errors that are neither fixed nor suppressed), mypy status from
  CI is not obvious, and it may be that the main way mypy is
  actually used when developing GitPython is locally. But keeping
  it down to 3.7 prints a warning and falls back to the version of
  Python that is used to run mypy, starting in mypy 1.9.0.

- Have each CI job that runs mypy do so for the Python version that
  job exists to test. (This is already the case for the *platform*
  that job exists to test, because the platform mypy should check
  for is not overridden in pyproject.toml.) This is done by passing
  the test matrix Python version with `--python-version`, which
  takes priority over the pyproject.toml option (which itself takes
  priority over the default version selection, which in this case
  would be the same as what we are turning it back into on CI).

  This is worthwhile even separately from the changes in mypy
  1.9.0. But one of its effects is to make it so the mypy step in
  the Python 3.7 test jobs still checks Python 3.7 (so the results
  for 3.7, which GitPython has not yet dropped support for) can
  still be examined. This works becuase the latest version of mypy
  that can *run* on 3.7, which pip selects automatically, *does*
  support type-checking for 3.7.
---
 .github/workflows/pythonpackage.yml | 2 +-
 pyproject.toml                      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 6b89530c3..e43f763e2 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -88,7 +88,7 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy -p git
+        mypy --python-version=${{ matrix.python-version }} -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
diff --git a/pyproject.toml b/pyproject.toml
index 7109389d7..03c68a5c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ 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.7"
+python_version = "3.8"
 disallow_untyped_defs = true
 no_implicit_optional = true
 warn_redundant_casts = true

From aeacb001e9d316ffcc054761c033eb3c107f03e1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 17:09:54 -0500
Subject: [PATCH 595/994] Colorize mypy output on CI for easier reading

See:

- https://github.com/python/mypy/issues/13815
- https://github.com/python/mypy/issues/13817

This seems to be working in in all jobs *except* for the Python 3.7
and 3.8 jobs on macOS.
---
 .github/workflows/pythonpackage.yml | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index e43f763e2..604110f70 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -89,8 +89,11 @@ jobs:
     - name: Check types with mypy
       run: |
         mypy --python-version=${{ matrix.python-version }} -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.
+      env:
+        MYPY_FORCE_COLOR: "1"
+        TERM: "xterm-256color"  # For color: https://github.com/python/mypy/issues/13817
+      # 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

From 84fc8062dccbf9c5e415f4ce0100e0a18680bca8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 19:21:11 -0500
Subject: [PATCH 596/994] Remove some unneeded mypy suppressions

These include suppressions that are no longer required because they
worked around mypy bugs that have been fixed, or because they were
not actually effective. It may also include some that are no longer
needed becuase of improvements made to GitPython's type annotations
(or other code) since they were introduced, I am not sure.

This deliberately keeps warn_unused_ignores uncommented in
pyproject.toml, at least for now.
---
 git/config.py         | 4 ++--
 git/objects/commit.py | 2 +-
 git/objects/tree.py   | 4 ++--
 git/objects/util.py   | 2 +-
 git/refs/reference.py | 4 ++--
 git/util.py           | 4 ++--
 pyproject.toml        | 2 +-
 7 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/git/config.py b/git/config.py
index 2164f67dc..78a88ba5e 100644
--- a/git/config.py
+++ b/git/config.py
@@ -344,9 +344,9 @@ def __init__(
             configuration files.
         """
         cp.RawConfigParser.__init__(self, dict_type=_OMD)
-        self._dict: Callable[..., _OMD]  # type: ignore   # mypy/typeshed bug?
+        self._dict: Callable[..., _OMD]
         self._defaults: _OMD
-        self._sections: _OMD  # type: ignore  # mypy/typeshed bug?
+        self._sections: _OMD
 
         # Used in Python 3. Needs to stay in sync with sections for underlying
         # implementation to work.
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 3246d9a36..28df86c9d 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -292,7 +292,7 @@ def name_rev(self) -> str:
     def iter_items(
         cls,
         repo: "Repo",
-        rev: Union[str, "Commit", "SymbolicReference"],  # type: ignore
+        rev: Union[str, "Commit", "SymbolicReference"],
         paths: Union[PathLike, Sequence[PathLike]] = "",
         **kwargs: Any,
     ) -> Iterator["Commit"]:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 995c9a663..385389f03 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -300,7 +300,7 @@ def cache(self) -> TreeModifier:
         return TreeModifier(self._cache)
 
     def traverse(
-        self,  # type: ignore[override]
+        self,
         predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True,
         prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False,
         depth: int = -1,
@@ -331,7 +331,7 @@ def traverse(
             super()._traverse(
                 predicate,
                 prune,
-                depth,  # type: ignore
+                depth,
                 branch_first,
                 visit_once,
                 ignore_self,
diff --git a/git/objects/util.py b/git/objects/util.py
index 6f4e7d087..194403ba5 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -619,7 +619,7 @@ class TraversableIterableObj(IterableObj, Traversable):
     def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]:
         return super()._list_traverse(*args, **kwargs)
 
-    @overload  # type: ignore
+    @overload
     def traverse(self: T_TIobj) -> Iterator[T_TIobj]:
         ...
 
diff --git a/git/refs/reference.py b/git/refs/reference.py
index a7b545fed..2da511ce3 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -150,7 +150,7 @@ def iter_items(
 
     # { Remote Interface
 
-    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
+    @property
     @require_remote_ref_path
     def remote_name(self) -> str:
         """
@@ -162,7 +162,7 @@ def remote_name(self) -> str:
         # /refs/remotes/<remote name>/<branch_name>
         return tokens[2]
 
-    @property  # type: ignore  # mypy cannot deal with properties with an extra decorator (2021-04-21).
+    @property
     @require_remote_ref_path
     def remote_head(self) -> str:
         """
diff --git a/git/util.py b/git/util.py
index 880d6625a..d9e223c76 100644
--- a/git/util.py
+++ b/git/util.py
@@ -510,8 +510,8 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[
     try:
         p = osp.expanduser(p)  # type: ignore
         if expand_vars:
-            p = osp.expandvars(p)  # type: ignore
-        return osp.normpath(osp.abspath(p))  # type: ignore
+            p = osp.expandvars(p)
+        return osp.normpath(osp.abspath(p))
     except Exception:
         return None
 
diff --git a/pyproject.toml b/pyproject.toml
index 03c68a5c6..2aeae59f9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ python_version = "3.8"
 disallow_untyped_defs = true
 no_implicit_optional = true
 warn_redundant_casts = true
-# warn_unused_ignores = true
+warn_unused_ignores = true
 warn_unreachable = true
 show_error_codes = true
 implicit_reexport = true

From 96ecc2edeb14b950737d7f6937055c2b19ea5d5a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 19:29:16 -0500
Subject: [PATCH 597/994] Drop deprecated mypy option

show_error_codes is deprecated, and represents the default now.
---
 pyproject.toml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 2aeae59f9..ef1189b0c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,6 @@ no_implicit_optional = true
 warn_redundant_casts = true
 warn_unused_ignores = true
 warn_unreachable = true
-show_error_codes = true
 implicit_reexport = true
 # strict = true
 

From 97d9b65937ffe9b7ce374dccdbfd4ef24b327b4a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 20:00:46 -0500
Subject: [PATCH 598/994] Apply intended suppression in Tree.traverse

The superclass _traverse method call in Tree.traverse appears to
have once had a working suppression for the incompatbile types of
two arguments, `predicate` and `prune`, as well as for an argument
that required (and requires) no suppression, `depth`. These three
arguments were written on the same line, which bad a `type: ignore`
comment on it. But when black formatting was applied in 21ec529
(#1442), that comment moved so that it was on a line with just the
`depth` call that didn't need it, rather than the others that did.

Since then, mypy has reported errors, which further seem intended
to suppress based on the surrounding context and the use of `cast`
to deal with the static type incompatibilities going the other way.

This misplaced suppression was one of the ones I very recently
removed in 84fc806. But really there should be a suppression for
those arguments (at least for now, while the code remains written
that way, given that a suppression is intended).

This suppresses the error effectively by inserting two suppression
comments, one for each of the two arguments. This is more specific
than a single suppression applying to the whole call, and keeping
the arguments on separate lines both makes black happy and makes
clear that it is not by coincidence that the error is suppressed
for both of them. The new suppressions are also written for the
specific mypy error at issue, rather than fully general as before.

This change decreases the number of mypy errors by two.
---
 git/objects/tree.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index 385389f03..30b6a9e4e 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -329,8 +329,8 @@ def traverse(
         return cast(
             Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]],
             super()._traverse(
-                predicate,
-                prune,
+                predicate,  # type: ignore[arg-type]
+                prune,  # type: ignore[arg-type]
                 depth,
                 branch_first,
                 visit_once,

From ad00c77b39707ca9a347780a18701e342128bcf2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 20:41:59 -0500
Subject: [PATCH 599/994] Spell self.Index as self.INDEX in IndexFile.diff

Because, as noted in 65863a2, it is now (slightly) preferable to
write INDEX. Note that this is solely stylstic: they are equivalent
and must remain so, so that existing code that uses GitPython and
accesses it as Index (including in overridden diff methods when
subclassing Diffable) continues to work.

This changes it in the exception message as well.
---
 git/index/base.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index dd58200ed..a95c3a622 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1500,7 +1500,7 @@ def diff(
         if self._file_path != self._index_path():
             raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
         # Index against index is always empty.
-        if other is self.Index:
+        if other is self.INDEX:
             return git_diff.DiffIndex()
 
         # Index against anything but None is a reverse diff with the respective item.
@@ -1514,12 +1514,12 @@ def diff(
             # Invert the existing R flag.
             cur_val = kwargs.get("R", False)
             kwargs["R"] = not cur_val
-            return other.diff(self.Index, paths, create_patch, **kwargs)
+            return other.diff(self.INDEX, paths, create_patch, **kwargs)
         # END diff against other item handling
 
         # If other is not None here, something is wrong.
         if other is not None:
-            raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other)
+            raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
 
         # Diff against working copy - can be handled by superclass natively.
         return super().diff(other, paths, create_patch, **kwargs)

From 2decbe45019fc8dcc6904fcdc6f1088f52b9f00c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 20:57:59 -0500
Subject: [PATCH 600/994] Test that redefined Diffable.Index should be
 compatible

This tests the most important characteristics of Diffable.Index
that must be (and are) preserved in making it an alias of the
enumeration constant INDEX instead of a nested class definition.

Adding this additional test also allows commit.INDEX to be used in
place of commit.Index in the existing test (since part of what this
tests is that they are the same object). That change is also made,
along with some very minor cleanup of immediately nearby test code.
---
 test/test_diff.py | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/test/test_diff.py b/test/test_diff.py
index ed82b1bbd..6d2f82649 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -12,7 +12,7 @@
 import ddt
 import pytest
 
-from git import NULL_TREE, Diff, DiffIndex, GitCommandError, Repo, Submodule
+from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
 from git.cmd import Git
 from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
 
@@ -352,7 +352,7 @@ def test_diff_submodule(self):
         self.assertIsInstance(diff.b_blob.size, int)
 
     def test_diff_interface(self):
-        # Test a few variations of the main diff routine.
+        """Test a few variations of the main diff routine."""
         assertion_map = {}
         for i, commit in enumerate(self.rorepo.iter_commits("0.1.6", max_count=2)):
             diff_item = commit
@@ -360,7 +360,7 @@ def test_diff_interface(self):
                 diff_item = commit.tree
             # END use tree every second item
 
-            for other in (None, NULL_TREE, commit.Index, commit.parents[0]):
+            for other in (None, NULL_TREE, commit.INDEX, commit.parents[0]):
                 for paths in (None, "CHANGES", ("CHANGES", "lib")):
                     for create_patch in range(2):
                         diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch)
@@ -406,10 +406,22 @@ def test_diff_interface(self):
         diff_index = c.diff(cp, ["does/not/exist"])
         self.assertEqual(len(diff_index), 0)
 
+    def test_diff_interface_stability(self):
+        """Test that the Diffable.Index redefinition should not break compatibility."""
+        self.assertIs(
+            Diffable.Index,
+            Diffable.INDEX,
+            "The old and new class attribute names must be aliases.",
+        )
+        self.assertIs(
+            type(Diffable.INDEX).__eq__,
+            object.__eq__,
+            "Equality comparison must be reference-based.",
+        )
+
     @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)
         file_a = osp.join(rw_dir, "file_a.txt")

From 88557bc066209db37101938236792bf0fdd6c535 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 9 Mar 2024 23:39:35 -0500
Subject: [PATCH 601/994] Have git module use sys.platform to check for Windows

This changes the way code throughout the git module checks to see
if it is running on a native Windows system.

Some checks using os.name were already changed to use sys.platform
in dc95a76 and 4191f7d. (See dc95a76 on why the specific question
of whether the system is native Windows can be answered by either
checking os.name or sys.platform, even though in general they
differ in granularity and are not always suited to the same tasks.)

The reasons for this change are:

- Consistency: Due to dc95a76 and 4191f7d, some of these checks use
  sys.platform, so in the absence of a reason to do otherwise, it
  is best that they all do. Otherwise, it creates an appearance
  that the technical reasons behind the difference are stronger
  than they really are, or even that differnt things are being
  checked.

- Better static typing: mypy treats sys.platform as a constant
  (and also allows checking on platform on another via --platform).
  Likewise, typeshed conditions platform-dependent declarations on
  sys.platform checks, rather than os.name or other checks. Really
  this is the original reason for those earlier, more selective
  changes, but here the goal is more general, since this is not
  needed to address any specific preexisting mypy errors.

This is incomplete, for two reasons:

- I'm deliberately not including changes in the tests in this
  commit. Arguably it should be kept as os.name in the tests, on
  the grounds that the test suite is not currently statically typed
  anyway, plus having them differ more compellingly shows that the
  behavior is the same whether an os.name or sys.platform check is
  used. However, it would be confusing to keep them different, and
  also somewhat unnatural; one approach would probably end up
  leaking through. Furthermore, because some tests have to check
  for cygwin specifically, which cannot be done with os.name, it
  may be clearer to use sys.platform for all platform checking in
  the test suite. But to verify and demonstrate that the change
  really is safe, I'm waiting to make the change in the tests until
  after making them in the code under test.

- Some forms of checks against constants produce unreachable code
  errors from mypy (https://github.com/python/mypy/issues/10773).
  This can be worked around, but I have not done that in this
  commit. Furthermore, the new mypy errors this produces--one on
  Windows, and one on non-Windows systems--could be fixed in a way
  that makes type annotations richer, by allowing the return type
  to be a literal on one platform or the other.
---
 git/config.py                 |  2 +-
 git/index/base.py             |  3 ++-
 git/index/fun.py              |  3 ++-
 git/objects/submodule/base.py |  3 ++-
 git/repo/base.py              |  7 ++++---
 git/util.py                   | 18 ++++++++++--------
 6 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/git/config.py b/git/config.py
index 78a88ba5e..4441c2187 100644
--- a/git/config.py
+++ b/git/config.py
@@ -246,7 +246,7 @@ 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 os.name == "nt" and config_level == "system":
+    if sys.platform == "win32" and config_level == "system":
         config_level = "global"
 
     if config_level == "system":
diff --git a/git/index/base.py b/git/index/base.py
index a95c3a622..704235241 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -13,6 +13,7 @@
 import os
 from stat import S_ISLNK
 import subprocess
+import sys
 import tempfile
 
 from git.compat import (
@@ -107,7 +108,7 @@ def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str,
         A context manager object that creates the file and provides its name on entry,
         and deletes it on exit.
     """
-    if os.name == "nt":
+    if sys.platform == "win32":
         fd, name = tempfile.mkstemp(dir=directory)
         os.close(fd)
         try:
diff --git a/git/index/fun.py b/git/index/fun.py
index 58335739e..60483c1f4 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -18,6 +18,7 @@
     S_IXUSR,
 )
 import subprocess
+import sys
 
 from git.cmd import handle_process_output, safer_popen
 from git.compat import defenc, force_bytes, force_text, safe_decode
@@ -99,7 +100,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
     env["GIT_EDITOR"] = ":"
     cmd = [hp]
     try:
-        if os.name == "nt" and not _has_file_extension(hp):
+        if sys.platform == "win32" and not _has_file_extension(hp):
             # Windows only uses extensions to determine how to open files
             # (doesn't understand shebangs). Try using bash to run the hook.
             relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index cdd7c8e1b..a7d638c59 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -7,6 +7,7 @@
 import os
 import os.path as osp
 import stat
+import sys
 import uuid
 
 import git
@@ -401,7 +402,7 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab
         """
         git_file = osp.join(working_tree_dir, ".git")
         rela_path = osp.relpath(module_abspath, start=working_tree_dir)
-        if os.name == "nt" and osp.isfile(git_file):
+        if sys.platform == "win32" and osp.isfile(git_file):
             os.remove(git_file)
         with open(git_file, "wb") as fp:
             fp.write(("gitdir: %s" % rela_path).encode(defenc))
diff --git a/git/repo/base.py b/git/repo/base.py
index 39a80b449..9b42ec85a 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -12,6 +12,7 @@
 from pathlib import Path
 import re
 import shlex
+import sys
 import warnings
 
 import gitdb
@@ -336,10 +337,10 @@ def close(self) -> None:
             # they are collected by the garbage collector, thus preventing deletion.
             # TODO: Find these references and ensure they are closed and deleted
             # synchronously rather than forcing a gc collection.
-            if os.name == "nt":
+            if sys.platform == "win32":
                 gc.collect()
             gitdb.util.mman.collect()
-            if os.name == "nt":
+            if sys.platform == "win32":
                 gc.collect()
 
     def __eq__(self, rhs: object) -> bool:
@@ -618,7 +619,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa
             git_dir = self.git_dir
         # We do not support an absolute path of the gitconfig on Windows.
         # Use the global config instead.
-        if os.name == "nt" and config_level == "system":
+        if sys.platform == "win32" and config_level == "system":
             config_level = "global"
 
         if config_level == "system":
diff --git a/git/util.py b/git/util.py
index d9e223c76..a0c1d61e5 100644
--- a/git/util.py
+++ b/git/util.py
@@ -117,7 +117,7 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
     :note:
         This only accesses the environment on Windows.
     """
-    if os.name != "nt":
+    if sys.platform != "win32":
         return False
 
     try:
@@ -223,7 +223,7 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
                 raise SkipTest(f"FIXME: fails with: PermissionError\n  {ex}") from ex
             raise
 
-    if os.name != "nt":
+    if sys.platform != "win32":
         shutil.rmtree(path)
     elif sys.version_info >= (3, 12):
         shutil.rmtree(path, onexc=handler)
@@ -235,7 +235,7 @@ def rmfile(path: PathLike) -> None:
     """Ensure file deleted also on *Windows* where read-only files need special
     treatment."""
     if osp.isfile(path):
-        if os.name == "nt":
+        if sys.platform == "win32":
             os.chmod(path, 0o777)
         os.remove(path)
 
@@ -276,7 +276,7 @@ def join_path(a: PathLike, *p: PathLike) -> PathLike:
     return path
 
 
-if os.name == "nt":
+if sys.platform == "win32":
 
     def to_native_path_windows(path: PathLike) -> PathLike:
         path = str(path)
@@ -328,7 +328,7 @@ def _get_exe_extensions() -> Sequence[str]:
     PATHEXT = os.environ.get("PATHEXT", None)
     if PATHEXT:
         return tuple(p.upper() for p in PATHEXT.split(os.pathsep))
-    elif os.name == "nt":
+    elif sys.platform == "win32":
         return (".BAT", "COM", ".EXE")
     else:
         return ()
@@ -354,7 +354,9 @@ def is_exec(fpath: str) -> bool:
         return (
             osp.isfile(fpath)
             and os.access(fpath, os.X_OK)
-            and (os.name != "nt" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts))
+            and (
+                sys.platform != "win32" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts)
+            )
         )
 
     progs = []
@@ -451,8 +453,8 @@ def is_cygwin_git(git_executable: PathLike) -> bool:
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if os.name == "nt":
-        # This is Windows-native Python, since Cygwin has os.name == "posix".
+    if sys.platform == "win32":
+        # This is Windows-native Python, since Cygwin has sys.platform == "cygwin".
         return False
 
     if git_executable is None:

From 7204c131fdcb4132e34a4f6d4c38af71083afdb1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 00:13:55 -0500
Subject: [PATCH 602/994] Fix new mypy error in _read_win_env_flag

As noted in 88557bc before this, that change added a couple of new
mypy errors about unreachable code, where it is intentionally
unreachable because it is for one platform and not another.

This fixes one of them, though a fix that incorporates more of what
can now be statically known about the return value based on the
platform may be preferable.
---
 git/util.py | 26 ++++++++++++++++----------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/git/util.py b/git/util.py
index a0c1d61e5..92f430777 100644
--- a/git/util.py
+++ b/git/util.py
@@ -107,19 +107,12 @@
 _logger = logging.getLogger(__name__)
 
 
-def _read_win_env_flag(name: str, default: bool) -> bool:
-    """Read a boolean flag from an environment variable on Windows.
+def _read_env_flag(name: str, default: bool) -> bool:
+    """Read a boolean flag from an environment variable.
 
     :return:
-        On Windows, the flag, or the `default` value if absent or ambiguous.
-        On all other operating systems, ``False``.
-
-    :note:
-        This only accesses the environment on Windows.
+        The flag, or the `default` value if absent or ambiguous.
     """
-    if sys.platform != "win32":
-        return False
-
     try:
         value = os.environ[name]
     except KeyError:
@@ -140,6 +133,19 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
     return default
 
 
+def _read_win_env_flag(name: str, default: bool) -> bool:
+    """Read a boolean flag from an environment variable on Windows.
+
+    :return:
+        On Windows, the flag, or the `default` value if absent or ambiguous.
+        On all other operating systems, ``False``.
+
+    :note:
+        This only accesses the environment on Windows.
+    """
+    return sys.platform == "win32" and _read_env_flag(name, default)
+
+
 #: We need an easy way to see if Appveyor TCs start failing,
 #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
 #: till then, we wish to hide them.

From 42e10c07436cd6186ffff1861c466838366fe9f0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 01:45:24 -0500
Subject: [PATCH 603/994] Fix new mypy error in is_cygwin_git

This fixes the second new mypy error that arose due to the change
in 88557bc. (7204c13 before this fixed the first of them.)
---
 git/util.py | 39 ++++++++++++++++++++-------------------
 1 file changed, 20 insertions(+), 19 deletions(-)

diff --git a/git/util.py b/git/util.py
index 92f430777..7ebce0c2c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -448,25 +448,7 @@ def decygpath(path: PathLike) -> str:
 _is_cygwin_cache: Dict[str, Optional[bool]] = {}
 
 
-@overload
-def is_cygwin_git(git_executable: None) -> Literal[False]:
-    ...
-
-
-@overload
-def is_cygwin_git(git_executable: PathLike) -> bool:
-    ...
-
-
-def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if sys.platform == "win32":
-        # This is Windows-native Python, since Cygwin has sys.platform == "cygwin".
-        return False
-
-    if git_executable is None:
-        return False
-
-    git_executable = str(git_executable)
+def _is_cygwin_git(git_executable: str) -> bool:
     is_cygwin = _is_cygwin_cache.get(git_executable)  # type: Optional[bool]
     if is_cygwin is None:
         is_cygwin = False
@@ -489,6 +471,25 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
     return is_cygwin
 
 
+@overload
+def is_cygwin_git(git_executable: None) -> Literal[False]:
+    ...
+
+
+@overload
+def is_cygwin_git(git_executable: PathLike) -> bool:
+    ...
+
+
+def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
+    if sys.platform == "win32":  # TODO: See if we can use `sys.platform != "cygwin"`.
+        return False
+    elif git_executable is None:
+        return False
+    else:
+        return _is_cygwin_git(str(git_executable))
+
+
 def get_user_id() -> str:
     """:return: String identifying the currently active system user as ``name@node``"""
     return "%s@%s" % (getpass.getuser(), platform.node())

From 465ab56b25261dc5830b3c4b91b39c7346c527df Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 01:52:35 -0500
Subject: [PATCH 604/994] Have test suite use sys.platform to check for Windows

See 88557bc on this change in general as well as the rationale for
making it in the tests. (It is alreay done in the code under test.)
---
 test/lib/helper.py     |  9 +++++----
 test/test_base.py      |  2 +-
 test/test_config.py    |  3 ++-
 test/test_diff.py      |  4 ++--
 test/test_git.py       | 10 +++++-----
 test/test_index.py     | 11 ++++++-----
 test/test_remote.py    | 10 +++++-----
 test/test_repo.py      |  6 +++---
 test/test_submodule.py |  4 ++--
 test/test_util.py      |  8 ++++----
 10 files changed, 35 insertions(+), 32 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 26469ed5d..0a6df242c 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -10,6 +10,7 @@
 import logging
 import os
 import os.path as osp
+import sys
 import tempfile
 import textwrap
 import time
@@ -176,7 +177,7 @@ def git_daemon_launched(base_path, ip, port):
 
     gd = None
     try:
-        if os.name == "nt":
+        if sys.platform == "win32":
             # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\,
             # but if invoked as 'git daemon', it detaches from parent `git` cmd,
             # and then CANNOT DIE!
@@ -200,7 +201,7 @@ def git_daemon_launched(base_path, ip, port):
                 as_process=True,
             )
         # Yes, I know... fortunately, this is always going to work if sleep time is just large enough.
-        time.sleep(1.0 if os.name == "nt" else 0.5)
+        time.sleep(1.0 if sys.platform == "win32" else 0.5)
     except Exception as ex:
         msg = textwrap.dedent(
             """
@@ -404,7 +405,7 @@ class VirtualEnvironment:
     __slots__ = ("_env_dir",)
 
     def __init__(self, env_dir, *, with_pip):
-        if os.name == "nt":
+        if sys.platform == "win32":
             self._env_dir = osp.realpath(env_dir)
             venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
         else:
@@ -432,7 +433,7 @@ def sources(self):
         return os.path.join(self.env_dir, "src")
 
     def _executable(self, basename):
-        if os.name == "nt":
+        if sys.platform == "win32":
             path = osp.join(self.env_dir, "Scripts", basename + ".exe")
         else:
             path = osp.join(self.env_dir, "bin", basename)
diff --git a/test/test_base.py b/test/test_base.py
index ef7486e86..e477b4837 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -130,7 +130,7 @@ def test_add_unicode(self, rw_repo):
         with open(file_path, "wb") as fp:
             fp.write(b"something")
 
-        if os.name == "nt":
+        if sys.platform == "win32":
             # On Windows, there is no way this works, see images on:
             # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
             # Therefore, it must be added using the Python implementation.
diff --git a/test/test_config.py b/test/test_config.py
index 4843d91eb..ac19a7fa8 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -7,6 +7,7 @@
 import io
 import os
 import os.path as osp
+import sys
 from unittest import mock
 
 import pytest
@@ -238,7 +239,7 @@ def check_test_value(cr, value):
             check_test_value(cr, tv)
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")',
         raises=AssertionError,
     )
diff --git a/test/test_diff.py b/test/test_diff.py
index 6d2f82649..96fbc60e3 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -4,9 +4,9 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import gc
-import os
 import os.path as osp
 import shutil
+import sys
 import tempfile
 
 import ddt
@@ -309,7 +309,7 @@ def test_diff_with_spaces(self):
         self.assertEqual(diff_index[0].b_path, "file with spaces", repr(diff_index[0].b_path))
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason='"Access is denied" when tearDown calls shutil.rmtree',
         raises=PermissionError,
     )
diff --git a/test/test_git.py b/test/test_git.py
index e1a8bda5e..dae0f6a39 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -74,7 +74,7 @@ def _fake_git(*version_info):
     fake_output = f"git version {fake_version} (fake)"
 
     with tempfile.TemporaryDirectory() as tdir:
-        if os.name == "nt":
+        if sys.platform == "win32":
             fake_git = Path(tdir, "fake-git.cmd")
             script = f"@echo {fake_output}\n"
             fake_git.write_text(script, encoding="utf-8")
@@ -215,7 +215,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
 
         repo = Repo.init(rw_dir)
 
-        if os.name == "nt":
+        if sys.platform == "win32":
             # Copy an actual binary executable that is not git. (On Windows, running
             # "hostname" only displays the hostname, it never tries to change it.)
             other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
@@ -228,7 +228,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
             os.chmod(impostor_path, 0o755)
 
         if use_shell_impostor:
-            shell_name = "cmd.exe" if os.name == "nt" else "sh"
+            shell_name = "cmd.exe" if sys.platform == "win32" else "sh"
             shutil.copy(impostor_path, Path(rw_dir, shell_name))
 
         with contextlib.ExitStack() as stack:
@@ -245,7 +245,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case):
         self.assertRegex(output, r"^git version\b")
 
     @skipUnless(
-        os.name == "nt",
+        sys.platform == "win32",
         "The regression only affected Windows, and this test logic is OS-specific.",
     )
     def test_it_avoids_upcasing_unrelated_environment_variable_names(self):
@@ -667,7 +667,7 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
             stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var}))
             stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
 
-            if os.name == "nt":
+            if sys.platform == "win32":
                 # On Windows, use a shell so "git" finds "git.cmd". (In the infrequent
                 # case that this effect is desired in production code, it should not be
                 # done with this technique. USE_SHELL=True is less secure and reliable,
diff --git a/test/test_index.py b/test/test_index.py
index fa64b82a2..622e7ca9a 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -14,6 +14,7 @@
 import shutil
 from stat import S_ISLNK, ST_MODE
 import subprocess
+import sys
 import tempfile
 
 import ddt
@@ -124,7 +125,7 @@ def check(cls):
         in System32; Popen finds it even if a shell would run another one, as on CI.
         (Without WSL, System32 may still have bash.exe; users sometimes put it there.)
         """
-        if os.name != "nt":
+        if sys.platform != "win32":
             return cls.Inapplicable()
 
         try:
@@ -561,7 +562,7 @@ def _count_existing(self, repo, files):
     # END num existing helper
 
     @pytest.mark.xfail(
-        os.name == "nt" and Git().config("core.symlinks") == "true",
+        sys.platform == "win32" and Git().config("core.symlinks") == "true",
         reason="Assumes symlinks are not created on Windows and opens a symlink to a nonexistent target.",
         raises=FileNotFoundError,
     )
@@ -754,7 +755,7 @@ def mixed_iterator():
         self.assertNotEqual(entries[0].hexsha, null_hex_sha)
 
         # Add symlink.
-        if os.name != "nt":
+        if sys.platform != "win32":
             for target in ("/etc/nonexisting", "/etc/passwd", "/etc"):
                 basename = "my_real_symlink"
 
@@ -812,7 +813,7 @@ def mixed_iterator():
         index.checkout(fake_symlink_path)
 
         # On Windows, we currently assume we will never get symlinks.
-        if os.name == "nt":
+        if sys.platform == "win32":
             # Symlinks should contain the link as text (which is what a
             # symlink actually is).
             with open(fake_symlink_path, "rt") as fd:
@@ -1043,7 +1044,7 @@ def test_run_commit_hook(self, rw_repo):
     def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         (chdir_to_repo,) = case
 
-        shell_name = "bash.exe" if os.name == "nt" else "sh"
+        shell_name = "bash.exe" if sys.platform == "win32" else "sh"
         maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext()
         repo = Repo.init(rw_dir)
 
diff --git a/test/test_remote.py b/test/test_remote.py
index c0bd11f80..5344a7324 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -4,10 +4,10 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import gc
-import os
 import os.path as osp
 from pathlib import Path
 import random
+import sys
 import tempfile
 from unittest import skipIf
 
@@ -769,7 +769,7 @@ def test_create_remote_unsafe_url(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value",
         raises=AssertionError,
     )
@@ -832,7 +832,7 @@ def test_fetch_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_fetch_unsafe_options must be adjusted in the "
@@ -900,7 +900,7 @@ def test_pull_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_pull_unsafe_options must be adjusted in the "
@@ -974,7 +974,7 @@ def test_push_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_push_unsafe_options must be adjusted in the "
diff --git a/test/test_repo.py b/test/test_repo.py
index 30a44b6c1..238f94712 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -309,7 +309,7 @@ def test_clone_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_clone_unsafe_options must be adjusted in the "
@@ -388,7 +388,7 @@ def test_clone_from_unsafe_options(self, rw_repo):
                 assert not tmp_file.exists()
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=(
             "File not created. A separate Windows command may be needed. This and the "
             "currently passing test test_clone_from_unsafe_options must be adjusted in the "
@@ -1389,7 +1389,7 @@ def test_do_not_strip_newline_in_stdout(self, rw_dir):
         self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), "hello\n")
 
     @pytest.mark.xfail(
-        os.name == "nt",
+        sys.platform == "win32",
         reason=R"fatal: could not create leading directories of '--upload-pack=touch C:\Users\ek\AppData\Local\Temp\tmpnantqizc\pwn': Invalid argument",  # noqa: E501
         raises=GitCommandError,
     )
diff --git a/test/test_submodule.py b/test/test_submodule.py
index 68164729b..ee7795dbb 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -987,7 +987,7 @@ def test_rename(self, rwdir):
         # This is needed to work around a PermissionError on Windows, resembling others,
         # except new in Python 3.12. (*Maybe* this could be due to changes in CPython's
         # garbage collector detailed in https://github.com/python/cpython/issues/97922.)
-        if os.name == "nt" and sys.version_info >= (3, 12):
+        if sys.platform == "win32" and sys.version_info >= (3, 12):
             gc.collect()
 
         new_path = "renamed/myname"
@@ -1071,7 +1071,7 @@ def test_branch_renames(self, rw_dir):
         assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset"
         assert sm_mod.head.ref.name == sm_pfb.name
 
-    @skipUnless(os.name == "nt", "Specifically for Windows.")
+    @skipUnless(sys.platform == "win32", "Specifically for Windows.")
     def test_to_relative_path_with_super_at_root_drive(self):
         class Repo:
             working_tree_dir = "D:\\"
diff --git a/test/test_util.py b/test/test_util.py
index 824b3ab3d..369896581 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -149,7 +149,7 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
         mocker.patch.object(pathlib.Path, "chmod")
 
     @pytest.mark.skipif(
-        os.name != "nt",
+        sys.platform != "win32",
         reason="PermissionError is only ever wrapped on Windows",
     )
     def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
@@ -168,7 +168,7 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
         "hide_windows_known_errors",
         [
             pytest.param(False),
-            pytest.param(True, marks=pytest.mark.skipif(os.name == "nt", reason="We would wrap on Windows")),
+            pytest.param(True, marks=pytest.mark.skipif(sys.platform == "win32", reason="We would wrap on Windows")),
         ],
     )
     def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
@@ -214,7 +214,7 @@ def _run_parse(name, value):
         return ast.literal_eval(output)
 
     @pytest.mark.skipif(
-        os.name != "nt",
+        sys.platform != "win32",
         reason="These environment variables are only used on Windows.",
     )
     @pytest.mark.parametrize(
@@ -410,7 +410,7 @@ def test_blocking_lock_file(self):
             elapsed = time.time() - start
 
         extra_time = 0.02
-        if os.name == "nt" or sys.platform == "cygwin":
+        if sys.platform in {"win32", "cygwin"}:
             extra_time *= 6  # Without this, we get indeterministic failures on Windows.
         elif sys.platform == "darwin":
             extra_time *= 18  # The situation on macOS is similar, but with more delay.

From ad8190bbb41db9056aefa1a29176b196c4edb466 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 03:14:35 -0400
Subject: [PATCH 605/994] Wrap docstrings and comments in _safer_popen_windows

It gained an indentation level in dc95a76, so its docstrings and
comments were no longer wrapped to 88 columns as most docstrings
and comments have been (absent a reason not to) since #1850.

This wraps it, but some parts may benefit from some other
adjustments.
---
 git/cmd.py | 61 ++++++++++++++++++++++++++++++------------------------
 1 file changed, 34 insertions(+), 27 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 9fd2c532d..d17c8836b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -232,47 +232,54 @@ def _safer_popen_windows(
         env: Optional[Mapping[str, str]] = None,
         **kwargs: Any,
     ) -> Popen:
-        """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
-
-        This avoids an untrusted search path condition where a file like ``git.exe`` in a
-        malicious repository would be run when GitPython operates on the repository. The
-        process using GitPython may have an untrusted repository's working tree as its
-        current working directory. Some operations may temporarily change to that directory
-        before running a subprocess. In addition, while by default GitPython does not run
-        external commands with a shell, it can be made to do so, in which case the CWD of
-        the subprocess, which GitPython usually sets to a repository working tree, can
-        itself be searched automatically by the shell. This wrapper covers all those cases.
+        """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the
+        search.
+
+        This avoids an untrusted search path condition where a file like ``git.exe`` in
+        a malicious repository would be run when GitPython operates on the repository.
+        The process using GitPython may have an untrusted repository's working tree as
+        its current working directory. Some operations may temporarily change to that
+        directory before running a subprocess. In addition, while by default GitPython
+        does not run external commands with a shell, it can be made to do so, in which
+        case the CWD of the subprocess, which GitPython usually sets to a repository
+        working tree, can itself be searched automatically by the shell. This wrapper
+        covers all those cases.
 
         :note:
-            This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath`
-            environment variable during subprocess creation. It also takes care of passing
-            Windows-specific process creation flags, but that is unrelated to path search.
+            This currently works by setting the
+            :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during
+            subprocess creation. It also takes care of passing Windows-specific process
+            creation flags, but that is unrelated to path search.
 
         :note:
             The current implementation contains a race condition on :attr:`os.environ`.
-            GitPython isn't thread-safe, but a program using it on one thread should ideally
-            be able to mutate :attr:`os.environ` on another, without unpredictable results.
-            See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
+            GitPython isn't thread-safe, but a program using it on one thread should
+            ideally be able to mutate :attr:`os.environ` on another, without
+            unpredictable results. See comments in:
+            https://github.com/gitpython-developers/GitPython/pull/1650
         """
-        # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
+        # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards.
         # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
         # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
         creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
 
-        # When using a shell, the shell is the direct subprocess, so the variable must be
-        # set in its environment, to affect its search behavior. (The "1" can be any value.)
+        # When using a shell, the shell is the direct subprocess, so the variable must
+        # be set in its environment, to affect its search behavior. (The "1" can be any
+        # value.)
         if shell:
-            # The original may be immutable or reused by the caller. Make changes in a copy.
+            # The original may be immutable or reused by the caller. Make changes in a
+            # copy.
             env = {} if env is None else dict(env)
             env["NoDefaultCurrentDirectoryInExePath"] = "1"
 
-        # When not using a shell, the current process does the search in a CreateProcessW
-        # API call, so the variable must be set in our environment. With a shell, this is
-        # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
-        # patched. If that is unpatched, then in the rare case the ComSpec environment
-        # variable is unset, the search for the shell itself is unsafe. Setting
-        # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and
-        # protects against that. (As above, the "1" can be any value.)
+        # When not using a shell, the current process does the search in a
+        # CreateProcessW API call, so the variable must be set in our environment. With
+        # a shell, this is unnecessary, in versions where
+        # https://github.com/python/cpython/issues/101283 is patched. If that is
+        # unpatched, then in the rare case the ComSpec environment variable is unset,
+        # the search for the shell itself is unsafe. Setting
+        # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler
+        # and protects against that. (As above, the "1" can be any value.)
         with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
             return Popen(
                 command,

From b9d9e5642f84e7796b5242d155df7bdc28413303 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 03:19:28 -0400
Subject: [PATCH 606/994] Further improve _safer_popen_windows doc

This adjusts the formatting of the _safer_popen_windows docstring
and comments, and rewords some parts for clarity and brevity.
---
 git/cmd.py | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index d17c8836b..85398497f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -264,22 +264,19 @@ def _safer_popen_windows(
         creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
 
         # When using a shell, the shell is the direct subprocess, so the variable must
-        # be set in its environment, to affect its search behavior. (The "1" can be any
-        # value.)
+        # be set in its environment, to affect its search behavior.
         if shell:
-            # The original may be immutable or reused by the caller. Make changes in a
-            # copy.
+            # The original may be immutable, or the caller may reuse it. Mutate a copy.
             env = {} if env is None else dict(env)
-            env["NoDefaultCurrentDirectoryInExePath"] = "1"
+            env["NoDefaultCurrentDirectoryInExePath"] = "1"  # The "1" can be an value.
 
         # When not using a shell, the current process does the search in a
         # CreateProcessW API call, so the variable must be set in our environment. With
-        # a shell, this is unnecessary, in versions where
-        # https://github.com/python/cpython/issues/101283 is patched. If that is
-        # unpatched, then in the rare case the ComSpec environment variable is unset,
-        # the search for the shell itself is unsafe. Setting
-        # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler
-        # and protects against that. (As above, the "1" can be any value.)
+        # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
+        # is patched. In Python versions where it is unpatched, and in the rare case the
+        # ComSpec environment variable is unset, the search for the shell itself is
+        # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
+        # is simpler and protects against that. (As above, the "1" can be any value.)
         with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
             return Popen(
                 command,

From 04a27531bf1d434a8da9d17298058d1f860fe019 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 03:36:32 -0400
Subject: [PATCH 607/994] Temporarily rename Commit_ish to Old_commit_ish

And Lit_commit_ish to Lit_old_commit_ish.

This is temporary, and only for purposes of bookkeeping while this
type is split into two types.

Specifically, Old_commit_ish will go away soon, because each
occurrence of it will be changed to one of:

- A newly redefined Commit_ish union of types representing only git
  object types that are sometimes commit-ish.

- A newly introduced union (using some name formerly not used in
  GitPython) of all four types representing git object types, the
  same types as the old Commit_ish, here renamed Old_commit_ish,
  had overbroadly covered.

- Perhaps in some cases something else.

For context, see #1858 (including comments), and comments in #1859.
---
 git/index/base.py             |  4 ++--
 git/objects/base.py           | 12 ++++++------
 git/objects/submodule/base.py | 16 ++++++++--------
 git/objects/submodule/root.py |  4 ++--
 git/refs/head.py              |  4 ++--
 git/refs/reference.py         |  4 ++--
 git/refs/symbolic.py          |  8 ++++----
 git/refs/tag.py               |  4 ++--
 git/remote.py                 | 10 +++++-----
 git/repo/base.py              |  8 ++++----
 git/repo/fun.py               | 12 ++++++------
 git/types.py                  |  8 ++++----
 12 files changed, 47 insertions(+), 47 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 704235241..668607772 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -80,7 +80,7 @@
     Union,
 )
 
-from git.types import Commit_ish, Literal, PathLike
+from git.types import Old_commit_ish, Literal, PathLike
 
 if TYPE_CHECKING:
     from subprocess import Popen
@@ -1127,7 +1127,7 @@ def move(
     def commit(
         self,
         message: str,
-        parent_commits: Union[Commit_ish, None] = None,
+        parent_commits: Union[Old_commit_ish, None] = None,
         head: bool = True,
         author: Union[None, "Actor"] = None,
         committer: Union[None, "Actor"] = None,
diff --git a/git/objects/base.py b/git/objects/base.py
index df9b2d9f1..dec07654a 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -16,7 +16,7 @@
 
 from typing import Any, TYPE_CHECKING, Union
 
-from git.types import PathLike, Commit_ish, Lit_commit_ish
+from git.types import PathLike, Old_commit_ish, Lit_old_commit_ish
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -53,7 +53,7 @@ class Object(LazyMixin):
     * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
 
     :note:
-        See the :class:`~git.types.Commit_ish` union type.
+        See the :class:`~git.types.Old_commit_ish` union type.
 
     :note:
         :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
@@ -77,7 +77,7 @@ class Object(LazyMixin):
 
     __slots__ = ("repo", "binsha", "size")
 
-    type: Union[Lit_commit_ish, None] = None
+    type: Union[Lit_old_commit_ish, None] = None
     """String identifying (a concrete :class:`Object` subtype for) a git object type.
 
     The subtypes that this may name correspond to the kinds of git objects that exist,
@@ -90,7 +90,7 @@ class Object(LazyMixin):
         ``None`` in concrete leaf subclasses representing specific git object types.
 
     :note:
-        See also :class:`~git.types.Commit_ish`.
+        See also :class:`~git.types.Old_commit_ish`.
     """
 
     def __init__(self, repo: "Repo", binsha: bytes):
@@ -113,7 +113,7 @@ def __init__(self, repo: "Repo", binsha: bytes):
         )
 
     @classmethod
-    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
+    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Old_commit_ish:
         """
         :return:
             New :class:`Object` instance of a type appropriate to the object type behind
@@ -130,7 +130,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish:
         return repo.rev_parse(str(id))
 
     @classmethod
-    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish:
+    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Old_commit_ish:
         """
         :return:
             New object instance of a type appropriate to represent the given binary sha1
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index a7d638c59..783990664 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -45,7 +45,7 @@
 from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
 from typing import Any, Iterator, Union
 
-from git.types import Commit_ish, Literal, PathLike, TBD
+from git.types import Old_commit_ish, Literal, PathLike, TBD
 
 if TYPE_CHECKING:
     from git.index import IndexFile
@@ -112,7 +112,7 @@ def __init__(
         mode: Union[int, None] = None,
         path: Union[PathLike, None] = None,
         name: Union[str, None] = None,
-        parent_commit: Union[Commit_ish, None] = None,
+        parent_commit: Union[Old_commit_ish, None] = None,
         url: Union[str, None] = None,
         branch_path: Union[PathLike, None] = None,
     ) -> None:
@@ -213,7 +213,7 @@ def __repr__(self) -> str:
 
     @classmethod
     def _config_parser(
-        cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool
+        cls, repo: "Repo", parent_commit: Union[Old_commit_ish, None], read_only: bool
     ) -> SubmoduleConfigParser:
         """
         :return:
@@ -264,7 +264,7 @@ def _clear_cache(self) -> None:
         # END for each name to delete
 
     @classmethod
-    def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
+    def _sio_modules(cls, parent_commit: Old_commit_ish) -> BytesIO:
         """
         :return:
             Configuration file as :class:`~io.BytesIO` - we only access it through the
@@ -277,7 +277,7 @@ def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO:
     def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
         """:return: Config parser constrained to our submodule in read or write mode"""
         try:
-            pc: Union["Commit_ish", None] = self.parent_commit
+            pc: Union["Old_commit_ish", None] = self.parent_commit
         except ValueError:
             pc = None
         # END handle empty parent repository
@@ -1242,7 +1242,7 @@ def remove(
 
         return self
 
-    def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) -> "Submodule":
+    def set_parent_commit(self, commit: Union[Old_commit_ish, None], check: bool = True) -> "Submodule":
         """Set this instance to use the given commit whose tree is supposed to
         contain the ``.gitmodules`` blob.
 
@@ -1495,7 +1495,7 @@ def url(self) -> str:
         return self._url
 
     @property
-    def parent_commit(self) -> "Commit_ish":
+    def parent_commit(self) -> "Old_commit_ish":
         """
         :return:
             :class:`~git.objects.commit.Commit` instance with the tree containing the
@@ -1557,7 +1557,7 @@ def children(self) -> IterableList["Submodule"]:
     def iter_items(
         cls,
         repo: "Repo",
-        parent_commit: Union[Commit_ish, str] = "HEAD",
+        parent_commit: Union[Old_commit_ish, str] = "HEAD",
         *Args: Any,
         **kwargs: Any,
     ) -> Iterator["Submodule"]:
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index d0d1818a9..8e697ee1d 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -12,7 +12,7 @@
 
 from typing import TYPE_CHECKING, Union
 
-from git.types import Commit_ish
+from git.types import Old_commit_ish
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -77,7 +77,7 @@ def _clear_cache(self) -> None:
 
     def update(  # type: ignore[override]
         self,
-        previous_commit: Union[Commit_ish, None] = None,
+        previous_commit: Union[Old_commit_ish, None] = None,
         recursive: bool = True,
         force_remove: bool = False,
         init: bool = True,
diff --git a/git/refs/head.py b/git/refs/head.py
index 4da614abd..e6bef598a 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -17,7 +17,7 @@
 
 from typing import Any, Sequence, Union, TYPE_CHECKING
 
-from git.types import PathLike, Commit_ish
+from git.types import PathLike, Old_commit_ish
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -62,7 +62,7 @@ def orig_head(self) -> SymbolicReference:
 
     def reset(
         self,
-        commit: Union[Commit_ish, SymbolicReference, str] = "HEAD",
+        commit: Union[Old_commit_ish, SymbolicReference, str] = "HEAD",
         index: bool = True,
         working_tree: bool = False,
         paths: Union[PathLike, Sequence[PathLike], None] = None,
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 2da511ce3..99366f710 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -11,7 +11,7 @@
 # typing ------------------------------------------------------------------
 
 from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING
-from git.types import Commit_ish, PathLike, _T
+from git.types import Old_commit_ish, PathLike, _T
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -81,7 +81,7 @@ def __str__(self) -> str:
     # @ReservedAssignment
     def set_object(
         self,
-        object: Union[Commit_ish, "SymbolicReference", str],
+        object: Union[Old_commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "Reference":
         """Special version which checks if the head-log needs an update as well.
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 142952e06..092824ff8 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -31,7 +31,7 @@
     TYPE_CHECKING,
     cast,
 )
-from git.types import Commit_ish, PathLike
+from git.types import Old_commit_ish, PathLike
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -278,7 +278,7 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T
         """
         return cls._get_ref_info_helper(repo, ref_path)
 
-    def _get_object(self) -> Commit_ish:
+    def _get_object(self) -> Old_commit_ish:
         """
         :return:
             The object our ref currently refers to. Refs can be cached, they will always
@@ -345,7 +345,7 @@ def set_commit(
 
     def set_object(
         self,
-        object: Union[Commit_ish, "SymbolicReference", str],
+        object: Union[Old_commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set the object we point to, possibly dereference our symbolic reference
@@ -404,7 +404,7 @@ def _get_reference(self) -> "SymbolicReference":
 
     def set_reference(
         self,
-        ref: Union[Commit_ish, "SymbolicReference", str],
+        ref: Union[Old_commit_ish, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set ourselves to the given `ref`.
diff --git a/git/refs/tag.py b/git/refs/tag.py
index 6a6dad09a..0cf45cf20 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -15,7 +15,7 @@
 # typing ------------------------------------------------------------------
 
 from typing import Any, Type, Union, TYPE_CHECKING
-from git.types import Commit_ish, PathLike
+from git.types import Old_commit_ish, PathLike
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -82,7 +82,7 @@ def tag(self) -> Union["TagObject", None]:
 
     # Make object read-only. It should be reasonably hard to adjust an existing tag.
     @property
-    def object(self) -> Commit_ish:  # type: ignore[override]
+    def object(self) -> Old_commit_ish:  # type: ignore[override]
         return Reference._get_object(self)
 
     @classmethod
diff --git a/git/remote.py b/git/remote.py
index 2bd674199..cebfafb7b 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -41,7 +41,7 @@
     overload,
 )
 
-from git.types import PathLike, Literal, Commit_ish
+from git.types import PathLike, Literal, Old_commit_ish
 
 if TYPE_CHECKING:
     from git.repo.base import Repo
@@ -196,7 +196,7 @@ def __init__(
         self.summary = summary
 
     @property
-    def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]:
+    def old_commit(self) -> Union[str, SymbolicReference, Old_commit_ish, None]:
         return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
 
     @property
@@ -362,7 +362,7 @@ def __init__(
         ref: SymbolicReference,
         flags: int,
         note: str = "",
-        old_commit: Union[Commit_ish, None] = None,
+        old_commit: Union[Old_commit_ish, None] = None,
         remote_ref_path: Optional[PathLike] = None,
     ) -> None:
         """Initialize a new instance."""
@@ -381,7 +381,7 @@ def name(self) -> str:
         return self.ref.name
 
     @property
-    def commit(self) -> Commit_ish:
+    def commit(self) -> Old_commit_ish:
         """:return: Commit of our remote ref"""
         return self.ref.commit
 
@@ -438,7 +438,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
 
         # Parse operation string for more info.
         # This makes no sense for symbolic refs, but we parse it anyway.
-        old_commit: Union[Commit_ish, None] = None
+        old_commit: Union[Old_commit_ish, None] = None
         is_tag_operation = False
         if "rejected" in operation:
             flags |= cls.REJECTED
diff --git a/git/repo/base.py b/git/repo/base.py
index 9b42ec85a..62f458f68 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -55,7 +55,7 @@
     TBD,
     PathLike,
     Lit_config_levels,
-    Commit_ish,
+    Old_commit_ish,
     CallableProgress,
     Tree_ish,
     assert_never,
@@ -696,7 +696,7 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo
         """
         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:
+    def commit(self, rev: Union[str, Old_commit_ish, None] = None) -> Commit:
         """The :class:`~git.objects.commit.Commit` object for the specified revision.
 
         :param rev:
@@ -772,7 +772,7 @@ def iter_commits(
 
         return Commit.iter_items(self, rev, paths, **kwargs)
 
-    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
+    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Old_commit_ish, None]]:
         R"""Find the closest common ancestor for the given revision
         (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
         :class:`~git.refs.reference.Reference`\s, etc.).
@@ -797,7 +797,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]:
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
         # END handle input
 
-        res: List[Union[Commit_ish, None]] = []
+        res: List[Union[Old_commit_ish, None]] = []
         try:
             lines = self.git.merge_base(*rev, **kwargs).splitlines()  # List[str]
         except GitCommandError as err:
diff --git a/git/repo/fun.py b/git/repo/fun.py
index e3c69c68c..6bc998bb7 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -25,7 +25,7 @@
 # Typing ----------------------------------------------------------------------
 
 from typing import Union, Optional, cast, TYPE_CHECKING
-from git.types import Commit_ish
+from git.types import Old_commit_ish
 
 if TYPE_CHECKING:
     from git.types import PathLike
@@ -249,7 +249,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         raise NotImplementedError("commit by message search (regex)")
     # END handle search
 
-    obj: Union[Commit_ish, "Reference", None] = None
+    obj: Union[Old_commit_ish, "Reference", None] = None
     ref = None
     output_type = "commit"
     start = 0
@@ -271,7 +271,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 if token == "@":
                     ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
                 else:
-                    obj = cast(Commit_ish, name_to_object(repo, rev[:start]))
+                    obj = cast(Old_commit_ish, name_to_object(repo, rev[:start]))
                 # END handle token
             # END handle refname
         else:
@@ -296,7 +296,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 pass  # Default.
             elif output_type == "tree":
                 try:
-                    obj = cast(Commit_ish, obj)
+                    obj = cast(Old_commit_ish, obj)
                     obj = to_commit(obj).tree
                 except (AttributeError, ValueError):
                     pass  # Error raised later.
@@ -369,7 +369,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         parsed_to = start
         # Handle hierarchy walk.
         try:
-            obj = cast(Commit_ish, obj)
+            obj = cast(Old_commit_ish, obj)
             if token == "~":
                 obj = to_commit(obj)
                 for _ in range(num):
@@ -398,7 +398,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
 
     # Still no obj? It's probably a simple name.
     if obj is None:
-        obj = cast(Commit_ish, name_to_object(repo, rev))
+        obj = cast(Old_commit_ish, name_to_object(repo, rev))
         parsed_to = lr
     # END handle simple name
 
diff --git a/git/types.py b/git/types.py
index 8f71d079a..e9ef48ace 100644
--- a/git/types.py
+++ b/git/types.py
@@ -61,7 +61,7 @@
     commit), is not covered as part of this union type.
 """
 
-Commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
+Old_commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
 """Union of the :class:`~git.objects.base.Object`-based types that represent git object
 types. This union is often usable where a commit-ish is expected, but is not actually
 limited to types representing commit-ish git objects.
@@ -86,18 +86,18 @@
     compatibility.
 """
 
-Lit_commit_ish = Literal["commit", "tag", "blob", "tree"]
+Lit_old_commit_ish = Literal["commit", "tag", "blob", "tree"]
 """Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes
 representing git object types.
 
 See the :class:`Object.type <git.objects.base.Object.type>` attribute.
 
 :note:
-    See also :class:`Commit_ish`, a union of the the :class:`~git.objects.base.Object`
+    See also :class:`Old_commit_ish`, a union of the the :class:`~git.objects.base.Object`
     subtypes associated with these literal strings.
 
 :note:
-    As noted in :class:`Commit_ish`, this is not limited to types of git objects that
+    As noted in :class:`Old_commit_ish`, this is not limited to types of git objects that
     are actually commit-ish.
 """
 

From 787f65cc7d80cd4fb73b37847718a32473b5d985 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 12:44:37 -0400
Subject: [PATCH 608/994] Define and document AnyGitObject and (new) Commit_ish

These provide for the changes planned in 04a2753 before this (at
least the first two cases listed). But annotations still need to
be changed to use these types.
---
 git/types.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 55 insertions(+), 1 deletion(-)

diff --git a/git/types.py b/git/types.py
index e9ef48ace..1ef2b1523 100644
--- a/git/types.py
+++ b/git/types.py
@@ -46,6 +46,33 @@
 _T = TypeVar("_T")
 """Type variable used internally in GitPython."""
 
+AnyGitObject = Union["Commit", "Tree", "TagObject", "Blob"]
+"""Union of the :class:`~git.objects.base.Object`-based types that represent actual git
+object types.
+
+As noted in :class:`~git.objects.base.Object`, which has further details, these are:
+
+* :class:`Blob <git.objects.blob.Blob>`
+* :class:`Tree <git.objects.tree.Tree>`
+* :class:`Commit <git.objects.commit.Commit>`
+* :class:`TagObject <git.objects.tag.TagObject>`
+
+Those GitPython classes represent the four git object types, per gitglossary(7):
+
+* "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+* "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
+* "commit object": https://git-scm.com/docs/gitglossary#def_commit_object
+* "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
+
+For more general information on git objects and their types as git understands them:
+
+* "object": https://git-scm.com/docs/gitglossary#def_object
+* "object type": https://git-scm.com/docs/gitglossary#def_object_type
+
+:note:
+    See also the :class:`Tree_ish` and :class:`Commit_ish` unions.
+"""
+
 Tree_ish = Union["Commit", "Tree"]
 """Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish.
 
@@ -54,13 +81,40 @@
 :note:
     This union comprises **only** the :class:`~git.objects.commit.Commit` and
     :class:`~git.objects.tree.Tree` classes, **all** of whose instances are tree-ish.
-    This is done because of the way GitPython uses it as a static type annotation.
+    This has been done because of the way GitPython uses it as a static type annotation.
 
     :class:`~git.objects.tag.TagObject`, some but not all of whose instances are
     tree-ish (those representing git tag objects that ultimately resolve to a tree or
     commit), is not covered as part of this union type.
+
+:note:
+    See also the :class:`AnyGitObject` union of all four classes corresponding to git
+    object types.
+"""
+
+Commit_ish = Union["Commit", "TagObject"]
+"""Union of :class:`~git.objects.base.Object`-based types that are sometimes commit-ish.
+
+See gitglossary(7) on "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish
+
+:note:
+    :class:`~git.objects.commit.Commit` is the only class whose instances are all
+    commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also
+    :class:`~git.objects.tag.TagObject`, only **some** of whose instances are
+    commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels
+    (recursively dereferences) to a commit can in general only be known at runtime.
+
+:note:
+    This is an inversion of the situation with :class:`Tree_ish`. This union is broader
+    than all commit-ish objects, while :class:`Tree_ish` is narrower than all tree-ish
+    objects.
+
+:note:
+    See also the :class:`AnyGitObject` union of all four classes corresponding to git
+    object types.
 """
 
+# FIXME: Replace uses with AnyGitObject and Commit_ish, and remove this.
 Old_commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
 """Union of the :class:`~git.objects.base.Object`-based types that represent git object
 types. This union is often usable where a commit-ish is expected, but is not actually

From 1fe4dc88dcab2a61608c247853a502f7c5186d4b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 14:09:13 -0400
Subject: [PATCH 609/994] Define GitObjectTypeString and update Object to use
 it

This is equivalent to the old Lit_commit_ish union; it is the type
of a literal string that has one of the four values that represent
actual git object types. The old Lit_commit_ish union was only used
in one place in GitPython: to annotate Object.type.

This replaces that and updates its docstring, as well as the Object
class docstring.

(One change in the Object class docstring--adding a mention of the
RootModule subclass of Submodule--is not conceptually related to
these other changes.)
---
 git/objects/base.py | 14 ++++++++------
 git/types.py        | 14 ++++++++++++++
 2 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index dec07654a..3b2514e9b 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -16,7 +16,7 @@
 
 from typing import Any, TYPE_CHECKING, Union
 
-from git.types import PathLike, Old_commit_ish, Lit_old_commit_ish
+from git.types import GitObjectTypeString, Old_commit_ish, PathLike
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -36,7 +36,7 @@
 class Object(LazyMixin):
     """Base class for classes representing git object types.
 
-    The following leaf classes represent specific kinds of git objects:
+    The following four leaf classes represent specific kinds of git objects:
 
     * :class:`Blob <git.objects.blob.Blob>`
     * :class:`Tree <git.objects.tree.Tree>`
@@ -53,12 +53,14 @@ class Object(LazyMixin):
     * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object
 
     :note:
-        See the :class:`~git.types.Old_commit_ish` union type.
+        See the :class:`~git.types.AnyGitObject` union type of the four leaf subclasses
+        that represent actual git object types.
 
     :note:
         :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy
         rooted at this :class:`Object` class, even though submodules are not really a
-        type of git object.
+        type of git object. (This also applies to its
+        :class:`~git.objects.submodule.root.RootModule` subclass.)
 
     :note:
         This :class:`Object` class should not be confused with :class:`object` (the root
@@ -77,7 +79,7 @@ class Object(LazyMixin):
 
     __slots__ = ("repo", "binsha", "size")
 
-    type: Union[Lit_old_commit_ish, None] = None
+    type: Union[GitObjectTypeString, None] = None
     """String identifying (a concrete :class:`Object` subtype for) a git object type.
 
     The subtypes that this may name correspond to the kinds of git objects that exist,
@@ -90,7 +92,7 @@ class Object(LazyMixin):
         ``None`` in concrete leaf subclasses representing specific git object types.
 
     :note:
-        See also :class:`~git.types.Old_commit_ish`.
+        See also :class:`~git.types.GitObjectTypeString`.
     """
 
     def __init__(self, repo: "Repo", binsha: bytes):
diff --git a/git/types.py b/git/types.py
index 1ef2b1523..95a5816f3 100644
--- a/git/types.py
+++ b/git/types.py
@@ -114,6 +114,18 @@
     object types.
 """
 
+GitObjectTypeString = Literal["commit", "tag", "blob", "tree"]
+"""Literal strings identifying git object types and the
+:class:`~git.objects.base.Object`-based types that represent them.
+
+See the :attr:`Object.type <git.objects.base.Object.type>` attribute. These are its
+values in :class:`~git.objects.base.Object` subclasses that represent git objects. These
+literals therefore correspond to the types in the :class:`AnyGitObject` union.
+
+These are the same strings git itself uses to identify its four object types. See
+gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type
+"""
+
 # FIXME: Replace uses with AnyGitObject and Commit_ish, and remove this.
 Old_commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
 """Union of the :class:`~git.objects.base.Object`-based types that represent git object
@@ -140,6 +152,8 @@
     compatibility.
 """
 
+# FIXME: After replacing the one use with GitObjectTypeString, define Lit_commit_ish
+#        somehow (it is a breaking change to remove it entirely). Maybe deprecate it.
 Lit_old_commit_ish = Literal["commit", "tag", "blob", "tree"]
 """Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes
 representing git object types.

From 7328a00170c20425760b0c20212042c1b884cb5b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 16:51:01 -0400
Subject: [PATCH 610/994] Start fixing annotations that used the old Commit_ish

---
 git/objects/base.py  | 18 +++++++++-------
 git/refs/symbolic.py | 50 +++++++++++++++++++++++---------------------
 git/refs/tag.py      |  9 ++++----
 git/remote.py        |  8 +++----
 git/repo/base.py     | 38 ++++++++++++++++-----------------
 5 files changed, 63 insertions(+), 60 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 3b2514e9b..90e20c2b6 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -3,12 +3,12 @@
 # This module is part of GitPython and is released under the
 # 3-Clause 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
-
 import gitdb.typ as dbtyp
 import os.path as osp
 
+from git.exc import WorkTreeRepositoryUnsupported
+from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex
+
 from .util import get_object_type_by_name
 
 
@@ -16,15 +16,17 @@
 
 from typing import Any, TYPE_CHECKING, Union
 
-from git.types import GitObjectTypeString, Old_commit_ish, PathLike
+from git.types import AnyGitObject, GitObjectTypeString, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from gitdb.base import OStream
+
+    from git.refs.reference import Reference
+    from git.repo import Repo
+
     from .tree import Tree
     from .blob import Blob
     from .submodule.base import Submodule
-    from git.refs.reference import Reference
 
 IndexObjUnion = Union["Tree", "Blob", "Submodule"]
 
@@ -115,7 +117,7 @@ def __init__(self, repo: "Repo", binsha: bytes):
         )
 
     @classmethod
-    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Old_commit_ish:
+    def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
         """
         :return:
             New :class:`Object` instance of a type appropriate to the object type behind
@@ -132,7 +134,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Old_commit_ish:
         return repo.rev_parse(str(id))
 
     @classmethod
-    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Old_commit_ish:
+    def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject:
         """
         :return:
             New object instance of a type appropriate to represent the given binary sha1
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 092824ff8..4a39875a7 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -6,17 +6,16 @@
 from git.compat import defenc
 from git.objects import Object
 from git.objects.commit import Commit
+from git.refs.log import RefLog
 from git.util import (
+    LockedFD,
+    assure_directory_exists,
+    hex_to_bin,
     join_path,
     join_path_native,
     to_native_path_linux,
-    assure_directory_exists,
-    hex_to_bin,
-    LockedFD,
 )
-from gitdb.exc import BadObject, BadName
-
-from .log import RefLog
+from gitdb.exc import BadName, BadObject
 
 # typing ------------------------------------------------------------------
 
@@ -24,21 +23,21 @@
     Any,
     Iterator,
     List,
+    TYPE_CHECKING,
     Tuple,
     Type,
     TypeVar,
     Union,
-    TYPE_CHECKING,
     cast,
 )
-from git.types import Old_commit_ish, PathLike
+from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
-    from git.refs import Head, TagReference, RemoteReference, Reference
-    from .log import RefLogEntry
     from git.config import GitConfigParser
     from git.objects.commit import Actor
+    from git.refs import Head, TagReference, RemoteReference, Reference
+    from git.refs.log import RefLogEntry
+    from git.repo import Repo
 
 
 T_References = TypeVar("T_References", bound="SymbolicReference")
@@ -278,7 +277,7 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T
         """
         return cls._get_ref_info_helper(repo, ref_path)
 
-    def _get_object(self) -> Old_commit_ish:
+    def _get_object(self) -> AnyGitObject:
         """
         :return:
             The object our ref currently refers to. Refs can be cached, they will always
@@ -345,7 +344,7 @@ def set_commit(
 
     def set_object(
         self,
-        object: Union[Old_commit_ish, "SymbolicReference", str],
+        object: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set the object we point to, possibly dereference our symbolic reference
@@ -353,9 +352,12 @@ def set_object(
 
         :param object:
             A refspec, a :class:`SymbolicReference` or an
-            :class:`~git.objects.base.Object` instance. :class:`SymbolicReference`
-            instances will be dereferenced beforehand to obtain the object they point
-            to.
+            :class:`~git.objects.base.Object` instance.
+
+            * :class:`SymbolicReference` instances will be dereferenced beforehand to
+              obtain the git object they point to.
+            * :class:`~git.objects.base.Object` instances must represent git objects
+              (:class:`~git.types.AnyGitObject`).
 
         :param logmsg:
             If not ``None``, the message will be used in the reflog entry to be written.
@@ -404,22 +406,22 @@ def _get_reference(self) -> "SymbolicReference":
 
     def set_reference(
         self,
-        ref: Union[Old_commit_ish, "SymbolicReference", str],
+        ref: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "SymbolicReference":
         """Set ourselves to the given `ref`.
 
-        It will stay a symbol if the ref is a :class:`~git.refs.reference.Reference`.
+        It will stay a symbol if the `ref` is a :class:`~git.refs.reference.Reference`.
 
-        Otherwise an Object, given as :class:`~git.objects.base.Object` instance or
-        refspec, is assumed and if valid, will be set which effectively detaches the
-        reference if it was a purely symbolic one.
+        Otherwise a git object, specified as a :class:`~git.objects.base.Object`
+        instance or refspec, is assumed. If it is valid, this reference will be set to
+        it, which effectively detaches the reference if it was a purely symbolic one.
 
         :param ref:
             A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object`
-            instance, or a refspec string. Only if the ref is a
-            :class:`SymbolicReference` instance, we will point to it. Everything else is
-            dereferenced to obtain the actual object.
+            instance (specifically an :class:`~git.types.AnyGitObject`), or a refspec
+            string. Only if the ref is a :class:`SymbolicReference` instance, we will
+            point to it. Everything else is dereferenced to obtain the actual object.
 
         :param logmsg:
             If set to a string, the message will be used in the reflog.
diff --git a/git/refs/tag.py b/git/refs/tag.py
index 0cf45cf20..a1d0b470f 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -14,14 +14,15 @@
 
 # typing ------------------------------------------------------------------
 
-from typing import Any, Type, Union, TYPE_CHECKING
-from git.types import Old_commit_ish, PathLike
+from typing import Any, TYPE_CHECKING, Type, Union
+
+from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit
     from git.objects import TagObject
     from git.refs import SymbolicReference
+    from git.repo import Repo
 
 
 # ------------------------------------------------------------------------------
@@ -82,7 +83,7 @@ def tag(self) -> Union["TagObject", None]:
 
     # Make object read-only. It should be reasonably hard to adjust an existing tag.
     @property
-    def object(self) -> Old_commit_ish:  # type: ignore[override]
+    def object(self) -> AnyGitObject:  # type: ignore[override]
         return Reference._get_object(self)
 
     @classmethod
diff --git a/git/remote.py b/git/remote.py
index cebfafb7b..01fe3771d 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -44,11 +44,9 @@
 from git.types import PathLike, Literal, Old_commit_ish
 
 if TYPE_CHECKING:
-    from git.repo.base import Repo
+    from git.objects.commit import Commit
     from git.objects.submodule.base import UpdateProgress
-
-    # from git.objects.commit import Commit
-    # from git.objects import Blob, Tree, TagObject
+    from git.repo.base import Repo
 
 flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
 
@@ -381,7 +379,7 @@ def name(self) -> str:
         return self.ref.name
 
     @property
-    def commit(self) -> Old_commit_ish:
+    def commit(self) -> "Commit":
         """:return: Commit of our remote ref"""
         return self.ref.commit
 
diff --git a/git/repo/base.py b/git/repo/base.py
index 62f458f68..5e7645366 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -34,29 +34,29 @@
 from git.remote import Remote, add_progress, to_progress_instance
 from git.util import (
     Actor,
-    finalize_process,
     cygpath,
-    hex_to_bin,
     expand_path,
+    finalize_process,
+    hex_to_bin,
     remove_password_if_present,
 )
 
 from .fun import (
-    rev_parse,
-    is_git_dir,
     find_submodule_git_dir,
-    touch,
     find_worktree_git_dir,
+    is_git_dir,
+    rev_parse,
+    touch,
 )
 
 # typing ------------------------------------------------------
 
 from git.types import (
-    TBD,
-    PathLike,
-    Lit_config_levels,
-    Old_commit_ish,
     CallableProgress,
+    Commit_ish,
+    Lit_config_levels,
+    PathLike,
+    TBD,
     Tree_ish,
     assert_never,
 )
@@ -68,25 +68,25 @@
     Iterator,
     List,
     Mapping,
+    NamedTuple,
     Optional,
     Sequence,
+    TYPE_CHECKING,
     TextIO,
     Tuple,
     Type,
     Union,
-    NamedTuple,
     cast,
-    TYPE_CHECKING,
 )
 
 from git.types import ConfigLevels_Tup, TypedDict
 
 if TYPE_CHECKING:
-    from git.util import IterableList
-    from git.refs.symbolic import SymbolicReference
     from git.objects import Tree
     from git.objects.submodule.base import UpdateProgress
+    from git.refs.symbolic import SymbolicReference
     from git.remote import RemoteProgress
+    from git.util import IterableList
 
 # -----------------------------------------------------------
 
@@ -96,7 +96,7 @@
 
 
 class BlameEntry(NamedTuple):
-    commit: Dict[str, "Commit"]
+    commit: Dict[str, Commit]
     linenos: range
     orig_path: Optional[str]
     orig_linenos: range
@@ -696,7 +696,7 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo
         """
         return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False)
 
-    def commit(self, rev: Union[str, Old_commit_ish, None] = None) -> Commit:
+    def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
         """The :class:`~git.objects.commit.Commit` object for the specified revision.
 
         :param rev:
@@ -772,7 +772,7 @@ def iter_commits(
 
         return Commit.iter_items(self, rev, paths, **kwargs)
 
-    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Old_commit_ish, None]]:
+    def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
         R"""Find the closest common ancestor for the given revision
         (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
         :class:`~git.refs.reference.Reference`\s, etc.).
@@ -797,9 +797,9 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Old_commit_ish, Non
             raise ValueError("Please specify at least two revs, got only %i" % len(rev))
         # END handle input
 
-        res: List[Union[Old_commit_ish, None]] = []
+        res: List[Commit] = []
         try:
-            lines = self.git.merge_base(*rev, **kwargs).splitlines()  # List[str]
+            lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
         except GitCommandError as err:
             if err.status == 128:
                 raise
@@ -815,7 +815,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Old_commit_ish, Non
 
         return res
 
-    def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool:
+    def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
         """Check if a commit is an ancestor of another.
 
         :param ancestor_rev:

From 191f4cf2e7fe2aef2e8583910b50b878bc66d3ed Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 18:50:28 -0400
Subject: [PATCH 611/994] Fix some annotations in git.repo.fun

---
 git/repo/fun.py | 50 ++++++++++++++++++++++++++-----------------------
 1 file changed, 27 insertions(+), 23 deletions(-)

diff --git a/git/repo/fun.py b/git/repo/fun.py
index 6bc998bb7..ad8a9fd90 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -6,34 +6,30 @@
 from __future__ import annotations
 
 import os
-import stat
+import os.path as osp
 from pathlib import Path
+import stat
 from string import digits
 
+from git.cmd import Git
 from git.exc import WorkTreeRepositoryUnsupported
 from git.objects import Object
 from git.refs import SymbolicReference
 from git.util import hex_to_bin, bin_to_hex, cygpath
-from gitdb.exc import (
-    BadObject,
-    BadName,
-)
-
-import os.path as osp
-from git.cmd import Git
+from gitdb.exc import BadName, BadObject
 
 # Typing ----------------------------------------------------------------------
 
-from typing import Union, Optional, cast, TYPE_CHECKING
-from git.types import Old_commit_ish
+from typing import Optional, TYPE_CHECKING, Union, cast, overload
+
+from git.types import AnyGitObject, Literal, Old_commit_ish, PathLike
 
 if TYPE_CHECKING:
-    from git.types import PathLike
-    from .base import Repo
     from git.db import GitCmdObjectDB
-    from git.refs.reference import Reference
     from git.objects import Commit, TagObject, Blob, Tree
+    from git.refs.reference import Reference
     from git.refs.tag import Tag
+    from .base import Repo
 
 # ----------------------------------------------------------------------------
 
@@ -56,7 +52,7 @@ def touch(filename: str) -> str:
     return filename
 
 
-def is_git_dir(d: "PathLike") -> bool:
+def is_git_dir(d: PathLike) -> bool:
     """This is taken from the git setup.c:is_git_directory function.
 
     :raise git.exc.WorkTreeRepositoryUnsupported:
@@ -79,7 +75,7 @@ def is_git_dir(d: "PathLike") -> bool:
     return False
 
 
-def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]:
+def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
     """Search for a gitdir for this worktree."""
     try:
         statbuf = os.stat(dotgit)
@@ -98,7 +94,7 @@ def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]:
     return None
 
 
-def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]:
+def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
     """Search for a submodule repo."""
     if is_git_dir(d):
         return d
@@ -141,9 +137,17 @@ def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
     # END exception handling
 
 
-def name_to_object(
-    repo: "Repo", name: str, return_ref: bool = False
-) -> Union[SymbolicReference, "Commit", "TagObject", "Blob", "Tree"]:
+@overload
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject:
+    ...
+
+
+@overload
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]:
+    ...
+
+
+def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:
     """
     :return:
         Object specified by the given name - hexshas (short and long) as well as
@@ -151,8 +155,8 @@ def name_to_object(
 
     :param return_ref:
         If ``True``, and name specifies a reference, we will return the reference
-        instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` or
-        `~gitdb.exc.BadName`.
+        instead of the object. Otherwise it will raise :class:`~gitdb.exc.BadObject` or
+        :class:`~gitdb.exc.BadName`.
     """
     hexsha: Union[None, str, bytes] = None
 
@@ -201,7 +205,7 @@ def name_to_object(
     return Object.new_from_sha(repo, hex_to_bin(hexsha))
 
 
-def deref_tag(tag: "Tag") -> "TagObject":
+def deref_tag(tag: "Tag") -> AnyGitObject:
     """Recursively dereference a tag and return the resulting object."""
     while True:
         try:
@@ -212,7 +216,7 @@ def deref_tag(tag: "Tag") -> "TagObject":
     return tag
 
 
-def to_commit(obj: Object) -> Union["Commit", "TagObject"]:
+def to_commit(obj: Object) -> "Commit":
     """Convert the given object to a commit if possible and return it."""
     if obj.type == "tag":
         obj = deref_tag(obj)

From d1ce94048ecaccf64f6bee0ad20f33df256e1139 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 18:53:06 -0400
Subject: [PATCH 612/994] Remove extra `parents` param in Commit.__init__
 docstring

It was listed earlier and was incorrect, not corresponding to the
position or type of this initializer's actual `parents` parameter.
---
 git/objects/commit.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/git/objects/commit.py b/git/objects/commit.py
index 28df86c9d..c5460b07b 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -112,15 +112,12 @@ def __init__(
         encoding: Union[str, None] = None,
         gpgsig: Union[str, None] = None,
     ) -> None:
-        R"""Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
+        """Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as
         default will be implicitly set on first query.
 
         :param binsha:
             20 byte sha1.
 
-        :param parents: tuple(Commit, ...)
-            A tuple of commit ids or actual :class:`Commit`\s.
-
         :param tree:
             A :class:`~git.objects.tree.Tree` object.
 

From fe42ca78c4dfb8a9afb442307d43c380c9751a71 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 21:12:22 -0400
Subject: [PATCH 613/994] Help tools know the type of a Commit's `parents`

mypy seems not to need this, and already infers the specific type
List[Commit], but some other type checkers -- at least pylance, and
thus probably also pyright -- do not infer this.
---
 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 c5460b07b..3dec9a14d 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -753,7 +753,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
         readline = stream.readline
         self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "")
 
-        self.parents = []
+        self.parents = []  # type: List[Commit]
         next_line = None
         while True:
             parent_line = readline()

From e66297a104c7acfcac21dd0a33e7d0581596f251 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 21:17:00 -0400
Subject: [PATCH 614/994] Keep the type of a Commit's `parents` from being too
 narrow

The type should be Sequence[Commit], rather than List[Commit] as
some type checkers seem already to have been inferring and as some
others were caused to infer by the indirect fix that precedes this.
---
 git/objects/commit.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/git/objects/commit.py b/git/objects/commit.py
index 3dec9a14d..6d3864be6 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -79,8 +79,8 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     # INVARIANTS
     default_encoding = "UTF-8"
 
-    # object configuration
     type: Literal["commit"] = "commit"
+
     __slots__ = (
         "tree",
         "author",
@@ -94,8 +94,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
         "encoding",
         "gpgsig",
     )
+
     _id_attribute_ = "hexsha"
 
+    parents: Sequence["Commit"]
+
     def __init__(
         self,
         repo: "Repo",
@@ -753,7 +756,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit":
         readline = stream.readline
         self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "")
 
-        self.parents = []  # type: List[Commit]
+        self.parents = []
         next_line = None
         while True:
             parent_line = readline()

From fe7f9f2b27fa43d0340887abce63820dd40ea607 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 21:25:48 -0400
Subject: [PATCH 615/994] Fix remaining old Commit_ish annotations in
 git.repo.fun

Some were replaced in 191f4cf (which also included other fixes).
The fixes here consist mostly of replacing the remaining uses of
the old Commit_ish, though some now-unneeded casting is also
removed.
---
 git/repo/fun.py | 32 ++++++++++++++++++--------------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/git/repo/fun.py b/git/repo/fun.py
index ad8a9fd90..b65432b09 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -22,11 +22,11 @@
 
 from typing import Optional, TYPE_CHECKING, Union, cast, overload
 
-from git.types import AnyGitObject, Literal, Old_commit_ish, PathLike
+from git.types import AnyGitObject, Literal, PathLike
 
 if TYPE_CHECKING:
     from git.db import GitCmdObjectDB
-    from git.objects import Commit, TagObject, Blob, Tree
+    from git.objects import Commit, TagObject
     from git.refs.reference import Reference
     from git.refs.tag import Tag
     from .base import Repo
@@ -227,12 +227,18 @@ def to_commit(obj: Object) -> "Commit":
     return obj
 
 
-def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
-    """
+def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
+    """Parse a revision string. Like ``git rev-parse``.
+
     :return:
-        `~git.objects.base.Object` at the given revision, either
-        `~git.objects.commit.Commit`, `~git.refs.tag.Tag`, `~git.objects.tree.Tree` or
-        `~git.objects.blob.Blob`.
+        `~git.objects.base.Object` at the given revision.
+
+        This may be any type of git object:
+
+        * :class:`Commit <git.objects.commit.Commit>`
+        * :class:`TagObject <git.objects.tag.TagObject>`
+        * :class:`Tree <git.objects.tree.Tree>`
+        * :class:`Blob <git.objects.blob.Blob>`
 
     :param rev:
         ``git rev-parse``-compatible revision specification as string. Please see
@@ -253,7 +259,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         raise NotImplementedError("commit by message search (regex)")
     # END handle search
 
-    obj: Union[Old_commit_ish, "Reference", None] = None
+    obj: Optional[AnyGitObject] = None
     ref = None
     output_type = "commit"
     start = 0
@@ -275,12 +281,10 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 if token == "@":
                     ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
                 else:
-                    obj = cast(Old_commit_ish, name_to_object(repo, rev[:start]))
+                    obj = name_to_object(repo, rev[:start])
                 # END handle token
             # END handle refname
         else:
-            assert obj is not None
-
             if ref is not None:
                 obj = cast("Commit", ref.commit)
             # END handle ref
@@ -300,7 +304,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
                 pass  # Default.
             elif output_type == "tree":
                 try:
-                    obj = cast(Old_commit_ish, obj)
+                    obj = cast(AnyGitObject, obj)
                     obj = to_commit(obj).tree
                 except (AttributeError, ValueError):
                     pass  # Error raised later.
@@ -373,7 +377,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
         parsed_to = start
         # Handle hierarchy walk.
         try:
-            obj = cast(Old_commit_ish, obj)
+            obj = cast(AnyGitObject, obj)
             if token == "~":
                 obj = to_commit(obj)
                 for _ in range(num):
@@ -402,7 +406,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]:
 
     # Still no obj? It's probably a simple name.
     if obj is None:
-        obj = cast(Old_commit_ish, name_to_object(repo, rev))
+        obj = name_to_object(repo, rev)
         parsed_to = lr
     # END handle simple name
 

From ab2782762515a8e49acc1f195bf8d8aebbafff6c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 22:12:37 -0400
Subject: [PATCH 616/994] Fix remaining old Commit_ish annotations in git.refs

(This also improves the sorting of imports where they are already
being changed, and fixes a typo in a comment.)
---
 git/refs/head.py      | 10 +++++-----
 git/refs/reference.py | 14 +++++---------
 2 files changed, 10 insertions(+), 14 deletions(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index e6bef598a..4eff7abb8 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -15,14 +15,14 @@
 
 # typing ---------------------------------------------------
 
-from typing import Any, Sequence, Union, TYPE_CHECKING
+from typing import Any, Sequence, TYPE_CHECKING, Union
 
-from git.types import PathLike, Old_commit_ish
+from git.types import Commit_ish, PathLike
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit
     from git.refs import RemoteReference
+    from git.repo import Repo
 
 # -------------------------------------------------------------------
 
@@ -62,7 +62,7 @@ def orig_head(self) -> SymbolicReference:
 
     def reset(
         self,
-        commit: Union[Old_commit_ish, SymbolicReference, str] = "HEAD",
+        commit: Union[Commit_ish, SymbolicReference, str] = "HEAD",
         index: bool = True,
         working_tree: bool = False,
         paths: Union[PathLike, Sequence[PathLike], None] = None,
@@ -99,7 +99,7 @@ def reset(
         if index:
             mode = "--mixed"
 
-            # Tt appears some git versions declare mixed and paths deprecated.
+            # It appears some git versions declare mixed and paths deprecated.
             # See http://github.com/Byron/GitPython/issues#issue/2.
             if paths:
                 mode = None
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 99366f710..cf418aa5d 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -1,24 +1,20 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.util import (
-    LazyMixin,
-    IterableObj,
-)
+from git.util import IterableObj, LazyMixin
 from .symbolic import SymbolicReference, T_References
 
-
 # typing ------------------------------------------------------------------
 
-from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING
-from git.types import Old_commit_ish, PathLike, _T
+from typing import Any, Callable, Iterator, TYPE_CHECKING, Type, Union
+
+from git.types import AnyGitObject, PathLike, _T
 
 if TYPE_CHECKING:
     from git.repo import Repo
 
 # ------------------------------------------------------------------------------
 
-
 __all__ = ["Reference"]
 
 # { Utilities
@@ -81,7 +77,7 @@ def __str__(self) -> str:
     # @ReservedAssignment
     def set_object(
         self,
-        object: Union[Old_commit_ish, "SymbolicReference", str],
+        object: Union[AnyGitObject, "SymbolicReference", str],
         logmsg: Union[str, None] = None,
     ) -> "Reference":
         """Special version which checks if the head-log needs an update as well.

From b4b6e1ee0c94dd22886ee326f476f8dcfd49647b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 22:32:12 -0400
Subject: [PATCH 617/994] Fix IndexFile.commit `parent_commits` annotation

This changes that parameter's annotation to be equivalent to the
annotation of the corresponding Commit.create_from_tree parameter.

It had been annotated as the old Commit_ish type, but it passes
that argument to Commit.create_from_tree, and it also refers to
that method in its docstring for usage. Commit.create_from_tree
annotates this argument as an optional list of Commit objects, and
if the objects obtained when it iterates them are not Commit
instances, then an exception is raised. Even if being able to pass
other commit-ish things (or references, strings, etc.) is
desirable, that does not currently work.

This also improves sorting (and grouping style) of imports in that
file (which were being changed already to no longer import the old
Commit_ish type).
---
 git/index/base.py | 43 +++++++++++++++----------------------------
 1 file changed, 15 insertions(+), 28 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 668607772..80fa90ee8 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -11,23 +11,16 @@
 import glob
 from io import BytesIO
 import os
+import os.path as osp
 from stat import S_ISLNK
 import subprocess
 import sys
 import tempfile
 
-from git.compat import (
-    force_bytes,
-    defenc,
-)
-from git.exc import GitCommandError, CheckoutError, GitError, InvalidGitRepositoryError
-from git.objects import (
-    Blob,
-    Submodule,
-    Tree,
-    Object,
-    Commit,
-)
+from git.compat import defenc, force_bytes
+import git.diff as git_diff
+from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
+from git.objects import Blob, Commit, Object, Submodule, Tree
 from git.objects.util import Serializable
 from git.util import (
     LazyMixin,
@@ -41,24 +34,17 @@
 from gitdb.base import IStream
 from gitdb.db import MemoryDB
 
-import git.diff as git_diff
-import os.path as osp
-
 from .fun import (
+    S_IFGITLINK,
+    aggressive_tree_merge,
     entry_key,
-    write_cache,
     read_cache,
-    aggressive_tree_merge,
-    write_tree_from_cache,
-    stat_mode_to_index_mode,
-    S_IFGITLINK,
     run_commit_hook,
+    stat_mode_to_index_mode,
+    write_cache,
+    write_tree_from_cache,
 )
-from .typ import (
-    BaseIndexEntry,
-    IndexEntry,
-    StageType,
-)
+from .typ import BaseIndexEntry, IndexEntry, StageType
 from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
 
 # typing -----------------------------------------------------------------------------
@@ -80,12 +66,13 @@
     Union,
 )
 
-from git.types import Old_commit_ish, Literal, PathLike
+from git.types import Literal, PathLike
 
 if TYPE_CHECKING:
     from subprocess import Popen
-    from git.repo import Repo
+
     from git.refs.reference import Reference
+    from git.repo import Repo
     from git.util import Actor
 
 
@@ -1127,7 +1114,7 @@ def move(
     def commit(
         self,
         message: str,
-        parent_commits: Union[Old_commit_ish, None] = None,
+        parent_commits: Union[List[Commit], None] = None,
         head: bool = True,
         author: Union[None, "Actor"] = None,
         committer: Union[None, "Actor"] = None,

From 5b2869faccbda659e12f2b1e7ca78b69ba770a11 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 10 Mar 2024 23:40:06 -0400
Subject: [PATCH 618/994] Fix old Commit_ish annotations in git.remote

The combination of changes here is a bit unintuitive.

- In PushInfo.__init__, the old_commit parameter is an optional
  string, whose value is bound to the _old_commit_sha instance
  attribute. The old_commit property attempts to create a Commit
  object using this string. When _old_commit_sha is None, this
  property returns None. Otherwise it calls Repo.commit on the Repo
  object for the PushInfo's associated remote.

  Repo.commit should return, and is annotated to return, a Commit
  object. Its return value is produced by calling rev_parse, which
  is actually the implementation in git.fun, which always returns
  an Object (and more specifically an AnyGitObject) and whose
  annotation was recently fixed in fe7f9f2. The way rev_parse is
  used appears to only be able to get a Commit, but even if it
  were to get something else, it would still be an Object and not
  a SymbolicReference or string.

  Therefore, the return annotation, which contained the old
  Commit_ish, was overly broad, in part because the old Commit_ish
  was defined over-broadly, but also in other ways.

  This changes it to declare that it returns a Commit object or
  None, not allowing any kind of refs, strings, or instances
  representing git objects other than commits. The new return
  annotation is also what type checkers infer for the operand of
  the return statement, ever since git.fun.rev_parse's own return
  annotation was fixed.

- In contrast, the situation with FetchInfo is almost the opposite.
  FetchInfo.__init__ had declared its old_commit parameter as being
  of the old Commit_ish type or None. Given the name old_commit,
  this may seem at first glance to be a case where only actually
  commit-ish values should be accepted, such that the annotation
  may have been overly broad due the overbroad old definition of
  Commit_ish. But on closer inspection it seems that may not be so.

  Specifically, when "git fetch" reports the refs it updated, and a
  ref points to something that is not commit-ish, the "old_commit"
  can be something that is not a commit. In particular, if a remote
  lightweight tag points to a blob or tree (rather than the usual
  case of pointing to a commit or tag), and that tag is then
  changed, then a fetch -- if done with flags that allow it to be
  reset -- should result in an "old_commit" of the blob or tree.

  More specifically, in FetchInfo._from_line (in the "tag update"
  case), in a line with ".." or "...", old_commit is set by parsing
  a field with repo.rev_parse, which is git.fun.rev_parse, which
  will return an Object that may be any of the four types. This is
  later passed as the old_commmit parameter when constructing a
  FetchInfo instance. Since a lightweight tag is a ref that can
  refer to any of the four types, any could end up being parsed
  into the old_commit attribute.

  This therefore keeps those as broad as before, channging their
  old Commit_ish annotations to AnyGitObject rather than to the
  newer Commit_ish type or anything narrower.

Note also that it is pure coincidence that entities named
old_commit were temporarily annotated as Old_commit_ish. (The
latter, as noted in 04a2753, is just what the old Commit_ish type
was temporarily renamed to.)
---
 git/remote.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 01fe3771d..e0dc08639 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -41,7 +41,7 @@
     overload,
 )
 
-from git.types import PathLike, Literal, Old_commit_ish
+from git.types import AnyGitObject, Literal, PathLike
 
 if TYPE_CHECKING:
     from git.objects.commit import Commit
@@ -194,7 +194,7 @@ def __init__(
         self.summary = summary
 
     @property
-    def old_commit(self) -> Union[str, SymbolicReference, Old_commit_ish, None]:
+    def old_commit(self) -> Union["Commit", None]:
         return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
 
     @property
@@ -360,7 +360,7 @@ def __init__(
         ref: SymbolicReference,
         flags: int,
         note: str = "",
-        old_commit: Union[Old_commit_ish, None] = None,
+        old_commit: Union[AnyGitObject, None] = None,
         remote_ref_path: Optional[PathLike] = None,
     ) -> None:
         """Initialize a new instance."""
@@ -436,7 +436,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
 
         # Parse operation string for more info.
         # This makes no sense for symbolic refs, but we parse it anyway.
-        old_commit: Union[Old_commit_ish, None] = None
+        old_commit: Union[AnyGitObject, None] = None
         is_tag_operation = False
         if "rejected" in operation:
             flags |= cls.REJECTED

From dd8ee4fabb8ae7921cf53b95b195f4d2186f136f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 11 Mar 2024 12:06:18 -0400
Subject: [PATCH 619/994] Start fixing venv test fixture pip toml bug

This is not yet a usable fix, because venv.create only supports
upgrade_deps on Python 3.9 and higher.
---
 test/lib/helper.py        | 6 +++---
 test/test_index.py        | 2 +-
 test/test_installation.py | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 26469ed5d..cf7b37b08 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -403,13 +403,13 @@ class VirtualEnvironment:
 
     __slots__ = ("_env_dir",)
 
-    def __init__(self, env_dir, *, with_pip):
+    def __init__(self, env_dir, *, need_pip):
         if os.name == "nt":
             self._env_dir = osp.realpath(env_dir)
-            venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
+            venv.create(self.env_dir, symlinks=False, with_pip=need_pip, upgrade_deps=need_pip)
         else:
             self._env_dir = env_dir
-            venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
+            venv.create(self.env_dir, symlinks=True, with_pip=need_pip, upgrade_deps=need_pip)
 
     @property
     def env_dir(self):
diff --git a/test/test_index.py b/test/test_index.py
index fa64b82a2..206023bfe 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1060,7 +1060,7 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         # from a venv may not run when copied outside of it, and a global interpreter
         # won't run when copied to a different location if it was installed from the
         # Microsoft Store. So we make a new venv in rw_dir and use its interpreter.
-        venv = VirtualEnvironment(rw_dir, with_pip=False)
+        venv = VirtualEnvironment(rw_dir, need_pip=False)
         shutil.copy(venv.python, Path(rw_dir, shell_name))
         shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir))
         payload = Path(rw_dir, "payload.txt")
diff --git a/test/test_installation.py b/test/test_installation.py
index ae6472e98..38c0c45b6 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -64,7 +64,7 @@ def test_installation(self, rw_dir):
 
     @staticmethod
     def _set_up_venv(rw_dir):
-        venv = VirtualEnvironment(rw_dir, with_pip=True)
+        venv = VirtualEnvironment(rw_dir, need_pip=True)
         os.symlink(
             os.path.dirname(os.path.dirname(__file__)),
             venv.sources,

From a262a06634ccb616f061fdae7cf67b00a92f0ba6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 11 Mar 2024 13:16:15 -0400
Subject: [PATCH 620/994] Upgrade test fixture pip in venv without upgrade_deps

Because the upgrade_deps parameter to venv.create, as well as
related functionality such as the EnvBuilder.upgrade_dependencies
method that it uses, are only available starting in Python 3.9.

This also puts the name of the VirtualEnvironment.__init__
parameter for setting up pip in the test fixture virtual
environment back from need_pip to with_pip, which may be more
intuitive.
---
 test/lib/helper.py        | 15 ++++++++++++---
 test/test_index.py        |  2 +-
 test/test_installation.py |  2 +-
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index cf7b37b08..27586c2b0 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -10,6 +10,8 @@
 import logging
 import os
 import os.path as osp
+import subprocess
+import sys
 import tempfile
 import textwrap
 import time
@@ -403,13 +405,20 @@ class VirtualEnvironment:
 
     __slots__ = ("_env_dir",)
 
-    def __init__(self, env_dir, *, need_pip):
+    def __init__(self, env_dir, *, with_pip):
         if os.name == "nt":
             self._env_dir = osp.realpath(env_dir)
-            venv.create(self.env_dir, symlinks=False, with_pip=need_pip, upgrade_deps=need_pip)
+            venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
         else:
             self._env_dir = env_dir
-            venv.create(self.env_dir, symlinks=True, with_pip=need_pip, upgrade_deps=need_pip)
+            venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
+
+        if with_pip:
+            # The upgrade_deps parameter to venv.create is 3.9+ only, so do it this way.
+            command = [self.python, "-m", "pip", "install", "--upgrade", "pip"]
+            if sys.version_info < (3, 12):
+                command.append("setuptools")
+            subprocess.check_output(command)
 
     @property
     def env_dir(self):
diff --git a/test/test_index.py b/test/test_index.py
index 206023bfe..fa64b82a2 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1060,7 +1060,7 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
         # from a venv may not run when copied outside of it, and a global interpreter
         # won't run when copied to a different location if it was installed from the
         # Microsoft Store. So we make a new venv in rw_dir and use its interpreter.
-        venv = VirtualEnvironment(rw_dir, need_pip=False)
+        venv = VirtualEnvironment(rw_dir, with_pip=False)
         shutil.copy(venv.python, Path(rw_dir, shell_name))
         shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir))
         payload = Path(rw_dir, "payload.txt")
diff --git a/test/test_installation.py b/test/test_installation.py
index 38c0c45b6..ae6472e98 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -64,7 +64,7 @@ def test_installation(self, rw_dir):
 
     @staticmethod
     def _set_up_venv(rw_dir):
-        venv = VirtualEnvironment(rw_dir, need_pip=True)
+        venv = VirtualEnvironment(rw_dir, with_pip=True)
         os.symlink(
             os.path.dirname(os.path.dirname(__file__)),
             venv.sources,

From 0b7f15945cc5adeb79ca9e25abafdbf8c565d716 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Mon, 11 Mar 2024 09:54:30 +0100
Subject: [PATCH 621/994] lint: replace `flake8` with `ruff` check

---
 .flake8                 | 26 --------------------------
 .pre-commit-config.yaml | 13 ++++++-------
 README.md               |  2 +-
 pyproject.toml          | 38 +++++++++++++++++++++++++++++++++++++-
 requirements-dev.txt    |  2 --
 5 files changed, 44 insertions(+), 37 deletions(-)
 delete mode 100644 .flake8

diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 1cc049a69..000000000
--- a/.flake8
+++ /dev/null
@@ -1,26 +0,0 @@
-[flake8]
-
-show-source = True
-count = True
-statistics = True
-
-# E266 = too many leading '#' for block comment
-# E731 = do not assign a lambda expression, use a def
-# TC002 = move third party import to TYPE_CHECKING
-# TC, TC2 = flake8-type-checking
-
-# select = C,E,F,W ANN, TC, TC2  # to enable code. Disabled if not listed, including builtin codes
-enable-extensions = TC, TC2  # only needed for extensions not enabled by default
-
-ignore = E266, E731
-
-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
-
-min-python-version = 3.7.0
-
-# for `black` compatibility
-max-line-length = 120
-extend-ignore = E203, W503
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1ac5baa00..cd5f58441 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,14 +14,13 @@ repos:
     name: black (format)
     exclude: ^git/ext/
 
-- repo: https://github.com/PyCQA/flake8
-  rev: 6.1.0
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  rev: v0.3.0
   hooks:
-  - id: flake8
-    additional_dependencies:
-    - flake8-bugbear==23.9.16
-    - flake8-comprehensions==3.14.0
-    - flake8-typing-imports==1.14.0
+  #- id: ruff-format  # todo: eventually replace Black with Ruff for consistency
+  #  args: ["--preview"]
+  - id: ruff
+    args: ["--fix"]
     exclude: ^doc|^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
diff --git a/README.md b/README.md
index 7faeae23b..1e4a59d7f 100644
--- a/README.md
+++ b/README.md
@@ -187,7 +187,7 @@ The same linting, and running tests on all the different supported Python versio
 Specific tools:
 
 - Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`.
-- Configuration for `flake8` is in the `./.flake8` file.
+- Configuration for `ruff` is in the `pyproject.toml` file.
 
 Orchestration tools:
 
diff --git a/pyproject.toml b/pyproject.toml
index 7109389d7..230a9cf3a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,6 @@ warn_unreachable = true
 show_error_codes = true
 implicit_reexport = true
 # strict = true
-
 # TODO: Remove when 'gitdb' is fully annotated.
 exclude = ["^git/ext/gitdb"]
 [[tool.mypy.overrides]]
@@ -47,3 +46,40 @@ omit = ["*/git/ext/*"]
 line-length = 120
 target-version = ["py37"]
 extend-exclude = "git/ext/gitdb"
+
+[tool.ruff]
+target-version = "py37"
+line-length = 120
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    "git/ext/",
+    "doc",
+    "build",
+    "dist",
+]
+# Enable Pyflakes `E` and `F` codes by default.
+lint.select = [
+    "E",
+    "W", # see: https://pypi.org/project/pycodestyle
+    "F", # see: https://pypi.org/project/pyflakes
+#    "I", #see: https://pypi.org/project/isort/
+#    "S", # see: https://pypi.org/project/flake8-bandit
+#    "UP", # see: https://docs.astral.sh/ruff/rules/#pyupgrade-up
+]
+lint.extend-select = [
+    "A",    # see: https://pypi.org/project/flake8-builtins
+    "B",    # see: https://pypi.org/project/flake8-bugbear
+    "C4",   # see: https://pypi.org/project/flake8-comprehensions
+    "TCH004", # see: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
+]
+lint.ignore = [
+    "E203", "W503"
+]
+lint.ignore-init-module-imports = true
+lint.unfixable = ["F401"]
+
+#[tool.ruff.lint.per-file-ignores]
+#"setup.py" = ["ANN202", "ANN401"]
+#"docs/source/conf.py" = ["A001", "D103"]
+#"src/**" = ["ANN401"]
+#"tests/**" = ["S101", "ANN001", "ANN201", "ANN202", "ANN401"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index e3030c597..69a79d13d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -3,7 +3,5 @@
 
 # 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
-
 pytest-icdiff
 # pytest-profiling

From aa9298a37f1bbdecfbfe9b13026c81374abdc2d7 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Mon, 11 Mar 2024 11:10:00 +0100
Subject: [PATCH 622/994] fixing lints / noqa

---
 git/index/base.py             |  8 ++++----
 git/objects/submodule/base.py |  2 +-
 git/objects/util.py           |  2 +-
 git/refs/symbolic.py          |  4 ++--
 pyproject.toml                | 12 +++++-------
 5 files changed, 13 insertions(+), 15 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 249144d54..985b1bccf 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -248,7 +248,7 @@ def write(
         # Make sure we have our entries read before getting a write lock.
         # Otherwise it would be done when streaming.
         # This can happen if one doesn't change the index, but writes it right away.
-        self.entries
+        self.entries  # noqa: B018
         lfd = LockedFD(file_path or self._file_path)
         stream = lfd.open(write=True, stream=True)
 
@@ -397,7 +397,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             with 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.
+                index.entries  # noqa: B018 # Force it to read the file as we will delete the temp-file.
                 return index
             # END index merge handling
 
@@ -1339,7 +1339,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
             # Make sure we have our entries loaded before we start checkout_index, which
             # will hold a lock on it. We try to get the lock as well during our entries
             # initialization.
-            self.entries
+            self.entries  # noqa: B018
 
             args.append("--stdin")
             kwargs["as_process"] = True
@@ -1379,7 +1379,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
                 self._flush_stdin_and_wait(proc, ignore_stdout=True)
             except GitCommandError:
                 # Without parsing stdout we don't know what failed.
-                raise CheckoutError(
+                raise CheckoutError(  # noqa: B904
                     "Some files could not be checked out from the index, probably because they didn't exist.",
                     failed_files,
                     [],
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index cdd7c8e1b..b3e681e94 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1445,7 +1445,7 @@ def exists(self) -> bool:
 
         try:
             try:
-                self.path
+                self.path  # noqa: B018
                 return True
             except Exception:
                 return False
diff --git a/git/objects/util.py b/git/objects/util.py
index 6f4e7d087..71eb9c230 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -439,7 +439,7 @@ def _list_traverse(
 
         if not as_edge:
             out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id)
-            out.extend(self.traverse(as_edge=as_edge, *args, **kwargs))
+            out.extend(self.traverse(as_edge=as_edge, *args, **kwargs))  # noqa: B026
             return out
             # Overloads in subclasses (mypy doesn't allow typing self: subclass).
             # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]]
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 16aada0a7..465acf872 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -496,7 +496,7 @@ def is_valid(self) -> bool:
             valid object or reference.
         """
         try:
-            self.object
+            self.object  # noqa: B018
         except (OSError, ValueError):
             return False
         else:
@@ -510,7 +510,7 @@ def is_detached(self) -> bool:
             instead to another reference.
         """
         try:
-            self.ref
+            self.ref  # noqa: B018
             return False
         except TypeError:
             return True
diff --git a/pyproject.toml b/pyproject.toml
index 230a9cf3a..af0e52ca4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -67,19 +67,17 @@ lint.select = [
 #    "UP", # see: https://docs.astral.sh/ruff/rules/#pyupgrade-up
 ]
 lint.extend-select = [
-    "A",    # see: https://pypi.org/project/flake8-builtins
+    #"A",    # see: https://pypi.org/project/flake8-builtins
     "B",    # see: https://pypi.org/project/flake8-bugbear
     "C4",   # see: https://pypi.org/project/flake8-comprehensions
     "TCH004", # see: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
 ]
 lint.ignore = [
-    "E203", "W503"
+    "E203",
+    "E731", # Do not assign a `lambda` expression, use a `def`
 ]
 lint.ignore-init-module-imports = true
 lint.unfixable = ["F401"]
 
-#[tool.ruff.lint.per-file-ignores]
-#"setup.py" = ["ANN202", "ANN401"]
-#"docs/source/conf.py" = ["A001", "D103"]
-#"src/**" = ["ANN401"]
-#"tests/**" = ["S101", "ANN001", "ANN201", "ANN202", "ANN401"]
+[tool.ruff.lint.per-file-ignores]
+"test/**" = ["B018"]

From 5f889c4137d1d4442ab3ceb8e45c3c03cafded1a Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Mon, 11 Mar 2024 11:18:06 +0100
Subject: [PATCH 623/994] try: from typing import Literal

---
 git/objects/blob.py           | 6 +++++-
 git/objects/commit.py         | 7 ++++++-
 git/objects/submodule/base.py | 7 ++++++-
 git/objects/tag.py            | 5 ++++-
 git/objects/tree.py           | 7 ++++++-
 5 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/git/objects/blob.py b/git/objects/blob.py
index 253ceccb5..4035c3e7c 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -6,7 +6,11 @@
 from mimetypes import guess_type
 from . import base
 
-from git.types import Literal
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 __all__ = ("Blob",)
 
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 06ab0898b..dcb3be695 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -44,7 +44,12 @@
     Dict,
 )
 
-from git.types import PathLike, Literal
+from git.types import PathLike
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index b3e681e94..e5933b116 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -44,7 +44,12 @@
 from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
 from typing import Any, Iterator, Union
 
-from git.types import Commit_ish, Literal, PathLike, TBD
+from git.types import Commit_ish, PathLike, TBD
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.index import IndexFile
diff --git a/git/objects/tag.py b/git/objects/tag.py
index f455c55fc..d8815e436 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -16,7 +16,10 @@
 
 from typing import List, TYPE_CHECKING, Union
 
-from git.types import Literal
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo
diff --git a/git/objects/tree.py b/git/objects/tree.py
index a506bba7d..731ab5fa1 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -31,7 +31,12 @@
     TYPE_CHECKING,
 )
 
-from git.types import PathLike, Literal
+from git.types import PathLike
+
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
 
 if TYPE_CHECKING:
     from git.repo import Repo

From 517f83aae949511fcc0696d4b67392d024acded1 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Mon, 11 Mar 2024 21:33:19 +0100
Subject: [PATCH 624/994] lint: switch Black with `ruff-format`

---
 .github/workflows/lint.yml |  2 --
 .pre-commit-config.yaml    | 18 ++----------------
 Makefile                   |  2 +-
 pyproject.toml             |  5 -----
 test-requirements.txt      |  1 -
 tox.ini                    |  2 --
 6 files changed, 3 insertions(+), 27 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d805124fc..a32fb6c4e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -16,5 +16,3 @@ jobs:
     - uses: pre-commit/action@v3.0.1
       with:
         extra_args: --all-files --hook-stage manual
-      env:
-        SKIP: black-format
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index cd5f58441..60bbe3518 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,24 +1,10 @@
 repos:
-- repo: https://github.com/psf/black-pre-commit-mirror
-  rev: 23.9.1
-  hooks:
-  - id: black
-    alias: black-check
-    name: black (check)
-    args: [--check, --diff]
-    exclude: ^git/ext/
-    stages: [manual]
-
-  - id: black
-    alias: black-format
-    name: black (format)
-    exclude: ^git/ext/
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
   rev: v0.3.0
   hooks:
-  #- id: ruff-format  # todo: eventually replace Black with Ruff for consistency
-  #  args: ["--preview"]
+  - id: ruff-format
+    exclude: ^git/ext/
   - id: ruff
     args: ["--fix"]
     exclude: ^doc|^git/ext/
diff --git a/Makefile b/Makefile
index 839dc9f78..71a370ef2 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ all:
 	@awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile
 
 lint:
-	SKIP=black-format pre-commit run --all-files --hook-stage manual
+	SKIP=pre-commit run --all-files --hook-stage manual
 
 clean:
 	rm -rf build/ dist/ .eggs/ .tox/
diff --git a/pyproject.toml b/pyproject.toml
index af0e52ca4..eb57cc7b7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,11 +42,6 @@ source = ["git"]
 include = ["*/git/*"]
 omit = ["*/git/ext/*"]
 
-[tool.black]
-line-length = 120
-target-version = ["py37"]
-extend-exclude = "git/ext/gitdb"
-
 [tool.ruff]
 target-version = "py37"
 line-length = 120
diff --git a/test-requirements.txt b/test-requirements.txt
index 7cfb977a1..e1f5e2ed4 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,3 @@
-black
 coverage[toml]
 ddt >= 1.1.1, != 1.4.3
 mock ; python_version < "3.8"
diff --git a/tox.ini b/tox.ini
index f9ac25b78..28b7b147f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,8 +12,6 @@ commands = pytest --color=yes {posargs}
 [testenv:lint]
 description = Lint via pre-commit
 base_python = py{39,310,311,312,38,37}
-set_env =
-    SKIP = black-format
 commands = pre-commit run --all-files --hook-stage manual
 
 [testenv:mypy]

From 2d0158c2c147dae90f9bbceeead1c4f322e90436 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Mon, 11 Mar 2024 21:36:25 +0100
Subject: [PATCH 625/994] apply `ruff-format`

---
 git/cmd.py          | 72 ++++++++++++++++++---------------------------
 git/compat.py       | 18 ++++--------
 git/index/fun.py    |  6 ++--
 git/objects/fun.py  |  6 ++--
 git/objects/tree.py |  2 +-
 git/objects/util.py | 12 +++-----
 git/remote.py       | 11 +++----
 git/util.py         |  9 ++----
 8 files changed, 52 insertions(+), 84 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 731d0fab8..915f46a05 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -495,9 +495,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                 if mode in quiet:
                     pass
                 elif mode in warn or mode in error:
-                    err = (
-                        dedent(
-                            """\
+                    err = dedent(
+                        """\
                         %s
                         All git commands will error until this is rectified.
 
@@ -510,16 +509,14 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                         Example:
                             export %s=%s
                         """
-                        )
-                        % (
-                            err,
-                            cls._refresh_env_var,
-                            "|".join(quiet),
-                            "|".join(warn),
-                            "|".join(error),
-                            cls._refresh_env_var,
-                            quiet[0],
-                        )
+                    ) % (
+                        err,
+                        cls._refresh_env_var,
+                        "|".join(quiet),
+                        "|".join(warn),
+                        "|".join(error),
+                        cls._refresh_env_var,
+                        quiet[0],
                     )
 
                     if mode in warn:
@@ -527,9 +524,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                     else:
                         raise ImportError(err)
                 else:
-                    err = (
-                        dedent(
-                            """\
+                    err = dedent(
+                        """\
                         %s environment variable has been set but it has been set with an invalid value.
 
                         Use only the following values:
@@ -537,13 +533,11 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool:
                             - %s: for a warning message (logging level CRITICAL, displayed by default)
                             - %s: for a raised exception
                         """
-                        )
-                        % (
-                            cls._refresh_env_var,
-                            "|".join(quiet),
-                            "|".join(warn),
-                            "|".join(error),
-                        )
+                    ) % (
+                        cls._refresh_env_var,
+                        "|".join(quiet),
+                        "|".join(warn),
+                        "|".join(error),
                     )
                     raise ImportError(err)
 
@@ -565,13 +559,11 @@ def is_cygwin(cls) -> bool:
 
     @overload
     @classmethod
-    def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str:
-        ...
+    def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ...
 
     @overload
     @classmethod
-    def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str:
-        ...
+    def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ...
 
     @classmethod
     def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
@@ -932,8 +924,7 @@ def execute(
         command: Union[str, Sequence[Any]],
         *,
         as_process: Literal[True],
-    ) -> "AutoInterrupt":
-        ...
+    ) -> "AutoInterrupt": ...
 
     @overload
     def execute(
@@ -942,8 +933,7 @@ def execute(
         *,
         as_process: Literal[False] = False,
         stdout_as_string: Literal[True],
-    ) -> Union[str, Tuple[int, str, str]]:
-        ...
+    ) -> Union[str, Tuple[int, str, str]]: ...
 
     @overload
     def execute(
@@ -952,8 +942,7 @@ def execute(
         *,
         as_process: Literal[False] = False,
         stdout_as_string: Literal[False] = False,
-    ) -> Union[bytes, Tuple[int, bytes, str]]:
-        ...
+    ) -> Union[bytes, Tuple[int, bytes, str]]: ...
 
     @overload
     def execute(
@@ -963,8 +952,7 @@ def execute(
         with_extended_output: Literal[False],
         as_process: Literal[False],
         stdout_as_string: Literal[True],
-    ) -> str:
-        ...
+    ) -> str: ...
 
     @overload
     def execute(
@@ -974,8 +962,7 @@ def execute(
         with_extended_output: Literal[False],
         as_process: Literal[False],
         stdout_as_string: Literal[False],
-    ) -> bytes:
-        ...
+    ) -> bytes: ...
 
     def execute(
         self,
@@ -1387,8 +1374,9 @@ def __call__(self, **kwargs: Any) -> "Git":
         return self
 
     @overload
-    def _call_process(self, method: str, *args: None, **kwargs: None) -> str:
-        ...  # If no args were given, execute the call with all defaults.
+    def _call_process(
+        self, method: str, *args: None, **kwargs: None
+    ) -> str: ...  # If no args were given, execute the call with all defaults.
 
     @overload
     def _call_process(
@@ -1398,14 +1386,12 @@ def _call_process(
         as_process: Literal[True],
         *args: Any,
         **kwargs: Any,
-    ) -> "Git.AutoInterrupt":
-        ...
+    ) -> "Git.AutoInterrupt": ...
 
     @overload
     def _call_process(
         self, method: str, *args: Any, **kwargs: Any
-    ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
-        ...
+    ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ...
 
     def _call_process(
         self, method: str, *args: Any, **kwargs: Any
diff --git a/git/compat.py b/git/compat.py
index 7753fe8b2..e64c645c7 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -69,13 +69,11 @@
 
 
 @overload
-def safe_decode(s: None) -> None:
-    ...
+def safe_decode(s: None) -> None: ...
 
 
 @overload
-def safe_decode(s: AnyStr) -> str:
-    ...
+def safe_decode(s: AnyStr) -> str: ...
 
 
 def safe_decode(s: Union[AnyStr, None]) -> Optional[str]:
@@ -91,13 +89,11 @@ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]:
 
 
 @overload
-def safe_encode(s: None) -> None:
-    ...
+def safe_encode(s: None) -> None: ...
 
 
 @overload
-def safe_encode(s: AnyStr) -> bytes:
-    ...
+def safe_encode(s: AnyStr) -> bytes: ...
 
 
 def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]:
@@ -113,13 +109,11 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]:
 
 
 @overload
-def win_encode(s: None) -> None:
-    ...
+def win_encode(s: None) -> None: ...
 
 
 @overload
-def win_encode(s: AnyStr) -> bytes:
-    ...
+def win_encode(s: AnyStr) -> bytes: ...
 
 
 def win_encode(s: Optional[AnyStr]) -> Optional[bytes]:
diff --git a/git/index/fun.py b/git/index/fun.py
index 58335739e..beca67d3f 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -286,9 +286,9 @@ def read_cache(
     #   4 bytes length of chunk
     #   Repeated 0 - N times
     extension_data = stream.read(~0)
-    assert (
-        len(extension_data) > 19
-    ), "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data)
+    assert len(extension_data) > 19, (
+        "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data)
+    )
 
     content_sha = extension_data[-20:]
 
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 22b99cb6b..5bd8a3d62 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -152,13 +152,11 @@ def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir:
 
 
 @overload
-def _to_full_path(item: None, path_prefix: str) -> None:
-    ...
+def _to_full_path(item: None, path_prefix: str) -> None: ...
 
 
 @overload
-def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup:
-    ...
+def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: ...
 
 
 def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 731ab5fa1..3964b016c 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -188,7 +188,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     _map_id_to_type: Dict[int, Type[IndexObjUnion]] = {
         commit_id: Submodule,
         blob_id: Blob,
-        symlink_id: Blob
+        symlink_id: Blob,
         # Tree ID added once Tree is defined.
     }
 
diff --git a/git/objects/util.py b/git/objects/util.py
index 71eb9c230..26a34f94c 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -620,8 +620,7 @@ def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TI
         return super()._list_traverse(*args, **kwargs)
 
     @overload  # type: ignore
-    def traverse(self: T_TIobj) -> Iterator[T_TIobj]:
-        ...
+    def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ...
 
     @overload
     def traverse(
@@ -633,8 +632,7 @@ def traverse(
         visit_once: bool,
         ignore_self: Literal[True],
         as_edge: Literal[False],
-    ) -> Iterator[T_TIobj]:
-        ...
+    ) -> Iterator[T_TIobj]: ...
 
     @overload
     def traverse(
@@ -646,8 +644,7 @@ def traverse(
         visit_once: bool,
         ignore_self: Literal[False],
         as_edge: Literal[True],
-    ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]:
-        ...
+    ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: ...
 
     @overload
     def traverse(
@@ -659,8 +656,7 @@ def traverse(
         visit_once: bool,
         ignore_self: Literal[True],
         as_edge: Literal[True],
-    ) -> Iterator[Tuple[T_TIobj, T_TIobj]]:
-        ...
+    ) -> Iterator[Tuple[T_TIobj, T_TIobj]]: ...
 
     def traverse(
         self: T_TIobj,
diff --git a/git/remote.py b/git/remote.py
index fd4de7100..b63cfc208 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -93,22 +93,19 @@ def add_progress(
 
 
 @overload
-def to_progress_instance(progress: None) -> RemoteProgress:
-    ...
+def to_progress_instance(progress: None) -> RemoteProgress: ...
 
 
 @overload
-def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress:
-    ...
+def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ...
 
 
 @overload
-def to_progress_instance(progress: RemoteProgress) -> RemoteProgress:
-    ...
+def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ...
 
 
 def to_progress_instance(
-    progress: Union[Callable[..., Any], RemoteProgress, None]
+    progress: Union[Callable[..., Any], RemoteProgress, None],
 ) -> Union[RemoteProgress, CallableRemoteProgress]:
     """Given the `progress` return a suitable object derived from
     :class:`~git.util.RemoteProgress`."""
diff --git a/git/util.py b/git/util.py
index 9d451eee2..27751f687 100644
--- a/git/util.py
+++ b/git/util.py
@@ -441,13 +441,11 @@ def decygpath(path: PathLike) -> str:
 
 
 @overload
-def is_cygwin_git(git_executable: None) -> Literal[False]:
-    ...
+def is_cygwin_git(git_executable: None) -> Literal[False]: ...
 
 
 @overload
-def is_cygwin_git(git_executable: PathLike) -> bool:
-    ...
+def is_cygwin_git(git_executable: PathLike) -> bool: ...
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
@@ -494,8 +492,7 @@ def finalize_process(proc: Union[subprocess.Popen, "Git.AutoInterrupt"], **kwarg
 
 
 @overload
-def expand_path(p: None, expand_vars: bool = ...) -> None:
-    ...
+def expand_path(p: None, expand_vars: bool = ...) -> None: ...
 
 
 @overload

From 1b8812a968a76f47acdf80bc1d15e6b0dd5d84bf Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Tue, 12 Mar 2024 21:08:54 +0100
Subject: [PATCH 626/994] drop `make lint`

---
 Makefile  | 5 +----
 README.md | 3 ---
 2 files changed, 1 insertion(+), 7 deletions(-)

diff --git a/Makefile b/Makefile
index 71a370ef2..d4f9acf87 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,8 @@
-.PHONY: all lint clean release force_release
+.PHONY: all clean release force_release
 
 all:
 	@awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile
 
-lint:
-	SKIP=pre-commit run --all-files --hook-stage manual
-
 clean:
 	rm -rf build/ dist/ .eggs/ .tox/
 
diff --git a/README.md b/README.md
index 1e4a59d7f..33e093945 100644
--- a/README.md
+++ b/README.md
@@ -165,9 +165,6 @@ To lint, and apply automatic code formatting, run:
 pre-commit run --all-files
 ```
 
-- Linting without modifying code can be done with: `make lint`
-- Auto-formatting without other lint checks can be done with: `black .`
-
 To typecheck, run:
 
 ```bash

From 395b70ae2fdf01590f157e94d96e7ddac7893ba9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Mar 2024 12:12:27 -0400
Subject: [PATCH 627/994] Very slightly improve readme presentation

- Add a missing period.

- Indicate sh rather than bash as as the language for syntax
  highlighting of shell commands that don't need bash.

I had held off on making that second change in previous revisions
because it would have involved either introducing an inconsistency
or editing the section giving the deprecated signature-checking
instructions. That section was removed in 2671167 (#1823).

(This also wraps a paragraph where the immediately surrounding text
was wrapped, but that should not affect rendered text, and broader
consistency improvements to Markdown wrapping style are not done.)
---
 README.md | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/README.md b/README.md
index 33e093945..458162212 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ probably the skills to scratch that itch of mine: implement `git` in a way that
 If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an
 implementation of 'git' in [Rust](https://www.rust-lang.org).
 
-*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required)*
+*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)*
 
 ## GitPython
 
@@ -39,9 +39,9 @@ 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.
-If it is not in your `PATH`, you can help GitPython find it by setting
-the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable.
+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=<path/to/git>` environment variable.
 
 - Git (1.7.x or newer)
 - Python >= 3.7
@@ -57,7 +57,7 @@ GitPython and its required package dependencies can be installed in any of the f
 
 To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run:
 
-```bash
+```sh
 pip install GitPython
 ```
 
@@ -67,7 +67,7 @@ pip install GitPython
 
 If you have downloaded the source code, run this from inside the unpacked `GitPython` directory:
 
-```bash
+```sh
 pip install .
 ```
 
@@ -75,7 +75,7 @@ pip install .
 
 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
+```sh
 git clone https://github.com/gitpython-developers/GitPython
 cd GitPython
 ./init-tests-after-clone.sh
@@ -85,7 +85,7 @@ On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell.
 
 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
+```sh
 gh repo clone GitPython
 ```
 
@@ -93,7 +93,7 @@ Having cloned the repo, create and activate your [virtual environment](https://d
 
 Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs):
 
-```bash
+```sh
 pip install -e ".[test]"
 ```
 
@@ -105,7 +105,7 @@ In rare cases, you may want to work on GitPython and one or both of its [gitdb](
 
 If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
 
-```bash
+```sh
 pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
 ```
 
@@ -141,13 +141,13 @@ you will encounter test failures.
 
 Ensure testing libraries are installed. This is taken care of already if you installed with:
 
-```bash
+```sh
 pip install -e ".[test]"
 ```
 
 Otherwise, you can run:
 
-```bash
+```sh
 pip install -r test-requirements.txt
 ```
 
@@ -155,19 +155,19 @@ pip install -r test-requirements.txt
 
 To test, run:
 
-```bash
+```sh
 pytest
 ```
 
 To lint, and apply automatic code formatting, run:
 
-```bash
+```sh
 pre-commit run --all-files
 ```
 
 To typecheck, run:
 
-```bash
+```sh
 mypy -p git
 ```
 

From 3a6ee9e0e478eea5f6defe2b851a7ca6c74976ca Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Mar 2024 12:31:06 -0400
Subject: [PATCH 628/994] Make installation instructions more consistent

If people who want to run the tests didn't install the test extra,
they can still install that extra. This simplifies the instructions
accordingly. test-requirements.txt is still mentioned near the
beginning in case people want to look at it to see dependencies.
But the changed code is the only place where the instructions had
still said to do anything with those files.

A possible disadvantage of this change is that in the rare case
that someone following those instructions to run the tests locally
didn't do an editable installation, then installing with the extra
shouldn't be done editably either. But this doesn't seem like a
likely problem, and the new text has an example command with -e to
clarify the kind of command whose effect is augmented here.

Because of that subtlety, it is not obvious that this change is
really justified for purposes of clarity. However, this also helps
prepare for a time when test-requirements.txt won't exist anymore,
as may happen when the project definition is made fully declarative
(see discussion in comments in #1716).
---
 README.md | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 458162212..ead8093f8 100644
--- a/README.md
+++ b/README.md
@@ -145,11 +145,8 @@ Ensure testing libraries are installed. This is taken care of already if you ins
 pip install -e ".[test]"
 ```
 
-Otherwise, you can run:
-
-```sh
-pip install -r test-requirements.txt
-```
+If you had installed with a command like `pip install -e .` instead, you can still run
+the above command to add the testing dependencies.
 
 #### Test commands
 

From 826234384d38126130388102355a274a353bfade Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Mar 2024 12:50:41 -0400
Subject: [PATCH 629/994] Update readme for recent tooling changes

This also reorganizes the "Specific tools" list, since they are all
configured in pyproject.toml now (only flake8 was not before, and
it was removed in favor of ruff in #1862). In doing so, I've also
added brief parenthesized phrases to characterize what each of
these four tools is for, so readers don't have to look around as
much to understand most of the tooling GitPython has set up.
---
 README.md | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index ead8093f8..30af532db 100644
--- a/README.md
+++ b/README.md
@@ -156,12 +156,14 @@ To test, run:
 pytest
 ```
 
-To lint, and apply automatic code formatting, run:
+To lint, and apply some linting fixes as well as automatic code formatting, run:
 
 ```sh
 pre-commit run --all-files
 ```
 
+This includes the linting and autoformatting done by Ruff, as well as some other checks.
+
 To typecheck, run:
 
 ```sh
@@ -170,7 +172,7 @@ mypy -p git
 
 #### CI (and tox)
 
-The same linting, and running tests on all the different supported Python versions, will be performed:
+Style and formatting checks, and running tests on all the different supported Python versions, will be performed:
 
 - Upon submitting a pull request.
 - On each push, *if* you have a fork with GitHub Actions enabled.
@@ -178,10 +180,12 @@ The same linting, and running tests on all the different supported Python versio
 
 #### Configuration files
 
-Specific tools:
+Specific tools are all configured in the `./pyproject.toml` file:
 
-- Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`.
-- Configuration for `ruff` is in the `pyproject.toml` file.
+- `pytest` (test runner)
+- `coverage.py` (code coverage)
+- `ruff` (linter and formatter)
+- `mypy` (type checker)
 
 Orchestration tools:
 

From b059cd580b71b44488bf3cc77000a6dea3cb1898 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 12 Mar 2024 13:27:34 -0400
Subject: [PATCH 630/994] Have tox skip linting unless requested, for now

This is to make it so simple `tox` usage has the expected property
of leaving all source code files in the working tree unchanged.

Linting how sometimes performs auto-fixes since #1862, and the
pre-commit command in tox.ini, which had also run `black --check`,
will do even more file editing due to the changes in #1865.

The bifurcation for black into separate mutating and non-mutating
hooks, introduced in 5d8ddd9 (#1693), was not carried over into
Ruff autoformatting in #1865. But also it:

- Was not necessarily a good approach, and likely should not be
  preserved in any form. It was an unusual and unintuitive use of
  pre-commit. (It can be brought back if no better approach is
  found, though.)

- Was done to avoid a situation where it was nontrivial to set up
  necessary dependencies for linting in the GitPython virtual
  environment itself, because flake8 and its various plugins would
  have to be installed.

  They were not listed in any existing or newly introduced extra
  (for example, they were not added to test-requirements.txt) in
  part in the hope that they would all be replaced by Ruff, which
  happened in #1862.

- Already did not achieve its goal as of #1862, since it was
  (probably rightly) not extended to Ruff linting to use/omit --fix.

Now that Ruff is being used, people can run `pip install ruff` in a
virtual environment, then run the `ruff` command however they like.
This takes the place of multiple tools and plugins.

The situation with the tox "lint" environment is thus now similar
to that of the tox "html" environment when it was added in e6ec6c8
(#1667), until it was improved in f094909 (#1693) to run with
proper isolation.
---
 tox.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tox.ini b/tox.ini
index 28b7b147f..6e02e5aee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 requires = tox>=4
-env_list = py{37,38,39,310,311,312}, lint, mypy, html
+env_list = py{37,38,39,310,311,312}, mypy, html
 
 [testenv]
 description = Run unit tests

From f78841870cea95926a95ccaae000bd51769ba412 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 10:46:53 -0400
Subject: [PATCH 631/994] Clean up mention of manual hook stage

This is no longer used. No pre-commit hook specifies it anymore in
`stages`, since 517f83a (#1865). See b059cd5 (#1868) for context.

In the lint.yml GitHub Actions workflow, this removes the
extra_args key altogether, because all that would remain there is
--all-files, which is already the default for that action, when
the extra_args key is absent.
---
 .github/workflows/lint.yml | 2 --
 tox.ini                    | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a32fb6c4e..a0e81a993 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,5 +14,3 @@ jobs:
         python-version: "3.x"
 
     - uses: pre-commit/action@v3.0.1
-      with:
-        extra_args: --all-files --hook-stage manual
diff --git a/tox.ini b/tox.ini
index 6e02e5aee..31ac9382d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ commands = pytest --color=yes {posargs}
 [testenv:lint]
 description = Lint via pre-commit
 base_python = py{39,310,311,312,38,37}
-commands = pre-commit run --all-files --hook-stage manual
+commands = pre-commit run --all-files
 
 [testenv:mypy]
 description = Typecheck with mypy

From 4c034db60e05f73104d265ec53099a8b00522269 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 11:27:07 -0400
Subject: [PATCH 632/994] Split tox "lint" env into three envs, all safe

It makes sense for ruff linting and ruff autoformatting to be
easily runnable individually and to have their results be shown
separately. Splitting them out in tox.ini also makes it so tox can
do the other tests corresponding to those in lint.yml on CI in an
environment that requries no custom behavior from pre-commit other
than skipping the ruff checks.

The ruff linting ("ruff") and ruff format checking ("format") tox
environments specify ruff as a dependency and call it directly
rather than through pre-commit, invoking it in such a way that it
does not attempt to modify any files in the working tree.

See b059cd5 (#1868) for context. All three of these tox envs that
"lint" has been split into are listed in the env_list and thus run
automatically when tox is run with no arguments, since no tox envs
unexpectedly (or at all) modify files in the working tree anymore.

One limitation of the current approach is that new pre-commit hooks
configured in .pre-commit-config.yml will automatically be run as
part of the "misc" tox environment, which means that new unexpected
mutating operatons could be added if the impact on tox is not
considered. The benefit of having it work this way is that most
hooks that are likely to be added to GitPython would not modify
files and would be wanted as part of "misc". But this may benefit
from further refinement.
---
 tox.ini | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/tox.ini b/tox.ini
index 31ac9382d..1fc7e2415 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 requires = tox>=4
-env_list = py{37,38,39,310,311,312}, mypy, html
+env_list = py{37,38,39,310,311,312}, ruff, format, mypy, html, misc
 
 [testenv]
 description = Run unit tests
@@ -9,10 +9,17 @@ extras = test
 pass_env = SSH_*
 commands = pytest --color=yes {posargs}
 
-[testenv:lint]
-description = Lint via pre-commit
+[testenv:ruff]
+description = Lint with Ruff
 base_python = py{39,310,311,312,38,37}
-commands = pre-commit run --all-files
+deps = ruff
+commands = ruff check .
+
+[testenv:format]
+description = Check formatting with Ruff
+base_python = py{39,310,311,312,38,37}
+deps = ruff
+commands = ruff format --check .
 
 [testenv:mypy]
 description = Typecheck with mypy
@@ -28,3 +35,10 @@ allowlist_externals = make
 commands =
     make BUILDDIR={env_tmp_dir}/doc/build -C doc clean
     make BUILDDIR={env_tmp_dir}/doc/build -C doc html
+
+[testenv:misc]
+description = Run other checks via pre-commit
+base_python = py{39,310,311,312,38,37}
+set_env =
+    SKIP = ruff-format,ruff
+commands = pre-commit run --all-files

From dcbd5dbc38d95dfd31dd5897cb7511a7fda0cce3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 11:38:44 -0400
Subject: [PATCH 633/994] Colorize ruff output when run through tox

In most other situations it is typically already colorized.

This also includes comments about how to override this to suppress
color.
---
 tox.ini | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tox.ini b/tox.ini
index 1fc7e2415..48c9ed2f9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,12 +13,16 @@ commands = pytest --color=yes {posargs}
 description = Lint with Ruff
 base_python = py{39,310,311,312,38,37}
 deps = ruff
+set_env =
+    CLICOLOR_FORCE = 1  # Set NO_COLOR to override this.
 commands = ruff check .
 
 [testenv:format]
 description = Check formatting with Ruff
 base_python = py{39,310,311,312,38,37}
 deps = ruff
+set_env =
+    CLICOLOR_FORCE = 1  # Set NO_COLOR to override this.
 commands = ruff format --check .
 
 [testenv:mypy]

From 89e519e05ca3342c3747b19901b8bb6c1d2b7f58 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 11:47:50 -0400
Subject: [PATCH 634/994] Colorize mypy output when run through tox

On Unix-like systems, this approach is currently only sufficient if
the TERM environment variable is set. In practice this means that
if tox is run on CI, color will not be shown. Because GitPython
does not itself use tox on CI, and other (e.g. downstream) projects
may not want color on CI or in other situations where this would be
insufficient, the further step of defining TERM as a workaround is
deliberately omitted.

See aeacb0 in #1859 for what adding TERM would look like. This does
not make any changes to how mypy runs on CI because that change is
already included there.
---
 tox.ini | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tox.ini b/tox.ini
index 48c9ed2f9..d6c23bf60 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,6 +28,8 @@ commands = ruff format --check .
 [testenv:mypy]
 description = Typecheck with mypy
 base_python = py{39,310,311,312,38,37}
+set_env =
+    MYPY_FORCE_COLOR = 1
 commands = mypy -p git
 ignore_outcome = true
 

From 00ff7e33c4c44adb9d75f92a1590cb3e3dc45729 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 13:17:39 -0400
Subject: [PATCH 635/994] Let Ruff scan doc/source/conf.py

And other .py files in doc/, if there ever were any.

This may have been turned off before either for performance or out
of a concern than some flake8 plugin would catch something in the
docs themselves, but that's no longer an issue.

The doc/build directory, which should still not be scanned, is
already excluded in other ways (and does not need to be excluded
for pre-commit to improve performance, since it is untracked).
---
 .pre-commit-config.yaml | 2 +-
 pyproject.toml          | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 60bbe3518..f269678e4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,7 @@ repos:
     exclude: ^git/ext/
   - id: ruff
     args: ["--fix"]
-    exclude: ^doc|^git/ext/
+    exclude: ^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
   rev: v0.9.0.5
diff --git a/pyproject.toml b/pyproject.toml
index eb57cc7b7..e8f3d720e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,6 @@ line-length = 120
 # Exclude a variety of commonly ignored directories.
 exclude = [
     "git/ext/",
-    "doc",
     "build",
     "dist",
 ]

From 4814775bfee418d7e0cc9ffb0ac30a4e44a48d75 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 13:32:28 -0400
Subject: [PATCH 636/994] Comment all Ruff rule codes; tweak formatting

This adds comments to all specific Ruff rules suppressed in
pyproject.toml (most of which are global but one is for the tests
only). Some already had such comments but now all do.

This also harmonizes the toml formatting style with the rest of
pyproject.toml.
---
 pyproject.toml | 30 +++++++++++++++++-------------
 1 file changed, 17 insertions(+), 13 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index e8f3d720e..57ae01058 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,24 +54,28 @@ exclude = [
 # Enable Pyflakes `E` and `F` codes by default.
 lint.select = [
     "E",
-    "W", # see: https://pypi.org/project/pycodestyle
-    "F", # see: https://pypi.org/project/pyflakes
-#    "I", #see: https://pypi.org/project/isort/
-#    "S", # see: https://pypi.org/project/flake8-bandit
-#    "UP", # see: https://docs.astral.sh/ruff/rules/#pyupgrade-up
+    "W",     # See: https://pypi.org/project/pycodestyle
+    "F",     # See: https://pypi.org/project/pyflakes
+    # "I",   # See: https://pypi.org/project/isort/
+    # "S",   # See: https://pypi.org/project/flake8-bandit
+    # "UP",  # See: https://docs.astral.sh/ruff/rules/#pyupgrade-up
 ]
 lint.extend-select = [
-    #"A",    # see: https://pypi.org/project/flake8-builtins
-    "B",    # see: https://pypi.org/project/flake8-bugbear
-    "C4",   # see: https://pypi.org/project/flake8-comprehensions
-    "TCH004", # see: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
+    # "A",     # See: https://pypi.org/project/flake8-builtins
+    "B",       # See: https://pypi.org/project/flake8-bugbear
+    "C4",      # See: https://pypi.org/project/flake8-comprehensions
+    "TCH004",  # See: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
 ]
 lint.ignore = [
-    "E203",
-    "E731", # Do not assign a `lambda` expression, use a `def`
+    "E203",  # Whitespace before ':'
+    "E731",  # Do not assign a `lambda` expression, use a `def`
 ]
 lint.ignore-init-module-imports = true
-lint.unfixable = ["F401"]
+lint.unfixable = [
+    "F401",  # Module imported but unused
+]
 
 [tool.ruff.lint.per-file-ignores]
-"test/**" = ["B018"]
+"test/**" = [
+    "B018",  # useless-expression
+]

From a8a73ff739785a01a528b2a274c8252456a1ebc2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 13:44:57 -0400
Subject: [PATCH 637/994] Update requirements-dev.txt (as long as we have it)

Although it seems likely that the requirements-dev.txt file will be
removed when the project definition is made declarative (discussed
in #1716 comments), if not before, for now it exists and might be
in use, so this updates it with tools that are currently used but
not listed in any extras or other requirements files:

- ruff: This has replaced flake8 and its plugins (#1862) as well as
  black (#1865). Currently there is no separate extra for tooling
  that is not part of unit testing, with some such tools listed in
  test-requirements.txt. The `ruff` package belongs here rather
  than there for now because it should not be installed just to run
  unit tests, since Ruff has to be built from source on some rarer
  platforms like Cygwin, which would take a long time and/or fail.

- shellcheck: The PyPI package for this is a convenience for
  installing it in projects that are already using pip (shellcheck
  is neither written in Python nor a tool to scan Python code). It
  installs pre-built binaries, which are not available for all
  platforms.

These packages remain listed:

- pytest-icdiff: This seems not to have been promoted to be in the
  test-requirements.txt file and the `test` extra because it does
  not work as well for diffs from tests that the pytest runner runs
  but that are written for the unittest framework.

- pytest-profiling [commented out]: I am not sure what the status
  of this is, perhaps it has just not been experimented with
  enough to know if it would be useful for profiling in GitPython.

This requirements-dev.txt file has a few limitations that suggest
it should be removed altogether sometime soon:

- It is not updated regularly.

- It is not always clear why something is there. Originally I
  believe it was for tools where the desire to use the tool was
  established but the tool did not yet work or worked but performed
  checks for which code had to be fixed. That purpose has drifted.

- It uses a different naming convention from the
  test-requirements.txt file in active use.

- It cannot be readily used to create an extra in the current
  project definition in setup.py because the simple parsing done
  there will not recognize the `-r` lines and will not skip the
  comments, and neither enhancement should be done in setup.py
  since that would move things farther away from a declarative
  project definition.

- It will naturally go away when the project definition is made
  declarative, since it will then be feasible to define as many
  extras as desired without proliferating separate requirements
  files (while still allowing their contents to be statically
  available to tools).

Since it may go away soon and is not regularly updated, I have kept
the explanations for why particular packages are there out of it.
But as long as it exists it may as well list the tools that really
are being used yet are not explicitly listed as dependencies.
---
 requirements-dev.txt | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 69a79d13d..f626644af 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,8 @@
 -r requirements.txt
 -r test-requirements.txt
 
-# libraries for additional local testing/linting - to be added to test-requirements.txt when all pass
-
+# For additional local testing/linting - to be added elsewhere eventually.
+ruff
+shellcheck
 pytest-icdiff
 # pytest-profiling

From ff1ebf8fe05f2b5bace15dc7edbc6bdc4aea2222 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 14:11:16 -0400
Subject: [PATCH 638/994] Bump pre-commit hook versions

The changes, per hook repository, are:

- ruff-pre-commit: Uses the newest stable version of Ruff. See:
  * https://github.com/astral-sh/ruff-pre-commit/releases/tag/v0.3.2

- shellcheck-py: Makes ShellCheck installable on ARM64 Windows
  machines. See:
  * https://github.com/shellcheck-py/shellcheck-py/compare/v0.9.0.5...v0.9.0.6
  * https://github.com/shellcheck-py/shellcheck-py/commit/e39ba22d34cf877af3e6529a253a6cfd3e9f8ea7

- pre-commit-hooks: Offers no relevant changes for the few hooks
  currently being used from that repository, but I went ahead and
  included this while bumping the others. This may be a small
  benefit if more hooks are added from it sometime soon and because
  we don't currently have a mechanism for keeping track of updates
  that are skipped as unneeded.
---
 .pre-commit-config.yaml | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f269678e4..585b4f04d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,6 @@
 repos:
-
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.3.0
+  rev: v0.3.2
   hooks:
   - id: ruff-format
     exclude: ^git/ext/
@@ -10,14 +9,14 @@ repos:
     exclude: ^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
-  rev: v0.9.0.5
+  rev: v0.9.0.6
   hooks:
   - id: shellcheck
     args: [--color]
     exclude: ^test/fixtures/polyglot$|^git/ext/
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.4.0
+  rev: v4.5.0
   hooks:
   - id: check-toml
   - id: check-yaml

From 1b43166951caac29ed223c9d1522534626b3598e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 17:09:03 -0400
Subject: [PATCH 639/994] Group setup.py imports

While setuptools has official status, it is not actually part of
the standard library (and since Python 3.12 cannot be treated as if
it is, since it is not installed by default), so its imports belong
in the second group rather than the first, per PEP-8.
---
 setup.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index 73d1ae952..e827d2483 100755
--- a/setup.py
+++ b/setup.py
@@ -1,11 +1,13 @@
 #!/usr/bin/env python
 
+import os
+import sys
 from typing import Sequence
+
 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 os
-import sys
+
 
 with open(os.path.join(os.path.dirname(__file__), "VERSION"), encoding="utf-8") as ver_file:
     VERSION = ver_file.readline().strip()

From 26dccd70713b2be6cb0af6892fd05703f17cda3e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 17:49:56 -0400
Subject: [PATCH 640/994] Streamline setup.py file reading

This is partly a refactoring, reducing logic duplication and
slightly decreasing overall length of the code that reads values
from files. However, it does make two small behavioral changes:

- In addition to the VERSION file for which this was already the
  case, requirements files and the readme are also taken relative
  to the directory that contains the setup.py file, rather than
  relative to the current directory if different. Supporting
  reading the readme and dependencies realtive to a different
  directory doesn't seem intended, and could lead to confusion in
  some cases.

- The VERSION file is now read entirely and whitespace stripped,
  rather than reading just the first line. The VERSION file should
  always contain exactly one line, and this logic is simpler.

This changes code for the reading of metadata only. It does not
make any changes to the version stamping code.
---
 setup.py | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/setup.py b/setup.py
index e827d2483..3f57e3327 100755
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,7 @@
 
 import os
 import sys
+from pathlib import Path
 from typing import Sequence
 
 from setuptools import setup, find_packages
@@ -9,17 +10,14 @@
 from setuptools.command.sdist import sdist as _sdist
 
 
-with open(os.path.join(os.path.dirname(__file__), "VERSION"), encoding="utf-8") as ver_file:
-    VERSION = ver_file.readline().strip()
+def _read_content(path: str) -> str:
+    return (Path(__file__).parent / path).read_text(encoding="utf-8")
 
-with open("requirements.txt", encoding="utf-8") as reqs_file:
-    requirements = reqs_file.read().splitlines()
 
-with open("test-requirements.txt", encoding="utf-8") as reqs_file:
-    test_requirements = reqs_file.read().splitlines()
-
-with open("README.md", encoding="utf-8") as rm_file:
-    long_description = rm_file.read()
+version = _read_content("VERSION").strip()
+requirements = _read_content("requirements.txt").splitlines()
+test_requirements = _read_content("test-requirements.txt").splitlines()
+long_description = _read_content("README.md")
 
 
 class build_py(_build_py):
@@ -50,7 +48,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:
@@ -66,7 +64,7 @@ def _stamp_version(filename: str) -> None:
 setup(
     name="GitPython",
     cmdclass={"build_py": build_py, "sdist": sdist},
-    version=VERSION,
+    version=version,
     description="GitPython is a Python library used to interact with Git repositories",
     author="Sebastian Thiel, Michael Trier",
     author_email="byronimo@gmail.com, mtrier@gmail.com",

From 74df5a8995b6f4e9ed053e126dda1cb6cfc465f5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 18:29:50 -0400
Subject: [PATCH 641/994] Add a "doc" extra for documentation build
 dependencies

And use it in:

- GitHub Actions CI checks
- Read the Docs configuration
- tox (for the "html" environment)

(The tox "html" environment was not previously overriding "extras"
to empty it out of dependencies needed only for linting or testing,
so this happens to make `tox -e html` faster.)
---
 .github/workflows/pythonpackage.yml | 2 +-
 .readthedocs.yaml                   | 3 ++-
 setup.py                            | 6 +++++-
 tox.ini                             | 2 +-
 4 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 6b89530c3..4ef741c36 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -100,5 +100,5 @@ jobs:
 
     - name: Documentation
       run: |
-        pip install -r doc/requirements.txt
+        pip install ".[doc]"
         make -C doc html
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 0b83e20ea..9bce80fd2 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -32,4 +32,5 @@ python:
   install:
   - method: pip
     path: .
-  - requirements: doc/requirements.txt
+    extra_requirements:
+    - doc
diff --git a/setup.py b/setup.py
index 3f57e3327..143206653 100755
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,7 @@ def _read_content(path: str) -> str:
 version = _read_content("VERSION").strip()
 requirements = _read_content("requirements.txt").splitlines()
 test_requirements = _read_content("test-requirements.txt").splitlines()
+doc_requirements = _read_content("doc/requirements.txt").splitlines()
 long_description = _read_content("README.md")
 
 
@@ -75,7 +76,10 @@ def _stamp_version(filename: str) -> None:
     package_dir={"git": "git"},
     python_requires=">=3.7",
     install_requires=requirements,
-    extras_require={"test": test_requirements},
+    extras_require={
+        "test": test_requirements,
+        "doc": doc_requirements,
+    },
     zip_safe=False,
     long_description=long_description,
     long_description_content_type="text/markdown",
diff --git a/tox.ini b/tox.ini
index 6e02e5aee..dfcb5ed8f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -23,7 +23,7 @@ ignore_outcome = true
 [testenv:html]
 description = Build HTML documentation
 base_python = py{39,310,311,312,38,37}
-deps = -r doc/requirements.txt
+extras = doc
 allowlist_externals = make
 commands =
     make BUILDDIR={env_tmp_dir}/doc/build -C doc clean

From 1541c62d86e334980d008d1e8ff0c6623ae2e2a0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 11 Mar 2024 01:06:17 -0400
Subject: [PATCH 642/994] Start on fixing Submodule parent_commit annotations

These used the old Commit_ish. They may not all be the same as each
other, since they perform different validation and some will look
up a commit while others do not. In particular, this is complicated
by how Submodile.__init__ documents its parent_commit parameter: it
says to see the set_parent_commit method. But __init__ sets the
_parent_commit attribute to parent_commit, while set_parent_commit
treats its commit parameter differently, calling self.repo.commit
on it to get get a Commit object from it even if it wasn't one to
begin with. See #1869 for full details.

There may be some other subtleties as well. A number of uses of the
of the old Commit_ish type appear in git.submodule.base, all
related to parent_commit. These are all remaining references to it
(identifiable due to the rename to Old_commit_ish done in 04a2753),
though. So once the changes begun here for git.submodule.* are
done, that will also have completed the broader replacement effort
begun in 7328a00. At that point cleanup can be done in git.types
(removing Old_commit_ish introduced in 04a27531, replacing
Lit_old_commit_ish with a possibly-updated and deprecated
Lit_commit_ish to avoid a breaking change, and possibly making
further refinements to additions to the newly added docstrings).
---
 git/objects/submodule/base.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 783990664..2a8c62b70 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -45,12 +45,13 @@
 from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
 from typing import Any, Iterator, Union
 
-from git.types import Old_commit_ish, Literal, PathLike, TBD
+from git.types import Commit_ish, Literal, Old_commit_ish, PathLike, TBD
 
 if TYPE_CHECKING:
     from git.index import IndexFile
-    from git.repo import Repo
+    from git.objects.commit import Commit
     from git.refs import Head
+    from git.repo import Repo
 
 # -----------------------------------------------------------------------------
 
@@ -99,7 +100,7 @@ class Submodule(IndexObject, TraversableIterableObj):
     """Submodule flags. Submodules are directories with link-status."""
 
     type: Literal["submodule"] = "submodule"  # type: ignore
-    """This is a bogus type for base class compatibility."""
+    """This is a bogus type string for base class compatibility."""
 
     __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
 
@@ -1242,7 +1243,7 @@ def remove(
 
         return self
 
-    def set_parent_commit(self, commit: Union[Old_commit_ish, None], check: bool = True) -> "Submodule":
+    def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
         """Set this instance to use the given commit whose tree is supposed to
         contain the ``.gitmodules`` blob.
 
@@ -1495,7 +1496,7 @@ def url(self) -> str:
         return self._url
 
     @property
-    def parent_commit(self) -> "Old_commit_ish":
+    def parent_commit(self) -> "Commit":
         """
         :return:
             :class:`~git.objects.commit.Commit` instance with the tree containing the
@@ -1557,8 +1558,8 @@ def children(self) -> IterableList["Submodule"]:
     def iter_items(
         cls,
         repo: "Repo",
-        parent_commit: Union[Old_commit_ish, str] = "HEAD",
-        *Args: Any,
+        parent_commit: Union[Commit_ish, str] = "HEAD",
+        *args: Any,
         **kwargs: Any,
     ) -> Iterator["Submodule"]:
         """

From 1f03e7fc23745c64de7a54a92c79ddfa96cd1025 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 13 Mar 2024 23:15:08 -0400
Subject: [PATCH 643/994] Fix other submodule.base parent_commit annotations

This finishes the work in git.objects.submodule.base begun in
1541c62. Some remaining occurrences of the old Commit_ish type are
in git.objects.submodule.root, which still need to be replaced.

Even once that is done, this will not have fixed #1869, though it
will be somewhat mitigated because each method's annotations
related to parent_commit will reflect the types that can actually
be used reliably under the current implementation, even if in some
cases that may not be as broad as was meant, or hoped, to be
supported. I expect this may lead the way to a fix for #1869, since
the existing behavior and relationships will be clearer.

Where the old Commit_ish annotations were changed here to "Commit",
it reflects assumptions found by examining the implementation that
other alternatives are not usable becuse something beloning to a
Commit is used and no conversion or fallback is performed for it.
Where a commit is looked up (which uses rev_parse), broader types
are annotated. Where the argument is used without conversion and
must have a `tree` attribute, it must be a Commit. Where the
argument could be broader but must be compared meaningfully with
something known to be a commit, it must at minimum have a binsha
attribute to allow that equality comparison, implemented in Object,
to be meaningful, so in such cases its type cannot include str.
---
 git/objects/submodule/base.py | 24 ++++++++++++++++--------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 2a8c62b70..61757092a 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -42,10 +42,19 @@
 
 # typing ----------------------------------------------------------------------
 
-from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast
-from typing import Any, Iterator, Union
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Iterator,
+    Mapping,
+    Sequence,
+    TYPE_CHECKING,
+    Union,
+    cast,
+)
 
-from git.types import Commit_ish, Literal, Old_commit_ish, PathLike, TBD
+from git.types import Commit_ish, Literal, PathLike, TBD
 
 if TYPE_CHECKING:
     from git.index import IndexFile
@@ -113,7 +122,7 @@ def __init__(
         mode: Union[int, None] = None,
         path: Union[PathLike, None] = None,
         name: Union[str, None] = None,
-        parent_commit: Union[Old_commit_ish, None] = None,
+        parent_commit: Union["Commit", None] = None,
         url: Union[str, None] = None,
         branch_path: Union[PathLike, None] = None,
     ) -> None:
@@ -145,7 +154,6 @@ def __init__(
         if url is not None:
             self._url = url
         if branch_path is not None:
-            # assert isinstance(branch_path, str)
             self._branch_path = branch_path
         if name is not None:
             self._name = name
@@ -214,7 +222,7 @@ def __repr__(self) -> str:
 
     @classmethod
     def _config_parser(
-        cls, repo: "Repo", parent_commit: Union[Old_commit_ish, None], read_only: bool
+        cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
     ) -> SubmoduleConfigParser:
         """
         :return:
@@ -265,7 +273,7 @@ def _clear_cache(self) -> None:
         # END for each name to delete
 
     @classmethod
-    def _sio_modules(cls, parent_commit: Old_commit_ish) -> BytesIO:
+    def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
         """
         :return:
             Configuration file as :class:`~io.BytesIO` - we only access it through the
@@ -278,7 +286,7 @@ def _sio_modules(cls, parent_commit: Old_commit_ish) -> BytesIO:
     def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
         """:return: Config parser constrained to our submodule in read or write mode"""
         try:
-            pc: Union["Old_commit_ish", None] = self.parent_commit
+            pc = self.parent_commit
         except ValueError:
             pc = None
         # END handle empty parent repository

From e66b8f1598343077e91fbb1c5483e22cdcdf0faa Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 00:07:47 -0400
Subject: [PATCH 644/994] Fix old Commit_ish annotation in RootModule

This finishes the annotation fixup begun in 7328a00. (The temporary
Old_commit_ish type is now ready to be removed.)
---
 git/objects/submodule/root.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index 8e697ee1d..f6a8c6807 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -1,18 +1,18 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import logging
+
+import git
+from git.exc import InvalidGitRepositoryError
 from .base import Submodule, UpdateProgress
 from .util import find_first_remote_branch
-from git.exc import InvalidGitRepositoryError
-import git
-
-import logging
 
 # typing -------------------------------------------------------------------
 
 from typing import TYPE_CHECKING, Union
 
-from git.types import Old_commit_ish
+from git.types import Commit_ish
 
 if TYPE_CHECKING:
     from git.repo import Repo
@@ -77,7 +77,7 @@ def _clear_cache(self) -> None:
 
     def update(  # type: ignore[override]
         self,
-        previous_commit: Union[Old_commit_ish, None] = None,
+        previous_commit: Union[Commit_ish, str, None] = None,
         recursive: bool = True,
         force_remove: bool = False,
         init: bool = True,

From 93d19dcbc9a44607420f3d47c602419efc7b4b82 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 00:25:46 -0400
Subject: [PATCH 645/994] Remove the temporary Old_commit_ish type

---
 git/types.py | 25 -------------------------
 1 file changed, 25 deletions(-)

diff --git a/git/types.py b/git/types.py
index 95a5816f3..d883d7324 100644
--- a/git/types.py
+++ b/git/types.py
@@ -126,31 +126,6 @@
 gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type
 """
 
-# FIXME: Replace uses with AnyGitObject and Commit_ish, and remove this.
-Old_commit_ish = Union["Commit", "TagObject", "Blob", "Tree"]
-"""Union of the :class:`~git.objects.base.Object`-based types that represent git object
-types. This union is often usable where a commit-ish is expected, but is not actually
-limited to types representing commit-ish git objects.
-
-See gitglossary(7) on:
-
-* "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish
-* "object type": https://git-scm.com/docs/gitglossary#def_object_type
-
-:note:
-    This union comprises **more** classes than those whose instances really represent
-    commit-ish git objects:
-
-    * A :class:`~git.objects.commit.Commit` is of course always commit-ish, and a
-      :class:`~git.objects.tag.TagObject` is commit-ish if, when peeled (recursively
-      followed), a :class:`~git.objects.commit.Commit` is obtained.
-    * However, :class:`~git.objects.blob.Blob` and :class:`~git.objects.tree.Tree` are
-      also included, and they represent git objects that are never really commit-ish.
-
-    This is an inversion of the situation with :class:`Tree_ish`, which is narrower than
-    all tree-ish objects. It is done for practical reasons including backward
-    compatibility.
-"""
 
 # FIXME: After replacing the one use with GitObjectTypeString, define Lit_commit_ish
 #        somehow (it is a breaking change to remove it entirely). Maybe deprecate it.

From ebcfced0eebe0008ac5c11dad8b1fea7091424bd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 01:11:05 -0400
Subject: [PATCH 646/994] Fix and deprecate Lit_commit_ish

It would be a breaking change to keep Lit_commit_ish undefined, so
this brings it back, but with a narrowed definition for consistency
with the corrected Commit_ish, and deprecated, with instructions on
what to use instead.

(This also improves import sorting in the same module.)
---
 git/types.py | 37 +++++++++++++++++--------------------
 1 file changed, 17 insertions(+), 20 deletions(-)

diff --git a/git/types.py b/git/types.py
index d883d7324..336f49082 100644
--- a/git/types.py
+++ b/git/types.py
@@ -4,38 +4,38 @@
 import os
 import sys
 from typing import (  # noqa: F401
+    Any,
+    Callable,
     Dict,
     NoReturn,
+    Optional,
     Sequence as Sequence,
     Tuple,
-    Union,
-    Any,
-    Optional,
-    Callable,
     TYPE_CHECKING,
     TypeVar,
+    Union,
 )
 
 if sys.version_info >= (3, 8):
     from typing import (  # noqa: F401
         Literal,
-        TypedDict,
         Protocol,
         SupportsIndex as SupportsIndex,
+        TypedDict,
         runtime_checkable,
     )
 else:
     from typing_extensions import (  # noqa: F401
         Literal,
+        Protocol,
         SupportsIndex as SupportsIndex,
         TypedDict,
-        Protocol,
         runtime_checkable,
     )
 
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.objects import Commit, Tree, TagObject, Blob
+    from git.repo import Repo
 
 PathLike = Union[str, "os.PathLike[str]"]
 """A :class:`str` (Unicode) based file or directory path."""
@@ -126,22 +126,19 @@
 gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type
 """
 
+Lit_commit_ish = Literal["commit", "tag"]
+"""Deprecated. Type of literal strings identifying sometimes-commitish git object types.
 
-# FIXME: After replacing the one use with GitObjectTypeString, define Lit_commit_ish
-#        somehow (it is a breaking change to remove it entirely). Maybe deprecate it.
-Lit_old_commit_ish = Literal["commit", "tag", "blob", "tree"]
-"""Literal strings identifying concrete :class:`~git.objects.base.Object` subtypes
-representing git object types.
-
-See the :class:`Object.type <git.objects.base.Object.type>` attribute.
+Prior to a bugfix, this type had been defined more broadly. Any usage is in practice
+ambiguous and likely to be incorrect. Instead of this type:
 
-:note:
-    See also :class:`Old_commit_ish`, a union of the the :class:`~git.objects.base.Object`
-    subtypes associated with these literal strings.
+* For the type of the string literals associated with :class:`Commit_ish`, use
+  ``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
+  this type as currently defined.
 
-:note:
-    As noted in :class:`Old_commit_ish`, this is not limited to types of git objects that
-    are actually commit-ish.
+* For the type of all four string literals associated with :class:`AnyGitObject`, use
+  :class:`GitObjectTypeString`. That is equivalent to the old definition of this type
+  prior to the bugfix.
 """
 
 # Config_levels ---------------------------------------------------------

From b070e933fef5d1e86d3fb43c1613c03a760bbc50 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 01:37:27 -0400
Subject: [PATCH 647/994] Make some broad mypy suppressions more specific

- Change every bare `# type: ignore` into `# type: ignore[reason]`,
  where the specific reason is verified by mypy.

- Use separate `# type: ignore[X]` and `# type: ignore[Y]` for
  different parts of statement where a single bare suppression was
  doing double duty, with each suppression only on the specific
  parts (splitting the statement into multiple lines for this).

- Use the same suppression twice on very long statements where only
  specific parts need suppressions.

This also formats all these comments with the same spacing (most
but not all already were). This makes them easier to grep for.
---
 git/__init__.py               |  2 +-
 git/cmd.py                    | 10 +++++-----
 git/objects/commit.py         |  8 ++++++--
 git/objects/submodule/base.py |  2 +-
 git/objects/tree.py           |  2 +-
 git/objects/util.py           | 10 +++++++++-
 git/refs/remote.py            |  2 +-
 git/refs/symbolic.py          | 19 ++++++++++++++++---
 git/repo/base.py              |  2 +-
 git/util.py                   |  4 ++--
 10 files changed, 43 insertions(+), 18 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index 026dc1461..ca5bed7a3 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -147,7 +147,7 @@ def refresh(path: Optional[PathLike] = None) -> None:
     if not Git.refresh(path=path):
         return
     if not FetchInfo.refresh():  # noqa: F405
-        return  # type: ignore [unreachable]
+        return  # type: ignore[unreachable]
 
     GIT_OK = True
 
diff --git a/git/cmd.py b/git/cmd.py
index 85398497f..74693b27a 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -172,7 +172,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)  # type: ignore [redundant-cast]
+        process = cast(Popen, process)  # type: ignore[redundant-cast]
         cmdline = getattr(process, "args", "")
         p_stdout = process.stdout
         p_stderr = process.stderr
@@ -215,7 +215,7 @@ def pump_stream(
                     error_str = error_str.encode()
                 # We ignore typing on the next line because mypy does not like the way
                 # we inferred that stderr takes str or bytes.
-                stderr_handler(error_str)  # type: ignore
+                stderr_handler(error_str)  # type: ignore[arg-type]
 
     if finalizer:
         finalizer(process)
@@ -1244,9 +1244,9 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
             if output_stream is None:
                 stdout_value, stderr_value = communicate()
                 # Strip trailing "\n".
-                if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore
+                if stdout_value.endswith(newline) and strip_newline_in_stdout:  # type: ignore[arg-type]
                     stdout_value = stdout_value[:-1]
-                if stderr_value.endswith(newline):  # type: ignore
+                if stderr_value.endswith(newline):  # type: ignore[arg-type]
                     stderr_value = stderr_value[:-1]
 
                 status = proc.returncode
@@ -1256,7 +1256,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
                 stdout_value = proc.stdout.read()
                 stderr_value = proc.stderr.read()
                 # Strip trailing "\n".
-                if stderr_value.endswith(newline):  # type: ignore
+                if stderr_value.endswith(newline):  # type: ignore[arg-type]
                     stderr_value = stderr_value[:-1]
                 status = proc.wait()
             # END stdout handling
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 6d3864be6..31aaf60f4 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -428,7 +428,11 @@ def trailers_list(self) -> List[Tuple[str, str]]:
             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
+        proc: Git.AutoInterrupt = self.repo.git.execute(  # type: ignore[call-overload]
+            cmd,
+            as_process=True,
+            istream=PIPE,
+        )
         trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
         trailer = trailer.strip()
 
@@ -507,7 +511,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)  # type: ignore [redundant-cast]
+            proc_or_stream = cast(IO, proc_or_stream)  # type: ignore[redundant-cast]
             stream = proc_or_stream
 
         readline = stream.readline
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 61757092a..f9e0a8e0f 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -108,7 +108,7 @@ class Submodule(IndexObject, TraversableIterableObj):
     k_default_mode = stat.S_IFDIR | stat.S_IFLNK
     """Submodule flags. Submodules are directories with link-status."""
 
-    type: Literal["submodule"] = "submodule"  # type: ignore
+    type: Literal["submodule"] = "submodule"  # type: ignore[assignment]
     """This is a bogus type string for base class compatibility."""
 
     __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 30b6a9e4e..e9c08a7cd 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -391,7 +391,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
         return False
 
     def __reversed__(self) -> Iterator[IndexObjUnion]:
-        return reversed(self._iter_convert_to_object(self._cache))  # type: ignore
+        return reversed(self._iter_convert_to_object(self._cache))  # type: ignore[call-overload]
 
     def _serialize(self, stream: "BytesIO") -> "Tree":
         """Serialize this tree into the stream. Assumes sorted tree data.
diff --git a/git/objects/util.py b/git/objects/util.py
index 194403ba5..576aca406 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -692,5 +692,13 @@ def traverse(
 
         return cast(
             Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]],
-            super()._traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge),  # type: ignore
+            super()._traverse(
+                predicate,  # type: ignore[arg-type]
+                prune,  # type: ignore[arg-type]
+                depth,
+                branch_first,
+                visit_once,
+                ignore_self,
+                as_edge,
+            ),
         )
diff --git a/git/refs/remote.py b/git/refs/remote.py
index bb2a4e438..5cbd1b81b 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -52,7 +52,7 @@ def iter_items(
     # subclasses and recommends Any or "type: ignore".
     # (See: https://github.com/python/typing/issues/241)
     @classmethod
-    def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore
+    def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:  # type: ignore[override]
         """Delete the given remote references.
 
         :note:
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 4a39875a7..1f1daa11b 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -387,8 +387,17 @@ def set_object(
         # set the commit on our reference
         return self._get_reference().set_object(object, logmsg)
 
-    commit = property(_get_commit, set_commit, doc="Query or set commits directly")  # type: ignore
-    object = property(_get_object, set_object, doc="Return the object our ref currently refers to")  # type: ignore
+    commit = property(
+        _get_commit,
+        set_commit,  # type: ignore[arg-type]
+        doc="Query or set commits directly",
+    )
+
+    object = property(
+        _get_object,
+        set_object,  # type: ignore[arg-type]
+        doc="Return the object our ref currently refers to",
+    )
 
     def _get_reference(self) -> "SymbolicReference":
         """
@@ -488,7 +497,11 @@ def set_reference(
 
     # Aliased reference
     reference: Union["Head", "TagReference", "RemoteReference", "Reference"]
-    reference = property(_get_reference, set_reference, doc="Returns the Reference we point to")  # type: ignore
+    reference = property(  # type: ignore[assignment]
+        _get_reference,
+        set_reference,  # type: ignore[arg-type]
+        doc="Returns the Reference we point to",
+    )
     ref = reference
 
     def is_valid(self) -> bool:
diff --git a/git/repo/base.py b/git/repo/base.py
index 5e7645366..fe01a9279 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -636,7 +636,7 @@ 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]
+            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 7ebce0c2c..8b60b1cb2 100644
--- a/git/util.py
+++ b/git/util.py
@@ -517,7 +517,7 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[
     if isinstance(p, pathlib.Path):
         return p.resolve()
     try:
-        p = osp.expanduser(p)  # type: ignore
+        p = osp.expanduser(p)  # type: ignore[arg-type]
         if expand_vars:
             p = osp.expandvars(p)
         return osp.normpath(osp.abspath(p))
@@ -1209,7 +1209,7 @@ def __getattr__(self, attr: str) -> T_IterableObj:
         # END for each item
         return list.__getattribute__(self, attr)
 
-    def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj:  # type: ignore
+    def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj:  # type: ignore[override]
         assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str"
 
         if isinstance(index, int):

From 011cb0a134396e5f2713897e0c22f5124b156cf4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 02:35:34 -0400
Subject: [PATCH 648/994] Apply Ruff auto-fixes not included in merge

---
 git/index/base.py | 4 +---
 git/repo/fun.py   | 6 ++----
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 883d086f5..74ad65394 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1467,9 +1467,7 @@ def reset(
     # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
     def diff(
         self,
-        other: Union[  # type: ignore[override]
-            Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None
-        ] = git_diff.INDEX,
+        other: Union[Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None] = git_diff.INDEX,  # type: ignore[override]
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
diff --git a/git/repo/fun.py b/git/repo/fun.py
index b65432b09..0ac481206 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -138,13 +138,11 @@ def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
 
 
 @overload
-def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject:
-    ...
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ...
 
 
 @overload
-def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]:
-    ...
+def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ...
 
 
 def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:

From 74f3c2e7ad65e112d7e04644bc05b838c9c60d7f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 02:36:30 -0400
Subject: [PATCH 649/994] Help Ruff avoid a very long line

---
 git/index/base.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/git/index/base.py b/git/index/base.py
index 74ad65394..59b019f0f 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1467,7 +1467,13 @@ def reset(
     # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
     def diff(
         self,
-        other: Union[Literal[git_diff.DiffConstants.INDEX], "Tree", "Commit", str, None] = git_diff.INDEX,  # type: ignore[override]
+        other: Union[  # type: ignore[override]
+            Literal[git_diff.DiffConstants.INDEX],
+            "Tree",
+            "Commit",
+            str,
+            None,
+        ] = git_diff.INDEX,
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,

From 5778b7a01b988e711216fa5541cfdc50c0460476 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 02:37:50 -0400
Subject: [PATCH 650/994] Use LBYL for imports where EAFP is a mypy type error

The conditional imports of Literal from either typing or
typing_extensions have to be done with if-else on the Python
version rather than with try-except, or it is a static type error.

This makes that change, checking sys.version_info.

(See discussion in #1861 and #1862 for broader context and
background on why this logic, before and after this change, is
repeated across multiple modules.)

This also reorders/regroups imports for consistency in some places,
especially where a new import of the sys module (for version_info)
would otherwise exacerbate inconsistency.

Since the merge commit 0b99041, the number of mypy errors had
increased from 5 to 10. This fixes all the new mypy errors, so the
count is back to 5.
---
 git/objects/blob.py           |  7 ++++---
 git/objects/commit.py         | 38 +++++++++++++++++------------------
 git/objects/submodule/base.py |  9 ++++-----
 git/objects/tag.py            |  6 ++++--
 git/objects/tree.py           | 25 +++++++++++------------
 5 files changed, 43 insertions(+), 42 deletions(-)

diff --git a/git/objects/blob.py b/git/objects/blob.py
index 0a8527407..b49930edf 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -4,12 +4,13 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from mimetypes import guess_type
-from . import base
+import sys
 
+from . import base
 
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
 __all__ = ("Blob",)
diff --git a/git/objects/commit.py b/git/objects/commit.py
index f17e3fdf5..473eae8cc 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -3,57 +3,57 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+from collections import defaultdict
 import datetime
+from io import BytesIO
+import logging
+import os
 import re
 from subprocess import Popen, PIPE
+import sys
+from time import altzone, daylight, localtime, time, timezone
+
 from gitdb import IStream
-from git.util import hex_to_bin, Actor, Stats, finalize_process
-from git.diff import Diffable
 from git.cmd import Git
+from git.diff import Diffable
+from git.util import hex_to_bin, Actor, Stats, finalize_process
 
 from .tree import Tree
-from . import base
 from .util import (
     Serializable,
     TraversableIterableObj,
-    parse_date,
     altz_to_utctz_str,
-    parse_actor_and_date,
     from_timestamp,
+    parse_actor_and_date,
+    parse_date,
 )
-
-from time import time, daylight, altzone, timezone, localtime
-import os
-from io import BytesIO
-import logging
-from collections import defaultdict
-
+from . import base
 
 # typing ------------------------------------------------------------------
 
 from typing import (
     Any,
+    Dict,
     IO,
     Iterator,
     List,
     Sequence,
     Tuple,
-    Union,
     TYPE_CHECKING,
+    Union,
     cast,
-    Dict,
 )
 
-from git.types import PathLike
-
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import PathLike
+
 if TYPE_CHECKING:
-    from git.repo import Repo
     from git.refs import SymbolicReference
+    from git.repo import Repo
 
 # ------------------------------------------------------------------------
 
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 8c4a7356e..4e5a2a964 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -39,7 +39,6 @@
     sm_section,
 )
 
-
 # typing ----------------------------------------------------------------------
 
 from typing import (
@@ -54,13 +53,13 @@
     cast,
 )
 
-from git.types import Commit_ish, PathLike, TBD
-
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import Commit_ish, PathLike, TBD
+
 if TYPE_CHECKING:
     from git.index import IndexFile
     from git.objects.commit import Commit
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 83cf4ae18..e7ecfa62b 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -9,6 +9,8 @@
 For lightweight tags, see the :mod:`git.refs.tag` module.
 """
 
+import sys
+
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
 from ..util import hex_to_bin
@@ -16,9 +18,9 @@
 
 from typing import List, TYPE_CHECKING, Union
 
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
 if TYPE_CHECKING:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 225061bb7..308dd47a0 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -3,17 +3,16 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from git.util import IterableList, join_path
+import sys
+
 import git.diff as git_diff
-from git.util import to_bin_sha
+from git.util import IterableList, join_path, to_bin_sha
 
-from . import util
-from .base import IndexObject, IndexObjUnion
+from .base import IndexObjUnion, IndexObject
 from .blob import Blob
-from .submodule.base import Submodule
-
 from .fun import tree_entries_from_data, tree_to_stream
-
+from .submodule.base import Submodule
+from . import util
 
 # typing -------------------------------------------------
 
@@ -25,22 +24,22 @@
     Iterator,
     List,
     Tuple,
+    TYPE_CHECKING,
     Type,
     Union,
     cast,
-    TYPE_CHECKING,
 )
 
-from git.types import PathLike
-
-try:
+if sys.version_info >= (3, 8):
     from typing import Literal
-except ImportError:
+else:
     from typing_extensions import Literal
 
+from git.types import PathLike
+
 if TYPE_CHECKING:
-    from git.repo import Repo
     from io import BytesIO
+    from git.repo import Repo
 
 TreeCacheTup = Tuple[bytes, int, str]
 

From 84e256d999f748f08bbd62bfa833f96664febcee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 18:40:08 -0400
Subject: [PATCH 651/994] Describe Submodule.__init__ parent_commit parameter

This includes a brief description of the Submodule.__init__
parent_commit parameter in its docstring, rather than only
referring to the set_parent_commit method, whose semantics differ
due to conversation and validation, and which accepts more types
than just Commit or None.

The wording is based on wording in set_parent_commit, adjusted for
the difference in types, and set_parent_commit remains reference
for further details.

This builds on 1f03e7f (#1859) in improving the situation described
in #1869.
---
 git/objects/submodule/base.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 4e5a2a964..0e51ae711 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -143,7 +143,9 @@ def __init__(
             See the `url` parameter.
 
         :param parent_commit:
-            See :meth:`set_parent_commit`.
+            The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
+            the ``.gitmodules`` blob, or ``None`` to always point to the most recent
+            commit. See :meth:`set_parent_commit` for details.
 
         :param url:
             The URL to the remote repository which is the submodule.
@@ -1260,7 +1262,7 @@ def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool =
         contain the ``.gitmodules`` blob.
 
         :param commit:
-            Commit-ish reference pointing at the root_tree, or ``None`` to always point
+            Commit-ish reference pointing at the root tree, or ``None`` to always point
             to the most recent commit.
 
         :param check:

From 70ef69ae118ef8f393eb3f5c84e72e24df09516b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 14 Mar 2024 21:36:49 -0400
Subject: [PATCH 652/994] Include TagObject in git.types.Tree_ish

The Tree_ish union omitted TagObject, whose instances are only
sometimes tree-ish, and unlike Commit_ish before #1859, it is not
inherently a bug to define Tree_ish this way.

However, this Tree_ish type actually has only one use in GitPython
(which was also the case before the changes in #1859): as, itself,
an alternative in the union used to annotate the rev parameter of
the Repo.tree method (whose other alternatives are str and None).
A TagObject may be passed, and if it points to a tree or commit
then that will be resolved. Just to avoid a mypy error, code doing
that would (before this change) have to convert it to str first.

That annotation should be improved, and the best way to do it is to
keep it written the same way but change the definition of Tree_ish
in git.types to include TagObject. The reason is that doing so
alleviates a major unintuitive aspect of the relationship between
the Commit_ish and Tree_ish types: Commit_ish was broader than
everything commit-ish, while Tree_ish was narrower than everything
tree-ish.

I had not considered making this change in #1859 because I didn't
want to modify Tree_ish unnecessarily, and its definition was not
inherently a bug. However, the change to Commit_ish is sufficiently
large (though it only affects static typing) that a change to
Tree_ish to make them coherent and intuitive may be justified.

This commit changes Tree_ish so that, in addition to its Commit and
Tree alternatives, it also includes TagObject. This also updates
and simplifies its docstring accordingly, bringing it in line with
that of Commit_ish which is already defined with the same kind of
breadth, and further revises both docstrings to more explicitly
clarify when tags are tree-ish or commit-ish and when they are not.

This does not change the separate nonpublic Treeish type defined in
git.index.base (and named with no underscore), which omits
TagObject but includes bytes and str, and which is used to annotate
parameters of the IndexFile.from_tree and IndexFile.merge_tree
methods. Changes there may be valuable, but the goal here is just
to build narrowly on #1859 to address a shortcoming of the
revisions to git.types.
---
 git/types.py | 25 ++++++++++---------------
 1 file changed, 10 insertions(+), 15 deletions(-)

diff --git a/git/types.py b/git/types.py
index 336f49082..64f7629dc 100644
--- a/git/types.py
+++ b/git/types.py
@@ -73,19 +73,18 @@
     See also the :class:`Tree_ish` and :class:`Commit_ish` unions.
 """
 
-Tree_ish = Union["Commit", "Tree"]
-"""Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish.
+Tree_ish = Union["Commit", "Tree", "TagObject"]
+"""Union of :class:`~git.objects.base.Object`-based types that are sometimes tree-ish.
 
 See gitglossary(7) on "tree-ish": https://git-scm.com/docs/gitglossary#def_tree-ish
 
 :note:
-    This union comprises **only** the :class:`~git.objects.commit.Commit` and
-    :class:`~git.objects.tree.Tree` classes, **all** of whose instances are tree-ish.
-    This has been done because of the way GitPython uses it as a static type annotation.
-
-    :class:`~git.objects.tag.TagObject`, some but not all of whose instances are
-    tree-ish (those representing git tag objects that ultimately resolve to a tree or
-    commit), is not covered as part of this union type.
+    :class:`~git.objects.tree.Tree` and :class:`~git.objects.commit.Commit` are the
+    classes whose instances are all tree-ish. This union includes them, but also
+    :class:`~git.objects.tag.TagObject`, only **some** of whose instances are tree-ish.
+    Whether a particular :class:`~git.objects.tag.TagObject` peels (recursively
+    dereferences) to a tree or commit, rather than a blob, can in general only be known
+    at runtime.
 
 :note:
     See also the :class:`AnyGitObject` union of all four classes corresponding to git
@@ -102,12 +101,8 @@
     commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also
     :class:`~git.objects.tag.TagObject`, only **some** of whose instances are
     commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels
-    (recursively dereferences) to a commit can in general only be known at runtime.
-
-:note:
-    This is an inversion of the situation with :class:`Tree_ish`. This union is broader
-    than all commit-ish objects, while :class:`Tree_ish` is narrower than all tree-ish
-    objects.
+    (recursively dereferences) to a commit, rather than a tree or blob, can in general
+    only be known at runtime.
 
 :note:
     See also the :class:`AnyGitObject` union of all four classes corresponding to git

From 0969db92133e7562038275086ae560bda5b9d6e4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 12:30:22 -0400
Subject: [PATCH 653/994] Put Sphinx conf.py in the same style as other code

This is mostly a change to comments.
---
 doc/source/conf.py | 87 ++++++++++++++++++++++------------------------
 1 file changed, 42 insertions(+), 45 deletions(-)

diff --git a/doc/source/conf.py b/doc/source/conf.py
index 9c22ca06a..e25c1e5dc 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -3,29 +3,28 @@
 #
 # This file is execfile()d with the current directory set to its containing dir.
 #
-# The contents of this file are pickled, so don't put values in the namespace
-# that aren't pickleable (module imports are okay, they're removed automatically).
+# The contents of this file are pickled, so don't put values in the namespace that
+# aren't pickleable (module imports are okay, they're removed automatically).
 #
-# Note that not all possible configuration values are present in this
-# autogenerated file.
+# Note that not all possible configuration values are present in this autogenerated
+# file.
 #
-# All configuration values have a default; values that are commented out
-# serve to show the default.
+# All configuration values have a default; values that are commented out serve to show
+# the default.
 
-import sys
 import os
+import sys
 
-# If your extensions are in another directory, add it here. If the directory
-# is relative to the documentation root, use os.path.abspath to make it
-# absolute, like shown here.
+# If your extensions are in another directory, add it here. If the directory is relative
+# to the documentation root, use os.path.abspath to make it absolute, like shown here.
 # sys.path.append(os.path.abspath('.'))
 sys.path.insert(0, os.path.abspath("../.."))
 
 # General configuration
 # ---------------------
 
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+# Add any Sphinx extension module names here, as strings. They can be extensions coming
+# with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -44,9 +43,8 @@
 project = "GitPython"
 copyright = "Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel"
 
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
+# The version info for the project you're documenting, acts as replacement for |version|
+# and |release|, also used in various other places throughout the built documents.
 #
 # The short X.Y version.
 with open(os.path.join(os.path.dirname(__file__), "..", "..", "VERSION")) as fd:
@@ -55,8 +53,8 @@
 # The full version, including alpha/beta/rc tags.
 release = VERSION
 
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
+# The language for content autogenerated by Sphinx. Refer to documentation for a list of
+# supported languages.
 # language = None
 
 # There are two options for replacing |today|: either, you set today to some
@@ -68,8 +66,8 @@
 # List of documents that shouldn't be included in the build.
 # unused_docs = []
 
-# List of directories, relative to source directory, that shouldn't be searched
-# for source files.
+# List of directories, relative to source directory, that shouldn't be searched for
+# source files.
 exclude_trees = ["build"]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
@@ -78,12 +76,12 @@
 # If true, '()' will be appended to :func: etc. cross-reference text.
 # add_function_parentheses = True
 
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
+# If true, the current module name will be prepended to all description unit titles
+# (such as .. function::).
 # add_module_names = True
 
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
+# If true, sectionauthor and moduleauthor directives will be shown in the output.
+# They are ignored by default.
 # show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
@@ -96,40 +94,39 @@
 html_theme = "sphinx_rtd_theme"
 html_theme_options = {}
 
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
+# The name for this set of Sphinx documents.
+# If None, it defaults to "<project> v<release> documentation".
 # html_title = None
 
-# A shorter title for the navigation bar.  Default is the same as html_title.
+# A shorter title for the navigation bar. Default is the same as html_title.
 # html_short_title = None
 
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
+# The name of an image file (relative to this directory) to place at the top of the
+# sidebar.
 # html_logo = None
 
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
+# The name of an image file (within the static path) to use as favicon of the docs.
+# This file should be a Windows icon file (.ico) being 16x16 or 32x32 pixels large.
 # html_favicon = None
 
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
+# Add any paths that contain custom static files (such as style sheets) here, relative
+# to this directory. They are copied after the builtin static files, so a file named
+# "default.css" will overwrite the builtin "default.css".
 html_static_path = []
 
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, using the
+# given strftime format.
 # html_last_updated_fmt = '%b %d, %Y'
 
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
+# If true, SmartyPants will be used to convert quotes and dashes to typographically
+# correct entities.
 # html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
 # html_sidebars = {}
 
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
+# Additional templates that should be rendered to pages, maps page names to template
+# names.
 # html_additional_pages = {}
 
 # If false, no module index is generated.
@@ -144,9 +141,9 @@
 # If true, the reST sources are included in the HTML build as _sources/<name>.
 # html_copy_source = True
 
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
+# If true, an OpenSearch description file will be output, and all pages will contain a
+# <link> tag referring to it. The value of this option must be the base URL from which
+# the finished HTML is served.
 # html_use_opensearch = ''
 
 # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
@@ -171,8 +168,8 @@
     ("index", "GitPython.tex", "GitPython Documentation", "Michael Trier", "manual"),
 ]
 
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
+# The name of an image file (relative to this directory) to place at the top of the
+# title page.
 # latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,

From c5a29a97e9305f86723f7fceb3a2cd5eb1dadfa6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 12:35:49 -0400
Subject: [PATCH 654/994] Link Sphinx manpage references to online Git docs

(This is grouped as a general option since it is technically
classified as such, even though it currently only affects HTML
output.)
---
 doc/source/conf.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/doc/source/conf.py b/doc/source/conf.py
index e25c1e5dc..809762483 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -87,6 +87,8 @@
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = "sphinx"
 
+manpages_url = "https://git-scm.com/docs/{page}"
+
 
 # Options for HTML output
 # -----------------------

From 7f1675d2f190c5471186af67d4e18107a4efb745 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 14:34:29 -0400
Subject: [PATCH 655/994] Remove a spurious extra backtick from a docstring

---
 git/objects/submodule/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 0e51ae711..a5d1a6ad1 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -464,7 +464,7 @@ def add(
 
         :param url:
             git-clone compatible URL, see git-clone reference for more information.
-            If ``None```, the repository is assumed to exist, and the url of the first
+            If ``None``, the repository is assumed to exist, and the url of the first
             remote is taken instead. This is useful if you want to make an existing
             repository a submodule of another one.
 

From e883293fd8e18ce2e1d2723aa395f3ff35e20213 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 14:43:35 -0400
Subject: [PATCH 656/994] Add a missing Sphinx reference to a class

---
 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 fe01a9279..1238f0e17 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1452,7 +1452,7 @@ def clone(
 
         :param kwargs:
             * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
-              implementation used by the returned Repo instance.
+              implementation used by the returned :class:`Repo` instance.
             * All remaining keyword arguments are given to the ``git clone`` command.
 
         :return:

From d8ab99c26ea0d60b8def5d0fd1e11b0750c47c37 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 16:02:20 -0400
Subject: [PATCH 657/994] Use Sphinx manpage references where applicable

In docstrings within the git module.

This makes text of the same general form as, e.g.

    git-rev-parse

or

    ``git rev-parse``

or URLs that link directly to a documentation page equivalent to a
manpage or that link to the first section where preceding material
is trivial...

...instead be in the form:

    :manpage:`git-rev-parse(1)`

with variations as appropriate, for example changing

    gitglossary(7)

to

    :manpage:`gitglossary(7)`

and making other changes accordingly, such as adjusting phrasing
and the use of hyphens in a small number of cases.

Together with c5a29a9, which made such references linkify to the'
official online documentation for Git, this makes it so that when
git subcommands are mentioned in docstrings, the Sphinx autodoc
generated documentation in the API Reference page now renders them
as links to the relevant documentation page.

Links to specific sections where it matters or potentially matters
that it goes to that section are not replaced. In particular, links
to specific entries in gitglossary(7) are not replaced. To do this
properly would involve adding a new Sphinx role for it, which would
work well in the rendered documentation but could be unclear when
the documentation is read in docstrings appearing in the code.
---
 git/cmd.py                    |  9 +++---
 git/config.py                 |  2 +-
 git/diff.py                   |  6 ++--
 git/index/base.py             | 55 ++++++++++++++++++-----------------
 git/index/typ.py              |  3 +-
 git/objects/base.py           |  2 +-
 git/objects/blob.py           |  3 +-
 git/objects/commit.py         | 23 ++++++++-------
 git/objects/submodule/base.py | 12 ++++----
 git/objects/tag.py            |  2 +-
 git/objects/tree.py           |  2 +-
 git/refs/head.py              |  2 +-
 git/refs/symbolic.py          |  3 +-
 git/refs/tag.py               |  2 +-
 git/remote.py                 | 20 ++++++-------
 git/repo/base.py              | 31 +++++++++++---------
 git/repo/fun.py               |  6 ++--
 git/types.py                  | 14 +++++----
 git/util.py                   | 11 +++----
 19 files changed, 109 insertions(+), 99 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index ab2688a25..2862b1600 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -901,7 +901,8 @@ def working_dir(self) -> Union[None, PathLike]:
     def version_info(self) -> Tuple[int, ...]:
         """
         :return: Tuple with integers representing the major, minor and additional
-            version numbers as parsed from ``git version``. Up to four fields are used.
+            version numbers as parsed from :manpage:`git-version(1)`. Up to four fields
+            are used.
 
             This value is generated on demand and is cached.
         """
@@ -1038,9 +1039,9 @@ def execute(
             3. Deeper descendants do not receive signals, though they may sometimes
                terminate as a consequence of their parent processes being killed.
             4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side
-               effects on a repository. For example, stale locks in case of ``git gc``
-               could render the repository incapable of accepting changes until the lock
-               is manually removed.
+               effects on a repository. For example, stale locks in case of
+               :manpage:`git-gc(1)` could render the repository incapable of accepting
+               changes until the lock is manually removed.
 
         :param with_stdout:
             If ``True``, default ``True``, we open stdout on the created process.
diff --git a/git/config.py b/git/config.py
index 4441c2187..f74d290cc 100644
--- a/git/config.py
+++ b/git/config.py
@@ -270,7 +270,7 @@ def get_config_path(config_level: Lit_config_levels) -> str:
 class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
     """Implements specifics required to read git style configuration files.
 
-    This variation behaves much like the ``git config`` command, such that the
+    This variation behaves much like the :manpage:`git-config(1)` command, such that the
     configuration will be read on demand based on the filepath given during
     initialization.
 
diff --git a/git/diff.py b/git/diff.py
index 06935f87e..a6322ff57 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -222,8 +222,8 @@ def diff(
             to be read and diffed.
 
         :param kwargs:
-            Additional arguments passed to ``git diff``, such as ``R=True`` to swap both
-            sides of the diff.
+            Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
+            swap both sides of the diff.
 
         :return:
             A :class:`DiffIndex` representing the computed diff.
@@ -590,7 +590,7 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
             The repository we are operating on.
 
         :param proc:
-            ``git diff`` process to read from
+            :manpage:`git-diff(1)` process to read from
             (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
 
         :return:
diff --git a/git/index/base.py b/git/index/base.py
index 59b019f0f..cb67d2c29 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -225,11 +225,11 @@ def write(
 
         :param ignore_extension_data:
             If ``True``, the TREE type extension data read in the index will not be
-            written to disk. NOTE that no extension data is actually written.
-            Use this if you have altered the index and would like to use
-            ``git write-tree`` afterwards to create a tree representing your written
-            changes. If this data is present in the written index, ``git write-tree``
-            will instead write the stored/cached tree.
+            written to disk. NOTE that no extension data is actually written. Use this
+            if you have altered the index and would like to use
+            :manpage:`git-write-tree(1)` afterwards to create a tree representing your
+            written changes. If this data is present in the written index,
+            :manpage:`git-write-tree(1)` will instead write the stored/cached tree.
             Alternatively, use :meth:`write_tree` to handle this case automatically.
         """
         # Make sure we have our entries read before getting a write lock.
@@ -343,7 +343,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
                tree, tree 3 is the 'other' one.
 
         :param kwargs:
-            Additional arguments passed to ``git read-tree``.
+            Additional arguments passed to :manpage:`git-read-tree(1)`.
 
         :return:
             New :class:`IndexFile` instance. It will point to a temporary index location
@@ -355,9 +355,9 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile
             automatically resolve more cases in a commonly correct manner. Specify
             ``trivial=True`` as a keyword argument to override that.
 
-            As the underlying ``git read-tree`` command takes into account the current
-            index, it will be temporarily moved out of the way to prevent any unexpected
-            interference.
+            As the underlying :manpage:`git-read-tree(1)` command takes into account the
+            current index, it will be temporarily moved out of the way to prevent any
+            unexpected interference.
         """
         if len(treeish) == 0 or len(treeish) > 3:
             raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
@@ -470,10 +470,10 @@ def _write_path_to_stdin(
             In that case, it will return ``None``.
 
         :note:
-            There is a bug in git-update-index that prevents it from sending reports
-            just in time. This is why we have a version that tries to read stdout and
-            one which doesn't. In fact, the stdout is not important as the piped-in
-            files are processed anyway and just in time.
+            There is a bug in :manpage:`git-update-index(1)` that prevents it from
+            sending reports just in time. This is why we have a version that tries to
+            read stdout and one which doesn't. In fact, the stdout is not important as
+            the piped-in files are processed anyway and just in time.
 
         :note:
             Newlines are essential here, git's behaviour is somewhat inconsistent on
@@ -782,11 +782,12 @@ def add(
                 directories like ``lib``, which will add all the files within the
                 directory and subdirectories.
 
-                This equals a straight ``git add``.
+                This equals a straight :manpage:`git-add(1)`.
 
                 They are added at stage 0.
 
-            - :class:~`git.objects.blob.Blob` or :class:`~git.objects.submodule.base.Submodule` object
+            - :class:~`git.objects.blob.Blob` or
+              :class:`~git.objects.submodule.base.Submodule` object
 
                 Blobs are added as they are assuming a valid mode is set.
 
@@ -818,8 +819,8 @@ def add(
         :param force:
             **CURRENTLY INEFFECTIVE**
             If ``True``, otherwise ignored or excluded files will be added anyway. As
-            opposed to the ``git add`` command, we enable this flag by default as the
-            API user usually wants the item to be added even though they might be
+            opposed to the :manpage:`git-add(1)` command, we enable this flag by default
+            as the API user usually wants the item to be added even though they might be
             excluded.
 
         :param fprogress:
@@ -850,8 +851,8 @@ def add(
         :param write_extension_data:
             If ``True``, extension data will be written back to the index. This can lead
             to issues in case it is containing the 'TREE' extension, which will cause
-            the ``git commit`` command to write an old tree, instead of a new one
-            representing the now changed index.
+            the :manpage:`git-commit(1)` command to write an old tree, instead of a new
+            one representing the now changed index.
 
             This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
             'TREE' extension altogether. You should set it to ``True`` if you intend to
@@ -1008,8 +1009,8 @@ def remove(
             uncommitted changes in it.
 
         :param kwargs:
-            Additional keyword arguments to be passed to ``git rm``, such as ``r`` to
-            allow recursive removal.
+            Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
+            ``r`` to allow recursive removal.
 
         :return:
             List(path_string, ...) list of repository relative paths that have been
@@ -1058,7 +1059,7 @@ def move(
             skipped.
 
         :param kwargs:
-            Additional arguments you would like to pass to ``git mv``, such as
+            Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
             ``dry_run`` or ``force``.
 
         :return:
@@ -1224,7 +1225,7 @@ def checkout(
             prior and after a file has been checked out.
 
         :param kwargs:
-            Additional arguments to be passed to ``git checkout-index``.
+            Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
 
         :return:
             Iterable yielding paths to files which have been checked out and are
@@ -1243,8 +1244,8 @@ def checkout(
             The checkout is limited to checking out the files in the index. Files which
             are not in the index anymore and exist in the working tree will not be
             deleted. This behaviour is fundamentally different to ``head.checkout``,
-            i.e. if you want ``git checkout`` like behaviour, use ``head.checkout``
-            instead of ``index.checkout``.
+            i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
+            ``head.checkout`` instead of ``index.checkout``.
         """
         args = ["--index"]
         if force:
@@ -1416,14 +1417,14 @@ def reset(
             raised.
 
         :param kwargs:
-            Additional keyword arguments passed to ``git reset``.
+            Additional keyword arguments passed to :manpage:`git-reset(1)`.
 
         :note:
             :meth:`IndexFile.reset`, as opposed to
             :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
             order to maintain a consistent working tree. Instead, it will just check out
             the files according to their state in the index.
-            If you want ``git reset``-like behaviour, use
+            If you want :manpage:`git-reset(1)`-like behaviour, use
             :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
 
         :return:
diff --git a/git/index/typ.py b/git/index/typ.py
index c247fab99..ffd76dc46 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -130,8 +130,7 @@ def stage(self) -> int:
             * 3 = stage of entries from the 'right' side of the merge
 
         :note:
-            For more information, see:
-            http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
+            For more information, see :manpage:`git-read-tree(1)`.
         """
         return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
 
diff --git a/git/objects/base.py b/git/objects/base.py
index f568a4bc5..22d939aa6 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -45,7 +45,7 @@ class Object(LazyMixin):
     * :class:`Commit <git.objects.commit.Commit>`
     * :class:`TagObject <git.objects.tag.TagObject>`
 
-    See gitglossary(7) on:
+    See :manpage:`gitglossary(7)` on:
 
     * "object": https://git-scm.com/docs/gitglossary#def_object
     * "object type": https://git-scm.com/docs/gitglossary#def_object_type
diff --git a/git/objects/blob.py b/git/objects/blob.py
index b49930edf..122d5f731 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -19,7 +19,8 @@
 class Blob(base.IndexObject):
     """A Blob encapsulates a git blob object.
 
-    See gitglossary(7) on "blob": https://git-scm.com/docs/gitglossary#def_blob_object
+    See :manpage:`gitglossary(7)` on "blob":
+    https://git-scm.com/docs/gitglossary#def_blob_object
     """
 
     DEFAULT_MIME_TYPE = "text/plain"
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 473eae8cc..3c5d8fba3 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -65,7 +65,7 @@
 class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     """Wraps a git commit object.
 
-    See gitglossary(7) on "commit object":
+    See :manpage:`gitglossary(7)` on "commit object":
     https://git-scm.com/docs/gitglossary#def_commit_object
 
     :note:
@@ -269,8 +269,9 @@ def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any)
             actually containing the paths.
 
         :param kwargs:
-            Additional options to be passed to ``git rev-list``. They must not alter the
-            output style of the command, or parsing will yield incorrect results.
+            Additional options to be passed to :manpage:`git-rev-list(1)`. They must not
+            alter the output style of the command, or parsing will yield incorrect
+            results.
 
         :return:
             An int defining the number of reachable commits
@@ -307,14 +308,14 @@ def iter_items(
             The :class:`~git.repo.base.Repo`.
 
         :param rev:
-            Revision specifier. See git-rev-parse for viable options.
+            Revision specifier. See :manpage:`git-rev-parse(1)` for viable options.
 
         :param paths:
             An optional path or list of paths. If set only :class:`Commit`\s that
             include the path or paths will be considered.
 
         :param kwargs:
-            Optional keyword arguments to ``git rev-list`` where:
+            Optional keyword arguments to :manpage:`git-rev-list(1)` where:
 
             * ``max_count`` is the maximum number of commits to fetch.
             * ``skip`` is the number of commits to skip.
@@ -353,7 +354,7 @@ def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs
             contain at least one of the paths.
 
         :param kwargs:
-            All arguments allowed by ``git rev-list``.
+            All arguments allowed by :manpage:`git-rev-list(1)`.
 
         :return:
             Iterator yielding :class:`Commit` objects which are parents of ``self``
@@ -404,7 +405,7 @@ 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 e-mail
-        headers (see: https://git-scm.com/docs/git-interpret-trailers).
+        headers. See :manpage:`git-interpret-trailers(1)`.
 
         This function calls ``git interpret-trailers --parse`` onto the message to
         extract the trailer information, returns the raw trailer data as a list.
@@ -456,7 +457,7 @@ 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).
+        headers. See :manpage:`git-interpret-trailers(1)`.
 
         This function calls ``git interpret-trailers --parse`` onto the message to
         extract the trailer information. The key value pairs are stripped of leading and
@@ -499,7 +500,7 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen,
         from our lighting fast object database.
 
         :param proc:
-            ``git rev-list`` process instance - one sha per line.
+            :manpage:`git-rev-list(1)` process instance - one sha per line.
 
         :return:
             Iterator supplying :class:`Commit` objects
@@ -596,8 +597,8 @@ def create_from_tree(
 
         :note:
             Additional information about the committer and author are taken from the
-            environment or from the git configuration. See git-commit-tree for more
-            information.
+            environment or from the git configuration. See :manpage:`git-commit-tree(1)`
+            for more information.
         """
         if parent_commits is None:
             try:
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index a5d1a6ad1..d01aa448f 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -342,7 +342,7 @@ def _clone_repo(
             Allow unsafe options to be used, like ``--upload-pack``.
 
         :param kwargs:
-            Additional arguments given to ``git clone``.
+            Additional arguments given to :manpage:`git-clone(1)`.
         """
         module_abspath = cls._module_abspath(repo, path, name)
         module_checkout_path = module_abspath
@@ -463,10 +463,10 @@ def add(
             It will be created as required during the repository initialization.
 
         :param url:
-            git-clone compatible URL, see git-clone reference for more information.
-            If ``None``, the repository is assumed to exist, and the url of the first
-            remote is taken instead. This is useful if you want to make an existing
-            repository a submodule of another one.
+            ``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more
+            information. If ``None``, the repository is assumed to exist, and the URL of
+            the first remote is taken instead. This is useful if you want to make an
+            existing repository a submodule of another one.
 
         :param branch:
             Name of branch at which the submodule should (later) be checked out. The
@@ -696,7 +696,7 @@ def update(
             its value.
 
         :param clone_multi_options:
-            List of ``git clone`` options.
+            List of :manpage:`git-clone(1)` options.
             Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
             They only take effect with the `init` option.
 
diff --git a/git/objects/tag.py b/git/objects/tag.py
index e7ecfa62b..52d79751f 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -37,7 +37,7 @@ class TagObject(base.Object):
     """Annotated (i.e. non-lightweight) tag carrying additional information about an
     object we are pointing to.
 
-    See gitglossary(7) on "tag object":
+    See :manpage:`gitglossary(7)` on "tag object":
     https://git-scm.com/docs/gitglossary#def_tag_object
     """
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 308dd47a0..ad67f8e47 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -168,7 +168,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
     R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and
     other :class:`Tree`\s.
 
-    See gitglossary(7) on "tree object":
+    See :manpage:`gitglossary(7)` on "tree object":
     https://git-scm.com/docs/gitglossary#def_tree_object
 
     Subscripting is supported, as with a list or dict:
diff --git a/git/refs/head.py b/git/refs/head.py
index aae5767d4..b65189621 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -89,7 +89,7 @@ def reset(
             that are to be reset. This allows to partially reset individual files.
 
         :param kwargs:
-            Additional arguments passed to ``git reset``.
+            Additional arguments passed to :manpage:`git-reset(1)`.
 
         :return:
             self
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 754e90089..dea047d83 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -173,8 +173,7 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) ->
     def _check_ref_name_valid(ref_path: PathLike) -> None:
         """Check a ref name for validity.
 
-        This is based on the rules described in:
-        https://git-scm.com/docs/git-check-ref-format/#_description
+        This is based on the rules described in :manpage:`git-check-ref-format(1)`.
         """
         previous: Union[str, None] = None
         one_before_previous: Union[str, None] = None
diff --git a/git/refs/tag.py b/git/refs/tag.py
index a1d0b470f..f653d4e7d 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -124,7 +124,7 @@ def create(
             If ``True``, force creation of a tag even though that tag already exists.
 
         :param kwargs:
-            Additional keyword arguments to be passed to ``git tag``.
+            Additional keyword arguments to be passed to :manpage:`git-tag(1)`.
 
         :return:
             A new :class:`TagReference`.
diff --git a/git/remote.py b/git/remote.py
index 1723216a4..ac58a4f18 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -333,8 +333,8 @@ class FetchInfo(IterableObj):
 
     @classmethod
     def refresh(cls) -> Literal[True]:
-        """Update information about which ``git fetch`` flags are supported by the git
-        executable being used.
+        """Update information about which :manpage:`git-fetch(1)` flags are supported
+        by the git executable being used.
 
         Called by the :func:`git.refresh` function in the top level ``__init__``.
         """
@@ -1015,13 +1015,13 @@ def fetch(
             "grab the master branch head from the $URL and store it as my origin
             branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream``
             means "publish my master branch head as to-upstream branch at $URL".
-            See also git-push(1).
+            See also :manpage:`git-push(1)`.
 
-            Taken from the git manual, gitglossary(7).
+            Taken from the git manual, :manpage:`gitglossary(7)`.
 
-            Fetch supports multiple refspecs (as the underlying git-fetch does) -
-            supplying a list rather than a string for 'refspec' will make use of this
-            facility.
+            Fetch supports multiple refspecs (as the underlying :manpage:`git-fetch(1)`
+            does) - supplying a list rather than a string for 'refspec' will make use of
+            this facility.
 
         :param progress:
             See the :meth:`push` method.
@@ -1040,7 +1040,7 @@ def fetch(
             Allow unsafe options to be used, like ``--upload-pack``.
 
         :param kwargs:
-            Additional arguments to be passed to ``git fetch``.
+            Additional arguments to be passed to :manpage:`git-fetch(1)`.
 
         :return:
             IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing
@@ -1104,7 +1104,7 @@ def pull(
             Allow unsafe options to be used, like ``--upload-pack``.
 
         :param kwargs:
-            Additional arguments to be passed to ``git pull``.
+            Additional arguments to be passed to :manpage:`git-pull(1)`.
 
         :return:
             Please see :meth:`fetch` method.
@@ -1170,7 +1170,7 @@ def push(
             Allow unsafe options to be used, like ``--receive-pack``.
 
         :param kwargs:
-            Additional arguments to be passed to ``git push``.
+            Additional arguments to be passed to :manpage:`git-push(1)`.
 
         :return:
             A :class:`PushInfoList` object, where each list member represents an
diff --git a/git/repo/base.py b/git/repo/base.py
index 1238f0e17..d6a161305 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -149,7 +149,7 @@ class Repo:
         "--config",
         "-c",
     ]
-    """Options to ``git clone`` that allow arbitrary commands to be executed.
+    """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
 
     The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
     directly:
@@ -572,7 +572,7 @@ def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
         """Delete the given heads.
 
         :param kwargs:
-            Additional keyword arguments to be passed to ``git branch``.
+            Additional keyword arguments to be passed to :manpage:`git-branch(1)`.
         """
         return Head.delete(self, *heads, **kwargs)
 
@@ -700,7 +700,7 @@ def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
         """The :class:`~git.objects.commit.Commit` object for the specified revision.
 
         :param rev:
-            Revision specifier, see ``git rev-parse`` for viable options.
+            Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
 
         :return:
             :class:`~git.objects.commit.Commit`
@@ -749,7 +749,7 @@ def iter_commits(
         history of a given ref/commit.
 
         :param rev:
-            Revision specifier, see ``git rev-parse`` for viable options.
+            Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
             If ``None``, the active branch will be used.
 
         :param paths:
@@ -757,7 +757,7 @@ def iter_commits(
             path or paths will be returned.
 
         :param kwargs:
-            Arguments to be passed to ``git rev-list``.
+            Arguments to be passed to :manpage:`git-rev-list(1)`.
             Common ones are ``max_count`` and ``skip``.
 
         :note:
@@ -930,8 +930,8 @@ def is_dirty(
         """
         :return:
             ``True`` if the repository is considered dirty. By default it will react
-            like a git-status without untracked files, hence it is dirty if the index or
-            the working copy have changes.
+            like a :manpage:`git-status(1)` without untracked files, hence it is dirty
+            if the index or the working copy have changes.
         """
         if self._bare:
             # Bare repositories with no associated working directory are
@@ -1001,7 +1001,7 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
     def ignored(self, *paths: PathLike) -> List[str]:
         """Checks if paths are ignored via ``.gitignore``.
 
-        This does so using the ``git check-ignore`` method.
+        This does so using the :manpage:`git-check-ignore(1)` method.
 
         :param paths:
             List of paths to check whether they are ignored or not.
@@ -1044,7 +1044,7 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) ->
         :param rev:
             Revision specifier. If ``None``, the blame will include all the latest
             uncommitted changes. Otherwise, anything successfully parsed by
-            ``git rev-parse`` is a valid option.
+            :manpage:`git-rev-parse(1)` is a valid option.
 
         :return:
             Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
@@ -1140,7 +1140,7 @@ def blame(
         :param rev:
             Revision specifier. If ``None``, the blame will include all the latest
             uncommitted changes. Otherwise, anything successfully parsed by
-            ``git rev-parse`` is a valid option.
+            :manpage:`git-rev-parse(1)` is a valid option.
 
         :return:
             list: [git.Commit, list: [<line>]]
@@ -1312,7 +1312,8 @@ def init(
             environment variables.
 
         :param kwargs:
-            Keyword arguments serving as additional options to the ``git init`` command.
+            Keyword arguments serving as additional options to the
+            :manpage:`git-init(1)` command.
 
         :return:
             :class:`Repo` (the newly created repo)
@@ -1432,7 +1433,8 @@ def clone(
             See :meth:`Remote.push <git.remote.Remote.push>`.
 
         :param multi_options:
-            A list of ``git clone`` options that can be provided multiple times.
+            A list of :manpage:`git-clone(1)` options that can be provided multiple
+            times.
 
             One option per list item which is passed exactly as specified to clone.
             For example::
@@ -1453,7 +1455,8 @@ def clone(
         :param kwargs:
             * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
               implementation used by the returned :class:`Repo` instance.
-            * All remaining keyword arguments are given to the ``git clone`` command.
+            * All remaining keyword arguments are given to the :manpage:`git-clone(1)`
+              command.
 
         :return:
             :class:`Repo` (the newly cloned repo)
@@ -1551,7 +1554,7 @@ def archive(
             The optional prefix to prepend to each filename in the archive.
 
         :param kwargs:
-            Additional arguments passed to ``git archive``:
+            Additional arguments passed to :manpage:`git-archive(1)`:
 
             * Use the ``format`` argument to define the kind of format. Use specialized
               ostreams to write any format supported by Python.
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 0ac481206..13c85ea22 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -226,7 +226,7 @@ def to_commit(obj: Object) -> "Commit":
 
 
 def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
-    """Parse a revision string. Like ``git rev-parse``.
+    """Parse a revision string. Like :manpage:`git-rev-parse(1)`.
 
     :return:
         `~git.objects.base.Object` at the given revision.
@@ -239,8 +239,8 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
         * :class:`Blob <git.objects.blob.Blob>`
 
     :param rev:
-        ``git rev-parse``-compatible revision specification as string. Please see
-        http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html for details.
+        :manpage:`git-rev-parse(1)`-compatible revision specification as string.
+        Please see :manpage:`git-rev-parse(1)` for details.
 
     :raise gitdb.exc.BadObject:
         If the given revision could not be found.
diff --git a/git/types.py b/git/types.py
index 64f7629dc..a30179a25 100644
--- a/git/types.py
+++ b/git/types.py
@@ -57,7 +57,8 @@
 * :class:`Commit <git.objects.commit.Commit>`
 * :class:`TagObject <git.objects.tag.TagObject>`
 
-Those GitPython classes represent the four git object types, per gitglossary(7):
+Those GitPython classes represent the four git object types, per
+:manpage:`gitglossary(7)`:
 
 * "blob": https://git-scm.com/docs/gitglossary#def_blob_object
 * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object
@@ -76,7 +77,8 @@
 Tree_ish = Union["Commit", "Tree", "TagObject"]
 """Union of :class:`~git.objects.base.Object`-based types that are sometimes tree-ish.
 
-See gitglossary(7) on "tree-ish": https://git-scm.com/docs/gitglossary#def_tree-ish
+See :manpage:`gitglossary(7)` on "tree-ish":
+https://git-scm.com/docs/gitglossary#def_tree-ish
 
 :note:
     :class:`~git.objects.tree.Tree` and :class:`~git.objects.commit.Commit` are the
@@ -94,7 +96,8 @@
 Commit_ish = Union["Commit", "TagObject"]
 """Union of :class:`~git.objects.base.Object`-based types that are sometimes commit-ish.
 
-See gitglossary(7) on "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish
+See :manpage:`gitglossary(7)` on "commit-ish":
+https://git-scm.com/docs/gitglossary#def_commit-ish
 
 :note:
     :class:`~git.objects.commit.Commit` is the only class whose instances are all
@@ -117,8 +120,9 @@
 values in :class:`~git.objects.base.Object` subclasses that represent git objects. These
 literals therefore correspond to the types in the :class:`AnyGitObject` union.
 
-These are the same strings git itself uses to identify its four object types. See
-gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type
+These are the same strings git itself uses to identify its four object types.
+See :manpage:`gitglossary(7)` on "object type":
+https://git-scm.com/docs/gitglossary#def_object_type
 """
 
 Lit_commit_ish = Literal["commit", "tag"]
diff --git a/git/util.py b/git/util.py
index 2a9dd10a9..bef828dbf 100644
--- a/git/util.py
+++ b/git/util.py
@@ -558,8 +558,8 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
 
 class RemoteProgress:
     """Handler providing an interface to parse progress information emitted by
-    ``git push`` and ``git fetch`` and to dispatch callbacks allowing subclasses to
-    react to the progress."""
+    :manpage:`git-push(1)` and :manpage:`git-fetch(1)` and to dispatch callbacks
+    allowing subclasses to react to the progress."""
 
     _num_op_codes: int = 9
     (
@@ -595,8 +595,8 @@ def __init__(self) -> None:
         self.other_lines: List[str] = []
 
     def _parse_progress_line(self, line: AnyStr) -> None:
-        """Parse progress information from the given line as retrieved by ``git push``
-        or ``git fetch``.
+        """Parse progress information from the given line as retrieved by
+        :manpage:`git-push(1)` or :manpage:`git-fetch(1)`.
 
         - Lines that do not contain progress info are stored in :attr:`other_lines`.
         - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``)
@@ -922,7 +922,8 @@ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]) -> None:
 
     @classmethod
     def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
-        """Create a :class:`Stats` object from output retrieved by ``git diff``.
+        """Create a :class:`Stats` object from output retrieved by
+        :manpage:`git-diff(1)`.
 
         :return:
             :class:`git.Stats`

From d271a84a67ab1cbb9c5922244fd5e17517520593 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 16:14:28 -0400
Subject: [PATCH 658/994] Use more official link to index-format documentation

This could also be written automatically by labeling it with the
:manpage: role, but it doesn't seem to be an actual manpage, so I
have not done that.
---
 git/index/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/index/base.py b/git/index/base.py
index cb67d2c29..dfb687260 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -862,7 +862,7 @@ def add(
             handled manually at all.
 
             All current built-in extensions are listed here:
-            http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/technical/index-format.txt
+            https://git-scm.com/docs/index-format
 
         :return:
             List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries

From ca95c420ce5ed45fe33381b25dd1a1c65e150d40 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 16:18:21 -0400
Subject: [PATCH 659/994] Use current main official link to git-clone URLS doc

I am still not using the :manpage: role for this because it points
to a specific section, which that role does not support, and it is
important that it go to that specific section. (See d8ab99c for
details.)
---
 git/repo/base.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index d6a161305..ce164274e 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -1488,8 +1488,7 @@ def clone_from(
         """Create a clone from the given URL.
 
         :param url:
-            Valid git url, see:
-            http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
+            Valid git url, see: https://git-scm.com/docs/git-clone#URLS
 
         :param to_path:
             Path to which the repository should be cloned to.

From 85434571090a24ac84d1ce81f3fa74909e3d5c5f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 15 Mar 2024 16:24:57 -0400
Subject: [PATCH 660/994] Use :const: for constants that had the :attr: role

Note that this intentionally does *not* include some all-caps class
attributes that are not really constants:

- Git.GIT_PYTHON_GIT_EXECUTABLE is set by refresh functions,
  including on subsequent calls, which is an important and
  documented part of what those functions do. (Also, it is set
  automatically from an enviroment variable, which is not constant
  across runs; that it could not even in principle be replaced by a
  specific literal value is a further reason it is not a constant.)

- The Git.USE_SHELL attribute is a more ambiguous case. It is given
  a literal value (False) which it preferably remains. But setting
  it has been documented as something users can do (in the
  changelog, and later in regard to setting it being a deprecated
  operation).

The first of these is decisively not a constant even under a loose
definition, while the second is less clear. I've kept both with the
:attr: role.
---
 git/remote.py |  2 +-
 git/util.py   | 16 ++++++++--------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index ac58a4f18..2c452022e 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -1177,7 +1177,7 @@ def push(
             individual head which had been updated on the remote side.
 
             If the push contains rejected heads, these will have the
-            :attr:`PushInfo.ERROR` bit set in their flags.
+            :const:`PushInfo.ERROR` bit set in their flags.
 
             If the operation fails completely, the length of the returned
             :class:`PushInfoList` will be 0.
diff --git a/git/util.py b/git/util.py
index bef828dbf..0de724ceb 100644
--- a/git/util.py
+++ b/git/util.py
@@ -713,14 +713,14 @@ def update(
         :param op_code:
             Integer allowing to be compared against Operation IDs and stage IDs.
 
-            Stage IDs are :attr:`BEGIN` and :attr:`END`. :attr:`BEGIN` will only be set
-            once for each Operation ID as well as :attr:`END`. It may be that
-            :attr:`BEGIN` and :attr:`END` are set at once in case only one progress
-            message was emitted due to the speed of the operation. Between :attr:`BEGIN`
-            and :attr:`END`, none of these flags will be set.
+            Stage IDs are :const:`BEGIN` and :const:`END`. :const:`BEGIN` will only be
+            set once for each Operation ID as well as :const:`END`. It may be that
+            :const:`BEGIN` and :const:`END` are set at once in case only one progress
+            message was emitted due to the speed of the operation. Between
+            :const:`BEGIN` and :const:`END`, none of these flags will be set.
 
-            Operation IDs are all held within the :attr:`OP_MASK`. Only one Operation ID
-            will be active per call.
+            Operation IDs are all held within the :const:`OP_MASK`. Only one Operation
+            ID will be active per call.
 
         :param cur_count:
             Current absolute count of items.
@@ -730,7 +730,7 @@ def update(
             maximum number of items or if it is (yet) unknown.
 
         :param message:
-            In case of the :attr:`WRITING` operation, it contains the amount of bytes
+            In case of the :const:`WRITING` operation, it contains the amount of bytes
             transferred. It may possibly be used for other purposes as well.
 
         :note:

From 6626117259b93d0a3085dd17ed71ef1acd08c7a5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 16 Mar 2024 01:58:30 -0400
Subject: [PATCH 661/994] Fix parse_date docstring spacing; use RFC role

There are only a three cases where a specific RFC is mentioned.
Only one is in parse_date and the others are elsewhere.
---
 git/objects/commit.py | 8 ++++----
 git/objects/util.py   | 5 ++---
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/git/objects/commit.py b/git/objects/commit.py
index 3c5d8fba3..6a60c30bd 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -404,8 +404,8 @@ def trailers(self) -> Dict[str, 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 e-mail
-        headers. See :manpage:`git-interpret-trailers(1)`.
+        Git messages can contain trailer information that are similar to :rfc:`822`
+        e-mail headers. See :manpage:`git-interpret-trailers(1)`.
 
         This function calls ``git interpret-trailers --parse`` onto the message to
         extract the trailer information, returns the raw trailer data as a list.
@@ -456,8 +456,8 @@ def trailers_list(self) -> List[Tuple[str, str]]:
     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 :manpage:`git-interpret-trailers(1)`.
+        Git messages can contain trailer information that are similar to :rfc:`822`
+        e-mail headers. See :manpage:`git-interpret-trailers(1)`.
 
         This function calls ``git interpret-trailers --parse`` onto the message to
         extract the trailer information. The key value pairs are stripped of leading and
diff --git a/git/objects/util.py b/git/objects/util.py
index 7cca05134..297b33b70 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -223,9 +223,8 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
 
         * Aware datetime instance
         * Git internal format: timestamp offset
-        * RFC 2822: ``Thu, 07 Apr 2005 22:13:13 +0200``
-        * ISO 8601: ``2005-04-07T22:13:13``
-            The T can be a space as well.
+        * :rfc:`2822`: ``Thu, 07 Apr 2005 22:13:13 +0200``
+        * ISO 8601: ``2005-04-07T22:13:13`` - The ``T`` can be a space as well.
 
     :return:
         Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch

From a957ae7a94ebede485e94d03da0258a0d2ad79e0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 16 Mar 2024 02:19:39 -0400
Subject: [PATCH 662/994] Use the :exc: role for exceptions

Rather than the more general :class: role that was used for them.
---
 git/db.py                     |  2 +-
 git/index/base.py             | 10 +++++-----
 git/objects/submodule/util.py |  2 +-
 git/objects/tree.py           |  4 ++--
 git/refs/head.py              |  2 +-
 git/refs/reference.py         |  2 +-
 git/refs/remote.py            |  2 +-
 git/refs/symbolic.py          |  2 +-
 git/repo/fun.py               |  4 ++--
 git/types.py                  |  2 +-
 git/util.py                   |  8 ++++----
 11 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/git/db.py b/git/db.py
index 15e791e6c..5b2ca4de2 100644
--- a/git/db.py
+++ b/git/db.py
@@ -59,7 +59,7 @@ def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
         :raise gitdb.exc.BadObject:
 
         :note:
-            Currently we only raise :class:`~gitdb.exc.BadObject` as git does not
+            Currently we only raise :exc:`~gitdb.exc.BadObject` as git does not
             communicate ambiguous objects separately.
         """
         try:
diff --git a/git/index/base.py b/git/index/base.py
index dfb687260..fb91e092c 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -550,8 +550,8 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
         This will effectively remove the index entries of the respective path at all
         non-null stages and add the given blob as new stage null blob.
 
-        For each path there may only be one blob, otherwise a :class:`ValueError` will
-        be raised claiming the path is already at stage 0.
+        For each path there may only be one blob, otherwise a :exc:`ValueError` will be
+        raised claiming the path is already at stage 0.
 
         :raise ValueError:
             If one of the blobs already existed at stage 0.
@@ -644,8 +644,8 @@ def _process_diff_args(
     def _to_relative_path(self, path: PathLike) -> PathLike:
         """
         :return:
-            Version of path relative to our git directory or raise :class:`ValueError`
-            if it is not within our git directory.
+            Version of path relative to our git directory or raise :exc:`ValueError` if
+            it is not within our git directory.
 
         :raise ValueError:
         """
@@ -1215,7 +1215,7 @@ def checkout(
         :param force:
             If ``True``, existing files will be overwritten even if they contain local
             modifications.
-            If ``False``, these will trigger a :class:`~git.exc.CheckoutError`.
+            If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
 
         :param fprogress:
             See :meth:`IndexFile.add` for signature and explanation.
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index e5af5eb31..10b994e9b 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -52,7 +52,7 @@ def mkhead(repo: "Repo", path: PathLike) -> "Head":
 
 def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference":
     """Find the remote branch matching the name of the given branch or raise
-    :class:`~git.exc.InvalidGitRepositoryError`."""
+    :exc:`~git.exc.InvalidGitRepositoryError`."""
     for remote in remotes:
         try:
             return remote.refs[branch_name]
diff --git a/git/objects/tree.py b/git/objects/tree.py
index ad67f8e47..c74df58a9 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -100,8 +100,8 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
         """Add the given item to the tree.
 
         If an item with the given name already exists, nothing will be done, but a
-        :class:`ValueError` will be raised if the sha and mode of the existing item do
-        not match the one you add, unless `force` is ``True``.
+        :exc:`ValueError` will be raised if the sha and mode of the existing item do not
+        match the one you add, unless `force` is ``True``.
 
         :param sha:
             The 20 or 40 byte sha of the item to add.
diff --git a/git/refs/head.py b/git/refs/head.py
index b65189621..86321d9ea 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -247,7 +247,7 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
 
         :param force:
             If ``True``, changes to the index and the working tree will be discarded.
-            If ``False``, :class:`~git.exc.GitCommandError` will be raised in that
+            If ``False``, :exc:`~git.exc.GitCommandError` will be raised in that
             situation.
 
         :param kwargs:
diff --git a/git/refs/reference.py b/git/refs/reference.py
index cf418aa5d..62fb58420 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -21,7 +21,7 @@
 
 
 def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]:
-    """A decorator raising :class:`ValueError` if we are not a valid remote, based on the
+    """A decorator raising :exc:`ValueError` if we are not a valid remote, based on the
     path."""
 
     def wrapper(self: T_References, *args: Any) -> _T:
diff --git a/git/refs/remote.py b/git/refs/remote.py
index 5cbd1b81b..3f9c6c6be 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -76,5 +76,5 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None:
 
     @classmethod
     def create(cls, *args: Any, **kwargs: Any) -> NoReturn:
-        """Raise :class:`TypeError`. Defined so the ``create`` method is disabled."""
+        """Raise :exc:`TypeError`. Defined so the ``create`` method is disabled."""
         raise TypeError("Cannot explicitly create remote references")
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index dea047d83..2701f9f2b 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -738,7 +738,7 @@ def create(
 
         :param force:
             If ``True``, force creation even if a symbolic reference with that name
-            already exists. Raise :class:`OSError` otherwise.
+            already exists. Raise :exc:`OSError` otherwise.
 
         :param logmsg:
             If not ``None``, the message to append to the reflog.
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 13c85ea22..738e5816d 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -153,8 +153,8 @@ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[A
 
     :param return_ref:
         If ``True``, and name specifies a reference, we will return the reference
-        instead of the object. Otherwise it will raise :class:`~gitdb.exc.BadObject` or
-        :class:`~gitdb.exc.BadName`.
+        instead of the object. Otherwise it will raise :exc:`~gitdb.exc.BadObject` or
+        :exc:`~gitdb.exc.BadName`.
     """
     hexsha: Union[None, str, bytes] = None
 
diff --git a/git/types.py b/git/types.py
index a30179a25..e3ae9e3d5 100644
--- a/git/types.py
+++ b/git/types.py
@@ -190,7 +190,7 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception,
         mismatch and cause a mypy error.
 
     :param raise_error:
-        If ``True``, will also raise :class:`ValueError` with a general "unhandled
+        If ``True``, will also raise :exc:`ValueError` with a general "unhandled
         literal" message, or the exception object passed as `exc`.
 
     :param exc:
diff --git a/git/util.py b/git/util.py
index 0de724ceb..37986edaa 100644
--- a/git/util.py
+++ b/git/util.py
@@ -158,8 +158,8 @@ def _read_win_env_flag(name: str, default: bool) -> bool:
 
 
 def unbare_repo(func: Callable[..., T]) -> Callable[..., T]:
-    """Methods with this decorator raise
-    :class:`~git.exc.InvalidGitRepositoryError` if they encounter a bare repository."""
+    """Methods with this decorator raise :exc:`~git.exc.InvalidGitRepositoryError` if
+    they encounter a bare repository."""
 
     from .exc import InvalidGitRepositoryError
 
@@ -1094,8 +1094,8 @@ def __init__(
         self._max_block_time = max_block_time_s
 
     def _obtain_lock(self) -> None:
-        """This method blocks until it obtained the lock, or raises :class:`IOError` if
-        it ran out of time or if the parent directory was not available anymore.
+        """This method blocks until it obtained the lock, or raises :exc:`IOError` if it
+        ran out of time or if the parent directory was not available anymore.
 
         If this method returns, you are guaranteed to own the lock.
         """

From 1e5a9449573bfe70987dcaf44e120895131e9c66 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 17 Mar 2024 19:25:34 -0400
Subject: [PATCH 663/994] Add a script to validate refactored imports

This script can be removed after imports are refactored and checked
to see that module contents are the same as before or otherwise
non-broken.

This script assumes that module contents are the same as the
contents of their dictionaries, and that all modules in the project
get imported as a consequence of importing the top-level module.
These are both the case currently for GitPython, but they do not
hold for all projects, and may not hold for GitPython at some point
in the future.
---
 .gitignore  |  4 ++++
 modattrs.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+)
 create mode 100755 modattrs.py

diff --git a/.gitignore b/.gitignore
index 7765293d8..1be4f3201 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,7 @@ output.txt
 
 # Finder metadata
 .DS_Store
+
+# Output files for modattrs.py (these entries will be removed soon)
+a
+b
diff --git a/modattrs.py b/modattrs.py
new file mode 100755
index 000000000..245f68912
--- /dev/null
+++ b/modattrs.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+"""Script to get the names and "stabilized" reprs of module attributes in GitPython.
+
+Run with :envvar:`PYTHONHASHSEED` set to ``0`` for fully comparable results. These are
+only still meaningful for comparing if the same platform and Python version are used.
+
+The output of this script should probably not be committed, because within the reprs of
+objects found in modules, it may contain sensitive information, such as API keys stored
+in environment variables. The "sanitization" performed here is only for common forms of
+whitespace that clash with the output format.
+"""
+
+# fmt: off
+
+__all__ = ["git", "main"]
+
+import itertools
+import re
+import sys
+
+import git
+
+
+def main():
+    # This assumes `import git` causes all of them to be loaded.
+    gitpython_modules = sorted(
+        (module_name, module)
+        for module_name, module in sys.modules.items()
+        if re.match(r"git(?:\.|$)", module_name)
+    )
+
+    # We will print two blank lines between successive module reports.
+    separators = itertools.chain(("",), itertools.repeat("\n\n"))
+
+    # Report each module's contents.
+    for (module_name, module), separator in zip(gitpython_modules, separators):
+        print(f"{separator}{module_name}:")
+
+        attributes = sorted(
+            (name, value)
+            for name, value in module.__dict__.items()
+            if name != "__all__"  # Because we are deliberately adding these.
+        )
+
+        for name, value in attributes:
+            sanitized_repr = re.sub(r"[\r\n\v\f]", "?", repr(value))
+            normalized_repr = re.sub(r" at 0x[0-9a-fA-F]+", " at 0x...", sanitized_repr)
+            print(f"    {name}:  {normalized_repr}")
+
+
+if __name__ == "__main__":
+    main()

From 5b2771d23af2156fb7e12ee6406bffbabcb9e95d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 10:59:37 -0400
Subject: [PATCH 664/994] Add regression tests of the git.util aliasing
 situation

Although this situation is not inherently desirable, for backward
compatibility it cannot change at this time. It may be possible to
change it in the next major version of GitPython, but even then it
should not be changed accidentally, which can easily happen while
refactoring imports.

This tests the highest-risk accidental change (of those that are
currently known) of the kind that the temporary modattrs.py script
exists to help safeguard against. That script will be removed when
the immediately forthcoming import refactoring is complete, whereas
these test cases can be kept.

For information about the specific situation this helps ensure
isn't changed accidentally, see the new test cases' docstrings, as
well as the next commit (which will test modattrs.py and these test
cases by performing an incomplete change that would be a bug until
completed).

This commit adds three test cases. The first tests the unintuitive
aspect of the current situation:

- test_git_util_attribute_is_git_index_util

The other two test the intuitive aspects of it, i.e., they test
that changes (perhaps in an attempt to preserve the aspect needed
for backward compatibility) do not make `git.util` unusual in new
(and themselves incompatible) ways:

- test_git_index_util_attribute_is_git_index_util
- test_separate_git_util_module_exists

The latter tests should also clarify, for readers of the tests, the
limited nature of the condition the first test asserts.
---
 test/test_imports.py | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)
 create mode 100644 test/test_imports.py

diff --git a/test/test_imports.py b/test/test_imports.py
new file mode 100644
index 000000000..8e70c6689
--- /dev/null
+++ b/test/test_imports.py
@@ -0,0 +1,32 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
+import sys
+
+import git
+
+
+def test_git_util_attribute_is_git_index_util():
+    """The top-level module's ``util`` attribute is really :mod:`git.index.util`.
+
+    Although this situation is unintuitive and not a design goal, this has historically
+    been the case, and it should not be changed without considering the effect on
+    backward compatibility. In practice, it cannot be changed at least until the next
+    major version of GitPython. This test checks that it is not accidentally changed,
+    which could happen when refactoring imports.
+    """
+    assert git.util is git.index.util
+
+
+def test_git_index_util_attribute_is_git_index_util():
+    """Nothing unusual is happening with git.index.util itself."""
+    assert git.index.util is sys.modules["git.index.util"]
+
+
+def test_separate_git_util_module_exists():
+    """The real git.util and git.index.util modules really are separate.
+
+    The real git.util module can be accessed to import a name ``...` by writing
+    ``from git.util import ...``, and the module object can be accessed in sys.modules.
+    """
+    assert sys.modules["git.util"] is not sys.modules["git.index.util"]

From fc86a238f99a6406ed949a7d690ec0f6be2f31e0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 24 Feb 2024 10:52:49 -0500
Subject: [PATCH 665/994] Incompletely change git.index imports to test
 modattrs.py

This also checks if the regression tests in test_imports.py work.

This replaces wildcard imports in git.index with explicit imports,
and adds git.index.__all__. But this temporarily omits from it the
public attributes of git.index that name its Python submodules and
are thus are present as an indirect effect of importing names
*from* them (since doing so also imports them).

This partial change, until it is completed in the next commit,
represents the kind of bug that modattrs.py seeks to safeguard
against, and verifies that a diff between old and current output of
modattrs.py clearly shows it. This diff is temporarily included as
ab.diff, which will be deleted in the next commit.

The key problem that diff reveals is the changed value of the util
attribute of the top-level git module. Due to the way wildcard
imports have been used within GitPython for its own modules,
expressions like `git.util` (where `git` is the top-level git
module) and imports like `from git import util` are actually
referring to git.index.util, rather than the util Python submodule
of the git module (conceptually git.util), which can only be
accessed via `from git.util import ...` or in `sys.modules`.
Although this is not an inherently good situation, in practice it
has to be maintained at least until the next major version, because
reasonable code that uses GitPython would be broken if the
situation were changed to be more intuitive.

But the more intuitive behavior automatically happens if wildcard
imports are replaced with explicit imports of the names they had
originally intended to import (in this case, in the top-level git
module, which has not yet been done but will be), or if __all__ is
added where helpful (in this case, in git.index, which this does).
Adding the names explicitly is sufficient to restore the old
behavior that is needed for backward compatibility, and has the
further advantage that the unintuitive behavior will be explicit
once all wildcard imports are replaced and __all__ is added to each
module where it would be helpful. The modattrs.py script serves to
ensure incompatible changes are not made; this commit checks it.

The tests in test_imports.py also cover this specific anticipated
incompatibility in git.util, but not all breakages that may arise
when refactoring imports and that diff-ing modattrs.py output would
help catch.
---
 ab.diff               | 30 ++++++++++++++++++++++++++++++
 git/index/__init__.py | 13 +++++++++++--
 git/index/base.py     |  5 ++---
 git/index/typ.py      |  5 ++---
 4 files changed, 45 insertions(+), 8 deletions(-)
 create mode 100644 ab.diff

diff --git a/ab.diff b/ab.diff
new file mode 100644
index 000000000..34abf9b16
--- /dev/null
+++ b/ab.diff
@@ -0,0 +1,30 @@
+diff --git a/a b/b
+index 81b3f984..7b8c8ede 100644
+--- a/a
++++ b/b
+@@ -83,14 +83,12 @@ git:
+     __path__:  ['C:\\Users\\ek\\source\\repos\\GitPython\\git']
+     __spec__:  ModuleSpec(name='git', loader=<_frozen_importlib_external.SourceFileLoader object at 0x...>, origin='C:\\Users\\ek\\source\\repos\\GitPython\\git\\__init__.py', submodule_search_locations=['C:\\Users\\ek\\source\\repos\\GitPython\\git'])
+     __version__:  'git'
+-    base:  <module 'git.index.base' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\base.py'>
+     cmd:  <module 'git.cmd' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\cmd.py'>
+     compat:  <module 'git.compat' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\compat.py'>
+     config:  <module 'git.config' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\config.py'>
+     db:  <module 'git.db' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\db.py'>
+     diff:  <module 'git.diff' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\diff.py'>
+     exc:  <module 'git.exc' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\exc.py'>
+-    fun:  <module 'git.index.fun' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\fun.py'>
+     head:  <module 'git.refs.head' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\head.py'>
+     index:  <module 'git.index' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\__init__.py'>
+     log:  <module 'git.refs.log' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\log.py'>
+@@ -106,9 +104,8 @@ git:
+     symbolic:  <module 'git.refs.symbolic' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\symbolic.py'>
+     tag:  <module 'git.refs.tag' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\tag.py'>
+     to_hex_sha:  <function to_hex_sha at 0x...>
+-    typ:  <module 'git.index.typ' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\typ.py'>
+     types:  <module 'git.types' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\types.py'>
+-    util:  <module 'git.index.util' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\util.py'>
++    util:  <module 'git.util' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\util.py'>
+ 
+ 
+ git.cmd:
diff --git a/git/index/__init__.py b/git/index/__init__.py
index c65722cd8..ba48110fd 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -3,5 +3,14 @@
 
 """Initialize the index package."""
 
-from .base import *  # noqa: F401 F403
-from .typ import *  # noqa: F401 F403
+__all__ = [
+    "BaseIndexEntry",
+    "BlobFilter",
+    "CheckoutError",
+    "IndexEntry",
+    "IndexFile",
+    "StageType",
+]
+
+from .base import CheckoutError, IndexFile
+from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType
diff --git a/git/index/base.py b/git/index/base.py
index fb91e092c..38c14252b 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -6,6 +6,8 @@
 """Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
 of index manipulations such as querying and merging."""
 
+__all__ = ("IndexFile", "CheckoutError", "StageType")
+
 import contextlib
 import datetime
 import glob
@@ -81,9 +83,6 @@
 # ------------------------------------------------------------------------------------
 
 
-__all__ = ("IndexFile", "CheckoutError", "StageType")
-
-
 @contextlib.contextmanager
 def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
     """Create a named temporary file git subprocesses can open, deleting it afterward.
diff --git a/git/index/typ.py b/git/index/typ.py
index ffd76dc46..eb0dd4341 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -3,13 +3,14 @@
 
 """Additional types used by the index."""
 
+__all__ = ("BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType")
+
 from binascii import b2a_hex
 from pathlib import Path
 
 from .util import pack, unpack
 from git.objects import Blob
 
-
 # typing ----------------------------------------------------------------------
 
 from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
@@ -23,8 +24,6 @@
 
 # ---------------------------------------------------------------------------------
 
-__all__ = ("BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType")
-
 # { Invariants
 CE_NAMEMASK = 0x0FFF
 CE_STAGEMASK = 0x3000

From 4badc19f489c177a976d99da85d3c632797f7aeb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 12:16:33 -0400
Subject: [PATCH 666/994] Fix git.index imports

- Add the base, fun, typ, and util Python submodules of git.index
  to git.index.__all__ to restore the old behavior.

- Import them explicitly for clarity, even though they are bound to
  the same-named attributes of git.index either way. This should
  help human readers know what the entries in __all__ are referring
  to, and may also help some automated tools.

  This is a preliminary decision and not necessarily the one that
  will ultimately be taken in this import refactoring. It *may*
  cause some kinds of mistakes, such as those arising from
  ill-advised use of wildcard imports in production code *outside*
  GitPython, somewhat worse in their effects.

- Remove the ab.diff file that showed the problem this solves.

This resolves the problem deliberately introduced in the previous
incomplete commit, which modattrs.py output and test_imports test
results confirm.
---
 ab.diff               | 30 ------------------------------
 git/index/__init__.py |  5 +++++
 2 files changed, 5 insertions(+), 30 deletions(-)
 delete mode 100644 ab.diff

diff --git a/ab.diff b/ab.diff
deleted file mode 100644
index 34abf9b16..000000000
--- a/ab.diff
+++ /dev/null
@@ -1,30 +0,0 @@
-diff --git a/a b/b
-index 81b3f984..7b8c8ede 100644
---- a/a
-+++ b/b
-@@ -83,14 +83,12 @@ git:
-     __path__:  ['C:\\Users\\ek\\source\\repos\\GitPython\\git']
-     __spec__:  ModuleSpec(name='git', loader=<_frozen_importlib_external.SourceFileLoader object at 0x...>, origin='C:\\Users\\ek\\source\\repos\\GitPython\\git\\__init__.py', submodule_search_locations=['C:\\Users\\ek\\source\\repos\\GitPython\\git'])
-     __version__:  'git'
--    base:  <module 'git.index.base' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\base.py'>
-     cmd:  <module 'git.cmd' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\cmd.py'>
-     compat:  <module 'git.compat' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\compat.py'>
-     config:  <module 'git.config' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\config.py'>
-     db:  <module 'git.db' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\db.py'>
-     diff:  <module 'git.diff' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\diff.py'>
-     exc:  <module 'git.exc' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\exc.py'>
--    fun:  <module 'git.index.fun' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\fun.py'>
-     head:  <module 'git.refs.head' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\head.py'>
-     index:  <module 'git.index' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\__init__.py'>
-     log:  <module 'git.refs.log' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\log.py'>
-@@ -106,9 +104,8 @@ git:
-     symbolic:  <module 'git.refs.symbolic' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\symbolic.py'>
-     tag:  <module 'git.refs.tag' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\refs\\tag.py'>
-     to_hex_sha:  <function to_hex_sha at 0x...>
--    typ:  <module 'git.index.typ' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\typ.py'>
-     types:  <module 'git.types' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\types.py'>
--    util:  <module 'git.index.util' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\index\\util.py'>
-+    util:  <module 'git.util' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\util.py'>
- 
- 
- git.cmd:
diff --git a/git/index/__init__.py b/git/index/__init__.py
index ba48110fd..2086d67f9 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -4,6 +4,10 @@
 """Initialize the index package."""
 
 __all__ = [
+    "base",
+    "fun",
+    "typ",
+    "util",
     "BaseIndexEntry",
     "BlobFilter",
     "CheckoutError",
@@ -12,5 +16,6 @@
     "StageType",
 ]
 
+from . import base, fun, typ, util
 from .base import CheckoutError, IndexFile
 from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType

From 1c9bda222077a6227beed2e06df6f26e2e864807 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 13:59:43 -0400
Subject: [PATCH 667/994] Improve relative order of import groups, and __all__,
 in git.index

---
 git/index/base.py |  5 +++--
 git/index/fun.py  | 41 ++++++++++++++++-------------------------
 git/index/typ.py  |  3 ++-
 git/index/util.py |  7 ++-----
 4 files changed, 23 insertions(+), 33 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 38c14252b..b49841435 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -19,6 +19,9 @@
 import sys
 import tempfile
 
+from gitdb.base import IStream
+from gitdb.db import MemoryDB
+
 from git.compat import defenc, force_bytes
 import git.diff as git_diff
 from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
@@ -33,8 +36,6 @@
     unbare_repo,
     to_bin_sha,
 )
-from gitdb.base import IStream
-from gitdb.db import MemoryDB
 
 from .fun import (
     S_IFGITLINK,
diff --git a/git/index/fun.py b/git/index/fun.py
index 001e8f6f2..b24e803a3 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -4,22 +4,28 @@
 """Standalone functions to accompany the index implementation and make it more
 versatile."""
 
+__all__ = (
+    "write_cache",
+    "read_cache",
+    "write_tree_from_cache",
+    "entry_key",
+    "stat_mode_to_index_mode",
+    "S_IFGITLINK",
+    "run_commit_hook",
+    "hook_path",
+)
+
 from io import BytesIO
 import os
 import os.path as osp
 from pathlib import Path
-from stat import (
-    S_IFDIR,
-    S_IFLNK,
-    S_ISLNK,
-    S_ISDIR,
-    S_IFMT,
-    S_IFREG,
-    S_IXUSR,
-)
+from stat import S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_ISDIR, S_ISLNK, S_IXUSR
 import subprocess
 import sys
 
+from gitdb.base import IStream
+from gitdb.typ import str_tree_type
+
 from git.cmd import handle_process_output, safer_popen
 from git.compat import defenc, force_bytes, force_text, safe_decode
 from git.exc import HookExecutionError, UnmergedEntriesError
@@ -29,8 +35,6 @@
     tree_to_stream,
 )
 from git.util import IndexFileSHA1Writer, finalize_process
-from gitdb.base import IStream
-from gitdb.typ import str_tree_type
 
 from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
 from .util import pack, unpack
@@ -42,31 +46,18 @@
 from git.types import PathLike
 
 if TYPE_CHECKING:
-    from .base import IndexFile
     from git.db import GitCmdObjectDB
     from git.objects.tree import TreeCacheTup
 
-    # from git.objects.fun import EntryTupOrNone
+    from .base import IndexFile
 
 # ------------------------------------------------------------------------------------
 
-
 S_IFGITLINK = S_IFLNK | S_IFDIR
 """Flags for a submodule."""
 
 CE_NAMEMASK_INV = ~CE_NAMEMASK
 
-__all__ = (
-    "write_cache",
-    "read_cache",
-    "write_tree_from_cache",
-    "entry_key",
-    "stat_mode_to_index_mode",
-    "S_IFGITLINK",
-    "run_commit_hook",
-    "hook_path",
-)
-
 
 def hook_path(name: str, git_dir: PathLike) -> str:
     """:return: path to the given named hook in the given git repository directory"""
diff --git a/git/index/typ.py b/git/index/typ.py
index eb0dd4341..0fbcd69f0 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -8,9 +8,10 @@
 from binascii import b2a_hex
 from pathlib import Path
 
-from .util import pack, unpack
 from git.objects import Blob
 
+from .util import pack, unpack
+
 # typing ----------------------------------------------------------------------
 
 from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast
diff --git a/git/index/util.py b/git/index/util.py
index 0bad11571..4aee61bce 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -3,6 +3,8 @@
 
 """Index utilities."""
 
+__all__ = ("TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir")
+
 import contextlib
 from functools import wraps
 import os
@@ -22,14 +24,9 @@
 
 # ---------------------------------------------------------------------------------
 
-
-__all__ = ("TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir")
-
 # { Aliases
 pack = struct.pack
 unpack = struct.unpack
-
-
 # } END aliases
 
 

From 8b51af36a4943f75fb66d553d55e381859fe3a34 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 14:31:12 -0400
Subject: [PATCH 668/994] Improve order of imports and __all__ in git.refs
 submodules

---
 git/refs/head.py      | 12 ++++++------
 git/refs/log.py       | 17 ++++++++---------
 git/refs/reference.py |  9 +++++----
 git/refs/remote.py    | 11 +++++------
 git/refs/symbolic.py  | 11 ++++++-----
 git/refs/tag.py       |  8 +++-----
 6 files changed, 33 insertions(+), 35 deletions(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index 86321d9ea..7e6fc3377 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -6,12 +6,14 @@
 Note the distinction between the :class:`HEAD` and :class:`Head` classes.
 """
 
+__all__ = ["HEAD", "Head"]
+
 from git.config import GitConfigParser, SectionConstraint
-from git.util import join_path
 from git.exc import GitCommandError
+from git.util import join_path
 
-from .symbolic import SymbolicReference
 from .reference import Reference
+from .symbolic import SymbolicReference
 
 # typing ---------------------------------------------------
 
@@ -26,8 +28,6 @@
 
 # -------------------------------------------------------------------
 
-__all__ = ["HEAD", "Head"]
-
 
 def strip_quotes(string: str) -> str:
     if string.startswith('"') and string.endswith('"'):
@@ -36,8 +36,8 @@ def strip_quotes(string: str) -> str:
 
 
 class HEAD(SymbolicReference):
-    """Special case of a SymbolicReference representing the repository's HEAD
-    reference."""
+    """Special case of a :class:`~git.refs.symbolic.SymbolicReference` representing the
+    repository's HEAD reference."""
 
     _HEAD_NAME = "HEAD"
     _ORIG_HEAD_NAME = "ORIG_HEAD"
diff --git a/git/refs/log.py b/git/refs/log.py
index f98f56f11..17e3a94b3 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -1,44 +1,43 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ["RefLog", "RefLogEntry"]
+
 from mmap import mmap
+import os.path as osp
 import re
 import time as _time
 
 from git.compat import defenc
 from git.objects.util import (
-    parse_date,
     Serializable,
     altz_to_utctz_str,
+    parse_date,
 )
 from git.util import (
     Actor,
     LockedFD,
     LockFile,
     assure_directory_exists,
-    to_native_path,
     bin_to_hex,
     file_contents_ro_filepath,
+    to_native_path,
 )
 
-import os.path as osp
-
-
 # typing ------------------------------------------------------------------
 
-from typing import Iterator, List, Tuple, Union, TYPE_CHECKING
+from typing import Iterator, List, Tuple, TYPE_CHECKING, Union
 
 from git.types import PathLike
 
 if TYPE_CHECKING:
     from io import BytesIO
-    from git.refs import SymbolicReference
+
     from git.config import GitConfigParser, SectionConstraint
+    from git.refs import SymbolicReference
 
 # ------------------------------------------------------------------------------
 
-__all__ = ["RefLog", "RefLogEntry"]
-
 
 class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
     """Named tuple allowing easy access to the revlog data fields."""
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 62fb58420..e5d473779 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -1,7 +1,10 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ["Reference"]
+
 from git.util import IterableObj, LazyMixin
+
 from .symbolic import SymbolicReference, T_References
 
 # typing ------------------------------------------------------------------
@@ -15,8 +18,6 @@
 
 # ------------------------------------------------------------------------------
 
-__all__ = ["Reference"]
-
 # { Utilities
 
 
@@ -34,7 +35,7 @@ def wrapper(self: T_References, *args: Any) -> _T:
     return wrapper
 
 
-# }END utilities
+# } END utilities
 
 
 class Reference(SymbolicReference, LazyMixin, IterableObj):
@@ -142,7 +143,7 @@ def iter_items(
         but will return non-detached references as well."""
         return cls._iter_items(repo, common_path)
 
-    # }END interface
+    # } END interface
 
     # { Remote Interface
 
diff --git a/git/refs/remote.py b/git/refs/remote.py
index 3f9c6c6be..b4f4f7b36 100644
--- a/git/refs/remote.py
+++ b/git/refs/remote.py
@@ -3,24 +3,23 @@
 
 """Module implementing a remote object allowing easy access to git remotes."""
 
+__all__ = ["RemoteReference"]
+
 import os
 
 from git.util import join_path
 
 from .head import Head
 
-
-__all__ = ["RemoteReference"]
-
 # typing ------------------------------------------------------------------
 
-from typing import Any, Iterator, NoReturn, Union, TYPE_CHECKING
-from git.types import PathLike
+from typing import Any, Iterator, NoReturn, TYPE_CHECKING, Union
 
+from git.types import PathLike
 
 if TYPE_CHECKING:
+    from git.remote import Remote
     from git.repo import Repo
-    from git import Remote
 
 # ------------------------------------------------------------------------------
 
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 2701f9f2b..510850b2e 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -1,10 +1,14 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ["SymbolicReference"]
+
 import os
 
+from gitdb.exc import BadName, BadObject
+
 from git.compat import defenc
-from git.objects import Object
+from git.objects.base import Object
 from git.objects.commit import Commit
 from git.refs.log import RefLog
 from git.util import (
@@ -15,7 +19,6 @@
     join_path_native,
     to_native_path_linux,
 )
-from gitdb.exc import BadName, BadObject
 
 # typing ------------------------------------------------------------------
 
@@ -30,6 +33,7 @@
     Union,
     cast,
 )
+
 from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
@@ -45,9 +49,6 @@
 # ------------------------------------------------------------------------------
 
 
-__all__ = ["SymbolicReference"]
-
-
 def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike:
     """Find the git dir that is appropriate for the path."""
     name = f"{path}"
diff --git a/git/refs/tag.py b/git/refs/tag.py
index f653d4e7d..1e38663ae 100644
--- a/git/refs/tag.py
+++ b/git/refs/tag.py
@@ -8,10 +8,10 @@
 :mod:`git.objects.tag` module.
 """
 
-from .reference import Reference
-
 __all__ = ["TagReference", "Tag"]
 
+from .reference import Reference
+
 # typing ------------------------------------------------------------------
 
 from typing import Any, TYPE_CHECKING, Type, Union
@@ -19,12 +19,10 @@
 from git.types import AnyGitObject, PathLike
 
 if TYPE_CHECKING:
-    from git.objects import Commit
-    from git.objects import TagObject
+    from git.objects import Commit, TagObject
     from git.refs import SymbolicReference
     from git.repo import Repo
 
-
 # ------------------------------------------------------------------------------
 
 

From b25dd7e6c1b5453fce111a470832e8942430c934 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 14:43:05 -0400
Subject: [PATCH 669/994] Replace wildcard imports in git.refs

This uses explicit imports rather than wildcard imports in git.refs
for names from its submodules.

A comment suggested that the import order was deliberate. But each
of the modules being imported from defined __all__, and there was
no overlap in the names across any of them.

The other main reason to import in a specific order is an order
dependency in the side effects, but that does not appear to apply,
at least not at this time.

(In addition, at least for now, this adds explicit imports for the
Python submodules of git.refs, so it is clear that they can always
be accessed directly in git.refs without further imports, if
desired. For clarity, those appear first, and that makes the order
of the "from" imports not relevant to such side effects, due to the
"from" imports no longer causing modules to be loaded for the first
time. However, this is a much less important reason to consider the
other imports' reordering okay, because these module imports may
end up being removed later during this refactoring; their clarity
benefit might not be justified, because if production code outside
GitPython ill-advisedly uses wildcard imports, the bad effect of
doing so could be increased.)
---
 git/refs/__init__.py | 32 ++++++++++++++++++++++++--------
 1 file changed, 24 insertions(+), 8 deletions(-)

diff --git a/git/refs/__init__.py b/git/refs/__init__.py
index b0233e902..04279901a 100644
--- a/git/refs/__init__.py
+++ b/git/refs/__init__.py
@@ -1,12 +1,28 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# Import all modules in order, fix the names they require.
+__all__ = [
+    "head",
+    "log",
+    "reference",
+    "remote",
+    "symbolic",
+    "tag",
+    "HEAD",
+    "Head",
+    "RefLog",
+    "RefLogEntry",
+    "Reference",
+    "RemoteReference",
+    "SymbolicReference",
+    "Tag",
+    "TagReference",
+]
 
-from .symbolic import *  # noqa: F401 F403
-from .reference import *  # noqa: F401 F403
-from .head import *  # noqa: F401 F403
-from .tag import *  # noqa: F401 F403
-from .remote import *  # noqa: F401 F403
-
-from .log import *  # noqa: F401 F403
+from . import head, log, reference, remote, symbolic, tag
+from .head import HEAD, Head
+from .log import RefLog, RefLogEntry
+from .reference import Reference
+from .remote import RemoteReference
+from .symbolic import SymbolicReference
+from .tag import Tag, TagReference

From b32ef6528c0da39b91b6399c5593d10ca41c5865 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 15:01:33 -0400
Subject: [PATCH 670/994] Improve order of imports and __all__ in git.repo
 submodules

---
 git/repo/base.py |  4 ++--
 git/repo/fun.py  | 30 ++++++++++++++++--------------
 2 files changed, 18 insertions(+), 16 deletions(-)

diff --git a/git/repo/base.py b/git/repo/base.py
index ce164274e..63b21fdbf 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -5,6 +5,8 @@
 
 from __future__ import annotations
 
+__all__ = ("Repo",)
+
 import gc
 import logging
 import os
@@ -92,8 +94,6 @@
 
 _logger = logging.getLogger(__name__)
 
-__all__ = ("Repo",)
-
 
 class BlameEntry(NamedTuple):
     commit: Dict[str, Commit]
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 738e5816d..1defcb1ac 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -5,18 +5,31 @@
 
 from __future__ import annotations
 
+__all__ = (
+    "rev_parse",
+    "is_git_dir",
+    "touch",
+    "find_submodule_git_dir",
+    "name_to_object",
+    "short_to_long",
+    "deref_tag",
+    "to_commit",
+    "find_worktree_git_dir",
+)
+
 import os
 import os.path as osp
 from pathlib import Path
 import stat
 from string import digits
 
+from gitdb.exc import BadName, BadObject
+
 from git.cmd import Git
 from git.exc import WorkTreeRepositoryUnsupported
 from git.objects import Object
 from git.refs import SymbolicReference
-from git.util import hex_to_bin, bin_to_hex, cygpath
-from gitdb.exc import BadName, BadObject
+from git.util import cygpath, bin_to_hex, hex_to_bin
 
 # Typing ----------------------------------------------------------------------
 
@@ -29,22 +42,11 @@
     from git.objects import Commit, TagObject
     from git.refs.reference import Reference
     from git.refs.tag import Tag
+
     from .base import Repo
 
 # ----------------------------------------------------------------------------
 
-__all__ = (
-    "rev_parse",
-    "is_git_dir",
-    "touch",
-    "find_submodule_git_dir",
-    "name_to_object",
-    "short_to_long",
-    "deref_tag",
-    "to_commit",
-    "find_worktree_git_dir",
-)
-
 
 def touch(filename: str) -> str:
     with open(filename, "ab"):

From 0ba06e9425e90f7f787ac39f48562af54ff41038 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 15:03:49 -0400
Subject: [PATCH 671/994] Add git.repo.__all__ and make submodules explicit

- No need to import Repo "as Repo". Some tools only recognize this
  to give the name conceptually public status when it appears in
  type stub files (.pyi files), and listing it in the newly created
  __all__ is sufficient to let humans and all tools know it has
  that status.

- As very recently done in git.refs, this explicitly imports the
  submodules, so it is clear they are available and don't have to
  be explicitly imported. (Fundamental to the way they are used is
  that they will end up being imported in order to import Repo.)

  However, also as in git.refs, it may be that the problems this
  could cause in some inherently flawed but plausible uses are too
  greater for it to be justified. So this may be revisited.
---
 git/repo/__init__.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index 50a8c6f86..3b784b975 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -3,4 +3,7 @@
 
 """Initialize the repo package."""
 
-from .base import Repo as Repo  # noqa: F401
+__all__ = ["base", "fun", "Repo"]
+
+from . import base, fun
+from .base import Repo

From c946906c4e92512e4294f587eaccaf0029a425df Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 15:29:45 -0400
Subject: [PATCH 672/994] Improve order of imports and __all__ in git.objects.*

But not yet in git.objects.submodule.* modules.
---
 git/objects/submodule/base.py |  4 ++--
 git/objects/submodule/root.py |  5 +++--
 git/objects/submodule/util.py | 24 +++++++++++-------------
 3 files changed, 16 insertions(+), 17 deletions(-)

diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index d01aa448f..fa60bcdaf 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1,6 +1,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ["Submodule", "UpdateProgress"]
+
 import gc
 from io import BytesIO
 import logging
@@ -68,8 +70,6 @@
 
 # -----------------------------------------------------------------------------
 
-__all__ = ["Submodule", "UpdateProgress"]
-
 _logger = logging.getLogger(__name__)
 
 
diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py
index ae56e5ef4..d93193fa3 100644
--- a/git/objects/submodule/root.py
+++ b/git/objects/submodule/root.py
@@ -1,10 +1,13 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ["RootModule", "RootUpdateProgress"]
+
 import logging
 
 import git
 from git.exc import InvalidGitRepositoryError
+
 from .base import Submodule, UpdateProgress
 from .util import find_first_remote_branch
 
@@ -20,8 +23,6 @@
 
 # ----------------------------------------------------------------------------
 
-__all__ = ["RootModule", "RootUpdateProgress"]
-
 _logger = logging.getLogger(__name__)
 
 
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index 10b994e9b..fda81249b 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -1,12 +1,20 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import git
-from git.exc import InvalidGitRepositoryError
-from git.config import GitConfigParser
+__all__ = (
+    "sm_section",
+    "sm_name",
+    "mkhead",
+    "find_first_remote_branch",
+    "SubmoduleConfigParser",
+)
+
 from io import BytesIO
 import weakref
 
+import git
+from git.config import GitConfigParser
+from git.exc import InvalidGitRepositoryError
 
 # typing -----------------------------------------------------------------------
 
@@ -22,15 +30,6 @@
     from git import Remote
     from git.refs import RemoteReference
 
-
-__all__ = (
-    "sm_section",
-    "sm_name",
-    "mkhead",
-    "find_first_remote_branch",
-    "SubmoduleConfigParser",
-)
-
 # { Utilities
 
 
@@ -65,7 +64,6 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "
 
 # } END utilities
 
-
 # { Classes
 
 

From 4e9a2f2facb0da3adb9c6fe5165dcfb5c6627cb7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 15:42:09 -0400
Subject: [PATCH 673/994] Improve order of imports and __all__ in
 git.object.submodule.*

---
 git/objects/base.py           | 12 ++++-----
 git/objects/blob.py           |  6 ++---
 git/objects/commit.py         |  9 ++++---
 git/objects/fun.py            | 19 +++++++-------
 git/objects/submodule/util.py |  9 ++++---
 git/objects/tag.py            | 14 +++++++---
 git/objects/tree.py           |  9 +++----
 git/objects/util.py           | 49 +++++++++++++++++++----------------
 8 files changed, 68 insertions(+), 59 deletions(-)

diff --git a/git/objects/base.py b/git/objects/base.py
index 22d939aa6..07ff639e5 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -3,15 +3,17 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-import gitdb.typ as dbtyp
+__all__ = ("Object", "IndexObject")
+
 import os.path as osp
 
+import gitdb.typ as dbtyp
+
 from git.exc import WorkTreeRepositoryUnsupported
-from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex
+from git.util import LazyMixin, bin_to_hex, join_path_native, stream_copy
 
 from .util import get_object_type_by_name
 
-
 # typing ------------------------------------------------------------------
 
 from typing import Any, TYPE_CHECKING, Union
@@ -24,16 +26,14 @@
     from git.refs.reference import Reference
     from git.repo import Repo
 
-    from .tree import Tree
     from .blob import Blob
     from .submodule.base import Submodule
+    from .tree import Tree
 
 IndexObjUnion = Union["Tree", "Blob", "Submodule"]
 
 # --------------------------------------------------------------------------
 
-__all__ = ("Object", "IndexObject")
-
 
 class Object(LazyMixin):
     """Base class for classes representing git object types.
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 122d5f731..50500f550 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -3,17 +3,17 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ("Blob",)
+
 from mimetypes import guess_type
 import sys
 
-from . import base
-
 if sys.version_info >= (3, 8):
     from typing import Literal
 else:
     from typing_extensions import Literal
 
-__all__ = ("Blob",)
+from . import base
 
 
 class Blob(base.IndexObject):
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 6a60c30bd..b22308726 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -3,6 +3,8 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ("Commit",)
+
 from collections import defaultdict
 import datetime
 from io import BytesIO
@@ -14,10 +16,12 @@
 from time import altzone, daylight, localtime, time, timezone
 
 from gitdb import IStream
+
 from git.cmd import Git
 from git.diff import Diffable
-from git.util import hex_to_bin, Actor, Stats, finalize_process
+from git.util import Actor, Stats, finalize_process, hex_to_bin
 
+from . import base
 from .tree import Tree
 from .util import (
     Serializable,
@@ -27,7 +31,6 @@
     parse_actor_and_date,
     parse_date,
 )
-from . import base
 
 # typing ------------------------------------------------------------------
 
@@ -59,8 +62,6 @@
 
 _logger = logging.getLogger(__name__)
 
-__all__ = ("Commit",)
-
 
 class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
     """Wraps a git commit object.
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 5bd8a3d62..9bd2d8f10 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -3,8 +3,14 @@
 
 """Functions that are supposed to be as fast as possible."""
 
-from stat import S_ISDIR
+__all__ = (
+    "tree_to_stream",
+    "tree_entries_from_data",
+    "traverse_trees_recursive",
+    "traverse_tree_recursive",
+)
 
+from stat import S_ISDIR
 
 from git.compat import safe_decode, defenc
 
@@ -23,22 +29,15 @@
 
 if TYPE_CHECKING:
     from _typeshed import ReadableBuffer
+
     from git import GitCmdObjectDB
 
-EntryTup = Tuple[bytes, int, str]  # same as TreeCacheTup in tree.py
+EntryTup = Tuple[bytes, int, str]  # Same as TreeCacheTup in tree.py.
 EntryTupOrNone = Union[EntryTup, None]
 
 # ---------------------------------------------------
 
 
-__all__ = (
-    "tree_to_stream",
-    "tree_entries_from_data",
-    "traverse_trees_recursive",
-    "traverse_tree_recursive",
-)
-
-
 def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
     """Write the given list of entries into a stream using its ``write`` method.
 
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index fda81249b..3a09d6a17 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -23,12 +23,13 @@
 from git.types import PathLike
 
 if TYPE_CHECKING:
-    from .base import Submodule
     from weakref import ReferenceType
+
+    from git.refs import Head, RemoteReference
+    from git.remote import Remote
     from git.repo import Repo
-    from git.refs import Head
-    from git import Remote
-    from git.refs import RemoteReference
+
+    from .base import Submodule
 
 # { Utilities
 
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 52d79751f..5ad311590 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -9,12 +9,17 @@
 For lightweight tags, see the :mod:`git.refs.tag` module.
 """
 
+__all__ = ("TagObject",)
+
 import sys
 
+from git.compat import defenc
+from git.util import hex_to_bin
+
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
-from ..util import hex_to_bin
-from ..compat import defenc
+
+# typing ----------------------------------------------
 
 from typing import List, TYPE_CHECKING, Union
 
@@ -26,11 +31,12 @@
 if TYPE_CHECKING:
     from git.repo import Repo
     from git.util import Actor
-    from .commit import Commit
+
     from .blob import Blob
+    from .commit import Commit
     from .tree import Tree
 
-__all__ = ("TagObject",)
+# ---------------------------------------------------
 
 
 class TagObject(base.Object):
diff --git a/git/objects/tree.py b/git/objects/tree.py
index c74df58a9..18ccf2f30 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -3,16 +3,18 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ("TreeModifier", "Tree")
+
 import sys
 
 import git.diff as git_diff
 from git.util import IterableList, join_path, to_bin_sha
 
+from . import util
 from .base import IndexObjUnion, IndexObject
 from .blob import Blob
 from .fun import tree_entries_from_data, tree_to_stream
 from .submodule.base import Submodule
-from . import util
 
 # typing -------------------------------------------------
 
@@ -39,23 +41,20 @@
 
 if TYPE_CHECKING:
     from io import BytesIO
+
     from git.repo import Repo
 
 TreeCacheTup = Tuple[bytes, int, str]
 
 TraversedTreeTup = Union[Tuple[Union["Tree", None], IndexObjUnion, Tuple["Submodule", "Submodule"]]]
 
-
 # def is_tree_cache(inp: Tuple[bytes, int, str]) -> TypeGuard[TreeCacheTup]:
 #     return isinstance(inp[0], bytes) and isinstance(inp[1], int) and isinstance([inp], str)
 
 # --------------------------------------------------------
 
-
 cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b)
 
-__all__ = ("TreeModifier", "Tree")
-
 
 class TreeModifier:
     """A utility class providing methods to alter the underlying cache in a list-like
diff --git a/git/objects/util.py b/git/objects/util.py
index 297b33b70..6f6927b47 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -5,26 +5,40 @@
 
 """General utility functions."""
 
+__all__ = (
+    "get_object_type_by_name",
+    "parse_date",
+    "parse_actor_and_date",
+    "ProcessStreamAdapter",
+    "Traversable",
+    "altz_to_utctz_str",
+    "utctz_to_altz",
+    "verify_utctz",
+    "Actor",
+    "tzoffset",
+    "utc",
+)
+
 from abc import ABC, abstractmethod
 import calendar
 from collections import deque
 from datetime import datetime, timedelta, tzinfo
-from string import digits
 import re
+from string import digits
 import time
 import warnings
 
-from git.util import IterableList, IterableObj, Actor
+from git.util import Actor, IterableList, IterableObj
 
 # typing ------------------------------------------------------------
+
 from typing import (
     Any,
     Callable,
     Deque,
-    Iterator,
     # Generic,
+    Iterator,
     NamedTuple,
-    overload,
     Sequence,
     TYPE_CHECKING,
     Tuple,
@@ -32,19 +46,22 @@
     TypeVar,
     Union,
     cast,
+    overload,
 )
 
 from git.types import Has_id_attribute, Literal  # , _T
 
 if TYPE_CHECKING:
     from io import BytesIO, StringIO
-    from .commit import Commit
-    from .blob import Blob
-    from .tag import TagObject
-    from .tree import Tree, TraversedTreeTup
     from subprocess import Popen
-    from .submodule.base import Submodule
+
     from git.types import Protocol, runtime_checkable
+
+    from .blob import Blob
+    from .commit import Commit
+    from .submodule.base import Submodule
+    from .tag import TagObject
+    from .tree import TraversedTreeTup, Tree
 else:
     # Protocol = Generic[_T]  # Needed for typing bug #572?
     Protocol = ABC
@@ -68,20 +85,6 @@ class TraverseNT(NamedTuple):
 
 # --------------------------------------------------------------------
 
-__all__ = (
-    "get_object_type_by_name",
-    "parse_date",
-    "parse_actor_and_date",
-    "ProcessStreamAdapter",
-    "Traversable",
-    "altz_to_utctz_str",
-    "utctz_to_altz",
-    "verify_utctz",
-    "Actor",
-    "tzoffset",
-    "utc",
-)
-
 ZERO = timedelta(0)
 
 # { Functions

From c58be4c0b6e82dc1c0445f9e41e53263f5d2ec4e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 15:44:52 -0400
Subject: [PATCH 674/994] Remove a bit of old commented-out code in
 git.objects.*

---
 git/objects/tree.py | 4 ----
 git/objects/util.py | 4 +---
 2 files changed, 1 insertion(+), 7 deletions(-)

diff --git a/git/objects/tree.py b/git/objects/tree.py
index 18ccf2f30..d8320b319 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -48,9 +48,6 @@
 
 TraversedTreeTup = Union[Tuple[Union["Tree", None], IndexObjUnion, Tuple["Submodule", "Submodule"]]]
 
-# def is_tree_cache(inp: Tuple[bytes, int, str]) -> TypeGuard[TreeCacheTup]:
-#     return isinstance(inp[0], bytes) and isinstance(inp[1], int) and isinstance([inp], str)
-
 # --------------------------------------------------------
 
 cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b)
@@ -124,7 +121,6 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
         index = self._index_by_name(name)
 
         item = (sha, mode, name)
-        # assert is_tree_cache(item)
 
         if index == -1:
             self._cache.append(item)
diff --git a/git/objects/util.py b/git/objects/util.py
index 6f6927b47..9491b067c 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -36,7 +36,6 @@
     Any,
     Callable,
     Deque,
-    # Generic,
     Iterator,
     NamedTuple,
     Sequence,
@@ -49,7 +48,7 @@
     overload,
 )
 
-from git.types import Has_id_attribute, Literal  # , _T
+from git.types import Has_id_attribute, Literal
 
 if TYPE_CHECKING:
     from io import BytesIO, StringIO
@@ -63,7 +62,6 @@
     from .tag import TagObject
     from .tree import TraversedTreeTup, Tree
 else:
-    # Protocol = Generic[_T]  # Needed for typing bug #572?
     Protocol = ABC
 
     def runtime_checkable(f):

From 01c95ebcc7d41bc68e1f64f9b1ec01c30f29d591 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 16:38:39 -0400
Subject: [PATCH 675/994] Don't patch IndexObject and Object into
 git.objects.submodule.util

Such patching, which was introduced in 9519f18, seems no longer to
be necessary. Since git.objects.submodule.util.__all__ has existed
for a long time without including those names, they are not
conceptually public attributes of git.objects.submodule.util, so
they should not be in use by any code outside GitPython either.

The modattrs.py script shows the change, as expected, showing these
two names as no longer being in the git.objects.submodule.util
module dictionary, in output of:

    python modattrs.py >b
    git diff --no-index a b

However, because the removal is intentional, this is okay.
---
 git/objects/__init__.py | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 1061ec874..d636873a0 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -8,17 +8,10 @@
 from .base import *  # noqa: F403
 from .blob import *  # noqa: F403
 from .commit import *  # noqa: F403
-from .submodule import util as smutil
 from .submodule.base import *  # noqa: F403
 from .submodule.root import *  # noqa: F403
 from .tag import *  # noqa: F403
 from .tree import *  # noqa: F403
 
-# Fix import dependency - add IndexObject to the util module, so that it can be imported
-# by the submodule.base.
-smutil.IndexObject = IndexObject  # type: ignore[attr-defined]  # noqa: F405
-smutil.Object = Object  # type: ignore[attr-defined]  # noqa: F405
-del smutil
-
 # Must come after submodule was made available.
 __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]

From f89d065742ba5b62056f1aa679e1197124bf7bcf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 18:50:37 -0400
Subject: [PATCH 676/994] Fix git.objects.__all__ and make submodules explicit

The submodules being made explicit here are of course Python
submodules, not git submodules.

The git.objects.submodule Python submodule (which is *about* git
submodules) is made explicit here (as are the names imported from
that Python submodule's own Python submodules) along with the other
Python submodules of git.objects.

Unlike some other submodules, but like the top-level git module
until #1659, git.objects already defined __all__ but it was
dynamically constructed. As with git.__all__ previously (as noted
in #1859), I have used https://github.com/EliahKagan/deltall here
to check that it is safe, sufficient, and probably necessary to
replace this dynamically constructed __all__ with a literal list of
strings in which all of the original elements are included. See:
https://gist.github.com/EliahKagan/e627603717ca5f9cafaf8de9d9f27ad7

Running the modattrs.py script, and diffing against the output from
before the current import refactoring began, reveals two changes to
module contents as a result of the change here:

- git.objects no longer contains `inspect`, which it imported just
  to build the dynamically constructed __all__. Because this was
  not itself included in that __all__ or otherwise made public or
  used out of git.objects, that is okay. This is exactly analogous
  to the situation in 8197e90, which removed it from the top-level
  git module after #1659.

- The top-level git module now has has new attributes blob, commit,
  submodule, and tree, aliasing the corresponding modules. This
  has happened as a result of them being put in git.objects.__all__
  along with various names imported from them. (As noted in some
  prior commits, there are tradeoffs associated with doing this,
  and it may be that such elements of __all__ should be removed,
  here and elsewhere.)
---
 git/objects/__init__.py | 37 ++++++++++++++++++++++++++-----------
 1 file changed, 26 insertions(+), 11 deletions(-)

diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index d636873a0..692a468d6 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -3,15 +3,30 @@
 
 """Import all submodules' main classes into the package space."""
 
-import inspect
+__all__ = [
+    "base",
+    "blob",
+    "commit",
+    "submodule",
+    "tag",
+    "tree",
+    "IndexObject",
+    "Object",
+    "Blob",
+    "Commit",
+    "Submodule",
+    "UpdateProgress",
+    "RootModule",
+    "RootUpdateProgress",
+    "TagObject",
+    "Tree",
+    "TreeModifier",
+]
 
-from .base import *  # noqa: F403
-from .blob import *  # noqa: F403
-from .commit import *  # noqa: F403
-from .submodule.base import *  # noqa: F403
-from .submodule.root import *  # noqa: F403
-from .tag import *  # noqa: F403
-from .tree import *  # noqa: F403
-
-# Must come after submodule was made available.
-__all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))]
+from .base import IndexObject, Object
+from .blob import Blob
+from .commit import Commit
+from .submodule.base import Submodule, UpdateProgress
+from .submodule.root import RootModule, RootUpdateProgress
+from .tag import TagObject
+from .tree import Tree, TreeModifier

From 3786307f965d38cde9e251d191f71ff26903935d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 19:10:17 -0400
Subject: [PATCH 677/994] Make git.objects.util module docstring more specific

So git.objects.util is less likely to be confused with git.util.

(The modattrs.py script reveals the change in the value of
git.objects.util.__doc__, as expected.)
---
 git/objects/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/util.py b/git/objects/util.py
index 9491b067c..08aef132a 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""General utility functions."""
+"""General utility functions for working with git objects."""
 
 __all__ = (
     "get_object_type_by_name",

From de540b78caa71c9f1a0f098c0d1608287c8927ac Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 19:17:48 -0400
Subject: [PATCH 678/994] Add __all__ and imports in git.objects.submodule

This is the top-level of the git.objects.submodule subpackage, for
its own Python submodules.

This appears only not to have been done before because of a cyclic
import problem involving imports that are no longer present. Doing
it improves consistency, since all the other subpackages under the
top-level git package already do this.

The modattrs.py script reveals the four new attributes of
git.objects.submodule for the four classes obtained by the new
imports, as expected. (The explicit module imports do not change
the attribues because they are the same attributes as come into
existence due to those Python submodules being imported anywhere in
any style.)
---
 git/objects/__init__.py           |  3 +--
 git/objects/submodule/__init__.py | 15 +++++++++++++--
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 692a468d6..4f18c8754 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -26,7 +26,6 @@
 from .base import IndexObject, Object
 from .blob import Blob
 from .commit import Commit
-from .submodule.base import Submodule, UpdateProgress
-from .submodule.root import RootModule, RootUpdateProgress
+from .submodule import RootModule, RootUpdateProgress, Submodule, UpdateProgress
 from .tag import TagObject
 from .tree import Tree, TreeModifier
diff --git a/git/objects/submodule/__init__.py b/git/objects/submodule/__init__.py
index b11b568f2..383439545 100644
--- a/git/objects/submodule/__init__.py
+++ b/git/objects/submodule/__init__.py
@@ -1,5 +1,16 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-# NOTE: Cannot import anything here as the top-level __init__ has to handle
-# our dependencies.
+__all__ = [
+    "base",
+    "root",
+    "util",
+    "Submodule",
+    "UpdateProgress",
+    "RootModule",
+    "RootUpdateProgress",
+]
+
+from . import base, root, util
+from .base import Submodule, UpdateProgress
+from .root import RootModule, RootUpdateProgress

From a05597a1aeacec0c18772944208ab2757b0db34b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 19:48:07 -0400
Subject: [PATCH 679/994] Improve how imports and __all__ are written in
 git.util

+ Update the detailed comment about the unused import situation.
---
 git/util.py | 102 +++++++++++++++++++++++++++-------------------------
 1 file changed, 53 insertions(+), 49 deletions(-)

diff --git a/git/util.py b/git/util.py
index 37986edaa..cf5949693 100644
--- a/git/util.py
+++ b/git/util.py
@@ -3,6 +3,32 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import sys
+
+__all__ = [
+    "stream_copy",
+    "join_path",
+    "to_native_path_linux",
+    "join_path_native",
+    "Stats",
+    "IndexFileSHA1Writer",
+    "IterableObj",
+    "IterableList",
+    "BlockingLockFile",
+    "LockFile",
+    "Actor",
+    "get_user_id",
+    "assure_directory_exists",
+    "RemoteProgress",
+    "CallableRemoteProgress",
+    "rmtree",
+    "unbare_repo",
+    "HIDE_WINDOWS_KNOWN_ERRORS",
+]
+
+if sys.platform == "win32":
+    __all__.append("to_native_path_windows")
+
 from abc import abstractmethod
 import contextlib
 from functools import wraps
@@ -16,11 +42,27 @@
 import shutil
 import stat
 import subprocess
-import sys
 import time
 from urllib.parse import urlsplit, urlunsplit
 import warnings
 
+# NOTE: Unused imports can be improved now that CI testing has fully resumed. Some of
+# these be used indirectly through other GitPython modules, which avoids having to write
+# gitdb all the time in their imports. They are not in __all__, at least currently,
+# because they could be removed or changed at any time, and so should not be considered
+# conceptually public to code outside GitPython. Linters of course do not like it.
+from gitdb.util import (  # noqa: F401  # @IgnorePep8
+    LazyMixin,  # @UnusedImport
+    LockedFD,  # @UnusedImport
+    bin_to_hex,  # @UnusedImport
+    file_contents_ro_filepath,  # @UnusedImport
+    file_contents_ro,  # @UnusedImport
+    hex_to_bin,  # @UnusedImport
+    make_sha,
+    to_bin_sha,  # @UnusedImport
+    to_hex_sha,  # @UnusedImport
+)
+
 # typing ---------------------------------------------------------
 
 from typing import (
@@ -37,73 +79,36 @@
     Pattern,
     Sequence,
     Tuple,
+    TYPE_CHECKING,
     TypeVar,
     Union,
-    TYPE_CHECKING,
     cast,
     overload,
 )
 
 if TYPE_CHECKING:
+    from git.cmd import Git
+    from git.config import GitConfigParser, SectionConstraint
     from git.remote import Remote
     from git.repo.base import Repo
-    from git.config import GitConfigParser, SectionConstraint
-    from git import Git
 
-from .types import (
+from git.types import (
+    Files_TD,
+    Has_id_attribute,
+    HSH_TD,
     Literal,
-    SupportsIndex,
-    Protocol,
-    runtime_checkable,  # because behind py version guards
     PathLike,
-    HSH_TD,
+    Protocol,
+    SupportsIndex,
     Total_TD,
-    Files_TD,  # aliases
-    Has_id_attribute,
+    runtime_checkable,
 )
 
 # ---------------------------------------------------------------------
 
-from gitdb.util import (  # noqa: F401  # @IgnorePep8
-    make_sha,
-    LockedFD,  # @UnusedImport
-    file_contents_ro,  # @UnusedImport
-    file_contents_ro_filepath,  # @UnusedImport
-    LazyMixin,  # @UnusedImport
-    to_hex_sha,  # @UnusedImport
-    to_bin_sha,  # @UnusedImport
-    bin_to_hex,  # @UnusedImport
-    hex_to_bin,  # @UnusedImport
-)
-
 T_IterableObj = TypeVar("T_IterableObj", bound=Union["IterableObj", "Has_id_attribute"], covariant=True)
 # So IterableList[Head] is subtype of IterableList[IterableObj].
 
-# NOTE:  Some of the unused imports might be used/imported by others.
-# Handle once test-cases are back up and running.
-# Most of these are unused here, but are for use by git-python modules so these
-# don't see gitdb all the time. Flake of course doesn't like it.
-__all__ = [
-    "stream_copy",
-    "join_path",
-    "to_native_path_linux",
-    "join_path_native",
-    "Stats",
-    "IndexFileSHA1Writer",
-    "IterableObj",
-    "IterableList",
-    "BlockingLockFile",
-    "LockFile",
-    "Actor",
-    "get_user_id",
-    "assure_directory_exists",
-    "RemoteProgress",
-    "CallableRemoteProgress",
-    "rmtree",
-    "unbare_repo",
-    "HIDE_WINDOWS_KNOWN_ERRORS",
-]
-
 _logger = logging.getLogger(__name__)
 
 
@@ -292,7 +297,6 @@ def to_native_path_linux(path: PathLike) -> str:
         path = str(path)
         return path.replace("\\", "/")
 
-    __all__.append("to_native_path_windows")
     to_native_path = to_native_path_windows
 else:
     # No need for any work on Linux.

From 2053a3d66c1667f3b130347e9984779f94875719 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 19:58:34 -0400
Subject: [PATCH 680/994] Remove old commented-out change_type assertions in
 git.diff

---
 git/diff.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index a6322ff57..54a5cf9af 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -40,11 +40,6 @@
 
 Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
 
-
-# def is_change_type(inp: str) -> TypeGuard[Lit_change_type]:
-#     # return True
-#     return inp in ['A', 'D', 'C', 'M', 'R', 'T', 'U']
-
 # ------------------------------------------------------------------------
 
 
@@ -693,7 +688,6 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
             # Change type can be R100
             # R: status letter
             # 100: score (in case of copy and rename)
-            # assert is_change_type(_change_type[0]), f"Unexpected value for change_type received: {_change_type[0]}"
             change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
             score_str = "".join(_change_type[1:])
             score = int(score_str) if score_str.isdigit() else None

From b8bab43db59c8a110d11dd16e24d61adf65c18fd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 20:02:55 -0400
Subject: [PATCH 681/994] Remove old commented-out flagKeyLiteral assertions in
 git.remote

---
 git/remote.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 2c452022e..65bafc0e9 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -50,10 +50,6 @@
 
 flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"]
 
-# def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]:
-#     return inp in [' ', '!', '+', '-', '=', '*', 't', '?']
-
-
 # -------------------------------------------------------------
 
 _logger = logging.getLogger(__name__)
@@ -415,7 +411,6 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo":
             remote_local_ref_str,
             note,
         ) = match.groups()
-        # assert is_flagKeyLiteral(control_character), f"{control_character}"
         control_character = cast(flagKeyLiteral, control_character)
         try:
             _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t")

From 3d4e47623ef7dc76883abfd5d44fcaef92e7bd1c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 20:07:17 -0400
Subject: [PATCH 682/994] Improve how second-level imports and __all__ are
 written

These are in the modules that are directly under the top-level git
module (and are not themselves subpackages, with their own
submodules in the Python sense), except for:

- git.util, where this change was very recently made.

- git.compat, where no improvements of this kind were needed.

- git.types, which currently has no __all__ and will only benefit from
  it if what should go in it is carefully considered (and where the
  imports themselves are grouped, sorted, and formatted already).
---
 git/cmd.py    | 15 +++++++--------
 git/config.py |  7 ++++---
 git/db.py     | 13 ++++++-------
 git/diff.py   | 24 +++++++++++-------------
 git/exc.py    |  4 +++-
 git/remote.py |  4 ++--
 6 files changed, 33 insertions(+), 34 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 2862b1600..7459fae97 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -5,18 +5,20 @@
 
 from __future__ import annotations
 
-import re
+__all__ = ("Git",)
+
 import contextlib
 import io
 import itertools
 import logging
 import os
+import re
 import signal
-from subprocess import Popen, PIPE, DEVNULL
 import subprocess
+from subprocess import DEVNULL, PIPE, Popen
 import sys
-import threading
 from textwrap import dedent
+import threading
 
 from git.compat import defenc, force_bytes, safe_decode
 from git.exc import (
@@ -57,12 +59,11 @@
     overload,
 )
 
-from git.types import PathLike, Literal, TBD
+from git.types import Literal, PathLike, TBD
 
 if TYPE_CHECKING:
-    from git.repo.base import Repo
     from git.diff import DiffIndex
-
+    from git.repo.base import Repo
 
 # ---------------------------------------------------------------------------------
 
@@ -84,8 +85,6 @@
 
 _logger = logging.getLogger(__name__)
 
-__all__ = ("Git",)
-
 
 # ==============================================================================
 ## @name Utilities
diff --git a/git/config.py b/git/config.py
index f74d290cc..1c9761c1b 100644
--- a/git/config.py
+++ b/git/config.py
@@ -5,6 +5,8 @@
 
 """Parser for reading and writing configuration files."""
 
+__all__ = ("GitConfigParser", "SectionConstraint")
+
 import abc
 import configparser as cp
 import fnmatch
@@ -40,9 +42,10 @@
 from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T
 
 if TYPE_CHECKING:
-    from git.repo.base import Repo
     from io import BytesIO
 
+    from git.repo.base import Repo
+
 T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser")
 T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool)
 
@@ -58,8 +61,6 @@
 
 # -------------------------------------------------------------
 
-__all__ = ("GitConfigParser", "SectionConstraint")
-
 _logger = logging.getLogger(__name__)
 
 CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
diff --git a/git/db.py b/git/db.py
index 5b2ca4de2..302192bdf 100644
--- a/git/db.py
+++ b/git/db.py
@@ -3,27 +3,26 @@
 
 """Module with our own gitdb implementation - it uses the git command."""
 
-from git.util import bin_to_hex, hex_to_bin
-from gitdb.base import OInfo, OStream
-from gitdb.db import GitDB
-from gitdb.db import LooseObjectDB
+__all__ = ("GitCmdObjectDB", "GitDB")
 
+from gitdb.base import OInfo, OStream
+from gitdb.db import GitDB, LooseObjectDB
 from gitdb.exc import BadObject
+
+from git.util import bin_to_hex, hex_to_bin
 from git.exc import GitCommandError
 
 # typing-------------------------------------------------
 
 from typing import TYPE_CHECKING
+
 from git.types import PathLike
 
 if TYPE_CHECKING:
     from git.cmd import Git
 
-
 # --------------------------------------------------------
 
-__all__ = ("GitCmdObjectDB", "GitDB")
-
 
 class GitCmdObjectDB(LooseObjectDB):
     """A database representing the default git object store, which includes loose
diff --git a/git/diff.py b/git/diff.py
index 54a5cf9af..1381deca0 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,17 +3,17 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff")
+
 import enum
 import re
 
 from git.cmd import handle_process_output
 from git.compat import defenc
+from git.objects.blob import Blob
+from git.objects.util import mode_str_to_int
 from git.util import finalize_process, hex_to_bin
 
-from .objects.blob import Blob
-from .objects.util import mode_str_to_int
-
-
 # typing ------------------------------------------------------------------
 
 from typing import (
@@ -23,29 +23,27 @@
     Match,
     Optional,
     Tuple,
+    TYPE_CHECKING,
     TypeVar,
     Union,
-    TYPE_CHECKING,
     cast,
 )
 from git.types import Literal, PathLike
 
 if TYPE_CHECKING:
-    from .objects.tree import Tree
-    from .objects import Commit
-    from git.repo.base import Repo
-    from git.objects.base import IndexObject
     from subprocess import Popen
-    from git import Git
+
+    from git.cmd import Git
+    from git.objects.base import IndexObject
+    from git.objects.commit import Commit
+    from git.objects.tree import Tree
+    from git.repo.base import Repo
 
 Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
 
 # ------------------------------------------------------------------------
 
 
-__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff")
-
-
 @enum.unique
 class DiffConstants(enum.Enum):
     """Special objects for :meth:`Diffable.diff`.
diff --git a/git/exc.py b/git/exc.py
index 9f6462b39..583eee8c1 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -42,12 +42,14 @@
     ParseError,
     UnsupportedOperation,
 )
+
 from git.compat import safe_decode
 from git.util import remove_password_if_present
 
 # typing ----------------------------------------------------
 
-from typing import List, Sequence, Tuple, Union, TYPE_CHECKING
+from typing import List, Sequence, Tuple, TYPE_CHECKING, Union
+
 from git.types import PathLike
 
 if TYPE_CHECKING:
diff --git a/git/remote.py b/git/remote.py
index 65bafc0e9..54a5c459c 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -5,6 +5,8 @@
 
 """Module implementing a remote object allowing easy access to git remotes."""
 
+__all__ = ("RemoteProgress", "PushInfo", "FetchInfo", "Remote")
+
 import contextlib
 import logging
 import re
@@ -54,8 +56,6 @@
 
 _logger = logging.getLogger(__name__)
 
-__all__ = ("RemoteProgress", "PushInfo", "FetchInfo", "Remote")
-
 # { Utilities
 
 

From 6318eea9743efec557bd7dbfdea46ddc21f654f2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 20:27:20 -0400
Subject: [PATCH 683/994] Make F401 "unused import" suppressions more specific

In git.compat and git.util.

This applies them individually to each name that is known to be an
unused import, rather than to entire import statements (except
where the entire statement fit on one line and everything it
imported is known to be unused).

Either Ruff has the ability to accept this more granular style of
suppression, or I had been unaware of it from flake8 (used before).
I have veriifed that the suppressions are not superfluous: with no
suppressions, Ruff does warn. This commit makes no change in
git.types because it looks like no suppression at all may be
needed there anymore; that will be covered in the next commit.

This also removes the old `@UnusedImport` comments, which had been
kept before because they were more granular; this specificity is
now achieved by comments the tools being used can recognize.
---
 git/compat.py | 14 +++++++-------
 git/util.py   | 18 +++++++++---------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/git/compat.py b/git/compat.py
index 6f5376c9d..4ede8c985 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -14,18 +14,18 @@
 import os
 import sys
 
-from gitdb.utils.encoding import force_bytes, force_text  # noqa: F401  # @UnusedImport
+from gitdb.utils.encoding import force_bytes, force_text  # noqa: F401
 
 # typing --------------------------------------------------------------------
 
-from typing import (  # noqa: F401
-    Any,
+from typing import (
+    Any,  # noqa: F401
     AnyStr,
-    Dict,
-    IO,
+    Dict,  # noqa: F401
+    IO,  # noqa: F401
     Optional,
-    Tuple,
-    Type,
+    Tuple,  # noqa: F401
+    Type,  # noqa: F401
     Union,
     overload,
 )
diff --git a/git/util.py b/git/util.py
index cf5949693..a56b63d69 100644
--- a/git/util.py
+++ b/git/util.py
@@ -51,16 +51,16 @@
 # gitdb all the time in their imports. They are not in __all__, at least currently,
 # because they could be removed or changed at any time, and so should not be considered
 # conceptually public to code outside GitPython. Linters of course do not like it.
-from gitdb.util import (  # noqa: F401  # @IgnorePep8
-    LazyMixin,  # @UnusedImport
-    LockedFD,  # @UnusedImport
-    bin_to_hex,  # @UnusedImport
-    file_contents_ro_filepath,  # @UnusedImport
-    file_contents_ro,  # @UnusedImport
-    hex_to_bin,  # @UnusedImport
+from gitdb.util import (
+    LazyMixin,  # noqa: F401
+    LockedFD,  # noqa: F401
+    bin_to_hex,  # noqa: F401
+    file_contents_ro_filepath,  # noqa: F401
+    file_contents_ro,  # noqa: F401
+    hex_to_bin,  # noqa: F401
     make_sha,
-    to_bin_sha,  # @UnusedImport
-    to_hex_sha,  # @UnusedImport
+    to_bin_sha,  # noqa: F401
+    to_hex_sha,  # noqa: F401
 )
 
 # typing ---------------------------------------------------------

From 31bc8a4e062841e041e9fff664bb08366d8fce48 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 20:33:33 -0400
Subject: [PATCH 684/994] Remove unneeded F401 "Unused import" suppressions

In git.types.

It appears Ruff, unlike flake8, recognizes "import X as X" to mean
that "X" should not be considered unused, even when it appears
outside a .pyi type stub file where that notation is more commonly
used.

Those imports in git.types may benefit from being changed in some
way that uses a syntax whose intent is clearer in context, and
depending on how that is done it may even be necessary to bring
back suppressions. If so, they can be written more specifically.
(For now, changing them would express more than is known about what
names that are only present in git.types becuase it imports them
should continue to be imported, should be considered conceptually
public, or should be condered suitable for use within GitPython.)
---
 git/types.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/types.py b/git/types.py
index e3ae9e3d5..230422dff 100644
--- a/git/types.py
+++ b/git/types.py
@@ -3,7 +3,7 @@
 
 import os
 import sys
-from typing import (  # noqa: F401
+from typing import (
     Any,
     Callable,
     Dict,
@@ -17,7 +17,7 @@
 )
 
 if sys.version_info >= (3, 8):
-    from typing import (  # noqa: F401
+    from typing import (
         Literal,
         Protocol,
         SupportsIndex as SupportsIndex,
@@ -25,7 +25,7 @@
         runtime_checkable,
     )
 else:
-    from typing_extensions import (  # noqa: F401
+    from typing_extensions import (
         Literal,
         Protocol,
         SupportsIndex as SupportsIndex,

From abbe74d030a647664f646a8d43e071608593b6a3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 20:40:13 -0400
Subject: [PATCH 685/994] Fix a tiny import sorting nit

---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index a56b63d69..5839f9720 100644
--- a/git/util.py
+++ b/git/util.py
@@ -55,8 +55,8 @@
     LazyMixin,  # noqa: F401
     LockedFD,  # noqa: F401
     bin_to_hex,  # noqa: F401
-    file_contents_ro_filepath,  # noqa: F401
     file_contents_ro,  # noqa: F401
+    file_contents_ro_filepath,  # noqa: F401
     hex_to_bin,  # noqa: F401
     make_sha,
     to_bin_sha,  # noqa: F401

From 774525087bcc0389bf585b88ee38eabf69933183 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 22:09:52 -0400
Subject: [PATCH 686/994] Replace wildcard imports in top-level git module

- Use explicit imports instead of * imports.
- Remove now-unneeded linter suppressions.
- Alphabetize inside the try-block, though this will be undone.

This currently fails to import due to a cyclic import error, so the
third change, alphabetizing the imports, will have to be undone (at
least in the absence of other changes). It is not merely that they
should not be reordered in a way that makes them cross into or out
of the try-block, but that within the try block not all orders will
work.

There will be more to do for backward compatibility, but the
modattrs.py script, which imports the top-level git module, cannot
be run until the cyclic import problem is fixed.
---
 git/__init__.py | 95 ++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 78 insertions(+), 17 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index ca5bed7a3..ebcaf89a2 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -5,7 +5,7 @@
 
 # @PydevCodeAnalysisIgnore
 
-__all__ = [  # noqa: F405
+__all__ = [
     "Actor",
     "AmbiguousObjectName",
     "BadName",
@@ -88,32 +88,93 @@
 
 __version__ = "git"
 
-from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
+from typing import List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
 
 from gitdb.util import to_hex_sha
-from git.exc import *  # noqa: F403  # @NoMove @IgnorePep8
+
+from git.exc import (
+    AmbiguousObjectName,
+    BadName,
+    BadObject,
+    BadObjectType,
+    CacheError,
+    CheckoutError,
+    CommandError,
+    GitCommandError,
+    GitCommandNotFound,
+    GitError,
+    HookExecutionError,
+    InvalidDBRoot,
+    InvalidGitRepositoryError,
+    NoSuchPathError,
+    ODBError,
+    ParseError,
+    RepositoryDirtyError,
+    UnmergedEntriesError,
+    UnsafeOptionError,
+    UnsafeProtocolError,
+    UnsupportedOperation,
+    WorkTreeRepositoryUnsupported,
+)
 from git.types import PathLike
 
 try:
-    from git.compat import safe_decode  # @NoMove @IgnorePep8
-    from git.config import GitConfigParser  # @NoMove @IgnorePep8
-    from git.objects import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.refs import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.diff import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.db import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.cmd import Git  # @NoMove @IgnorePep8
-    from git.repo import Repo  # @NoMove @IgnorePep8
-    from git.remote import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.index import *  # noqa: F403  # @NoMove @IgnorePep8
-    from git.util import (  # @NoMove @IgnorePep8
-        LockFile,
+    from git.cmd import Git  # @NoMove
+    from git.compat import safe_decode  # @NoMove
+    from git.config import GitConfigParser  # @NoMove
+    from git.db import GitCmdObjectDB, GitDB  # @NoMove
+    from git.diff import (  # @NoMove
+        INDEX,
+        NULL_TREE,
+        Diff,
+        DiffConstants,
+        DiffIndex,
+        Diffable,
+    )
+    from git.index import (  # @NoMove
+        BaseIndexEntry,
+        BlobFilter,
+        CheckoutError,
+        IndexEntry,
+        IndexFile,
+        StageType,
+        util,  # noqa: F401  # For backward compatibility.
+    )
+    from git.objects import (  # @NoMove
+        Blob,
+        Commit,
+        IndexObject,
+        Object,
+        RootModule,
+        RootUpdateProgress,
+        Submodule,
+        TagObject,
+        Tree,
+        TreeModifier,
+        UpdateProgress,
+    )
+    from git.refs import (  # @NoMove
+        HEAD,
+        Head,
+        RefLog,
+        RefLogEntry,
+        Reference,
+        RemoteReference,
+        SymbolicReference,
+        Tag,
+        TagReference,
+    )
+    from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress  # @NoMove
+    from git.repo import Repo  # @NoMove
+    from git.util import (  # @NoMove
+        Actor,
         BlockingLockFile,
+        LockFile,
         Stats,
-        Actor,
         remove_password_if_present,
         rmtree,
     )
-except GitError as _exc:  # noqa: F405
+except GitError as _exc:
     raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
 
 # { Initialize git executable path

From 64c9efdad216954b19cb91a4c04ea2de9d598159 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 22:13:45 -0400
Subject: [PATCH 687/994] Restore relative order to fix circular import error

This still uses all explicit rather than wildcard imports (and
still omits suppressions that are no longer needed due to wildcard
imports not being used). But it brings back the old relative order
of the `from ... import ...` statements inside the try-block.

Since this fixes the circular import problem, it is possible to run
the modattrs.py script to check for changes. New changes since
replacing wildcard imports, which are probably undesirable, are the
removal of these attributes pointing to indirect Python submodules
of the git module:

    base -> git.index.base
    fun -> git.index.fun
    head -> git.refs.head
    log -> git.refs.log
    reference -> git.refs.reference
    symbolic -> git.refs.symbolic
    tag -> git.refs.tag
    typ -> git.index.typ
---
 git/__init__.py | 40 ++++++++++++++++++++--------------------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index ebcaf89a2..7e07dcf13 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -119,27 +119,8 @@
 from git.types import PathLike
 
 try:
-    from git.cmd import Git  # @NoMove
     from git.compat import safe_decode  # @NoMove
     from git.config import GitConfigParser  # @NoMove
-    from git.db import GitCmdObjectDB, GitDB  # @NoMove
-    from git.diff import (  # @NoMove
-        INDEX,
-        NULL_TREE,
-        Diff,
-        DiffConstants,
-        DiffIndex,
-        Diffable,
-    )
-    from git.index import (  # @NoMove
-        BaseIndexEntry,
-        BlobFilter,
-        CheckoutError,
-        IndexEntry,
-        IndexFile,
-        StageType,
-        util,  # noqa: F401  # For backward compatibility.
-    )
     from git.objects import (  # @NoMove
         Blob,
         Commit,
@@ -164,8 +145,27 @@
         Tag,
         TagReference,
     )
-    from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress  # @NoMove
+    from git.diff import (  # @NoMove
+        INDEX,
+        NULL_TREE,
+        Diff,
+        DiffConstants,
+        DiffIndex,
+        Diffable,
+    )
+    from git.db import GitCmdObjectDB, GitDB  # @NoMove
+    from git.cmd import Git  # @NoMove
     from git.repo import Repo  # @NoMove
+    from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress  # @NoMove
+    from git.index import (  # @NoMove
+        BaseIndexEntry,
+        BlobFilter,
+        CheckoutError,
+        IndexEntry,
+        IndexFile,
+        StageType,
+        util,  # noqa: F401  # For backward compatibility.
+    )
     from git.util import (  # @NoMove
         Actor,
         BlockingLockFile,

From 31f89a1fa9f7fea7a13ba7aa9079364c22fb0e68 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 23:17:09 -0400
Subject: [PATCH 688/994] Add the nonpublic indirect submodule aliases back for
 now

These should definitely never be used by code inside or outside of
GitPython, as they have never been public, having even been omitted
by the dynamic (and expansive) technique used to build git.__all__
in the past (which omitted modules intentionally).

However, to ease compatibility, for now they are back. This is so
that the change of making all imports explicit rather than using
wildcards does not break anything. However, code using these names
could never be relied on to work, and these should be considered
eligible for removal, at least from the perspective of code outside
GitPython.

That is for the indirect submodules whose absence as a same-named
direct submodule or attribute listed in __all__ was readily
discoverable.

The more difficult case is util. git.util is a module, git/util.py,
which is how it is treated when it appears immediately after the
"from" keyword, or as a key in sys.modules. However, the expression
`git.util`, and a `from git import util` import, instead access the
separate git.index.util module, which due to a wildcard import has
been an attribute of the top-level git module for a long time.

It may not be safe to change this, because although any code
anywhere is better off not relying on this, this situation hasn't
been (and isn't) immediately clear. To help with it somewhat, this
also includes a detailed comment over where util is imported from
git.index, explaining the situation.
---
 git/__init__.py | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/git/__init__.py b/git/__init__.py
index 7e07dcf13..a13030456 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -144,6 +144,11 @@
         SymbolicReference,
         Tag,
         TagReference,
+        head,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.head.
+        log,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.log.
+        reference,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.reference.
+        symbolic,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.symbolic.
+        tag,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.tag.
     )
     from git.diff import (  # @NoMove
         INDEX,
@@ -164,7 +169,21 @@
         IndexEntry,
         IndexFile,
         StageType,
-        util,  # noqa: F401  # For backward compatibility.
+        base,  # noqa: F401  # Nonpublic. May disappear! Use git.index.base.
+        fun,  # noqa: F401  # Nonpublic. May disappear! Use git.index.fun.
+        typ,  # noqa: F401  # Nonpublic. May disappear! Use git.index.typ.
+        #
+        # NOTE: The expression `git.util` evaluates to git.index.util, and the import
+        # `from git import util` imports git.index.util, NOT git.util. It may not be
+        # feasible to change this until the next major version, to avoid breaking code
+        # inadvertently relying on it. If git.index.util really is what you want, use or
+        # import from that name, to avoid confusion. To use the "real" git.util module,
+        # write `from git.util import ...`, or access it as `sys.modules["git.util"]`.
+        # (This differs from other historical indirect-submodule imports that are
+        # unambiguously nonpublic and are subject to immediate removal. Here, the public
+        # git.util module, even though different, makes it less discoverable that the
+        # expression `git.util` refers to a non-public attribute of the git module.)
+        util,  # noqa: F401
     )
     from git.util import (  # @NoMove
         Actor,

From 9bbbcb5e847c9f0e4c91cb75d1c93be2b9cb1f57 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 23:28:09 -0400
Subject: [PATCH 689/994] Further improve git.objects.util module docstring

---
 git/objects/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/objects/util.py b/git/objects/util.py
index 08aef132a..2cba89a9f 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""General utility functions for working with git objects."""
+"""Utility functions for working with git objects."""
 
 __all__ = (
     "get_object_type_by_name",

From 00f4cbcf46426311f3cb6748912a1ea675639cf2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 23:32:06 -0400
Subject: [PATCH 690/994] Add missing submodule imports in git.objects

Since they are listed in __all__. (They are imported either way
because names are imported from them, and this caused them to be
present with the same effect.)

Though they are proably about to be removed along with the
corresponding entries in __all__.
---
 git/objects/__init__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index 4f18c8754..d146901a1 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -23,6 +23,7 @@
     "TreeModifier",
 ]
 
+from . import base, blob, commit, submodule, tag, tree
 from .base import IndexObject, Object
 from .blob import Blob
 from .commit import Commit

From fcc741838dbffa45648f3f224c01b9cb8941adc2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 23:35:58 -0400
Subject: [PATCH 691/994] Don't explicitly list direct submodules in __all__

This is for non-toplevel __all__, as git.__all__ already did not
do this.

As noted in some of the previous commit messags that added them,
omitting them might be a bit safer in terms of the impact of bugs
bugs in code using GitPython, in that unexpected modules, some of
which have the same name as other modules within GitPython, won't
be imported due to wildcard imports from intermediate subpackages
(those that are below the top-level git package/module but collect
non-subpackage modules). Though it is hard to know, since some of
these would have been imported before, when an __all__ was not
defined at that level.

However, a separate benefit of omitting them is consistency with
git.__all__, which does not list the direct Python submodules of
the git module.

This does not affect the output of the modattrs.py script, because
the attributes exist with the same objects as their values as a
result of those Python submodules being imported (in "from" imports
and otherwise), including when importing the top-level git module.
---
 git/index/__init__.py             |  5 -----
 git/objects/__init__.py           |  7 -------
 git/objects/submodule/__init__.py | 11 +----------
 git/refs/__init__.py              |  7 -------
 git/repo/__init__.py              |  3 +--
 5 files changed, 2 insertions(+), 31 deletions(-)

diff --git a/git/index/__init__.py b/git/index/__init__.py
index 2086d67f9..ba48110fd 100644
--- a/git/index/__init__.py
+++ b/git/index/__init__.py
@@ -4,10 +4,6 @@
 """Initialize the index package."""
 
 __all__ = [
-    "base",
-    "fun",
-    "typ",
-    "util",
     "BaseIndexEntry",
     "BlobFilter",
     "CheckoutError",
@@ -16,6 +12,5 @@
     "StageType",
 ]
 
-from . import base, fun, typ, util
 from .base import CheckoutError, IndexFile
 from .typ import BaseIndexEntry, BlobFilter, IndexEntry, StageType
diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index d146901a1..4447ca50d 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -4,12 +4,6 @@
 """Import all submodules' main classes into the package space."""
 
 __all__ = [
-    "base",
-    "blob",
-    "commit",
-    "submodule",
-    "tag",
-    "tree",
     "IndexObject",
     "Object",
     "Blob",
@@ -23,7 +17,6 @@
     "TreeModifier",
 ]
 
-from . import base, blob, commit, submodule, tag, tree
 from .base import IndexObject, Object
 from .blob import Blob
 from .commit import Commit
diff --git a/git/objects/submodule/__init__.py b/git/objects/submodule/__init__.py
index 383439545..c0604e76f 100644
--- a/git/objects/submodule/__init__.py
+++ b/git/objects/submodule/__init__.py
@@ -1,16 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = [
-    "base",
-    "root",
-    "util",
-    "Submodule",
-    "UpdateProgress",
-    "RootModule",
-    "RootUpdateProgress",
-]
+__all__ = ["Submodule", "UpdateProgress", "RootModule", "RootUpdateProgress"]
 
-from . import base, root, util
 from .base import Submodule, UpdateProgress
 from .root import RootModule, RootUpdateProgress
diff --git a/git/refs/__init__.py b/git/refs/__init__.py
index 04279901a..d6157e6f3 100644
--- a/git/refs/__init__.py
+++ b/git/refs/__init__.py
@@ -2,12 +2,6 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 __all__ = [
-    "head",
-    "log",
-    "reference",
-    "remote",
-    "symbolic",
-    "tag",
     "HEAD",
     "Head",
     "RefLog",
@@ -19,7 +13,6 @@
     "TagReference",
 ]
 
-from . import head, log, reference, remote, symbolic, tag
 from .head import HEAD, Head
 from .log import RefLog, RefLogEntry
 from .reference import Reference
diff --git a/git/repo/__init__.py b/git/repo/__init__.py
index 3b784b975..66319ef95 100644
--- a/git/repo/__init__.py
+++ b/git/repo/__init__.py
@@ -3,7 +3,6 @@
 
 """Initialize the repo package."""
 
-__all__ = ["base", "fun", "Repo"]
+__all__ = ["Repo"]
 
-from . import base, fun
 from .base import Repo

From 78055a8b8473a25e8f36a3846b920a698e8ba66b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 18 Mar 2024 23:52:57 -0400
Subject: [PATCH 692/994] Pick a consistent type for __all__ (for now, list)

This makes all __all__ everywhere in the git package lists. Before,
roughly half were lists and half were tuples. There are reasonable
theoretical arguments for both, and in practice all tools today
support both. Traditionally using a list is far more common, and it
remains at least somewhat more common. Furthermore, git/util.py
uses a list and is currently written to append an element to it
that is conditionally defined on Windows (though it would probably
be fine for that to be the only list, since it reflects an actual
relevant difference about it).

The goal here is just to remove inconsistency that does not signify
anything and is the result of drift over time. If a reason (even
preference) arises to make them all tuples in the future, then that
is also probably fine.
---
 git/cmd.py                    | 2 +-
 git/config.py                 | 2 +-
 git/db.py                     | 2 +-
 git/diff.py                   | 2 +-
 git/index/base.py             | 2 +-
 git/index/fun.py              | 4 ++--
 git/index/typ.py              | 2 +-
 git/index/util.py             | 2 +-
 git/objects/base.py           | 2 +-
 git/objects/blob.py           | 2 +-
 git/objects/commit.py         | 2 +-
 git/objects/fun.py            | 4 ++--
 git/objects/submodule/util.py | 4 ++--
 git/objects/tag.py            | 2 +-
 git/objects/tree.py           | 2 +-
 git/objects/util.py           | 4 ++--
 git/remote.py                 | 2 +-
 git/repo/base.py              | 2 +-
 git/repo/fun.py               | 4 ++--
 19 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7459fae97..03e5d7ffc 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -5,7 +5,7 @@
 
 from __future__ import annotations
 
-__all__ = ("Git",)
+__all__ = ["Git"]
 
 import contextlib
 import io
diff --git a/git/config.py b/git/config.py
index 1c9761c1b..3ce9b123f 100644
--- a/git/config.py
+++ b/git/config.py
@@ -5,7 +5,7 @@
 
 """Parser for reading and writing configuration files."""
 
-__all__ = ("GitConfigParser", "SectionConstraint")
+__all__ = ["GitConfigParser", "SectionConstraint"]
 
 import abc
 import configparser as cp
diff --git a/git/db.py b/git/db.py
index 302192bdf..cacd030d0 100644
--- a/git/db.py
+++ b/git/db.py
@@ -3,7 +3,7 @@
 
 """Module with our own gitdb implementation - it uses the git command."""
 
-__all__ = ("GitCmdObjectDB", "GitDB")
+__all__ = ["GitCmdObjectDB", "GitDB"]
 
 from gitdb.base import OInfo, OStream
 from gitdb.db import GitDB, LooseObjectDB
diff --git a/git/diff.py b/git/diff.py
index 1381deca0..0e39fe7a8 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff")
+__all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"]
 
 import enum
 import re
diff --git a/git/index/base.py b/git/index/base.py
index b49841435..b8161ea52 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -6,7 +6,7 @@
 """Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
 of index manipulations such as querying and merging."""
 
-__all__ = ("IndexFile", "CheckoutError", "StageType")
+__all__ = ["IndexFile", "CheckoutError", "StageType"]
 
 import contextlib
 import datetime
diff --git a/git/index/fun.py b/git/index/fun.py
index b24e803a3..59cce6ae6 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -4,7 +4,7 @@
 """Standalone functions to accompany the index implementation and make it more
 versatile."""
 
-__all__ = (
+__all__ = [
     "write_cache",
     "read_cache",
     "write_tree_from_cache",
@@ -13,7 +13,7 @@
     "S_IFGITLINK",
     "run_commit_hook",
     "hook_path",
-)
+]
 
 from io import BytesIO
 import os
diff --git a/git/index/typ.py b/git/index/typ.py
index 0fbcd69f0..974252528 100644
--- a/git/index/typ.py
+++ b/git/index/typ.py
@@ -3,7 +3,7 @@
 
 """Additional types used by the index."""
 
-__all__ = ("BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType")
+__all__ = ["BlobFilter", "BaseIndexEntry", "IndexEntry", "StageType"]
 
 from binascii import b2a_hex
 from pathlib import Path
diff --git a/git/index/util.py b/git/index/util.py
index 4aee61bce..e59cb609f 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -3,7 +3,7 @@
 
 """Index utilities."""
 
-__all__ = ("TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir")
+__all__ = ["TemporaryFileSwap", "post_clear_cache", "default_index", "git_working_dir"]
 
 import contextlib
 from functools import wraps
diff --git a/git/objects/base.py b/git/objects/base.py
index 07ff639e5..eeaebc09b 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = ("Object", "IndexObject")
+__all__ = ["Object", "IndexObject"]
 
 import os.path as osp
 
diff --git a/git/objects/blob.py b/git/objects/blob.py
index 50500f550..58de59642 100644
--- a/git/objects/blob.py
+++ b/git/objects/blob.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = ("Blob",)
+__all__ = ["Blob"]
 
 from mimetypes import guess_type
 import sys
diff --git a/git/objects/commit.py b/git/objects/commit.py
index b22308726..8de52980c 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = ("Commit",)
+__all__ = ["Commit"]
 
 from collections import defaultdict
 import datetime
diff --git a/git/objects/fun.py b/git/objects/fun.py
index 9bd2d8f10..fe57da13a 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -3,12 +3,12 @@
 
 """Functions that are supposed to be as fast as possible."""
 
-__all__ = (
+__all__ = [
     "tree_to_stream",
     "tree_entries_from_data",
     "traverse_trees_recursive",
     "traverse_tree_recursive",
-)
+]
 
 from stat import S_ISDIR
 
diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py
index 3a09d6a17..c021510d8 100644
--- a/git/objects/submodule/util.py
+++ b/git/objects/submodule/util.py
@@ -1,13 +1,13 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = (
+__all__ = [
     "sm_section",
     "sm_name",
     "mkhead",
     "find_first_remote_branch",
     "SubmoduleConfigParser",
-)
+]
 
 from io import BytesIO
 import weakref
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 5ad311590..a3bb0b882 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -9,7 +9,7 @@
 For lightweight tags, see the :mod:`git.refs.tag` module.
 """
 
-__all__ = ("TagObject",)
+__all__ = ["TagObject"]
 
 import sys
 
diff --git a/git/objects/tree.py b/git/objects/tree.py
index d8320b319..09184a781 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -3,7 +3,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-__all__ = ("TreeModifier", "Tree")
+__all__ = ["TreeModifier", "Tree"]
 
 import sys
 
diff --git a/git/objects/util.py b/git/objects/util.py
index 2cba89a9f..5c56e6134 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -5,7 +5,7 @@
 
 """Utility functions for working with git objects."""
 
-__all__ = (
+__all__ = [
     "get_object_type_by_name",
     "parse_date",
     "parse_actor_and_date",
@@ -17,7 +17,7 @@
     "Actor",
     "tzoffset",
     "utc",
-)
+]
 
 from abc import ABC, abstractmethod
 import calendar
diff --git a/git/remote.py b/git/remote.py
index 54a5c459c..f2ecd0f36 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -5,7 +5,7 @@
 
 """Module implementing a remote object allowing easy access to git remotes."""
 
-__all__ = ("RemoteProgress", "PushInfo", "FetchInfo", "Remote")
+__all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"]
 
 import contextlib
 import logging
diff --git a/git/repo/base.py b/git/repo/base.py
index 63b21fdbf..51ea76901 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -5,7 +5,7 @@
 
 from __future__ import annotations
 
-__all__ = ("Repo",)
+__all__ = ["Repo"]
 
 import gc
 import logging
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 1defcb1ac..e44d9c644 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -5,7 +5,7 @@
 
 from __future__ import annotations
 
-__all__ = (
+__all__ = [
     "rev_parse",
     "is_git_dir",
     "touch",
@@ -15,7 +15,7 @@
     "deref_tag",
     "to_commit",
     "find_worktree_git_dir",
-)
+]
 
 import os
 import os.path as osp

From ecdb6aa25a95e2cb3a650a26f3ffd9c45e127ed1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 00:05:15 -0400
Subject: [PATCH 693/994] Save diff of non-__all__ attributes across import
 changes

This commits the diff of the output of the modattrs.py script
between when the script was introduced in 1e5a944, and now (with
the import refactoring finished and no wildcard imports remaining
outside the test suite, and only there in one import statement for
test helpers).

Neither this diff nor modattrs.py itself will be retained. Both
will be removed in the next commit. This is committed here to
facilitate inspection and debugging (especially if turns out there
are thus-far undetected regressions).
---
 ab.diff | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 ab.diff

diff --git a/ab.diff b/ab.diff
new file mode 100644
index 000000000..1f2885b90
--- /dev/null
+++ b/ab.diff
@@ -0,0 +1,42 @@
+diff --git a/a b/b
+index 81b3f984..099fa33b 100644
+--- a/a
++++ b/b
+@@ -634,7 +634,6 @@ git.objects:
+     blob:  <module 'git.objects.blob' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\blob.py'>
+     commit:  <module 'git.objects.commit' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\commit.py'>
+     fun:  <module 'git.objects.fun' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\fun.py'>
+-    inspect:  <module 'inspect' from 'C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.12_3.12.752.0_x64__qbz5n2kfra8p0\\Lib\\inspect.py'>
+     submodule:  <module 'git.objects.submodule' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\submodule\\__init__.py'>
+     tag:  <module 'git.objects.tag' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\tag.py'>
+     tree:  <module 'git.objects.tree' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\tree.py'>
+@@ -770,6 +769,10 @@ git.objects.fun:
+ 
+ 
+ git.objects.submodule:
++    RootModule:  <class 'git.objects.submodule.root.RootModule'>
++    RootUpdateProgress:  <class 'git.objects.submodule.root.RootUpdateProgress'>
++    Submodule:  <class 'git.objects.submodule.base.Submodule'>
++    UpdateProgress:  <class 'git.objects.submodule.base.UpdateProgress'>
+     __builtins__:  {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThis module is not normally accessed explicitly by most\napplications, but can be useful in modules that provide\nobjects with the same name as a built-in value, but in\nwhich the built-in of that name is also needed.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'BaseExceptionGroup': <class 'BaseExceptionGroup'>, 'Exception': <class 'Exception'>, 'GeneratorExit': <class 'GeneratorExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'SystemExit': <class 'SystemExit'>, 'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BufferError': <class 'BufferError'>, 'EOFError': <class 'EOFError'>, 'ImportError': <class 'ImportError'>, 'LookupError': <class 'LookupError'>, 'MemoryError': <class 'MemoryError'>, 'NameError': <class 'NameError'>, 'OSError': <class 'OSError'>, 'ReferenceError': <class 'ReferenceError'>, 'RuntimeError': <class 'RuntimeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'SyntaxError': <class 'SyntaxError'>, 'SystemError': <class 'SystemError'>, 'TypeError': <class 'TypeError'>, 'ValueError': <class 'ValueError'>, 'Warning': <class 'Warning'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'BytesWarning': <class 'BytesWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'UserWarning': <class 'UserWarning'>, 'BlockingIOError': <class 'BlockingIOError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionError': <class 'ConnectionError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'InterruptedError': <class 'InterruptedError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'IndentationError': <class 'IndentationError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'RecursionError': <class 'RecursionError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'UnicodeError': <class 'UnicodeError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'TabError': <class 'TabError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'ExceptionGroup': <class 'ExceptionGroup'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'WindowsError': <class 'OSError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-Z plus Return to exit, 'exit': Use exit() or Ctrl-Z plus Return to exit, 'copyright': Copyright (c) 2001-2023 Python Software Foundation.?All Rights Reserved.??Copyright (c) 2000 BeOpen.com.?All Rights Reserved.??Copyright (c) 1995-2001 Corporation for National Research Initiatives.?All Rights Reserved.??Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.?All Rights Reserved., 'credits':     Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands?    for supporting Python development.  See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}
+     __cached__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\submodule\\__pycache__\\__init__.cpython-312.pyc'
+     __doc__:  None
+@@ -881,9 +884,7 @@ git.objects.submodule.util:
+     Any:  typing.Any
+     BytesIO:  <class '_io.BytesIO'>
+     GitConfigParser:  <class 'git.config.GitConfigParser'>
+-    IndexObject:  <class 'git.objects.base.IndexObject'>
+     InvalidGitRepositoryError:  <class 'git.exc.InvalidGitRepositoryError'>
+-    Object:  <class 'git.objects.base.Object'>
+     PathLike:  typing.Union[str, ForwardRef('os.PathLike[str]')]
+     Sequence:  typing.Sequence
+     SubmoduleConfigParser:  <class 'git.objects.submodule.util.SubmoduleConfigParser'>
+@@ -998,7 +999,7 @@ git.objects.util:
+     ZERO:  datetime.timedelta(0)
+     __builtins__:  {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThis module is not normally accessed explicitly by most\napplications, but can be useful in modules that provide\nobjects with the same name as a built-in value, but in\nwhich the built-in of that name is also needed.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'BaseExceptionGroup': <class 'BaseExceptionGroup'>, 'Exception': <class 'Exception'>, 'GeneratorExit': <class 'GeneratorExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'SystemExit': <class 'SystemExit'>, 'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BufferError': <class 'BufferError'>, 'EOFError': <class 'EOFError'>, 'ImportError': <class 'ImportError'>, 'LookupError': <class 'LookupError'>, 'MemoryError': <class 'MemoryError'>, 'NameError': <class 'NameError'>, 'OSError': <class 'OSError'>, 'ReferenceError': <class 'ReferenceError'>, 'RuntimeError': <class 'RuntimeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'SyntaxError': <class 'SyntaxError'>, 'SystemError': <class 'SystemError'>, 'TypeError': <class 'TypeError'>, 'ValueError': <class 'ValueError'>, 'Warning': <class 'Warning'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'BytesWarning': <class 'BytesWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'UserWarning': <class 'UserWarning'>, 'BlockingIOError': <class 'BlockingIOError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionError': <class 'ConnectionError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'InterruptedError': <class 'InterruptedError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'IndentationError': <class 'IndentationError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'RecursionError': <class 'RecursionError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'UnicodeError': <class 'UnicodeError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'TabError': <class 'TabError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'ExceptionGroup': <class 'ExceptionGroup'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'WindowsError': <class 'OSError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-Z plus Return to exit, 'exit': Use exit() or Ctrl-Z plus Return to exit, 'copyright': Copyright (c) 2001-2023 Python Software Foundation.?All Rights Reserved.??Copyright (c) 2000 BeOpen.com.?All Rights Reserved.??Copyright (c) 1995-2001 Corporation for National Research Initiatives.?All Rights Reserved.??Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.?All Rights Reserved., 'credits':     Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands?    for supporting Python development.  See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}
+     __cached__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\__pycache__\\util.cpython-312.pyc'
+-    __doc__:  'General utility functions.'
++    __doc__:  'Utility functions for working with git objects.'
+     __file__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\util.py'
+     __loader__:  <_frozen_importlib_external.SourceFileLoader object at 0x...>
+     __name__:  'git.objects.util'

From f705fd6dde6047c7787809aeb691988fa4af80d8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 00:15:50 -0400
Subject: [PATCH 694/994] Remove modattrs.py and related

Now that it has served its purpose. (Of course, it can also be
brought back from the history at any time if needed.)

Changes:

- Delete the modattrs.py script that was used to check for
  unintended changes to what module attributes existed and what
  objects they referred to, while doing the import refactoring.

- Delete the ab.diff file showing the overall diff, which was
  temporarily introduced in the previous commit.

- Remove the "a" and "b" temporary entries in .gitignore that were
  used to facilitate efficient use of modattrs.py while carrying
  out the import refactoring.
---
 .gitignore  |  4 ----
 ab.diff     | 42 ------------------------------------------
 modattrs.py | 53 -----------------------------------------------------
 3 files changed, 99 deletions(-)
 delete mode 100644 ab.diff
 delete mode 100755 modattrs.py

diff --git a/.gitignore b/.gitignore
index 1be4f3201..7765293d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,7 +47,3 @@ output.txt
 
 # Finder metadata
 .DS_Store
-
-# Output files for modattrs.py (these entries will be removed soon)
-a
-b
diff --git a/ab.diff b/ab.diff
deleted file mode 100644
index 1f2885b90..000000000
--- a/ab.diff
+++ /dev/null
@@ -1,42 +0,0 @@
-diff --git a/a b/b
-index 81b3f984..099fa33b 100644
---- a/a
-+++ b/b
-@@ -634,7 +634,6 @@ git.objects:
-     blob:  <module 'git.objects.blob' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\blob.py'>
-     commit:  <module 'git.objects.commit' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\commit.py'>
-     fun:  <module 'git.objects.fun' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\fun.py'>
--    inspect:  <module 'inspect' from 'C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.12_3.12.752.0_x64__qbz5n2kfra8p0\\Lib\\inspect.py'>
-     submodule:  <module 'git.objects.submodule' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\submodule\\__init__.py'>
-     tag:  <module 'git.objects.tag' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\tag.py'>
-     tree:  <module 'git.objects.tree' from 'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\tree.py'>
-@@ -770,6 +769,10 @@ git.objects.fun:
- 
- 
- git.objects.submodule:
-+    RootModule:  <class 'git.objects.submodule.root.RootModule'>
-+    RootUpdateProgress:  <class 'git.objects.submodule.root.RootUpdateProgress'>
-+    Submodule:  <class 'git.objects.submodule.base.Submodule'>
-+    UpdateProgress:  <class 'git.objects.submodule.base.UpdateProgress'>
-     __builtins__:  {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThis module is not normally accessed explicitly by most\napplications, but can be useful in modules that provide\nobjects with the same name as a built-in value, but in\nwhich the built-in of that name is also needed.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'BaseExceptionGroup': <class 'BaseExceptionGroup'>, 'Exception': <class 'Exception'>, 'GeneratorExit': <class 'GeneratorExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'SystemExit': <class 'SystemExit'>, 'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BufferError': <class 'BufferError'>, 'EOFError': <class 'EOFError'>, 'ImportError': <class 'ImportError'>, 'LookupError': <class 'LookupError'>, 'MemoryError': <class 'MemoryError'>, 'NameError': <class 'NameError'>, 'OSError': <class 'OSError'>, 'ReferenceError': <class 'ReferenceError'>, 'RuntimeError': <class 'RuntimeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'SyntaxError': <class 'SyntaxError'>, 'SystemError': <class 'SystemError'>, 'TypeError': <class 'TypeError'>, 'ValueError': <class 'ValueError'>, 'Warning': <class 'Warning'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'BytesWarning': <class 'BytesWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'UserWarning': <class 'UserWarning'>, 'BlockingIOError': <class 'BlockingIOError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionError': <class 'ConnectionError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'InterruptedError': <class 'InterruptedError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'IndentationError': <class 'IndentationError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'RecursionError': <class 'RecursionError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'UnicodeError': <class 'UnicodeError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'TabError': <class 'TabError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'ExceptionGroup': <class 'ExceptionGroup'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'WindowsError': <class 'OSError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-Z plus Return to exit, 'exit': Use exit() or Ctrl-Z plus Return to exit, 'copyright': Copyright (c) 2001-2023 Python Software Foundation.?All Rights Reserved.??Copyright (c) 2000 BeOpen.com.?All Rights Reserved.??Copyright (c) 1995-2001 Corporation for National Research Initiatives.?All Rights Reserved.??Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.?All Rights Reserved., 'credits':     Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands?    for supporting Python development.  See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}
-     __cached__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\submodule\\__pycache__\\__init__.cpython-312.pyc'
-     __doc__:  None
-@@ -881,9 +884,7 @@ git.objects.submodule.util:
-     Any:  typing.Any
-     BytesIO:  <class '_io.BytesIO'>
-     GitConfigParser:  <class 'git.config.GitConfigParser'>
--    IndexObject:  <class 'git.objects.base.IndexObject'>
-     InvalidGitRepositoryError:  <class 'git.exc.InvalidGitRepositoryError'>
--    Object:  <class 'git.objects.base.Object'>
-     PathLike:  typing.Union[str, ForwardRef('os.PathLike[str]')]
-     Sequence:  typing.Sequence
-     SubmoduleConfigParser:  <class 'git.objects.submodule.util.SubmoduleConfigParser'>
-@@ -998,7 +999,7 @@ git.objects.util:
-     ZERO:  datetime.timedelta(0)
-     __builtins__:  {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThis module is not normally accessed explicitly by most\napplications, but can be useful in modules that provide\nobjects with the same name as a built-in value, but in\nwhich the built-in of that name is also needed.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'BaseExceptionGroup': <class 'BaseExceptionGroup'>, 'Exception': <class 'Exception'>, 'GeneratorExit': <class 'GeneratorExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'SystemExit': <class 'SystemExit'>, 'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BufferError': <class 'BufferError'>, 'EOFError': <class 'EOFError'>, 'ImportError': <class 'ImportError'>, 'LookupError': <class 'LookupError'>, 'MemoryError': <class 'MemoryError'>, 'NameError': <class 'NameError'>, 'OSError': <class 'OSError'>, 'ReferenceError': <class 'ReferenceError'>, 'RuntimeError': <class 'RuntimeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'SyntaxError': <class 'SyntaxError'>, 'SystemError': <class 'SystemError'>, 'TypeError': <class 'TypeError'>, 'ValueError': <class 'ValueError'>, 'Warning': <class 'Warning'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'BytesWarning': <class 'BytesWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'UserWarning': <class 'UserWarning'>, 'BlockingIOError': <class 'BlockingIOError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionError': <class 'ConnectionError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'InterruptedError': <class 'InterruptedError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'IndentationError': <class 'IndentationError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'RecursionError': <class 'RecursionError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'UnicodeError': <class 'UnicodeError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'TabError': <class 'TabError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'ExceptionGroup': <class 'ExceptionGroup'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'WindowsError': <class 'OSError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-Z plus Return to exit, 'exit': Use exit() or Ctrl-Z plus Return to exit, 'copyright': Copyright (c) 2001-2023 Python Software Foundation.?All Rights Reserved.??Copyright (c) 2000 BeOpen.com.?All Rights Reserved.??Copyright (c) 1995-2001 Corporation for National Research Initiatives.?All Rights Reserved.??Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.?All Rights Reserved., 'credits':     Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands?    for supporting Python development.  See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}
-     __cached__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\__pycache__\\util.cpython-312.pyc'
--    __doc__:  'General utility functions.'
-+    __doc__:  'Utility functions for working with git objects.'
-     __file__:  'C:\\Users\\ek\\source\\repos\\GitPython\\git\\objects\\util.py'
-     __loader__:  <_frozen_importlib_external.SourceFileLoader object at 0x...>
-     __name__:  'git.objects.util'
diff --git a/modattrs.py b/modattrs.py
deleted file mode 100755
index 245f68912..000000000
--- a/modattrs.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env python
-
-"""Script to get the names and "stabilized" reprs of module attributes in GitPython.
-
-Run with :envvar:`PYTHONHASHSEED` set to ``0`` for fully comparable results. These are
-only still meaningful for comparing if the same platform and Python version are used.
-
-The output of this script should probably not be committed, because within the reprs of
-objects found in modules, it may contain sensitive information, such as API keys stored
-in environment variables. The "sanitization" performed here is only for common forms of
-whitespace that clash with the output format.
-"""
-
-# fmt: off
-
-__all__ = ["git", "main"]
-
-import itertools
-import re
-import sys
-
-import git
-
-
-def main():
-    # This assumes `import git` causes all of them to be loaded.
-    gitpython_modules = sorted(
-        (module_name, module)
-        for module_name, module in sys.modules.items()
-        if re.match(r"git(?:\.|$)", module_name)
-    )
-
-    # We will print two blank lines between successive module reports.
-    separators = itertools.chain(("",), itertools.repeat("\n\n"))
-
-    # Report each module's contents.
-    for (module_name, module), separator in zip(gitpython_modules, separators):
-        print(f"{separator}{module_name}:")
-
-        attributes = sorted(
-            (name, value)
-            for name, value in module.__dict__.items()
-            if name != "__all__"  # Because we are deliberately adding these.
-        )
-
-        for name, value in attributes:
-            sanitized_repr = re.sub(r"[\r\n\v\f]", "?", repr(value))
-            normalized_repr = re.sub(r" at 0x[0-9a-fA-F]+", " at 0x...", sanitized_repr)
-            print(f"    {name}:  {normalized_repr}")
-
-
-if __name__ == "__main__":
-    main()

From 4a4d880fec4364c7d49e7708238a962942ae69f3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 00:45:40 -0400
Subject: [PATCH 695/994] Improve test suite import grouping/sorting, __all__
 placement

There is only one __all__ in the test suite, so this is mostly the
change to imports, grouping and sorting them in a fully consistent
style that is the same as the import style in the code under test.
---
 test/lib/helper.py               | 32 ++++++++++++++++----------------
 test/performance/__init__.py     |  2 ++
 test/performance/lib.py          |  5 +++--
 test/performance/test_commit.py  |  6 ++++--
 test/performance/test_odb.py     |  2 +-
 test/performance/test_streams.py |  8 ++++----
 test/test_actor.py               |  3 ++-
 test/test_base.py                | 10 +++++-----
 test/test_blob.py                |  3 ++-
 test/test_clone.py               |  5 +----
 test/test_commit.py              | 20 +++++++++++---------
 test/test_config.py              |  2 +-
 test/test_db.py                  |  5 +++--
 test/test_diff.py                |  1 +
 test/test_docs.py                |  3 +--
 test/test_exc.py                 |  7 ++++---
 test/test_fun.py                 | 17 ++++++++---------
 test/test_git.py                 |  3 ++-
 test/test_index.py               | 14 ++++----------
 test/test_reflog.py              |  5 +++--
 test/test_refs.py                | 23 ++++++++++++-----------
 test/test_remote.py              |  3 +--
 test/test_repo.py                |  7 ++-----
 test/test_stats.py               |  3 ++-
 test/test_submodule.py           |  1 +
 test/test_tree.py                |  3 ++-
 test/test_util.py                |  1 +
 27 files changed, 99 insertions(+), 95 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 45a778b7d..5d91447ea 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -3,6 +3,22 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+__all__ = [
+    "fixture_path",
+    "fixture",
+    "StringProcessAdapter",
+    "with_rw_directory",
+    "with_rw_repo",
+    "with_rw_and_rw_remote_repo",
+    "TestBase",
+    "VirtualEnvironment",
+    "TestCase",
+    "SkipTest",
+    "skipIf",
+    "GIT_REPO",
+    "GIT_DAEMON_PORT",
+]
+
 import contextlib
 from functools import wraps
 import gc
@@ -31,22 +47,6 @@
 GIT_REPO = os.environ.get("GIT_PYTHON_TEST_GIT_REPO_BASE", ospd(ospd(ospd(__file__))))
 GIT_DAEMON_PORT = os.environ.get("GIT_PYTHON_TEST_GIT_DAEMON_PORT", "19418")
 
-__all__ = (
-    "fixture_path",
-    "fixture",
-    "StringProcessAdapter",
-    "with_rw_directory",
-    "with_rw_repo",
-    "with_rw_and_rw_remote_repo",
-    "TestBase",
-    "VirtualEnvironment",
-    "TestCase",
-    "SkipTest",
-    "skipIf",
-    "GIT_REPO",
-    "GIT_DAEMON_PORT",
-)
-
 _logger = logging.getLogger(__name__)
 
 # { Routines
diff --git a/test/performance/__init__.py b/test/performance/__init__.py
index e69de29bb..56b5d89db 100644
--- a/test/performance/__init__.py
+++ b/test/performance/__init__.py
@@ -0,0 +1,2 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
diff --git a/test/performance/lib.py b/test/performance/lib.py
index d08e1027f..2ca3c409b 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -5,13 +5,14 @@
 
 import logging
 import os
+import os.path as osp
 import tempfile
 
 from git import Repo
 from git.db import GitCmdObjectDB, GitDB
-from test.lib import TestBase
 from git.util import rmtree
-import os.path as osp
+
+from test.lib import TestBase
 
 # { Invariants
 
diff --git a/test/performance/test_commit.py b/test/performance/test_commit.py
index 00d768f0a..b943f1975 100644
--- a/test/performance/test_commit.py
+++ b/test/performance/test_commit.py
@@ -10,9 +10,11 @@
 from time import time
 import sys
 
-from .lib import TestBigRepoRW
-from git import Commit
 from gitdb import IStream
+
+from git import Commit
+
+from test.performance.lib import TestBigRepoRW
 from test.test_commit import TestCommitSerialization
 
 
diff --git a/test/performance/test_odb.py b/test/performance/test_odb.py
index 00e245fb7..fdbbeb8c3 100644
--- a/test/performance/test_odb.py
+++ b/test/performance/test_odb.py
@@ -6,7 +6,7 @@
 import sys
 from time import time
 
-from .lib import TestBigRepoR
+from test.performance.lib import TestBigRepoR
 
 
 class TestObjDBPerformance(TestBigRepoR):
diff --git a/test/performance/test_streams.py b/test/performance/test_streams.py
index 56b5274ec..f6ffeba8e 100644
--- a/test/performance/test_streams.py
+++ b/test/performance/test_streams.py
@@ -5,18 +5,18 @@
 
 import gc
 import os
+import os.path as osp
 import subprocess
 import sys
 from time import time
 
-from test.lib import with_rw_repo
-from git.util import bin_to_hex
 from gitdb import LooseObjectDB, IStream
 from gitdb.test.lib import make_memory_file
 
-import os.path as osp
+from git.util import bin_to_hex
 
-from .lib import TestBigRepoR
+from test.lib import with_rw_repo
+from test.performance.lib import TestBigRepoR
 
 
 class TestObjDBPerformance(TestBigRepoR):
diff --git a/test/test_actor.py b/test/test_actor.py
index caf095739..5e6635709 100644
--- a/test/test_actor.py
+++ b/test/test_actor.py
@@ -3,9 +3,10 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from test.lib import TestBase
 from git import Actor
 
+from test.lib import TestBase
+
 
 class TestActor(TestBase):
     def test_from_string_should_separate_name_and_email(self):
diff --git a/test/test_base.py b/test/test_base.py
index e477b4837..86bcc5c79 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -5,18 +5,18 @@
 
 import gc
 import os
+import os.path as osp
 import sys
 import tempfile
 from unittest import skipIf
 
 from git import Repo
-from git.objects import Blob, Tree, Commit, TagObject
+from git.objects import Blob, Commit, TagObject, Tree
+import git.objects.base as base
 from git.objects.util import get_object_type_by_name
-from test.lib import TestBase as _TestBase, with_rw_repo, with_rw_and_rw_remote_repo
-from git.util import hex_to_bin, HIDE_WINDOWS_FREEZE_ERRORS
+from git.util import HIDE_WINDOWS_FREEZE_ERRORS, hex_to_bin
 
-import git.objects.base as base
-import os.path as osp
+from test.lib import TestBase as _TestBase, with_rw_and_rw_remote_repo, with_rw_repo
 
 
 class TestBase(_TestBase):
diff --git a/test/test_blob.py b/test/test_blob.py
index ff59c67ea..affaa60fc 100644
--- a/test/test_blob.py
+++ b/test/test_blob.py
@@ -3,9 +3,10 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from test.lib import TestBase
 from git import Blob
 
+from test.lib import TestBase
+
 
 class TestBlob(TestBase):
     def test_mime_type_should_return_mime_type_for_known_types(self):
diff --git a/test/test_clone.py b/test/test_clone.py
index be2e6b19b..126ef0063 100644
--- a/test/test_clone.py
+++ b/test/test_clone.py
@@ -6,10 +6,7 @@
 
 import git
 
-from .lib import (
-    TestBase,
-    with_rw_directory,
-)
+from test.lib import TestBase, with_rw_directory
 
 
 class TestClone(TestBase):
diff --git a/test/test_commit.py b/test/test_commit.py
index 5571b9ecb..5832258de 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -6,23 +6,25 @@
 import copy
 from datetime import datetime
 from io import BytesIO
+import os.path as osp
 import re
 import sys
 import time
 from unittest.mock import Mock
 
-from git import (
-    Commit,
-    Actor,
-)
-from git import Repo
+from gitdb import IStream
+
+from git import Actor, Commit, Repo
 from git.objects.util import tzoffset, utc
 from git.repo.fun import touch
-from test.lib import TestBase, with_rw_repo, fixture_path, StringProcessAdapter
-from test.lib import with_rw_directory
-from gitdb import IStream
 
-import os.path as osp
+from test.lib import (
+    StringProcessAdapter,
+    TestBase,
+    fixture_path,
+    with_rw_directory,
+    with_rw_repo,
+)
 
 
 class TestCommitSerialization(TestBase):
diff --git a/test/test_config.py b/test/test_config.py
index ac19a7fa8..0911d0262 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -15,8 +15,8 @@
 from git import GitConfigParser
 from git.config import _OMD, cp
 from git.util import rmfile
-from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory
 
+from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory
 
 _tc_lock_fpaths = osp.join(osp.dirname(__file__), "fixtures/*.lock")
 
diff --git a/test/test_db.py b/test/test_db.py
index de093cbd8..72d63b44b 100644
--- a/test/test_db.py
+++ b/test/test_db.py
@@ -3,12 +3,13 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+import os.path as osp
+
 from git.db import GitCmdObjectDB
 from git.exc import BadObject
-from test.lib import TestBase
 from git.util import bin_to_hex
 
-import os.path as osp
+from test.lib import TestBase
 
 
 class TestDB(TestBase):
diff --git a/test/test_diff.py b/test/test_diff.py
index 96fbc60e3..928a9f428 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -14,6 +14,7 @@
 
 from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
 from git.cmd import Git
+
 from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
 
 
diff --git a/test/test_docs.py b/test/test_docs.py
index 409f66bb3..b3547c1de 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -5,6 +5,7 @@
 
 import gc
 import os
+import os.path
 import sys
 
 import pytest
@@ -12,8 +13,6 @@
 from test.lib import TestBase
 from test.lib.helper import with_rw_directory
 
-import os.path
-
 
 class Tutorials(TestBase):
     def tearDown(self):
diff --git a/test/test_exc.py b/test/test_exc.py
index 3f4d0b803..c1eae7240 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -3,9 +3,11 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
+from itertools import product
 import re
 
 import ddt
+
 from git.exc import (
     InvalidGitRepositoryError,
     WorkTreeRepositoryUnsupported,
@@ -20,9 +22,8 @@
     RepositoryDirtyError,
 )
 from git.util import remove_password_if_present
-from test.lib import TestBase
 
-import itertools as itt
+from test.lib import TestBase
 
 
 _cmd_argvs = (
@@ -79,7 +80,7 @@ def test_ExceptionsHaveBaseClass(self):
         for ex_class in exception_classes:
             self.assertTrue(issubclass(ex_class, GitError))
 
-    @ddt.data(*list(itt.product(_cmd_argvs, _causes_n_substrings, _streams_n_substrings)))
+    @ddt.data(*list(product(_cmd_argvs, _causes_n_substrings, _streams_n_substrings)))
     def test_CommandError_unicode(self, case):
         argv, (cause, subs), stream = case
         cls = CommandError
diff --git a/test/test_fun.py b/test/test_fun.py
index 2d30d355a..b8593b400 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -2,27 +2,26 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from io import BytesIO
-from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR
+from stat import S_IFDIR, S_IFLNK, S_IFREG, S_IXUSR
 from os import stat
 import os.path as osp
 
+from gitdb.base import IStream
+from gitdb.typ import str_tree_type
+
 from git import Git
 from git.index import IndexFile
-from git.index.fun import (
-    aggressive_tree_merge,
-    stat_mode_to_index_mode,
-)
+from git.index.fun import aggressive_tree_merge, stat_mode_to_index_mode
 from git.objects.fun import (
     traverse_tree_recursive,
     traverse_trees_recursive,
-    tree_to_stream,
     tree_entries_from_data,
+    tree_to_stream,
 )
 from git.repo.fun import find_worktree_git_dir
-from test.lib import TestBase, with_rw_repo, with_rw_directory
 from git.util import bin_to_hex, cygpath, join_path_native
-from gitdb.base import IStream
-from gitdb.typ import str_tree_type
+
+from test.lib import TestBase, with_rw_directory, with_rw_repo
 
 
 class TestFun(TestBase):
diff --git a/test/test_git.py b/test/test_git.py
index dae0f6a39..112e2f0eb 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -25,8 +25,9 @@
 
 import ddt
 
-from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
+from git import Git, GitCommandError, GitCommandNotFound, Repo, cmd, refresh
 from git.util import cwd, finalize_process
+
 from test.lib import TestBase, fixture_path, with_rw_directory
 
 
diff --git a/test/test_index.py b/test/test_index.py
index 622e7ca9a..b92258c92 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -17,18 +17,12 @@
 import sys
 import tempfile
 
+from gitdb.base import IStream
+
 import ddt
 import pytest
 
-from git import (
-    BlobFilter,
-    Diff,
-    Git,
-    IndexFile,
-    Object,
-    Repo,
-    Tree,
-)
+from git import BlobFilter, Diff, Git, IndexFile, Object, Repo, Tree
 from git.exc import (
     CheckoutError,
     GitCommandError,
@@ -41,7 +35,7 @@
 from git.index.util import TemporaryFileSwap
 from git.objects import Blob
 from git.util import Actor, cwd, hex_to_bin, rmtree
-from gitdb.base import IStream
+
 from test.lib import (
     TestBase,
     VirtualEnvironment,
diff --git a/test/test_reflog.py b/test/test_reflog.py
index 1bd2e5dab..7ce64219a 100644
--- a/test/test_reflog.py
+++ b/test/test_reflog.py
@@ -5,9 +5,10 @@
 import tempfile
 
 from git.objects import IndexObject
-from git.refs import RefLogEntry, RefLog
+from git.refs import RefLog, RefLogEntry
+from git.util import Actor, hex_to_bin, rmtree
+
 from test.lib import TestBase, fixture_path
-from git.util import Actor, rmtree, hex_to_bin
 
 
 class TestRefLog(TestBase):
diff --git a/test/test_refs.py b/test/test_refs.py
index 28db70c6e..08096e69e 100644
--- a/test/test_refs.py
+++ b/test/test_refs.py
@@ -4,27 +4,28 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 from itertools import chain
+import os.path as osp
 from pathlib import Path
+import tempfile
+
+from gitdb.exc import BadName
 
 from git import (
-    Reference,
-    Head,
-    TagReference,
-    RemoteReference,
     Commit,
-    SymbolicReference,
     GitCommandError,
-    RefLog,
     GitConfigParser,
+    Head,
+    RefLog,
+    Reference,
+    RemoteReference,
+    SymbolicReference,
+    TagReference,
 )
 from git.objects.tag import TagObject
-from test.lib import TestBase, with_rw_repo
+import git.refs as refs
 from git.util import Actor
-from gitdb.exc import BadName
 
-import git.refs as refs
-import os.path as osp
-import tempfile
+from test.lib import TestBase, with_rw_repo
 
 
 class TestRefs(TestBase):
diff --git a/test/test_remote.py b/test/test_remote.py
index f84452deb..5ddb41bc0 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -28,7 +28,7 @@
 )
 from git.cmd import Git
 from git.exc import UnsafeOptionError, UnsafeProtocolError
-from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList
+from git.util import HIDE_WINDOWS_FREEZE_ERRORS, IterableList, rmtree
 from test.lib import (
     GIT_DAEMON_PORT,
     TestBase,
@@ -37,7 +37,6 @@
     with_rw_repo,
 )
 
-
 # Make sure we have repeatable results.
 random.seed(0)
 
diff --git a/test/test_repo.py b/test/test_repo.py
index 238f94712..e38da5bb6 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -36,13 +36,10 @@
     Submodule,
     Tree,
 )
-from git.exc import (
-    BadObject,
-    UnsafeOptionError,
-    UnsafeProtocolError,
-)
+from git.exc import BadObject, UnsafeOptionError, UnsafeProtocolError
 from git.repo.fun import touch
 from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree
+
 from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo
 
 
diff --git a/test/test_stats.py b/test/test_stats.py
index 4efb6f313..eec73c802 100644
--- a/test/test_stats.py
+++ b/test/test_stats.py
@@ -3,10 +3,11 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-from test.lib import TestBase, fixture
 from git import Stats
 from git.compat import defenc
 
+from test.lib import TestBase, fixture
+
 
 class TestStats(TestBase):
     def test_list_from_string(self):
diff --git a/test/test_submodule.py b/test/test_submodule.py
index ee7795dbb..d88f9dab0 100644
--- a/test/test_submodule.py
+++ b/test/test_submodule.py
@@ -27,6 +27,7 @@
 from git.objects.submodule.root import RootModule, RootUpdateProgress
 from git.repo.fun import find_submodule_git_dir, touch
 from git.util import HIDE_WINDOWS_KNOWN_ERRORS, join_path_native, to_native_path_linux
+
 from test.lib import TestBase, with_rw_directory, with_rw_repo
 
 
diff --git a/test/test_tree.py b/test/test_tree.py
index 0c06b950c..73158113d 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -8,8 +8,9 @@
 from pathlib import Path
 import subprocess
 
-from git.objects import Tree, Blob
+from git.objects import Blob, Tree
 from git.util import cwd
+
 from test.lib import TestBase, with_rw_directory
 
 
diff --git a/test/test_util.py b/test/test_util.py
index 369896581..dad2f3dcd 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -38,6 +38,7 @@
     remove_password_if_present,
     rmtree,
 )
+
 from test.lib import TestBase, with_rw_repo
 
 

From d524c76a460b339b224c59b6753607eb2e546b2a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 01:02:20 -0400
Subject: [PATCH 696/994] Fix slightly unsorted imports in setup.py

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 143206653..f28fedb85 100755
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,8 @@
 #!/usr/bin/env python
 
 import os
-import sys
 from pathlib import Path
+import sys
 from typing import Sequence
 
 from setuptools import setup, find_packages

From 838eb923751aad3063a47ef24c0b806e9c4e2453 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 13:22:46 -0400
Subject: [PATCH 697/994] Clarify how tag objects are usually tree-ish and
 commit-ish

This revises the docstrings of the Tree_ish and Commit_ish unions,
to make clearer that tags objects are usually used to identify
commits and are thus usually tree-ish and commit-ish.

This change relates to the discussion in #1878, starting at:
https://github.com/gitpython-developers/GitPython/pull/1878#pullrequestreview-1938343498
---
 git/types.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/git/types.py b/git/types.py
index 230422dff..a93ebdb4f 100644
--- a/git/types.py
+++ b/git/types.py
@@ -75,7 +75,7 @@
 """
 
 Tree_ish = Union["Commit", "Tree", "TagObject"]
-"""Union of :class:`~git.objects.base.Object`-based types that are sometimes tree-ish.
+"""Union of :class:`~git.objects.base.Object`-based types that are typically tree-ish.
 
 See :manpage:`gitglossary(7)` on "tree-ish":
 https://git-scm.com/docs/gitglossary#def_tree-ish
@@ -83,10 +83,11 @@
 :note:
     :class:`~git.objects.tree.Tree` and :class:`~git.objects.commit.Commit` are the
     classes whose instances are all tree-ish. This union includes them, but also
-    :class:`~git.objects.tag.TagObject`, only **some** of whose instances are tree-ish.
+    :class:`~git.objects.tag.TagObject`, only **most** of whose instances are tree-ish.
     Whether a particular :class:`~git.objects.tag.TagObject` peels (recursively
     dereferences) to a tree or commit, rather than a blob, can in general only be known
-    at runtime.
+    at runtime. In practice, git tag objects are nearly always used for tagging commits,
+    and such tags are tree-ish because commits are tree-ish.
 
 :note:
     See also the :class:`AnyGitObject` union of all four classes corresponding to git
@@ -94,7 +95,7 @@
 """
 
 Commit_ish = Union["Commit", "TagObject"]
-"""Union of :class:`~git.objects.base.Object`-based types that are sometimes commit-ish.
+"""Union of :class:`~git.objects.base.Object`-based types that are typically commit-ish.
 
 See :manpage:`gitglossary(7)` on "commit-ish":
 https://git-scm.com/docs/gitglossary#def_commit-ish
@@ -102,10 +103,11 @@
 :note:
     :class:`~git.objects.commit.Commit` is the only class whose instances are all
     commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also
-    :class:`~git.objects.tag.TagObject`, only **some** of whose instances are
+    :class:`~git.objects.tag.TagObject`, only **most** of whose instances are
     commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels
     (recursively dereferences) to a commit, rather than a tree or blob, can in general
-    only be known at runtime.
+    only be known at runtime. In practice, git tag objects are nearly always used for
+    tagging commits, and such tags are of course commit-ish.
 
 :note:
     See also the :class:`AnyGitObject` union of all four classes corresponding to git

From 2382891377f60c1467c1965b25e3ecf293f39b80 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 19:41:55 -0400
Subject: [PATCH 698/994] Test that deprecated Diff.renamed property warns

This starts on a test.deprecation subpackage for deprecation tests.
Having these tests in a separate directory inside the test suite
may or may not be how they will ultimately be orgnaized, but it has
two advantages:

- Once all the tests are written, it should be easy to see what in
  GitPython is deprecated.

- Some deprecation warnings -- those on module or class attribute
  access -- will require the introduction of new dynamic behavior,
  and thus run the risk of breaking static type checking. So that
  should be checked for, where applicable. But currently the test
  suite has no type annotations and is not checked by mypy. Having
  deprecation-related tests under the same path will make it easier
  to enable mypy for just this part of the test suite (for now).

It is also for this latter reason that the one test so far is
written without using the GitPython test suite's existing fixtures
whose uses are harder to annotate. This may be changed if
warranted, though some of the more complex deprecation-related
tests may benefit from being written as pure pytest tests.

Although a number of deprecated features in GitPython do already
issue warnings, Diff.renamed is one of the features that does not
yet do so. So the newly introduced test will fail until that is
fixed in the next commit.
---
 test/deprecation/__init__.py     |  2 ++
 test/deprecation/test_various.py | 20 ++++++++++++++++++++
 2 files changed, 22 insertions(+)
 create mode 100644 test/deprecation/__init__.py
 create mode 100644 test/deprecation/test_various.py

diff --git a/test/deprecation/__init__.py b/test/deprecation/__init__.py
new file mode 100644
index 000000000..56b5d89db
--- /dev/null
+++ b/test/deprecation/__init__.py
@@ -0,0 +1,2 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
new file mode 100644
index 000000000..9f948bc48
--- /dev/null
+++ b/test/deprecation/test_various.py
@@ -0,0 +1,20 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
+"""Tests of assorted deprecation warnings with no extra subtleties to check."""
+
+from git.diff import NULL_TREE
+from git.repo import Repo
+
+import pytest
+
+
+def test_diff_renamed_warns(tmp_path):
+    (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
+    repo = Repo.init(tmp_path)
+    repo.index.add(["a.txt"])
+    commit = repo.index.commit("Initial commit")
+    (diff,) = commit.diff(NULL_TREE)  # Exactly one file in the diff.
+
+    with pytest.deprecated_call():
+        diff.renamed

From e7dec7d0eecc362f02418820a146735a68430fbd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 19:52:17 -0400
Subject: [PATCH 699/994] Have the deprecated Diff.renamed property issue a
 warning

---
 git/diff.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/git/diff.py b/git/diff.py
index 0e39fe7a8..f89b12d98 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -7,6 +7,7 @@
 
 import enum
 import re
+import warnings
 
 from git.cmd import handle_process_output
 from git.compat import defenc
@@ -554,6 +555,11 @@ def renamed(self) -> bool:
             This property is deprecated.
             Please use the :attr:`renamed_file` property instead.
         """
+        warnings.warn(
+            "Diff.renamed is deprecated, use Diff.renamed_file instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         return self.renamed_file
 
     @property

From a8f109ca1704827b017349d6d97f8a0a3229d9a8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 20:04:25 -0400
Subject: [PATCH 700/994] Fix exception in Popen.__del__ in test on Windows

The newly introduced test would pass, but show:

    Exception ignored in: <function Popen.__del__ at 0x000002483DE14900>
    Traceback (most recent call last):
      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.752.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1130, in __del__
        self._internal_poll(_deadstate=_maxsize)
      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.752.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1575, in _internal_poll
        if _WaitForSingleObject(self._handle, 0) == _WAIT_OBJECT_0:
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    OSError: [WinError 6] The handle is invalid

This was due to how, at least for now, the deprecation warning
tests are not using GitPython's normal fixtures, which help clean
things up on Windows.

This adds a small amount of ad-hoc cleanup. It also moves the
acquisition of the Diff object into a pytest fixture, which can be
reused for the immediately forthcoming test that the preferred
property does not warn.
---
 test/deprecation/test_various.py | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 9f948bc48..056e0ac0d 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -3,18 +3,27 @@
 
 """Tests of assorted deprecation warnings with no extra subtleties to check."""
 
-from git.diff import NULL_TREE
-from git.repo import Repo
+import gc
 
 import pytest
 
+from git.diff import NULL_TREE
+from git.repo import Repo
+
 
-def test_diff_renamed_warns(tmp_path):
+@pytest.fixture
+def single_diff(tmp_path):
+    """Fixture to supply a single-file diff."""
     (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
     repo = Repo.init(tmp_path)
     repo.index.add(["a.txt"])
     commit = repo.index.commit("Initial commit")
     (diff,) = commit.diff(NULL_TREE)  # Exactly one file in the diff.
+    yield diff
+    del repo, commit, diff
+    gc.collect()
+
 
+def test_diff_renamed_warns(single_diff):
     with pytest.deprecated_call():
-        diff.renamed
+        single_diff.renamed

From fffa6cea663b179c0e5d53cf5eb93159e2bbc3b0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 20:31:10 -0400
Subject: [PATCH 701/994] Test that the preferred renamed_file property does
 not warn

---
 test/deprecation/test_various.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 056e0ac0d..9dd95f723 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -4,6 +4,7 @@
 """Tests of assorted deprecation warnings with no extra subtleties to check."""
 
 import gc
+import warnings
 
 import pytest
 
@@ -25,5 +26,14 @@ def single_diff(tmp_path):
 
 
 def test_diff_renamed_warns(single_diff):
+    """The deprecated Diff.renamed property issues a deprecation warning."""
     with pytest.deprecated_call():
         single_diff.renamed
+
+
+def test_diff_renamed_file_does_not_warn(single_diff):
+    """The preferred Diff.renamed_file property issues no deprecation warning."""
+    with warnings.catch_warnings():
+        # FIXME: Refine this to filter for deprecation warnings from GitPython.
+        warnings.simplefilter("error", DeprecationWarning)
+        single_diff.renamed_file

From bc111b799b06990fd7dde6385265d0e6d3a6289d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 20:31:30 -0400
Subject: [PATCH 702/994] Add a TODO for simplifying the single_diff fixture

---
 test/deprecation/test_various.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 9dd95f723..efdb0a57c 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -15,6 +15,7 @@
 @pytest.fixture
 def single_diff(tmp_path):
     """Fixture to supply a single-file diff."""
+    # TODO: Consider making a fake diff rather than using a real repo and commit.
     (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
     repo = Repo.init(tmp_path)
     repo.index.add(["a.txt"])

From e3728c3dca9cac2b49f827dbf5d1a100602f33d9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 20:54:50 -0400
Subject: [PATCH 703/994] Decompose new fixture logic better

This will be even more helpful when testing a deprecated member of
the Commit class (not yet done).
---
 test/deprecation/test_various.py | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index efdb0a57c..398367b61 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -13,28 +13,33 @@
 
 
 @pytest.fixture
-def single_diff(tmp_path):
-    """Fixture to supply a single-file diff."""
-    # TODO: Consider making a fake diff rather than using a real repo and commit.
+def commit(tmp_path):
+    """Fixture to supply a one-commit repo's commit, enough for deprecation tests."""
     (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
     repo = Repo.init(tmp_path)
     repo.index.add(["a.txt"])
-    commit = repo.index.commit("Initial commit")
+    yield repo.index.commit("Initial commit")
+    del repo
+    gc.collect()
+
+
+@pytest.fixture
+def diff(commit):
+    """Fixture to supply a single-file diff."""
+    # TODO: Consider making a fake diff rather than using a real repo and commit.
     (diff,) = commit.diff(NULL_TREE)  # Exactly one file in the diff.
     yield diff
-    del repo, commit, diff
-    gc.collect()
 
 
-def test_diff_renamed_warns(single_diff):
+def test_diff_renamed_warns(diff):
     """The deprecated Diff.renamed property issues a deprecation warning."""
     with pytest.deprecated_call():
-        single_diff.renamed
+        diff.renamed
 
 
-def test_diff_renamed_file_does_not_warn(single_diff):
+def test_diff_renamed_file_does_not_warn(diff):
     """The preferred Diff.renamed_file property issues no deprecation warning."""
     with warnings.catch_warnings():
         # FIXME: Refine this to filter for deprecation warnings from GitPython.
         warnings.simplefilter("error", DeprecationWarning)
-        single_diff.renamed_file
+        diff.renamed_file

From ff4b58dd56d382b26d83f2f41f7d11d15e9db8dc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 20:57:40 -0400
Subject: [PATCH 704/994] Extract no-deprecation-warning asserter as a context
 manager

+ Remove the TODO suggesting the diff not be computed from a real
  commit, since we're going to have the logic for that in use in
  forthcoming commit-specific deprecation warning tests anyway.
---
 test/deprecation/test_various.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 398367b61..60f94311f 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -3,6 +3,7 @@
 
 """Tests of assorted deprecation warnings with no extra subtleties to check."""
 
+import contextlib
 import gc
 import warnings
 
@@ -12,6 +13,15 @@
 from git.repo import Repo
 
 
+@contextlib.contextmanager
+def _assert_no_deprecation_warning():
+    """Context manager to assert that code does not issue any deprecation warnings."""
+    with warnings.catch_warnings():
+        # FIXME: Refine this to filter for deprecation warnings from GitPython.
+        warnings.simplefilter("error", DeprecationWarning)
+        yield
+
+
 @pytest.fixture
 def commit(tmp_path):
     """Fixture to supply a one-commit repo's commit, enough for deprecation tests."""
@@ -26,7 +36,6 @@ def commit(tmp_path):
 @pytest.fixture
 def diff(commit):
     """Fixture to supply a single-file diff."""
-    # TODO: Consider making a fake diff rather than using a real repo and commit.
     (diff,) = commit.diff(NULL_TREE)  # Exactly one file in the diff.
     yield diff
 
@@ -39,7 +48,5 @@ def test_diff_renamed_warns(diff):
 
 def test_diff_renamed_file_does_not_warn(diff):
     """The preferred Diff.renamed_file property issues no deprecation warning."""
-    with warnings.catch_warnings():
-        # FIXME: Refine this to filter for deprecation warnings from GitPython.
-        warnings.simplefilter("error", DeprecationWarning)
+    with _assert_no_deprecation_warning():
         diff.renamed_file

From 2c526962ebf0a5d4d31a725577db112f2ce60447 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 21:04:06 -0400
Subject: [PATCH 705/994] Test that the deprecated Commit.trailers property
 warns

And that the non-deprecated recommended alternative trailers_list
and trailers_dict properties do not warn.

The test that trailers warns does not yet pass yet, because it has
not yet been made to warn.
---
 test/deprecation/test_various.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 60f94311f..99ce882b0 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -50,3 +50,21 @@ def test_diff_renamed_file_does_not_warn(diff):
     """The preferred Diff.renamed_file property issues no deprecation warning."""
     with _assert_no_deprecation_warning():
         diff.renamed_file
+
+
+def test_commit_trailers_warns(commit):
+    """The deprecated Commit.trailers property issues a deprecation warning."""
+    with pytest.deprecated_call():
+        commit.trailers
+
+
+def test_commit_trailers_list_does_not_warn(commit):
+    """The nondeprecated Commit.trailers_list property issues no deprecation warning."""
+    with _assert_no_deprecation_warning():
+        commit.trailers_list
+
+
+def test_commit_trailers_dict_does_not_warn(commit):
+    """The nondeprecated Commit.trailers_dict property issues no deprecation warning."""
+    with _assert_no_deprecation_warning():
+        commit.trailers_dict

From 03464d90a66caf6a5d8dbbd37318758331c85168 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 21:12:41 -0400
Subject: [PATCH 706/994] Have the deprecated Commit.trailers property issue a
 warning

---
 git/objects/commit.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/git/objects/commit.py b/git/objects/commit.py
index 8de52980c..d957c9051 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -14,6 +14,7 @@
 from subprocess import Popen, PIPE
 import sys
 from time import altzone, daylight, localtime, time, timezone
+import warnings
 
 from gitdb import IStream
 
@@ -399,6 +400,11 @@ def trailers(self) -> Dict[str, str]:
             Dictionary containing whitespace stripped trailer information.
             Only contains the latest instance of each trailer key.
         """
+        warnings.warn(
+            "Commit.trailers is deprecated, use Commit.trailers_list or Commit.trailers_dict instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         return {k: v[0] for k, v in self.trailers_dict.items()}
 
     @property

From 9d096e08d7645bc5206c4728b45efcf26486b635 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 21:51:11 -0400
Subject: [PATCH 707/994] Test that Traversable.{list_,}traverse, but not
 overrides, warn

---
 test/deprecation/test_various.py | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 99ce882b0..be3195a5e 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -10,6 +10,7 @@
 import pytest
 
 from git.diff import NULL_TREE
+from git.objects.util import Traversable
 from git.repo import Repo
 
 
@@ -68,3 +69,27 @@ def test_commit_trailers_dict_does_not_warn(commit):
     """The nondeprecated Commit.trailers_dict property issues no deprecation warning."""
     with _assert_no_deprecation_warning():
         commit.trailers_dict
+
+
+def test_traverse_list_traverse_in_base_class_warns(commit):
+    """Traversable.list_traverse's base implementation issues a deprecation warning."""
+    with pytest.deprecated_call():
+        Traversable.list_traverse(commit)
+
+
+def test_traversable_list_traverse_override_does_not_warn(commit):
+    """Calling list_traverse on concrete subclasses is not deprecated, does not warn."""
+    with _assert_no_deprecation_warning():
+        commit.list_traverse()
+
+
+def test_traverse_traverse_in_base_class_warns(commit):
+    """Traversable.traverse's base implementation issues a deprecation warning."""
+    with pytest.deprecated_call():
+        Traversable.traverse(commit)
+
+
+def test_traverse_traverse_override_does_not_warn(commit):
+    """Calling traverse on concrete subclasses is not deprecated, does not warn."""
+    with _assert_no_deprecation_warning():
+        commit.traverse()

From 21c2b72b019627dba81b35085117b79660567abd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 01:51:54 -0400
Subject: [PATCH 708/994] Use the :exc: Sphinx role for DeprecationWarning

Python warnings are exceptions, to facilitate converting them to
errors by raising them. Thus the more specific :exc: role applies
and is a better fit than the more general :class: role.
---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 5839f9720..8c1c26012 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1285,7 +1285,7 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
 
 
 class IterableClassWatcher(type):
-    """Metaclass that issues :class:`DeprecationWarning` when :class:`git.util.Iterable`
+    """Metaclass that issues :exc:`DeprecationWarning` when :class:`git.util.Iterable`
     is subclassed."""
 
     def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:

From ca385a59439006355de02ddab9012031e8577e41 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 02:00:43 -0400
Subject: [PATCH 709/994] Test that subclassing deprecated git.util.Iterable
 warns

And that subclassing the strongly preferred git.util.Iterable class
does not warn.
---
 test/deprecation/test_various.py | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index be3195a5e..71f9cf940 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -12,6 +12,7 @@
 from git.diff import NULL_TREE
 from git.objects.util import Traversable
 from git.repo import Repo
+from git.util import Iterable as _Iterable, IterableObj
 
 
 @contextlib.contextmanager
@@ -93,3 +94,19 @@ def test_traverse_traverse_override_does_not_warn(commit):
     """Calling traverse on concrete subclasses is not deprecated, does not warn."""
     with _assert_no_deprecation_warning():
         commit.traverse()
+
+
+def test_iterable_inheriting_warns():
+    """Subclassing the deprecated git.util.Iterable issues a deprecation warning."""
+    with pytest.deprecated_call():
+
+        class Derived(_Iterable):
+            pass
+
+
+def test_iterable_obj_inheriting_does_not_warn():
+    """Subclassing git.util.IterableObj is not deprecated, does not warn."""
+    with _assert_no_deprecation_warning():
+
+        class Derived(IterableObj):
+            pass

From 8bbcb26ea6e798a10969570d24cd5e9c401feed7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 11:43:13 -0400
Subject: [PATCH 710/994] Call repo.close() instead of manually collecting

The code this replaces in the `commit` pytest fixture seems not to
be needed anymore, and could arguably be removed now with no
further related changes. But whether it is needed depends on
subtle and sometimes nondeterministic factors, and may also vary
across Python versions.

To be safe, this replaces it with a call to the Repo instance's
close method, which is in effect more robust than what was being
done before, as it calls clear_cache on the the Git object that the
Repo object uses, does a gitdb/smmap collection, and on Windows
calls gc.collect both before and after that collection.

This may happen immediately anyway if the Repo object is not
reachable from any cycle, since the reference count should go to
zero after each of the deprecation warning tests (the fixture's
lifetime is that of the test case), and Repo.close is called in
Repo.__del__. But this makes it happen immediately even if there is
a cycle.
---
 test/deprecation/test_various.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_various.py
index 71f9cf940..a82b989b7 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_various.py
@@ -4,7 +4,6 @@
 """Tests of assorted deprecation warnings with no extra subtleties to check."""
 
 import contextlib
-import gc
 import warnings
 
 import pytest
@@ -31,8 +30,7 @@ def commit(tmp_path):
     repo = Repo.init(tmp_path)
     repo.index.add(["a.txt"])
     yield repo.index.commit("Initial commit")
-    del repo
-    gc.collect()
+    repo.close()
 
 
 @pytest.fixture

From b8ce99031d3183a895f15e49fb7149a56a653065 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 19:39:00 -0400
Subject: [PATCH 711/994] Better name and document the basic deprecation test
 module

The other deprecation test modules this refers to don't exist yet
but will be introduced soon.
---
 .../deprecation/{test_various.py => test_basic.py} | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)
 rename test/deprecation/{test_various.py => test_basic.py} (80%)

diff --git a/test/deprecation/test_various.py b/test/deprecation/test_basic.py
similarity index 80%
rename from test/deprecation/test_various.py
rename to test/deprecation/test_basic.py
index a82b989b7..459d79268 100644
--- a/test/deprecation/test_various.py
+++ b/test/deprecation/test_basic.py
@@ -1,7 +1,19 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Tests of assorted deprecation warnings with no extra subtleties to check."""
+"""Tests of assorted deprecation warnings when there are no extra subtleties to check.
+
+This tests deprecation warnings where all that needs be verified is that a deprecated
+property, function, or class issues a DeprecationWarning when used and, if applicable,
+that recommended alternatives do not issue the warning.
+
+This is in contrast to other modules within test.deprecation, which test warnings where
+there is a risk of breaking other runtime behavior, or of breaking static type checking
+or making it less useful, by introducing the warning or in plausible future changes to
+how the warning is implemented. That happens when it is necessary to customize attribute
+access on a module or class, in a way it was not customized before, to issue a warning.
+It is inapplicable to the deprecations whose warnings are tested in this module.
+"""
 
 import contextlib
 import warnings

From 61273aa2084ada8c48d0f9e511556d3d72eec32c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 20:49:34 -0400
Subject: [PATCH 712/994] Annotate basic deprecation tests; have mypy scan it

- Add type hints to test/deprecation/basic.py. As its module
  docstring (already) notes, that test module does not contain
  code where it is specifically important that it be type checked
  to verify important properties of the code under test. However,
  other test.deprecation.* modules will, and it is much more
  convenient to be able to scan the whole directory than the
  directory except for one file. (Less importantly, for the
  deprecation tests to be easily readable as a coherent whole, it
  makes sense that all, not just most, would have annotations.)

- Configure mypy in pyproject.toml so it can be run without
  arguments (mypy errors when run that way unless configured),
  where the effect is to scan the git/ directory, as was commonly
  done before, as well as the test/deprecation/ directory.

- Change the CI test workflow, as well as tox.ini, to run mypy with
  no arguments instead of passing `-p git`, so that it will scan
  both the git package as it had before and the test.deprecation
  package (due to the new pyproject.toml configuration).

- Change the readme to recommend running it that way, too.
---
 .github/workflows/pythonpackage.yml |  2 +-
 README.md                           |  2 +-
 pyproject.toml                      |  1 +
 test/deprecation/test_basic.py      | 40 +++++++++++++++++++----------
 tox.ini                             |  2 +-
 5 files changed, 30 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 7cee0cd64..4c918a92d 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -88,7 +88,7 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy --python-version=${{ matrix.python-version }} -p git
+        mypy --python-version=${{ matrix.python-version }}
       env:
         MYPY_FORCE_COLOR: "1"
         TERM: "xterm-256color"  # For color: https://github.com/python/mypy/issues/13817
diff --git a/README.md b/README.md
index 30af532db..9bedaaae7 100644
--- a/README.md
+++ b/README.md
@@ -167,7 +167,7 @@ This includes the linting and autoformatting done by Ruff, as well as some other
 To typecheck, run:
 
 ```sh
-mypy -p git
+mypy
 ```
 
 #### CI (and tox)
diff --git a/pyproject.toml b/pyproject.toml
index 1dc1e6aed..5eac2be09 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ testpaths = "test"  # Space separated list of paths from root e.g test tests doc
 
 [tool.mypy]
 python_version = "3.8"
+files = ["git/", "test/deprecation/"]
 disallow_untyped_defs = true
 no_implicit_optional = true
 warn_redundant_casts = true
diff --git a/test/deprecation/test_basic.py b/test/deprecation/test_basic.py
index 459d79268..8ee7e72b1 100644
--- a/test/deprecation/test_basic.py
+++ b/test/deprecation/test_basic.py
@@ -25,9 +25,21 @@
 from git.repo import Repo
 from git.util import Iterable as _Iterable, IterableObj
 
+# typing -----------------------------------------------------------------
+
+from typing import Generator, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pathlib import Path
+
+    from git.diff import Diff
+    from git.objects.commit import Commit
+
+# ------------------------------------------------------------------------
+
 
 @contextlib.contextmanager
-def _assert_no_deprecation_warning():
+def _assert_no_deprecation_warning() -> Generator[None, None, None]:
     """Context manager to assert that code does not issue any deprecation warnings."""
     with warnings.catch_warnings():
         # FIXME: Refine this to filter for deprecation warnings from GitPython.
@@ -36,7 +48,7 @@ def _assert_no_deprecation_warning():
 
 
 @pytest.fixture
-def commit(tmp_path):
+def commit(tmp_path: "Path") -> Generator["Commit", None, None]:
     """Fixture to supply a one-commit repo's commit, enough for deprecation tests."""
     (tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
     repo = Repo.init(tmp_path)
@@ -46,67 +58,67 @@ def commit(tmp_path):
 
 
 @pytest.fixture
-def diff(commit):
+def diff(commit: "Commit") -> Generator["Diff", None, None]:
     """Fixture to supply a single-file diff."""
     (diff,) = commit.diff(NULL_TREE)  # Exactly one file in the diff.
     yield diff
 
 
-def test_diff_renamed_warns(diff):
+def test_diff_renamed_warns(diff: "Diff") -> None:
     """The deprecated Diff.renamed property issues a deprecation warning."""
     with pytest.deprecated_call():
         diff.renamed
 
 
-def test_diff_renamed_file_does_not_warn(diff):
+def test_diff_renamed_file_does_not_warn(diff: "Diff") -> None:
     """The preferred Diff.renamed_file property issues no deprecation warning."""
     with _assert_no_deprecation_warning():
         diff.renamed_file
 
 
-def test_commit_trailers_warns(commit):
+def test_commit_trailers_warns(commit: "Commit") -> None:
     """The deprecated Commit.trailers property issues a deprecation warning."""
     with pytest.deprecated_call():
         commit.trailers
 
 
-def test_commit_trailers_list_does_not_warn(commit):
+def test_commit_trailers_list_does_not_warn(commit: "Commit") -> None:
     """The nondeprecated Commit.trailers_list property issues no deprecation warning."""
     with _assert_no_deprecation_warning():
         commit.trailers_list
 
 
-def test_commit_trailers_dict_does_not_warn(commit):
+def test_commit_trailers_dict_does_not_warn(commit: "Commit") -> None:
     """The nondeprecated Commit.trailers_dict property issues no deprecation warning."""
     with _assert_no_deprecation_warning():
         commit.trailers_dict
 
 
-def test_traverse_list_traverse_in_base_class_warns(commit):
+def test_traverse_list_traverse_in_base_class_warns(commit: "Commit") -> None:
     """Traversable.list_traverse's base implementation issues a deprecation warning."""
     with pytest.deprecated_call():
         Traversable.list_traverse(commit)
 
 
-def test_traversable_list_traverse_override_does_not_warn(commit):
+def test_traversable_list_traverse_override_does_not_warn(commit: "Commit") -> None:
     """Calling list_traverse on concrete subclasses is not deprecated, does not warn."""
     with _assert_no_deprecation_warning():
         commit.list_traverse()
 
 
-def test_traverse_traverse_in_base_class_warns(commit):
+def test_traverse_traverse_in_base_class_warns(commit: "Commit") -> None:
     """Traversable.traverse's base implementation issues a deprecation warning."""
     with pytest.deprecated_call():
         Traversable.traverse(commit)
 
 
-def test_traverse_traverse_override_does_not_warn(commit):
+def test_traverse_traverse_override_does_not_warn(commit: "Commit") -> None:
     """Calling traverse on concrete subclasses is not deprecated, does not warn."""
     with _assert_no_deprecation_warning():
         commit.traverse()
 
 
-def test_iterable_inheriting_warns():
+def test_iterable_inheriting_warns() -> None:
     """Subclassing the deprecated git.util.Iterable issues a deprecation warning."""
     with pytest.deprecated_call():
 
@@ -114,7 +126,7 @@ class Derived(_Iterable):
             pass
 
 
-def test_iterable_obj_inheriting_does_not_warn():
+def test_iterable_obj_inheriting_does_not_warn() -> None:
     """Subclassing git.util.IterableObj is not deprecated, does not warn."""
     with _assert_no_deprecation_warning():
 
diff --git a/tox.ini b/tox.ini
index 33074a78a..fc62fa587 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,7 +30,7 @@ description = Typecheck with mypy
 base_python = py{39,310,311,312,38,37}
 set_env =
     MYPY_FORCE_COLOR = 1
-commands = mypy -p git
+commands = mypy
 ignore_outcome = true
 
 [testenv:html]

From b7a3d8c08537d00aac065b7dcbe1a4896919ee07 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 17:05:06 -0400
Subject: [PATCH 713/994] Start on top-level module attribute access regression
 tests

---
 test/deprecation/test_attributes.py | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 test/deprecation/test_attributes.py

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
new file mode 100644
index 000000000..2673cc172
--- /dev/null
+++ b/test/deprecation/test_attributes.py
@@ -0,0 +1,10 @@
+"""Tests for dynamic and static attribute errors."""
+
+import pytest
+
+import git
+
+
+def test_no_attribute() -> None:
+    with pytest.raises(AttributeError):
+        git.foo

From 105f50056126a73e8cbfeb0d1157695fe6b296b1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 19 Mar 2024 17:15:02 -0400
Subject: [PATCH 714/994] Test attribute access and importing separately

Rather than only testing attribute access.
---
 test/deprecation/test_attributes.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 2673cc172..aea3278f8 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -2,9 +2,14 @@
 
 import pytest
 
-import git
 
+def test_cannot_get_undefined() -> None:
+    import git
 
-def test_no_attribute() -> None:
     with pytest.raises(AttributeError):
         git.foo
+
+
+def test_cannot_import_undefined() -> None:
+    with pytest.raises(ImportError):
+        from git import foo

From 859e38cfb395e6a937c30fedd36a9b3896747188 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 20:14:22 -0400
Subject: [PATCH 715/994] Expand to test top-level deprecated names

---
 test/deprecation/test_attributes.py | 89 ++++++++++++++++++++++++++++-
 1 file changed, 88 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index aea3278f8..428dab236 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -1,5 +1,7 @@
 """Tests for dynamic and static attribute errors."""
 
+import importlib
+
 import pytest
 
 
@@ -12,4 +14,89 @@ def test_cannot_get_undefined() -> None:
 
 def test_cannot_import_undefined() -> None:
     with pytest.raises(ImportError):
-        from git import foo
+        from git import foo  # noqa: F401
+
+
+def test_util_alias_access_resolves() -> None:
+    """These resolve for now, though they're private we do not guarantee this."""
+    import git
+
+    assert git.util is git.index.util
+
+
+def test_util_alias_import_resolves() -> None:
+    from git import util
+    import git
+
+    util is git.index.util
+
+
+def test_util_alias_access_warns() -> None:
+    import git
+
+    with pytest.deprecated_call() as ctx:
+        git.util
+
+    assert len(ctx) == 1
+    message = ctx[0].message.args[0]
+    assert "git.util" in message
+    assert "git.index.util" in message
+    assert "should not be relied on" in message
+
+
+def test_util_alias_import_warns() -> None:
+    with pytest.deprecated_call() as ctx:
+        from git import util  # noqa: F401
+
+    message = ctx[0].message.args[0]
+    assert "git.util" in message
+    assert "git.index.util" in message
+    assert "should not be relied on" in message
+
+
+_parametrize_by_private_alias = pytest.mark.parametrize(
+    "name, fullname",
+    [
+        ("head", "git.refs.head"),
+        ("log", "git.refs.log"),
+        ("reference", "git.refs.reference"),
+        ("symbolic", "git.refs.symbolic"),
+        ("tag", "git.refs.tag"),
+        ("base", "git.index.base"),
+        ("fun", "git.index.fun"),
+        ("typ", "git.index.typ"),
+    ],
+)
+
+
+@_parametrize_by_private_alias
+def test_private_module_alias_access_resolves(name: str, fullname: str) -> None:
+    """These resolve for now, though they're private we do not guarantee this."""
+    import git
+
+    assert getattr(git, name) is importlib.import_module(fullname)
+
+
+@_parametrize_by_private_alias
+def test_private_module_alias_import_resolves(name: str, fullname: str) -> None:
+    exec(f"from git import {name}")
+    locals()[name] is importlib.import_module(fullname)
+
+
+@_parametrize_by_private_alias
+def test_private_module_alias_access_warns(name: str, fullname: str) -> None:
+    import git
+
+    with pytest.deprecated_call() as ctx:
+        getattr(git, name)
+
+    assert len(ctx) == 1
+    assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+
+
+@_parametrize_by_private_alias
+def test_private_module_alias_import_warns(name: str, fullname: str) -> None:
+    with pytest.deprecated_call() as ctx:
+        exec(f"from git import {name}")
+
+    assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")

From 46a739da24331849323a7c583ffd61a51583d3c1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 20:20:04 -0400
Subject: [PATCH 716/994] Hoist `import git` to module level in test module

Because it's going to be necessary to express things in terms of it
in parametrization markings, in order for mypy to show the expected
errors for names that are available dynamically but deliberately
static type errors.
---
 test/deprecation/test_attributes.py | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 428dab236..85aa7a579 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -4,10 +4,10 @@
 
 import pytest
 
+import git
 
-def test_cannot_get_undefined() -> None:
-    import git
 
+def test_cannot_get_undefined() -> None:
     with pytest.raises(AttributeError):
         git.foo
 
@@ -19,21 +19,16 @@ def test_cannot_import_undefined() -> None:
 
 def test_util_alias_access_resolves() -> None:
     """These resolve for now, though they're private we do not guarantee this."""
-    import git
-
     assert git.util is git.index.util
 
 
 def test_util_alias_import_resolves() -> None:
     from git import util
-    import git
 
     util is git.index.util
 
 
 def test_util_alias_access_warns() -> None:
-    import git
-
     with pytest.deprecated_call() as ctx:
         git.util
 
@@ -72,8 +67,6 @@ def test_util_alias_import_warns() -> None:
 @_parametrize_by_private_alias
 def test_private_module_alias_access_resolves(name: str, fullname: str) -> None:
     """These resolve for now, though they're private we do not guarantee this."""
-    import git
-
     assert getattr(git, name) is importlib.import_module(fullname)
 
 
@@ -85,8 +78,6 @@ def test_private_module_alias_import_resolves(name: str, fullname: str) -> None:
 
 @_parametrize_by_private_alias
 def test_private_module_alias_access_warns(name: str, fullname: str) -> None:
-    import git
-
     with pytest.deprecated_call() as ctx:
         getattr(git, name)
 

From a2df3a8283274dda9236d0d41cf44a38317560cb Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 20:40:17 -0400
Subject: [PATCH 717/994] Test static typing of private module aliases

This tests that mypy considers them not to be present.

That mypy is configured with `warn_unused_ignores = true` is key,
since that is what verifies that the type errors really do occur,
based on the suppressions written for them.
---
 test/deprecation/test_attributes.py | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 85aa7a579..53612bde2 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -9,12 +9,12 @@
 
 def test_cannot_get_undefined() -> None:
     with pytest.raises(AttributeError):
-        git.foo
+        git.foo  # type: ignore[attr-defined]
 
 
 def test_cannot_import_undefined() -> None:
     with pytest.raises(ImportError):
-        from git import foo  # noqa: F401
+        from git import foo  # type: ignore[attr-defined]  # noqa: F401
 
 
 def test_util_alias_access_resolves() -> None:
@@ -49,6 +49,21 @@ def test_util_alias_import_warns() -> None:
     assert "should not be relied on" in message
 
 
+def test_private_module_aliases() -> None:
+    """These exist dynamically but mypy will show them as absent (intentionally).
+
+    More detailed dynamic behavior is examined in the subsequent test cases.
+    """
+    git.head  # type: ignore[attr-defined]
+    git.log  # type: ignore[attr-defined]
+    git.reference  # type: ignore[attr-defined]
+    git.symbolic  # type: ignore[attr-defined]
+    git.tag  # type: ignore[attr-defined]
+    git.base  # type: ignore[attr-defined]
+    git.fun  # type: ignore[attr-defined]
+    git.typ  # type: ignore[attr-defined]
+
+
 _parametrize_by_private_alias = pytest.mark.parametrize(
     "name, fullname",
     [

From a15a830d47089ba625374fa3fd65e8568b7e0372 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 20:52:56 -0400
Subject: [PATCH 718/994] Improve a couple test case docstrings

---
 test/deprecation/test_attributes.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 53612bde2..6df1359f5 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -18,7 +18,7 @@ def test_cannot_import_undefined() -> None:
 
 
 def test_util_alias_access_resolves() -> None:
-    """These resolve for now, though they're private we do not guarantee this."""
+    """These resolve for now, though they're private and we do not guarantee this."""
     assert git.util is git.index.util
 
 
@@ -50,7 +50,7 @@ def test_util_alias_import_warns() -> None:
 
 
 def test_private_module_aliases() -> None:
-    """These exist dynamically but mypy will show them as absent (intentionally).
+    """These exist dynamically (for now) but mypy treats them as absent (intentionally).
 
     More detailed dynamic behavior is examined in the subsequent test cases.
     """

From dbaa535e47b61db0f9ce29c65b2a6616f6454196 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 20:57:05 -0400
Subject: [PATCH 719/994] Add a couple missing assert keywords

---
 test/deprecation/test_attributes.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 6df1359f5..b9ca1d7c2 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -25,7 +25,7 @@ def test_util_alias_access_resolves() -> None:
 def test_util_alias_import_resolves() -> None:
     from git import util
 
-    util is git.index.util
+    assert util is git.index.util
 
 
 def test_util_alias_access_warns() -> None:
@@ -88,7 +88,7 @@ def test_private_module_alias_access_resolves(name: str, fullname: str) -> None:
 @_parametrize_by_private_alias
 def test_private_module_alias_import_resolves(name: str, fullname: str) -> None:
     exec(f"from git import {name}")
-    locals()[name] is importlib.import_module(fullname)
+    assert locals()[name] is importlib.import_module(fullname)
 
 
 @_parametrize_by_private_alias

From d00c843434806389bfb0bf7992505f358e97513f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 21:00:05 -0400
Subject: [PATCH 720/994] Clarify how test_private_module_aliases is statically
 checkable

---
 test/deprecation/test_attributes.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index b9ca1d7c2..386ae1838 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -52,6 +52,9 @@ def test_util_alias_import_warns() -> None:
 def test_private_module_aliases() -> None:
     """These exist dynamically (for now) but mypy treats them as absent (intentionally).
 
+    This code verifies the effect of static type checking when analyzed by mypy, if mypy
+    is configured with ``warn_unused_ignores = true``.
+
     More detailed dynamic behavior is examined in the subsequent test cases.
     """
     git.head  # type: ignore[attr-defined]

From 983fda774a6eedda3b62ac2fa94ce54675a5f662 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 21 Mar 2024 02:04:21 -0400
Subject: [PATCH 721/994] Move mark-sharing tests into a class

---
 test/deprecation/test_attributes.py | 46 +++++++++++++----------------
 1 file changed, 20 insertions(+), 26 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 386ae1838..7af77000f 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -49,8 +49,8 @@ def test_util_alias_import_warns() -> None:
     assert "should not be relied on" in message
 
 
-def test_private_module_aliases() -> None:
-    """These exist dynamically (for now) but mypy treats them as absent (intentionally).
+def test_private_module_aliases_exist_dynamically() -> None:
+    """These exist at runtime (for now) but mypy treats them as absent (intentionally).
 
     This code verifies the effect of static type checking when analyzed by mypy, if mypy
     is configured with ``warn_unused_ignores = true``.
@@ -67,7 +67,7 @@ def test_private_module_aliases() -> None:
     git.typ  # type: ignore[attr-defined]
 
 
-_parametrize_by_private_alias = pytest.mark.parametrize(
+@pytest.mark.parametrize(
     "name, fullname",
     [
         ("head", "git.refs.head"),
@@ -80,32 +80,26 @@ def test_private_module_aliases() -> None:
         ("typ", "git.index.typ"),
     ],
 )
+class TestPrivateModuleAliases:
+    """Tests of the private module aliases' shared specific runtime behaviors."""
 
+    def test_private_module_alias_access_resolves(self, name: str, fullname: str) -> None:
+        """These resolve for now, though they're private we do not guarantee this."""
+        assert getattr(git, name) is importlib.import_module(fullname)
 
-@_parametrize_by_private_alias
-def test_private_module_alias_access_resolves(name: str, fullname: str) -> None:
-    """These resolve for now, though they're private we do not guarantee this."""
-    assert getattr(git, name) is importlib.import_module(fullname)
-
-
-@_parametrize_by_private_alias
-def test_private_module_alias_import_resolves(name: str, fullname: str) -> None:
-    exec(f"from git import {name}")
-    assert locals()[name] is importlib.import_module(fullname)
-
-
-@_parametrize_by_private_alias
-def test_private_module_alias_access_warns(name: str, fullname: str) -> None:
-    with pytest.deprecated_call() as ctx:
-        getattr(git, name)
+    def test_private_module_alias_import_resolves(self, name: str, fullname: str) -> None:
+        exec(f"from git import {name}")
+        assert locals()[name] is importlib.import_module(fullname)
 
-    assert len(ctx) == 1
-    assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+    def test_private_module_alias_access_warns(self, name: str, fullname: str) -> None:
+        with pytest.deprecated_call() as ctx:
+            getattr(git, name)
 
+        assert len(ctx) == 1
+        assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
 
-@_parametrize_by_private_alias
-def test_private_module_alias_import_warns(name: str, fullname: str) -> None:
-    with pytest.deprecated_call() as ctx:
-        exec(f"from git import {name}")
+    def test_private_module_alias_import_warns(self, name: str, fullname: str) -> None:
+        with pytest.deprecated_call() as ctx:
+            exec(f"from git import {name}")
 
-    assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+        assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")

From 19acd4cf551dfd6c7774e1ca7794ef83b321c8b6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Mar 2024 01:33:00 -0400
Subject: [PATCH 722/994] Add FIXME for what to do next

---
 test/deprecation/test_attributes.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 7af77000f..f249ae871 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -103,3 +103,14 @@ def test_private_module_alias_import_warns(self, name: str, fullname: str) -> No
             exec(f"from git import {name}")
 
         assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+
+
+reveal_type(git.util.git_working_dir)
+
+# FIXME: Add one or more test cases that access something like git.util.git_working_dir
+# to verify that it is available, and also use assert_type on it to ensure mypy knows
+# that accesses to expressions of the form git.util.XYZ resolve to git.index.util.XYZ.
+#
+# It may be necessary for GitPython, in git/__init__.py, to import util from git.index
+# explicitly before (still) deleting the util global, in order for mypy to know what is
+# going on. Also check pyright.

From f39bbb5172987b0462b5d1845c0cb0cf3824b3d5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Mar 2024 10:52:32 -0400
Subject: [PATCH 723/994] Fix a test docstring

---
 test/deprecation/test_attributes.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index f249ae871..69a1aa1f3 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -84,7 +84,7 @@ class TestPrivateModuleAliases:
     """Tests of the private module aliases' shared specific runtime behaviors."""
 
     def test_private_module_alias_access_resolves(self, name: str, fullname: str) -> None:
-        """These resolve for now, though they're private we do not guarantee this."""
+        """These resolve for now, though they're private and we do not guarantee this."""
         assert getattr(git, name) is importlib.import_module(fullname)
 
     def test_private_module_alias_import_resolves(self, name: str, fullname: str) -> None:

From aee7078e28b5de9bb5cd605ff23f37d7328dccc9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Mar 2024 18:56:02 -0400
Subject: [PATCH 724/994] Test resolution into git.index.util using git.util

---
 test/deprecation/test_attributes.py | 29 ++++++++++++++++++-----------
 1 file changed, 18 insertions(+), 11 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 69a1aa1f3..74f51a09e 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -1,6 +1,7 @@
 """Tests for dynamic and static attribute errors."""
 
 import importlib
+from typing import Type
 
 import pytest
 
@@ -28,6 +29,23 @@ def test_util_alias_import_resolves() -> None:
     assert util is git.index.util
 
 
+def test_util_alias_members_resolve() -> None:
+    """git.index.util members can be accessed via git.util, and mypy recognizes it."""
+    # TODO: When typing_extensions is made a test dependency, use assert_type for this.
+    gu_tfs = git.util.TemporaryFileSwap
+    from git.index.util import TemporaryFileSwap
+
+    def accepts_tfs_type(t: Type[TemporaryFileSwap]) -> None:
+        pass
+
+    def rejects_tfs_type(t: Type[git.Git]) -> None:
+        pass
+
+    accepts_tfs_type(gu_tfs)
+    rejects_tfs_type(gu_tfs)  # type: ignore[arg-type]
+    assert gu_tfs is TemporaryFileSwap
+
+
 def test_util_alias_access_warns() -> None:
     with pytest.deprecated_call() as ctx:
         git.util
@@ -103,14 +121,3 @@ def test_private_module_alias_import_warns(self, name: str, fullname: str) -> No
             exec(f"from git import {name}")
 
         assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
-
-
-reveal_type(git.util.git_working_dir)
-
-# FIXME: Add one or more test cases that access something like git.util.git_working_dir
-# to verify that it is available, and also use assert_type on it to ensure mypy knows
-# that accesses to expressions of the form git.util.XYZ resolve to git.index.util.XYZ.
-#
-# It may be necessary for GitPython, in git/__init__.py, to import util from git.index
-# explicitly before (still) deleting the util global, in order for mypy to know what is
-# going on. Also check pyright.

From 7f4a19135c755065538d13ed4faeb9c10cc203c8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Mar 2024 19:01:11 -0400
Subject: [PATCH 725/994] Fix brittle way of checking warning messages

Which was causing a type error.
---
 test/deprecation/test_attributes.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 74f51a09e..0f142fbe7 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -51,7 +51,7 @@ def test_util_alias_access_warns() -> None:
         git.util
 
     assert len(ctx) == 1
-    message = ctx[0].message.args[0]
+    message = str(ctx[0].message)
     assert "git.util" in message
     assert "git.index.util" in message
     assert "should not be relied on" in message
@@ -61,7 +61,7 @@ def test_util_alias_import_warns() -> None:
     with pytest.deprecated_call() as ctx:
         from git import util  # noqa: F401
 
-    message = ctx[0].message.args[0]
+    message = str(ctx[0].message)
     assert "git.util" in message
     assert "git.index.util" in message
     assert "should not be relied on" in message
@@ -114,10 +114,12 @@ def test_private_module_alias_access_warns(self, name: str, fullname: str) -> No
             getattr(git, name)
 
         assert len(ctx) == 1
-        assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+        message = str(ctx[0].message)
+        assert message.endswith(f"Use {fullname} instead.")
 
     def test_private_module_alias_import_warns(self, name: str, fullname: str) -> None:
         with pytest.deprecated_call() as ctx:
             exec(f"from git import {name}")
 
-        assert ctx[0].message.args[0].endswith(f"Use {fullname} instead.")
+        message = str(ctx[0].message)
+        assert message.endswith(f"Use {fullname} instead.")

From d08a5768f6e8aa78de5f10c7e7a0777d2e4dfec3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 22 Mar 2024 19:09:44 -0400
Subject: [PATCH 726/994] Clarify todo

---
 test/deprecation/test_attributes.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 0f142fbe7..829ff29d2 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -31,7 +31,6 @@ def test_util_alias_import_resolves() -> None:
 
 def test_util_alias_members_resolve() -> None:
     """git.index.util members can be accessed via git.util, and mypy recognizes it."""
-    # TODO: When typing_extensions is made a test dependency, use assert_type for this.
     gu_tfs = git.util.TemporaryFileSwap
     from git.index.util import TemporaryFileSwap
 
@@ -41,8 +40,10 @@ def accepts_tfs_type(t: Type[TemporaryFileSwap]) -> None:
     def rejects_tfs_type(t: Type[git.Git]) -> None:
         pass
 
+    # TODO: When typing_extensions is made a test dependency, use assert_type for this.
     accepts_tfs_type(gu_tfs)
     rejects_tfs_type(gu_tfs)  # type: ignore[arg-type]
+
     assert gu_tfs is TemporaryFileSwap
 
 

From 9d58e6d367dc4651213e674b9f586e0a18d787bc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 14:35:36 -0400
Subject: [PATCH 727/994] Start reorganizing new tests more in the GitPython
 style

---
 test/deprecation/test_attributes.py | 142 +++++++++++++++-------------
 1 file changed, 77 insertions(+), 65 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 829ff29d2..97aa9bc50 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -1,4 +1,8 @@
-"""Tests for dynamic and static attribute errors."""
+"""Tests for dynamic and static attribute errors in GitPython's top-level git module.
+
+Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
+checks static typing of the code under test. (Running pytest checks dynamic behavior.)
+"""
 
 import importlib
 from typing import Type
@@ -18,17 +22,6 @@ def test_cannot_import_undefined() -> None:
         from git import foo  # type: ignore[attr-defined]  # noqa: F401
 
 
-def test_util_alias_access_resolves() -> None:
-    """These resolve for now, though they're private and we do not guarantee this."""
-    assert git.util is git.index.util
-
-
-def test_util_alias_import_resolves() -> None:
-    from git import util
-
-    assert util is git.index.util
-
-
 def test_util_alias_members_resolve() -> None:
     """git.index.util members can be accessed via git.util, and mypy recognizes it."""
     gu_tfs = git.util.TemporaryFileSwap
@@ -68,59 +61,78 @@ def test_util_alias_import_warns() -> None:
     assert "should not be relied on" in message
 
 
-def test_private_module_aliases_exist_dynamically() -> None:
-    """These exist at runtime (for now) but mypy treats them as absent (intentionally).
-
-    This code verifies the effect of static type checking when analyzed by mypy, if mypy
-    is configured with ``warn_unused_ignores = true``.
-
-    More detailed dynamic behavior is examined in the subsequent test cases.
-    """
-    git.head  # type: ignore[attr-defined]
-    git.log  # type: ignore[attr-defined]
-    git.reference  # type: ignore[attr-defined]
-    git.symbolic  # type: ignore[attr-defined]
-    git.tag  # type: ignore[attr-defined]
-    git.base  # type: ignore[attr-defined]
-    git.fun  # type: ignore[attr-defined]
-    git.typ  # type: ignore[attr-defined]
-
-
-@pytest.mark.parametrize(
-    "name, fullname",
-    [
-        ("head", "git.refs.head"),
-        ("log", "git.refs.log"),
-        ("reference", "git.refs.reference"),
-        ("symbolic", "git.refs.symbolic"),
-        ("tag", "git.refs.tag"),
-        ("base", "git.index.base"),
-        ("fun", "git.index.fun"),
-        ("typ", "git.index.typ"),
-    ],
+# Split out util and have all its tests be separate, above.
+_MODULE_ALIAS_TARGETS = (
+    git.refs.head,
+    git.refs.log,
+    git.refs.reference,
+    git.refs.symbolic,
+    git.refs.tag,
+    git.index.base,
+    git.index.fun,
+    git.index.typ,
+    git.index.util,
 )
-class TestPrivateModuleAliases:
-    """Tests of the private module aliases' shared specific runtime behaviors."""
 
-    def test_private_module_alias_access_resolves(self, name: str, fullname: str) -> None:
-        """These resolve for now, though they're private and we do not guarantee this."""
-        assert getattr(git, name) is importlib.import_module(fullname)
 
-    def test_private_module_alias_import_resolves(self, name: str, fullname: str) -> None:
-        exec(f"from git import {name}")
-        assert locals()[name] is importlib.import_module(fullname)
-
-    def test_private_module_alias_access_warns(self, name: str, fullname: str) -> None:
-        with pytest.deprecated_call() as ctx:
-            getattr(git, name)
-
-        assert len(ctx) == 1
-        message = str(ctx[0].message)
-        assert message.endswith(f"Use {fullname} instead.")
-
-    def test_private_module_alias_import_warns(self, name: str, fullname: str) -> None:
-        with pytest.deprecated_call() as ctx:
-            exec(f"from git import {name}")
-
-        message = str(ctx[0].message)
-        assert message.endswith(f"Use {fullname} instead.")
+def test_private_module_alias_access_on_git_module() -> None:
+    """Private alias access works, warns, and except for util is a mypy error."""
+    with pytest.deprecated_call() as ctx:
+        assert (
+            git.head,  # type: ignore[attr-defined]
+            git.log,  # type: ignore[attr-defined]
+            git.reference,  # type: ignore[attr-defined]
+            git.symbolic,  # type: ignore[attr-defined]
+            git.tag,  # type: ignore[attr-defined]
+            git.base,  # type: ignore[attr-defined]
+            git.fun,  # type: ignore[attr-defined]
+            git.typ,  # type: ignore[attr-defined]
+            git.util,
+        ) == _MODULE_ALIAS_TARGETS
+
+    messages = [str(w.message) for w in ctx]
+    for target, message in zip(_MODULE_ALIAS_TARGETS[:-1], messages[:-1], strict=True):
+        assert message.endswith(f"Use {target.__name__} instead.")
+
+    util_message = messages[-1]
+    assert "git.util" in util_message
+    assert "git.index.util" in util_message
+    assert "should not be relied on" in util_message
+
+
+def test_private_module_alias_import_from_git_module() -> None:
+    """Private alias import works, warns, and except for util is a mypy error."""
+    with pytest.deprecated_call() as ctx:
+        from git import head  # type: ignore[attr-defined]
+        from git import log  # type: ignore[attr-defined]
+        from git import reference  # type: ignore[attr-defined]
+        from git import symbolic  # type: ignore[attr-defined]
+        from git import tag  # type: ignore[attr-defined]
+        from git import base  # type: ignore[attr-defined]
+        from git import fun  # type: ignore[attr-defined]
+        from git import typ  # type: ignore[attr-defined]
+        from git import util
+
+    assert (
+        head,
+        log,
+        reference,
+        symbolic,
+        tag,
+        base,
+        fun,
+        typ,
+        util,
+    ) == _MODULE_ALIAS_TARGETS
+
+    # FIXME: This fails because, with imports, multiple consecutive accesses may occur.
+    # In practice, with CPython, it is always exactly two accesses, the first from the
+    # equivalent of a hasattr, and the second to fetch the attribute intentionally.
+    messages = [str(w.message) for w in ctx]
+    for target, message in zip(_MODULE_ALIAS_TARGETS[:-1], messages[:-1], strict=True):
+        assert message.endswith(f"Use {target.__name__} instead.")
+
+    util_message = messages[-1]
+    assert "git.util" in util_message
+    assert "git.index.util" in util_message
+    assert "should not be relied on" in util_message

From 45c128bcd82cd278d7926425179503c22ce1271c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 15:50:39 -0400
Subject: [PATCH 728/994] Finish reorganizing; fix assertion for duplicated
 messages

To support the changes, this adds typing-extensions as a test
dependency, since we are now importing from it in a test module.
But this should probably be required and used conditionally based
on whether the Python version has assert_type in its typing module.
---
 test-requirements.txt               |  1 +
 test/deprecation/test_attributes.py | 98 ++++++++++++++---------------
 2 files changed, 50 insertions(+), 49 deletions(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index e1f5e2ed4..106b46aee 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -8,3 +8,4 @@ pytest-cov
 pytest-instafail
 pytest-mock
 pytest-sugar
+typing-extensions
diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 97aa9bc50..35e2e48bb 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -4,61 +4,72 @@
 checks static typing of the code under test. (Running pytest checks dynamic behavior.)
 """
 
-import importlib
+from itertools import groupby
 from typing import Type
 
 import pytest
+from typing_extensions import assert_type
 
 import git
 
 
-def test_cannot_get_undefined() -> None:
+def test_cannot_access_undefined() -> None:
+    """Accessing a bogus attribute in git remains both a dynamic and static error."""
     with pytest.raises(AttributeError):
         git.foo  # type: ignore[attr-defined]
 
 
 def test_cannot_import_undefined() -> None:
+    """Importing a bogus attribute from git remains both a dynamic and static error."""
     with pytest.raises(ImportError):
         from git import foo  # type: ignore[attr-defined]  # noqa: F401
 
 
-def test_util_alias_members_resolve() -> None:
-    """git.index.util members can be accessed via git.util, and mypy recognizes it."""
-    gu_tfs = git.util.TemporaryFileSwap
-    from git.index.util import TemporaryFileSwap
+def test_util_alias_access() -> None:
+    """Accessing util in git works, warns, and mypy verifies it and its attributes."""
+    # The attribute access should succeed.
+    with pytest.deprecated_call() as ctx:
+        util = git.util
 
-    def accepts_tfs_type(t: Type[TemporaryFileSwap]) -> None:
-        pass
+    # There should be exactly one warning and it should have our util-specific message.
+    (message,) = [str(entry.message) for entry in ctx]
+    assert "git.util" in message
+    assert "git.index.util" in message
+    assert "should not be relied on" in message
 
-    def rejects_tfs_type(t: Type[git.Git]) -> None:
-        pass
+    # We check access through the util alias to the TemporaryFileSwap member, since it
+    # is slightly simpler to validate and reason about than the other public members,
+    # which are functions (specifically, higher-order functions for use as decorators).
+    from git.index.util import TemporaryFileSwap
 
-    # TODO: When typing_extensions is made a test dependency, use assert_type for this.
-    accepts_tfs_type(gu_tfs)
-    rejects_tfs_type(gu_tfs)  # type: ignore[arg-type]
+    assert_type(util.TemporaryFileSwap, Type[TemporaryFileSwap])
 
-    assert gu_tfs is TemporaryFileSwap
+    # This comes after the static assertion, just in case it would affect the inference.
+    assert util.TemporaryFileSwap is TemporaryFileSwap
 
 
-def test_util_alias_access_warns() -> None:
+def test_util_alias_import() -> None:
+    """Importing util from git works, warns, and mypy verifies it and its attributes."""
+    # The import should succeed.
     with pytest.deprecated_call() as ctx:
-        git.util
+        from git import util
 
-    assert len(ctx) == 1
-    message = str(ctx[0].message)
+    # There may be multiple warnings. In CPython there will be currently always be
+    # exactly two, possibly due to the equivalent of calling hasattr to do a pre-check
+    # prior to retrieving the attribute for actual use. However, all warnings should
+    # have the same message, and it should be our util-specific message.
+    (message,) = {str(entry.message) for entry in ctx}
     assert "git.util" in message
     assert "git.index.util" in message
     assert "should not be relied on" in message
 
+    # As above, we check access through the util alias to the TemporaryFileSwap member.
+    from git.index.util import TemporaryFileSwap
 
-def test_util_alias_import_warns() -> None:
-    with pytest.deprecated_call() as ctx:
-        from git import util  # noqa: F401
+    assert_type(util.TemporaryFileSwap, Type[TemporaryFileSwap])
 
-    message = str(ctx[0].message)
-    assert "git.util" in message
-    assert "git.index.util" in message
-    assert "should not be relied on" in message
+    # This comes after the static assertion, just in case it would affect the inference.
+    assert util.TemporaryFileSwap is TemporaryFileSwap
 
 
 # Split out util and have all its tests be separate, above.
@@ -71,12 +82,11 @@ def test_util_alias_import_warns() -> None:
     git.index.base,
     git.index.fun,
     git.index.typ,
-    git.index.util,
 )
 
 
-def test_private_module_alias_access_on_git_module() -> None:
-    """Private alias access works, warns, and except for util is a mypy error."""
+def test_private_module_alias_access() -> None:
+    """Non-util private alias access works, warns, but is a deliberate mypy error."""
     with pytest.deprecated_call() as ctx:
         assert (
             git.head,  # type: ignore[attr-defined]
@@ -87,21 +97,16 @@ def test_private_module_alias_access_on_git_module() -> None:
             git.base,  # type: ignore[attr-defined]
             git.fun,  # type: ignore[attr-defined]
             git.typ,  # type: ignore[attr-defined]
-            git.util,
         ) == _MODULE_ALIAS_TARGETS
 
+    # Each should have warned exactly once, and note what to use instead.
     messages = [str(w.message) for w in ctx]
-    for target, message in zip(_MODULE_ALIAS_TARGETS[:-1], messages[:-1], strict=True):
+    for target, message in zip(_MODULE_ALIAS_TARGETS, messages, strict=True):
         assert message.endswith(f"Use {target.__name__} instead.")
 
-    util_message = messages[-1]
-    assert "git.util" in util_message
-    assert "git.index.util" in util_message
-    assert "should not be relied on" in util_message
 
-
-def test_private_module_alias_import_from_git_module() -> None:
-    """Private alias import works, warns, and except for util is a mypy error."""
+def test_private_module_alias_import() -> None:
+    """Non-util private alias access works, warns, but is a deliberate mypy error."""
     with pytest.deprecated_call() as ctx:
         from git import head  # type: ignore[attr-defined]
         from git import log  # type: ignore[attr-defined]
@@ -111,7 +116,6 @@ def test_private_module_alias_import_from_git_module() -> None:
         from git import base  # type: ignore[attr-defined]
         from git import fun  # type: ignore[attr-defined]
         from git import typ  # type: ignore[attr-defined]
-        from git import util
 
     assert (
         head,
@@ -122,17 +126,13 @@ def test_private_module_alias_import_from_git_module() -> None:
         base,
         fun,
         typ,
-        util,
     ) == _MODULE_ALIAS_TARGETS
 
-    # FIXME: This fails because, with imports, multiple consecutive accesses may occur.
-    # In practice, with CPython, it is always exactly two accesses, the first from the
-    # equivalent of a hasattr, and the second to fetch the attribute intentionally.
-    messages = [str(w.message) for w in ctx]
-    for target, message in zip(_MODULE_ALIAS_TARGETS[:-1], messages[:-1], strict=True):
+    # Each import may warn multiple times. In CPython there will be currently always be
+    # exactly two warnings per import, possibly due to the equivalent of calling hasattr
+    # to do a pre-check prior to retrieving the attribute for actual use. However, for
+    # each import, all messages should be the same and should note what to use instead.
+    messages_with_duplicates = [str(w.message) for w in ctx]
+    messages = [message for message, _ in groupby(messages_with_duplicates)]
+    for target, message in zip(_MODULE_ALIAS_TARGETS, messages, strict=True):
         assert message.endswith(f"Use {target.__name__} instead.")
-
-    util_message = messages[-1]
-    assert "git.util" in util_message
-    assert "git.index.util" in util_message
-    assert "should not be relied on" in util_message

From 247dc15fd81ecc806be732d7f1ef0c12e26920d8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 15:55:57 -0400
Subject: [PATCH 729/994] Add imports so pyright recognizes refs and index

pyright still reports git.util as private, as it should.

(mypy does not, or does not by default, report private member
access. GitPython does not generally use pyright as part of
development at this time, but I am checking some code with it
during the process of writing the new tests.)
---
 test/deprecation/test_attributes.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 35e2e48bb..95feaaf8e 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -11,6 +11,14 @@
 from typing_extensions import assert_type
 
 import git
+import git.index.base
+import git.index.fun
+import git.index.typ
+import git.refs.head
+import git.refs.log
+import git.refs.reference
+import git.refs.symbolic
+import git.refs.tag
 
 
 def test_cannot_access_undefined() -> None:

From b05963c3749494f633117316a493ab2b179e0069 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 16:05:53 -0400
Subject: [PATCH 730/994] Expand and clarify test module docstring

About why there are so many separate mypy suppressions even when
they could be consolidated into a smaller number in some places.
---
 test/deprecation/test_attributes.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 95feaaf8e..218b9dd13 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -1,7 +1,11 @@
 """Tests for dynamic and static attribute errors in GitPython's top-level git module.
 
 Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
-checks static typing of the code under test. (Running pytest checks dynamic behavior.)
+checks static typing of the code under test. This is the reason for the many separate
+single-line attr-defined suppressions, so those should not be replaced with a smaller
+number of more broadly scoped suppressions, even where it is feasible to do so.
+
+Running pytest checks dynamic behavior as usual.
 """
 
 from itertools import groupby

From 074bbc72a84db7605f3136dc9f4b4ad822a3b481 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 16:08:37 -0400
Subject: [PATCH 731/994] Tiny import tweak

---
 test/deprecation/test_attributes.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 218b9dd13..1bcca44d5 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -8,7 +8,7 @@
 Running pytest checks dynamic behavior as usual.
 """
 
-from itertools import groupby
+import itertools
 from typing import Type
 
 import pytest
@@ -145,6 +145,6 @@ def test_private_module_alias_import() -> None:
     # to do a pre-check prior to retrieving the attribute for actual use. However, for
     # each import, all messages should be the same and should note what to use instead.
     messages_with_duplicates = [str(w.message) for w in ctx]
-    messages = [message for message, _ in groupby(messages_with_duplicates)]
+    messages = [message for message, _ in itertools.groupby(messages_with_duplicates)]
     for target, message in zip(_MODULE_ALIAS_TARGETS, messages, strict=True):
         assert message.endswith(f"Use {target.__name__} instead.")

From 18608e472535149e542cc30ff95755ed962c9156 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 16:12:59 -0400
Subject: [PATCH 732/994] Pick a better name for _MODULE_ALIAS_TARGETS

And add a docstring to document it, mainly to clarify that util is
intentionally omitted from that constant.
---
 test/deprecation/test_attributes.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 1bcca44d5..6e98a5e09 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -85,7 +85,7 @@ def test_util_alias_import() -> None:
 
 
 # Split out util and have all its tests be separate, above.
-_MODULE_ALIAS_TARGETS = (
+_PRIVATE_MODULE_ALIAS_TARGETS = (
     git.refs.head,
     git.refs.log,
     git.refs.reference,
@@ -95,6 +95,7 @@ def test_util_alias_import() -> None:
     git.index.fun,
     git.index.typ,
 )
+"""Targets of private aliases in the git module to some modules, not including util."""
 
 
 def test_private_module_alias_access() -> None:
@@ -109,11 +110,11 @@ def test_private_module_alias_access() -> None:
             git.base,  # type: ignore[attr-defined]
             git.fun,  # type: ignore[attr-defined]
             git.typ,  # type: ignore[attr-defined]
-        ) == _MODULE_ALIAS_TARGETS
+        ) == _PRIVATE_MODULE_ALIAS_TARGETS
 
     # Each should have warned exactly once, and note what to use instead.
     messages = [str(w.message) for w in ctx]
-    for target, message in zip(_MODULE_ALIAS_TARGETS, messages, strict=True):
+    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages, strict=True):
         assert message.endswith(f"Use {target.__name__} instead.")
 
 
@@ -138,7 +139,7 @@ def test_private_module_alias_import() -> None:
         base,
         fun,
         typ,
-    ) == _MODULE_ALIAS_TARGETS
+    ) == _PRIVATE_MODULE_ALIAS_TARGETS
 
     # Each import may warn multiple times. In CPython there will be currently always be
     # exactly two warnings per import, possibly due to the equivalent of calling hasattr
@@ -146,5 +147,5 @@ def test_private_module_alias_import() -> None:
     # each import, all messages should be the same and should note what to use instead.
     messages_with_duplicates = [str(w.message) for w in ctx]
     messages = [message for message, _ in itertools.groupby(messages_with_duplicates)]
-    for target, message in zip(_MODULE_ALIAS_TARGETS, messages, strict=True):
+    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages, strict=True):
         assert message.endswith(f"Use {target.__name__} instead.")

From 1f290f17943be338bb79a54ce1bef21e90da4402 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 16:36:22 -0400
Subject: [PATCH 733/994] Use typing_extensions only if needed

This makes the typing-extensions test dependency < 3.11 only, and
conditionally imports assert_type from typing or typing_extensions
depending on the Python version.
---
 test-requirements.txt               | 2 +-
 test/deprecation/test_attributes.py | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/test-requirements.txt b/test-requirements.txt
index 106b46aee..75e9e81fa 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -8,4 +8,4 @@ pytest-cov
 pytest-instafail
 pytest-mock
 pytest-sugar
-typing-extensions
+typing-extensions ; python_version < "3.11"
diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 6e98a5e09..2150186f9 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -9,8 +9,14 @@
 """
 
 import itertools
+import sys
 from typing import Type
 
+if sys.version_info >= (3, 11):
+    from typing import assert_type
+else:
+    from typing_extensions import assert_type
+
 import pytest
 from typing_extensions import assert_type
 

From 7a4f7eb092fbf68b058de38ee2df193c86d4e34e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 17:07:32 -0400
Subject: [PATCH 734/994] Fix zip calls

This omits strict=True, which is only supported in Python 3.10 and
later, and instead explicitly asserts that the arguments are the
same length (which is arguably better for its explicitness anyway).
---
 test/deprecation/test_attributes.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index 2150186f9..cd26f602b 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -120,7 +120,10 @@ def test_private_module_alias_access() -> None:
 
     # Each should have warned exactly once, and note what to use instead.
     messages = [str(w.message) for w in ctx]
-    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages, strict=True):
+
+    assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
+
+    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages):
         assert message.endswith(f"Use {target.__name__} instead.")
 
 
@@ -153,5 +156,8 @@ def test_private_module_alias_import() -> None:
     # each import, all messages should be the same and should note what to use instead.
     messages_with_duplicates = [str(w.message) for w in ctx]
     messages = [message for message, _ in itertools.groupby(messages_with_duplicates)]
-    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages, strict=True):
+
+    assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
+
+    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages):
         assert message.endswith(f"Use {target.__name__} instead.")

From 5977a6ec9e1646ac94514428a0ad2363be8da2f9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 17:19:39 -0400
Subject: [PATCH 735/994] Fix (and improve wording) of docstrings

---
 test/deprecation/test_attributes.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index cd26f602b..e4fb39975 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -105,7 +105,7 @@ def test_util_alias_import() -> None:
 
 
 def test_private_module_alias_access() -> None:
-    """Non-util private alias access works, warns, but is a deliberate mypy error."""
+    """Non-util private alias access works but warns and is a deliberate mypy error."""
     with pytest.deprecated_call() as ctx:
         assert (
             git.head,  # type: ignore[attr-defined]
@@ -128,7 +128,7 @@ def test_private_module_alias_access() -> None:
 
 
 def test_private_module_alias_import() -> None:
-    """Non-util private alias access works, warns, but is a deliberate mypy error."""
+    """Non-util private alias import works but warns and is a deliberate mypy error."""
     with pytest.deprecated_call() as ctx:
         from git import head  # type: ignore[attr-defined]
         from git import log  # type: ignore[attr-defined]

From 5b1fa580400c386932eb9f66c568e4e0090e2779 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 17:46:57 -0400
Subject: [PATCH 736/994] Remove extra import "from typing_extensions"

---
 test/deprecation/test_attributes.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_attributes.py
index e4fb39975..eb909b299 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_attributes.py
@@ -18,7 +18,6 @@
     from typing_extensions import assert_type
 
 import pytest
-from typing_extensions import assert_type
 
 import git
 import git.index.base

From a07be0e35118874f682fa2e2be3b793170bf4853 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:12:19 -0400
Subject: [PATCH 737/994] Start on test_compat

And rename test_attributes to test_toplevel accordingly.
---
 test/deprecation/test_compat.py               | 33 +++++++++++++++++++
 .../{test_attributes.py => test_toplevel.py}  |  6 ++--
 2 files changed, 36 insertions(+), 3 deletions(-)
 create mode 100644 test/deprecation/test_compat.py
 rename test/deprecation/{test_attributes.py => test_toplevel.py} (95%)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
new file mode 100644
index 000000000..dd2f0b0c2
--- /dev/null
+++ b/test/deprecation/test_compat.py
@@ -0,0 +1,33 @@
+"""Tests for dynamic and static errors and warnings in GitPython's git.compat module.
+
+These tests verify that the is_<platform> aliases are available, and are even listed in
+the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized)
+attribute access is still an error both at runtime and with mypy. This is similar to
+some of the tests in test_toplevel, but the situation being tested here is simpler
+because it does not involve unintuitive module aliasing or import behavior. So this only
+tests attribute access, not "from" imports (whose behavior can be intuitively inferred).
+"""
+
+import os
+import sys
+
+import pytest
+
+import git.compat
+
+
+_MESSAGE_LEADER = "{} and other is_<platform> aliases are deprecated."
+
+
+def test_cannot_access_undefined() -> None:
+    """Accessing a bogus attribute in git.compat remains a dynamic and static error."""
+    with pytest.raises(AttributeError):
+        git.compat.foo  # type: ignore[attr-defined]
+
+
+def test_is_win() -> None:
+    with pytest.deprecated_call() as ctx:
+        value = git.compat.is_win
+    (message,) = [str(entry.message) for entry in ctx]  # Exactly one message.
+    assert message.startswith(_MESSAGE_LEADER.format("git.compat.is_win"))
+    assert value == (os.name == "nt")
diff --git a/test/deprecation/test_attributes.py b/test/deprecation/test_toplevel.py
similarity index 95%
rename from test/deprecation/test_attributes.py
rename to test/deprecation/test_toplevel.py
index eb909b299..2a662127b 100644
--- a/test/deprecation/test_attributes.py
+++ b/test/deprecation/test_toplevel.py
@@ -1,4 +1,4 @@
-"""Tests for dynamic and static attribute errors in GitPython's top-level git module.
+"""Tests for dynamic and static errors and warnings in GitPython's top-level git module.
 
 Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
 checks static typing of the code under test. This is the reason for the many separate
@@ -31,13 +31,13 @@
 
 
 def test_cannot_access_undefined() -> None:
-    """Accessing a bogus attribute in git remains both a dynamic and static error."""
+    """Accessing a bogus attribute in git remains a dynamic and static error."""
     with pytest.raises(AttributeError):
         git.foo  # type: ignore[attr-defined]
 
 
 def test_cannot_import_undefined() -> None:
-    """Importing a bogus attribute from git remains both a dynamic and static error."""
+    """Importing a bogus attribute from git remains a dynamic and static error."""
     with pytest.raises(ImportError):
         from git import foo  # type: ignore[attr-defined]  # noqa: F401
 

From d4917d0a326d935ee0d6728af3268e9ece8b09df Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:19:36 -0400
Subject: [PATCH 738/994] Expand to test all three is_<platform> aliases

---
 test/deprecation/test_compat.py | 30 +++++++++++++++++++++++++-----
 1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index dd2f0b0c2..6d2d87a39 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -25,9 +25,29 @@ def test_cannot_access_undefined() -> None:
         git.compat.foo  # type: ignore[attr-defined]
 
 
-def test_is_win() -> None:
+def test_is_platform() -> None:
+    """The is_<platform> aliases work, warn, and mypy accepts code accessing them."""
+    fully_qualified_names = [
+        "git.compat.is_win",
+        "git.compat.is_posix",
+        "git.compat.is_darwin",
+    ]
+
     with pytest.deprecated_call() as ctx:
-        value = git.compat.is_win
-    (message,) = [str(entry.message) for entry in ctx]  # Exactly one message.
-    assert message.startswith(_MESSAGE_LEADER.format("git.compat.is_win"))
-    assert value == (os.name == "nt")
+        is_win = git.compat.is_win
+        is_posix = git.compat.is_posix
+        is_darwin = git.compat.is_darwin
+
+    messages = [str(entry.message) for entry in ctx]
+    assert len(messages) == 3
+
+    for fullname, message in zip(fully_qualified_names, messages):
+        assert message.startswith(_MESSAGE_LEADER.format(fullname))
+
+    # These exactly reproduce the expressions in the code under test, so they are not
+    # good for testing that the values are correct. Instead, the purpose of this test is
+    # to ensure that any dynamic machinery put in place in git.compat to cause warnings
+    # to be issued does not get in the way of the intended values being accessed.
+    assert is_win == (os.name == "nt")
+    assert is_posix == (os.name == "posix")
+    assert is_darwin == (sys.platform == "darwin")

From f4e5f423f019c7d04798c896118f7fd48b8c3155 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:22:30 -0400
Subject: [PATCH 739/994] Slightly improve docstrings

---
 test/deprecation/test_compat.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 6d2d87a39..45d631e37 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -1,7 +1,7 @@
 """Tests for dynamic and static errors and warnings in GitPython's git.compat module.
 
-These tests verify that the is_<platform> aliases are available, and are even listed in
-the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized)
+These tests verify that the is_<platform> attributes are available, and are even listed
+in the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized)
 attribute access is still an error both at runtime and with mypy. This is similar to
 some of the tests in test_toplevel, but the situation being tested here is simpler
 because it does not involve unintuitive module aliasing or import behavior. So this only
@@ -15,8 +15,8 @@
 
 import git.compat
 
-
 _MESSAGE_LEADER = "{} and other is_<platform> aliases are deprecated."
+"""Form taken by the beginning of the warnings issues for is_<platform> access."""
 
 
 def test_cannot_access_undefined() -> None:
@@ -26,7 +26,7 @@ def test_cannot_access_undefined() -> None:
 
 
 def test_is_platform() -> None:
-    """The is_<platform> aliases work, warn, and mypy accepts code accessing them."""
+    """The is_<platform> attributes work, warn, and mypy accepts code accessing them."""
     fully_qualified_names = [
         "git.compat.is_win",
         "git.compat.is_posix",

From d54f851d52f4217e904cbd163032aabbf33ec394 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:27:55 -0400
Subject: [PATCH 740/994] Add test of dir() on git.compat

---
 test/deprecation/test_compat.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 45d631e37..08911d17c 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -51,3 +51,17 @@ def test_is_platform() -> None:
     assert is_win == (os.name == "nt")
     assert is_posix == (os.name == "posix")
     assert is_darwin == (sys.platform == "darwin")
+
+
+def test_dir() -> None:
+    """dir() on git.compat lists attributes meant to be public, even if deprecated."""
+    expected = {
+        "defenc",
+        "safe_decode",
+        "safe_encode",
+        "win_encode",
+        "is_darwin",
+        "is_win",
+        "is_posix",
+    }
+    assert expected <= set(dir(git.compat))

From aaf046aba4b46fbe0dddf8c10d31f42b6c4d7c57 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:28:05 -0400
Subject: [PATCH 741/994] Add static type assertions to is_platform test

---
 test/deprecation/test_compat.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 08911d17c..c3cc5b0dd 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -11,6 +11,11 @@
 import os
 import sys
 
+if sys.version_info >= (3, 11):
+    from typing import assert_type
+else:
+    from typing_extensions import assert_type
+
 import pytest
 
 import git.compat
@@ -38,6 +43,10 @@ def test_is_platform() -> None:
         is_posix = git.compat.is_posix
         is_darwin = git.compat.is_darwin
 
+    assert_type(is_win, bool)
+    assert_type(is_posix, bool)
+    assert_type(is_darwin, bool)
+
     messages = [str(entry.message) for entry in ctx]
     assert len(messages) == 3
 

From 84d734d5b88cb8a03ab723f92bf83593d53d030f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:29:23 -0400
Subject: [PATCH 742/994] Refactor test_compat.test_dir for clarity

---
 test/deprecation/test_compat.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index c3cc5b0dd..6e42d0209 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -64,7 +64,7 @@ def test_is_platform() -> None:
 
 def test_dir() -> None:
     """dir() on git.compat lists attributes meant to be public, even if deprecated."""
-    expected = {
+    expected_subset = {
         "defenc",
         "safe_decode",
         "safe_encode",
@@ -73,4 +73,5 @@ def test_dir() -> None:
         "is_win",
         "is_posix",
     }
-    assert expected <= set(dir(git.compat))
+    actual = set(dir(git.compat))
+    assert expected_subset <= actual

From 3a621b38ee98a1d1413a12fcb68aae17d102f396 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:45:31 -0400
Subject: [PATCH 743/994] Add top-level dir() tests

---
 test/deprecation/test_toplevel.py | 49 +++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index 2a662127b..f74f09457 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -160,3 +160,52 @@ def test_private_module_alias_import() -> None:
 
     for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages):
         assert message.endswith(f"Use {target.__name__} instead.")
+
+
+def test_dir_contains_public_attributes() -> None:
+    """All public attributes of the git module are present when dir() is called on it.
+
+    This is naturally the case, but some ways of adding dynamic attribute access
+    behavior can change it, especially if __dir__ is defined but care is not taken to
+    preserve the contents that should already be present.
+
+    Note that dir() should usually automatically list non-public attributes if they are
+    actually "physically" present as well, so the approach taken here to test it should
+    not be reproduced if __dir__ is added (instead, a call to globals() could be used,
+    as its keys list the automatic values).
+    """
+    expected_subset = set(git.__all__)
+    actual = set(dir(git))
+    assert expected_subset <= actual
+
+
+def test_dir_does_not_contain_util() -> None:
+    """The util attribute is absent from the dir() of git.
+
+    Because this behavior is less confusing than including it, where its meaning would
+    be assumed by users examining the dir() for what is available.
+    """
+    assert "util" not in dir(git)
+
+
+def test_dir_does_not_contain_private_module_aliases() -> None:
+    """Names from inside index and refs only pretend to be there and are not in dir().
+
+    The reason for omitting these is not that they are private, since private members
+    are usually included in dir() when actually present. Instead, these are only sort
+    of even there, no longer being imported and only being resolved dynamically for the
+    time being. In addition, it would be confusing to list these because doing so would
+    obscure the module structure of GitPython.
+    """
+    expected_absent = {
+        "head",
+        "log",
+        "reference",
+        "symbolic",
+        "tag",
+        "base",
+        "fun",
+        "typ",
+    }
+    actual = set(dir(git))
+    assert not (expected_absent & actual), "They should be completely disjoint."

From 05e0878aef38a5fb1cb3d5860b3649cf7c1d4fda Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 18:47:27 -0400
Subject: [PATCH 744/994] Remove old comment meant as todo (that was done)

---
 test/deprecation/test_toplevel.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index f74f09457..fe7045d46 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -89,7 +89,6 @@ def test_util_alias_import() -> None:
     assert util.TemporaryFileSwap is TemporaryFileSwap
 
 
-# Split out util and have all its tests be separate, above.
 _PRIVATE_MODULE_ALIAS_TARGETS = (
     git.refs.head,
     git.refs.log,

From 3fe2f15d218744496e4af77b6a7926791480adfe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 19:01:24 -0400
Subject: [PATCH 745/994] Test that top-level aliases point to modules with
 normal __name__

---
 test/deprecation/test_toplevel.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index fe7045d46..135cc53cc 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -102,6 +102,26 @@ def test_util_alias_import() -> None:
 """Targets of private aliases in the git module to some modules, not including util."""
 
 
+_PRIVATE_MODULE_ALIAS_TARGET_NAMES = (
+    "git.refs.head",
+    "git.refs.log",
+    "git.refs.reference",
+    "git.refs.symbolic",
+    "git.refs.tag",
+    "git.index.base",
+    "git.index.fun",
+    "git.index.typ",
+)
+"""Expected ``__name__`` attributes of targets of private aliases in the git module."""
+
+
+def test_alias_target_module_names_are_by_location() -> None:
+    """The aliases are weird, but their targets are normal, even in ``__name__``."""
+    actual = [module.__name__ for module in _PRIVATE_MODULE_ALIAS_TARGETS]
+    expected = list(_PRIVATE_MODULE_ALIAS_TARGET_NAMES)
+    assert actual == expected
+
+
 def test_private_module_alias_access() -> None:
     """Non-util private alias access works but warns and is a deliberate mypy error."""
     with pytest.deprecated_call() as ctx:

From 246cc1703f69e8eba791915bb025945b03abc86b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 19:02:56 -0400
Subject: [PATCH 746/994] Use names directly on other tests

The tests are written broadly (per the style elsewhere in this test
suite), but narrowing the message-checking tests in this specific
way has the further advantage that the logic of the code under test
will be less reflected in the logic of the tests, so that bugs are
less likely to be missed by being duplicated across code and tests.
---
 test/deprecation/test_toplevel.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index 135cc53cc..16c41d4e8 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -141,8 +141,8 @@ def test_private_module_alias_access() -> None:
 
     assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
 
-    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages):
-        assert message.endswith(f"Use {target.__name__} instead.")
+    for fullname, message in zip(_PRIVATE_MODULE_ALIAS_TARGET_NAMES, messages):
+        assert message.endswith(f"Use {fullname} instead.")
 
 
 def test_private_module_alias_import() -> None:
@@ -177,8 +177,8 @@ def test_private_module_alias_import() -> None:
 
     assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
 
-    for target, message in zip(_PRIVATE_MODULE_ALIAS_TARGETS, messages):
-        assert message.endswith(f"Use {target.__name__} instead.")
+    for fullname, message in zip(_PRIVATE_MODULE_ALIAS_TARGET_NAMES, messages):
+        assert message.endswith(f"Use {fullname} instead.")
 
 
 def test_dir_contains_public_attributes() -> None:

From d7b6b31f632593bf9e280fbb2be87dd4e16ef7c5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 19:07:35 -0400
Subject: [PATCH 747/994] Fix a small docstring typo

---
 test/deprecation/test_compat.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 6e42d0209..0da5462b2 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -21,7 +21,7 @@
 import git.compat
 
 _MESSAGE_LEADER = "{} and other is_<platform> aliases are deprecated."
-"""Form taken by the beginning of the warnings issues for is_<platform> access."""
+"""Form taken by the beginning of the warnings issued for is_<platform> access."""
 
 
 def test_cannot_access_undefined() -> None:

From 96089c82c0d8982935bbd3326ccfea36ce72e43b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 19:17:15 -0400
Subject: [PATCH 748/994] Improve description in test module docstrings

---
 test/deprecation/test_compat.py   | 2 +-
 test/deprecation/test_toplevel.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 0da5462b2..5007fa1cc 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -1,4 +1,4 @@
-"""Tests for dynamic and static errors and warnings in GitPython's git.compat module.
+"""Tests for dynamic and static characteristics of git.compat module attributes.
 
 These tests verify that the is_<platform> attributes are available, and are even listed
 in the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized)
diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index 16c41d4e8..398938616 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -1,4 +1,4 @@
-"""Tests for dynamic and static errors and warnings in GitPython's top-level git module.
+"""Tests for dynamic and static characteristics of top-level git module attributes.
 
 Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
 checks static typing of the code under test. This is the reason for the many separate

From a0ef53778d4ae665474ebd96bd25ebbb340a8a16 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 23:12:12 -0400
Subject: [PATCH 749/994] Start on test_types

---
 test/deprecation/test_types.py | 39 ++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 test/deprecation/test_types.py

diff --git a/test/deprecation/test_types.py b/test/deprecation/test_types.py
new file mode 100644
index 000000000..a2ac45829
--- /dev/null
+++ b/test/deprecation/test_types.py
@@ -0,0 +1,39 @@
+"""Tests for dynamic and static characteristics of git.types module attributes."""
+
+import sys
+
+if sys.version_info >= (3, 8):
+    from typing import Literal
+else:
+    from typing_extensions import Literal
+
+import pytest
+
+import git.types
+
+
+def test_cannot_access_undefined() -> None:
+    """Accessing a bogus attribute in git.types remains a dynamic and static error."""
+    with pytest.raises(AttributeError):
+        git.types.foo  # type: ignore[attr-defined]
+
+
+def test_lit_commit_ish() -> None:
+    """ """
+    # It would be fine to test attribute access rather than a "from" import. But a
+    # "from" import is more likely to appear in actual usage, so it is used here.
+    with pytest.deprecated_call() as ctx:
+        from git.types import Lit_commit_ish
+
+    # As noted in test_toplevel.test_util_alias_import, there may be multiple warnings,
+    # but all with the same message.
+    (message,) = {str(entry.message) for entry in ctx}
+    assert "Lit_commit_ish is deprecated." in message
+    assert 'Literal["commit", "tag", "blob", "tree"]' in message, "Has old definition."
+    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]
+
+    # It should be as documented (even though deliberately unusable in static checks).
+    assert Lit_commit_ish == Literal["commit", "tag"]

From 52e7360cad2ebde77a1302b205eb7cf9182a75c2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 23:13:53 -0400
Subject: [PATCH 750/994] Explain substring assertions in test_toplevel

---
 test/deprecation/test_toplevel.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index 398938616..54dc8e358 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -76,9 +76,9 @@ def test_util_alias_import() -> None:
     # prior to retrieving the attribute for actual use. However, all warnings should
     # have the same message, and it should be our util-specific message.
     (message,) = {str(entry.message) for entry in ctx}
-    assert "git.util" in message
-    assert "git.index.util" in message
-    assert "should not be relied on" in message
+    assert "git.util" in message, "Has alias."
+    assert "git.index.util" in message, "Has target."
+    assert "should not be relied on" in message, "Distinct from other messages."
 
     # As above, we check access through the util alias to the TemporaryFileSwap member.
     from git.index.util import TemporaryFileSwap

From e3675a086fe6ddfb6f8e4050497d47a74e851ed7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 23:17:55 -0400
Subject: [PATCH 751/994] Expand Lit_commit_ish test name and write docstring

---
 test/deprecation/test_types.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_types.py b/test/deprecation/test_types.py
index a2ac45829..419435964 100644
--- a/test/deprecation/test_types.py
+++ b/test/deprecation/test_types.py
@@ -18,8 +18,8 @@ def test_cannot_access_undefined() -> None:
         git.types.foo  # type: ignore[attr-defined]
 
 
-def test_lit_commit_ish() -> None:
-    """ """
+def test_can_access_lit_commit_ish_but_it_is_not_usable() -> None:
+    """Lit_commit_ish_can be accessed, but warns and is an invalid type annotation."""
     # It would be fine to test attribute access rather than a "from" import. But a
     # "from" import is more likely to appear in actual usage, so it is used here.
     with pytest.deprecated_call() as ctx:

From 4857ff08fc9ae0183b65a4b94bd0813bd08a74b4 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 23:26:43 -0400
Subject: [PATCH 752/994] Clarify test_compat.test_dir

---
 test/deprecation/test_compat.py | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 5007fa1cc..6699a24d5 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -63,15 +63,19 @@ def test_is_platform() -> None:
 
 
 def test_dir() -> None:
-    """dir() on git.compat lists attributes meant to be public, even if deprecated."""
+    """dir() on git.compat includes all public attributes, even if deprecated.
+
+    As dir() usually does, it also has nonpublic attributes, which should also not be
+    removed by a custom __dir__ function, but those are less important to test.
+    """
     expected_subset = {
+        "is_win",
+        "is_posix",
+        "is_darwin",
         "defenc",
         "safe_decode",
         "safe_encode",
         "win_encode",
-        "is_darwin",
-        "is_win",
-        "is_posix",
     }
     actual = set(dir(git.compat))
     assert expected_subset <= actual

From 488cc13a5b125be886bb361342b8e4709c7944ba Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 23 Mar 2024 23:32:11 -0400
Subject: [PATCH 753/994] Add test of dir() on git.types

---
 test/deprecation/test_types.py | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/test/deprecation/test_types.py b/test/deprecation/test_types.py
index 419435964..cd53fa210 100644
--- a/test/deprecation/test_types.py
+++ b/test/deprecation/test_types.py
@@ -37,3 +37,30 @@ def test_can_access_lit_commit_ish_but_it_is_not_usable() -> None:
 
     # It should be as documented (even though deliberately unusable in static checks).
     assert Lit_commit_ish == Literal["commit", "tag"]
+
+
+def test_dir() -> None:
+    """dir() on git.types includes public names, even ``Lit_commit_ish``.
+
+    It also contains private names that we don't test. See test_compat.test_dir.
+    """
+    expected_subset = {
+        "PathLike",
+        "TBD",
+        "AnyGitObject",
+        "Tree_ish",
+        "Commit_ish",
+        "GitObjectTypeString",
+        "Lit_commit_ish",
+        "Lit_config_levels",
+        "ConfigLevels_Tup",
+        "CallableProgress",
+        "assert_never",
+        "Files_TD",
+        "Total_TD",
+        "HSH_TD",
+        "Has_Repo",
+        "Has_id_attribute",
+    }
+    actual = set(dir(git.types))
+    assert expected_subset <= actual

From 19b3c0820b607558b3bc52d3d9f3841295f04ac5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Mar 2024 01:31:33 -0400
Subject: [PATCH 754/994] Clarify comment about is_<platform> value assertions

---
 test/deprecation/test_compat.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 6699a24d5..22f733083 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -53,10 +53,10 @@ def test_is_platform() -> None:
     for fullname, message in zip(fully_qualified_names, messages):
         assert message.startswith(_MESSAGE_LEADER.format(fullname))
 
-    # These exactly reproduce the expressions in the code under test, so they are not
-    # good for testing that the values are correct. Instead, the purpose of this test is
-    # to ensure that any dynamic machinery put in place in git.compat to cause warnings
-    # to be issued does not get in the way of the intended values being accessed.
+    # These assertions exactly reproduce the expressions in the code under test, so they
+    # are not good for testing that the values are correct. Instead, their purpose is to
+    # ensure that any dynamic machinery put in place in git.compat to cause warnings to
+    # be issued does not get in the way of the intended values being accessed.
     assert is_win == (os.name == "nt")
     assert is_posix == (os.name == "posix")
     assert is_darwin == (sys.platform == "darwin")

From 28bd4a3f6e562eadc1018ee7c4d561a3c4352fb5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 20 Mar 2024 14:31:58 -0400
Subject: [PATCH 755/994] Issue warnings for some deprecated attributes of
 modules

---
 git/__init__.py | 97 ++++++++++++++++++++++++++++++++++++-------------
 git/compat.py   | 40 ++++++++++++++++++--
 git/types.py    | 39 +++++++++++++++-----
 3 files changed, 138 insertions(+), 38 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index a13030456..aa838d4f5 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -88,7 +88,10 @@
 
 __version__ = "git"
 
-from typing import List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
+from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
+
+if TYPE_CHECKING:
+    from types import ModuleType
 
 from gitdb.util import to_hex_sha
 
@@ -144,11 +147,6 @@
         SymbolicReference,
         Tag,
         TagReference,
-        head,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.head.
-        log,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.log.
-        reference,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.reference.
-        symbolic,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.symbolic.
-        tag,  # noqa: F401  # Nonpublic. May disappear! Use git.refs.tag.
     )
     from git.diff import (  # @NoMove
         INDEX,
@@ -169,21 +167,6 @@
         IndexEntry,
         IndexFile,
         StageType,
-        base,  # noqa: F401  # Nonpublic. May disappear! Use git.index.base.
-        fun,  # noqa: F401  # Nonpublic. May disappear! Use git.index.fun.
-        typ,  # noqa: F401  # Nonpublic. May disappear! Use git.index.typ.
-        #
-        # NOTE: The expression `git.util` evaluates to git.index.util, and the import
-        # `from git import util` imports git.index.util, NOT git.util. It may not be
-        # feasible to change this until the next major version, to avoid breaking code
-        # inadvertently relying on it. If git.index.util really is what you want, use or
-        # import from that name, to avoid confusion. To use the "real" git.util module,
-        # write `from git.util import ...`, or access it as `sys.modules["git.util"]`.
-        # (This differs from other historical indirect-submodule imports that are
-        # unambiguously nonpublic and are subject to immediate removal. Here, the public
-        # git.util module, even though different, makes it less discoverable that the
-        # expression `git.util` refers to a non-public attribute of the git module.)
-        util,  # noqa: F401
     )
     from git.util import (  # @NoMove
         Actor,
@@ -196,7 +179,72 @@
 except GitError as _exc:
     raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
 
+
+# NOTE: The expression `git.util` evaluates to git.index.util and `from git import util`
+# imports git.index.util, NOT git.util. It may not be feasible to change this until the
+# next major version, to avoid breaking code inadvertently relying on it.
+#
+# - If git.index.util *is* what you want, use or import from that, to avoid confusion.
+#
+# - To use the "real" git.util module, write `from git.util import ...`, or if necessary
+#   access it as `sys.modules["git.util"]`.
+#
+# (This differs from other indirect-submodule imports that are unambiguously non-public
+# and subject to immediate removal. Here, the public git.util module, though different,
+# makes less discoverable that the expression `git.util` refers to a non-public
+# attribute of the git module.)
+#
+# This had come about by a wildcard import. Now that all intended imports are explicit,
+# the intuitive but potentially incompatible binding occurs due to the usual rules for
+# Python submodule bindings. So for now we delete that and let __getattr__ handle it.
+#
+del util  # type: ignore[name-defined]  # noqa: F821
+
+
+def _warned_import(message: str, fullname: str) -> "ModuleType":
+    import importlib
+    import warnings
+
+    warnings.warn(message, DeprecationWarning, stacklevel=3)
+    return importlib.import_module(fullname)
+
+
+def _getattr(name: str) -> Any:
+    # TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
+
+    if name == "util":
+        return _warned_import(
+            "The expression `git.util` and the import `from git import util` actually "
+            "reference git.index.util, and not the git.util module accessed in "
+            '`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
+            "confusing behavior is currently preserved for compatibility, but may be "
+            "changed in the future and should not be relied on.",
+            fullname="git.index.util",
+        )
+
+    for names, prefix in (
+        ({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
+        ({"base", "fun", "typ"}, "git.index"),
+    ):
+        if name not in names:
+            continue
+
+        fullname = f"{prefix}.{name}"
+
+        return _warned_import(
+            f"{__name__}.{name} is a private alias of {fullname} and subject to "
+            f"immediate removal. Use {fullname} instead.",
+            fullname=fullname,
+        )
+
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+if not TYPE_CHECKING:  # Preserve static checking for undefined/misspelled attributes.
+    __getattr__ = _getattr
+
 # { Initialize git executable path
+
 GIT_OK = None
 
 
@@ -232,12 +280,9 @@ def refresh(path: Optional[PathLike] = None) -> None:
     GIT_OK = True
 
 
-# } END initialize git executable path
-
-
-#################
 try:
     refresh()
 except Exception as _exc:
     raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc
-#################
+
+# } END initialize git executable path
diff --git a/git/compat.py b/git/compat.py
index 4ede8c985..f1b95a80e 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -23,7 +23,9 @@
     AnyStr,
     Dict,  # noqa: F401
     IO,  # noqa: F401
+    List,
     Optional,
+    TYPE_CHECKING,
     Tuple,  # noqa: F401
     Type,  # noqa: F401
     Union,
@@ -33,7 +35,39 @@
 # ---------------------------------------------------------------------------
 
 
-is_win = os.name == "nt"
+_deprecated_platform_aliases = {
+    "is_win": os.name == "nt",
+    "is_posix": os.name == "posix",
+    "is_darwin": sys.platform == "darwin",
+}
+
+
+def _getattr(name: str) -> Any:
+    try:
+        value = _deprecated_platform_aliases[name]
+    except KeyError:
+        raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
+
+    import warnings
+
+    warnings.warn(
+        f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
+        "Write the desired os.name or sys.platform check explicitly instead.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return value
+
+
+if not TYPE_CHECKING:  # Preserve static checking for undefined/misspelled attributes.
+    __getattr__ = _getattr
+
+
+def __dir__() -> List[str]:
+    return [*globals(), *_deprecated_platform_aliases]
+
+
+is_win: bool
 """Deprecated alias for ``os.name == "nt"`` to check for native Windows.
 
 This is deprecated because it is clearer to write out :attr:`os.name` or
@@ -45,7 +79,7 @@
     Cygwin, use ``sys.platform == "cygwin"``.
 """
 
-is_posix = os.name == "posix"
+is_posix: bool
 """Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
 
 This is deprecated because it clearer to write out :attr:`os.name` or
@@ -58,7 +92,7 @@
     (Darwin).
 """
 
-is_darwin = sys.platform == "darwin"
+is_darwin: bool
 """Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
 
 This is deprecated because it clearer to write out :attr:`os.name` or
diff --git a/git/types.py b/git/types.py
index a93ebdb4f..1be32de1d 100644
--- a/git/types.py
+++ b/git/types.py
@@ -7,11 +7,13 @@
     Any,
     Callable,
     Dict,
+    List,
     NoReturn,
     Optional,
     Sequence as Sequence,
     Tuple,
     TYPE_CHECKING,
+    Type,
     TypeVar,
     Union,
 )
@@ -127,21 +129,40 @@
 https://git-scm.com/docs/gitglossary#def_object_type
 """
 
-Lit_commit_ish = Literal["commit", "tag"]
-"""Deprecated. Type of literal strings identifying sometimes-commitish git object types.
+Lit_commit_ish: Type[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
-ambiguous and likely to be incorrect. Instead of this type:
+ambiguous and likely to be incorrect. This type has therefore been made a static type
+error to appear in annotations. It is preserved, with a deprecated status, to avoid
+introducing runtime errors in code that refers to it, but it should not be used.
+
+Instead of this type:
 
 * For the type of the string literals associated with :class:`Commit_ish`, use
   ``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
-  this type as currently defined.
+  this type as currently defined (but usable in statically checked type annotations).
 
 * For the type of all four string literals associated with :class:`AnyGitObject`, use
   :class:`GitObjectTypeString`. That is equivalent to the old definition of this type
-  prior to the bugfix.
+  prior to the bugfix (and is also usable in statically checked type annotations).
 """
 
+
+def _getattr(name: str) -> Any:
+    if name == "Lit_commit_ish":
+        return Literal["commit", "tag"]
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+if not TYPE_CHECKING:  # Preserve static checking for undefined/misspelled attributes.
+    __getattr__ = _getattr
+
+
+def __dir__() -> List[str]:
+    return [*globals(), "Lit_commit_ish"]
+
+
 # Config_levels ---------------------------------------------------------
 
 Lit_config_levels = Literal["system", "global", "user", "repository"]
@@ -188,12 +209,12 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception,
 
     :param inp:
         If all members are handled, the argument for `inp` will have the
-        :class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will
-        mismatch and cause a mypy error.
+        :class:`~typing.Never`/:class:`~typing.NoReturn` type.
+        Otherwise, the type will mismatch and cause a mypy error.
 
     :param raise_error:
-        If ``True``, will also raise :exc:`ValueError` with a general "unhandled
-        literal" message, or the exception object passed as `exc`.
+        If ``True``, will also raise :exc:`ValueError` with a general
+        "unhandled literal" message, or the exception object passed as `exc`.
 
     :param exc:
         It not ``None``, this should be an already-constructed exception object, to be

From dffa930a426747d9b30496fceb6efd621ab8795b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Mar 2024 01:38:26 -0400
Subject: [PATCH 756/994] Refine deprecated module attributes and their
 warnings

- In the top-level git module, import util from git.index so there
  is no ambiguity for tools in detecting which module the util
  attribute of the top-level git module (i.e., git.util in an
  *expression*) is, even though it's subsequently deleted (and then
  dynamically supplied when requested, in a way that is opaque to
  static type checkers due to being only when `not TYPE_CHECKING`).
  This seems to be necessary for some tools.

  Curiously, guarding the del statement under `not TYPE_CHECKING`
  seems *not* to be needed by any tools of any kind. It should
  still possibly be done, but that's not included in these changes.

- Add the missing deprecation warning for git.types.Lit_commit_ish.

- When importing the warnings module, do so with a top-level import
  as in the other GitPython modules that have long (and reasonably)
  done so.

  In git/__init__.py, there already had to be an import of
  importlib, which seemed like it should be done locally in case of
  delays. Neither the importlib module nor any of its submodules
  were already imported anywhere in GitPython, and the code that
  uses it will most often not be exercised. So there is a potential
  benefit to avoiding loading it when not needed.

  When writing a local import for that, I had included the warnings
  module as a local import as well. But this obscures the potential
  benefit of locally importing importlib, and could lead to
  ill-advised changes in the future based on the idea that the
  degree of justification to be local imports was the same for them
  both.
---
 git/__init__.py |  6 ++++--
 git/compat.py   |  3 +--
 git/types.py    | 17 ++++++++++++++---
 3 files changed, 19 insertions(+), 7 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index aa838d4f5..fd1b87707 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -93,6 +93,8 @@
 if TYPE_CHECKING:
     from types import ModuleType
 
+import warnings
+
 from gitdb.util import to_hex_sha
 
 from git.exc import (
@@ -167,6 +169,7 @@
         IndexEntry,
         IndexFile,
         StageType,
+        util,
     )
     from git.util import (  # @NoMove
         Actor,
@@ -198,12 +201,11 @@
 # the intuitive but potentially incompatible binding occurs due to the usual rules for
 # Python submodule bindings. So for now we delete that and let __getattr__ handle it.
 #
-del util  # type: ignore[name-defined]  # noqa: F821
+del util
 
 
 def _warned_import(message: str, fullname: str) -> "ModuleType":
     import importlib
-    import warnings
 
     warnings.warn(message, DeprecationWarning, stacklevel=3)
     return importlib.import_module(fullname)
diff --git a/git/compat.py b/git/compat.py
index f1b95a80e..d7d9a55a9 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -13,6 +13,7 @@
 import locale
 import os
 import sys
+import warnings
 
 from gitdb.utils.encoding import force_bytes, force_text  # noqa: F401
 
@@ -48,8 +49,6 @@ def _getattr(name: str) -> Any:
     except KeyError:
         raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
 
-    import warnings
-
     warnings.warn(
         f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
         "Write the desired os.name or sys.platform check explicitly instead.",
diff --git a/git/types.py b/git/types.py
index 1be32de1d..584450146 100644
--- a/git/types.py
+++ b/git/types.py
@@ -17,6 +17,7 @@
     TypeVar,
     Union,
 )
+import warnings
 
 if sys.version_info >= (3, 8):
     from typing import (
@@ -150,9 +151,19 @@
 
 
 def _getattr(name: str) -> Any:
-    if name == "Lit_commit_ish":
-        return Literal["commit", "tag"]
-    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+    if name != "Lit_commit_ish":
+        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+    warnings.warn(
+        "Lit_commit_ish is deprecated. It is currently defined as "
+        '`Literal["commit", "tag"]`, which should be used in its place if desired. It '
+        'had previously been defined as `Literal["commit", "tag", "blob", "tree"]`, '
+        "covering all four git object type strings including those that are never "
+        "commit-ish. For that, use the GitObjectTypeString type instead.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return Literal["commit", "tag"]
 
 
 if not TYPE_CHECKING:  # Preserve static checking for undefined/misspelled attributes.

From 7ab27c5bb1891af9eec7a99e33ea35e234e322d5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Mar 2024 19:31:00 -0400
Subject: [PATCH 757/994] Start on test module about Git.USE_SHELL and Git
 attributes

---
 test/deprecation/test_cmd_git.py | 168 +++++++++++++++++++++++++++++++
 1 file changed, 168 insertions(+)
 create mode 100644 test/deprecation/test_cmd_git.py

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
new file mode 100644
index 000000000..fc8bab6ee
--- /dev/null
+++ b/test/deprecation/test_cmd_git.py
@@ -0,0 +1,168 @@
+"""Tests for static and dynamic characteristics of Git class and instance attributes.
+
+Currently this all relates to the deprecated :class:`Git.USE_SHELL` class attribute,
+which can also be accessed through instances. Some tests directly verify its behavior,
+including deprecation warnings, while others verify that other aspects of attribute
+access are not inadvertently broken by mechanisms introduced to issue the warnings.
+"""
+
+import contextlib
+import sys
+from typing import Generator
+
+if sys.version_info >= (3, 11):
+    from typing import assert_type
+else:
+    from typing_extensions import assert_type
+
+import pytest
+
+from git.cmd import Git
+
+_USE_SHELL_DEPRECATED_FRAGMENT = "Git.USE_SHELL is deprecated"
+"""Text contained in all USE_SHELL deprecation warnings, and starting most of them."""
+
+_USE_SHELL_DANGEROUS_FRAGMENT = "Setting Git.USE_SHELL to True is unsafe and insecure"
+"""Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
+
+
+@pytest.fixture
+def reset_backing_attribute() -> Generator[None, None, None]:
+    """Fixture to reset the private ``_USE_SHELL`` attribute.
+
+    This is used to decrease the likelihood of state changes leaking out and affecting
+    other tests. But the goal is not to assert that ``_USE_SHELL`` is used, nor anything
+    about how or when it is used, which is an implementation detail subject to change.
+
+    This is possible but inelegant to do with pytest's monkeypatch fixture, which only
+    restores attributes that it has previously been used to change, create, or remove.
+    """
+    no_value = object()
+    try:
+        old_value = Git._USE_SHELL
+    except AttributeError:
+        old_value = no_value
+
+    yield
+
+    if old_value is no_value:
+        with contextlib.suppress(AttributeError):
+            del Git._USE_SHELL
+    else:
+        Git._USE_SHELL = old_value
+
+
+def test_cannot_access_undefined_on_git_class() -> None:
+    """Accessing a bogus attribute on the Git class remains a dynamic and static error.
+
+    This differs from Git instances, where most attribute names will dynamically
+    synthesize a "bound method" that runs a git subcommand when called.
+    """
+    with pytest.raises(AttributeError):
+        Git.foo  # type: ignore[attr-defined]
+
+
+def test_get_use_shell_on_class_default() -> None:
+    """USE_SHELL can be read as a class attribute, defaulting to False and warning."""
+    with pytest.deprecated_call() as ctx:
+        use_shell = Git.USE_SHELL
+
+    (message,) = [str(entry.message) for entry in ctx]  # Exactly one warning.
+    assert message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
+
+    assert_type(use_shell, bool)
+
+    # This comes after the static assertion, just in case it would affect the inference.
+    assert not use_shell
+
+
+# FIXME: More robustly check that each operation really issues exactly one deprecation
+# warning, even if this requires relying more on reset_backing_attribute doing its job.
+def test_use_shell_on_class(reset_backing_attribute) -> None:
+    """USE_SHELL can be written and re-read as a class attribute, always warning."""
+    # We assert in a "safe" order, using reset_backing_attribute only as a backstop.
+    with pytest.deprecated_call() as ctx:
+        Git.USE_SHELL = True
+        set_value = Git.USE_SHELL
+        Git.USE_SHELL = False
+        reset_value = Git.USE_SHELL
+
+    # The attribute should take on the values set to it.
+    assert set_value is True
+    assert reset_value is False
+
+    messages = [str(entry.message) for entry in ctx]
+    set_message, check_message, reset_message, recheck_message = messages
+
+    # Setting it to True should produce the special warning for that.
+    assert _USE_SHELL_DEPRECATED_FRAGMENT in set_message
+    assert set_message.startswith(_USE_SHELL_DANGEROUS_FRAGMENT)
+
+    # All other operations should produce a usual warning.
+    assert check_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
+    assert reset_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
+    assert recheck_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
+
+
+# FIXME: Test behavior on instances (where we can get but not set).
+
+# FIXME: Test behavior with multiprocessing (the attribute needs to pickle properly).
+
+
+_EXPECTED_DIR_SUBSET = {
+    "cat_file_all",
+    "cat_file_header",
+    "GIT_PYTHON_TRACE",
+    "USE_SHELL",  # The attribute we get deprecation warnings for.
+    "GIT_PYTHON_GIT_EXECUTABLE",
+    "refresh",
+    "is_cygwin",
+    "polish_url",
+    "check_unsafe_protocols",
+    "check_unsafe_options",
+    "AutoInterrupt",
+    "CatFileContentStream",
+    "__init__",
+    "__getattr__",
+    "set_persistent_git_options",
+    "working_dir",
+    "version_info",
+    "execute",
+    "environment",
+    "update_environment",
+    "custom_environment",
+    "transform_kwarg",
+    "transform_kwargs",
+    "__call__",
+    "_call_process",  # Not currently considered public, but unlikely to change.
+    "get_object_header",
+    "get_object_data",
+    "stream_object_data",
+    "clear_cache",
+}
+"""Some stable attributes dir() should include on the Git class and its instances.
+
+This is intentionally incomplete, but includes substantial variety. Most importantly, it
+includes both ``USE_SHELL`` and a wide sampling of other attributes.
+"""
+
+
+def test_class_dir() -> None:
+    """dir() on the Git class includes its statically known attributes.
+
+    This tests that the mechanism that adds dynamic behavior to USE_SHELL accesses so
+    that all accesses issue warnings does not break dir() for the class, neither for
+    USE_SHELL nor for ordinary (non-deprecated) attributes.
+    """
+    actual = set(dir(Git))
+    assert _EXPECTED_DIR_SUBSET <= actual
+
+
+def test_instance_dir() -> None:
+    """dir() on Git objects includes its statically known attributes.
+
+    This is like test_class_dir, but for Git instance rather than the class itself.
+    """
+    instance = Git()
+    actual = set(dir(instance))
+    assert _EXPECTED_DIR_SUBSET <= actual

From af723d5eb04dc919f76369c0b44a087638217352 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 24 Mar 2024 21:05:57 -0400
Subject: [PATCH 758/994] Make test_use_shell_on_class more robust

---
 test/deprecation/test_cmd_git.py | 54 ++++++++++++++++++++++----------
 1 file changed, 37 insertions(+), 17 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index fc8bab6ee..319bf7865 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -9,6 +9,7 @@
 import contextlib
 import sys
 from typing import Generator
+import warnings
 
 if sys.version_info >= (3, 11):
     from typing import assert_type
@@ -26,9 +27,16 @@
 """Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
 
 
+@contextlib.contextmanager
+def _suppress_deprecation_warning() -> Generator[None, None, None]:
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", category=DeprecationWarning)
+        yield
+
+
 @pytest.fixture
-def reset_backing_attribute() -> Generator[None, None, None]:
-    """Fixture to reset the private ``_USE_SHELL`` attribute.
+def try_restore_use_shell_state() -> Generator[None, None, None]:
+    """Fixture to attempt to restore state associated with the ``USE_SHELL`` attribute.
 
     This is used to decrease the likelihood of state changes leaking out and affecting
     other tests. But the goal is not to assert that ``_USE_SHELL`` is used, nor anything
@@ -38,18 +46,27 @@ def reset_backing_attribute() -> Generator[None, None, None]:
     restores attributes that it has previously been used to change, create, or remove.
     """
     no_value = object()
+
     try:
-        old_value = Git._USE_SHELL
+        old_backing_value = Git._USE_SHELL
     except AttributeError:
-        old_value = no_value
+        old_backing_value = no_value
+    try:
+        with _suppress_deprecation_warning():
+            old_public_value = Git.USE_SHELL
 
-    yield
+        # This doesn't have its own try-finally because pytest catches exceptions raised
+        # during the yield. (The outer try-finally catches exceptions in this fixture.)
+        yield
 
-    if old_value is no_value:
-        with contextlib.suppress(AttributeError):
-            del Git._USE_SHELL
-    else:
-        Git._USE_SHELL = old_value
+        with _suppress_deprecation_warning():
+            Git.USE_SHELL = old_public_value
+    finally:
+        if old_backing_value is no_value:
+            with contextlib.suppress(AttributeError):
+                del Git._USE_SHELL
+        else:
+            Git._USE_SHELL = old_backing_value
 
 
 def test_cannot_access_undefined_on_git_class() -> None:
@@ -76,23 +93,26 @@ def test_get_use_shell_on_class_default() -> None:
     assert not use_shell
 
 
-# FIXME: More robustly check that each operation really issues exactly one deprecation
-# warning, even if this requires relying more on reset_backing_attribute doing its job.
-def test_use_shell_on_class(reset_backing_attribute) -> None:
+def test_use_shell_on_class(try_restore_use_shell_state) -> None:
     """USE_SHELL can be written and re-read as a class attribute, always warning."""
-    # We assert in a "safe" order, using reset_backing_attribute only as a backstop.
-    with pytest.deprecated_call() as ctx:
+    with pytest.deprecated_call() as setting:
         Git.USE_SHELL = True
+    with pytest.deprecated_call() as checking:
         set_value = Git.USE_SHELL
+    with pytest.deprecated_call() as resetting:
         Git.USE_SHELL = False
+    with pytest.deprecated_call() as rechecking:
         reset_value = Git.USE_SHELL
 
     # The attribute should take on the values set to it.
     assert set_value is True
     assert reset_value is False
 
-    messages = [str(entry.message) for entry in ctx]
-    set_message, check_message, reset_message, recheck_message = messages
+    # Each access should warn exactly once.
+    (set_message,) = [str(entry.message) for entry in setting]
+    (check_message,) = [str(entry.message) for entry in checking]
+    (reset_message,) = [str(entry.message) for entry in resetting]
+    (recheck_message,) = [str(entry.message) for entry in rechecking]
 
     # Setting it to True should produce the special warning for that.
     assert _USE_SHELL_DEPRECATED_FRAGMENT in set_message

From bf1388896ac2d052cf956123513f0c8b33f34dd6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Mar 2024 00:06:40 -0400
Subject: [PATCH 759/994] Write most remaining Git attribute/deprecation tests

---
 test/deprecation/test_cmd_git.py | 99 ++++++++++++++++++++++++++++----
 1 file changed, 88 insertions(+), 11 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 319bf7865..ef6f5b5a6 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -17,6 +17,7 @@
     from typing_extensions import assert_type
 
 import pytest
+from pytest import WarningsRecorder
 
 from git.cmd import Git
 
@@ -93,17 +94,34 @@ def test_get_use_shell_on_class_default() -> None:
     assert not use_shell
 
 
-def test_use_shell_on_class(try_restore_use_shell_state) -> None:
-    """USE_SHELL can be written and re-read as a class attribute, always warning."""
-    with pytest.deprecated_call() as setting:
-        Git.USE_SHELL = True
-    with pytest.deprecated_call() as checking:
-        set_value = Git.USE_SHELL
-    with pytest.deprecated_call() as resetting:
-        Git.USE_SHELL = False
-    with pytest.deprecated_call() as rechecking:
-        reset_value = Git.USE_SHELL
+def test_get_use_shell_on_instance_default() -> None:
+    """USE_SHELL can be read as an instance attribute, defaulting to False and warning.
+
+    This is the same as test_get_use_shell_on_class_default above, but for instances.
+    The test is repeated, instead of using parametrization, for clearer static analysis.
+    """
+    instance = Git()
 
+    with pytest.deprecated_call() as ctx:
+        use_shell = instance.USE_SHELL
+
+    (message,) = [str(entry.message) for entry in ctx]  # Exactly one warning.
+    assert message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
+
+    assert_type(use_shell, bool)
+
+    # This comes after the static assertion, just in case it would affect the inference.
+    assert not use_shell
+
+
+def _assert_use_shell_full_results(
+    set_value: bool,
+    reset_value: bool,
+    setting: WarningsRecorder,
+    checking: WarningsRecorder,
+    resetting: WarningsRecorder,
+    rechecking: WarningsRecorder,
+) -> None:
     # The attribute should take on the values set to it.
     assert set_value is True
     assert reset_value is False
@@ -124,7 +142,66 @@ def test_use_shell_on_class(try_restore_use_shell_state) -> None:
     assert recheck_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
 
 
-# FIXME: Test behavior on instances (where we can get but not set).
+def test_use_shell_set_and_get_on_class(try_restore_use_shell_state: None) -> None:
+    """USE_SHELL can be set and re-read as a class attribute, always warning."""
+    with pytest.deprecated_call() as setting:
+        Git.USE_SHELL = True
+    with pytest.deprecated_call() as checking:
+        set_value = Git.USE_SHELL
+    with pytest.deprecated_call() as resetting:
+        Git.USE_SHELL = False
+    with pytest.deprecated_call() as rechecking:
+        reset_value = Git.USE_SHELL
+
+    _assert_use_shell_full_results(
+        set_value,
+        reset_value,
+        setting,
+        checking,
+        resetting,
+        rechecking,
+    )
+
+
+def test_use_shell_set_on_class_get_on_instance(try_restore_use_shell_state: None) -> None:
+    """USE_SHELL can be set on the class and read on an instance, always warning.
+
+    This is like test_use_shell_set_and_get_on_class but it performs reads on an
+    instance. There is some redundancy here in assertions about warnings when the
+    attribute is set, but it is a separate test so that any bugs where a read on the
+    class (or an instance) is needed first before a read on an instance (or the class)
+    are detected.
+    """
+    instance = Git()
+
+    with pytest.deprecated_call() as setting:
+        Git.USE_SHELL = True
+    with pytest.deprecated_call() as checking:
+        set_value = instance.USE_SHELL
+    with pytest.deprecated_call() as resetting:
+        Git.USE_SHELL = False
+    with pytest.deprecated_call() as rechecking:
+        reset_value = instance.USE_SHELL
+
+    _assert_use_shell_full_results(
+        set_value,
+        reset_value,
+        setting,
+        checking,
+        resetting,
+        rechecking,
+    )
+
+
+@pytest.mark.parametrize("value", [False, True])
+def test_use_shell_cannot_set_on_instance(
+    value: bool,
+    try_restore_use_shell_state: None,  # In case of a bug where it does set USE_SHELL.
+) -> None:
+    instance = Git()
+    with pytest.raises(AttributeError):
+        instance.USE_SHELL = value
+
 
 # FIXME: Test behavior with multiprocessing (the attribute needs to pickle properly).
 

From 602de0c93572972d29d33c4ecacfd9c6e192484a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Mar 2024 14:59:13 -0400
Subject: [PATCH 760/994] Begin multiprocessing misadventure

There is no per-instance state involved in USE_SHELL, so pickling
is far less directly relevant than usual to multiprocessing: the
spawn and forkserver methods will not preserve a subsequently
changed attribute value unless side effects of loading a module (or
other unpickling of a function or its arguments that are submitted
to run on a worker subprocess) causes it to run again; the fork
method will.

This will be (automatically) the same with any combination of
metaclasses, properties, and custom descriptors as in the more
straightforward case of a simple class attribute. Subtleties arise
in the code that uses GitPython and multiprocessing, but should not
arise unintentionally from the change in implementation of USE_SHELL
done to add deprecation warnings, except possibly with respect to
whether warnings will be repeated in worker processes, which is
less important than whether the actual state is preserved.
---
 test/deprecation/test_cmd_git.py | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index ef6f5b5a6..72c1f7cd8 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -6,6 +6,7 @@
 access are not inadvertently broken by mechanisms introduced to issue the warnings.
 """
 
+from concurrent.futures import ProcessPoolExecutor
 import contextlib
 import sys
 from typing import Generator
@@ -36,7 +37,7 @@ def _suppress_deprecation_warning() -> Generator[None, None, None]:
 
 
 @pytest.fixture
-def try_restore_use_shell_state() -> Generator[None, None, None]:
+def restore_use_shell_state() -> Generator[None, None, None]:
     """Fixture to attempt to restore state associated with the ``USE_SHELL`` attribute.
 
     This is used to decrease the likelihood of state changes leaking out and affecting
@@ -142,7 +143,7 @@ def _assert_use_shell_full_results(
     assert recheck_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
 
 
-def test_use_shell_set_and_get_on_class(try_restore_use_shell_state: None) -> None:
+def test_use_shell_set_and_get_on_class(restore_use_shell_state: None) -> None:
     """USE_SHELL can be set and re-read as a class attribute, always warning."""
     with pytest.deprecated_call() as setting:
         Git.USE_SHELL = True
@@ -163,7 +164,7 @@ def test_use_shell_set_and_get_on_class(try_restore_use_shell_state: None) -> No
     )
 
 
-def test_use_shell_set_on_class_get_on_instance(try_restore_use_shell_state: None) -> None:
+def test_use_shell_set_on_class_get_on_instance(restore_use_shell_state: None) -> None:
     """USE_SHELL can be set on the class and read on an instance, always warning.
 
     This is like test_use_shell_set_and_get_on_class but it performs reads on an
@@ -196,14 +197,31 @@ class (or an instance) is needed first before a read on an instance (or the clas
 @pytest.mark.parametrize("value", [False, True])
 def test_use_shell_cannot_set_on_instance(
     value: bool,
-    try_restore_use_shell_state: None,  # In case of a bug where it does set USE_SHELL.
+    restore_use_shell_state: None,  # In case of a bug where it does set USE_SHELL.
 ) -> None:
     instance = Git()
     with pytest.raises(AttributeError):
         instance.USE_SHELL = value
 
 
-# FIXME: Test behavior with multiprocessing (the attribute needs to pickle properly).
+def _check_use_shell_in_worker(value: bool) -> None:
+    # USE_SHELL should have the value set in the parent before starting the worker.
+    assert Git.USE_SHELL is value
+
+    # FIXME: Check that mutation still works and raises the warning.
+
+
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
+@pytest.mark.parametrize("value", [False, True])
+def test_use_shell_preserved_in_multiprocessing(
+    value: bool,
+    restore_use_shell_state: None,
+) -> None:
+    """The USE_SHELL class attribute pickles accurately for multiprocessing."""
+    Git.USE_SHELL = value
+    with ProcessPoolExecutor(max_workers=1) as executor:
+        # Calling result() marshals any exception back to this process and raises it.
+        executor.submit(_check_use_shell_in_worker, value).result()
 
 
 _EXPECTED_DIR_SUBSET = {

From d4b50c94ff3694e3285a1ea8ed9b42c685726baf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 25 Mar 2024 15:01:23 -0400
Subject: [PATCH 761/994] Somewhat clarify multiprocessing misadventure

---
 test/deprecation/test_cmd_git.py | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 72c1f7cd8..04fb0747c 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -204,11 +204,8 @@ def test_use_shell_cannot_set_on_instance(
         instance.USE_SHELL = value
 
 
-def _check_use_shell_in_worker(value: bool) -> None:
-    # USE_SHELL should have the value set in the parent before starting the worker.
-    assert Git.USE_SHELL is value
-
-    # FIXME: Check that mutation still works and raises the warning.
+def _get_value_in_current_process() -> bool:
+    return Git.USE_SHELL
 
 
 @pytest.mark.filterwarnings("ignore::DeprecationWarning")
@@ -220,8 +217,8 @@ def test_use_shell_preserved_in_multiprocessing(
     """The USE_SHELL class attribute pickles accurately for multiprocessing."""
     Git.USE_SHELL = value
     with ProcessPoolExecutor(max_workers=1) as executor:
-        # Calling result() marshals any exception back to this process and raises it.
-        executor.submit(_check_use_shell_in_worker, value).result()
+        marshaled_value = executor.submit(_get_value_in_current_process).result()
+    assert marshaled_value is value
 
 
 _EXPECTED_DIR_SUBSET = {

From 02c2f0008082e710ddb73f1c4ff784b74eed4f8f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Mar 2024 13:09:08 -0400
Subject: [PATCH 762/994] Discuss multiprocessing in test module docstring;
 remove bad test

---
 test/deprecation/test_cmd_git.py | 44 ++++++++++++++++++--------------
 1 file changed, 25 insertions(+), 19 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 04fb0747c..9dfe37220 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -1,12 +1,35 @@
 """Tests for static and dynamic characteristics of Git class and instance attributes.
 
-Currently this all relates to the deprecated :class:`Git.USE_SHELL` class attribute,
+Currently this all relates to the deprecated :attr:`Git.USE_SHELL` class attribute,
 which can also be accessed through instances. Some tests directly verify its behavior,
 including deprecation warnings, while others verify that other aspects of attribute
 access are not inadvertently broken by mechanisms introduced to issue the warnings.
+
+A note on multiprocessing: Because USE_SHELL has no instance state, this module does not
+include tests of pickling and multiprocessing.
+
+- Just as with a simple class attribute, when a class attribute with custom logic is
+  later set to a new value, it may have either its initial value or the new value when
+  accessed from a worker process, depending on the process start method. With "fork",
+  changes are preserved. With "spawn" or "forkserver", re-importing the modules causes
+  initial values to be set. Then the value in the parent at the time it dispatches the
+  task is only set in the children if the parent has the task set it, or if it is set as
+  a side effect of importing needed modules, or of unpickling objects passed to the
+  child (for example, if it is set in a top-level statement of the module that defines
+  the function submitted for the child worker process to call).
+
+- When an attribute gains new logic provided by a property or custom descriptor, and the
+  attribute involves instance-level state, incomplete or corrupted pickling can break
+  multiprocessing. (For example, if an instance attribute is reimplemented using a
+  descriptor that stores data in a global WeakKeyDictionary, pickled instances should be
+  tested to ensure they are still working correctly.) But nothing like that applies
+  here, because instance state is not involved. Although the situation is inherently
+  complex as described above, it is independent of the attribute implementation.
+
+- That USE_SHELL cannot be set on instances, and that when retrieved on instances it
+  always gives the same value as on the class, is covered in the tests here.
 """
 
-from concurrent.futures import ProcessPoolExecutor
 import contextlib
 import sys
 from typing import Generator
@@ -204,23 +227,6 @@ def test_use_shell_cannot_set_on_instance(
         instance.USE_SHELL = value
 
 
-def _get_value_in_current_process() -> bool:
-    return Git.USE_SHELL
-
-
-@pytest.mark.filterwarnings("ignore::DeprecationWarning")
-@pytest.mark.parametrize("value", [False, True])
-def test_use_shell_preserved_in_multiprocessing(
-    value: bool,
-    restore_use_shell_state: None,
-) -> None:
-    """The USE_SHELL class attribute pickles accurately for multiprocessing."""
-    Git.USE_SHELL = value
-    with ProcessPoolExecutor(max_workers=1) as executor:
-        marshaled_value = executor.submit(_get_value_in_current_process).result()
-    assert marshaled_value is value
-
-
 _EXPECTED_DIR_SUBSET = {
     "cat_file_all",
     "cat_file_header",

From 46df79f75e6ee4db9852c398ec99a5788fd966b2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Mar 2024 16:03:58 -0400
Subject: [PATCH 763/994] Discuss metaclass conflicts in test module docstring

---
 test/deprecation/test_cmd_git.py | 27 +++++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 9dfe37220..a5d241cc4 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -5,8 +5,11 @@
 including deprecation warnings, while others verify that other aspects of attribute
 access are not inadvertently broken by mechanisms introduced to issue the warnings.
 
-A note on multiprocessing: Because USE_SHELL has no instance state, this module does not
-include tests of pickling and multiprocessing.
+A note on multiprocessing
+=========================
+
+Because USE_SHELL has no instance state, this module does not include tests of pickling
+and multiprocessing:
 
 - Just as with a simple class attribute, when a class attribute with custom logic is
   later set to a new value, it may have either its initial value or the new value when
@@ -28,6 +31,26 @@
 
 - That USE_SHELL cannot be set on instances, and that when retrieved on instances it
   always gives the same value as on the class, is covered in the tests here.
+
+A note on metaclass conflicts
+=============================
+
+The most important DeprecationWarning is for the code ``Git.USE_SHELL = True``, which is
+a security risk. But this warning may not be possible to implement without a custom
+metaclass. This is because a descriptor in a class can customize all forms of attribute
+access on its instances, but can only customize getting an attribute on the class.
+Retrieving a descriptor from a class calls its ``__get__`` method (if defined), but
+replacing or deleting it does not call its ``__set__`` or ``__delete__`` methods.
+
+Adding a metaclass is a potentially breaking change. This is because derived classes
+that use an unrelated metaclass, whether directly or by inheriting from a class such as
+abc.ABC that uses one, will raise TypeError when defined. These would have to be
+modified to use a newly introduced metaclass that is a lower bound of both. Subclasses
+remain unbroken in the far more typical case that they use no custom metaclass.
+
+The tests in this module do not establish whether the danger of setting Git.USE_SHELL to
+True is high enough, and applications of deriving from Git and using an unrelated custom
+metaclass marginal enough, to justify introducing a metaclass to issue the warnings.
 """
 
 import contextlib

From 40ed842cf35b0c280dc408d77c764988a2d2746a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Mar 2024 18:30:34 -0400
Subject: [PATCH 764/994] Revise test module docstring for clarity

---
 test/deprecation/test_cmd_git.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index a5d241cc4..2ebcb3ad7 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -11,19 +11,19 @@
 Because USE_SHELL has no instance state, this module does not include tests of pickling
 and multiprocessing:
 
-- Just as with a simple class attribute, when a class attribute with custom logic is
-  later set to a new value, it may have either its initial value or the new value when
-  accessed from a worker process, depending on the process start method. With "fork",
-  changes are preserved. With "spawn" or "forkserver", re-importing the modules causes
-  initial values to be set. Then the value in the parent at the time it dispatches the
-  task is only set in the children if the parent has the task set it, or if it is set as
-  a side effect of importing needed modules, or of unpickling objects passed to the
-  child (for example, if it is set in a top-level statement of the module that defines
-  the function submitted for the child worker process to call).
+- Just as with a simple class attribute, when a class attribute with custom logic is set
+  to another value, even before a worker process is created that uses the class, the
+  worker process may see either the initial or new value, depending on the process start
+  method. With "fork", changes are preserved. With "spawn" or "forkserver", re-importing
+  the modules causes initial values to be set. Then the value in the parent at the time
+  it dispatches the task is only set in the children if the parent has the task set it,
+  or if it is set as a side effect of importing needed modules, or of unpickling objects
+  passed to the child (for example, if it is set in a top-level statement of the module
+  that defines the function submitted for the child worker process to call).
 
 - When an attribute gains new logic provided by a property or custom descriptor, and the
   attribute involves instance-level state, incomplete or corrupted pickling can break
-  multiprocessing. (For example, if an instance attribute is reimplemented using a
+  multiprocessing. (For example, when an instance attribute is reimplemented using a
   descriptor that stores data in a global WeakKeyDictionary, pickled instances should be
   tested to ensure they are still working correctly.) But nothing like that applies
   here, because instance state is not involved. Although the situation is inherently
@@ -35,8 +35,8 @@
 A note on metaclass conflicts
 =============================
 
-The most important DeprecationWarning is for the code ``Git.USE_SHELL = True``, which is
-a security risk. But this warning may not be possible to implement without a custom
+The most important DeprecationWarning is for code like ``Git.USE_SHELL = True``, which
+is a security risk. But this warning may not be possible to implement without a custom
 metaclass. This is because a descriptor in a class can customize all forms of attribute
 access on its instances, but can only customize getting an attribute on the class.
 Retrieving a descriptor from a class calls its ``__get__`` method (if defined), but

From 6a35261ab6a58bfd8fbb2b008aacfb2decf27053 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 03:28:38 -0400
Subject: [PATCH 765/994] Test that USE_SHELL is unittest.mock.patch patchable

---
 test/deprecation/test_cmd_git.py | 36 ++++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 2ebcb3ad7..5d710b0d4 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -56,6 +56,7 @@
 import contextlib
 import sys
 from typing import Generator
+import unittest.mock
 import warnings
 
 if sys.version_info >= (3, 11):
@@ -250,6 +251,41 @@ def test_use_shell_cannot_set_on_instance(
         instance.USE_SHELL = value
 
 
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
+@pytest.mark.parametrize("original_value", [False, True])
+def test_use_shell_is_mock_patchable_on_class_as_object_attribute(
+    original_value: bool,
+    restore_use_shell_state: None,
+) -> None:
+    """Asymmetric patching looking up USE_SHELL in ``__dict__`` doesn't corrupt state.
+
+    Code using GitPython may temporarily set Git.USE_SHELL to a different value. Ideally
+    it does not use unittest.mock.patch to do so, because that makes subtle assumptions
+    about the relationship between attributes and dictionaries. If the attribute can be
+    retrieved from the ``__dict__`` rather than directly, that value is assumed the
+    correct one to restore, even by a normal setattr.
+
+    The effect is that some ways of simulating a class attribute with added behavior can
+    cause a descriptor, such as a property, to be set to its own backing attribute
+    during unpatching; then subsequent reads raise RecursionError. This happens if both
+    (a) setting it on the class is customized in a metaclass and (b) getting it on
+    instances is customized with a descriptor (such as a property) in the class itself.
+
+    Although ideally code outside GitPython would not rely on being able to patch
+    Git.USE_SHELL with unittest.mock.patch, the technique is widespread. Thus, USE_SHELL
+    should be implemented in some way compatible with it. This test checks for that.
+    """
+    Git.USE_SHELL = original_value
+    if Git.USE_SHELL is not original_value:
+        raise RuntimeError(f"Can't set up the test")
+    new_value = not original_value
+
+    with unittest.mock.patch.object(Git, "USE_SHELL", new_value):
+        assert Git.USE_SHELL is new_value
+
+    assert Git.USE_SHELL is original_value
+
+
 _EXPECTED_DIR_SUBSET = {
     "cat_file_all",
     "cat_file_header",

From e725c8254cccd88d02b38b414bac875212a7074b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 11:45:25 -0400
Subject: [PATCH 766/994] Make the restore_use_shell_state fixture more robust

---
 test/deprecation/test_cmd_git.py | 52 ++++++++++++++++++++++++--------
 1 file changed, 39 insertions(+), 13 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 5d710b0d4..4183018e6 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -54,6 +54,7 @@
 """
 
 import contextlib
+import logging
 import sys
 from typing import Generator
 import unittest.mock
@@ -75,6 +76,8 @@
 _USE_SHELL_DANGEROUS_FRAGMENT = "Setting Git.USE_SHELL to True is unsafe and insecure"
 """Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
 
+_logger = logging.getLogger(__name__)
+
 
 @contextlib.contextmanager
 def _suppress_deprecation_warning() -> Generator[None, None, None]:
@@ -85,22 +88,44 @@ def _suppress_deprecation_warning() -> Generator[None, None, None]:
 
 @pytest.fixture
 def restore_use_shell_state() -> Generator[None, None, None]:
-    """Fixture to attempt to restore state associated with the ``USE_SHELL`` attribute.
+    """Fixture to attempt to restore state associated with the USE_SHELL attribute.
 
     This is used to decrease the likelihood of state changes leaking out and affecting
-    other tests. But the goal is not to assert that ``_USE_SHELL`` is used, nor anything
-    about how or when it is used, which is an implementation detail subject to change.
+    other tests. But the goal is not to assert implementation details of USE_SHELL.
+
+    This covers two of the common implementation strategies, for convenience in testing
+    both. USE_SHELL could be implemented in the metaclass:
 
-    This is possible but inelegant to do with pytest's monkeypatch fixture, which only
-    restores attributes that it has previously been used to change, create, or remove.
+    * With a separate _USE_SHELL backing attribute. If using a property or other
+      descriptor, this is the natural way to do it, but custom __getattribute__ and
+      __setattr__ logic, if it does more than adding warnings, may also use that.
+    * Like a simple attribute, using USE_SHELL itself, stored as usual in the class
+      dictionary, with custom __getattribute__/__setattr__ logic only to warn.
+
+    This tries to save private state, tries to save the public attribute value, yields
+    to the test case, tries to restore the public attribute value, then tries to restore
+    private state. The idea is that if the getting or setting logic is wrong in the code
+    under test, the state will still most likely be reset successfully.
     """
     no_value = object()
 
+    # Try to save the original private state.
     try:
-        old_backing_value = Git._USE_SHELL
+        old_private_value = Git._USE_SHELL
     except AttributeError:
-        old_backing_value = no_value
+        separate_backing_attribute = False
+        try:
+            old_private_value = type.__getattribute__(Git, "USE_SHELL")
+        except AttributeError:
+            old_private_value = no_value
+            _logger.error("Cannot retrieve old private _USE_SHELL or USE_SHELL value")
+    else:
+        separate_backing_attribute = True
+
     try:
+        # Try to save the original public value. Rather than attempt to restore a state
+        # where the attribute is not set, if we cannot do this we allow AttributeError
+        # to propagate out of the fixture, erroring the test case before its code runs.
         with _suppress_deprecation_warning():
             old_public_value = Git.USE_SHELL
 
@@ -108,14 +133,15 @@ def restore_use_shell_state() -> Generator[None, None, None]:
         # during the yield. (The outer try-finally catches exceptions in this fixture.)
         yield
 
+        # Try to restore the original public value.
         with _suppress_deprecation_warning():
             Git.USE_SHELL = old_public_value
     finally:
-        if old_backing_value is no_value:
-            with contextlib.suppress(AttributeError):
-                del Git._USE_SHELL
-        else:
-            Git._USE_SHELL = old_backing_value
+        # Try to restore the original private state.
+        if separate_backing_attribute:
+            Git._USE_SHELL = old_private_value
+        elif old_private_value is not no_value:
+            type.__setattr__(Git, "USE_SHELL", old_private_value)
 
 
 def test_cannot_access_undefined_on_git_class() -> None:
@@ -277,7 +303,7 @@ def test_use_shell_is_mock_patchable_on_class_as_object_attribute(
     """
     Git.USE_SHELL = original_value
     if Git.USE_SHELL is not original_value:
-        raise RuntimeError(f"Can't set up the test")
+        raise RuntimeError("Can't set up the test")
     new_value = not original_value
 
     with unittest.mock.patch.object(Git, "USE_SHELL", new_value):

From 436bcaa85712f73640ac719679d6d1366c376483 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 12:05:52 -0400
Subject: [PATCH 767/994] Add `type: ignore` in test that we can't set
 USE_SHELL on instances

As with other `type: ignore` comments in the deprecation tests,
mypy will detect if it is superfluous (provided warn_unused_ignores
is set to true), thereby enforcing that such code is a static type
error.
---
 test/deprecation/test_cmd_git.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 4183018e6..4925fada8 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -274,7 +274,7 @@ def test_use_shell_cannot_set_on_instance(
 ) -> None:
     instance = Git()
     with pytest.raises(AttributeError):
-        instance.USE_SHELL = value
+        instance.USE_SHELL = value  # type: ignore[misc]  # Name not in __slots__.
 
 
 @pytest.mark.filterwarnings("ignore::DeprecationWarning")

From febda6f6e9aa56f4b525020153ed88459906641f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 12:09:15 -0400
Subject: [PATCH 768/994] Clarify unittest.mock.patch patchability test
 docstring

---
 test/deprecation/test_cmd_git.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 4925fada8..b792fddb8 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -292,10 +292,11 @@ def test_use_shell_is_mock_patchable_on_class_as_object_attribute(
     correct one to restore, even by a normal setattr.
 
     The effect is that some ways of simulating a class attribute with added behavior can
-    cause a descriptor, such as a property, to be set to its own backing attribute
-    during unpatching; then subsequent reads raise RecursionError. This happens if both
-    (a) setting it on the class is customized in a metaclass and (b) getting it on
-    instances is customized with a descriptor (such as a property) in the class itself.
+    cause a descriptor, such as a property, to be set as the value of its own backing
+    attribute during unpatching; then subsequent reads raise RecursionError. This
+    happens if both (a) setting it on the class is customized in a metaclass and (b)
+    getting it on instances is customized with a descriptor (such as a property) in the
+    class itself.
 
     Although ideally code outside GitPython would not rely on being able to patch
     Git.USE_SHELL with unittest.mock.patch, the technique is widespread. Thus, USE_SHELL

From 40371087ac83497f307d2239f36646a321028829 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 13:31:22 -0400
Subject: [PATCH 769/994] Test that Git.execute's own read of USE_SHELL does
 not warn

+ Use simplefilter where we can (separate from this test).
---
 test/deprecation/test_cmd_git.py | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index b792fddb8..f17756fb6 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -82,7 +82,7 @@
 @contextlib.contextmanager
 def _suppress_deprecation_warning() -> Generator[None, None, None]:
     with warnings.catch_warnings():
-        warnings.filterwarnings("ignore", category=DeprecationWarning)
+        warnings.simplefilter("ignore", DeprecationWarning)
         yield
 
 
@@ -313,6 +313,24 @@ class itself.
     assert Git.USE_SHELL is original_value
 
 
+def test_execute_without_shell_arg_does_not_warn() -> None:
+    """No deprecation warning is issued from operations implemented using Git.execute().
+
+    When no ``shell`` argument is passed to Git.execute, which is when the value of
+    USE_SHELL is to be used, the way Git.execute itself accesses USE_SHELL does not
+    issue a deprecation warning.
+    """
+    with warnings.catch_warnings():
+        for category in DeprecationWarning, PendingDeprecationWarning:
+            warnings.filterwarnings(
+                action="error",
+                category=category,
+                module=r"git(?:\.|$)",
+            )
+
+        Git().version()
+
+
 _EXPECTED_DIR_SUBSET = {
     "cat_file_all",
     "cat_file_header",

From 0e311bf5da4332acd314aa6927138b36bbd9e527 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 14:42:26 -0400
Subject: [PATCH 770/994] Suppress type errors in restore_use_shell_state
 _USE_SHELL branches

These conditional branches are kept so alternative implementations
can be examined, including if they need to be investigated to
satisfy some future requirement. But to be unittest.mock.patch
patchable, the approaches that would have a _USE_SHELL backing
attribute would be difficult to implement in a straightforward way,
which seems not to be needed or justified at this time.

Since that is not anticipated (except as an intermediate step in
development), these suppressions make sense, and they will also be
reported by mypy if the implementation changes to benefit from them
(so long as it is configured with warn_unused_ignores set to true).
---
 test/deprecation/test_cmd_git.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index f17756fb6..b6af5c770 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -111,7 +111,7 @@ def restore_use_shell_state() -> Generator[None, None, None]:
 
     # Try to save the original private state.
     try:
-        old_private_value = Git._USE_SHELL
+        old_private_value = Git._USE_SHELL  # type: ignore[attr-defined]
     except AttributeError:
         separate_backing_attribute = False
         try:
@@ -139,7 +139,7 @@ def restore_use_shell_state() -> Generator[None, None, None]:
     finally:
         # Try to restore the original private state.
         if separate_backing_attribute:
-            Git._USE_SHELL = old_private_value
+            Git._USE_SHELL = old_private_value  # type: ignore[attr-defined]
         elif old_private_value is not no_value:
             type.__setattr__(Git, "USE_SHELL", old_private_value)
 

From df4c5c03d14a53448a232a78eeab94017ac7e7ba Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 15:33:47 -0400
Subject: [PATCH 771/994] Fix wrong/unclear grammar in test_instance_dir
 docstring

---
 test/deprecation/test_cmd_git.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index b6af5c770..d312b8778 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -383,7 +383,7 @@ def test_class_dir() -> None:
 def test_instance_dir() -> None:
     """dir() on Git objects includes its statically known attributes.
 
-    This is like test_class_dir, but for Git instance rather than the class itself.
+    This is like test_class_dir, but for Git instances rather than the class itself.
     """
     instance = Git()
     actual = set(dir(instance))

From d38e721c1f35695ecdaf6e88b721881fb7b3ed6f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Tue, 26 Mar 2024 21:55:04 -0400
Subject: [PATCH 772/994] Issue warnings whenever Git.USE_SHELL is accessed

With a special message when it is assigned a True value, which is
the dangerous use that underlies its deprecation.

The warnings are all DeprecationWarning.
---
 git/cmd.py | 78 ++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 61 insertions(+), 17 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 03e5d7ffc..0b7536d91 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -19,6 +19,7 @@
 import sys
 from textwrap import dedent
 import threading
+import warnings
 
 from git.compat import defenc, force_bytes, safe_decode
 from git.exc import (
@@ -54,6 +55,7 @@
     TYPE_CHECKING,
     TextIO,
     Tuple,
+    Type,
     Union,
     cast,
     overload,
@@ -307,8 +309,45 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 
 ## -- End Utilities -- @}
 
+_USE_SHELL_DEFAULT_MESSAGE = (
+    "Git.USE_SHELL is deprecated, because only its default value of False is safe. "
+    "It will be removed in a future release."
+)
+
+_USE_SHELL_DANGER_MESSAGE = (
+    "Setting Git.USE_SHELL to True is unsafe and insecure, and should be avoided, "
+    "because the effect of shell metacharacters and shell expansions cannot usually be "
+    "accounted for. In addition, Git.USE_SHELL is deprecated and will be removed in a "
+    "future release."
+)
+
+
+def _warn_use_shell(extra_danger: bool) -> None:
+    warnings.warn(
+        _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
+        DeprecationWarning,
+        stacklevel=3,
+    )
+
+
+class _GitMeta(type):
+    """Metaclass for :class:`Git`.
+
+    This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
+    """
 
-class Git:
+    @property
+    def USE_SHELL(cls: Type[Git]) -> bool:
+        _warn_use_shell(False)
+        return cls._USE_SHELL
+
+    @USE_SHELL.setter
+    def USE_SHELL(cls: Type[Git], value: bool) -> None:
+        _warn_use_shell(value)
+        cls._USE_SHELL = value
+
+
+class Git(metaclass=_GitMeta):
     """The Git class manages communication with the Git binary.
 
     It provides a convenient interface to calling the Git binary, such as in::
@@ -358,25 +397,30 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
     """Enables debugging of GitPython's git commands."""
 
-    USE_SHELL = False
-    """Deprecated. If set to ``True``, a shell will be used when executing git commands.
+    _USE_SHELL: bool = False
 
-    Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
-    in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
-    GitPython solves that problem more robustly and safely by using the
-    ``CREATE_NO_WINDOW`` process creation flag on Windows.
+    @property
+    def USE_SHELL(self) -> bool:
+        """Deprecated. If set to ``True``, a shell will be used to execute git commands.
 
-    Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
-    functions should be updated to use the default value of ``False`` instead. ``True``
-    is unsafe unless the effect of shell expansions is fully considered and accounted
-    for, which is not possible under most circumstances.
+        Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console
+        windows in graphical Windows applications. In 2.0.8 and higher, it provides no
+        benefit, as GitPython solves that problem more robustly and safely by using the
+        ``CREATE_NO_WINDOW`` process creation flag on Windows.
 
-    See:
+        Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any
+        GitPython functions should be updated to use the default value of ``False``
+        instead. ``True`` is unsafe unless the effect of shell expansions is fully
+        considered and accounted for, which is not possible under most circumstances.
 
-    - :meth:`Git.execute` (on the ``shell`` parameter).
-    - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
-    - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
-    """
+        See:
+
+        - :meth:`Git.execute` (on the ``shell`` parameter).
+        - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
+        - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
+        """
+        _warn_use_shell(False)
+        return self._USE_SHELL
 
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
     _refresh_env_var = "GIT_PYTHON_REFRESH"
@@ -1138,7 +1182,7 @@ def execute(
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
         if shell is None:
-            shell = self.USE_SHELL
+            shell = self._USE_SHELL
         _logger.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,

From 05de5c0d3bc81f4d56283399417db2f6927fe233 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 03:29:41 -0400
Subject: [PATCH 773/994] Implement instance USE_SHELL lookup in __getattr__

This is with the intention of making it so that Git.USE_SHELL is
unittest.mock.patch patchable. However, while this moves in the
right direction, it can't be done this way, as the attribute is
found to be absent on the class, so when unittest.mock.patch
unpatches, it tries to delete the attribute it has set, which
fails due to the metaclass's USE_SHELL instance property having no
deleter.
---
 git/cmd.py | 38 ++++++++++++++++++++------------------
 1 file changed, 20 insertions(+), 18 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 0b7536d91..d01c15b04 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -399,28 +399,25 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
 
     _USE_SHELL: bool = False
 
-    @property
-    def USE_SHELL(self) -> bool:
-        """Deprecated. If set to ``True``, a shell will be used to execute git commands.
+    USE_SHELL: bool
+    """Deprecated. If set to ``True``, a shell will be used to execute git commands.
 
-        Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console
-        windows in graphical Windows applications. In 2.0.8 and higher, it provides no
-        benefit, as GitPython solves that problem more robustly and safely by using the
-        ``CREATE_NO_WINDOW`` process creation flag on Windows.
+    Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
+    in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
+    GitPython solves that problem more robustly and safely by using the
+    ``CREATE_NO_WINDOW`` process creation flag on Windows.
 
-        Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any
-        GitPython functions should be updated to use the default value of ``False``
-        instead. ``True`` is unsafe unless the effect of shell expansions is fully
-        considered and accounted for, which is not possible under most circumstances.
+    Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
+    functions should be updated to use the default value of ``False`` instead. ``True``
+    is unsafe unless the effect of shell expansions is fully considered and accounted
+    for, which is not possible under most circumstances.
 
-        See:
+    See:
 
-        - :meth:`Git.execute` (on the ``shell`` parameter).
-        - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
-        - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
-        """
-        _warn_use_shell(False)
-        return self._USE_SHELL
+    - :meth:`Git.execute` (on the ``shell`` parameter).
+    - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
+    - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
+    """
 
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
     _refresh_env_var = "GIT_PYTHON_REFRESH"
@@ -921,6 +918,11 @@ def __getattr__(self, name: str) -> Any:
         """
         if name.startswith("_"):
             return super().__getattribute__(name)
+
+        if name == "USE_SHELL":
+            _warn_use_shell(False)
+            return self._USE_SHELL
+
         return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
 
     def set_persistent_git_options(self, **kwargs: Any) -> None:

From 04eb09c728fbdc4501fbbba1d6f58a0bb7050470 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 03:40:14 -0400
Subject: [PATCH 774/994] Have USE_SHELL warn but work like normal via super()

This reimplements Git.USE_SHELL with no properties or descriptors.
The metaclass is still needed, but instad of defining properties it
defines __getattribute__ and __setattr__, which check for USE_SHELL
and warn, then invoke the default attribute access via super().

Likewise, in the Git class itself, a __getatttribute__ override is
introduced (not to be confused with __getattr__, which was already
present and handles attribute access when an attribute is otherwise
absent, unlike __getattribute__ which is always used). This checks
for reading USE_SHELL on an instance and warns, then invokes the
default attribute access via super().

Advantages:

- Git.USE_SHELL is again unittest.mock.patch patchable.

- AttributeError messages are automatically as before.

- It effectively is a simple attribute, yet with warning, so other
  unanticipated ways of accessing it may be less likely to break.

- The code is simpler, cleaner, and clearer. There is some
  overhead, but it is small, especially compared to a subprocess
  invocation as is done for performing most git operations.

However, this does introduce disadvantages that must be addressed:

- Although attribute access on Git instances was already highly
  dynamic, as "methods" are synthesized for git subcommands, this
  was and is not the case for the Git class itself, whose
  attributes remain exactly those that can be inferred without
  considering the existence of __getattribute__ and __setattr__ on
  the metaclass. So static type checkers need to be prevented from
  accounting for those metaclass methods in a way that causes them
  to infer that arbitrary class attribute access is allowed.

- The occurrence of Git.USE_SHELL in the Git.execute method (where
  the USE_SHELL attribute is actually examined) should be changed
  so it does not itself issue DeprecationWarning (it is not enough
  that by default a DeprecationWarning from there isn't displayed).
---
 git/cmd.py | 33 +++++++++++++++------------------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index d01c15b04..1c2aebd3c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -55,7 +55,6 @@
     TYPE_CHECKING,
     TextIO,
     Tuple,
-    Type,
     Union,
     cast,
     overload,
@@ -336,15 +335,15 @@ class _GitMeta(type):
     This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
     """
 
-    @property
-    def USE_SHELL(cls: Type[Git]) -> bool:
-        _warn_use_shell(False)
-        return cls._USE_SHELL
+    def __getattribute__(cls, name: str) -> Any:
+        if name == "USE_SHELL":
+            _warn_use_shell(False)
+        return super().__getattribute__(name)
 
-    @USE_SHELL.setter
-    def USE_SHELL(cls: Type[Git], value: bool) -> None:
-        _warn_use_shell(value)
-        cls._USE_SHELL = value
+    def __setattr__(cls, name: str, value: Any) -> Any:
+        if name == "USE_SHELL":
+            _warn_use_shell(value)
+        super().__setattr__(name, value)
 
 
 class Git(metaclass=_GitMeta):
@@ -397,9 +396,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
     """Enables debugging of GitPython's git commands."""
 
-    _USE_SHELL: bool = False
-
-    USE_SHELL: bool
+    USE_SHELL: bool = False
     """Deprecated. If set to ``True``, a shell will be used to execute git commands.
 
     Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
@@ -909,6 +906,11 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
         self.cat_file_header: Union[None, TBD] = None
         self.cat_file_all: Union[None, TBD] = None
 
+    def __getattribute__(self, name: str) -> Any:
+        if name == "USE_SHELL":
+            _warn_use_shell(False)
+        return super().__getattribute__(name)
+
     def __getattr__(self, name: str) -> Any:
         """A convenience method as it allows to call the command as if it was an object.
 
@@ -918,11 +920,6 @@ def __getattr__(self, name: str) -> Any:
         """
         if name.startswith("_"):
             return super().__getattribute__(name)
-
-        if name == "USE_SHELL":
-            _warn_use_shell(False)
-            return self._USE_SHELL
-
         return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
 
     def set_persistent_git_options(self, **kwargs: Any) -> None:
@@ -1184,7 +1181,7 @@ def execute(
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
         if shell is None:
-            shell = self._USE_SHELL
+            shell = self.USE_SHELL
         _logger.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,

From c6f518b40e42d394dfbcf338aacf101cad58b700 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 14:14:52 -0400
Subject: [PATCH 775/994] Keep mypy from thinking Git has arbitrary class
 attributes

The existence of __getattr__ or __getattribute__ on a class causes
static type checkers like mypy to stop inferring that reads of
unrecognized instance attributes are static type errors. When the
class is a metaclass, this causes static type checkers to stop
inferring that reads of unrecognized class attributes, on classes
that use (i.e., that have as their type) the metaclass, are static
type errors.

The Git class itself defines __getattr__ and __getattribute__, but
Git objects' instance attributes really are dynamically synthesized
(in __getattr__). However, class attributes of Git are not dynamic,
even though _GitMeta defines __getattribute__. Therefore, something
special is needed so mypy infers nothing about Git class attributes
from the existence of _GitMeta.__getattribute__.

This takes the same approach as taken to the analogous problem with
__getattr__ at module level, defining __getattribute__ with a
different name first and then assigning that to __getattribute__
under an `if not TYPE_CHECKING:`. (Allowing static type checkers to
see the original definition allows them to find some kinds of type
errors in the body of the method, which is why the method isn't
just defined in the `if not TYPE_CHECKING`.)

Although it may not currently be necessary to hide __setattr__ too,
the same is done with it in _GitMeta as well. The reason is that
the intent may otherwise be subtly amgiguous to human readers and
maybe future tools: when __setattr__ exists, the effect of setting
a class attribute that did not previously exist is in principle
unclear, and might not make the attribute readble. (I am unsure if
this is the right choice; either approach seems reasonable.)
---
 git/cmd.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 1c2aebd3c..ce19e733c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -335,16 +335,23 @@ class _GitMeta(type):
     This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
     """
 
-    def __getattribute__(cls, name: str) -> Any:
+    def __getattribute(cls, name: str) -> Any:
         if name == "USE_SHELL":
             _warn_use_shell(False)
         return super().__getattribute__(name)
 
-    def __setattr__(cls, name: str, value: Any) -> Any:
+    def __setattr(cls, name: str, value: Any) -> Any:
         if name == "USE_SHELL":
             _warn_use_shell(value)
         super().__setattr__(name, value)
 
+    if not TYPE_CHECKING:
+        # To preserve static checking for undefined/misspelled attributes while letting
+        # the methods' bodies be type-checked, these are defined as non-special methods,
+        # then bound to special names out of view of static type checkers.
+        __getattribute__ = __getattribute
+        __setattr__ = __setattr
+
 
 class Git(metaclass=_GitMeta):
     """The Git class manages communication with the Git binary.

From c5d5b16bfb4829dd510e3ff258b3daca4cd3b57d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 14:20:54 -0400
Subject: [PATCH 776/994] Clarify that the private name mangling is intentional

---
 git/cmd.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index ce19e733c..de5c6929b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -348,7 +348,8 @@ def __setattr(cls, name: str, value: Any) -> Any:
     if not TYPE_CHECKING:
         # To preserve static checking for undefined/misspelled attributes while letting
         # the methods' bodies be type-checked, these are defined as non-special methods,
-        # then bound to special names out of view of static type checkers.
+        # then bound to special names out of view of static type checkers. (The original
+        # names invoke name mangling (leading "__") to avoid confusion in other scopes.)
         __getattribute__ = __getattribute
         __setattr__ = __setattr
 

From 84bf2ca034721cd54e792f57585083a1dbffc6ea Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 15:25:20 -0400
Subject: [PATCH 777/994] Read USE_SHELL in Git.execute without
 DeprecationWarning

This changes how Git.execute itself accesses the USE_SHELL
attribute, so that its own read of it does not issue a warning.
---
 git/cmd.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index de5c6929b..eed82d7dd 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1189,7 +1189,12 @@ def execute(
 
         stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
         if shell is None:
-            shell = self.USE_SHELL
+            # Get the value of USE_SHELL with no deprecation warning. Do this without
+            # warnings.catch_warnings, to avoid a race condition with application code
+            # configuring warnings. The value could be looked up in type(self).__dict__
+            # or Git.__dict__, but those can break under some circumstances. This works
+            # the same as self.USE_SHELL in more situations; see Git.__getattribute__.
+            shell = super().__getattribute__("USE_SHELL")
         _logger.debug(
             "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
             redacted_command,

From 5bef7ed7369e1d6f93161607e01dc02bcd1720ab Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 28 Mar 2024 23:35:03 -0400
Subject: [PATCH 778/994] Add GitPython project top comments to new test
 modules

---
 test/deprecation/test_cmd_git.py  | 3 +++
 test/deprecation/test_compat.py   | 3 +++
 test/deprecation/test_toplevel.py | 3 +++
 test/deprecation/test_types.py    | 3 +++
 4 files changed, 12 insertions(+)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index d312b8778..b15f219d8 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Tests for static and dynamic characteristics of Git class and instance attributes.
 
 Currently this all relates to the deprecated :attr:`Git.USE_SHELL` class attribute,
diff --git a/test/deprecation/test_compat.py b/test/deprecation/test_compat.py
index 22f733083..2d7805e61 100644
--- a/test/deprecation/test_compat.py
+++ b/test/deprecation/test_compat.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Tests for dynamic and static characteristics of git.compat module attributes.
 
 These tests verify that the is_<platform> attributes are available, and are even listed
diff --git a/test/deprecation/test_toplevel.py b/test/deprecation/test_toplevel.py
index 54dc8e358..740408193 100644
--- a/test/deprecation/test_toplevel.py
+++ b/test/deprecation/test_toplevel.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Tests for dynamic and static characteristics of top-level git module attributes.
 
 Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
diff --git a/test/deprecation/test_types.py b/test/deprecation/test_types.py
index cd53fa210..f97375a85 100644
--- a/test/deprecation/test_types.py
+++ b/test/deprecation/test_types.py
@@ -1,3 +1,6 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 """Tests for dynamic and static characteristics of git.types module attributes."""
 
 import sys

From 3da47c26db44f287fa2b10e95995689de1f578bc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Mar 2024 11:46:38 -0400
Subject: [PATCH 779/994] Hide `del util` from type checkers

Even though current type checkers don't seem to need it.

As noted in dffa930, it appears neither mypy nor pyright currently
needs `del util` to be guarded by `if not TYPE_CHECKING:` -- they
currently treat util as bound even without it, even with `del util`
present.

It is not obvious that this is the best type checker behavior or
that type checkers will continue to behave this way. (In addition,
it may help humans for all statements whose effects are intended
not to be considered for purposes of static typing to be guarded by
`if not TYPE_CHECKING:`.) So this guards the deletion of util the
same as the binding of __getattr__.

This also moves, clarifies, and expands the commented explanations
of what is going on.
---
 git/__init__.py | 54 +++++++++++++++++++++++++++++--------------------
 1 file changed, 32 insertions(+), 22 deletions(-)

diff --git a/git/__init__.py b/git/__init__.py
index fd1b87707..1b2360e3a 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -169,6 +169,8 @@
         IndexEntry,
         IndexFile,
         StageType,
+        # NOTE: This tells type checkers what util resolves to. We delete it, and it is
+        # really resolved by __getattr__, which warns. See below on what to use instead.
         util,
     )
     from git.util import (  # @NoMove
@@ -183,27 +185,6 @@
     raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
 
 
-# NOTE: The expression `git.util` evaluates to git.index.util and `from git import util`
-# imports git.index.util, NOT git.util. It may not be feasible to change this until the
-# next major version, to avoid breaking code inadvertently relying on it.
-#
-# - If git.index.util *is* what you want, use or import from that, to avoid confusion.
-#
-# - To use the "real" git.util module, write `from git.util import ...`, or if necessary
-#   access it as `sys.modules["git.util"]`.
-#
-# (This differs from other indirect-submodule imports that are unambiguously non-public
-# and subject to immediate removal. Here, the public git.util module, though different,
-# makes less discoverable that the expression `git.util` refers to a non-public
-# attribute of the git module.)
-#
-# This had come about by a wildcard import. Now that all intended imports are explicit,
-# the intuitive but potentially incompatible binding occurs due to the usual rules for
-# Python submodule bindings. So for now we delete that and let __getattr__ handle it.
-#
-del util
-
-
 def _warned_import(message: str, fullname: str) -> "ModuleType":
     import importlib
 
@@ -242,7 +223,36 @@ def _getattr(name: str) -> Any:
     raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
 
-if not TYPE_CHECKING:  # Preserve static checking for undefined/misspelled attributes.
+if not TYPE_CHECKING:
+    # NOTE: The expression `git.util` gives git.index.util and `from git import util`
+    # imports git.index.util, NOT git.util. It may not be feasible to change this until
+    # the next major version, to avoid breaking code inadvertently relying on it.
+    #
+    # - If git.index.util *is* what you want, use (or import from) that, to avoid
+    #   confusion.
+    #
+    # - To use the "real" git.util module, write `from git.util import ...`, or if
+    #   necessary access it as `sys.modules["git.util"]`.
+    #
+    # Note also that `import git.util` technically imports the "real" git.util... but
+    # the *expression* `git.util` after doing so is still git.index.util!
+    #
+    # (This situation differs from that of other indirect-submodule imports that are
+    # unambiguously non-public and subject to immediate removal. Here, the public
+    # git.util module, though different, makes less discoverable that the expression
+    # `git.util` refers to a non-public attribute of the git module.)
+    #
+    # This had originally come about by a wildcard import. Now that all intended imports
+    # are explicit, the intuitive but potentially incompatible binding occurs due to the
+    # usual rules for Python submodule bindings. So for now we replace that binding with
+    # git.index.util, delete that, and let __getattr__ handle it and issue a warning.
+    #
+    # For the same runtime behavior, it would be enough to forgo importing util, and
+    # delete util as created naturally; __getattr__ would behave the same. But type
+    # checkers would not know what util refers to when accessed as an attribute of git.
+    del util
+
+    # This is "hidden" to preserve static checking for undefined/misspelled attributes.
     __getattr__ = _getattr
 
 # { Initialize git executable path

From bdabb21fe62063ce51fcae6b5f499f10f55525c5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Mar 2024 18:52:10 -0400
Subject: [PATCH 780/994] Expand USE_SHELL docstring; clarify a test usage

In the USE_SHELL docstring:

- Restore the older wording "when executing git commands" rather
  than "to execute git commands". I've realized that longer phrase,
  which dates back to the introduction of USE_SHELL in 1c2dd54, is
  clearer, because readers less familiar with GitPython's overall
  design and operation will still not be misled into thinking
  USE_SHELL ever affects whether GitPython uses an external git
  command, versus some other mechanism, to do something.

- Give some more information about why USE_SHELL = True is unsafe
  (though further revision or clarification may be called for).

- Note some non-obvious aspects of USE_SHELL, that some of the way
  it interacts with other aspects of GitPython is not part of what
  is or has been documented about it, and in practice changes over
  time. The first example relates to #1792; the second may help
  users understand why code that enables USE_SHELL on Windows, in
  addition to being unsafe there, often breaks immediately on other
  platforms; the third is included so the warnings in the expanded
  docstring are not interpreted as a new commitment that any shell
  syntax that may have a *desired* effect in some application will
  continue to have the same effect in the future.

- Cover a second application that might lead, or have led, to
  setting USE_SHELL to True, and explain what to do instead.

In test_successful_default_refresh_invalidates_cached_version_info:

- Rewrite the commented explanation of a special variant of that
  second application, where the usual easy alternatives are not
  used because part of the goal of the test is to check a "default"
  scenario that does not include either of them. This better
  explains why that choice is made in the test, and also hopefully
  will prevent anyone from thinking that test is a model for
  another situation (which, as noted, is unlikely to be the case
  even in unit tests).
---
 git/cmd.py       | 40 +++++++++++++++++++++++++++++++---------
 test/test_git.py | 14 +++++++-------
 2 files changed, 38 insertions(+), 16 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index eed82d7dd..42e6e927c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -405,23 +405,45 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     """Enables debugging of GitPython's git commands."""
 
     USE_SHELL: bool = False
-    """Deprecated. If set to ``True``, a shell will be used to execute git commands.
+    """Deprecated. If set to ``True``, a shell will be used when executing git commands.
+
+    Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
+    functions should be updated to use the default value of ``False`` instead. ``True``
+    is unsafe unless the effect of syntax treated specially by the shell is fully
+    considered and accounted for, which is not possible under most circumstances. As
+    detailed below, it is also no longer needed, even where it had been in the past.
+
+    In addition, how a value of ``True`` interacts with some aspects of GitPython's
+    operation is not precisely specified and may change without warning, even before
+    GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
+
+    * Whether or how GitPython automatically customizes the shell environment.
+
+    * Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
+      separate arguments even when ``shell=True``), this can be used with any GitPython
+      functionality other than direct calls to the :meth:`execute` method.
+
+    * Whether any GitPython feature that runs git commands ever attempts to partially
+      sanitize data a shell may treat specially. Currently this is not done.
 
     Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
     in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
     GitPython solves that problem more robustly and safely by using the
     ``CREATE_NO_WINDOW`` process creation flag on Windows.
 
-    Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
-    functions should be updated to use the default value of ``False`` instead. ``True``
-    is unsafe unless the effect of shell expansions is fully considered and accounted
-    for, which is not possible under most circumstances.
+    Because Windows path search differs subtly based on whether a shell is used, in rare
+    cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
+    such as a batch file, from being found. To fix this, set the command name or full
+    path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
+    full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
 
-    See:
+    Further reading:
 
-    - :meth:`Git.execute` (on the ``shell`` parameter).
-    - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
-    - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
+    * :meth:`Git.execute` (on the ``shell`` parameter).
+    * https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
+    * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
+    * https://github.com/python/cpython/issues/91558#issuecomment-1100942950
+    * https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
     """
 
     _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
diff --git a/test/test_git.py b/test/test_git.py
index 112e2f0eb..94e68ecf0 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -669,13 +669,13 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
             stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
 
             if sys.platform == "win32":
-                # On Windows, use a shell so "git" finds "git.cmd". (In the infrequent
-                # case that this effect is desired in production code, it should not be
-                # done with this technique. USE_SHELL=True is less secure and reliable,
-                # as unintended shell expansions can occur, and is deprecated. Instead,
-                # use a custom command, by setting the GIT_PYTHON_GIT_EXECUTABLE
-                # environment variable to git.cmd or by passing git.cmd's full path to
-                # git.refresh. Or wrap the script with a .exe shim.)
+                # On Windows, use a shell so "git" finds "git.cmd". The correct and safe
+                # ways to do this straightforwardly are to set GIT_PYTHON_GIT_EXECUTABLE
+                # to git.cmd in the environment, or call git.refresh with the command's
+                # full path. See the Git.USE_SHELL docstring for deprecation details.
+                # But this tests a "default" scenario where neither is done. The
+                # approach used here, setting USE_SHELL to True so PATHEXT is honored,
+                # should not be used in production code (nor even in most test cases).
                 stack.enter_context(mock.patch.object(Git, "USE_SHELL", True))
 
             new_git = Git()

From b51b08052821f91a61a40808328aed0ac35935a8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Mar 2024 19:51:32 -0400
Subject: [PATCH 781/994] Explain the approach in test.deprecation to static
 checking

And why this increases the importance of the warn_unused_ignored
mypy configuration option.
---
 pyproject.toml                   |  2 +-
 test/deprecation/__init__.py     | 17 +++++++++++++++++
 test/deprecation/test_cmd_git.py |  2 +-
 3 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 5eac2be09..6cb05f96e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,7 @@ files = ["git/", "test/deprecation/"]
 disallow_untyped_defs = true
 no_implicit_optional = true
 warn_redundant_casts = true
-warn_unused_ignores = true
+warn_unused_ignores = true  # Useful in general, but especially in test/deprecation.
 warn_unreachable = true
 implicit_reexport = true
 # strict = true
diff --git a/test/deprecation/__init__.py b/test/deprecation/__init__.py
index 56b5d89db..fec3126d2 100644
--- a/test/deprecation/__init__.py
+++ b/test/deprecation/__init__.py
@@ -1,2 +1,19 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
+"""Tests of deprecation warnings and possible related attribute bugs.
+
+Most deprecation warnings are "basic" in the sense that there is no special complexity
+to consider, in introducing them. However, to issue deprecation warnings on mere
+attribute access can involve adding new dynamic behavior. This can lead to subtle bugs
+or less useful dynamic metadata. It can also weaken static typing, as happens if a type
+checker sees a method like ``__getattr__`` in a module or class whose attributes it did
+not already judge to be dynamic. This test.deprecation submodule covers all three cases:
+the basic cases, subtle dynamic behavior, and subtle static type checking issues.
+
+Static type checking is "tested" by a combination of code that should not be treated as
+a type error but would be in the presence of particular bugs, and code that *should* be
+treated as a type error and is accordingly marked ``# type: ignore[REASON]`` (for
+specific ``REASON``. The latter will only produce mypy errors when the expectation is
+not met if it is configured with ``warn_unused_ignores = true``.
+"""
diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index b15f219d8..3f87037a0 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Tests for static and dynamic characteristics of Git class and instance attributes.
+"""Tests for dynamic and static characteristics of Git class and instance attributes.
 
 Currently this all relates to the deprecated :attr:`Git.USE_SHELL` class attribute,
 which can also be accessed through instances. Some tests directly verify its behavior,

From 7cd3aa913077d55bbdb7ca01e6b7df593121f643 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Mar 2024 22:55:36 -0400
Subject: [PATCH 782/994] Make test.performance.lib docstring more specific

To distinguish it from the more general test.lib as well as from
the forthcoming test.deprecation.lib.
---
 test/performance/lib.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/performance/lib.py b/test/performance/lib.py
index 2ca3c409b..c24599986 100644
--- a/test/performance/lib.py
+++ b/test/performance/lib.py
@@ -1,7 +1,7 @@
 # This module is part of GitPython and is released under the
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
-"""Support library for tests."""
+"""Support library for performance tests."""
 
 import logging
 import os

From cf2576e406006ec28bcc85565a7ef85864cbd39e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 29 Mar 2024 23:15:52 -0400
Subject: [PATCH 783/994] Make/use test.deprecation.lib; abandon idea to filter
 by module

This creates a module test.deprecation.lib in the test suite for
a couple general helpers used in deprecation tests, one of which is
now used in two of the test modules and the other of which happens
only to be used in one but is concepually related to the first.

The assert_no_deprecation_warning context manager function fixes
two different flawed approaches to that, which were in use earlier:

- In test_basic, only DeprecationWarning and not the less
  significant PendingDeprecationWarning had been covere, which it
  should, at least for symmetry, since pytest.deprecated_call()
  treats it like DeprecationWarning.

  There was also a comment expressing a plan to make it filter only
  for warnings originating from GitPython, which was a flawed idea,
  as detailed below.

- In test_cmd_git, the flawed idea of attempting to filter only for
  warnings originating from GitPython was implemented. The problem
  with this is that it is heavily affected by stacklevel and its
  interaction with the pattern of calls through which the warning
  arises: warnings could actually emanate from code in GitPython's
  git module, but be registered as having come from test code, a
  callback in gitdb, smmap, or the standard library, or even the
  pytest test runner. Some of these are more likely than others,
  but all are possible especially considering the possibility of a
  bug in the value passed to warning.warn as stacklevel. (It may be
  valuable for such bugs to cause tests to fail, but they should not
  cause otherwise failing tests to wrongly pass.)

  It is probably feasible to implement such filtering successfully,
  but I don't think it's worthwhile for the small reduction in
  likelihood of failing later on an unrealted DeprecationWarning
  from somewhere else, especially considering that GitPython's main
  dependencies are so minimal.
---
 test/deprecation/lib.py          | 27 +++++++++++++++++++++++++++
 test/deprecation/test_basic.py   | 26 ++++++++------------------
 test/deprecation/test_cmd_git.py | 24 +++++-------------------
 3 files changed, 40 insertions(+), 37 deletions(-)
 create mode 100644 test/deprecation/lib.py

diff --git a/test/deprecation/lib.py b/test/deprecation/lib.py
new file mode 100644
index 000000000..9fe623a3a
--- /dev/null
+++ b/test/deprecation/lib.py
@@ -0,0 +1,27 @@
+# This module is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
+"""Support library for deprecation tests."""
+
+__all__ = ["assert_no_deprecation_warning", "suppress_deprecation_warning"]
+
+import contextlib
+import warnings
+
+from typing import Generator
+
+
+@contextlib.contextmanager
+def assert_no_deprecation_warning() -> Generator[None, None, None]:
+    """Context manager to assert that code does not issue any deprecation warnings."""
+    with warnings.catch_warnings():
+        warnings.simplefilter("error", DeprecationWarning)
+        warnings.simplefilter("error", PendingDeprecationWarning)
+        yield
+
+
+@contextlib.contextmanager
+def suppress_deprecation_warning() -> Generator[None, None, None]:
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", DeprecationWarning)
+        yield
diff --git a/test/deprecation/test_basic.py b/test/deprecation/test_basic.py
index 8ee7e72b1..6235a836c 100644
--- a/test/deprecation/test_basic.py
+++ b/test/deprecation/test_basic.py
@@ -15,9 +15,6 @@
 It is inapplicable to the deprecations whose warnings are tested in this module.
 """
 
-import contextlib
-import warnings
-
 import pytest
 
 from git.diff import NULL_TREE
@@ -25,6 +22,8 @@
 from git.repo import Repo
 from git.util import Iterable as _Iterable, IterableObj
 
+from .lib import assert_no_deprecation_warning
+
 # typing -----------------------------------------------------------------
 
 from typing import Generator, TYPE_CHECKING
@@ -38,15 +37,6 @@
 # ------------------------------------------------------------------------
 
 
-@contextlib.contextmanager
-def _assert_no_deprecation_warning() -> Generator[None, None, None]:
-    """Context manager to assert that code does not issue any deprecation warnings."""
-    with warnings.catch_warnings():
-        # FIXME: Refine this to filter for deprecation warnings from GitPython.
-        warnings.simplefilter("error", DeprecationWarning)
-        yield
-
-
 @pytest.fixture
 def commit(tmp_path: "Path") -> Generator["Commit", None, None]:
     """Fixture to supply a one-commit repo's commit, enough for deprecation tests."""
@@ -72,7 +62,7 @@ def test_diff_renamed_warns(diff: "Diff") -> None:
 
 def test_diff_renamed_file_does_not_warn(diff: "Diff") -> None:
     """The preferred Diff.renamed_file property issues no deprecation warning."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
         diff.renamed_file
 
 
@@ -84,13 +74,13 @@ def test_commit_trailers_warns(commit: "Commit") -> None:
 
 def test_commit_trailers_list_does_not_warn(commit: "Commit") -> None:
     """The nondeprecated Commit.trailers_list property issues no deprecation warning."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
         commit.trailers_list
 
 
 def test_commit_trailers_dict_does_not_warn(commit: "Commit") -> None:
     """The nondeprecated Commit.trailers_dict property issues no deprecation warning."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
         commit.trailers_dict
 
 
@@ -102,7 +92,7 @@ def test_traverse_list_traverse_in_base_class_warns(commit: "Commit") -> None:
 
 def test_traversable_list_traverse_override_does_not_warn(commit: "Commit") -> None:
     """Calling list_traverse on concrete subclasses is not deprecated, does not warn."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
         commit.list_traverse()
 
 
@@ -114,7 +104,7 @@ def test_traverse_traverse_in_base_class_warns(commit: "Commit") -> None:
 
 def test_traverse_traverse_override_does_not_warn(commit: "Commit") -> None:
     """Calling traverse on concrete subclasses is not deprecated, does not warn."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
         commit.traverse()
 
 
@@ -128,7 +118,7 @@ class Derived(_Iterable):
 
 def test_iterable_obj_inheriting_does_not_warn() -> None:
     """Subclassing git.util.IterableObj is not deprecated, does not warn."""
-    with _assert_no_deprecation_warning():
+    with assert_no_deprecation_warning():
 
         class Derived(IterableObj):
             pass
diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 3f87037a0..6c592454a 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -56,12 +56,10 @@
 metaclass marginal enough, to justify introducing a metaclass to issue the warnings.
 """
 
-import contextlib
 import logging
 import sys
 from typing import Generator
 import unittest.mock
-import warnings
 
 if sys.version_info >= (3, 11):
     from typing import assert_type
@@ -73,6 +71,8 @@
 
 from git.cmd import Git
 
+from .lib import assert_no_deprecation_warning, suppress_deprecation_warning
+
 _USE_SHELL_DEPRECATED_FRAGMENT = "Git.USE_SHELL is deprecated"
 """Text contained in all USE_SHELL deprecation warnings, and starting most of them."""
 
@@ -82,13 +82,6 @@
 _logger = logging.getLogger(__name__)
 
 
-@contextlib.contextmanager
-def _suppress_deprecation_warning() -> Generator[None, None, None]:
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore", DeprecationWarning)
-        yield
-
-
 @pytest.fixture
 def restore_use_shell_state() -> Generator[None, None, None]:
     """Fixture to attempt to restore state associated with the USE_SHELL attribute.
@@ -129,7 +122,7 @@ def restore_use_shell_state() -> Generator[None, None, None]:
         # Try to save the original public value. Rather than attempt to restore a state
         # where the attribute is not set, if we cannot do this we allow AttributeError
         # to propagate out of the fixture, erroring the test case before its code runs.
-        with _suppress_deprecation_warning():
+        with suppress_deprecation_warning():
             old_public_value = Git.USE_SHELL
 
         # This doesn't have its own try-finally because pytest catches exceptions raised
@@ -137,7 +130,7 @@ def restore_use_shell_state() -> Generator[None, None, None]:
         yield
 
         # Try to restore the original public value.
-        with _suppress_deprecation_warning():
+        with suppress_deprecation_warning():
             Git.USE_SHELL = old_public_value
     finally:
         # Try to restore the original private state.
@@ -323,14 +316,7 @@ def test_execute_without_shell_arg_does_not_warn() -> None:
     USE_SHELL is to be used, the way Git.execute itself accesses USE_SHELL does not
     issue a deprecation warning.
     """
-    with warnings.catch_warnings():
-        for category in DeprecationWarning, PendingDeprecationWarning:
-            warnings.filterwarnings(
-                action="error",
-                category=category,
-                module=r"git(?:\.|$)",
-            )
-
+    with assert_no_deprecation_warning():
         Git().version()
 
 

From c7675d2cedcd737f20359a4a786e213510452413 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sat, 30 Mar 2024 08:56:41 +0100
Subject: [PATCH 784/994] update security policy, to use GitHub instead of
 email

---
 SECURITY.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index cbfaafdde..d39425b70 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -11,5 +11,4 @@ Only the latest version of GitPython can receive security updates. If a vulnerab
 
 ## Reporting a Vulnerability
 
-Please report private portions of a vulnerability to sebastian.thiel@icloud.com that would help to reproduce and fix it. To receive updates on progress and provide
-general information to the public, you can create an issue [on the issue tracker](https://github.com/gitpython-developers/GitPython/issues).
+Please report private portions of a vulnerability to <https://github.com/gitpython-developers/GitPython/security/advisories/new>. Doing so helps to receive updates and collaborate on the matter, without disclosing it publicliy right away.

From f92f4c3bf902c7fc3887cfd969b3e54f581f18f8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 30 Mar 2024 20:16:45 -0400
Subject: [PATCH 785/994] Clarify security risk in USE_SHELL doc and warnings

---
 git/cmd.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 42e6e927c..b2829801f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -314,10 +314,10 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 )
 
 _USE_SHELL_DANGER_MESSAGE = (
-    "Setting Git.USE_SHELL to True is unsafe and insecure, and should be avoided, "
-    "because the effect of shell metacharacters and shell expansions cannot usually be "
-    "accounted for. In addition, Git.USE_SHELL is deprecated and will be removed in a "
-    "future release."
+    "Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
+    "shell syntax cannot usually be accounted for. This can result in a command "
+    "injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
+    "and will be removed in a future release."
 )
 
 
@@ -413,6 +413,13 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
     considered and accounted for, which is not possible under most circumstances. As
     detailed below, it is also no longer needed, even where it had been in the past.
 
+    It is in many if not most cases a command injection vulnerability for an application
+    to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
+    fragment of text to make its way into any part of any argument to any git command
+    (including paths, branch names, etc.) can cause the shell to read and write
+    arbitrary files and execute arbitrary commands. Innocent input may also accidentally
+    contain special shell syntax, leading to inadvertent malfunctions.
+
     In addition, how a value of ``True`` interacts with some aspects of GitPython's
     operation is not precisely specified and may change without warning, even before
     GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:

From 8327b458bdcf73895aa3b0a9a990a61ce2e54ee9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 16:39:20 -0400
Subject: [PATCH 786/994] Test GitMeta alias

---
 test/deprecation/test_cmd_git.py | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_cmd_git.py b/test/deprecation/test_cmd_git.py
index 6c592454a..e44490273 100644
--- a/test/deprecation/test_cmd_git.py
+++ b/test/deprecation/test_cmd_git.py
@@ -69,7 +69,7 @@
 import pytest
 from pytest import WarningsRecorder
 
-from git.cmd import Git
+from git.cmd import Git, GitMeta
 
 from .lib import assert_no_deprecation_warning, suppress_deprecation_warning
 
@@ -377,3 +377,15 @@ def test_instance_dir() -> None:
     instance = Git()
     actual = set(dir(instance))
     assert _EXPECTED_DIR_SUBSET <= actual
+
+
+def test_metaclass_alias() -> None:
+    """GitMeta aliases Git's metaclass, whether that is type or a custom metaclass."""
+
+    def accept_metaclass_instance(cls: GitMeta) -> None:
+        """Check that cls is statically recognizable as an instance of GitMeta."""
+
+    accept_metaclass_instance(Git)  # assert_type would expect Type[Git], not GitMeta.
+
+    # This comes after the static check, just in case it would affect the inference.
+    assert type(Git) is GitMeta

From f6060df576acda613227a57f03c01d235eceaeae Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 27 Mar 2024 17:38:31 -0400
Subject: [PATCH 787/994] Add GitMeta alias

Hopefully no one will ever need it.
---
 git/cmd.py | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/git/cmd.py b/git/cmd.py
index b2829801f..90fc39cd6 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -5,7 +5,7 @@
 
 from __future__ import annotations
 
-__all__ = ["Git"]
+__all__ = ["GitMeta", "Git"]
 
 import contextlib
 import io
@@ -354,6 +354,32 @@ def __setattr(cls, name: str, value: Any) -> Any:
         __setattr__ = __setattr
 
 
+GitMeta = _GitMeta
+"""Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
+
+Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
+custom metaclass is not documented and may change at any time. This statically checkable
+metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
+used. Code that benefits from it is likely to be remain brittle even if it is used.
+
+In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
+callable attributes representing git subcommands, it rarely makes sense to inherit from
+:class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
+to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
+as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
+might have to be introduced to solve a metaclass conflict, is not recommended.
+
+:note:
+    The correct static type of the :class:`Git` class itself, and any subclasses, is
+    ``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
+
+    :class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
+    intended or otherwise possible to use. This alias is truly only for very rare and
+    inherently precarious situations where it is necessary to deal with the metaclass
+    explicitly.
+"""
+
+
 class Git(metaclass=_GitMeta):
     """The Git class manages communication with the Git binary.
 

From 53640535cf8314366a01da081947dd8504a299cd Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sun, 31 Mar 2024 10:05:27 +0200
Subject: [PATCH 788/994] bump version to 3.1.43

---
 VERSION                | 2 +-
 doc/source/changes.rst | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/VERSION b/VERSION
index f69b7f25e..d1bf6638d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.42
+3.1.43
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index c75e5eded..0bc757134 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,15 @@
 Changelog
 =========
 
+3.1.43
+======
+
+A major visible change will be the added deprecation- or user-warnings,
+and greatly improved typing.
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.43
+
 3.1.42
 ======
 

From 83bed19f360b69dad26b7d9b00ffd837c8075b7a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 31 Mar 2024 14:57:52 -0400
Subject: [PATCH 789/994] Fix wording of comment about the /cygdrive prefix

---
 git/repo/fun.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/repo/fun.py b/git/repo/fun.py
index e44d9c644..182cf82ed 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -112,7 +112,7 @@ def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
             path = content[8:]
 
             if Git.is_cygwin():
-                # Cygwin creates submodules prefixed with `/cygdrive/...` suffixes.
+                # Cygwin creates submodules prefixed with `/cygdrive/...`.
                 # Cygwin git understands Cygwin paths much better than Windows ones.
                 # Also the Cygwin tests are assuming Cygwin paths.
                 path = cygpath(path)

From 988d97bf12c5b15ff4693a5893134271dd8d8a28 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 31 Mar 2024 15:40:47 -0400
Subject: [PATCH 790/994] Fix typo in _get_exe_extensions PATHEXT fallback

PATHEXT lists file extensions with the ".". In the fallback given
in _get_exe_extensions, the other extensions had this, but ".COM"
was listed without the ".". This fixes that.

This is very minor because _get_exe_extensions is nonpublic and not
currently used on native Windows, which is the platform where the
PATHEXT fallback code would be used.

Specifically, _get_exe_extensions is called only in py_where, which
while named with no leading underscore is nonpublic do not being
(and never having been) listed in __all__. As its docstring states,
it is an implementation detail of is_cygwin_git and not intended
for any other use. More specifically, is_cygwin_git currently
immediately returns False on *native* Windows (even if the git
executable GitPython is using is a Cygwin git executable). Only on
Cygwin, or other systems that are not native Windows, does it try
to check the git executable (by calling its _is_cygwin_git helper,
which uses py_where).
---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 8c1c26012..11f963e02 100644
--- a/git/util.py
+++ b/git/util.py
@@ -339,7 +339,7 @@ def _get_exe_extensions() -> Sequence[str]:
     if PATHEXT:
         return tuple(p.upper() for p in PATHEXT.split(os.pathsep))
     elif sys.platform == "win32":
-        return (".BAT", "COM", ".EXE")
+        return (".BAT", ".COM", ".EXE")
     else:
         return ()
 

From f18df8edcf5de5971e4dd01f15ad32411e38244e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 31 Mar 2024 18:07:32 -0400
Subject: [PATCH 791/994] Don't pass --disable-warnings to pytest

---
 pyproject.toml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 6cb05f96e..ee54edb78 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools"]
 build-backend = "setuptools.build_meta"
 
 [tool.pytest.ini_options]
-addopts = "--cov=git --cov-report=term --disable-warnings -ra"
+addopts = "--cov=git --cov-report=term -ra"
 filterwarnings = "ignore::DeprecationWarning"
 python_files = "test_*.py"
 tmp_path_retention_policy = "failed"
@@ -13,7 +13,6 @@ testpaths = "test"  # Space separated list of paths from root e.g test tests doc
 # --cov-report term-missing # to terminal with line numbers
 # --cov-report html:path  # html file at path
 # --maxfail  # number of errors before giving up
-# -disable-warnings  # Disable pytest warnings (not codebase warnings)
 # -rfE  # default test summary: list fail and error
 # -ra   # test summary: list all non-passing (fail, error, skip, xfail, xpass)
 # --ignore-glob=**/gitdb/*  # ignore glob paths

From 0152b528a035566364fc8a0e1a80829c6d495301 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Mon, 1 Apr 2024 14:53:43 -0400
Subject: [PATCH 792/994] Update the comment about `--mixed` and paths

This updates the comment in HEAD.reset about why `--mixed` is
omitted from the git command constructed to perform a reset where
paths are being passed, adding specific information about the git
versions where this is deprecated, and changing the more-info link
from an old GitPython issue that is no longer retrievable to #1876.
---
 git/refs/head.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/refs/head.py b/git/refs/head.py
index 7e6fc3377..683634451 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -99,8 +99,8 @@ def reset(
         if index:
             mode = "--mixed"
 
-            # It appears some git versions declare mixed and paths deprecated.
-            # See http://github.com/Byron/GitPython/issues#issue/2.
+            # Explicit "--mixed" when passing paths is deprecated since git 1.5.4.
+            # See https://github.com/gitpython-developers/GitPython/discussions/1876.
             if paths:
                 mode = None
             # END special case

From a9593c7c56e76bdef35245be00bf23abdc3ba4c0 Mon Sep 17 00:00:00 2001
From: Eduard Talanov <89387701+EduardTalanov@users.noreply.github.com>
Date: Fri, 5 Apr 2024 10:10:53 +0200
Subject: [PATCH 793/994] Update remote.py

Fixed the error of updating shallow submodules
---
 git/remote.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/remote.py b/git/remote.py
index f2ecd0f36..37c991d27 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -316,7 +316,7 @@ class FetchInfo(IterableObj):
         ERROR,
     ) = [1 << x for x in range(8)]
 
-    _re_fetch_result = re.compile(r"^ *(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)(    \(.*\)?$)?")
+    _re_fetch_result = re.compile(r"^ *(?:.{0,3})(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)(    \(.*\)?$)?")
 
     _flag_map: Dict[flagKeyLiteral, int] = {
         "!": ERROR,

From 55c30a34185e13c31ab14c39f7a7dd0db3a494e4 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 11 Apr 2024 19:55:10 -0400
Subject: [PATCH 794/994] OSS-Fuzz test initial migration

Migrates the OSS-Fuzz tests and setup scripts from the OSS-Fuzz
repository to GitPython's repo as discussed here:
https://github.com/gitpython-developers/GitPython/issues/1887#issuecomment-2028599381

These files include the changes that were originally proposed in:
https://github.com/google/oss-fuzz/pull/11763

Additional changes include:
- A first pass at documenting the contents of the fuzzing set up in a
  dedicated README.md
- Adding the dictionary files to this repo for improved visibility. Seed
  corpra zips are still located in an external repo pending further
 discussion regarding where those should live in the long term.
---
 fuzzing/README.md                             | 172 ++++++++++++++++++
 fuzzing/dictionaries/fuzz_config.dict         |  56 ++++++
 fuzzing/dictionaries/fuzz_tree.dict           |  13 ++
 fuzzing/fuzz-targets/fuzz_config.py           |  51 ++++++
 fuzzing/fuzz-targets/fuzz_tree.py             |  59 ++++++
 fuzzing/oss-fuzz-scripts/build.sh             |  37 ++++
 .../container-environment-bootstrap.sh        |  56 ++++++
 7 files changed, 444 insertions(+)
 create mode 100644 fuzzing/README.md
 create mode 100644 fuzzing/dictionaries/fuzz_config.dict
 create mode 100644 fuzzing/dictionaries/fuzz_tree.dict
 create mode 100644 fuzzing/fuzz-targets/fuzz_config.py
 create mode 100644 fuzzing/fuzz-targets/fuzz_tree.py
 create mode 100644 fuzzing/oss-fuzz-scripts/build.sh
 create mode 100644 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh

diff --git a/fuzzing/README.md b/fuzzing/README.md
new file mode 100644
index 000000000..6853c5002
--- /dev/null
+++ b/fuzzing/README.md
@@ -0,0 +1,172 @@
+# Fuzzing GitPython
+
+[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/gitpython.svg)][oss-fuzz-issue-tracker]
+
+This directory contains files related to GitPython's suite of fuzz tests that are executed daily on automated
+infrastructure provided by [OSS-Fuzz][oss-fuzz-repo]. This document aims to provide necessary information for working
+with fuzzing in GitPython.
+
+The details about the latest OSS-Fuzz test status, including build logs and coverage reports, is made available
+at [this link](https://introspector.oss-fuzz.com/project-profile?project=gitpython).
+
+## How to Contribute
+
+There are many ways to contribute to GitPython's fuzzing efforts! Contributions are welcomed through issues,
+discussions, or pull requests on this repository.
+
+Areas that are particularly appreciated include:
+
+- **Tackling the existing backlog of open issues**. While fuzzing is an effective way to identify bugs, that information
+  isn't useful unless they are fixed. If you are not sure where to start, the issues tab is a great place to get ideas!
+- **Improvements to this (or other) documentation** make it easier for new contributors to get involved, so even small
+  improvements can have a large impact over time. If you see something that could be made easier by a documentation
+  update of any size, please consider suggesting it!
+
+For everything else, such as expanding test coverage, optimizing test performance, or enhancing error detection
+capabilities, jump in to the "Getting Started" section below.
+
+## Getting Started with Fuzzing GitPython
+
+> [!TIP]
+> **New to fuzzing or unfamiliar with OSS-Fuzz?**
+>
+> These resources are an excellent place to start:
+>
+> - [OSS-Fuzz documentation][oss-fuzz-docs] - Continuous fuzzing service for open source software.
+> - [Google/fuzzing][google-fuzzing-repo] - Tutorials, examples, discussions, research proposals, and other resources
+    related to fuzzing.
+> - [CNCF Fuzzing Handbook](https://github.com/cncf/tag-security/blob/main/security-fuzzing-handbook/handbook-fuzzing.pdf) -
+    A comprehensive guide for fuzzing open source software.
+> - [Efficient Fuzzing Guide by The Chromium Project](https://chromium.googlesource.com/chromium/src/+/main/testing/libfuzzer/efficient_fuzzing.md) -
+    Explores strategies to enhance the effectiveness of your fuzz tests, recommended for those looking to optimize their
+    testing efforts.
+
+### Setting Up Your Local Environment
+
+Before contributing to fuzzing efforts, ensure Python and Docker are installed on your machine. Docker is required for
+running fuzzers in containers provided by OSS-Fuzz. [Install Docker](https://docs.docker.com/get-docker/) following the
+official guide if you do not already have it.
+
+### Understanding Existing Fuzz Targets
+
+Review the `fuzz-targets/` directory to familiarize yourself with how existing tests are implemented. See
+the [Files & Directories Overview](#files--directories-overview) for more details on the directory structure.
+
+### Contributing to Fuzz Tests
+
+Start by reviewing the [Atheris documentation][atheris-repo] and the section
+on [Running Fuzzers Locally](#running-fuzzers-locally) to begin writing or improving fuzz tests.
+
+## Files & Directories Overview
+
+The `fuzzing/` directory is organized into three key areas:
+
+### Fuzz Targets (`fuzz-targets/`)
+
+Contains Python files for each fuzz test, targeting specific functionalities of GitPython.
+
+**Things to Know**:
+
+- Each fuzz test targets a specific part of GitPython's functionality.
+- Test files adhere to the naming convention: `fuzz_<API Under Test>.py`, where `<API Under Test>` indicates the
+  functionality targeted by the test.
+- Any functionality that involves performing operations on input data is a possible candidate for fuzz testing, but
+  features that involve processing untrusted user input or parsing operations are typically going to be the most
+  interesting.
+- The goal of these tests is to identify previously unknown or unexpected error cases caused by a given input. For that
+  reason, fuzz tests should gracefully handle anticipated exception cases with a `try`/`except` block to avoid false
+  positives that halt the fuzzing engine.
+
+### Dictionaries (`dictionaries/`)
+
+Provides hints to the fuzzing engine about inputs that might trigger unique code paths. Each fuzz target may have a
+corresponding `.dict` file. For details on how these are used, refer
+to [LibFuzzer documentation](https://llvm.org/docs/LibFuzzer.html#dictionaries).
+
+**Things to Know**:
+
+- OSS-Fuzz loads dictionary files per fuzz target if one exists with the same name, all others are ignored.
+- Most entries in the dictionary files found here are escaped hex or Unicode values that were recommended by the fuzzing
+  engine after previous runs.
+- A default set of dictionary entries are created for all fuzz targets as part of the build process, regardless of an
+  existing file here.
+- Development or updates to dictionaries should reflect the varied formats and edge cases relevant to the
+  functionalities under test.
+- Example dictionaries (some of which are used to build the default dictionaries mentioned above) are can be found here:
+  - [AFL++ dictionary repository](https://github.com/AFLplusplus/AFLplusplus/tree/stable/dictionaries#readme)
+  - [Google/fuzzing dictionary repository](https://github.com/google/fuzzing/tree/master/dictionaries)
+
+### OSS-Fuzz Scripts (`oss-fuzz-scripts/`)
+
+Includes scripts for building and integrating fuzz targets with OSS-Fuzz:
+
+- **`container-environment-bootstrap.sh`** - Sets up the execution environment. It is responsible for fetching default
+  dictionary entries and ensuring all required build dependencies are installed and up-to-date.
+- **`build.sh`** - Executed within the Docker container, this script builds fuzz targets with necessary instrumentation
+  and prepares seed corpora and dictionaries for use.
+
+## Running Fuzzers Locally
+
+### Direct Execution of Fuzz Targets
+
+For quick testing of changes, [Atheris][atheris-repo] makes it possible to execute a fuzz target directly:
+
+1. Install Atheris following the [installation guide][atheris-repo] for your operating system.
+2. Execute a fuzz target, for example:
+
+```shell
+python fuzzing/fuzz-targets/fuzz_config.py
+```
+
+### Running OSS-Fuzz Locally
+
+This approach uses Docker images provided by OSS-Fuzz for building and running fuzz tests locally. It offers
+comprehensive features but requires a local clone of the OSS-Fuzz repository and sufficient disk space for Docker
+containers.
+
+#### Preparation
+
+Set environment variables to simplify command usage:
+
+```shell
+export SANITIZER=address  # Can be either 'address' or 'undefined'.
+export FUZZ_TARGET=fuzz_config # specify the fuzz target without the .py extension.
+```
+
+#### Build and Run
+
+Clone the OSS-Fuzz repository and prepare the Docker environment:
+
+```shell
+git clone --depth 1 https://github.com/google/oss-fuzz.git oss-fuzz
+cd oss-fuzz
+python infra/helper.py build_image gitpython
+python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython
+```
+
+Verify the build of your fuzzers with the optional `check_build` command:
+
+```shell
+python infra/helper.py check_build gitpython
+```
+
+Execute the desired fuzz target:
+
+```shell
+python infra/helper.py run_fuzzer gitpython $FUZZ_TARGET
+```
+
+#### Next Steps
+
+For detailed instructions on advanced features like reproducing OSS-Fuzz issues or using the Fuzz Introspector, refer
+to [the official OSS-Fuzz documentation][oss-fuzz-docs].
+
+[oss-fuzz-repo]: https://github.com/google/oss-fuzz
+
+[oss-fuzz-docs]: https://google.github.io/oss-fuzz
+
+[oss-fuzz-issue-tracker]: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:gitpython
+
+[google-fuzzing-repo]: https://github.com/google/fuzzing
+
+[atheris-repo]: https://github.com/google/atheris
diff --git a/fuzzing/dictionaries/fuzz_config.dict b/fuzzing/dictionaries/fuzz_config.dict
new file mode 100644
index 000000000..b545ddfc8
--- /dev/null
+++ b/fuzzing/dictionaries/fuzz_config.dict
@@ -0,0 +1,56 @@
+"\\004\\000\\000\\000\\000\\000\\000\\000"
+"\\006\\000\\000\\000\\000\\000\\000\\000"
+"_validate_value_"
+"\\000\\000\\000\\000\\000\\000\\000\\000"
+"rem"
+"__eq__"
+"\\001\\000\\000\\000"
+"__abstrac"
+"_mutating_methods_"
+"items"
+"\\0021\\""
+"\\001\\000"
+"\\000\\000\\000\\000"
+"DEFAULT"
+"getfloat"
+"\\004\\000\\000\\000\\000\\000\\000\\000"
+"news"
+"\\037\\000\\000\\000\\000\\000\\000\\000"
+"\\001\\000\\000\\000\\000\\000\\000\\037"
+"\\000\\000\\000\\000\\000\\000\\000\\014"
+"list"
+"\\376\\377\\377\\377\\377\\377\\377\\377"
+"items_all"
+"\\004\\000\\000\\000\\000\\000\\000\\000"
+"\\377\\377\\377\\377\\377\\377\\377\\014"
+"\\001\\000\\000\\000"
+"_acqui"
+"\\000\\000\\000\\000\\000\\000\\000\\000"
+"__ne__"
+"__exit__"
+"__modu"
+"uucp"
+"__str__"
+"\\001\\000\\000\\000"
+"\\017\\000\\000\\000\\000\\000\\000\\000"
+"_has_incl"
+"update"
+"\\377\\377\\377\\377\\377\\377\\377\\023"
+"setdef"
+"setdefaul"
+"\\000\\000\\000\\000"
+"\\001\\000\\000\\000"
+"\\001\\000"
+"\\022\\000\\000\\000\\000\\000\\000\\000"
+"_value_to_string"
+"__abstr"
+"\\001\\000\\000\\000\\000\\000\\000\\000"
+"\\000\\000\\000\\000\\000\\000\\000\\022"
+"\\377\\377\\377\\377"
+"\\004\\000\\000\\000\\000\\000\\000\\000"
+"\\000\\000\\000\\000\\000\\000\\000\\000"
+"\\000\\000\\000\\000\\000\\000\\000\\037"
+"\\001\\000\\000\\000\\000\\000\\000\\013"
+"_OPT_TM"
+"__name__"
+"_get_conv"
diff --git a/fuzzing/dictionaries/fuzz_tree.dict b/fuzzing/dictionaries/fuzz_tree.dict
new file mode 100644
index 000000000..3ebe52b7f
--- /dev/null
+++ b/fuzzing/dictionaries/fuzz_tree.dict
@@ -0,0 +1,13 @@
+"\\001\\000\\000\\000"
+"_join_multiline_va"
+"setdef"
+"1\\000\\000\\000\\000\\000\\000\\000"
+"\\000\\000\\000\\000\\000\\000\\000\\020"
+"\\377\\377\\377\\377\\377\\377\\377r"
+"\\001\\000\\000\\000\\000\\000\\000\\001"
+"\\000\\000\\000\\000\\000\\000\\000\\014"
+"\\000\\000\\000\\000\\000\\000\\000\\003"
+"\\001\\000"
+"\\032\\000\\000\\000\\000\\000\\000\\000"
+"-\\000\\000\\000\\000\\000\\000\\000"
+"__format"
diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
new file mode 100644
index 000000000..1403c96e4
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -0,0 +1,51 @@
+#!/usr/bin/python3
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import atheris
+import sys
+import io
+from configparser import MissingSectionHeaderError, ParsingError
+
+with atheris.instrument_imports():
+    from git import GitConfigParser
+
+
+def TestOneInput(data):
+    sio = io.BytesIO(data)
+    sio.name = "/tmp/fuzzconfig.config"
+    git_config = GitConfigParser(sio)
+    try:
+        git_config.read()
+    except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError):
+        return -1  # Reject inputs raising expected exceptions
+    except (IndexError, ValueError) as e:
+        if isinstance(e, IndexError) and "string index out of range" in str(e):
+            # Known possibility that might be patched
+            # See: https://github.com/gitpython-developers/GitPython/issues/1887
+            pass
+        elif isinstance(e, ValueError) and "embedded null byte" in str(e):
+            # The `os.path.expanduser` function, which does not accept strings
+            # containing null bytes might raise this.
+            return -1
+        else:
+            raise e  # Raise unanticipated exceptions as they might be bugs
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
new file mode 100644
index 000000000..53258fb1e
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_tree.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python3
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import atheris
+import io
+import sys
+import os
+import shutil
+
+with atheris.instrument_imports():
+    from git.objects import Tree
+    from git.repo import Repo
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+    git_dir = "/tmp/.git"
+    head_file = os.path.join(git_dir, "HEAD")
+    refs_dir = os.path.join(git_dir, "refs")
+    common_dir = os.path.join(git_dir, "commondir")
+    objects_dir = os.path.join(git_dir, "objects")
+
+    if os.path.isdir(git_dir):
+        shutil.rmtree(git_dir)
+
+    os.mkdir(git_dir)
+    with open(head_file, "w") as f:
+        f.write(fdp.ConsumeUnicodeNoSurrogates(1024))
+    os.mkdir(refs_dir)
+    os.mkdir(common_dir)
+    os.mkdir(objects_dir)
+
+    _repo = Repo("/tmp/")
+
+    fuzz_tree = Tree(_repo, Tree.NULL_BIN_SHA, 0, "")
+    try:
+        fuzz_tree._deserialize(io.BytesIO(data))
+    except IndexError:
+        return -1
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
new file mode 100644
index 000000000..fdab7a1e0
--- /dev/null
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+python3 -m pip install .
+
+# Directory to look in for dictionaries, options files, and seed corpa:
+SEED_DATA_DIR="$SRC/seed_data"
+
+find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name '*.dict' \) \
+  ! \( -name '__base.*' \) -exec printf 'Copying: %s\n' {} \; \
+  -exec chmod a-x {} \; \
+  -exec cp {} "$OUT" \;
+
+# Build fuzzers in $OUT.
+find "$SRC" -name 'fuzz_*.py' -print0 | while IFS= read -r -d $'\0' fuzz_harness; do
+  compile_python_fuzzer "$fuzz_harness"
+
+  common_base_dictionary_filename="$SEED_DATA_DIR/__base.dict"
+  if [[ -r "$common_base_dictionary_filename" ]]; then
+    # Strip the `.py` extension from the filename and replace it with `.dict`.
+    fuzz_harness_dictionary_filename="$(basename "$fuzz_harness" .py).dict"
+    output_file="$OUT/$fuzz_harness_dictionary_filename"
+
+    printf 'Appending %s to %s\n' "$common_base_dictionary_filename" "$output_file"
+    if [[ -s "$output_file" ]]; then
+      # If a dictionary file for this fuzzer already exists and is not empty,
+      # we append a new line to the end of it before appending any new entries.
+      #
+      # libfuzzer will happily ignore multiple empty lines in a dictionary but crash
+      # if any single line has incorrect syntax (e.g., if we accidentally add two entries to the same line.)
+      # See docs for valid syntax: https://llvm.org/docs/LibFuzzer.html#id32
+      echo >>"$output_file"
+    fi
+    cat "$common_base_dictionary_filename" >>"$output_file"
+  fi
+done
diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
new file mode 100644
index 000000000..43c21a8da
--- /dev/null
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+#################
+# Prerequisites #
+#################
+
+for cmd in python3 git wget rsync; do
+  command -v "$cmd" >/dev/null 2>&1 || {
+    printf '[%s] Required command %s not found, exiting.\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$cmd" >&2
+    exit 1
+  }
+done
+
+SEED_DATA_DIR="$SRC/seed_data"
+mkdir -p "$SEED_DATA_DIR"
+
+#############
+# Functions #
+#############
+
+download_and_concatenate_common_dictionaries() {
+  # Assign the first argument as the target file where all contents will be concatenated
+  target_file="$1"
+
+  # Shift the arguments so the first argument (target_file path) is removed
+  # and only URLs are left for the loop below.
+  shift
+
+  for url in "$@"; do
+    wget -qO- "$url" >>"$target_file"
+    # Ensure there's a newline between each file's content
+    echo >>"$target_file"
+  done
+}
+
+fetch_seed_corpra() {
+  # Seed corpus zip files are hosted in a separate repository to avoid additional bloat in this repo.
+  git clone --depth 1 https://github.com/DaveLak/oss-fuzz-inputs.git oss-fuzz-inputs &&
+    rsync -avc oss-fuzz-inputs/gitpython/corpra/ "$SEED_DATA_DIR/" &&
+    rm -rf oss-fuzz-inputs; # Clean up the cloned repo to keep the Docker image as slim as possible.
+}
+
+########################
+# Main execution logic #
+########################
+
+fetch_seed_corpra;
+
+download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
+  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \
+  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict";
+
+# The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
+python3 -m pip install --upgrade pip;
+python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0'; # Uses the latest versions know to work at the time of this commit.

From d0c6ee62ad64a074ca885c0c2edbbc5074542b6e Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 11 Apr 2024 20:11:34 -0400
Subject: [PATCH 795/994] Update documentation to include fuzzing specific info

As per discussion in https://github.com/gitpython-developers/GitPython/discussions/1889
---
 CONTRIBUTING.md |  5 +++++
 README.md       | 12 ++++++++++++
 2 files changed, 17 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e108f1b80..8536d7f73 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,3 +8,8 @@ The following is a short step-by-step rundown of what one typically would do to
 - Try to avoid massive commits and prefer to take small steps, with one commit for each.
 - Feel free to add yourself to AUTHORS file.
 - Create a pull request.
+
+## Fuzzing Test Specific Documentation
+
+For details related to contributing to the fuzzing test suite and OSS-Fuzz integration, please 
+refer to the dedicated [fuzzing README](./fuzzing/README.md).
diff --git a/README.md b/README.md
index 9bedaaae7..39f5496dc 100644
--- a/README.md
+++ b/README.md
@@ -240,5 +240,17 @@ Please have a look at the [contributions file][contributing].
 
 [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
 
+> [!NOTE]
+> There are two special case files located in the `fuzzzing/` directory that are licensed differently:
+>
+> `fuzz_config.py` and `fuzz_tree.py` were migrated here from the OSS-Fuzz project repository where they were initially
+> created and retain the original licence and copyright notice (Apache License, Version 2.0 and Copyright 2023 Google
+> LLC respectively.)
+>
+> - **These files do not impact the licence under which GitPython releases or source code are distributed.**
+> - The files located in the `fuzzzing/` directory are part of the project test suite and neither packaged nor distributed as
+    part of any release.
+
+
 [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
 [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE

From 1bc9a1a8250aa7291255cf389be2fa871c9049db Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 11 Apr 2024 21:19:24 -0400
Subject: [PATCH 796/994] Improve fuzzing README

Adds additional documentation links and fixes some typos.
---
 fuzzing/README.md | 27 ++++++++++++++++++++++-----
 1 file changed, 22 insertions(+), 5 deletions(-)

diff --git a/fuzzing/README.md b/fuzzing/README.md
index 6853c5002..97a62724a 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -6,7 +6,7 @@ This directory contains files related to GitPython's suite of fuzz tests that ar
 infrastructure provided by [OSS-Fuzz][oss-fuzz-repo]. This document aims to provide necessary information for working
 with fuzzing in GitPython.
 
-The details about the latest OSS-Fuzz test status, including build logs and coverage reports, is made available
+The latest details regarding OSS-Fuzz test status, including build logs and coverage reports, is made available
 at [this link](https://introspector.oss-fuzz.com/project-profile?project=gitpython).
 
 ## How to Contribute
@@ -23,7 +23,7 @@ Areas that are particularly appreciated include:
   update of any size, please consider suggesting it!
 
 For everything else, such as expanding test coverage, optimizing test performance, or enhancing error detection
-capabilities, jump in to the "Getting Started" section below.
+capabilities, jump into the "Getting Started" section below.
 
 ## Getting Started with Fuzzing GitPython
 
@@ -63,7 +63,7 @@ The `fuzzing/` directory is organized into three key areas:
 
 ### Fuzz Targets (`fuzz-targets/`)
 
-Contains Python files for each fuzz test, targeting specific functionalities of GitPython.
+Contains Python files for each fuzz test.
 
 **Things to Know**:
 
@@ -81,7 +81,7 @@ Contains Python files for each fuzz test, targeting specific functionalities of
 
 Provides hints to the fuzzing engine about inputs that might trigger unique code paths. Each fuzz target may have a
 corresponding `.dict` file. For details on how these are used, refer
-to [LibFuzzer documentation](https://llvm.org/docs/LibFuzzer.html#dictionaries).
+to the [LibFuzzer documentation on the subject](https://llvm.org/docs/LibFuzzer.html#dictionaries).
 
 **Things to Know**:
 
@@ -105,6 +105,11 @@ Includes scripts for building and integrating fuzz targets with OSS-Fuzz:
 - **`build.sh`** - Executed within the Docker container, this script builds fuzz targets with necessary instrumentation
   and prepares seed corpora and dictionaries for use.
 
+**Where to learn more:**
+
+- [OSS-Fuzz documentation on the build.sh](https://google.github.io/oss-fuzz/getting-started/new-project-guide/#buildsh)
+- [See GitPython's build.sh and Dockerfile in the OSS-Fuzz repository](https://github.com/google/oss-fuzz/tree/master/projects/gitpython)
+
 ## Running Fuzzers Locally
 
 ### Direct Execution of Fuzz Targets
@@ -153,9 +158,21 @@ python infra/helper.py check_build gitpython
 Execute the desired fuzz target:
 
 ```shell
-python infra/helper.py run_fuzzer gitpython $FUZZ_TARGET
+python infra/helper.py run_fuzzer gitpython $FUZZ_TARGET -- -max_total_time=60 -print_final_stats=1
 ```
 
+> [!TIP]
+> In the example above, the "`-- -max_total_time=60 -print_final_stats=1`" portion of the command is optional but quite
+> useful.
+>
+> Every argument provided after "`--`" in the above command is passed to the fuzzing engine directly. In this case:
+> - `-max_total_time=60` tells the LibFuzzer to stop execution after 60 seconds have elapsed.
+> - `-print_final_stats=1` tells the LibFuzzer to print a summary of useful metrics about the target run upon
+    completion.
+>
+> But almost any [LibFuzzer option listed in the documentation](https://llvm.org/docs/LibFuzzer.html#options) should
+> work as well.
+
 #### Next Steps
 
 For detailed instructions on advanced features like reproducing OSS-Fuzz issues or using the Fuzz Introspector, refer

From 576a858b7298bd14b1e87b118150df963af447dd Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 11 Apr 2024 22:06:05 -0400
Subject: [PATCH 797/994] Updates to support easily running OSS-Fuzz using
 local repo sources

- Updates the fuzzing documentation to include steps for working with
  locally modified versions of the gitpython repository.
- Updates the build.sh script to make the fuzz target search path more
  specific, reducing the risk of local OSS-Fuzz builds picking up
  files located outside of where we expect them (for example, in a .venv
  directory.)
- add artifacts produced by local OSS-Fuzz runs to gitignore
---
 .gitignore                        |  3 +++
 fuzzing/README.md                 | 19 +++++++++++++++++--
 fuzzing/oss-fuzz-scripts/build.sh |  2 +-
 3 files changed, 21 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore
index 7765293d8..d85569405 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,6 @@ output.txt
 
 # Finder metadata
 .DS_Store
+
+# Files created by OSS-Fuzz when running locally
+fuzz_*.pkg.spec
diff --git a/fuzzing/README.md b/fuzzing/README.md
index 97a62724a..acaf17d06 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -134,8 +134,10 @@ containers.
 Set environment variables to simplify command usage:
 
 ```shell
-export SANITIZER=address  # Can be either 'address' or 'undefined'.
-export FUZZ_TARGET=fuzz_config # specify the fuzz target without the .py extension.
+# $SANITIZER can be either 'address' or 'undefined':
+export SANITIZER=address
+# specify the fuzz target without the .py extension:
+export FUZZ_TARGET=fuzz_config
 ```
 
 #### Build and Run
@@ -149,6 +151,19 @@ python infra/helper.py build_image gitpython
 python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython
 ```
 
+> [!TIP]
+> The `build_fuzzers` command above accepts a local file path pointing to your gitpython repository clone as the last
+> argument.
+> This makes it easy to build fuzz targets you are developing locally in this repository without changing anything in
+> the OSS-Fuzz repo!
+> For example, if you have cloned this repository (or a fork of it) into: `~/code/GitPython`
+> Then running this command would build new or modified fuzz targets using the `~/code/GitPython/fuzzing/fuzz-targets`
+> directory:
+> ```shell
+> python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython ~/code/GitPython
+> ```
+
+
 Verify the build of your fuzzers with the optional `check_build` command:
 
 ```shell
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index fdab7a1e0..aff1c4347 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -13,7 +13,7 @@ find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name
   -exec cp {} "$OUT" \;
 
 # Build fuzzers in $OUT.
-find "$SRC" -name 'fuzz_*.py' -print0 | while IFS= read -r -d $'\0' fuzz_harness; do
+find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d $'\0' fuzz_harness; do
   compile_python_fuzzer "$fuzz_harness"
 
   common_base_dictionary_filename="$SEED_DATA_DIR/__base.dict"

From 5e56e96821878bd2808c640b8b39f84738ed8cf8 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 11 Apr 2024 23:37:54 -0400
Subject: [PATCH 798/994] Clarify documentation

- Fix typos in the documentation on dictionaries
- Link to the fuzzing directory in the main README where it is
  referenced.
---
 README.md         | 2 +-
 fuzzing/README.md | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 39f5496dc..b4cdc0a97 100644
--- a/README.md
+++ b/README.md
@@ -241,7 +241,7 @@ Please have a look at the [contributions file][contributing].
 [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
 
 > [!NOTE]
-> There are two special case files located in the `fuzzzing/` directory that are licensed differently:
+> There are two special case files located in the [`fuzzzing/` directory](./fuzzing) that are licensed differently:
 >
 > `fuzz_config.py` and `fuzz_tree.py` were migrated here from the OSS-Fuzz project repository where they were initially
 > created and retain the original licence and copyright notice (Apache License, Version 2.0 and Copyright 2023 Google
diff --git a/fuzzing/README.md b/fuzzing/README.md
index acaf17d06..c57e31d86 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -80,8 +80,8 @@ Contains Python files for each fuzz test.
 ### Dictionaries (`dictionaries/`)
 
 Provides hints to the fuzzing engine about inputs that might trigger unique code paths. Each fuzz target may have a
-corresponding `.dict` file. For details on how these are used, refer
-to the [LibFuzzer documentation on the subject](https://llvm.org/docs/LibFuzzer.html#dictionaries).
+corresponding `.dict` file. For information about dictionary syntax, refer to
+the [LibFuzzer documentation on the subject](https://llvm.org/docs/LibFuzzer.html#dictionaries).
 
 **Things to Know**:
 
@@ -92,7 +92,7 @@ to the [LibFuzzer documentation on the subject](https://llvm.org/docs/LibFuzzer.
   existing file here.
 - Development or updates to dictionaries should reflect the varied formats and edge cases relevant to the
   functionalities under test.
-- Example dictionaries (some of which are used to build the default dictionaries mentioned above) are can be found here:
+- Example dictionaries (some of which are used to build the default dictionaries mentioned above) can be found here:
   - [AFL++ dictionary repository](https://github.com/AFLplusplus/AFLplusplus/tree/stable/dictionaries#readme)
   - [Google/fuzzing dictionary repository](https://github.com/google/fuzzing/tree/master/dictionaries)
 

From 2041ba9972e7720f05bf570e2304fc0a5a2463d7 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Sun, 14 Apr 2024 22:59:01 -0400
Subject: [PATCH 799/994] Use gitpython-developers org ownd repository for seed
 corpra

This repo was created after discussion in PR #1901.
---
 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 43c21a8da..881161fae 100644
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -36,9 +36,9 @@ download_and_concatenate_common_dictionaries() {
 
 fetch_seed_corpra() {
   # Seed corpus zip files are hosted in a separate repository to avoid additional bloat in this repo.
-  git clone --depth 1 https://github.com/DaveLak/oss-fuzz-inputs.git oss-fuzz-inputs &&
-    rsync -avc oss-fuzz-inputs/gitpython/corpra/ "$SEED_DATA_DIR/" &&
-    rm -rf oss-fuzz-inputs; # Clean up the cloned repo to keep the Docker image as slim as possible.
+  git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git qa-assets &&
+    rsync -avc qa-assets/gitpython/corpra/ "$SEED_DATA_DIR/" &&
+    rm -rf qa-assets; # Clean up the cloned repo to keep the Docker image as slim as possible.
 }
 
 ########################

From 945a767ccd13c84946b2a49fbde4227fdfc84a26 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 02:06:50 -0400
Subject: [PATCH 800/994] Updates to comply with the terms of the Apache
 License

Addresses feedback and encorperates suggestions from PR #1901 to ensure
that the Apache License requirements are met for the two files that they
apply to, and the documentation pertaining to licensing of the files in
this repository is clear and concise.

The contects of LICENSE-APACHE were coppied from the LICENSE file of the
OSS-Fuzz repository that the two fuzz harnesses came from as of commit:
https://github.com/google/oss-fuzz/blob/c2c0632831767ff05c568e7b552cef2801d739ff/LICENSE
---
 README.md                           |  13 +-
 fuzzing/LICENSE-APACHE              | 201 ++++++++++++++++++++++++++++
 fuzzing/README.md                   |  12 ++
 fuzzing/fuzz-targets/fuzz_config.py |   6 +
 fuzzing/fuzz-targets/fuzz_tree.py   |   6 +
 5 files changed, 227 insertions(+), 11 deletions(-)
 create mode 100644 fuzzing/LICENSE-APACHE

diff --git a/README.md b/README.md
index b4cdc0a97..987e40e6c 100644
--- a/README.md
+++ b/README.md
@@ -240,17 +240,8 @@ Please have a look at the [contributions file][contributing].
 
 [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
 
-> [!NOTE]
-> There are two special case files located in the [`fuzzzing/` directory](./fuzzing) that are licensed differently:
->
-> `fuzz_config.py` and `fuzz_tree.py` were migrated here from the OSS-Fuzz project repository where they were initially
-> created and retain the original licence and copyright notice (Apache License, Version 2.0 and Copyright 2023 Google
-> LLC respectively.)
->
-> - **These files do not impact the licence under which GitPython releases or source code are distributed.**
-> - The files located in the `fuzzzing/` directory are part of the project test suite and neither packaged nor distributed as
-    part of any release.
-
+Two files exclusively used for fuzz testing are subject to [a separate license, detailed here](./fuzzing/README.md#license).
+These files are not included in the wheel or sdist packages published by the maintainers of GitPython.
 
 [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
 [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
diff --git a/fuzzing/LICENSE-APACHE b/fuzzing/LICENSE-APACHE
new file mode 100644
index 000000000..8dada3eda
--- /dev/null
+++ b/fuzzing/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/fuzzing/README.md b/fuzzing/README.md
index c57e31d86..09d6fc003 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -193,6 +193,18 @@ python infra/helper.py run_fuzzer gitpython $FUZZ_TARGET -- -max_total_time=60 -
 For detailed instructions on advanced features like reproducing OSS-Fuzz issues or using the Fuzz Introspector, refer
 to [the official OSS-Fuzz documentation][oss-fuzz-docs].
 
+## LICENSE
+
+All files located within the `fuzzing/` directory are subject to [the same license](../LICENSE)
+as [the other files in this repository](../README.md#license) with two exceptions:
+
+Two files located in this directory, [`fuzz_config.py`](./fuzz-targets/fuzz_config.py)
+and [`fuzz_tree.py`](./fuzz-targets/fuzz_tree.py), have been migrated here from the OSS-Fuzz project repository where
+they were originally created. As such, these two files retain their original license and copyright notice (Apache
+License, Version 2.0 and Copyright 2023 Google LLC respectively.) Each file includes a notice in their respective header
+comments stating that they have been modified. [LICENSE-APACHE](./LICENSE-APACHE) contains the original license used by
+the OSS-Fuzz project repository at the time they were migrated.
+
 [oss-fuzz-repo]: https://github.com/google/oss-fuzz
 
 [oss-fuzz-docs]: https://google.github.io/oss-fuzz
diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index 1403c96e4..fc2f0960a 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -12,6 +12,12 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+#
+###############################################################################
+# Note: This file has been modified by contributors to GitPython.
+# The original state of this file may be referenced here:
+# https://github.com/google/oss-fuzz/commit/f26f254558fc48f3c9bc130b10507386b94522da
+###############################################################################
 import atheris
 import sys
 import io
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
index 53258fb1e..b4e0e6b55 100644
--- a/fuzzing/fuzz-targets/fuzz_tree.py
+++ b/fuzzing/fuzz-targets/fuzz_tree.py
@@ -12,6 +12,12 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+#
+###############################################################################
+# Note: This file has been modified by contributors to GitPython.
+# The original state of this file may be referenced here:
+# https://github.com/google/oss-fuzz/commit/f26f254558fc48f3c9bc130b10507386b94522da
+###############################################################################
 import atheris
 import io
 import sys

From 68194a913fa0d9f601a55fcd08ff13b7ac35be75 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 14:41:36 -0400
Subject: [PATCH 801/994] Remove shebangs from fuzz harnesses

Prefer executing these files using the OSS-Fuzz or `python` command
methods outlined in the `fuzzing/README`.

Based on feedback and discussion on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/fuzz-targets/fuzz_config.py | 1 -
 fuzzing/fuzz-targets/fuzz_tree.py   | 1 -
 2 files changed, 2 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index fc2f0960a..0a06956c8 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 # Copyright 2023 Google LLC
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
index b4e0e6b55..464235098 100644
--- a/fuzzing/fuzz-targets/fuzz_tree.py
+++ b/fuzzing/fuzz-targets/fuzz_tree.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 # Copyright 2023 Google LLC
 #
 # Licensed under the Apache License, Version 2.0 (the "License");

From 8954c7151e098a0b12d4d2dec277fe6c63980579 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 14:45:49 -0400
Subject: [PATCH 802/994] Replace shebang in `build.sh` with ShellCheck
 directive

This script is meant to be sourced by the OSS-Fuzz file of the same
name, rather than executed directly. The shebang may lead to the
incorrect assumption that the script is meant for direct execution.
Replacing it with this directive instructs ShellCheck to treat
the script as a Bash script, regardless of how it is executed.

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/oss-fuzz-scripts/build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index aff1c4347..4c7de8799 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+# shellcheck shell=bash
 
 set -euo pipefail
 

From b0a5b8e66c4da3d603d8e27a71c70aaad53542b8 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 14:59:05 -0400
Subject: [PATCH 803/994] Set executable bit on
 `container-environment-bootstrap.sh`

This script is executed directly, not sourced as is the case with
`build.sh`, so it should have an executable bit set to avoid ambiguity.

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 mode change 100644 => 100755 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh

diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
old mode 100644
new mode 100755

From 25f360090cb6a7fd0f01bc127f2a2280659757a2 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 15:31:05 -0400
Subject: [PATCH 804/994] Minor clarity improvements in `fuzzing/README.md`

- Make the link text for the OSS-Fuzz test status URL more descriptive
- Fix capitalization of GitPython repository name

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/fuzzing/README.md b/fuzzing/README.md
index 09d6fc003..65e311d4a 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -6,8 +6,8 @@ This directory contains files related to GitPython's suite of fuzz tests that ar
 infrastructure provided by [OSS-Fuzz][oss-fuzz-repo]. This document aims to provide necessary information for working
 with fuzzing in GitPython.
 
-The latest details regarding OSS-Fuzz test status, including build logs and coverage reports, is made available
-at [this link](https://introspector.oss-fuzz.com/project-profile?project=gitpython).
+The latest details regarding OSS-Fuzz test status, including build logs and coverage reports, is available
+on [the Open Source Fuzzing Introspection website](https://introspector.oss-fuzz.com/project-profile?project=gitpython).
 
 ## How to Contribute
 
@@ -152,7 +152,7 @@ python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython
 ```
 
 > [!TIP]
-> The `build_fuzzers` command above accepts a local file path pointing to your gitpython repository clone as the last
+> The `build_fuzzers` command above accepts a local file path pointing to your GitPython repository clone as the last
 > argument.
 > This makes it easy to build fuzz targets you are developing locally in this repository without changing anything in
 > the OSS-Fuzz repo!

From d79c176384f1a5b6cb615f500037dbcecd9ee7d9 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 16:10:08 -0400
Subject: [PATCH 805/994] Simplify read delimiter to use empty string in fuzz
 harness loop

Replaces the null character delimiter `-d $'\0'` with the simpler
empty string `-d ''` in the fuzzing harness build loop.

This changes leverages the Bash `read` builtin behavior to avoid
unnecessary complexity and improving script readability.

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/oss-fuzz-scripts/build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index 4c7de8799..a412a1d15 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -13,7 +13,7 @@ find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name
   -exec cp {} "$OUT" \;
 
 # Build fuzzers in $OUT.
-find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d $'\0' fuzz_harness; do
+find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do
   compile_python_fuzzer "$fuzz_harness"
 
   common_base_dictionary_filename="$SEED_DATA_DIR/__base.dict"

From e038526b846f4bc5e75a91c736f3384616800aa1 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 16:27:43 -0400
Subject: [PATCH 806/994] Remove unnecessary semicolon for consistent script
 formatting

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 .../container-environment-bootstrap.sh                 | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 881161fae..87f817993 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -38,19 +38,19 @@ fetch_seed_corpra() {
   # Seed corpus zip files are hosted in a separate repository to avoid additional bloat in this repo.
   git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git qa-assets &&
     rsync -avc qa-assets/gitpython/corpra/ "$SEED_DATA_DIR/" &&
-    rm -rf qa-assets; # Clean up the cloned repo to keep the Docker image as slim as possible.
+    rm -rf qa-assets # Clean up the cloned repo to keep the Docker image as slim as possible.
 }
 
 ########################
 # Main execution logic #
 ########################
 
-fetch_seed_corpra;
+fetch_seed_corpra
 
 download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
   "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \
-  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict";
+  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict"
 
 # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
-python3 -m pip install --upgrade pip;
-python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0'; # Uses the latest versions know to work at the time of this commit.
+python3 -m pip install --upgrade pip
+python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0' # Uses the latest versions know to work at the time of this commit.

From d25ae2def1f995afcb7fad69250b12f5bf07b3bb Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 16 Apr 2024 16:38:21 -0400
Subject: [PATCH 807/994] Fix various misspellings of "corpora" & improve
 script comments

A misspelling in the https://github.com/gitpython-developers/qa-assets
repository is still present here. It will need to be fixed in that
repository first.

"corpora" is a difficult word to spell consistently I guess. This made
for a good opportunity to improve the phrasing of two other comments at
at least.

Based @EliahKagan's suggestion and feedback on:
https://github.com/gitpython-developers/GitPython/pull/1901
---
 fuzzing/oss-fuzz-scripts/build.sh                          | 4 ++--
 .../oss-fuzz-scripts/container-environment-bootstrap.sh    | 7 ++++---
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index a412a1d15..ab46ec7a2 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -4,7 +4,7 @@ set -euo pipefail
 
 python3 -m pip install .
 
-# Directory to look in for dictionaries, options files, and seed corpa:
+# Directory to look in for dictionaries, options files, and seed corpora:
 SEED_DATA_DIR="$SRC/seed_data"
 
 find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name '*.dict' \) \
@@ -27,7 +27,7 @@ find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d
       # If a dictionary file for this fuzzer already exists and is not empty,
       # we append a new line to the end of it before appending any new entries.
       #
-      # libfuzzer will happily ignore multiple empty lines in a dictionary but crash
+      # LibFuzzer will happily ignore multiple empty lines in a dictionary but fail with an error
       # if any single line has incorrect syntax (e.g., if we accidentally add two entries to the same line.)
       # See docs for valid syntax: https://llvm.org/docs/LibFuzzer.html#id32
       echo >>"$output_file"
diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 87f817993..0be012ccd 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -34,7 +34,7 @@ download_and_concatenate_common_dictionaries() {
   done
 }
 
-fetch_seed_corpra() {
+fetch_seed_corpora() {
   # Seed corpus zip files are hosted in a separate repository to avoid additional bloat in this repo.
   git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git qa-assets &&
     rsync -avc qa-assets/gitpython/corpra/ "$SEED_DATA_DIR/" &&
@@ -45,7 +45,7 @@ fetch_seed_corpra() {
 # Main execution logic #
 ########################
 
-fetch_seed_corpra
+fetch_seed_corpora
 
 download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
   "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \
@@ -53,4 +53,5 @@ download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
 
 # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
 python3 -m pip install --upgrade pip
-python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0' # Uses the latest versions know to work at the time of this commit.
+ # Upgrade to the latest versions known to work at the time the below changes were introduced:
+python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0'

From 23a505f3ef51c4c26998fed924f4edad2438c757 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 17 Apr 2024 19:40:44 -0400
Subject: [PATCH 808/994] Remove comment suggesting the `undefined` sanitizer
 is a valid option

Also makes come structural improvements to how the local instructions
for running OSS-Fuzz are presented now that only the single `address`
sanitizer is a valid option.

The `undefined` sanitizer was removed from GitPython's `project.yaml`
OSS-Fuzz configuration file at the request of OSS-Fuzz project reviewers
in https://github.com/google/oss-fuzz/pull/11803.

The `undefined` sanitizer is only useful in Python projects that use
native exstensions (such as C, C++, Rust, ect.), which GitPython does
not currently do. This commit updates the `fuzzing/README` reference to
that sanitizer accoirdingly.
See:
- https://github.com/google/oss-fuzz/pull/11803/commits/b210fb21427f1f994c91f07e95ca0cc977f61f66
- https://github.com/google/oss-fuzz/pull/11803#discussion_r1569160945
---
 fuzzing/README.md | 28 +++++++++++++---------------
 1 file changed, 13 insertions(+), 15 deletions(-)

diff --git a/fuzzing/README.md b/fuzzing/README.md
index 65e311d4a..ab9f6a63f 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -129,18 +129,7 @@ This approach uses Docker images provided by OSS-Fuzz for building and running f
 comprehensive features but requires a local clone of the OSS-Fuzz repository and sufficient disk space for Docker
 containers.
 
-#### Preparation
-
-Set environment variables to simplify command usage:
-
-```shell
-# $SANITIZER can be either 'address' or 'undefined':
-export SANITIZER=address
-# specify the fuzz target without the .py extension:
-export FUZZ_TARGET=fuzz_config
-```
-
-#### Build and Run
+#### Build the Execution Environment
 
 Clone the OSS-Fuzz repository and prepare the Docker environment:
 
@@ -148,7 +137,7 @@ Clone the OSS-Fuzz repository and prepare the Docker environment:
 git clone --depth 1 https://github.com/google/oss-fuzz.git oss-fuzz
 cd oss-fuzz
 python infra/helper.py build_image gitpython
-python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython
+python infra/helper.py build_fuzzers --sanitizer address gitpython
 ```
 
 > [!TIP]
@@ -160,16 +149,25 @@ python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython
 > Then running this command would build new or modified fuzz targets using the `~/code/GitPython/fuzzing/fuzz-targets`
 > directory:
 > ```shell
-> python infra/helper.py build_fuzzers --sanitizer $SANITIZER gitpython ~/code/GitPython
+> python infra/helper.py build_fuzzers --sanitizer address gitpython ~/code/GitPython
 > ```
 
-
 Verify the build of your fuzzers with the optional `check_build` command:
 
 ```shell
 python infra/helper.py check_build gitpython
 ```
 
+#### Run a Fuzz Target
+
+Setting an environment variable for the fuzz target argument of the execution command makes it easier to quickly select
+a different target between runs:
+
+```shell
+# specify the fuzz target without the .py extension:
+export FUZZ_TARGET=fuzz_config
+```
+
 Execute the desired fuzz target:
 
 ```shell

From 1d54d4b0b2bdba60cc742f73a4b8d5a88cce8f64 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 17 Apr 2024 22:11:00 -0400
Subject: [PATCH 809/994] Remove unintentional leading space from comment

---
 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 0be012ccd..662808e27 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -53,5 +53,5 @@ download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
 
 # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
 python3 -m pip install --upgrade pip
- # Upgrade to the latest versions known to work at the time the below changes were introduced:
+# Upgrade to the latest versions known to work at the time the below changes were introduced:
 python3 -m pip install 'setuptools~=69.0' 'pyinstaller~=6.0'

From fdce8375c80abd02b1dc08bd218f09849fea8233 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Sat, 20 Apr 2024 16:48:00 -0400
Subject: [PATCH 810/994] Dockerized "Direct Execution of Fuzz Targets"

Adds a Dockerfile to enable easily executing the fuzz targets directly
inside a container environment instead of directly on a host machine.
This addresses concerns raised in PR #1901 related to how `fuzz_tree.py`
writes to the real `/tmp` directory of the file system it is executed on
as part of setting up its own test fixtures, but also makes for an
easier to use development workflow.

See this related comment on PR #1901 for additional context:
https://github.com/gitpython-developers/GitPython/pull/1901#issuecomment-2063818998
---
 fuzzing/README.md                    | 43 ++++++++++++++++++++++++----
 fuzzing/local-dev-helpers/Dockerfile | 22 ++++++++++++++
 2 files changed, 59 insertions(+), 6 deletions(-)
 create mode 100644 fuzzing/local-dev-helpers/Dockerfile

diff --git a/fuzzing/README.md b/fuzzing/README.md
index ab9f6a63f..0a62b4c85 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -44,8 +44,7 @@ capabilities, jump into the "Getting Started" section below.
 ### Setting Up Your Local Environment
 
 Before contributing to fuzzing efforts, ensure Python and Docker are installed on your machine. Docker is required for
-running fuzzers in containers provided by OSS-Fuzz. [Install Docker](https://docs.docker.com/get-docker/) following the
-official guide if you do not already have it.
+running fuzzers in containers provided by OSS-Fuzz and for safely executing test files directly. [Install Docker](https://docs.docker.com/get-docker/) following the official guide if you do not already have it.
 
 ### Understanding Existing Fuzz Targets
 
@@ -110,19 +109,51 @@ Includes scripts for building and integrating fuzz targets with OSS-Fuzz:
 - [OSS-Fuzz documentation on the build.sh](https://google.github.io/oss-fuzz/getting-started/new-project-guide/#buildsh)
 - [See GitPython's build.sh and Dockerfile in the OSS-Fuzz repository](https://github.com/google/oss-fuzz/tree/master/projects/gitpython)
 
+### Local Development Helpers (`local-dev-helpers/`)
+
+Contains tools to make local development tasks easier.
+See [the "Running Fuzzers Locally" section below](#running-fuzzers-locally) for further documentation and use cases related to files found here.
+
 ## Running Fuzzers Locally
 
+> [!WARNING]
+> **Some fuzz targets in this repository write to the filesystem** during execution.
+> For that reason, it is strongly recommended to **always use Docker when executing fuzz targets**, even when it may be
+> possible to do so without it.
+>
+> Although [I/O operations such as writing to disk are not considered best practice](https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md#io), the current implementation of at least one test requires it. 
+> See [the "Setting Up Your Local Environment" section above](#setting-up-your-local-environment) if you do not already have Docker installed on your machine.
+>
+> PRs that replace disk I/O with in-memory alternatives are very much welcomed!
+
 ### Direct Execution of Fuzz Targets
 
-For quick testing of changes, [Atheris][atheris-repo] makes it possible to execute a fuzz target directly:
+Directly executing fuzz targets allows for quick iteration and testing of changes which can be helpful during early
+development of new fuzz targets or for validating changes made to an existing test.
+The [Dockerfile](./local-dev-helpers/Dockerfile) located in the `local-dev-helpers/` subdirectory provides a lightweight
+container environment preconfigured with [Atheris][atheris-repo] that makes it easy to execute a fuzz target directly.
+
+**From the root directory of your GitPython repository clone**:
 
-1. Install Atheris following the [installation guide][atheris-repo] for your operating system.
-2. Execute a fuzz target, for example:
+1. Build the local development helper image:
 
 ```shell
-python fuzzing/fuzz-targets/fuzz_config.py
+docker build -f fuzzing/local-dev-helpers/Dockerfile -t gitpython-fuzzdev .
 ```
 
+2. Then execute a fuzz target inside the image, for example:
+
+```shell
+ docker run -it -v "$PWD":/src gitpython-fuzzdev python fuzzing/fuzz-targets/fuzz_config.py -atheris_runs=10000
+```
+
+The above command executes [`fuzz_config.py`](./fuzz-targets/fuzz_config.py) and exits after `10000` runs, or earlier if
+the fuzzer finds an error.
+
+Docker CLI's `-v` flag specifies a volume mount in Docker that maps the directory in which the command is run (which
+should be the root directory of your local GitPython clone) to a directory inside the container, so any modifications
+made between invocations will be reflected immediately without the need to rebuild the image each time.
+
 ### Running OSS-Fuzz Locally
 
 This approach uses Docker images provided by OSS-Fuzz for building and running fuzz tests locally. It offers
diff --git a/fuzzing/local-dev-helpers/Dockerfile b/fuzzing/local-dev-helpers/Dockerfile
new file mode 100644
index 000000000..77808ed1d
--- /dev/null
+++ b/fuzzing/local-dev-helpers/Dockerfile
@@ -0,0 +1,22 @@
+# syntax=docker/dockerfile:1
+
+# Use the same Python version as OSS-Fuzz to accidental incompatibilities in test code
+FROM python:3.8-slim
+
+LABEL project="GitPython Fuzzing Local Dev Helper"
+
+WORKDIR /src
+
+COPY . .
+
+# Update package managers, install necessary packages, and cleanup unnecessary files in a single RUN to keep the image smaller.
+RUN apt-get update && \
+    apt-get install --no-install-recommends -y git && \
+    python -m pip install --upgrade pip && \
+    python -m pip install atheris && \
+    python -m pip install -e . && \
+    apt-get clean && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/* /root/.cache
+
+CMD ["bash"]

From f1451219c5a3a221615d3d38ac251bf5bbe46119 Mon Sep 17 00:00:00 2001
From: DaveLak <github@themoderndev.com>
Date: Sun, 21 Apr 2024 12:19:57 -0400
Subject: [PATCH 811/994] Fix Atheris install in local dev helper Docker image

The Atheris package bundles a binary that supplies libFuzzer on some
host machines, but in some cases (such as ARM based mac hosts) Atheris
seems to require building libFuzzer at install time while pip builds the
wheel. In the latter case, clang and related dependencies must be
present and available for the build, which itself requires using a non
"slim" version of the Python base image and not passing the
`--no-install-recommends` flag to `apt-get install` as both prevent the
required related libraries from being automatically installed.

It is also worth noting that at the time of this commit, the default
version of LLVM & Clang installed when `clang` is installed from `apt`
is version 14, while the latest stable version is 17 and OSS-Fuzz uses
15. The decision to install the default version (14) available via the
debian repos was intentional because a) it appears to work fine for our
needs and Atheris version b) specifying a different version requires
more complexity depending on install method, but the goal of this
Dockerfile is simplicity and low maintenance.

If it becomes neccissary to upgrade Clang/LLVM in the future, one option
to consider besides installing from source is the apt repository
maintained by the LLVM project: https://apt.llvm.org/

See the discussion in this issue for additional context to this change:
https://github.com/gitpython-developers/GitPython/pull/1904
---
 fuzzing/local-dev-helpers/Dockerfile | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/fuzzing/local-dev-helpers/Dockerfile b/fuzzing/local-dev-helpers/Dockerfile
index 77808ed1d..426de05dd 100644
--- a/fuzzing/local-dev-helpers/Dockerfile
+++ b/fuzzing/local-dev-helpers/Dockerfile
@@ -1,7 +1,7 @@
 # syntax=docker/dockerfile:1
 
 # Use the same Python version as OSS-Fuzz to accidental incompatibilities in test code
-FROM python:3.8-slim
+FROM python:3.8-bookworm
 
 LABEL project="GitPython Fuzzing Local Dev Helper"
 
@@ -11,12 +11,12 @@ COPY . .
 
 # Update package managers, install necessary packages, and cleanup unnecessary files in a single RUN to keep the image smaller.
 RUN apt-get update && \
-    apt-get install --no-install-recommends -y git && \
+    apt-get install -y git clang && \
     python -m pip install --upgrade pip && \
     python -m pip install atheris && \
     python -m pip install -e . && \
     apt-get clean && \
     apt-get autoremove -y && \
-    rm -rf /var/lib/apt/lists/* /root/.cache
+    rm -rf /var/lib/apt/lists/*
 
 CMD ["bash"]

From f4b95cf089706a29396b744b53a4ecdcc924d31c Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Mon, 22 Apr 2024 16:07:54 -0400
Subject: [PATCH 812/994] Fix Fuzzer Crash in ClusterFuzz Due to Missing Git
 Executable

A Git executable is not globally available in the ClusterFuzz container
environment where OSS-Fuzz executes fuzz tests, causing an error in the fuzz
harnesses when GitPython attempts to initialize, crashing the tests before they
can run.

To avoid this issue, we bundle the `git` binary that is available in the OSS-Fuzz
build container with the fuzz harness via Pyinstaller's `--add-binary` flag in
`build.sh` and use GitPython's `git.refresh(<full-path-to-git-executable>)`
method inside a Pyinstaller runtime check to initialize GitPython with the
bundled Git executable when running from the bundled application.

In all other execution environments, we assume a `git` executable is available
globally.

Fixes:
- https://github.com/gitpython-developers/GitPython/issues/1905
- https://github.com/google/oss-fuzz/issues/10600
---
 fuzzing/fuzz-targets/fuzz_config.py |  9 +++++++--
 fuzzing/fuzz-targets/fuzz_tree.py   | 11 +++++++----
 fuzzing/oss-fuzz-scripts/build.sh   |  2 +-
 3 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index 0a06956c8..7623ab98f 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -20,16 +20,21 @@
 import atheris
 import sys
 import io
+import os
 from configparser import MissingSectionHeaderError, ParsingError
 
 with atheris.instrument_imports():
-    from git import GitConfigParser
+    import git
 
 
 def TestOneInput(data):
+    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+        path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+        git.refresh(path_to_bundled_git_binary)
+
     sio = io.BytesIO(data)
     sio.name = "/tmp/fuzzconfig.config"
-    git_config = GitConfigParser(sio)
+    git_config = git.GitConfigParser(sio)
     try:
         git_config.read()
     except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError):
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
index 464235098..7187c4a6f 100644
--- a/fuzzing/fuzz-targets/fuzz_tree.py
+++ b/fuzzing/fuzz-targets/fuzz_tree.py
@@ -24,11 +24,14 @@
 import shutil
 
 with atheris.instrument_imports():
-    from git.objects import Tree
-    from git.repo import Repo
+    import git
 
 
 def TestOneInput(data):
+    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+        path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+        git.refresh(path_to_bundled_git_binary)
+
     fdp = atheris.FuzzedDataProvider(data)
     git_dir = "/tmp/.git"
     head_file = os.path.join(git_dir, "HEAD")
@@ -46,9 +49,9 @@ def TestOneInput(data):
     os.mkdir(common_dir)
     os.mkdir(objects_dir)
 
-    _repo = Repo("/tmp/")
+    _repo = git.Repo("/tmp/")
 
-    fuzz_tree = Tree(_repo, Tree.NULL_BIN_SHA, 0, "")
+    fuzz_tree = git.Tree(_repo, git.Tree.NULL_BIN_SHA, 0, "")
     try:
         fuzz_tree._deserialize(io.BytesIO(data))
     except IndexError:
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index ab46ec7a2..be31ac32a 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -14,7 +14,7 @@ find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name
 
 # Build fuzzers in $OUT.
 find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do
-  compile_python_fuzzer "$fuzz_harness"
+  compile_python_fuzzer "$fuzz_harness" --add-binary="$(command -v git):."
 
   common_base_dictionary_filename="$SEED_DATA_DIR/__base.dict"
   if [[ -r "$common_base_dictionary_filename" ]]; then

From 2b0a9693ea98ab7ff025a8ab1235b6f8ea0da676 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Mon, 22 Apr 2024 16:36:17 -0400
Subject: [PATCH 813/994] Add GitPython's standard license header comments to
 oss-fuzz scripts

These files are already BSD-3-Clause even without the headers, but
adding these comments and the `LICENSE-BSD` symlink to the root level
`LICENSE` file are helpful to reinforce that there are only two
particular files in the `fuzzing/` that are not under BSD-3-Clause.

See:
https://github.com/gitpython-developers/GitPython/pull/1901#discussion_r1567849271
---
 fuzzing/LICENSE-BSD                                         | 1 +
 fuzzing/oss-fuzz-scripts/build.sh                           | 3 +++
 fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh | 4 ++++
 3 files changed, 8 insertions(+)
 create mode 120000 fuzzing/LICENSE-BSD

diff --git a/fuzzing/LICENSE-BSD b/fuzzing/LICENSE-BSD
new file mode 120000
index 000000000..ea5b60640
--- /dev/null
+++ b/fuzzing/LICENSE-BSD
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index ab46ec7a2..a79cbe895 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -1,4 +1,7 @@
 # shellcheck shell=bash
+#
+# This file is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 set -euo pipefail
 
diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 662808e27..76ec97c7f 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
+#
+# This file is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+
 set -euo pipefail
 
 #################

From b021a76354bb2779fc8f43c647a3a85b67f3d01a Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Mon, 22 Apr 2024 16:43:03 -0400
Subject: [PATCH 814/994] Add GitPython's standard license header comments to
 top level scripts

While discussing adding similar license comments to the shell scripts
introduced in PR #1901, it was noticed that the shell scripts in the
repository root directory did not have such comments and suggested that
we could add them when the scripts in the `fuzzing/` directory were
updated, so this commit does just that.

See:
https://github.com/gitpython-developers/GitPython/pull/1901#discussion_r1567849271
---
 build-release.sh          | 3 +++
 check-version.sh          | 3 +++
 init-tests-after-clone.sh | 3 +++
 3 files changed, 9 insertions(+)

diff --git a/build-release.sh b/build-release.sh
index 49c13b93a..1a8dce2c2 100755
--- a/build-release.sh
+++ b/build-release.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 #
+# This file is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+#
 # 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 dac386e46..579cf789f 100755
--- a/check-version.sh
+++ b/check-version.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 #
+# This file is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
+#
 # 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.
diff --git a/init-tests-after-clone.sh b/init-tests-after-clone.sh
index 118e1de22..bfada01b0 100755
--- a/init-tests-after-clone.sh
+++ b/init-tests-after-clone.sh
@@ -1,4 +1,7 @@
 #!/bin/sh
+#
+# This file is part of GitPython and is released under the
+# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 set -eu
 

From 47e5738074e7f9acfb64d164206770bbd41685a0 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Mon, 22 Apr 2024 18:48:26 -0400
Subject: [PATCH 815/994] Fix IndexError in GitConfigParser when config value
 ends in new line

Improve the guarding `if` check in `GitConfigParser`'s `string_decode`
function to safely handle empty strings and prevent `IndexError`s when
accessing string elements.

This resolves an IndexError in the `GitConfigParser`'s `.read()`
method when the config file contains a quoted value containing a
trailing new line.

Fixes:
https://github.com/gitpython-developers/GitPython/issues/1887
---
 fuzzing/fuzz-targets/fuzz_config.py | 8 ++------
 git/config.py                       | 2 +-
 test/test_config.py                 | 8 ++++++++
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index 0a06956c8..81dcf9a88 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -34,12 +34,8 @@ def TestOneInput(data):
         git_config.read()
     except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError):
         return -1  # Reject inputs raising expected exceptions
-    except (IndexError, ValueError) as e:
-        if isinstance(e, IndexError) and "string index out of range" in str(e):
-            # Known possibility that might be patched
-            # See: https://github.com/gitpython-developers/GitPython/issues/1887
-            pass
-        elif isinstance(e, ValueError) and "embedded null byte" in str(e):
+    except ValueError as e:
+        if isinstance(e, ValueError) and "embedded null byte" in str(e):
             # The `os.path.expanduser` function, which does not accept strings
             # containing null bytes might raise this.
             return -1
diff --git a/git/config.py b/git/config.py
index 3ce9b123f..c9b49684c 100644
--- a/git/config.py
+++ b/git/config.py
@@ -452,7 +452,7 @@ def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
         e = None  # None, or an exception.
 
         def string_decode(v: str) -> str:
-            if v[-1] == "\\":
+            if v and v[-1] == "\\":
                 v = v[:-1]
             # END cut trailing escapes to prevent decode error
 
diff --git a/test/test_config.py b/test/test_config.py
index 0911d0262..92997422d 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -142,6 +142,14 @@ def test_multi_line_config(self):
             )
             self.assertEqual(len(config.sections()), 23)
 
+    def test_config_value_with_trailing_new_line(self):
+        config_content = b'[section-header]\nkey:"value\n"'
+        config_file = io.BytesIO(config_content)
+        config_file.name = "multiline_value.config"
+
+        git_config = GitConfigParser(config_file)
+        git_config.read()  # This should not throw an exception
+
     def test_base(self):
         path_repo = fixture_path("git_config")
         path_global = fixture_path("git_config_global")

From c2283f6e3566606300f64c44a12197f0b65f0d71 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Tue, 23 Apr 2024 08:21:58 +0200
Subject: [PATCH 816/994] Avoid unnecessary isinstance check

---
 fuzzing/fuzz-targets/fuzz_config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index 81dcf9a88..80ab7b08d 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -35,7 +35,7 @@ def TestOneInput(data):
     except (MissingSectionHeaderError, ParsingError, UnicodeDecodeError):
         return -1  # Reject inputs raising expected exceptions
     except ValueError as e:
-        if isinstance(e, ValueError) and "embedded null byte" in str(e):
+        if "embedded null byte" in str(e):
             # The `os.path.expanduser` function, which does not accept strings
             # containing null bytes might raise this.
             return -1

From 1a0ab5bbdaa9df5d04d1b6946af419492b650fce Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Fri, 26 Apr 2024 07:15:13 +0200
Subject: [PATCH 817/994] Use endswith() for more clarity

---
 git/config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/config.py b/git/config.py
index c9b49684c..de3508360 100644
--- a/git/config.py
+++ b/git/config.py
@@ -452,7 +452,7 @@ def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
         e = None  # None, or an exception.
 
         def string_decode(v: str) -> str:
-            if v and v[-1] == "\\":
+            if v and v.endswith("\\"):
                 v = v[:-1]
             # END cut trailing escapes to prevent decode error
 

From dac3535d3dc4aaff9bd98a6ea70f46b132537694 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Fri, 26 Apr 2024 18:13:11 -0400
Subject: [PATCH 818/994] Attempt 2 - Fix Missing Git Executable Causing
 ClusterFuzz Crash

This is a second attempt at #1906 and should resolve:
- https://github.com/gitpython-developers/GitPython/issues/1905
- https://github.com/google/oss-fuzz/issues/10600

PR #1906 had the right idea but wrong implementation, and the differences between
the ClusterFuzz image that it was supposed to fix and the OSS-Fuzz image where
the fix was tested led to the issue not being fully resolved.

The root cause of the issue is the same: A Git executable is not globally
available in the ClusterFuzz container environment where OSS-Fuzz executes
fuzz tests.

 #1906 attempted to fix the issue by bundling the Git binary and using
GitPython's `git.refresh(<full-path-to-git-executable>)` method to set it
inside the `TestOneInput` function of the test harness.

However, GitPython attempts to set the binary at import time via its `__init__`
hook, and crashes the test if no executable is found during the import.

This issue is fixed here by setting the environment variable that GitPython
looks in before importing it, so it's available for the import. This was tested
by setting the `$PATH` to an empty string inside the test files, which
reproduced the crash, then adding the changes introduced here with `$PATH` still
empty, which avoided the crash indicating that the bundled Git executable is
working as expected.
---
 fuzzing/fuzz-targets/fuzz_config.py | 8 ++++----
 fuzzing/fuzz-targets/fuzz_tree.py   | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_config.py b/fuzzing/fuzz-targets/fuzz_config.py
index 6f2caad4b..4eddc32ff 100644
--- a/fuzzing/fuzz-targets/fuzz_config.py
+++ b/fuzzing/fuzz-targets/fuzz_config.py
@@ -23,15 +23,15 @@
 import os
 from configparser import MissingSectionHeaderError, ParsingError
 
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
 with atheris.instrument_imports():
     import git
 
 
 def TestOneInput(data):
-    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
-        path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
-        git.refresh(path_to_bundled_git_binary)
-
     sio = io.BytesIO(data)
     sio.name = "/tmp/fuzzconfig.config"
     git_config = git.GitConfigParser(sio)
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
index 7187c4a6f..4e2038add 100644
--- a/fuzzing/fuzz-targets/fuzz_tree.py
+++ b/fuzzing/fuzz-targets/fuzz_tree.py
@@ -23,15 +23,15 @@
 import os
 import shutil
 
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
 with atheris.instrument_imports():
     import git
 
 
 def TestOneInput(data):
-    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
-        path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
-        git.refresh(path_to_bundled_git_binary)
-
     fdp = atheris.FuzzedDataProvider(data)
     git_dir = "/tmp/.git"
     head_file = os.path.join(git_dir, "HEAD")

From c84e643c6aa177f364ebe28e4c7bab1e37fb0242 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Sun, 28 Apr 2024 22:41:11 -0400
Subject: [PATCH 819/994] Replace the suboptimal fuzz_tree harness with a
 better alternative

As discussed in the initial fuzzing integration PR[^1], `fuzz_tree.py`'s
implementation was not ideal in terms of coverage and its reading/writing to
hard-coded paths inside `/tmp` was problematic as (among other concerns), it
causes intermittent crashes on ClusterFuzz[^2] when multiple workers execute
the test at the same time on the same machine.

The changes here replace `fuzz_tree.py` completely with a completely new
`fuzz_repo.py` fuzz target which:

- Uses `tempfile.TemporaryDirectory()` to safely manage tmpdir creation and
  tear down, including during multi-worker execution runs.
- Retains the same feature coverage as `fuzz_tree.py`, but it also adds
  considerably more from much smaller data inputs and with less memory consumed
  (and it doesn't even have a seed corpus or target specific dictionary yet.)
- Can likely be improved further in the future by exercising additional features
  of `Repo` to the harness.

Because `fuzz_tree.py` was removed and `fuzz_repo.py` was not derived from it,
the Apache License call outs in the docs were also updated as they only apply to
the singe `fuzz_config.py` file now.

[^1]: https://github.com/gitpython-developers/GitPython/pull/1901#discussion_r1565001609
[^2]: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=68355
---
 README.md                           |  4 +-
 fuzzing/README.md                   | 16 +++----
 fuzzing/dictionaries/fuzz_tree.dict | 13 ------
 fuzzing/fuzz-targets/fuzz_repo.py   | 47 ++++++++++++++++++++
 fuzzing/fuzz-targets/fuzz_tree.py   | 67 -----------------------------
 5 files changed, 57 insertions(+), 90 deletions(-)
 delete mode 100644 fuzzing/dictionaries/fuzz_tree.dict
 create mode 100644 fuzzing/fuzz-targets/fuzz_repo.py
 delete mode 100644 fuzzing/fuzz-targets/fuzz_tree.py

diff --git a/README.md b/README.md
index 987e40e6c..d365a6584 100644
--- a/README.md
+++ b/README.md
@@ -240,8 +240,8 @@ Please have a look at the [contributions file][contributing].
 
 [3-Clause BSD License](https://opensource.org/license/bsd-3-clause/), also known as the New BSD License. See the [LICENSE file][license].
 
-Two files exclusively used for fuzz testing are subject to [a separate license, detailed here](./fuzzing/README.md#license).
-These files are not included in the wheel or sdist packages published by the maintainers of GitPython.
+One file exclusively used for fuzz testing is subject to [a separate license, detailed here](./fuzzing/README.md#license).
+This file is not included in the wheel or sdist packages published by the maintainers of GitPython.
 
 [contributing]: https://github.com/gitpython-developers/GitPython/blob/main/CONTRIBUTING.md
 [license]: https://github.com/gitpython-developers/GitPython/blob/main/LICENSE
diff --git a/fuzzing/README.md b/fuzzing/README.md
index 0a62b4c85..9d02bf72f 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -225,14 +225,14 @@ to [the official OSS-Fuzz documentation][oss-fuzz-docs].
 ## LICENSE
 
 All files located within the `fuzzing/` directory are subject to [the same license](../LICENSE)
-as [the other files in this repository](../README.md#license) with two exceptions:
-
-Two files located in this directory, [`fuzz_config.py`](./fuzz-targets/fuzz_config.py)
-and [`fuzz_tree.py`](./fuzz-targets/fuzz_tree.py), have been migrated here from the OSS-Fuzz project repository where
-they were originally created. As such, these two files retain their original license and copyright notice (Apache
-License, Version 2.0 and Copyright 2023 Google LLC respectively.) Each file includes a notice in their respective header
-comments stating that they have been modified. [LICENSE-APACHE](./LICENSE-APACHE) contains the original license used by
-the OSS-Fuzz project repository at the time they were migrated.
+as [the other files in this repository](../README.md#license) with one exception:
+
+[`fuzz_config.py`](./fuzz-targets/fuzz_config.py) was migrated to this repository from the OSS-Fuzz project's repository
+where it was originally created. As such, [`fuzz_config.py`](./fuzz-targets/fuzz_config.py) retains its original license
+and copyright notice (Apache License, Version 2.0 and Copyright 2023 Google LLC respectively) as in a header
+comment, followed by a notice stating that it has have been modified contributors to GitPython.
+[LICENSE-APACHE](./LICENSE-APACHE) contains the original license used by the OSS-Fuzz project repository at the time the
+file was migrated.
 
 [oss-fuzz-repo]: https://github.com/google/oss-fuzz
 
diff --git a/fuzzing/dictionaries/fuzz_tree.dict b/fuzzing/dictionaries/fuzz_tree.dict
deleted file mode 100644
index 3ebe52b7f..000000000
--- a/fuzzing/dictionaries/fuzz_tree.dict
+++ /dev/null
@@ -1,13 +0,0 @@
-"\\001\\000\\000\\000"
-"_join_multiline_va"
-"setdef"
-"1\\000\\000\\000\\000\\000\\000\\000"
-"\\000\\000\\000\\000\\000\\000\\000\\020"
-"\\377\\377\\377\\377\\377\\377\\377r"
-"\\001\\000\\000\\000\\000\\000\\000\\001"
-"\\000\\000\\000\\000\\000\\000\\000\\014"
-"\\000\\000\\000\\000\\000\\000\\000\\003"
-"\\001\\000"
-"\\032\\000\\000\\000\\000\\000\\000\\000"
-"-\\000\\000\\000\\000\\000\\000\\000"
-"__format"
diff --git a/fuzzing/fuzz-targets/fuzz_repo.py b/fuzzing/fuzz-targets/fuzz_repo.py
new file mode 100644
index 000000000..7bd82c120
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_repo.py
@@ -0,0 +1,47 @@
+import atheris
+import io
+import sys
+import os
+import tempfile
+
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
+with atheris.instrument_imports():
+    import git
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        repo = git.Repo.init(path=temp_dir)
+
+        # Generate a minimal set of files based on fuzz data to minimize I/O operations.
+        file_paths = [os.path.join(temp_dir, f"File{i}") for i in range(min(3, fdp.ConsumeIntInRange(1, 3)))]
+        for file_path in file_paths:
+            with open(file_path, "wb") as f:
+                # The chosen upperbound for count of bytes we consume by writing to these
+                # files is somewhat arbitrary and may be worth experimenting with if the
+                # fuzzer coverage plateaus.
+                f.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512)))
+
+        repo.index.add(file_paths)
+        repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 80)))
+
+        fuzz_tree = git.Tree(repo, git.Tree.NULL_BIN_SHA, 0, "")
+
+        try:
+            fuzz_tree._deserialize(io.BytesIO(data))
+        except IndexError:
+            return -1
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzing/fuzz-targets/fuzz_tree.py b/fuzzing/fuzz-targets/fuzz_tree.py
deleted file mode 100644
index 4e2038add..000000000
--- a/fuzzing/fuzz-targets/fuzz_tree.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-###############################################################################
-# Note: This file has been modified by contributors to GitPython.
-# The original state of this file may be referenced here:
-# https://github.com/google/oss-fuzz/commit/f26f254558fc48f3c9bc130b10507386b94522da
-###############################################################################
-import atheris
-import io
-import sys
-import os
-import shutil
-
-if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
-    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
-    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
-
-with atheris.instrument_imports():
-    import git
-
-
-def TestOneInput(data):
-    fdp = atheris.FuzzedDataProvider(data)
-    git_dir = "/tmp/.git"
-    head_file = os.path.join(git_dir, "HEAD")
-    refs_dir = os.path.join(git_dir, "refs")
-    common_dir = os.path.join(git_dir, "commondir")
-    objects_dir = os.path.join(git_dir, "objects")
-
-    if os.path.isdir(git_dir):
-        shutil.rmtree(git_dir)
-
-    os.mkdir(git_dir)
-    with open(head_file, "w") as f:
-        f.write(fdp.ConsumeUnicodeNoSurrogates(1024))
-    os.mkdir(refs_dir)
-    os.mkdir(common_dir)
-    os.mkdir(objects_dir)
-
-    _repo = git.Repo("/tmp/")
-
-    fuzz_tree = git.Tree(_repo, git.Tree.NULL_BIN_SHA, 0, "")
-    try:
-        fuzz_tree._deserialize(io.BytesIO(data))
-    except IndexError:
-        return -1
-
-
-def main():
-    atheris.Setup(sys.argv, TestOneInput)
-    atheris.Fuzz()
-
-
-if __name__ == "__main__":
-    main()

From 48abb1cbc138cd9c013369ea4608dd2fe5ca7a62 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Sat, 4 May 2024 14:40:26 -0400
Subject: [PATCH 820/994] Add git.Blob fuzz target

Based on the `test_blob.py` unit test.
---
 fuzzing/dictionaries/fuzz_blob.dict |  1 +
 fuzzing/fuzz-targets/fuzz_blob.py   | 36 +++++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
 create mode 100644 fuzzing/dictionaries/fuzz_blob.dict
 create mode 100644 fuzzing/fuzz-targets/fuzz_blob.py

diff --git a/fuzzing/dictionaries/fuzz_blob.dict b/fuzzing/dictionaries/fuzz_blob.dict
new file mode 100644
index 000000000..7f123f830
--- /dev/null
+++ b/fuzzing/dictionaries/fuzz_blob.dict
@@ -0,0 +1 @@
+"\\377\\377\\377\\377\\377\\377\\377\\377" 
diff --git a/fuzzing/fuzz-targets/fuzz_blob.py b/fuzzing/fuzz-targets/fuzz_blob.py
new file mode 100644
index 000000000..9d296de40
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_blob.py
@@ -0,0 +1,36 @@
+import atheris
+import sys
+import os
+import tempfile
+
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
+with atheris.instrument_imports():
+    import git
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        repo = git.Repo.init(path=temp_dir)
+        blob = git.Blob(
+            repo,
+            **{
+                "binsha": git.Blob.NULL_BIN_SHA,
+                "path": fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes()),
+            },
+        )
+
+        _ = blob.mime_type
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()

From 6823e4543f33eb623df14a5a27c9731199de7a4f Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Sat, 4 May 2024 15:44:23 -0400
Subject: [PATCH 821/994] Use fuzzed data for all git.Blob arguments

This increases the edges reached by the fuzzer, making for a more
effective test with higher coverage.
---
 fuzzing/fuzz-targets/fuzz_blob.py | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_blob.py b/fuzzing/fuzz-targets/fuzz_blob.py
index 9d296de40..ce888e85f 100644
--- a/fuzzing/fuzz-targets/fuzz_blob.py
+++ b/fuzzing/fuzz-targets/fuzz_blob.py
@@ -16,13 +16,17 @@ def TestOneInput(data):
 
     with tempfile.TemporaryDirectory() as temp_dir:
         repo = git.Repo.init(path=temp_dir)
-        blob = git.Blob(
-            repo,
-            **{
-                "binsha": git.Blob.NULL_BIN_SHA,
-                "path": fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes()),
-            },
-        )
+        binsha = fdp.ConsumeBytes(20)
+        mode = fdp.ConsumeInt(fdp.ConsumeIntInRange(0, fdp.remaining_bytes()))
+        path = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
+
+        try:
+            blob = git.Blob(repo, binsha, mode, path)
+        except AssertionError as e:
+            if "Require 20 byte binary sha, got" in str(e):
+                return -1
+            else:
+                raise e
 
         _ = blob.mime_type
 

From e15caab8e70adc44b796bd3d972e1d34d30ad7ee Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Tue, 7 May 2024 19:26:54 +0200
Subject: [PATCH 822/994] lint: switch order Ruff's hooks `fix` -> `format`

---
 .pre-commit-config.yaml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 585b4f04d..987d86cd9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,12 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.3.2
+  rev: v0.4.3
   hooks:
-  - id: ruff-format
-    exclude: ^git/ext/
   - id: ruff
     args: ["--fix"]
     exclude: ^git/ext/
+  - id: ruff-format
+    exclude: ^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
   rev: v0.9.0.6

From 2cfd2007b4a73bb061506e7c521570e9a0ec3f96 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 8 May 2024 03:20:18 -0400
Subject: [PATCH 823/994] Update OSS-Fuzz Scripts to Use New QA-Assets Repo
 Structure

This change is required to support the changes to the seed data repo
structure introduced in:
https://github.com/gitpython-developers/qa-assets/pull/2

This moves most of the seed data related build steps into the OSS-Fuzz
Docker image build via `container-environment-bootstrap.sh`. This
includes moveing the dictionaries into that repo.

The fuzzing/README.md here should be updated in a follow-up with a link
to the qa-assets repo (and probably some context setting about corpora
in general) but I have opted to defer that as I think the functionality
added by the seed data improvements is valuable as is and shouldn't be
blocked by documentation writers block.
---
 fuzzing/README.md                             | 19 ------
 fuzzing/dictionaries/fuzz_blob.dict           |  1 -
 fuzzing/dictionaries/fuzz_config.dict         | 56 ----------------
 fuzzing/oss-fuzz-scripts/build.sh             | 27 +-------
 .../container-environment-bootstrap.sh        | 64 +++++++++++++++----
 5 files changed, 53 insertions(+), 114 deletions(-)
 delete mode 100644 fuzzing/dictionaries/fuzz_blob.dict
 delete mode 100644 fuzzing/dictionaries/fuzz_config.dict

diff --git a/fuzzing/README.md b/fuzzing/README.md
index 9d02bf72f..286f529eb 100644
--- a/fuzzing/README.md
+++ b/fuzzing/README.md
@@ -76,25 +76,6 @@ Contains Python files for each fuzz test.
   reason, fuzz tests should gracefully handle anticipated exception cases with a `try`/`except` block to avoid false
   positives that halt the fuzzing engine.
 
-### Dictionaries (`dictionaries/`)
-
-Provides hints to the fuzzing engine about inputs that might trigger unique code paths. Each fuzz target may have a
-corresponding `.dict` file. For information about dictionary syntax, refer to
-the [LibFuzzer documentation on the subject](https://llvm.org/docs/LibFuzzer.html#dictionaries).
-
-**Things to Know**:
-
-- OSS-Fuzz loads dictionary files per fuzz target if one exists with the same name, all others are ignored.
-- Most entries in the dictionary files found here are escaped hex or Unicode values that were recommended by the fuzzing
-  engine after previous runs.
-- A default set of dictionary entries are created for all fuzz targets as part of the build process, regardless of an
-  existing file here.
-- Development or updates to dictionaries should reflect the varied formats and edge cases relevant to the
-  functionalities under test.
-- Example dictionaries (some of which are used to build the default dictionaries mentioned above) can be found here:
-  - [AFL++ dictionary repository](https://github.com/AFLplusplus/AFLplusplus/tree/stable/dictionaries#readme)
-  - [Google/fuzzing dictionary repository](https://github.com/google/fuzzing/tree/master/dictionaries)
-
 ### OSS-Fuzz Scripts (`oss-fuzz-scripts/`)
 
 Includes scripts for building and integrating fuzz targets with OSS-Fuzz:
diff --git a/fuzzing/dictionaries/fuzz_blob.dict b/fuzzing/dictionaries/fuzz_blob.dict
deleted file mode 100644
index 7f123f830..000000000
--- a/fuzzing/dictionaries/fuzz_blob.dict
+++ /dev/null
@@ -1 +0,0 @@
-"\\377\\377\\377\\377\\377\\377\\377\\377" 
diff --git a/fuzzing/dictionaries/fuzz_config.dict b/fuzzing/dictionaries/fuzz_config.dict
deleted file mode 100644
index b545ddfc8..000000000
--- a/fuzzing/dictionaries/fuzz_config.dict
+++ /dev/null
@@ -1,56 +0,0 @@
-"\\004\\000\\000\\000\\000\\000\\000\\000"
-"\\006\\000\\000\\000\\000\\000\\000\\000"
-"_validate_value_"
-"\\000\\000\\000\\000\\000\\000\\000\\000"
-"rem"
-"__eq__"
-"\\001\\000\\000\\000"
-"__abstrac"
-"_mutating_methods_"
-"items"
-"\\0021\\""
-"\\001\\000"
-"\\000\\000\\000\\000"
-"DEFAULT"
-"getfloat"
-"\\004\\000\\000\\000\\000\\000\\000\\000"
-"news"
-"\\037\\000\\000\\000\\000\\000\\000\\000"
-"\\001\\000\\000\\000\\000\\000\\000\\037"
-"\\000\\000\\000\\000\\000\\000\\000\\014"
-"list"
-"\\376\\377\\377\\377\\377\\377\\377\\377"
-"items_all"
-"\\004\\000\\000\\000\\000\\000\\000\\000"
-"\\377\\377\\377\\377\\377\\377\\377\\014"
-"\\001\\000\\000\\000"
-"_acqui"
-"\\000\\000\\000\\000\\000\\000\\000\\000"
-"__ne__"
-"__exit__"
-"__modu"
-"uucp"
-"__str__"
-"\\001\\000\\000\\000"
-"\\017\\000\\000\\000\\000\\000\\000\\000"
-"_has_incl"
-"update"
-"\\377\\377\\377\\377\\377\\377\\377\\023"
-"setdef"
-"setdefaul"
-"\\000\\000\\000\\000"
-"\\001\\000\\000\\000"
-"\\001\\000"
-"\\022\\000\\000\\000\\000\\000\\000\\000"
-"_value_to_string"
-"__abstr"
-"\\001\\000\\000\\000\\000\\000\\000\\000"
-"\\000\\000\\000\\000\\000\\000\\000\\022"
-"\\377\\377\\377\\377"
-"\\004\\000\\000\\000\\000\\000\\000\\000"
-"\\000\\000\\000\\000\\000\\000\\000\\000"
-"\\000\\000\\000\\000\\000\\000\\000\\037"
-"\\001\\000\\000\\000\\000\\000\\000\\013"
-"_OPT_TM"
-"__name__"
-"_get_conv"
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index 58c9adb5a..e0b3a50ab 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -7,34 +7,13 @@ set -euo pipefail
 
 python3 -m pip install .
 
-# Directory to look in for dictionaries, options files, and seed corpora:
-SEED_DATA_DIR="$SRC/seed_data"
-
-find "$SEED_DATA_DIR" \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name '*.dict' \) \
-  ! \( -name '__base.*' \) -exec printf 'Copying: %s\n' {} \; \
+find "$SRC" -maxdepth 1 \
+  \( -name '*_seed_corpus.zip' -o -name '*.options' -o -name '*.dict' \) \
+  -exec printf '[%s] Copying: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" {} \; \
   -exec chmod a-x {} \; \
   -exec cp {} "$OUT" \;
 
 # Build fuzzers in $OUT.
 find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do
   compile_python_fuzzer "$fuzz_harness" --add-binary="$(command -v git):."
-
-  common_base_dictionary_filename="$SEED_DATA_DIR/__base.dict"
-  if [[ -r "$common_base_dictionary_filename" ]]; then
-    # Strip the `.py` extension from the filename and replace it with `.dict`.
-    fuzz_harness_dictionary_filename="$(basename "$fuzz_harness" .py).dict"
-    output_file="$OUT/$fuzz_harness_dictionary_filename"
-
-    printf 'Appending %s to %s\n' "$common_base_dictionary_filename" "$output_file"
-    if [[ -s "$output_file" ]]; then
-      # If a dictionary file for this fuzzer already exists and is not empty,
-      # we append a new line to the end of it before appending any new entries.
-      #
-      # LibFuzzer will happily ignore multiple empty lines in a dictionary but fail with an error
-      # if any single line has incorrect syntax (e.g., if we accidentally add two entries to the same line.)
-      # See docs for valid syntax: https://llvm.org/docs/LibFuzzer.html#id32
-      echo >>"$output_file"
-    fi
-    cat "$common_base_dictionary_filename" >>"$output_file"
-  fi
 done
diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index 76ec97c7f..bbdcf5357 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -9,23 +9,20 @@ set -euo pipefail
 # Prerequisites #
 #################
 
-for cmd in python3 git wget rsync; do
+for cmd in python3 git wget zip; do
   command -v "$cmd" >/dev/null 2>&1 || {
     printf '[%s] Required command %s not found, exiting.\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$cmd" >&2
     exit 1
   }
 done
 
-SEED_DATA_DIR="$SRC/seed_data"
-mkdir -p "$SEED_DATA_DIR"
-
 #############
 # Functions #
 #############
 
 download_and_concatenate_common_dictionaries() {
   # Assign the first argument as the target file where all contents will be concatenated
-  target_file="$1"
+  local target_file="$1"
 
   # Shift the arguments so the first argument (target_file path) is removed
   # and only URLs are left for the loop below.
@@ -38,22 +35,61 @@ download_and_concatenate_common_dictionaries() {
   done
 }
 
-fetch_seed_corpora() {
-  # Seed corpus zip files are hosted in a separate repository to avoid additional bloat in this repo.
-  git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git qa-assets &&
-    rsync -avc qa-assets/gitpython/corpra/ "$SEED_DATA_DIR/" &&
-    rm -rf qa-assets # Clean up the cloned repo to keep the Docker image as slim as possible.
+create_seed_corpora_zips() {
+  local seed_corpora_dir="$1"
+  local output_zip
+  for dir in "$seed_corpora_dir"/*; do
+    if [ -d "$dir" ] && [ -n "$dir" ]; then
+      output_zip="$SRC/$(basename "$dir")_seed_corpus.zip"
+      printf '[%s] Zipping the contents of %s into %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$dir" "$output_zip"
+      zip -jur "$output_zip" "$dir"/*
+    fi
+  done
+}
+
+prepare_dictionaries_for_fuzz_targets() {
+  local dictionaries_dir="$1"
+  local fuzz_targets_dir="$2"
+  local common_base_dictionary_filename="$WORK/__base.dict"
+
+  printf '[%s] Copying .dict files from %s to %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"  "$dictionaries_dir" "$SRC/"
+  cp -v "$dictionaries_dir"/*.dict "$SRC/"
+
+  download_and_concatenate_common_dictionaries "$common_base_dictionary_filename" \
+    "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \
+    "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict"
+
+  find "$fuzz_targets_dir" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do
+    if [[ -r "$common_base_dictionary_filename" ]]; then
+      # Strip the `.py` extension from the filename and replace it with `.dict`.
+      fuzz_harness_dictionary_filename="$(basename "$fuzz_harness" .py).dict"
+      local output_file="$SRC/$fuzz_harness_dictionary_filename"
+
+      printf '[%s] Appending %s to %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$common_base_dictionary_filename" "$output_file"
+      if [[ -s "$output_file" ]]; then
+        # If a dictionary file for this fuzzer already exists and is not empty,
+        # we append a new line to the end of it before appending any new entries.
+        #
+        # LibFuzzer will happily ignore multiple empty lines in a dictionary but fail with an error
+        # if any single line has incorrect syntax (e.g., if we accidentally add two entries to the same line.)
+        # See docs for valid syntax: https://llvm.org/docs/LibFuzzer.html#id32
+        echo >>"$output_file"
+      fi
+      cat "$common_base_dictionary_filename" >>"$output_file"
+    fi
+  done
 }
 
 ########################
 # Main execution logic #
 ########################
+# Seed corpora and dictionaries are hosted in a separate repository to avoid additional bloat in this repo.
+# We clone into the $WORK directory because OSS-Fuzz cleans it up after building the image, keeping the image small.
+git clone --depth 1 https://github.com/gitpython-developers/qa-assets.git "$WORK/qa-assets"
 
-fetch_seed_corpora
+create_seed_corpora_zips "$WORK/qa-assets/gitpython/corpora"
 
-download_and_concatenate_common_dictionaries "$SEED_DATA_DIR/__base.dict" \
-  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/utf8.dict" \
-  "https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/url.dict"
+prepare_dictionaries_for_fuzz_targets "$WORK/qa-assets/gitpython/dictionaries" "$SRC/gitpython/fuzzing"
 
 # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
 python3 -m pip install --upgrade pip

From a915adf08e570d8989bb070f647e2a3ee941871d Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 8 May 2024 17:06:19 -0400
Subject: [PATCH 824/994] Add `Diff` Fuzz Target

Adds a new `fuzz_diff.py` fuzz target that covers `Diff` class
initialization using fuzzed data.
---
 fuzzing/fuzz-targets/fuzz_diff.py | 54 +++++++++++++++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 fuzzing/fuzz-targets/fuzz_diff.py

diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py
new file mode 100644
index 000000000..cf01e7ffa
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_diff.py
@@ -0,0 +1,54 @@
+import sys
+import os
+import tempfile
+from binascii import Error as BinasciiError
+
+import atheris
+
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
+with atheris.instrument_imports():
+    from git import Repo, Diff
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        repo = Repo.init(path=temp_dir)
+        try:
+            Diff(
+                repo,
+                a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                a_blob_id=fdp.ConsumeBytes(20),
+                b_blob_id=fdp.ConsumeBytes(20),
+                a_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                b_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                new_file=fdp.ConsumeBool(),
+                deleted_file=fdp.ConsumeBool(),
+                copied_file=fdp.ConsumeBool(),
+                raw_rename_from=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                raw_rename_to=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                diff=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                change_type=fdp.PickValueInList(["A", "D", "C", "M", "R", "T", "U"]),
+                score=fdp.ConsumeIntInRange(0, fdp.remaining_bytes()),
+            )
+        except BinasciiError:
+            return -1
+        except AssertionError as e:
+            if "Require 20 byte binary sha, got" in str(e):
+                return -1
+            else:
+                raise e
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()

From 989ae1ac03e25a5ce51d4c615128dcf75b9e24f5 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 8 May 2024 19:28:29 -0400
Subject: [PATCH 825/994] Read class properties & call methods to cover more
 features

Property access and private methods on the `Diff` class are complex and
involve encoding and decoding operations that warrant being tested.

This test borrows its design from the `test_diff.py` unit test file.
---
 fuzzing/fuzz-targets/fuzz_diff.py | 31 ++++++++++++++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py
index cf01e7ffa..ba44995f2 100644
--- a/fuzzing/fuzz-targets/fuzz_diff.py
+++ b/fuzzing/fuzz-targets/fuzz_diff.py
@@ -1,5 +1,6 @@
 import sys
 import os
+import io
 import tempfile
 from binascii import Error as BinasciiError
 
@@ -13,13 +14,26 @@
     from git import Repo, Diff
 
 
+class BytesProcessAdapter:
+    """Allows bytes to be used as process objects returned by subprocess.Popen."""
+
+    def __init__(self, input_string):
+        self.stdout = io.BytesIO(input_string)
+        self.stderr = io.BytesIO()
+
+    def wait(self):
+        return 0
+
+    poll = wait
+
+
 def TestOneInput(data):
     fdp = atheris.FuzzedDataProvider(data)
 
     with tempfile.TemporaryDirectory() as temp_dir:
         repo = Repo.init(path=temp_dir)
         try:
-            Diff(
+            diff = Diff(
                 repo,
                 a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
                 b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
@@ -44,6 +58,21 @@ def TestOneInput(data):
             else:
                 raise e
 
+        _ = diff.__str__()
+        _ = diff.a_path
+        _ = diff.b_path
+        _ = diff.rename_from
+        _ = diff.rename_to
+        _ = diff.renamed_file
+
+        diff_index = diff._index_from_patch_format(
+            repo, proc=BytesProcessAdapter(fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())))
+        )
+
+        diff._handle_diff_line(
+            lines_bytes=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), repo=repo, index=diff_index
+        )
+
 
 def main():
     atheris.Setup(sys.argv, TestOneInput)

From 315a2fd03c94c93d4a7089d23d734e4aaccbe066 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 15 May 2024 13:36:29 -0400
Subject: [PATCH 826/994] Instrument test utility functions to increase fuzzer
 efficiency

Fuzz Introspector was reporting a high percentage of fuzz blockers
in the `fuzz_diff` test. This means the fuzzing engine was unable
to gain visibility into functions lower in the call stack than the
blocking functions, making it less effective at producing interesting
input data.

This clears a large percentage of the fuzz blockers by adding fuzzer
instrumentation to them via the `@atheris.instrument_func` decorator.
---
 fuzzing/fuzz-targets/fuzz_diff.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py
index ba44995f2..d4bd68b57 100644
--- a/fuzzing/fuzz-targets/fuzz_diff.py
+++ b/fuzzing/fuzz-targets/fuzz_diff.py
@@ -17,16 +17,19 @@
 class BytesProcessAdapter:
     """Allows bytes to be used as process objects returned by subprocess.Popen."""
 
+    @atheris.instrument_func
     def __init__(self, input_string):
         self.stdout = io.BytesIO(input_string)
         self.stderr = io.BytesIO()
 
+    @atheris.instrument_func
     def wait(self):
         return 0
 
     poll = wait
 
 
+@atheris.instrument_func
 def TestOneInput(data):
     fdp = atheris.FuzzedDataProvider(data)
 

From cf81c6c98155c24d69af6f1a8eca368ad1a5d962 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 26 May 2024 16:40:23 -0400
Subject: [PATCH 827/994] Momentarily downgrade Git on Cygwin to investigate
 failures

Using this older version is not in general secure, since the new
version is a security update. It is sometimes acceptable to run
software with security bugs in CI workflows, but the intent of this
change is just to check if the version of the Cygwin `git` package
is the cause of the failures. If so, they can probably be fixed or
worked around in a better way than downgrading. (Furthermore, the
lower version of the `git` package will not always be avaialable
from Cygwin's repositories.)
---
 .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 61e6a3089..a2fe588ad 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Set up Cygwin
       uses: egor-tensin/setup-cygwin@v4
       with:
-        packages: python39=3.9.16-1 python39-pip python39-virtualenv git
+        packages: python39=3.9.16-1 python39-pip python39-virtualenv git=2.43.0-1
 
     - name: Arrange for verbose output
       run: |

From eb06a18d83eda0ae04e2a00b2d656da147e9188a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 26 May 2024 16:45:57 -0400
Subject: [PATCH 828/994] Unpin Cygwin `git`; add our `.git` as a
 `safe.directory`

This undoes the change of pinning Git to an earlier version (before
the recent security update) on Cygwin, and instead adds the `.git`
subdirectory of the `GitPython` directory as an additional value of
the multi-valued `safe.directory` Git configuration variable.
---
 .github/workflows/cygwin-test.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index a2fe588ad..bde4ea659 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Set up Cygwin
       uses: egor-tensin/setup-cygwin@v4
       with:
-        packages: python39=3.9.16-1 python39-pip python39-virtualenv git=2.43.0-1
+        packages: python39=3.9.16-1 python39-pip python39-virtualenv git
 
     - name: Arrange for verbose output
       run: |
@@ -40,6 +40,7 @@ jobs:
     - name: Special configuration for Cygwin git
       run: |
         git config --global --add safe.directory "$(pwd)"
+        git config --global --add safe.directory "$(pwd)/.git"
         git config --global core.autocrlf false
 
     - name: Prepare this repo for tests

From d3b181d54a8da6b8561474dba1333682b47b7ba7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 27 May 2024 13:22:29 +0000
Subject: [PATCH 829/994] Bump Vampire/setup-wsl from 3.0.0 to 3.1.0

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 4c918a92d..574048620 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -44,7 +44,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v3.0.0
+      uses: Vampire/setup-wsl@v3.1.0
       with:
         distribution: Debian
 

From 7bdcfa556ad476d89a3643137e97ee5749e3c7df Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Mon, 27 May 2024 21:27:07 +0200
Subject: [PATCH 830/994] Update to the fixed version of `Vampire`

---
 .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 574048620..031b0e6b2 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -44,7 +44,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v3.1.0
+      uses: Vampire/setup-wsl@v3.1.1
       with:
         distribution: Debian
 

From 6d52bdbe6a546ecb76e28f7dde45b44fe8577010 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 29 May 2024 22:06:40 -0400
Subject: [PATCH 831/994] Add Submodules Fuzz Target

Fuzz Introspector heuristics suggest the Submodule API code represent
"optimal analysis targets" that should yield a meaningful increase in
code coverage. The changes here introduce a first pass at implementing a
fuzz harness that cover the primary APIs/methods related to Submodules.
Of particular interest to me is the `Submodule.config_writer()`
coverage.

Please note however, there is likely plenty of room for improvement in
this harness in terms of both code coverage as well as performance; the
latter of which will see significant benefit from a well curated seed
corpus of `.gitmodules` file like inputs. The `ParsingError` raised by
the fuzzer without a good seed corpus hinders test efficacy
significantly.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 93 ++++++++++++++++++++++++++
 fuzzing/fuzz-targets/utils.py          | 22 ++++++
 2 files changed, 115 insertions(+)
 create mode 100644 fuzzing/fuzz-targets/fuzz_submodule.py
 create mode 100644 fuzzing/fuzz-targets/utils.py

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
new file mode 100644
index 000000000..ddcbaa00f
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -0,0 +1,93 @@
+import atheris
+import sys
+import os
+import tempfile
+from configparser import ParsingError
+from utils import is_expected_exception_message
+
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
+with atheris.instrument_imports():
+    from git import Repo, GitCommandError, InvalidGitRepositoryError
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+
+    with tempfile.TemporaryDirectory() as repo_temp_dir:
+        repo = Repo.init(path=repo_temp_dir)
+        repo.index.commit("Initial commit")
+
+        try:
+            with tempfile.TemporaryDirectory() as submodule_temp_dir:
+                sub_repo = Repo.init(submodule_temp_dir, bare=fdp.ConsumeBool())
+                sub_repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512)))
+
+                submodule_name = f"submodule_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}"
+                submodule_path = os.path.join(repo.working_tree_dir, submodule_name)
+                submodule_url = sub_repo.git_dir
+
+                submodule = repo.create_submodule(submodule_name, submodule_path, url=submodule_url)
+                repo.index.commit(f"Added submodule {submodule_name}")
+
+                with submodule.config_writer() as writer:
+                    key_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))
+                    value_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))
+
+                    writer.set_value(
+                        fdp.ConsumeUnicodeNoSurrogates(key_length), fdp.ConsumeUnicodeNoSurrogates(value_length)
+                    )
+                    writer.release()
+
+                submodule.update(init=fdp.ConsumeBool(), dry_run=fdp.ConsumeBool(), force=fdp.ConsumeBool())
+
+                submodule_repo = submodule.module()
+                new_file_path = os.path.join(
+                    submodule_repo.working_tree_dir,
+                    f"new_file_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}",
+                )
+                with open(new_file_path, "wb") as new_file:
+                    new_file.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512)))
+                submodule_repo.index.add([new_file_path])
+                submodule_repo.index.commit("Added new file to submodule")
+
+                repo.submodule_update(recursive=fdp.ConsumeBool())
+                submodule_repo.head.reset(commit="HEAD~1", working_tree=fdp.ConsumeBool(), head=fdp.ConsumeBool())
+                # Use fdp.PickValueInList to ensure at least one of 'module' or 'configuration' is True
+                module_option_value, configuration_option_value = fdp.PickValueInList(
+                    [(True, False), (False, True), (True, True)]
+                )
+                submodule.remove(
+                    module=module_option_value,
+                    configuration=configuration_option_value,
+                    dry_run=fdp.ConsumeBool(),
+                    force=fdp.ConsumeBool(),
+                )
+                repo.index.commit(f"Removed submodule {submodule_name}")
+
+        except (ParsingError, GitCommandError, InvalidGitRepositoryError, FileNotFoundError, BrokenPipeError):
+            return -1
+        except (ValueError, OSError) as e:
+            expected_messages = [
+                "SHA is empty",
+                "Reference at",
+                "embedded null byte",
+                "This submodule instance does not exist anymore",
+                "cmd stdin was empty",
+                "File name too long",
+            ]
+            if is_expected_exception_message(e, expected_messages):
+                return -1
+            else:
+                raise e
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzing/fuzz-targets/utils.py b/fuzzing/fuzz-targets/utils.py
new file mode 100644
index 000000000..42faa8eb0
--- /dev/null
+++ b/fuzzing/fuzz-targets/utils.py
@@ -0,0 +1,22 @@
+import atheris  # pragma: no cover
+from typing import List  # pragma: no cover
+
+
+@atheris.instrument_func
+def is_expected_exception_message(exception: Exception, error_message_list: List[str]) -> bool:  # pragma: no cover
+    """
+    Checks if the message of a given exception matches any of the expected error messages, case-insensitively.
+
+    Args:
+         exception (Exception): The exception object raised during execution.
+         error_message_list (List[str]): A list of error message substrings to check against the exception's message.
+
+    Returns:
+      bool: True if the exception's message contains any of the substrings from the error_message_list,
+      case-insensitively, otherwise False.
+    """
+    exception_message = str(exception).lower()
+    for error in error_message_list:
+        if error.lower() in exception_message:
+            return True
+    return False

From 9e67138819c7e081fee89a5b855c89b538a8f604 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Thu, 30 May 2024 12:22:45 +0200
Subject: [PATCH 832/994] precommit: enable `end-of-file-fixer`

---
 .pre-commit-config.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 585b4f04d..50f430084 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,6 +18,7 @@ repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
   rev: v4.5.0
   hooks:
+  - id: end-of-file-fixer
   - id: check-toml
   - id: check-yaml
   - id: check-merge-conflict

From 96e21f0055060b01d03b705cdb582edd3551aa43 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Thu, 30 May 2024 12:24:19 +0200
Subject: [PATCH 833/994] apply

---
 doc/source/index.rst                      | 1 -
 doc/source/intro.rst                      | 1 -
 doc/source/roadmap.rst                    | 1 -
 test/fixtures/.gitconfig                  | 2 +-
 test/fixtures/blame                       | 2 +-
 test/fixtures/cat_file_blob               | 2 +-
 test/fixtures/git_config                  | 1 -
 test/fixtures/git_config_with_empty_value | 2 +-
 test/fixtures/rev_list_bisect_all         | 1 -
 test/fixtures/rev_list_commit_diffs       | 1 -
 test/fixtures/rev_list_commit_idabbrev    | 1 -
 test/fixtures/rev_list_commit_stats       | 1 -
 12 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/doc/source/index.rst b/doc/source/index.rst
index 72db8ee5a..ca5229ac3 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -21,4 +21,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/doc/source/intro.rst b/doc/source/intro.rst
index 4f22a0942..d053bd117 100644
--- a/doc/source/intro.rst
+++ b/doc/source/intro.rst
@@ -122,4 +122,3 @@ License Information
 ===================
 GitPython is licensed under the New BSD License.  See the LICENSE file for
 more information.
-
diff --git a/doc/source/roadmap.rst b/doc/source/roadmap.rst
index a573df33a..34c953626 100644
--- a/doc/source/roadmap.rst
+++ b/doc/source/roadmap.rst
@@ -6,4 +6,3 @@ The full list of milestones including associated tasks can be found on GitHub:
 https://github.com/gitpython-developers/GitPython/issues
 
 Select the respective milestone to filter the list of issues accordingly.
-
diff --git a/test/fixtures/.gitconfig b/test/fixtures/.gitconfig
index 6a0459f6b..f6c25c15a 100644
--- a/test/fixtures/.gitconfig
+++ b/test/fixtures/.gitconfig
@@ -1,3 +1,3 @@
 [alias]
     rbi = "!g() { git rebase -i origin/${1:-master} ; } ; g"
-    expush = "!f() { git branch -f tmp ; { git rbi $1 && git push ; } ; git reset --hard tmp ; git rebase origin/${1:-master}; } ; f"
\ No newline at end of file
+    expush = "!f() { git branch -f tmp ; { git rbi $1 && git push ; } ; git reset --hard tmp ; git rebase origin/${1:-master}; } ; f"
diff --git a/test/fixtures/blame b/test/fixtures/blame
index 10c141dda..949976c5d 100644
--- a/test/fixtures/blame
+++ b/test/fixtures/blame
@@ -128,4 +128,4 @@ b6e1b765e0c15586a2c5b9832854f95defd71e1f 23 23
 634396b2f541a9f2d58b00be1a07f0c358b999b3 11 24 2
 	  VERSION = '1.0.0'
 634396b2f541a9f2d58b00be1a07f0c358b999b3 12 25
-	end
\ No newline at end of file
+	end
diff --git a/test/fixtures/cat_file_blob b/test/fixtures/cat_file_blob
index 70c379b63..802992c42 100644
--- a/test/fixtures/cat_file_blob
+++ b/test/fixtures/cat_file_blob
@@ -1 +1 @@
-Hello world
\ No newline at end of file
+Hello world
diff --git a/test/fixtures/git_config b/test/fixtures/git_config
index a8cad56e8..d3066d86e 100644
--- a/test/fixtures/git_config
+++ b/test/fixtures/git_config
@@ -43,4 +43,3 @@
 # inclusions should be processed immediately
 [sec]
     var1 = value1_main
-
diff --git a/test/fixtures/git_config_with_empty_value b/test/fixtures/git_config_with_empty_value
index 0427caea5..83de84c8b 100644
--- a/test/fixtures/git_config_with_empty_value
+++ b/test/fixtures/git_config_with_empty_value
@@ -1,4 +1,4 @@
 [color]
 	ui
 [core]
-	filemode = true
\ No newline at end of file
+	filemode = true
diff --git a/test/fixtures/rev_list_bisect_all b/test/fixtures/rev_list_bisect_all
index 342ea94ae..60d382d01 100644
--- a/test/fixtures/rev_list_bisect_all
+++ b/test/fixtures/rev_list_bisect_all
@@ -48,4 +48,3 @@ committer David Aguilar <davvid@gmail.com> 1220418344 -0700
     This resolves the issue mentioned in that thread.
     
     Signed-off-by: David Aguilar <davvid@gmail.com>
-
diff --git a/test/fixtures/rev_list_commit_diffs b/test/fixtures/rev_list_commit_diffs
index 20397e2e4..c39df2061 100644
--- a/test/fixtures/rev_list_commit_diffs
+++ b/test/fixtures/rev_list_commit_diffs
@@ -5,4 +5,3 @@ author Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
 committer Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
 
     fix some initialization warnings
-
diff --git a/test/fixtures/rev_list_commit_idabbrev b/test/fixtures/rev_list_commit_idabbrev
index 9385ba713..6266df93e 100644
--- a/test/fixtures/rev_list_commit_idabbrev
+++ b/test/fixtures/rev_list_commit_idabbrev
@@ -5,4 +5,3 @@ author tom <tom@taco.(none)> 1195608462 -0800
 committer tom <tom@taco.(none)> 1195608462 -0800
 
     fix tests on other machines
-
diff --git a/test/fixtures/rev_list_commit_stats b/test/fixtures/rev_list_commit_stats
index 60aa8cf58..c78aadeb5 100644
--- a/test/fixtures/rev_list_commit_stats
+++ b/test/fixtures/rev_list_commit_stats
@@ -4,4 +4,3 @@ author Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
 committer Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
 
     initial grit setup
-

From 2ce9675b7238fb1a498f1ea4f5dae8a26d4b89ec Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Thu, 30 May 2024 12:25:33 +0200
Subject: [PATCH 834/994] precommit: enable `validate-pyproject`

---
 .pre-commit-config.yaml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 585b4f04d..02950db8c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,3 +21,8 @@ repos:
   - id: check-toml
   - id: check-yaml
   - id: check-merge-conflict
+
+- repo: https://github.com/abravalheri/validate-pyproject
+  rev: v0.16
+  hooks:
+    - id: validate-pyproject
\ No newline at end of file

From 7b684cd43cf0f9c54adb8a8def54fa07f5cfd145 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 30 May 2024 10:34:49 -0400
Subject: [PATCH 835/994] Add graceful handling of expected exceptions in
 `fuzz_submodule.py`

Fixes: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69350

**`IsADirectoryError`**

Fuzzer provided input data can sometimes produce filenames that look
like directories and raise `IsADirectoryError` exceptions which crash
the fuzzer. This commit catches those cases and returns -1 to instruct
libfuzzer that the inputs are not valuable to add to the corpus.

**`FileExistsError`**

Similar to the above, this is a possible exception case produced by the
fuzzed data and not a bug so its handled the same.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index ddcbaa00f..9406fc68b 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -67,7 +67,15 @@ def TestOneInput(data):
                 )
                 repo.index.commit(f"Removed submodule {submodule_name}")
 
-        except (ParsingError, GitCommandError, InvalidGitRepositoryError, FileNotFoundError, BrokenPipeError):
+        except (
+            ParsingError,
+            GitCommandError,
+            InvalidGitRepositoryError,
+            FileNotFoundError,
+            FileExistsError,
+            IsADirectoryError,
+            BrokenPipeError,
+        ):
             return -1
         except (ValueError, OSError) as e:
             expected_messages = [

From 6c00ce602eb19eda342e827a25d005610ce92fa8 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 30 May 2024 13:53:42 -0400
Subject: [PATCH 836/994] Improve file name generation to prevent "File name
 too long" `OSError`'s

Adds a utility function to limit the maximum file name legnth produced
by the fuzzer to a max size dictated by the host its run on.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 13 ++++++-------
 fuzzing/fuzz-targets/utils.py          | 15 +++++++++++++++
 2 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 9406fc68b..817ce8f98 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -3,7 +3,7 @@
 import os
 import tempfile
 from configparser import ParsingError
-from utils import is_expected_exception_message
+from utils import is_expected_exception_message, get_max_filename_length
 
 if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
     path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
@@ -42,12 +42,12 @@ def TestOneInput(data):
                     writer.release()
 
                 submodule.update(init=fdp.ConsumeBool(), dry_run=fdp.ConsumeBool(), force=fdp.ConsumeBool())
-
                 submodule_repo = submodule.module()
-                new_file_path = os.path.join(
-                    submodule_repo.working_tree_dir,
-                    f"new_file_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}",
+
+                new_file_name = fdp.ConsumeUnicodeNoSurrogates(
+                    fdp.ConsumeIntInRange(1, max(1, get_max_filename_length(submodule_repo.working_tree_dir)))
                 )
+                new_file_path = os.path.join(submodule_repo.working_tree_dir, new_file_name)
                 with open(new_file_path, "wb") as new_file:
                     new_file.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512)))
                 submodule_repo.index.add([new_file_path])
@@ -77,14 +77,13 @@ def TestOneInput(data):
             BrokenPipeError,
         ):
             return -1
-        except (ValueError, OSError) as e:
+        except ValueError as e:
             expected_messages = [
                 "SHA is empty",
                 "Reference at",
                 "embedded null byte",
                 "This submodule instance does not exist anymore",
                 "cmd stdin was empty",
-                "File name too long",
             ]
             if is_expected_exception_message(e, expected_messages):
                 return -1
diff --git a/fuzzing/fuzz-targets/utils.py b/fuzzing/fuzz-targets/utils.py
index 42faa8eb0..86f049341 100644
--- a/fuzzing/fuzz-targets/utils.py
+++ b/fuzzing/fuzz-targets/utils.py
@@ -1,4 +1,5 @@
 import atheris  # pragma: no cover
+import os
 from typing import List  # pragma: no cover
 
 
@@ -20,3 +21,17 @@ def is_expected_exception_message(exception: Exception, error_message_list: List
         if error.lower() in exception_message:
             return True
     return False
+
+
+@atheris.instrument_func
+def get_max_filename_length(path: str) -> int:
+    """
+    Get the maximum filename length for the filesystem containing the given path.
+
+    Args:
+        path (str): The path to check the filesystem for.
+
+    Returns:
+        int: The maximum filename length.
+    """
+    return os.pathconf(path, "PC_NAME_MAX")

From 2a2294f9d1e46d9bbe11cd2031d62e5441fe19c4 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 30 May 2024 14:02:27 -0400
Subject: [PATCH 837/994] Improve `fuzz_submodule.py` coverage & efficacy

The fuzzer was having trouble analyzing `fuzz_submodule.py` when using
the `atheris.instrument_imports()` context manager. Switching to
`atheris.instrument_all()` instead slightly increases the startup time
for the fuzzer, but significantly improves the fuzzing engines ability
to identify new coverage.

The changes here also disable warnings that are logged to `stdout` from
the SUT. These warnings are expected to happen with some inputs and
clutter the fuzzer output logs. They can be optionally re-enabled for
debugging by passing a flag o the Python interpreter command line or
setting the `PYTHONWARNINGS` environment variable.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 16 +++++++++++++---
 fuzzing/fuzz-targets/utils.py          |  4 ++--
 2 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 817ce8f98..53f5a7884 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -4,13 +4,22 @@
 import tempfile
 from configparser import ParsingError
 from utils import is_expected_exception_message, get_max_filename_length
+from git import Repo, GitCommandError, InvalidGitRepositoryError
 
-if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):  # pragma: no cover
     path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
     os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
 
-with atheris.instrument_imports():
-    from git import Repo, GitCommandError, InvalidGitRepositoryError
+if not sys.warnoptions:  # pragma: no cover
+    # The warnings filter below can be overridden by passing the -W option
+    # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
+    import warnings
+    import logging
+
+    # Fuzzing data causes some plugins to generate a large number of warnings
+    # which are not usually interesting and make the test output hard to read, so we ignore them.
+    warnings.simplefilter("ignore")
+    logging.getLogger().setLevel(logging.ERROR)
 
 
 def TestOneInput(data):
@@ -92,6 +101,7 @@ def TestOneInput(data):
 
 
 def main():
+    atheris.instrument_all()
     atheris.Setup(sys.argv, TestOneInput)
     atheris.Fuzz()
 
diff --git a/fuzzing/fuzz-targets/utils.py b/fuzzing/fuzz-targets/utils.py
index 86f049341..f522d2959 100644
--- a/fuzzing/fuzz-targets/utils.py
+++ b/fuzzing/fuzz-targets/utils.py
@@ -1,5 +1,5 @@
 import atheris  # pragma: no cover
-import os
+import os  # pragma: no cover
 from typing import List  # pragma: no cover
 
 
@@ -24,7 +24,7 @@ def is_expected_exception_message(exception: Exception, error_message_list: List
 
 
 @atheris.instrument_func
-def get_max_filename_length(path: str) -> int:
+def get_max_filename_length(path: str) -> int:  # pragma: no cover
     """
     Get the maximum filename length for the filesystem containing the given path.
 

From 57a56a8a2874d2ab76f4034b9d3c98e09ed7fa35 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 30 May 2024 14:12:02 -0400
Subject: [PATCH 838/994] Add graceful handling for `NotADirectoryError`s

---
 fuzzing/fuzz-targets/fuzz_submodule.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 53f5a7884..cfd1a6d3f 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -83,6 +83,7 @@ def TestOneInput(data):
             FileNotFoundError,
             FileExistsError,
             IsADirectoryError,
+            NotADirectoryError,
             BrokenPipeError,
         ):
             return -1

From 2b64dee466ed72523684f90a037d604355121df0 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 30 May 2024 14:17:20 -0400
Subject: [PATCH 839/994] Improve comment wording

---
 fuzzing/fuzz-targets/fuzz_submodule.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index cfd1a6d3f..92b569949 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -16,7 +16,7 @@
     import warnings
     import logging
 
-    # Fuzzing data causes some plugins to generate a large number of warnings
+    # Fuzzing data causes some modules to generate a large number of warnings
     # which are not usually interesting and make the test output hard to read, so we ignore them.
     warnings.simplefilter("ignore")
     logging.getLogger().setLevel(logging.ERROR)

From 882425ded5ae210c7092b87f4ea6bc871784ae89 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Fri, 31 May 2024 07:24:47 +0200
Subject: [PATCH 840/994] Add missing newline in `prec-commit-config.yaml`

Just to be sure the coming linting won't be disturbed by that.
---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 02950db8c..fe966adad 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,4 +25,4 @@ repos:
 - repo: https://github.com/abravalheri/validate-pyproject
   rev: v0.16
   hooks:
-    - id: validate-pyproject
\ No newline at end of file
+    - id: validate-pyproject

From 59a0c88a08de4b35608d82b107844915a787f192 Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Mon, 3 Jun 2024 00:26:11 +0500
Subject: [PATCH 841/994] Fix IndexFile items argument type

Error before commit:

path: os.PathLike = ...
repo = git.Repo(path_dir)
repo.index.add(path)
---
 git/index/base.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index b8161ea52..fc4474cac 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -658,7 +658,7 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
         return os.path.relpath(path, self.repo.working_tree_dir)
 
     def _preprocess_add_items(
-        self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]
+        self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
     ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
         """Split the items into two lists of path strings and BaseEntries."""
         paths = []
@@ -749,7 +749,7 @@ def _entries_for_paths(
 
     def add(
         self,
-        items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
+        items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
         force: bool = True,
         fprogress: Callable = lambda *args: None,
         path_rewriter: Union[Callable[..., PathLike], None] = None,
@@ -976,7 +976,7 @@ def _items_to_rela_paths(
     @default_index
     def remove(
         self,
-        items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
+        items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
         working_tree: bool = False,
         **kwargs: Any,
     ) -> List[str]:
@@ -1036,7 +1036,7 @@ def remove(
     @default_index
     def move(
         self,
-        items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
+        items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
         skip_errors: bool = False,
         **kwargs: Any,
     ) -> List[Tuple[str, str]]:

From 77fb5f06bd86a02f481a1d34ca0938bb5b7f5219 Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Mon, 3 Jun 2024 00:28:45 +0500
Subject: [PATCH 842/994] Specify DiffIndex generic type

Example before this commit:

repo = git.Repo(path_dir)
diff = repo.index.diff(None)
modified_files = [d for d in repo.index.diff(None)]
reveal_type(modified_files) # list[Unknown] instead of list[Diff]
---
 git/diff.py       | 8 ++++----
 git/index/base.py | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/git/diff.py b/git/diff.py
index f89b12d98..e9f7e209f 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -187,7 +187,7 @@ def diff(
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
-    ) -> "DiffIndex":
+    ) -> "DiffIndex[Diff]":
         """Create diffs between two items being trees, trees and index or an index and
         the working tree. Detects renames automatically.
 
@@ -581,7 +581,7 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m
         return None
 
     @classmethod
-    def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex:
+    def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
         """Create a new :class:`DiffIndex` from the given process output which must be
         in patch format.
 
@@ -674,7 +674,7 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
         return index
 
     @staticmethod
-    def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> None:
+    def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
         lines = lines_bytes.decode(defenc)
 
         # Discard everything before the first colon, and the colon itself.
@@ -747,7 +747,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non
             index.append(diff)
 
     @classmethod
-    def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex":
+    def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
         """Create a new :class:`DiffIndex` from the given process output which must be
         in raw format.
 
diff --git a/git/index/base.py b/git/index/base.py
index fc4474cac..28b60a880 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1478,7 +1478,7 @@ def diff(
         paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
         create_patch: bool = False,
         **kwargs: Any,
-    ) -> git_diff.DiffIndex:
+    ) -> git_diff.DiffIndex[git_diff.Diff]:
         """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
         or :class:`~git.objects.commit.Commit` object.
 

From 491e134d2a930d12cc4250951e9e986dbab2be2d Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 4 Jun 2024 06:58:07 -0400
Subject: [PATCH 843/994] Fix Improper Import Order Breaking `fuzz_submodule`
 Fuzzer

ClusterFuzz runs of the `fuzz_submodule` target have been failing
because the `git` import was placed before the condition that sets the
Git executable path.

The order in which `git` is imported matters because it attempts to find
a Git executable as the import is loaded (via `refresh()` in
`git/__init__.py`.) As per #1909, we configure the ClusterFuzz
environment to use a bundled Git executable via the env variable
condition in all fuzz targets.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 92b569949..ca47690ea 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -4,12 +4,13 @@
 import tempfile
 from configparser import ParsingError
 from utils import is_expected_exception_message, get_max_filename_length
-from git import Repo, GitCommandError, InvalidGitRepositoryError
 
 if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):  # pragma: no cover
     path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
     os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
 
+from git import Repo, GitCommandError, InvalidGitRepositoryError
+
 if not sys.warnoptions:  # pragma: no cover
     # The warnings filter below can be overridden by passing the -W option
     # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.

From d59708812f50362f526d4c6aa67b7218d12024f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kamil=20Krzy=C5=9Bk=C3=B3w?= <kamilzary@gmail.com>
Date: Sat, 8 Jun 2024 01:23:21 +0200
Subject: [PATCH 844/994] Add deprecation test for DiffIndex.iter_change_type

---
 test/deprecation/test_basic.py | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/test/deprecation/test_basic.py b/test/deprecation/test_basic.py
index 6235a836c..3bf0287c7 100644
--- a/test/deprecation/test_basic.py
+++ b/test/deprecation/test_basic.py
@@ -31,7 +31,7 @@
 if TYPE_CHECKING:
     from pathlib import Path
 
-    from git.diff import Diff
+    from git.diff import Diff, DiffIndex
     from git.objects.commit import Commit
 
 # ------------------------------------------------------------------------
@@ -54,6 +54,12 @@ def diff(commit: "Commit") -> Generator["Diff", None, None]:
     yield diff
 
 
+@pytest.fixture
+def diffs(commit: "Commit") -> Generator["DiffIndex", None, None]:
+    """Fixture to supply a DiffIndex."""
+    yield commit.diff(NULL_TREE)
+
+
 def test_diff_renamed_warns(diff: "Diff") -> None:
     """The deprecated Diff.renamed property issues a deprecation warning."""
     with pytest.deprecated_call():
@@ -122,3 +128,10 @@ def test_iterable_obj_inheriting_does_not_warn() -> None:
 
         class Derived(IterableObj):
             pass
+
+
+def test_diff_iter_change_type(diffs: "DiffIndex") -> None:
+    """The internal DiffIndex.iter_change_type function issues no deprecation warning."""
+    with assert_no_deprecation_warning():
+        for change_type in diffs.change_type:
+            [*diffs.iter_change_type(change_type=change_type)]

From e1c660d224a1d27469c9275940c9db841570b8d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kamil=20Krzy=C5=9Bk=C3=B3w?=
 <34622465+kamilkrzyskow@users.noreply.github.com>
Date: Tue, 28 May 2024 05:53:44 +0200
Subject: [PATCH 845/994] Fix iter_change_type diff renamed property to prevent
 warning

---
 git/diff.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/diff.py b/git/diff.py
index e9f7e209f..8d2646f99 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -325,7 +325,7 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
                 yield diffidx
             elif change_type == "C" and diffidx.copied_file:
                 yield diffidx
-            elif change_type == "R" and diffidx.renamed:
+            elif change_type == "R" and diffidx.renamed_file:
                 yield diffidx
             elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
                 yield diffidx

From f1ec1f15ec13e369bb5a4d758e94d7877e481ed3 Mon Sep 17 00:00:00 2001
From: Nick Papior <nickpapior@gmail.com>
Date: Thu, 13 Jun 2024 14:35:03 +0200
Subject: [PATCH 846/994] fixed doc to not faulty do #1924

Signed-off-by: Nick Papior <nickpapior@gmail.com>
---
 test/test_docs.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/test_docs.py b/test/test_docs.py
index b3547c1de..cc0bbf26a 100644
--- a/test/test_docs.py
+++ b/test/test_docs.py
@@ -469,11 +469,11 @@ def test_references_and_objects(self, rw_dir):
         # ![30-test_references_and_objects]
 
         # [31-test_references_and_objects]
-        git = repo.git
-        git.checkout("HEAD", b="my_new_branch")  # Create a new branch.
-        git.branch("another-new-one")
-        git.branch("-D", "another-new-one")  # Pass strings for full control over argument order.
-        git.for_each_ref()  # '-' becomes '_' when calling it.
+        git_cmd = repo.git
+        git_cmd.checkout("HEAD", b="my_new_branch")  # Create a new branch.
+        git_cmd.branch("another-new-one")
+        git_cmd.branch("-D", "another-new-one")  # Pass strings for full control over argument order.
+        git_cmd.for_each_ref()  # '-' becomes '_' when calling it.
         # ![31-test_references_and_objects]
 
         repo.git.clear_cache()

From d35998f5f420db780a30d73b703296498e3aa531 Mon Sep 17 00:00:00 2001
From: Guillaume Cardoen <G.Cardoen@outlook.com>
Date: Mon, 17 Jun 2024 09:28:32 +0200
Subject: [PATCH 847/994] fix: fix beginning whitespace error

---
 git/diff.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/diff.py b/git/diff.py
index 8d2646f99..9c6ae59e0 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -695,7 +695,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]
             change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
             score_str = "".join(_change_type[1:])
             score = int(score_str) if score_str.isdigit() else None
-            path = path.strip()
+            path = path.strip("\n")
             a_path = path.encode(defenc)
             b_path = path.encode(defenc)
             deleted_file = False

From 9910a886ddeb05a39b774e9f3520837fd9a76dca Mon Sep 17 00:00:00 2001
From: Guillaume Cardoen <G.Cardoen@outlook.com>
Date: Mon, 17 Jun 2024 09:31:51 +0200
Subject: [PATCH 848/994] test: add test for diff with beginning whitespace

---
 test/test_diff.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/test/test_diff.py b/test/test_diff.py
index 928a9f428..6cae3fbf2 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -529,3 +529,23 @@ def test_diff_patch_with_external_engine(self, rw_dir):
         self.assertEqual(len(index_against_head), 1)
         index_against_working_tree = repo.index.diff(None, create_patch=True)
         self.assertEqual(len(index_against_working_tree), 1)
+
+    @with_rw_directory
+    def test_beginning_space(self, rw_dir):
+        # Create a file beginning by a whitespace
+        repo = Repo.init(rw_dir)
+        file = osp.join(rw_dir, " file.txt")
+        with open(file, "w") as f:
+            f.write("hello world")
+        repo.git.add(Git.polish_url(file))
+        repo.index.commit("first commit")
+        
+        # Diff the commit with an empty tree
+        # and check the paths
+        diff_index = repo.head.commit.diff(NULL_TREE)
+        d = diff_index[0]
+        a_path = d.a_path
+        b_path = d.b_path
+        self.assertEqual(a_path, " file.txt")
+        self.assertEqual(b_path, " file.txt")
+    
\ No newline at end of file

From 97fad9cb8322e510647cf58cc023702a7b7e077f Mon Sep 17 00:00:00 2001
From: Guillaume Cardoen <G.Cardoen@outlook.com>
Date: Tue, 18 Jun 2024 08:51:00 +0200
Subject: [PATCH 849/994] style: ruff

---
 test/test_diff.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/test_diff.py b/test/test_diff.py
index 6cae3fbf2..612fbd9e0 100644
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -539,7 +539,7 @@ def test_beginning_space(self, rw_dir):
             f.write("hello world")
         repo.git.add(Git.polish_url(file))
         repo.index.commit("first commit")
-        
+
         # Diff the commit with an empty tree
         # and check the paths
         diff_index = repo.head.commit.diff(NULL_TREE)
@@ -548,4 +548,3 @@ def test_beginning_space(self, rw_dir):
         b_path = d.b_path
         self.assertEqual(a_path, " file.txt")
         self.assertEqual(b_path, " file.txt")
-    
\ No newline at end of file

From f96eb0cdaeb6e33bf7725e1fb0385509f6030969 Mon Sep 17 00:00:00 2001
From: Patrick Massot <patrickmassot@free.fr>
Date: Mon, 24 Jun 2024 14:02:53 -0400
Subject: [PATCH 850/994] Change aliases to work around mypy issue.

Fixes #1934
Note this should also gives better LSP support to these property aliases.
---
 git/remote.py    | 11 +++++++++--
 git/repo/base.py | 24 ++++++++++++++++++++----
 2 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 37c991d27..15e360064 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -828,8 +828,15 @@ def remove(cls, repo: "Repo", name: str) -> str:
             name._clear_cache()
         return name
 
-    # `rm` is an alias.
-    rm = remove
+    @classmethod
+    def rm(cls, repo: "Repo", name: str) -> str:
+        """Alias of remove.
+        Remove the remote with the given name.
+
+        :return:
+            The passed remote name to remove
+        """
+        return cls.remove(repo, name)
 
     def rename(self, new_name: str) -> "Remote":
         """Rename self to the given `new_name`.
diff --git a/git/repo/base.py b/git/repo/base.py
index 51ea76901..346248ddb 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -402,6 +402,17 @@ def heads(self) -> "IterableList[Head]":
         """
         return Head.list_items(self)
 
+    @property
+    def branches(self) -> "IterableList[Head]":
+        """Alias for heads.
+        A list of :class:`~git.refs.head.Head` objects representing the branch heads
+        in this repo.
+
+        :return:
+            ``git.IterableList(Head, ...)``
+        """
+        return self.heads
+
     @property
     def references(self) -> "IterableList[Reference]":
         """A list of :class:`~git.refs.reference.Reference` objects representing tags,
@@ -412,11 +423,16 @@ def references(self) -> "IterableList[Reference]":
         """
         return Reference.list_items(self)
 
-    # Alias for references.
-    refs = references
+    @property
+    def refs(self) -> "IterableList[Reference]":
+        """Alias for references.
+        A list of :class:`~git.refs.reference.Reference` objects representing tags,
+        heads and remote references.
 
-    # Alias for heads.
-    branches = heads
+        :return:
+            ``git.IterableList(Reference, ...)``
+        """
+        return self.references
 
     @property
     def index(self) -> "IndexFile":

From 366a60760cea066b40ed33815fa8256b25afdfcc Mon Sep 17 00:00:00 2001
From: jirka <jirka.borovec@seznam.cz>
Date: Tue, 16 Jul 2024 12:35:36 +0200
Subject: [PATCH 851/994] exclude: test/fixtures/

---
 .pre-commit-config.yaml                   | 1 +
 test/fixtures/.gitconfig                  | 2 +-
 test/fixtures/blame                       | 2 +-
 test/fixtures/cat_file_blob               | 2 +-
 test/fixtures/git_config                  | 1 +
 test/fixtures/git_config_with_empty_value | 2 +-
 test/fixtures/rev_list_bisect_all         | 1 +
 test/fixtures/rev_list_commit_diffs       | 1 +
 test/fixtures/rev_list_commit_idabbrev    | 1 +
 test/fixtures/rev_list_commit_stats       | 1 +
 10 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 50f430084..5491c4297 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,6 +19,7 @@ repos:
   rev: v4.5.0
   hooks:
   - id: end-of-file-fixer
+    exclude: test/fixtures/
   - id: check-toml
   - id: check-yaml
   - id: check-merge-conflict
diff --git a/test/fixtures/.gitconfig b/test/fixtures/.gitconfig
index f6c25c15a..6a0459f6b 100644
--- a/test/fixtures/.gitconfig
+++ b/test/fixtures/.gitconfig
@@ -1,3 +1,3 @@
 [alias]
     rbi = "!g() { git rebase -i origin/${1:-master} ; } ; g"
-    expush = "!f() { git branch -f tmp ; { git rbi $1 && git push ; } ; git reset --hard tmp ; git rebase origin/${1:-master}; } ; f"
+    expush = "!f() { git branch -f tmp ; { git rbi $1 && git push ; } ; git reset --hard tmp ; git rebase origin/${1:-master}; } ; f"
\ No newline at end of file
diff --git a/test/fixtures/blame b/test/fixtures/blame
index 949976c5d..10c141dda 100644
--- a/test/fixtures/blame
+++ b/test/fixtures/blame
@@ -128,4 +128,4 @@ b6e1b765e0c15586a2c5b9832854f95defd71e1f 23 23
 634396b2f541a9f2d58b00be1a07f0c358b999b3 11 24 2
 	  VERSION = '1.0.0'
 634396b2f541a9f2d58b00be1a07f0c358b999b3 12 25
-	end
+	end
\ No newline at end of file
diff --git a/test/fixtures/cat_file_blob b/test/fixtures/cat_file_blob
index 802992c42..70c379b63 100644
--- a/test/fixtures/cat_file_blob
+++ b/test/fixtures/cat_file_blob
@@ -1 +1 @@
-Hello world
+Hello world
\ No newline at end of file
diff --git a/test/fixtures/git_config b/test/fixtures/git_config
index d3066d86e..a8cad56e8 100644
--- a/test/fixtures/git_config
+++ b/test/fixtures/git_config
@@ -43,3 +43,4 @@
 # inclusions should be processed immediately
 [sec]
     var1 = value1_main
+
diff --git a/test/fixtures/git_config_with_empty_value b/test/fixtures/git_config_with_empty_value
index 83de84c8b..0427caea5 100644
--- a/test/fixtures/git_config_with_empty_value
+++ b/test/fixtures/git_config_with_empty_value
@@ -1,4 +1,4 @@
 [color]
 	ui
 [core]
-	filemode = true
+	filemode = true
\ No newline at end of file
diff --git a/test/fixtures/rev_list_bisect_all b/test/fixtures/rev_list_bisect_all
index 60d382d01..342ea94ae 100644
--- a/test/fixtures/rev_list_bisect_all
+++ b/test/fixtures/rev_list_bisect_all
@@ -48,3 +48,4 @@ committer David Aguilar <davvid@gmail.com> 1220418344 -0700
     This resolves the issue mentioned in that thread.
     
     Signed-off-by: David Aguilar <davvid@gmail.com>
+
diff --git a/test/fixtures/rev_list_commit_diffs b/test/fixtures/rev_list_commit_diffs
index c39df2061..20397e2e4 100644
--- a/test/fixtures/rev_list_commit_diffs
+++ b/test/fixtures/rev_list_commit_diffs
@@ -5,3 +5,4 @@ author Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
 committer Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
 
     fix some initialization warnings
+
diff --git a/test/fixtures/rev_list_commit_idabbrev b/test/fixtures/rev_list_commit_idabbrev
index 6266df93e..9385ba713 100644
--- a/test/fixtures/rev_list_commit_idabbrev
+++ b/test/fixtures/rev_list_commit_idabbrev
@@ -5,3 +5,4 @@ author tom <tom@taco.(none)> 1195608462 -0800
 committer tom <tom@taco.(none)> 1195608462 -0800
 
     fix tests on other machines
+
diff --git a/test/fixtures/rev_list_commit_stats b/test/fixtures/rev_list_commit_stats
index c78aadeb5..60aa8cf58 100644
--- a/test/fixtures/rev_list_commit_stats
+++ b/test/fixtures/rev_list_commit_stats
@@ -4,3 +4,4 @@ author Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
 committer Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
 
     initial grit setup
+

From 1c88b0a734142cfc05114ad2ca0794c565294fb9 Mon Sep 17 00:00:00 2001
From: jirka <jirka.borovec@seznam.cz>
Date: Tue, 7 May 2024 19:32:10 +0200
Subject: [PATCH 852/994] use codespell

---
 .pre-commit-config.yaml | 6 ++++++
 pyproject.toml          | 5 +++++
 2 files changed, 11 insertions(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 551d8be34..23272bc25 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,10 @@
 repos:
+- repo: https://github.com/codespell-project/codespell
+  rev: v2.2.4
+  hooks:
+  - id: codespell
+    additional_dependencies: [tomli]
+
 - repo: https://github.com/astral-sh/ruff-pre-commit
   rev: v0.4.3
   hooks:
diff --git a/pyproject.toml b/pyproject.toml
index ee54edb78..7fc809a6d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,3 +78,8 @@ lint.unfixable = [
 "test/**" = [
     "B018",  # useless-expression
 ]
+
+[tool.codespell]
+#skip = '*.po,*.ts,./src/3rdParty,./src/Test'
+#count = true
+quiet-level = 3
\ No newline at end of file

From 2ce013cc0043f7968f126ea38482a32077efa991 Mon Sep 17 00:00:00 2001
From: Jirka <jirka.borovec@seznam.cz>
Date: Tue, 7 May 2024 19:38:44 +0200
Subject: [PATCH 853/994] fix & skip

---
 .pre-commit-config.yaml | 1 +
 git/index/base.py       | 2 +-
 git/remote.py           | 2 +-
 pyproject.toml          | 3 ++-
 4 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 23272bc25..03730febd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,6 +4,7 @@ repos:
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
+    args: ["--write-changes"]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
   rev: v0.4.3
diff --git a/git/index/base.py b/git/index/base.py
index 28b60a880..a317e71c0 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1443,7 +1443,7 @@ def reset(
                     key = entry_key(path, 0)
                     self.entries[key] = nie[key]
                 except KeyError:
-                    # If key is not in theirs, it musn't be in ours.
+                    # If key is not in theirs, it mustn't be in ours.
                     try:
                         del self.entries[key]
                     except KeyError:
diff --git a/git/remote.py b/git/remote.py
index 15e360064..1e09e210e 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -250,7 +250,7 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
                 flags |= cls.NEW_TAG
             elif "[new branch]" in summary:
                 flags |= cls.NEW_HEAD
-            # uptodate encoded in control character
+            # up-to-date encoded in control character
         else:
             # Fast-forward or forced update - was encoded in control character,
             # but we parse the old and new commit.
diff --git a/pyproject.toml b/pyproject.toml
index 7fc809a6d..8b4522824 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -80,6 +80,7 @@ lint.unfixable = [
 ]
 
 [tool.codespell]
-#skip = '*.po,*.ts,./src/3rdParty,./src/Test'
+skip = 'test/fixtures/reflog_*'
+ignore-words-list="gud,doesnt"
 #count = true
 quiet-level = 3
\ No newline at end of file

From 93993b201458fd18059e97fa25a08a14fae2af1f Mon Sep 17 00:00:00 2001
From: jirka <jirka.borovec@seznam.cz>
Date: Wed, 17 Jul 2024 12:31:02 +0200
Subject: [PATCH 854/994] fixing

---
 .pre-commit-config.yaml | 3 ++-
 README.md               | 2 +-
 doc/source/changes.rst  | 2 +-
 git/objects/util.py     | 6 +++---
 pyproject.toml          | 3 +--
 test/test_exc.py        | 2 +-
 test/test_index.py      | 6 +++---
 7 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 03730febd..692c7fa2a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,8 @@ repos:
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
-    args: ["--write-changes"]
+    # args: ["--write-changes"]  # consider enabling for auto-fif
+    exclude: "test/fixtures/"
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
   rev: v0.4.3
diff --git a/README.md b/README.md
index d365a6584..59c6f995b 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ In the less common case that you do not want to install test dependencies, `pip
 
 #### With editable *dependencies* (not preferred, and rarely needed)
 
-In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediatley reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
+In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediately reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
 
 If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
 
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 0bc757134..3c903423c 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -20,7 +20,7 @@ https://github.com/gitpython-developers/GitPython/releases/tag/3.1.42
 3.1.41
 ======
 
-This release is relevant for security as it fixes a possible arbitary
+This release is relevant for security as it fixes a possible arbitrary
 code execution on Windows.
 
 See this PR for details: https://github.com/gitpython-developers/GitPython/pull/1792
diff --git a/git/objects/util.py b/git/objects/util.py
index 5c56e6134..a68d701f5 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -568,11 +568,11 @@ def addToStack(
                 yield rval
 
             # Only continue to next level if this is appropriate!
-            nd = d + 1
-            if depth > -1 and nd > depth:
+            next_d = d + 1
+            if depth > -1 and next_d > depth:
                 continue
 
-            addToStack(stack, item, branch_first, nd)
+            addToStack(stack, item, branch_first, next_d)
         # END for each item on work stack
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 8b4522824..603e2597c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -80,7 +80,6 @@ lint.unfixable = [
 ]
 
 [tool.codespell]
-skip = 'test/fixtures/reflog_*'
 ignore-words-list="gud,doesnt"
 #count = true
-quiet-level = 3
\ No newline at end of file
+quiet-level = 3
diff --git a/test/test_exc.py b/test/test_exc.py
index c1eae7240..2e979f5a1 100644
--- a/test/test_exc.py
+++ b/test/test_exc.py
@@ -52,7 +52,7 @@
 
 _streams_n_substrings = (
     None,
-    "steram",
+    "stream",
     "ομορφο stream",
 )
 
diff --git a/test/test_index.py b/test/test_index.py
index b92258c92..2684cfd81 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1018,7 +1018,7 @@ class Mocked:
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",
-        rasies=HookExecutionError,
+        raises=HookExecutionError,
     )
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
@@ -1077,7 +1077,7 @@ def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",
-        rasies=HookExecutionError,
+        raises=HookExecutionError,
     )
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.WslNoDistro,
@@ -1120,7 +1120,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",
-        rasies=HookExecutionError,
+        raises=HookExecutionError,
     )
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Wsl,

From 813520c123d44a8cf87a219fc710faeb1f1559ca Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Wed, 17 Jul 2024 12:34:14 +0200
Subject: [PATCH 855/994] Apply suggestions from code review

---
 git/remote.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/remote.py b/git/remote.py
index 1e09e210e..9de3dace4 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -250,7 +250,7 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo":
                 flags |= cls.NEW_TAG
             elif "[new branch]" in summary:
                 flags |= cls.NEW_HEAD
-            # up-to-date encoded in control character
+            # `uptodate` encoded in control character
         else:
             # Fast-forward or forced update - was encoded in control character,
             # but we parse the old and new commit.

From ce8a69a4141d2149bac2cbf56ea7d4b1f2ed7257 Mon Sep 17 00:00:00 2001
From: Jonas Scharpf <jonas.scharpf@checkmk.com>
Date: Wed, 17 Jul 2024 11:01:09 +0200
Subject: [PATCH 856/994] Add type of change to files_dict of a commit

This allows to not only get the total, inserted or deleted number of
lines being changed but also the type of change like
Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R),
type changed (T), Unmerged (U), Unknown (X), or pairing Broken (B)
---
 AUTHORS                    |  1 +
 git/objects/commit.py      | 24 +++++++++++++++++-------
 git/types.py               |  1 +
 git/util.py                |  4 +++-
 test/fixtures/diff_numstat |  5 +++--
 test/test_commit.py        |  9 ++++++---
 test/test_stats.py         | 12 +++++++++---
 7 files changed, 40 insertions(+), 16 deletions(-)

diff --git a/AUTHORS b/AUTHORS
index 9311b3962..45b14c961 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -54,5 +54,6 @@ Contributors are:
 -Wenhan Zhu <wzhu.cosmos _at_ gmail.com>
 -Eliah Kagan <eliah.kagan _at_ gmail.com>
 -Ethan Lin <et.repositories _at_ gmail.com>
+-Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
 
 Portions derived from other open source works and are clearly marked.
diff --git a/git/objects/commit.py b/git/objects/commit.py
index d957c9051..0ceb46609 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -377,15 +377,25 @@ def stats(self) -> Stats:
         :return:
             :class:`Stats`
         """
-        if not self.parents:
-            text = self.repo.git.diff_tree(self.hexsha, "--", numstat=True, no_renames=True, root=True)
-            text2 = ""
-            for line in text.splitlines()[1:]:
+
+        def process_lines(lines: List[str]) -> str:
+            text = ""
+            for file_info, line in zip(lines, lines[len(lines) // 2 :]):
+                change_type = file_info.split("\t")[0][-1]
                 (insertions, deletions, filename) = line.split("\t")
-                text2 += "%s\t%s\t%s\n" % (insertions, deletions, filename)
-            text = text2
+                text += "%s\t%s\t%s\t%s\n" % (change_type, insertions, deletions, filename)
+            return text
+
+        if not self.parents:
+            lines = self.repo.git.diff_tree(
+                self.hexsha, "--", numstat=True, no_renames=True, root=True, raw=True
+            ).splitlines()[1:]
+            text = process_lines(lines)
         else:
-            text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True)
+            lines = self.repo.git.diff(
+                self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True, raw=True
+            ).splitlines()
+            text = process_lines(lines)
         return Stats._list_from_string(self.repo, text)
 
     @property
diff --git a/git/types.py b/git/types.py
index 584450146..cce184530 100644
--- a/git/types.py
+++ b/git/types.py
@@ -248,6 +248,7 @@ class Files_TD(TypedDict):
     insertions: int
     deletions: int
     lines: int
+    change_type: str
 
 
 class Total_TD(TypedDict):
diff --git a/git/util.py b/git/util.py
index 11f963e02..9e8ac821d 100644
--- a/git/util.py
+++ b/git/util.py
@@ -910,6 +910,7 @@ class Stats:
       deletions = number of deleted lines as int
       insertions = number of inserted lines as int
       lines = total number of lines changed as int, or deletions + insertions
+      change_type = type of change as str, A|C|D|M|R|T|U|X|B
 
     ``full-stat-dict``
 
@@ -938,7 +939,7 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
             "files": {},
         }
         for line in text.splitlines():
-            (raw_insertions, raw_deletions, filename) = line.split("\t")
+            (change_type, raw_insertions, raw_deletions, filename) = line.split("\t")
             insertions = raw_insertions != "-" and int(raw_insertions) or 0
             deletions = raw_deletions != "-" and int(raw_deletions) or 0
             hsh["total"]["insertions"] += insertions
@@ -949,6 +950,7 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
                 "insertions": insertions,
                 "deletions": deletions,
                 "lines": insertions + deletions,
+                "change_type": change_type,
             }
             hsh["files"][filename.strip()] = files_dict
         return Stats(hsh["total"], hsh["files"])
diff --git a/test/fixtures/diff_numstat b/test/fixtures/diff_numstat
index 44c6ca2d5..b76e467eb 100644
--- a/test/fixtures/diff_numstat
+++ b/test/fixtures/diff_numstat
@@ -1,2 +1,3 @@
-29	18	a.txt
-0	5	b.txt
+M	29	18	a.txt
+M	0	5	b.txt
+A	7	0	c.txt
\ No newline at end of file
diff --git a/test/test_commit.py b/test/test_commit.py
index 5832258de..37c66e3e7 100644
--- a/test/test_commit.py
+++ b/test/test_commit.py
@@ -135,9 +135,12 @@ def test_stats(self):
         commit = self.rorepo.commit("33ebe7acec14b25c5f84f35a664803fcab2f7781")
         stats = commit.stats
 
-        def check_entries(d):
+        def check_entries(d, has_change_type=False):
             assert isinstance(d, dict)
-            for key in ("insertions", "deletions", "lines"):
+            keys = ("insertions", "deletions", "lines")
+            if has_change_type:
+                keys += ("change_type",)
+            for key in keys:
                 assert key in d
 
         # END assertion helper
@@ -148,7 +151,7 @@ def check_entries(d):
         assert "files" in stats.total
 
         for _filepath, d in stats.files.items():
-            check_entries(d)
+            check_entries(d, True)
         # END for each stated file
 
         # Check that data is parsed properly.
diff --git a/test/test_stats.py b/test/test_stats.py
index eec73c802..91d2cf6ae 100644
--- a/test/test_stats.py
+++ b/test/test_stats.py
@@ -14,13 +14,19 @@ def test_list_from_string(self):
         output = fixture("diff_numstat").decode(defenc)
         stats = Stats._list_from_string(self.rorepo, output)
 
-        self.assertEqual(2, stats.total["files"])
-        self.assertEqual(52, stats.total["lines"])
-        self.assertEqual(29, stats.total["insertions"])
+        self.assertEqual(3, stats.total["files"])
+        self.assertEqual(59, stats.total["lines"])
+        self.assertEqual(36, stats.total["insertions"])
         self.assertEqual(23, stats.total["deletions"])
 
         self.assertEqual(29, stats.files["a.txt"]["insertions"])
         self.assertEqual(18, stats.files["a.txt"]["deletions"])
+        self.assertEqual("M", stats.files["a.txt"]["change_type"])
 
         self.assertEqual(0, stats.files["b.txt"]["insertions"])
         self.assertEqual(5, stats.files["b.txt"]["deletions"])
+        self.assertEqual("M", stats.files["b.txt"]["change_type"])
+
+        self.assertEqual(7, stats.files["c.txt"]["insertions"])
+        self.assertEqual(0, stats.files["c.txt"]["deletions"])
+        self.assertEqual("A", stats.files["c.txt"]["change_type"])

From 58a9a58f58e6aae220efda8ce95bf4c2e0fd9ca0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jul 2024 02:10:57 -0400
Subject: [PATCH 857/994] Use Alpine Linux in WSL on CI

Some of the CI tests use WSL. This switches the WSL distribution
from Debian to Alpine, which might be slightly faster. For the way
it is being used here, the main expected speed improvement would be
to how long the image would take to download, as Alpine is smaller.

(The reason for this is thus unrelated to the reason for the Alpine
docker CI test job added in #1826. There, the goal was to test on a
wider variety of systems and environments, and that runs the whole
test suite in Alpine. This just changes the WSL distro, used by a
few tests on Windows, from Debian to Alpine.)

Two things have changed that, taken together, have unblocked this:

- https://github.com/Vampire/setup-wsl/issues/50 was fixed, so the
  action we are using is able to install Alpine Linux. See:
  https://github.com/gitpython-developers/GitPython/pull/1917#pullrequestreview-2081550232

- #1893 was fixed in #1888. So if switching the WSL distro from
  Debian to Alpine breaks any tests, including by making them fail
  in an unexpected way that raises the wrong exception, we are
  likely to find out.
---
 .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 031b0e6b2..61ab2206c 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -46,7 +46,7 @@ jobs:
       if: startsWith(matrix.os, 'windows')
       uses: Vampire/setup-wsl@v3.1.1
       with:
-        distribution: Debian
+        distribution: Alpine
 
     - name: Prepare this repo for tests
       run: |

From ce5eefd90b6c652083ce615583b5ee62b39ae187 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jul 2024 02:39:03 -0400
Subject: [PATCH 858/994] Enable Python 3.8 and 3.9 on M1 runners

These were excluded in 9ad28c3 (#1817) due to
https://github.com/actions/setup-python/issues/808, which was later
fixed by https://github.com/actions/python-versions/pull/259.

Because Python 3.7 has been end-of-life for a while, it is very
unlikely to have AArch64 builds added in python-versions for use
on GitHub Actions CI runners (preinstalled or via setup-python).
---
 .github/workflows/pythonpackage.yml | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 031b0e6b2..f3c837742 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -18,10 +18,6 @@ jobs:
         exclude:
         - os: "macos-14"
           python-version: "3.7"
-        - os: "macos-14"
-          python-version: "3.8"
-        - os: "macos-14"
-          python-version: "3.9"
         include:
         - experimental: false
 

From 055394a548d19dded2ad9791a208bbcc54879b14 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jul 2024 03:25:31 -0400
Subject: [PATCH 859/994] Install bash in WSL Alpine distro

Because Alpine Linux does not ship with bash, and the tests that
use WSL use it.
---
 .github/workflows/pythonpackage.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 61ab2206c..1902ecb19 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -47,6 +47,7 @@ jobs:
       uses: Vampire/setup-wsl@v3.1.1
       with:
         distribution: Alpine
+        additional-packages: bash
 
     - name: Prepare this repo for tests
       run: |

From c2bbaf47e14dac5f0470938b3ecf67836ca1695d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jul 2024 04:34:00 -0400
Subject: [PATCH 860/994] Remove the non-ARM macOS CI jobs

This keeps only the macos-14 jobs, which run on Apple Silicon M1,
and removes the macos-13 jobs, which ran on x86-64.

Other operating systems jobs continue to run on x86-64 machines
(and none on ARM, yet). Only the macOS jobs are removed.

This change leaves Python 3.7 without any macOS test job. That is
probably okay, since it has been end-of-life for some time, and
it remains tested on Ubuntu and Windows.
---
 .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 49f6c5254..7547aecf9 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "macos-13", "macos-14", "windows-latest"]
+        os: ["ubuntu-latest", "macos-14", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         exclude:
         - os: "macos-14"

From be6744b6e4365fc42996a1be7f026f133f992928 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 24 Jul 2024 04:38:06 -0400
Subject: [PATCH 861/994] Use the macos-latest label rather than macos-14

Currently they are the same. The macos-latest label will move to
later versions automatically in the future, like the ubuntu-latest
and windows-latest labels that we are already using.

In this repo, the macos-14 label had been used originally because
it was added before the migration of macos-latest to be macos-14
was completed. See https://github.com/github/roadmap/issues/926. It
was kept for clarity of constrast with the macos-13 jobs that were
also in use, some for the same Python versions.

Now that the macos-13 jobs have been removed in c2bbaf4, the
macos-latest label can be used here without confusion.
---
 .github/workflows/pythonpackage.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 7547aecf9..0f1d17544 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,10 +13,10 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "macos-14", "windows-latest"]
+        os: ["ubuntu-latest", "macos-latest", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         exclude:
-        - os: "macos-14"
+        - os: "macos-latest"
           python-version: "3.7"
         include:
         - experimental: false

From af0cd933e84b9f83210c0f12f95a456606ee79e9 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 6 Jun 2024 02:17:25 -0400
Subject: [PATCH 862/994] Fix "OSError: [Errno 36] File name too long" in
 fuzz_submodule

Fixes a bug in the `fuzz_submodule` harness where the fuzzed data can
produce file names that exceed the maximum size allowed byt the OS. This
issue came up previously and was fixed in #1922, but the submodule file
name fixed here was missed in that PR.

Fixes: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69456
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index ca47690ea..9f5828d8d 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -35,12 +35,13 @@ def TestOneInput(data):
                 sub_repo = Repo.init(submodule_temp_dir, bare=fdp.ConsumeBool())
                 sub_repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512)))
 
-                submodule_name = f"submodule_{fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512))}"
+                submodule_name = fdp.ConsumeUnicodeNoSurrogates(
+                    fdp.ConsumeIntInRange(1, max(1, get_max_filename_length(repo.working_tree_dir)))
+                )
                 submodule_path = os.path.join(repo.working_tree_dir, submodule_name)
-                submodule_url = sub_repo.git_dir
 
-                submodule = repo.create_submodule(submodule_name, submodule_path, url=submodule_url)
-                repo.index.commit(f"Added submodule {submodule_name}")
+                submodule = repo.create_submodule(submodule_name, submodule_path, url=sub_repo.git_dir)
+                repo.index.commit("Added submodule")
 
                 with submodule.config_writer() as writer:
                     key_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))

From 7de1556d3895c718f0f0772530ff7cde5457d9d8 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 8 Aug 2024 16:54:37 -0400
Subject: [PATCH 863/994] Filter out non-bug exceptions using a pre-defined
 exception list.

This reduces false positive test failures by identifying and
gracefully handling exceptions that are explicitly raised by GitPython,
thus reducing the false-positive fuzzing test failure rate.
---
 fuzzing/fuzz-targets/fuzz_submodule.py        | 56 +++++++++++++++----
 fuzzing/oss-fuzz-scripts/build.sh             |  2 +-
 .../container-environment-bootstrap.sh        | 11 ++++
 3 files changed, 56 insertions(+), 13 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 9f5828d8d..05c543bf8 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -1,16 +1,51 @@
+# ruff: noqa: E402
 import atheris
 import sys
 import os
+import traceback
 import tempfile
 from configparser import ParsingError
-from utils import is_expected_exception_message, get_max_filename_length
+from utils import get_max_filename_length
+import re
+
+bundle_dir = os.path.dirname(os.path.abspath(__file__))
 
 if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):  # pragma: no cover
-    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
-    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+    bundled_git_binary_path = os.path.join(bundle_dir, "git")
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path
 
 from git import Repo, GitCommandError, InvalidGitRepositoryError
 
+
+def load_exception_list(file_path):
+    """Load and parse the exception list from a file."""
+    try:
+        with open(file_path, "r") as file:
+            lines = file.readlines()
+        exception_list = set()
+        for line in lines:
+            match = re.match(r"(.+):(\d+):", line)
+            if match:
+                file_path = match.group(1).strip()
+                line_number = int(match.group(2).strip())
+                exception_list.add((file_path, line_number))
+        return exception_list
+    except FileNotFoundError:
+        print("File not found: %s", file_path)
+        return set()
+    except Exception as e:
+        print("Error loading exception list: %s", e)
+        return set()
+
+
+def check_exception_against_list(exception_list, exc_traceback):
+    """Check if the exception traceback matches any entry in the exception list."""
+    for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
+        if (filename, lineno) in exception_list:
+            return True
+    return False
+
+
 if not sys.warnoptions:  # pragma: no cover
     # The warnings filter below can be overridden by passing the -W option
     # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
@@ -89,17 +124,14 @@ def TestOneInput(data):
             BrokenPipeError,
         ):
             return -1
-        except ValueError as e:
-            expected_messages = [
-                "SHA is empty",
-                "Reference at",
-                "embedded null byte",
-                "This submodule instance does not exist anymore",
-                "cmd stdin was empty",
-            ]
-            if is_expected_exception_message(e, expected_messages):
+        except Exception as e:
+            exc_traceback = e.__traceback__
+            exception_list = load_exception_list(os.path.join(bundle_dir, "explicit-exceptions-list.txt"))
+            if check_exception_against_list(exception_list, exc_traceback):
+                print("Exception matches an entry in the exception list.")
                 return -1
             else:
+                print("Exception does not match any entry in the exception list.")
                 raise e
 
 
diff --git a/fuzzing/oss-fuzz-scripts/build.sh b/fuzzing/oss-fuzz-scripts/build.sh
index e0b3a50ab..c156e872d 100644
--- a/fuzzing/oss-fuzz-scripts/build.sh
+++ b/fuzzing/oss-fuzz-scripts/build.sh
@@ -15,5 +15,5 @@ find "$SRC" -maxdepth 1 \
 
 # Build fuzzers in $OUT.
 find "$SRC/gitpython/fuzzing" -name 'fuzz_*.py' -print0 | while IFS= read -r -d '' fuzz_harness; do
-  compile_python_fuzzer "$fuzz_harness" --add-binary="$(command -v git):."
+  compile_python_fuzzer "$fuzz_harness" --add-binary="$(command -v git):." --add-data="$SRC/explicit-exceptions-list.txt:."
 done
diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index bbdcf5357..af1ddf014 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -91,6 +91,17 @@ create_seed_corpora_zips "$WORK/qa-assets/gitpython/corpora"
 
 prepare_dictionaries_for_fuzz_targets "$WORK/qa-assets/gitpython/dictionaries" "$SRC/gitpython/fuzzing"
 
+pushd "$SRC/gitpython/"
+# Search for 'raise' and 'assert' statements in Python files within GitPython's 'git/' directory and its submodules,
+# remove trailing colons, and save to 'explicit-exceptions-list.txt'. This file can then be used by fuzz harnesses to
+# check exception tracebacks:
+# If an exception found by the fuzzer originated in a file + line number in explicit-exceptions-list.txt, then it is not a bug.
+
+git grep -n --recurse-submodules -e '\braise\b' -e '\bassert\b' -- "git/**/*.py" > "$SRC/explicit-exceptions-list.txt"
+
+popd
+
+
 # The OSS-Fuzz base image has outdated dependencies by default so we upgrade them below.
 python3 -m pip install --upgrade pip
 # Upgrade to the latest versions known to work at the time the below changes were introduced:

From 799b9cae745f50f2c0c590e8b3e19bfea199c463 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 8 Aug 2024 18:58:28 -0400
Subject: [PATCH 864/994] Improve `check_exception_against_list` matching logic
 using regex

Changes:
   - `match_exception_with_traceback` uses regular expressions for more
     flexible matching of file paths and line numbers. This allows for
     partial matches and more complex patterns.

   - Improve `check_exception_against_list` by delegating to
     `match_exception_with_traceback` for checking tracebacks against
     exception list entries.

   - `load_exception_list`: Remains largely unchanged, as it correctly
     parses the file and line number from each exception entry. However,
     we ensure the set consists of regex patterns to match against
     tracebacks.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 05c543bf8..37f069079 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -31,21 +31,27 @@ def load_exception_list(file_path):
                 exception_list.add((file_path, line_number))
         return exception_list
     except FileNotFoundError:
-        print("File not found: %s", file_path)
+        print(f"File not found: {file_path}")
         return set()
     except Exception as e:
-        print("Error loading exception list: %s", e)
+        print(f"Error loading exception list: {e}")
         return set()
 
 
-def check_exception_against_list(exception_list, exc_traceback):
-    """Check if the exception traceback matches any entry in the exception list."""
+def match_exception_with_traceback(exception_list, exc_traceback):
+    """Match exception traceback with the entries in the exception list."""
     for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
-        if (filename, lineno) in exception_list:
-            return True
+        for file_pattern, line_pattern in exception_list:
+            if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)):
+                return True
     return False
 
 
+def check_exception_against_list(exception_list, exc_traceback):
+    """Check if the exception traceback matches any entry in the exception list."""
+    return match_exception_with_traceback(exception_list, exc_traceback)
+
+
 if not sys.warnoptions:  # pragma: no cover
     # The warnings filter below can be overridden by passing the -W option
     # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
@@ -128,10 +134,8 @@ def TestOneInput(data):
             exc_traceback = e.__traceback__
             exception_list = load_exception_list(os.path.join(bundle_dir, "explicit-exceptions-list.txt"))
             if check_exception_against_list(exception_list, exc_traceback):
-                print("Exception matches an entry in the exception list.")
                 return -1
             else:
-                print("Exception does not match any entry in the exception list.")
                 raise e
 
 

From 2e9c23995b70372a18edc4d0b143b6b522d3fb39 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 8 Aug 2024 19:38:06 -0400
Subject: [PATCH 865/994] Extract environment setup and exception checking
 boilerplate logic

Changes:

   - Simplify exception handling in test harnesses via `handle_exception(e)`
     in the `except Exception as e:` block.

   - `setup_git_environment` is a step towards centralizing environment
     variable and logging configuration set up consistently across
     different fuzzing scripts. **Only applying it to a single test for
     now is an intentional choice in case it fails to work in the
     ClusterFuzz environment!** If it proves successful, a follow-up
     change set will be welcome.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 70 +++------------------
 fuzzing/fuzz-targets/utils.py          | 87 +++++++++++++++++++++++++-
 2 files changed, 95 insertions(+), 62 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 37f069079..634572bf2 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -1,67 +1,17 @@
-# ruff: noqa: E402
 import atheris
 import sys
 import os
-import traceback
 import tempfile
 from configparser import ParsingError
-from utils import get_max_filename_length
-import re
-
-bundle_dir = os.path.dirname(os.path.abspath(__file__))
-
-if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):  # pragma: no cover
-    bundled_git_binary_path = os.path.join(bundle_dir, "git")
-    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path
-
 from git import Repo, GitCommandError, InvalidGitRepositoryError
+from utils import (
+    setup_git_environment,
+    handle_exception,
+    get_max_filename_length,
+)
 
-
-def load_exception_list(file_path):
-    """Load and parse the exception list from a file."""
-    try:
-        with open(file_path, "r") as file:
-            lines = file.readlines()
-        exception_list = set()
-        for line in lines:
-            match = re.match(r"(.+):(\d+):", line)
-            if match:
-                file_path = match.group(1).strip()
-                line_number = int(match.group(2).strip())
-                exception_list.add((file_path, line_number))
-        return exception_list
-    except FileNotFoundError:
-        print(f"File not found: {file_path}")
-        return set()
-    except Exception as e:
-        print(f"Error loading exception list: {e}")
-        return set()
-
-
-def match_exception_with_traceback(exception_list, exc_traceback):
-    """Match exception traceback with the entries in the exception list."""
-    for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
-        for file_pattern, line_pattern in exception_list:
-            if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)):
-                return True
-    return False
-
-
-def check_exception_against_list(exception_list, exc_traceback):
-    """Check if the exception traceback matches any entry in the exception list."""
-    return match_exception_with_traceback(exception_list, exc_traceback)
-
-
-if not sys.warnoptions:  # pragma: no cover
-    # The warnings filter below can be overridden by passing the -W option
-    # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
-    import warnings
-    import logging
-
-    # Fuzzing data causes some modules to generate a large number of warnings
-    # which are not usually interesting and make the test output hard to read, so we ignore them.
-    warnings.simplefilter("ignore")
-    logging.getLogger().setLevel(logging.ERROR)
+# Setup the git environment
+setup_git_environment()
 
 
 def TestOneInput(data):
@@ -131,12 +81,10 @@ def TestOneInput(data):
         ):
             return -1
         except Exception as e:
-            exc_traceback = e.__traceback__
-            exception_list = load_exception_list(os.path.join(bundle_dir, "explicit-exceptions-list.txt"))
-            if check_exception_against_list(exception_list, exc_traceback):
+            if isinstance(e, ValueError) and "embedded null byte" in str(e):
                 return -1
             else:
-                raise e
+                return handle_exception(e)
 
 
 def main():
diff --git a/fuzzing/fuzz-targets/utils.py b/fuzzing/fuzz-targets/utils.py
index f522d2959..97e6eab98 100644
--- a/fuzzing/fuzz-targets/utils.py
+++ b/fuzzing/fuzz-targets/utils.py
@@ -1,6 +1,9 @@
 import atheris  # pragma: no cover
 import os  # pragma: no cover
-from typing import List  # pragma: no cover
+import re  # pragma: no cover
+import traceback  # pragma: no cover
+import sys  # pragma: no cover
+from typing import Set, Tuple, List  # pragma: no cover
 
 
 @atheris.instrument_func
@@ -35,3 +38,85 @@ def get_max_filename_length(path: str) -> int:  # pragma: no cover
         int: The maximum filename length.
     """
     return os.pathconf(path, "PC_NAME_MAX")
+
+
+@atheris.instrument_func
+def read_lines_from_file(file_path: str) -> list:
+    """Read lines from a file and return them as a list."""
+    try:
+        with open(file_path, "r") as f:
+            return [line.strip() for line in f if line.strip()]
+    except FileNotFoundError:
+        print(f"File not found: {file_path}")
+        return []
+    except IOError as e:
+        print(f"Error reading file {file_path}: {e}")
+        return []
+
+
+@atheris.instrument_func
+def load_exception_list(file_path: str = "explicit-exceptions-list.txt") -> Set[Tuple[str, str]]:
+    """Load and parse the exception list from a default or specified file."""
+    try:
+        bundle_dir = os.path.dirname(os.path.abspath(__file__))
+        full_path = os.path.join(bundle_dir, file_path)
+        lines = read_lines_from_file(full_path)
+        exception_list: Set[Tuple[str, str]] = set()
+        for line in lines:
+            match = re.match(r"(.+):(\d+):", line)
+            if match:
+                file_path: str = match.group(1).strip()
+                line_number: str = str(match.group(2).strip())
+                exception_list.add((file_path, line_number))
+        return exception_list
+    except Exception as e:
+        print(f"Error loading exception list: {e}")
+        return set()
+
+
+@atheris.instrument_func
+def match_exception_with_traceback(exception_list: Set[Tuple[str, str]], exc_traceback) -> bool:
+    """Match exception traceback with the entries in the exception list."""
+    for filename, lineno, _, _ in traceback.extract_tb(exc_traceback):
+        for file_pattern, line_pattern in exception_list:
+            # Ensure filename and line_number are strings for regex matching
+            if re.fullmatch(file_pattern, filename) and re.fullmatch(line_pattern, str(lineno)):
+                return True
+    return False
+
+
+@atheris.instrument_func
+def check_exception_against_list(exc_traceback, exception_file: str = "explicit-exceptions-list.txt") -> bool:
+    """Check if the exception traceback matches any entry in the exception list."""
+    exception_list = load_exception_list(exception_file)
+    return match_exception_with_traceback(exception_list, exc_traceback)
+
+
+@atheris.instrument_func
+def handle_exception(e: Exception) -> int:
+    """Encapsulate exception handling logic for reusability."""
+    exc_traceback = e.__traceback__
+    if check_exception_against_list(exc_traceback):
+        return -1
+    else:
+        raise e
+
+
+@atheris.instrument_func
+def setup_git_environment() -> None:
+    """Set up the environment variables for Git."""
+    bundle_dir = os.path.dirname(os.path.abspath(__file__))
+    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):  # pragma: no cover
+        bundled_git_binary_path = os.path.join(bundle_dir, "git")
+        os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = bundled_git_binary_path
+
+    if not sys.warnoptions:  # pragma: no cover
+        # The warnings filter below can be overridden by passing the -W option
+        # to the Python interpreter command line or setting the `PYTHONWARNINGS` environment variable.
+        import warnings
+        import logging
+
+        # Fuzzing data causes some modules to generate a large number of warnings
+        # which are not usually interesting and make the test output hard to read, so we ignore them.
+        warnings.simplefilter("ignore")
+        logging.getLogger().setLevel(logging.ERROR)

From 27de8676c64b549038b4fdd994a20f1ce996ad5e Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Thu, 8 Aug 2024 20:35:13 -0400
Subject: [PATCH 866/994] Fix buggy `git grep` pathspec args

To ensure that all necessary files are included in the
explicit-exceptions-list.txt file and unwanted files and directories are
not.
---
 .../container-environment-bootstrap.sh                 | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
index af1ddf014..924a3cbf3 100755
--- a/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
+++ b/fuzzing/oss-fuzz-scripts/container-environment-bootstrap.sh
@@ -92,12 +92,12 @@ create_seed_corpora_zips "$WORK/qa-assets/gitpython/corpora"
 prepare_dictionaries_for_fuzz_targets "$WORK/qa-assets/gitpython/dictionaries" "$SRC/gitpython/fuzzing"
 
 pushd "$SRC/gitpython/"
-# Search for 'raise' and 'assert' statements in Python files within GitPython's 'git/' directory and its submodules,
-# remove trailing colons, and save to 'explicit-exceptions-list.txt'. This file can then be used by fuzz harnesses to
-# check exception tracebacks:
-# If an exception found by the fuzzer originated in a file + line number in explicit-exceptions-list.txt, then it is not a bug.
+# Search for 'raise' and 'assert' statements in Python files within GitPython's source code and submodules, saving the
+# matched file path, line number, and line content to a file named 'explicit-exceptions-list.txt'.
+# This file can then be used by fuzz harnesses to check exception tracebacks and filter out explicitly raised or otherwise
+# anticipated exceptions to reduce false positive test failures.
 
-git grep -n --recurse-submodules -e '\braise\b' -e '\bassert\b' -- "git/**/*.py" > "$SRC/explicit-exceptions-list.txt"
+git grep -n --recurse-submodules -e '\braise\b' -e '\bassert\b' -- '*.py' -- ':!setup.py' -- ':!test/**' -- ':!fuzzing/**' > "$SRC/explicit-exceptions-list.txt"
 
 popd
 

From 2ed33345667706c5755708e88c989ede06f2414f Mon Sep 17 00:00:00 2001
From: David Lakin <david.js.lakin@gmail.com>
Date: Fri, 9 Aug 2024 00:06:44 -0400
Subject: [PATCH 867/994] Fix order of environment setup and git module import

The environment setup must happen before the `git` module is imported,
otherwise GitPython won't be able to find the Git executable and raise
an exception that causes the ClusterFuzz fuzzer runs to fail.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 2 +-
 pyproject.toml                         | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 634572bf2..997133b70 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -3,7 +3,6 @@
 import os
 import tempfile
 from configparser import ParsingError
-from git import Repo, GitCommandError, InvalidGitRepositoryError
 from utils import (
     setup_git_environment,
     handle_exception,
@@ -12,6 +11,7 @@
 
 # Setup the git environment
 setup_git_environment()
+from git import Repo, GitCommandError, InvalidGitRepositoryError
 
 
 def TestOneInput(data):
diff --git a/pyproject.toml b/pyproject.toml
index 603e2597c..6cf4b3f5d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,6 +78,10 @@ lint.unfixable = [
 "test/**" = [
     "B018",  # useless-expression
 ]
+"fuzzing/fuzz-targets/**" = [
+  "E402",  # environment setup must happen before the `git` module is imported, thus cannot happen at top of file
+]
+
 
 [tool.codespell]
 ignore-words-list="gud,doesnt"

From 096851b61fa99df233176b090146efb52e524f48 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Fri, 9 Aug 2024 11:01:34 -0400
Subject: [PATCH 868/994] Gracefully handle `PermissionError` exceptions that
 crash fuzzer

Fuzzing inputs sometimes produce directory paths that are protected
inside the fuzzer execution environment. This is not an issue in
GitPython's code, so it should not crash the fuzzer.

Fixes OSS-Fuzz Issue 69456:
https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69870
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index 997133b70..c2bf1e4fe 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -78,6 +78,7 @@ def TestOneInput(data):
             IsADirectoryError,
             NotADirectoryError,
             BrokenPipeError,
+            PermissionError,
         ):
             return -1
         except Exception as e:

From 7126ce16a03e0aea5ef4d031c62596992a6d7cb5 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Tue, 13 Aug 2024 01:09:37 -0400
Subject: [PATCH 869/994] Fuzzing: Gracefully Handle Uninteresting Error to Fix
 OSS-Fuzz Issue 71095

Fuzzing data can generate filenames that trigger:
> OSError: [Errno 36] File name too long

The changes here add handling for these exceptions because they di not
indicate a bug and should not crash the fuzzer.
a
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index c2bf1e4fe..d22b0aa5b 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -84,6 +84,8 @@ def TestOneInput(data):
         except Exception as e:
             if isinstance(e, ValueError) and "embedded null byte" in str(e):
                 return -1
+            elif isinstance(e, OSError) and "File name too long" in str(e):
+                return -1
             else:
                 return handle_exception(e)
 

From d1582d181bfeb5138d9cae306b40dfa2fe87fe39 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 18:08:22 -0400
Subject: [PATCH 870/994] Update versions of pre-commit hooks

---
 .pre-commit-config.yaml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 692c7fa2a..7d93876ed 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 - repo: https://github.com/codespell-project/codespell
-  rev: v2.2.4
+  rev: v2.3.0
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
@@ -8,7 +8,7 @@ repos:
     exclude: "test/fixtures/"
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.4.3
+  rev: v0.6.0
   hooks:
   - id: ruff
     args: ["--fix"]
@@ -17,14 +17,14 @@ repos:
     exclude: ^git/ext/
 
 - repo: https://github.com/shellcheck-py/shellcheck-py
-  rev: v0.9.0.6
+  rev: v0.10.0.1
   hooks:
   - id: shellcheck
     args: [--color]
     exclude: ^test/fixtures/polyglot$|^git/ext/
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.5.0
+  rev: v4.6.0
   hooks:
   - id: end-of-file-fixer
     exclude: test/fixtures/
@@ -33,6 +33,6 @@ repos:
   - id: check-merge-conflict
 
 - repo: https://github.com/abravalheri/validate-pyproject
-  rev: v0.16
+  rev: v0.19
   hooks:
     - id: validate-pyproject

From 016fa44a64ac244de2335b00338af67e3f8585ee Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 18:09:06 -0400
Subject: [PATCH 871/994] Have codespell ignore words that cause new false
 positives

---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 6cf4b3f5d..090972eed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,6 +84,6 @@ lint.unfixable = [
 
 
 [tool.codespell]
-ignore-words-list="gud,doesnt"
+ignore-words-list="afile,assertIn,doesnt,gud,uptodate"
 #count = true
 quiet-level = 3

From c82bb65fd263603b374b925f61483efc47c2a264 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 18:13:28 -0400
Subject: [PATCH 872/994] Fix a spelling error that codespell didn't catch

---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7d93876ed..90b899f8e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ repos:
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
-    # args: ["--write-changes"]  # consider enabling for auto-fif
+    # args: ["--write-changes"]  # consider enabling for auto-fix
     exclude: "test/fixtures/"
 
 - repo: https://github.com/astral-sh/ruff-pre-commit

From 9556f63a965877db19002849d7bfeec71e84a2c7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 18:19:03 -0400
Subject: [PATCH 873/994] Drop suggestion to auto-fix spelling (many false
 positives)

---
 .pre-commit-config.yaml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 90b899f8e..c47d9a2c7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,6 @@ repos:
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
-    # args: ["--write-changes"]  # consider enabling for auto-fix
     exclude: "test/fixtures/"
 
 - repo: https://github.com/astral-sh/ruff-pre-commit

From 7a138eea78fd922b21f6049d273aaeca5f02bfb0 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 18:55:48 -0400
Subject: [PATCH 874/994] Fix small inconsistencies in test/fixtures/
 exclusions

---
 .pre-commit-config.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c47d9a2c7..f5635b2a0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ repos:
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
-    exclude: "test/fixtures/"
+    exclude: ^test/fixtures/
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
   rev: v0.6.0
@@ -26,7 +26,7 @@ repos:
   rev: v4.6.0
   hooks:
   - id: end-of-file-fixer
-    exclude: test/fixtures/
+    exclude: ^test/fixtures/
   - id: check-toml
   - id: check-yaml
   - id: check-merge-conflict

From 53ec790e0dbc1ec9e4451394edb5c572c807b817 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 19:01:02 -0400
Subject: [PATCH 875/994] Fix inconsistent indentation

---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f5635b2a0..0cbf5aa73 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -34,4 +34,4 @@ repos:
 - repo: https://github.com/abravalheri/validate-pyproject
   rev: v0.19
   hooks:
-    - id: validate-pyproject
+  - id: validate-pyproject

From bdfa280f6dd412464419dd133ad02781cd27a312 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 19:02:14 -0400
Subject: [PATCH 876/994] Temporarily let end-of-file-fixer break LICENSE-BSD
 symlink

On Windows, when `core.symlinks` is `false` or unset (since it
defaults to `false` on Windows), Git checks out symbolic links as
regular files whose contents are symlinks' target paths. Modifying
those regular files and committing the changes alters the symlink
target in the repository, and when they are checked out as actual
symlinks, the targets are different.

But the `end-of-file-fixer` pre-commit hook automatically adds
newlines to the end of regular files that lack them. It doesn't do
this on actual symlinks, but it does do it to regular files that
stand in for symlinks. This causes it to carry a risk of breaking
symlinks if it is run on Windows and the changes committed, and it
is easy to miss that this will happen because `git diff` output
shows it the same way as other additions of absent newlines.

This deliberately commits the change that end-of-file-fixer makes
to the `LICENSE-BSD` symlink, in order to allow a mitigation beyond
just excluding that symlink (or replacing it with a regular file)
to be tested. This change must be undone, of course.
---
 fuzzing/LICENSE-BSD | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/LICENSE-BSD b/fuzzing/LICENSE-BSD
index ea5b60640..4f88f81bf 120000
--- a/fuzzing/LICENSE-BSD
+++ b/fuzzing/LICENSE-BSD
@@ -1 +1 @@
-../LICENSE
\ No newline at end of file
+../LICENSE

From 965ea8bebcd768f6cadbc6cae6b7fe65868f1fb6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 20:17:24 -0400
Subject: [PATCH 877/994] Enable check-symlinks pre-commit hook

Rationale:

- Small but likely benefit in general, since there are no currently
  foreseen intentional use cases of committing of broken/dangling
  symlinks in this project. So such symlinks that arise are likely
  unintentional.

- If the end-of-file-fixer hook has run on a Windows system where
  `core.symlinks` has *not* been set to `true`, and symlinks' paths
  have not been excluded, then a newline character is added to the
  end of the path held in the regular file Git checks out to stand
  in for the symlink. Because it is not actually a symlink, this
  will not detect the problem at that time (regardless of the order
  in which this and that hook run relative to each other). But when
  it is then run on CI on a system where symlinks are checked out,
  it will detect the problem.
---
 .pre-commit-config.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0cbf5aa73..3f6892687 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,6 +27,7 @@ repos:
   hooks:
   - id: end-of-file-fixer
     exclude: ^test/fixtures/
+  - id: check-symlinks
   - id: check-toml
   - id: check-yaml
   - id: check-merge-conflict

From e9782487b8119147aa0c456c708f61ca7e3139e1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 20:24:36 -0400
Subject: [PATCH 878/994] Revert "Temporarily let end-of-file-fixer break
 LICENSE-BSD symlink"

This reverts commit bdfa280f6dd412464419dd133ad02781cd27a312.
---
 fuzzing/LICENSE-BSD | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fuzzing/LICENSE-BSD b/fuzzing/LICENSE-BSD
index 4f88f81bf..ea5b60640 120000
--- a/fuzzing/LICENSE-BSD
+++ b/fuzzing/LICENSE-BSD
@@ -1 +1 @@
-../LICENSE
+../LICENSE
\ No newline at end of file

From cae0d8743a31fb0eda3c224a45c14de8cabd0d90 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 15 Aug 2024 20:28:46 -0400
Subject: [PATCH 879/994] Don't fix end-of-file in files named like licenses

The unanchored `LICENSE` and `COPYING` alternatives match the
pattern anywhere, and therefore exclude the currently used path
`fuzzing/LICENSE-BSD`.

License files are more likely than other files in this project to
be introduced as symlinks, and less likely to be noticed
immediately if they break. Symlinks can be checked out as regular
files when `core.symlinks` is set to `false`, which is rare outside
of Windows but is the default behavior when unset on Windows.

This exclusion fixes the current problem that end-of-file-fixer
breaks those links by adding a newline character to the end (the
symlinks are checked out broken if that is committed). It also
guards against most future cases involving licenses, though
possibly not all, and not other unrelated cases where symlinks may
be used for other purposes.

Although the pre-commit-hooks repository also provides a
destroyed-symlinks hook that detects the situation of a symlink
that has been replaced by a regular file, this does not add that
hook, because this situation is not inherently a problem. The code
here does not require symlinks to be checked out to work, and
adding that would break significant uses of the repository on
Windows.

Note that this leaves the situation where a license file may be a
symlink to another license file and may thus be checked out as a
regular file containing that file's path. However, it is easy to
understand that situation and manually follow the path. That
differs from the scenario where a symlink is created but broken,
because attempting to open it gives an error, and the error message
is often non-obvious, reporting that a file is not found but giving
the name of the symlink rather than its target.
---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3f6892687..424cc5f37 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
   rev: v4.6.0
   hooks:
   - id: end-of-file-fixer
-    exclude: ^test/fixtures/
+    exclude: ^test/fixtures/|COPYING|LICENSE
   - id: check-symlinks
   - id: check-toml
   - id: check-yaml

From 16fc99fee45412c3dae44bdd7f59d921a11c00b3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 17 Aug 2024 12:51:13 -0400
Subject: [PATCH 880/994] Upgrade sphinx to ~7.1.2

The old pinned version and its explicitly constrained dependencies
are retained for now for Python 3.7 so that documentation can be
built even with 3.7. (This could maybe be removed soon as a
preliminary step toward evenutally dropping 3.7 support.)

For Python 3.8 and higher, version 7.1.2 is used, allowing later
patch versions but constrained to remain 7.1.*. This is so the same
versions are likely to be selected on all Python version from 3.8
and higher, to minimize small differences in generated documentation
that different versions could give, and also to simplify debugging.

The reason to upgrade Sphinx now is to suppport Python 3.13, which
shall be (and, in the pre-releases available, is) incompatible with
versions of Sphinx below 6.2. This is because those earlier Sphinx
versions use the deprecated `imghdr` module, which 3.13 removes:

- https://docs.python.org/3.13/whatsnew/3.13.html#whatsnew313-pep594
- https://github.com/python/cpython/issues/104818

On old versions of Sphinx, that gives the error:

    Extension error:
    Could not import extension sphinx.builders.epub3 (exception: No module named 'imghdr')

Using Sphinx 6.2 is sufficient to avoid this, but there do not seem
to be any disadvantages for GitPython to remain below 7.1.2.

The reason we did not upgrade Sphinx before is that, last time we
considered doing so, we ran into a problem of new warnings (that we
treat as errors). This is detailed in the "Can we upgrade Sphinx?"
section of #1802, especially the "What Sphinx 5 is talking about"
subsection. The problem is warnings about `Actor` when it comes
in through type annotations:

    WARNING: more than one target found for cross-reference 'Actor': git.objects.util.Actor, git.util.Actor

So this includes other changes to fix that problem as well. The
solution used here is to import `Actor` even when `TYPE_CHECKING`
is `false`, and write it unquoted in annotations, as `Actor` rather
than `"Actor"`. This allows Sphinx to discern where it should
consider it to be located, for the purpose of linking to its
documentation.

This does not incur overhead, because:

- The affected modules already have imports from `git.util`, so also
  importing `Actor` from `git.util` does not cause any modules to
  be imported that were not imported otherwise, nor even to be
  imported at a different time.

- Even if that that had not been the case, most modules in GitPython
  including `git.util` have members imported them into the top-level
  `git` module in `git.__init__` when `git` is imported (and thus
  when any Python submodule of `git` is imported).

The only disadvantage is the presence of the additional name in
those modules at runtime, which a user might inadvertently use and
thus write brittle code that could break if it is later removed.
But:

- The affected modules define `__all__` and do not include
  `"Actor"` in `__all__`, so it is non-public.

- There are many places in GitPython (and most Python libraries)
  where the onus is already on the author of code that uses the
  library to avoid doing this.

The reasons for this approach, rather than any of several others,
were:

1. I did not write out the annotations as `git.util.Actor` to
   resolve the ambiguity because annotations should, and for some
   uses must, also be interpretable as expressions. But while
   `from git.util import Actor` works and makes `Actor` available,
   `git.util.Actor` cannot be used as an expression even after
   `import git.util`. This is because, even after such an import,
   `git.util` actually refers to `git.index.util`. This is as
   detailed in the warnings issued when it is accessed, originally
   from an overly broad `*` import but retained because removing it
   could be a breaking change. See `git/__init__.py` for details.

2. The reason I did not write out the annotations as
   `git.objects.util.Actor` to resolve the ambiguity is that not
   all occurrences where Sphinx needed to be told which module to
   document it as being from were within the `git.objects` Python
   submodule. Two of the warnings were in `git/objects/tag.py`,
   where annotating it that way would not be confusing. But the
   other two were in `git/index/base.py`.

3. Although removing `Actor` from `git.objects.util.__all__` would
   resolve the ambiguity, this should not be done, because:

   - This would be a breaking change.

   - It seems to be there deliberately, since `git.objects.util`
     contains other members that relate to it directly.

4. The reasons I did not take this opportunity to move the contents
   of `git/util.py` to a new file in that directory and make
   `git/util.py` re-export the contents, even though this would
   allow a solution analogous to (1) but for the new module to be
   used, while also simplifying importing elsewhere, were:

   - That seems like a change that should be done separately, based
     either on the primary benefit to users or on a greater need
     for it.

   - If and when that is done, it may make sense to change the
     interface as well. For example, `git/util.py` has a number of
     members that it makes available for use throughout GitPython
     but that are deliberately omitted from `__all__` and are meant
     as non-public outside the project. One approach would be to make
     a module with a leading `_` for these "internal" members, and
     another public ones with everything else. But that also cannot
     be decided based on the considerations that motivate the
     changes here.

   - The treatment of `HIDE_WINDOWS_KNOWN_ERRORS`, which is public
     in `git/util.py` and which currently *does* have an effect,
     will need to be considered. Although it cannot be re-bound by
     assigning to `git.util.HIDE_WINDOWS_KNOWN_ERRORS` because the
     `git.util` subexpression would evaluate to `git.index.util`,
     there may be code that re-binds it in another way, such as by
     accessing the module through `sys.modules`. Unlike functions
     and classes that should not be monkey-patched from code
     outside GitPython's test suite anyway, this attribute may
     reasonably be assigned to, so it matters what module it is
     actually in, unless the action of assigning to it elsewhere
     is customized dynamically to carry over to the "real" place.

5. An alternative solution that may be reasonable in the near
   future is to modify `reference.rst` so duplicate documentation
   is no longer emitted for functions and classes that are defined
   in one place but imported and "re-exported" elsewhere. I suspect
   this may solve the problem, allowing the `Actor` imports to go
   back under `if TYPE_CHECKING:` and to annotate with `"Actor"`
   again while still running `make -C doc html` with no warnings.

   However, this would entail design decisions about how to still
   document those members. They should probably have links to where
   they are fully documented. So an entry for `Actor` in the
   section of `reference.rst` for `git.objects.util` would still
   exist, but be very short and refer to the full autodoc item for
   `Actor` the section for `git.util`. Since a `:class:` reference
   to `git.objects.util.Actor` should still go to the stub that
   links to `git.util.Actor`, it is not obvious that solving the
   duplication in documentation generated for `reference.rst` ought
   to be done in a way that would address the ambiguity Sphinx
   warns about, even if it *can* be done in such a way.

   Therefore, that should also be a separate consideration and, if
   warranted, a separate change.
---
 doc/requirements.txt | 13 +++++++------
 git/index/base.py    |  6 +++---
 git/objects/tag.py   |  5 ++---
 3 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/doc/requirements.txt b/doc/requirements.txt
index 7769af5ae..a90a7a496 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,8 +1,9 @@
-sphinx == 4.3.2
+sphinx >= 7.1.2, < 7.2 ; python_version >= "3.8"
+sphinx == 4.3.2 ; python_version < "3.8"
+sphinxcontrib-applehelp >= 1.0.2, <= 1.0.4 ; python_version < "3.8"
+sphinxcontrib-devhelp == 1.0.2 ; python_version < "3.8"
+sphinxcontrib-htmlhelp >= 2.0.0, <= 2.0.1 ; python_version < "3.8"
+sphinxcontrib-qthelp == 1.0.3 ; python_version < "3.8"
+sphinxcontrib-serializinghtml == 1.1.5 ; python_version < "3.8"
 sphinx_rtd_theme
-sphinxcontrib-applehelp >= 1.0.2, <= 1.0.4
-sphinxcontrib-devhelp == 1.0.2
-sphinxcontrib-htmlhelp >= 2.0.0, <= 2.0.1
-sphinxcontrib-qthelp == 1.0.3
-sphinxcontrib-serializinghtml == 1.1.5
 sphinx-autodoc-typehints
diff --git a/git/index/base.py b/git/index/base.py
index a317e71c0..47925ad1c 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -28,6 +28,7 @@
 from git.objects import Blob, Commit, Object, Submodule, Tree
 from git.objects.util import Serializable
 from git.util import (
+    Actor,
     LazyMixin,
     LockedFD,
     join_path_native,
@@ -76,7 +77,6 @@
 
     from git.refs.reference import Reference
     from git.repo import Repo
-    from git.util import Actor
 
 
 Treeish = Union[Tree, Commit, str, bytes]
@@ -1117,8 +1117,8 @@ def commit(
         message: str,
         parent_commits: Union[List[Commit], None] = None,
         head: bool = True,
-        author: Union[None, "Actor"] = None,
-        committer: Union[None, "Actor"] = None,
+        author: Union[None, Actor] = None,
+        committer: Union[None, Actor] = None,
         author_date: Union[datetime.datetime, str, None] = None,
         commit_date: Union[datetime.datetime, str, None] = None,
         skip_hooks: bool = False,
diff --git a/git/objects/tag.py b/git/objects/tag.py
index a3bb0b882..88671d316 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -14,7 +14,7 @@
 import sys
 
 from git.compat import defenc
-from git.util import hex_to_bin
+from git.util import Actor, hex_to_bin
 
 from . import base
 from .util import get_object_type_by_name, parse_actor_and_date
@@ -30,7 +30,6 @@
 
 if TYPE_CHECKING:
     from git.repo import Repo
-    from git.util import Actor
 
     from .blob import Blob
     from .commit import Commit
@@ -64,7 +63,7 @@ def __init__(
         binsha: bytes,
         object: Union[None, base.Object] = None,
         tag: Union[None, str] = None,
-        tagger: Union[None, "Actor"] = None,
+        tagger: Union[None, Actor] = None,
         tagged_date: Union[int, None] = None,
         tagger_tz_offset: Union[int, None] = None,
         message: Union[str, None] = None,

From 44f7a738b85f75d86516a3ee1f128f4098fbcda6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 18 Aug 2024 14:02:40 -0400
Subject: [PATCH 881/994] Don't support building documentation on Python 3.7

This removes the specially cased alternative lower versions of
`sphinx` and its dependencies that, since #1954, were only for
Python 3.7. As discussed in comments there, this simplifies the
documentation dependencies and avoids a situation where the version
of Python used to build the documentation has a noticeable effect
on the generated result.

This also conditions running the "Documentation" step in the main
CI test workflow (`pythonpackage.yml`) on the Python version not
being 3.7 (otherwise the job would always fail).

The only change this makes to the support status of GitPython on
Python 3.7 is to no longer support building documentation on 3.7.
GitPython can still be installed and used on 3.7 (though usually
this would not be a good idea, outside of testing, since Python 3.7
itself has not been supported by the Python Software Foundation for
quite some time). In addition, the documentation, which can be
built on any version >= 3.8 (including 3.13 starting in #1954) is
no less relevant to usage on Python 3.7 than it was before.
---
 .github/workflows/pythonpackage.yml | 1 +
 doc/requirements.txt                | 8 +-------
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 0f1d17544..292c9fc86 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -99,6 +99,7 @@ jobs:
       continue-on-error: false
 
     - name: Documentation
+      if: matrix.python-version != '3.7'
       run: |
         pip install ".[doc]"
         make -C doc html
diff --git a/doc/requirements.txt b/doc/requirements.txt
index a90a7a496..81140d898 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,9 +1,3 @@
-sphinx >= 7.1.2, < 7.2 ; python_version >= "3.8"
-sphinx == 4.3.2 ; python_version < "3.8"
-sphinxcontrib-applehelp >= 1.0.2, <= 1.0.4 ; python_version < "3.8"
-sphinxcontrib-devhelp == 1.0.2 ; python_version < "3.8"
-sphinxcontrib-htmlhelp >= 2.0.0, <= 2.0.1 ; python_version < "3.8"
-sphinxcontrib-qthelp == 1.0.3 ; python_version < "3.8"
-sphinxcontrib-serializinghtml == 1.1.5 ; python_version < "3.8"
+sphinx >= 7.1.2, < 7.2
 sphinx_rtd_theme
 sphinx-autodoc-typehints

From f2254af5d3fd183ec150740d517bd0f8070fc67d Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Sat, 14 Sep 2024 10:45:53 +0500
Subject: [PATCH 882/994] _to_relative_path to support mixing slashes and
 backslashes

Working on Windows you sometime end up having some paths with backslashes (windows native) and some with slashes - this PR will resolve the issue using gitpython for those kind of cases (see example below). It will also fix the issues if paths contain redundant separators or "..".

```
import git

repo = git.Repo(r"C:\gittest")
repo.index.add(r"C:\gittest\1.txt")
# Traceback (most recent call last):
#   File "c:\second_test.py", line 5, in <module>
#     repo.index.add(r"C:/gittest/2.txt")
#   File "Python311\Lib\site-packages\git\index\base.py", line 879, in add
#     paths, entries = self._preprocess_add_items(items)
#                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "Python311\Lib\site-packages\git\index\base.py", line 672, in _preprocess_add_items
#     paths.append(self._to_relative_path(item))
#                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "Python311\Lib\site-packages\git\index\base.py", line 657, in _to_relative_path
#     raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
# ValueError: Absolute path 'C:/gittest/2.txt' is not in git repository at 'C:\\gittest'

repo.index.add(r"C:/gittest/2.txt")

repo.index.commit("test")
```
---
 git/index/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/index/base.py b/git/index/base.py
index 47925ad1c..7f53e614a 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -653,7 +653,7 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
             return path
         if self.repo.bare:
             raise InvalidGitRepositoryError("require non-bare repository")
-        if not str(path).startswith(str(self.repo.working_tree_dir)):
+        if not osp.normpath(str(path)).startswith(osp.normpath(str(self.repo.working_tree_dir))):
             raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
         return os.path.relpath(path, self.repo.working_tree_dir)
 

From ca06b11efde845080354dac71e9062ea6d63ab84 Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Sat, 14 Sep 2024 16:51:41 +0500
Subject: [PATCH 883/994] test adding a file using non-normalized path

---
 test/test_index.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/test/test_index.py b/test/test_index.py
index 2684cfd81..efd5b83a6 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1181,6 +1181,18 @@ def test_index_add_pathlike(self, rw_repo):
 
         rw_repo.index.add(file)
 
+    @with_rw_repo("HEAD")
+    def test_index_add_non_normalized_path(self, rw_repo):
+        git_dir = Path(rw_repo.git_dir)
+
+        file = git_dir / "file.txt"
+        file.touch()
+        non_normalized_path = file.as_posix()
+        if os.name != "nt":
+            non_normalized_path = non_normalized_path.replace("/", "\\")
+
+        rw_repo.index.add(non_normalized_path)
+
 
 class TestIndexUtils:
     @pytest.mark.parametrize("file_path_type", [str, Path])

From 46740590f7918fd5b789c95db7e41fbda06fb46f Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Sat, 14 Sep 2024 16:52:56 +0500
Subject: [PATCH 884/994] Remove redundant path normalization for
 working_tree_dir

---
 git/index/base.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/index/base.py b/git/index/base.py
index 7f53e614a..39cc9143c 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -653,7 +653,7 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
             return path
         if self.repo.bare:
             raise InvalidGitRepositoryError("require non-bare repository")
-        if not osp.normpath(str(path)).startswith(osp.normpath(str(self.repo.working_tree_dir))):
+        if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
             raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
         return os.path.relpath(path, self.repo.working_tree_dir)
 

From 8327b82a1079f667006f649cb3f1bbdcc8792955 Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Sat, 14 Sep 2024 21:18:18 +0500
Subject: [PATCH 885/994] Fix test failing on unix

---
 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 efd5b83a6..c586a0b5a 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -1189,7 +1189,7 @@ def test_index_add_non_normalized_path(self, rw_repo):
         file.touch()
         non_normalized_path = file.as_posix()
         if os.name != "nt":
-            non_normalized_path = non_normalized_path.replace("/", "\\")
+            non_normalized_path = "/" + non_normalized_path[1:].replace("/", "//")
 
         rw_repo.index.add(non_normalized_path)
 

From 49ca9099dc75d0d686ec6737da36637cbee1c000 Mon Sep 17 00:00:00 2001
From: No big deal <69958306+alex20230721@users.noreply.github.com>
Date: Sat, 5 Oct 2024 17:23:19 +0800
Subject: [PATCH 886/994] Update base.py (#1965)

Improve documentation around opening repositories.

Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
---
 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 346248ddb..db89cdf41 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -179,7 +179,7 @@ def __init__(
         R"""Create a new :class:`Repo` instance.
 
         :param path:
-            The path to either the root git directory or the bare git repo::
+            The path to either the worktree directory or the .git directory itself::
 
                 repo = Repo("/Users/mtrier/Development/git-python")
                 repo = Repo("/Users/mtrier/Development/git-python.git")

From 1bb465122f9673c9834b094c49d815148e84b8eb Mon Sep 17 00:00:00 2001
From: Florent Valette <florent.valette@ledger.fr>
Date: Mon, 14 Oct 2024 21:39:37 +0200
Subject: [PATCH 887/994] git,remote: use universal new lines for fetch/pull
 stderr capture

See https://github.com/gitpython-developers/GitPython/issues/1969

stderr parser call RemoteProgress update on each line received.
With universal_newlines set to False, there is a mixup between
line feed and carriage return.
In the `handle_process_output` thread, this is thus seen as a single
line for the whole output on each steps.
---
 git/remote.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/git/remote.py b/git/remote.py
index 9de3dace4..20e42b412 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -894,7 +894,7 @@ def _get_fetch_info_from_stderr(
             None,
             progress_handler,
             finalizer=None,
-            decode_streams=True,
+            decode_streams=False,
             kill_after_timeout=kill_after_timeout,
         )
 
@@ -1071,7 +1071,7 @@ def fetch(
             Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
 
         proc = self.repo.git.fetch(
-            "--", self, *args, as_process=True, with_stdout=False, universal_newlines=False, v=verbose, **kwargs
+            "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
         )
         res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
         if hasattr(self.repo.odb, "update_cache"):
@@ -1125,7 +1125,7 @@ def pull(
             Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
 
         proc = self.repo.git.pull(
-            "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=False, v=True, **kwargs
+            "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
         )
         res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout)
         if hasattr(self.repo.odb, "update_cache"):

From 52cceaf2663422a79a0f1d21f905eb132e46b556 Mon Sep 17 00:00:00 2001
From: Florent Valette <florent.valette@ledger.fr>
Date: Tue, 15 Oct 2024 18:04:44 +0200
Subject: [PATCH 888/994] git,cmd: add encoding arg to popen if universal
 newlines is True

---
 git/cmd.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/git/cmd.py b/git/cmd.py
index 90fc39cd6..2048a43fa 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1269,6 +1269,7 @@ def execute(
                 stdout=stdout_sink,
                 shell=shell,
                 universal_newlines=universal_newlines,
+                encoding=defenc if universal_newlines else None,
                 **subprocess_kwargs,
             )
         except cmd_not_found_exception as err:

From d6cdb67bcaa2cf606bfc0a9295aacb54677ea86d Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Tue, 15 Oct 2024 20:35:29 +0200
Subject: [PATCH 889/994] See if python 3.7 still works when using an older
 Ubuntu version.

This should be undone once python 3.7 is EOL.
---
 .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 292c9fc86..747db62f0 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+        os: ["ubuntu-22.04", "macos-latest", "windows-latest"]
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         exclude:
         - os: "macos-latest"

From e51bf80ad576256f2fbeead41ea3f0b667c77055 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 2 Jan 2025 08:24:01 +0100
Subject: [PATCH 890/994] update GitDB submodule to latest pubslished version

---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 3d3e9572d..775cfe829 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 3d3e9572dc452fea53d328c101b3d1440bbefe40
+Subproject commit 775cfe8299ea5474f605935469359a9d1cdb49dc

From fb1b05124f1070ed56231a782daee0ffce9e1372 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Thu, 2 Jan 2025 08:27:54 +0100
Subject: [PATCH 891/994] bump patch level to prepare new version

---
 VERSION                | 2 +-
 doc/source/changes.rst | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/VERSION b/VERSION
index d1bf6638d..e6af1c454 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.43
+3.1.44
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 3c903423c..00a3c660e 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,12 @@
 Changelog
 =========
 
+3.1.44
+======
+
+See the following for all changes.
+https://github.com/gitpython-developers/GitPython/releases/tag/3.1.44
+
 3.1.43
 ======
 

From 9429be69c85442e744ef697bd79bad3fb4236e0a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 04:21:49 -0500
Subject: [PATCH 892/994] Special-case Python 3.7 for OSes we can install it on

This is analogous to the 3.7-related CI change in gitdb that was
part of https://github.com/gitpython-developers/gitdb/pull/114, as
to part of https://github.com/gitpython-developers/smmap/pull/58.

Since some tests are not yet passing on 3.13, this does not add
3.13 to CI, nor to the documentation of supported versions in
`setup.py`. Note that the list there is not enforced; GitPython can
already be installed on Python 3.13 and probably *mostly* works.

(See https://github.com/gitpython-developers/GitPython/pull/1955
for details on other changes that should be made to fully support
running GitPython on Python 3.13.)
---
 .github/workflows/pythonpackage.yml | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 747db62f0..deebe9e11 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -11,15 +11,19 @@ permissions:
 jobs:
   build:
     strategy:
-      fail-fast: false
       matrix:
-        os: ["ubuntu-22.04", "macos-latest", "windows-latest"]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
-        exclude:
-        - os: "macos-latest"
-          python-version: "3.7"
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
         - experimental: false
+        - os: ubuntu-22.04
+          python-version: "3.7"
+          experimental: false
+        - os: windows-latest
+          python-version: "3.7"
+          experimental: false
+
+      fail-fast: false
 
     runs-on: ${{ matrix.os }}
 

From affab8eda6a716363bc36c703de305676afc4ae3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 9 Dec 2024 13:14:20 +0000
Subject: [PATCH 893/994] Bump Vampire/setup-wsl from 3.1.1 to 4.0.0

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 3.1.1 to 4.0.0.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v3.1.1...v4.0.0)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 deebe9e11..b8e6391a1 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -44,7 +44,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v3.1.1
+      uses: Vampire/setup-wsl@v4.0.0
       with:
         distribution: Alpine
         additional-packages: bash

From 01e40a7b06c4ba3d0cf937ba0974949392446b51 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 04:38:44 -0500
Subject: [PATCH 894/994] Do everything in the venv in the Alpine test

---
 .github/workflows/alpine-test.yml | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 2c1eed391..8c00b1086 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -16,10 +16,10 @@ jobs:
     steps:
     - name: Prepare Alpine Linux
       run: |
-        apk add sudo git git-daemon python3 py3-pip
+        apk add sudo git git-daemon python3 py3-pip py3-setuptools py3-virtualenv py3-wheel
         echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
         addgroup -g 127 docker
-        adduser -D -u 1001 runner
+        adduser -D -u 1001 runner  # TODO: Check if this still works on GHA as intended.
         adduser runner docker
       shell: sh -exo pipefail {0}  # Run this as root, not the "runner" user.
 
@@ -50,17 +50,14 @@ jobs:
         . .venv/bin/activate
         printf '%s=%s\n' 'PATH' "$PATH" 'VIRTUAL_ENV' "$VIRTUAL_ENV" >>"$GITHUB_ENV"
 
-    - name: Update PyPA packages
-      run: |
-        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
-
     - name: Install project and test dependencies
       run: |
+        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
+        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -69,4 +66,5 @@ jobs:
 
     - name: Test with pytest
       run: |
+        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv

From 0300de986ef78a4e7a5562638592f5e91bfd8fa7 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 05:24:47 -0500
Subject: [PATCH 895/994] Instrument `handle_process_output` test

To try to find the bug that causes it to fail on Cygwin on CI, but
not on other systems on CI, and not locally on Cygwin.

It looks like there's an extra line being read from stderr when the
test fails, so let's examine the lines themselves.
---
 test/test_git.py | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 94e68ecf0..3f9687dfe 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -762,14 +762,17 @@ def test_environment(self, rw_dir):
     def test_handle_process_output(self):
         from git.cmd import handle_process_output, safer_popen
 
-        line_count = 5002
-        count = [None, 0, 0]
+        expected_line_count = 5002
+        line_counts = [None, 0, 0]
+        lines = [None, [], []]
 
-        def counter_stdout(line):
-            count[1] += 1
+        def stdout_handler(line):
+            line_counts[1] += 1
+            lines[1].append(line)
 
-        def counter_stderr(line):
-            count[2] += 1
+        def stderr_handler(line):
+            line_counts[2] += 1
+            lines[2].append(line)
 
         cmdline = [
             sys.executable,
@@ -784,10 +787,10 @@ def counter_stderr(line):
             shell=False,
         )
 
-        handle_process_output(proc, counter_stdout, counter_stderr, finalize_process)
+        handle_process_output(proc, stdout_handler, stderr_handler, finalize_process)
 
-        self.assertEqual(count[1], line_count)
-        self.assertEqual(count[2], line_count)
+        self.assertEqual(line_counts[1], expected_line_count, repr(lines[1]))
+        self.assertEqual(line_counts[2], expected_line_count, repr(lines[2]))
 
     def test_execute_kwargs_set_agrees_with_method(self):
         parameter_names = inspect.signature(cmd.Git.execute).parameters.keys()

From a0f8425c94992bdf3fdde9cbf7c3c7f9dc12e52c Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 05:28:31 -0500
Subject: [PATCH 896/994] Slightly simplify and clarify instrumented code

---
 test/test_git.py | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/test/test_git.py b/test/test_git.py
index 3f9687dfe..274511f8d 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -763,16 +763,13 @@ def test_handle_process_output(self):
         from git.cmd import handle_process_output, safer_popen
 
         expected_line_count = 5002
-        line_counts = [None, 0, 0]
-        lines = [None, [], []]
+        actual_lines = [None, [], []]
 
         def stdout_handler(line):
-            line_counts[1] += 1
-            lines[1].append(line)
+            actual_lines[1].append(line)
 
         def stderr_handler(line):
-            line_counts[2] += 1
-            lines[2].append(line)
+            actual_lines[2].append(line)
 
         cmdline = [
             sys.executable,
@@ -789,8 +786,8 @@ def stderr_handler(line):
 
         handle_process_output(proc, stdout_handler, stderr_handler, finalize_process)
 
-        self.assertEqual(line_counts[1], expected_line_count, repr(lines[1]))
-        self.assertEqual(line_counts[2], expected_line_count, repr(lines[2]))
+        self.assertEqual(len(actual_lines[1]), expected_line_count, repr(actual_lines[1]))
+        self.assertEqual(len(actual_lines[2]), expected_line_count, repr(actual_lines[2]))
 
     def test_execute_kwargs_set_agrees_with_method(self):
         parameter_names = inspect.signature(cmd.Git.execute).parameters.keys()

From 4aad37a303ca51a594700976b04e17f0f835d61d Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 06:00:23 -0500
Subject: [PATCH 897/994] Improve environment isolation in Cygwin and Alpine
 Linux on CI

The main change here is to the Cygwin test workflow, which now runs
the tests in a venv (a Python virtual environment), to avoid mixing
up our intended dependencies with other files installed by Python
related packages on the system. This should either fix the problem
where `test_handle_process_output` reports an extra line in stderr
for the `cat_file.py` subprocess on CI or Cygwin, or at least make
it somewhat easier to continue investigating the problem. I think
this change is also valuable for its own sake.

The connection to the `test_handle_process_output` failure is that
the extra line in stderr appears at the beginning and is an error
message about a missing import needed for subprocess code coverage.
With the recently added more detailed error reporting, it shows:

            self.assertEqual(line_counts[1], expected_line_count, repr(lines[1]))
    >       self.assertEqual(line_counts[2], expected_line_count, repr(lines[2]))
    E       AssertionError: 5003 != 5002 : ['pytest-cov: Failed to setup subprocess coverage. Environ: {\'COV_CORE_SOURCE\': \'git\', \'COV_CORE_CONFIG\': \':\', \'COV_CORE_DATAFILE\': \'/cygdrive/d/a/GitPython/GitPython/.coverage\'} Exception: ModuleNotFoundError("No module named \'iniconfig\'")\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', ' = [up to date]      testcommit100 -> origin/testcommit100\n', ' = [up to date]      testcommit1000 -> origin/testcommit1000\n', ' = [up to date]      testcommit1001 -> origin/testcommit1001\n', ' = [up to date]      testcommit1002 -> origin/testcommit1002\n', ' = [up to date]      testcommit1003 -> origin/testcommit1003\n', ' = [up to date]      testcommit1004 -> origin/testcommit1004\n', ' = [up to date]      testcommit1005 -> origin/testcommit1005\n', ' = [up to date]      testcommit
    test/test_git.py:793: AssertionError

This could possibly be worked around by attempting to install a
package to provide `iniconfig`, by configuring `pytest-cov` in a
way that does not require it, or by temporarily disabling code
coverage reports on Cygwin. Before exploring those or other
options, this change seeks to prepare a more isolated environment
in which different package versions are more likely to work
properly together.

In addition to that change to the Cygwin workflow, this also
changes the way the Alpine Linux test workflow is made to use a
venv, using the technique that is used here, and undoing some
changes in the Alpine Linux workflow that should not be necessary.
The reason for making that change together with this Cygwin change
is that if either does not work as expected, it might shed light on
what is going wrong with the other.

Although the same technique is used on Cygwin and in Alpine Linux,
it looks a little different, because the environment variable on
Cygwin is `BASH_ENV` (since script steps are run in `bash`), while
the environment variable is `ENV` (since script steps are run in
`busybox sh`) and this must also be allowed to pass through `sudo`
(since `sh`, which is `busybox sh` on this system, is being invoked
through `sudo`).
---
 .github/workflows/alpine-test.yml | 15 ++++++++-------
 .github/workflows/cygwin-test.yml |  6 +++---
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 8c00b1086..ca141cf03 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -16,8 +16,8 @@ jobs:
     steps:
     - name: Prepare Alpine Linux
       run: |
-        apk add sudo git git-daemon python3 py3-pip py3-setuptools py3-virtualenv py3-wheel
-        echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
+        apk add sudo git git-daemon python3 py3-pip py3-virtualenv
+        echo 'Defaults env_keep += "CI ENV GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
         addgroup -g 127 docker
         adduser -D -u 1001 runner  # TODO: Check if this still works on GHA as intended.
         adduser runner docker
@@ -47,17 +47,19 @@ jobs:
     - name: Set up virtualenv
       run: |
         python -m venv .venv
-        . .venv/bin/activate
-        printf '%s=%s\n' 'PATH' "$PATH" 'VIRTUAL_ENV' "$VIRTUAL_ENV" >>"$GITHUB_ENV"
+        echo 'ENV=.venv/bin/activate' >> "$GITHUB_ENV"  # ENV (not BASH_ENV) for BusyBox sh.
+
+    - name: Update PyPA packages
+      run: |
+        # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
+        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
-        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
-        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -66,5 +68,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv
diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index bde4ea659..ebe50240d 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -55,10 +55,10 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
-    - name: Ensure the "pip" command is available
+    - name: Set up virtualenv
       run: |
-        # This is used unless, and before, an updated pip is installed.
-        ln -s pip3 /usr/bin/pip
+        python -m venv .venv
+        echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV"
 
     - name: Update PyPA packages
       run: |

From 39cd608b762256663b862224bcb46bdb2fc18817 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 06:28:48 -0500
Subject: [PATCH 898/994] Put back explicit venv activation in Alpine Linux

`busybox sh` does not appear to read commands from a file whose
path is given as the value of `$ENV`, in this situation. I think I
may have misunderstood that; the documentation does not say much
about it and maybe, in Almquist-style shells, it is only read by
interactive shells? I am not sure.

This removes everything about `ENV` and activates the venv in each
step where the venv should be used.

The good news is that the technique did work fully in Cygwin: not
only did `BASH_ENV` work (which was not much in doubt), but using
a virtual environment for all steps that run test code on Cygwin
fixed the problem and allowed all tests to pass. That seems to have
been the reason I didn't reproduce the problem locally: on my local
system I always use a venv in Cygwin since I would otherwise not
have adequate isolation.

Thus, this commit changes only the Alpine workflow and not the
Cygwin workflow.
---
 .github/workflows/alpine-test.yml | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index ca141cf03..6dc62f596 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -17,7 +17,7 @@ jobs:
     - name: Prepare Alpine Linux
       run: |
         apk add sudo git git-daemon python3 py3-pip py3-virtualenv
-        echo 'Defaults env_keep += "CI ENV GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
+        echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
         addgroup -g 127 docker
         adduser -D -u 1001 runner  # TODO: Check if this still works on GHA as intended.
         adduser runner docker
@@ -47,19 +47,21 @@ jobs:
     - name: Set up virtualenv
       run: |
         python -m venv .venv
-        echo 'ENV=.venv/bin/activate' >> "$GITHUB_ENV"  # ENV (not BASH_ENV) for BusyBox sh.
 
     - 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 $(pip freeze --all | grep -ow ^setuptools) wheel
 
     - name: Install project and test dependencies
       run: |
+        . .venv/bin/activate
         pip install ".[test]"
 
     - name: Show version and platform information
       run: |
+        . .venv/bin/activate
         uname -a
         command -v git python
         git version
@@ -68,4 +70,5 @@ jobs:
 
     - name: Test with pytest
       run: |
+        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv

From 8a05390925ef416736ce9c0be8569977ca48fa07 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 07:22:11 -0500
Subject: [PATCH 899/994] Tune CI matrix adjustments so reports are clearer

Since #1987, test jobs from `pythonpackage.yml` appear in an
unintuitive order, and some show an extra bool matrix variable in
their names while others don't (this corresponds to `experimental`,
which was always set to some value, but was set in different ways).

This fixes that by:

- Listing all tested versions, rather than introducing some in an
  `include` key. (The `include:`-introduced jobs didn't distinguish
  between originally-present matrix variables and those that are
  introduced based on the values of the original ones.)

- Replacing `os` with `os-type`, which has only the first part of
  the value for `runs-on:` (e.g., `ubuntu`), and adding `os-ver`
  to each matrix job, defaulting it to `latest`, but using `22.04`
  for Python 3.7 on Ubuntu.

This should also naturally extend to adding 3.13, with or without
setting `continue-on-error` to temporarily work around the problems
obseved in #1955, but nothing 3.13-related is done in this commit.
---
 .github/workflows/pythonpackage.yml | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index b8e6391a1..aff6354f4 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -12,20 +12,21 @@ jobs:
   build:
     strategy:
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
-        include:
-        - experimental: false
-        - os: ubuntu-22.04
+        os-type: [ubuntu, macos, windows]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        exclude:
+        - os-type: macos
           python-version: "3.7"
-          experimental: false
-        - os: windows-latest
+        include:
+        - os-ver: latest
+        - os-type: ubuntu
           python-version: "3.7"
-          experimental: false
+          os-ver: "22.04"
+        - experimental: false
 
       fail-fast: false
 
-    runs-on: ${{ matrix.os }}
+    runs-on: ${{ matrix.os-type }}-${{ matrix.os-ver }}
 
     defaults:
       run:
@@ -43,7 +44,7 @@ jobs:
         allow-prereleases: ${{ matrix.experimental }}
 
     - name: Set up WSL (Windows)
-      if: startsWith(matrix.os, 'windows')
+      if: matrix.os-type == 'windows'
       uses: Vampire/setup-wsl@v4.0.0
       with:
         distribution: Alpine
@@ -80,7 +81,7 @@ jobs:
 
     # For debugging hook tests on native Windows systems that may have WSL.
     - name: Show bash.exe candidates (Windows)
-      if: startsWith(matrix.os, 'windows')
+      if: matrix.os-type == 'windows'
       run: |
         set +e
         bash.exe -c 'printenv WSL_DISTRO_NAME; uname -a'

From 41377d5254d3e4a03a7edd5adc8fd4b5a0767210 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 2 Jan 2025 07:39:19 -0500
Subject: [PATCH 900/994] Change test job keys from `build` to `test`

This goes a bit further in the direction of the preceding commit,
making CI reports/logs a bit more intuitive.
---
 .github/workflows/alpine-test.yml   | 2 +-
 .github/workflows/cygwin-test.yml   | 2 +-
 .github/workflows/pythonpackage.yml | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 6dc62f596..bd09a939b 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -3,7 +3,7 @@ name: test-alpine
 on: [push, pull_request, workflow_dispatch]
 
 jobs:
-  build:
+  test:
     runs-on: ubuntu-latest
 
     container:
diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index ebe50240d..575fb26ef 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -3,7 +3,7 @@ name: test-cygwin
 on: [push, pull_request, workflow_dispatch]
 
 jobs:
-  build:
+  test:
     runs-on: windows-latest
 
     strategy:
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index aff6354f4..9225624f0 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -9,7 +9,7 @@ permissions:
   contents: read
 
 jobs:
-  build:
+  test:
     strategy:
       matrix:
         os-type: [ubuntu, macos, windows]

From 73ddb22f5c06fc3f09addf9be176d569d770b469 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 4 Jan 2025 09:55:50 -0500
Subject: [PATCH 901/994] Continue testing Python 3.9 on CI but unpin 3.9.16-1

We pinned Python 3.9.16 on Cygwin CI in #1814 (by requiring
3.9.16-1 as the exact version of the `python39` Cygwin package,
along with other supporting changes). We did this to solve a
problem where Python 3.9.18-1, which contained a bug that broke
GitPython CI (and various other software), would be selected.

Version 3.9.18-1 was marked back to being a "test" package shortly
after the bug was reported, and was subsequently removed altogether
from the Cygwin repositories. Because the affected package version
effectively no longer exists, and because this issue is known and
a non-"test" version still affected by it is very unlikely to be
released in the future, this pinning has been decisively
unnecessary for some time, though still not harmful.

This commit undoes the pinning, so that the `python39` package can
be installed at a higher version if one becomes available. This
serves two purposes.

- There is work under way in porting Python 3.12 to Cygwin. To test
  this with GitPython (either while it is in development or later),
  it will be useful to turn the Cygwin test job into a matrix job
  definition, generating two jobs, one for Python 3.9 and one for
  Python 3.12. Since 3.12 will probably not benefit from pinning,
  dropping pinning simplifies this.

- If the port of Python 3.12 to Cygwin is successful, it might
  lead to a solution to the but that currently keeps 3.9.18 from
  being made available for Cygwin. In that case, another 3.9.18-*
  Cygwin package would be released, which we would want to use.

Although this is uncertain, the change is a simplification, so I
think it is reasonable to do now.

Note that the pinning being undone here only affects the
distinction between different 3.9.* versions. `python39` and
`python312` are different Cygwin packages altogether, with
correspondingly different `python39-*` and `python312-*` associated
packages; this is not unpinning Python 3.9 in a way that would
cause Python 3.12 to be selected instead of it.
---
 .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 575fb26ef..d42eb0587 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Set up Cygwin
       uses: egor-tensin/setup-cygwin@v4
       with:
-        packages: python39=3.9.16-1 python39-pip python39-virtualenv git
+        packages: python39 python39-pip python39-virtualenv git
 
     - name: Arrange for verbose output
       run: |

From 85d72ef55eb3ba64f5db6016198724ec45769961 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Jan 2025 02:38:34 -0500
Subject: [PATCH 902/994] Test Python 3.13 on Ubuntu and macOS on CI

But not Windows yet (#1955).
---
 .github/workflows/pythonpackage.yml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 9225624f0..245844972 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,10 +13,12 @@ jobs:
     strategy:
       matrix:
         os-type: [ubuntu, macos, windows]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
         exclude:
         - os-type: macos
-          python-version: "3.7"
+          python-version: "3.7"  # Not available for the ARM-based macOS runners.
+        - os-type: windows
+          python-version: "3.13"  # FIXME: Fix and enable Python 3.13 on Windows (#1955).
         include:
         - os-ver: latest
         - os-type: ubuntu

From b20de09016ce221943a7bc4c7b67be5bacad9a15 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Jan 2025 03:24:28 -0500
Subject: [PATCH 903/994] Affirm that gitdb and smmap advisories can also be
 created

This expands `SECURITY.md` to affirm the claims in the new
`SECURITY.md` files in gitdb and smmap that vulnerabilities found
in them can be reported in the GitPython repository with the same
link as one would use to report a GitPython vulnerability, as well
as to note how the distinction between affected package can be
specified when it is known at the time a vulnerability is reported.

Along with https://github.com/gitpython-developers/smmap/pull/59
and https://github.com/gitpython-developers/gitdb/pull/117, this
fixes https://github.com/gitpython-developers/gitdb/issues/116.
---
 SECURITY.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/SECURITY.md b/SECURITY.md
index d39425b70..3f7d9f27e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -11,4 +11,6 @@ Only the latest version of GitPython can receive security updates. If a vulnerab
 
 ## Reporting a Vulnerability
 
-Please report private portions of a vulnerability to <https://github.com/gitpython-developers/GitPython/security/advisories/new>. Doing so helps to receive updates and collaborate on the matter, without disclosing it publicliy right away.
+Please report private portions of a vulnerability to <https://github.com/gitpython-developers/GitPython/security/advisories/new>. Doing so helps to receive updates and collaborate on the matter, without disclosing it publicly right away.
+
+Vulnerabilities in GitPython's dependencies [gitdb](https://github.com/gitpython-developers/gitdb/blob/main/SECURITY.md) or [smmap](https://github.com/gitpython-developers/smmap/blob/main/SECURITY.md), which primarily exist to support GitPython, can be reported here as well, at that same link. The affected package (`GitPython`, `gitdb`, or `smmap`) can be included in the report, if known.

From 55f36e64d79e0e8acc8f8763af5e7b18a3b214f6 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 5 Jan 2025 11:51:31 -0500
Subject: [PATCH 904/994] Fix links to gitdb and smmap `SECURITY.md` files

The links in #1991 did not work, as I got the branch names wrong.
---
 SECURITY.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/SECURITY.md b/SECURITY.md
index 3f7d9f27e..0aea34845 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -13,4 +13,4 @@ Only the latest version of GitPython can receive security updates. If a vulnerab
 
 Please report private portions of a vulnerability to <https://github.com/gitpython-developers/GitPython/security/advisories/new>. Doing so helps to receive updates and collaborate on the matter, without disclosing it publicly right away.
 
-Vulnerabilities in GitPython's dependencies [gitdb](https://github.com/gitpython-developers/gitdb/blob/main/SECURITY.md) or [smmap](https://github.com/gitpython-developers/smmap/blob/main/SECURITY.md), which primarily exist to support GitPython, can be reported here as well, at that same link. The affected package (`GitPython`, `gitdb`, or `smmap`) can be included in the report, if known.
+Vulnerabilities in GitPython's dependencies [gitdb](https://github.com/gitpython-developers/gitdb/blob/master/SECURITY.md) or [smmap](https://github.com/gitpython-developers/smmap/blob/master/SECURITY.md), which primarily exist to support GitPython, can be reported here as well, at that same link. The affected package (`GitPython`, `gitdb`, or `smmap`) can be included in the report, if known.

From 80096b95e119e90c9324f5ba898705fda4581c84 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 6 Jan 2025 13:38:24 +0000
Subject: [PATCH 905/994] Bump Vampire/setup-wsl from 4.0.0 to 4.1.0

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 245844972..68b4fd8f9 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -47,7 +47,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: matrix.os-type == 'windows'
-      uses: Vampire/setup-wsl@v4.0.0
+      uses: Vampire/setup-wsl@v4.1.0
       with:
         distribution: Alpine
         additional-packages: bash

From fb102cfc5a86155d2c8b8f10cf5957f3f5d9e435 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 6 Jan 2025 13:45:04 +0000
Subject: [PATCH 906/994] Bump git/ext/gitdb from `775cfe8` to `9e68ea1`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `775cfe8` to `9e68ea1`.
- [Release notes](https://github.com/gitpython-developers/gitdb/releases)
- [Commits](https://github.com/gitpython-developers/gitdb/compare/775cfe8299ea5474f605935469359a9d1cdb49dc...9e68ea1687bbda84776c3605b96bb0b43e846bea)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 775cfe829..9e68ea168 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 775cfe8299ea5474f605935469359a9d1cdb49dc
+Subproject commit 9e68ea1687bbda84776c3605b96bb0b43e846bea

From e4f1aa71dd255583ff19c1bd40410e94da8e15af Mon Sep 17 00:00:00 2001
From: Frank Lichtenheld <frank@lichtenheld.com>
Date: Thu, 9 Jan 2025 17:57:51 +0100
Subject: [PATCH 907/994] Repo.rev_parse: Handle <tag>^{commit} correctly

This should resolve to commit object.

Fixes: #1995

Signed-off-by: Frank Lichtenheld <frank@lichtenheld.com>
---
 git/repo/fun.py   | 8 +++++++-
 test/test_repo.py | 4 ++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/git/repo/fun.py b/git/repo/fun.py
index 182cf82ed..125ba5936 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -301,7 +301,13 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
 
             # Handle type.
             if output_type == "commit":
-                pass  # Default.
+                obj = cast("TagObject", obj)
+                if obj and obj.type == "tag":
+                    obj = deref_tag(obj)
+                else:
+                    # Cannot do anything for non-tags.
+                    pass
+                # END handle tag
             elif output_type == "tree":
                 try:
                     obj = cast(AnyGitObject, obj)
diff --git a/test/test_repo.py b/test/test_repo.py
index e38da5bb6..bfa1bbb78 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -1064,9 +1064,9 @@ def test_rev_parse(self):
         # TODO: Dereference tag into a blob 0.1.7^{blob} - quite a special one.
         # Needs a tag which points to a blob.
 
-        # ref^0 returns commit being pointed to, same with ref~0, and ^{}
+        # ref^0 returns commit being pointed to, same with ref~0, ^{}, and ^{commit}
         tag = rev_parse("0.1.4")
-        for token in ("~0", "^0", "^{}"):
+        for token in ("~0", "^0", "^{}", "^{commit}"):
             self.assertEqual(tag.object, rev_parse("0.1.4%s" % token))
         # END handle multiple tokens
 

From 7751d0b98920f9fc74b2f109e92cb0abf3a98b15 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Fri, 10 Jan 2025 13:52:56 -0500
Subject: [PATCH 908/994] Fuzzing: Fix broken test for Git submodule handling

Ensured submodule names, paths, and commit messages are sanitized to
avoid invalid states that are expected to cause exceptions and should
not halt the fuzzer.

In particular, the changes here:
- Sanitized inputs for submodule names, paths, and commit messages.
- Added validation for submodule SHA and path integrity.
---
 fuzzing/fuzz-targets/fuzz_submodule.py | 59 ++++++++++++++++++--------
 1 file changed, 42 insertions(+), 17 deletions(-)

diff --git a/fuzzing/fuzz-targets/fuzz_submodule.py b/fuzzing/fuzz-targets/fuzz_submodule.py
index d22b0aa5b..afa653d0d 100644
--- a/fuzzing/fuzz-targets/fuzz_submodule.py
+++ b/fuzzing/fuzz-targets/fuzz_submodule.py
@@ -9,11 +9,17 @@
     get_max_filename_length,
 )
 
-# Setup the git environment
+# Setup the Git environment
 setup_git_environment()
 from git import Repo, GitCommandError, InvalidGitRepositoryError
 
 
+def sanitize_input(input_str, max_length=255):
+    """Sanitize and truncate inputs to avoid invalid Git operations."""
+    sanitized = "".join(ch for ch in input_str if ch.isalnum() or ch in ("-", "_", "."))
+    return sanitized[:max_length]
+
+
 def TestOneInput(data):
     fdp = atheris.FuzzedDataProvider(data)
 
@@ -24,12 +30,23 @@ def TestOneInput(data):
         try:
             with tempfile.TemporaryDirectory() as submodule_temp_dir:
                 sub_repo = Repo.init(submodule_temp_dir, bare=fdp.ConsumeBool())
-                sub_repo.index.commit(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512)))
+                commit_message = sanitize_input(fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(1, 512)))
+                sub_repo.index.commit(commit_message)
 
-                submodule_name = fdp.ConsumeUnicodeNoSurrogates(
-                    fdp.ConsumeIntInRange(1, max(1, get_max_filename_length(repo.working_tree_dir)))
+                submodule_name = sanitize_input(
+                    fdp.ConsumeUnicodeNoSurrogates(
+                        fdp.ConsumeIntInRange(1, get_max_filename_length(repo.working_tree_dir))
+                    )
                 )
-                submodule_path = os.path.join(repo.working_tree_dir, submodule_name)
+
+                submodule_path = os.path.relpath(
+                    os.path.join(repo.working_tree_dir, submodule_name),
+                    start=repo.working_tree_dir,
+                )
+
+                # Ensure submodule_path is valid
+                if not submodule_name or submodule_name.startswith("/") or ".." in submodule_name:
+                    return -1  # Reject invalid input so they are not added to the corpus
 
                 submodule = repo.create_submodule(submodule_name, submodule_path, url=sub_repo.git_dir)
                 repo.index.commit("Added submodule")
@@ -39,25 +56,38 @@ def TestOneInput(data):
                     value_length = fdp.ConsumeIntInRange(1, max(1, fdp.remaining_bytes()))
 
                     writer.set_value(
-                        fdp.ConsumeUnicodeNoSurrogates(key_length), fdp.ConsumeUnicodeNoSurrogates(value_length)
+                        sanitize_input(fdp.ConsumeUnicodeNoSurrogates(key_length)),
+                        sanitize_input(fdp.ConsumeUnicodeNoSurrogates(value_length)),
                     )
                     writer.release()
 
-                submodule.update(init=fdp.ConsumeBool(), dry_run=fdp.ConsumeBool(), force=fdp.ConsumeBool())
+                submodule.update(
+                    init=fdp.ConsumeBool(),
+                    dry_run=fdp.ConsumeBool(),
+                    force=fdp.ConsumeBool(),
+                )
+
                 submodule_repo = submodule.module()
 
-                new_file_name = fdp.ConsumeUnicodeNoSurrogates(
-                    fdp.ConsumeIntInRange(1, max(1, get_max_filename_length(submodule_repo.working_tree_dir)))
+                new_file_name = sanitize_input(
+                    fdp.ConsumeUnicodeNoSurrogates(
+                        fdp.ConsumeIntInRange(1, get_max_filename_length(submodule_repo.working_tree_dir))
+                    )
                 )
                 new_file_path = os.path.join(submodule_repo.working_tree_dir, new_file_name)
                 with open(new_file_path, "wb") as new_file:
                     new_file.write(fdp.ConsumeBytes(fdp.ConsumeIntInRange(1, 512)))
+
                 submodule_repo.index.add([new_file_path])
                 submodule_repo.index.commit("Added new file to submodule")
 
                 repo.submodule_update(recursive=fdp.ConsumeBool())
-                submodule_repo.head.reset(commit="HEAD~1", working_tree=fdp.ConsumeBool(), head=fdp.ConsumeBool())
-                # Use fdp.PickValueInList to ensure at least one of 'module' or 'configuration' is True
+                submodule_repo.head.reset(
+                    commit="HEAD~1",
+                    working_tree=fdp.ConsumeBool(),
+                    head=fdp.ConsumeBool(),
+                )
+
                 module_option_value, configuration_option_value = fdp.PickValueInList(
                     [(True, False), (False, True), (True, True)]
                 )
@@ -82,12 +112,7 @@ def TestOneInput(data):
         ):
             return -1
         except Exception as e:
-            if isinstance(e, ValueError) and "embedded null byte" in str(e):
-                return -1
-            elif isinstance(e, OSError) and "File name too long" in str(e):
-                return -1
-            else:
-                return handle_exception(e)
+            return handle_exception(e)
 
 
 def main():

From 5c547f33063d811f445c82de19b0d2f6aad0e995 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 27 Jan 2025 13:17:13 +0000
Subject: [PATCH 909/994] Bump git/ext/gitdb from `9e68ea1` to `f36c0cc`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `9e68ea1` to `f36c0cc`.
- [Release notes](https://github.com/gitpython-developers/gitdb/releases)
- [Commits](https://github.com/gitpython-developers/gitdb/compare/9e68ea1687bbda84776c3605b96bb0b43e846bea...f36c0cc42ea2f529291e441073f74e920988d4d2)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index 9e68ea168..f36c0cc42 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit 9e68ea1687bbda84776c3605b96bb0b43e846bea
+Subproject commit f36c0cc42ea2f529291e441073f74e920988d4d2

From 9e22a4a5811157e59a61778285d79b686bbec9ad Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:17:30 -0500
Subject: [PATCH 910/994] Run `python3.9` explicitly on Cygwin CI

In case somehow another one leaked in.
---
 .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 d42eb0587..0a66a9b46 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -57,7 +57,7 @@ jobs:
 
     - name: Set up virtualenv
       run: |
-        python -m venv .venv
+        python3.9 -m venv .venv
         echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV"
 
     - name: Update PyPA packages

From f401b1020f4177b217d679d71a41df28754bef6f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:19:56 -0500
Subject: [PATCH 911/994] Switch back to cygwin/cygwin-install-action

Since we don't need pinning, and this avoids installing and using
the chocolatey package manager, which we're not using for anything
else.
---
 .github/workflows/cygwin-test.yml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 0a66a9b46..fbf9b8307 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -15,7 +15,7 @@ jobs:
 
     defaults:
       run:
-        shell: C:\tools\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}"
+        shell: C:\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr "{0}"
 
     steps:
     - name: Force LF line endings
@@ -27,10 +27,11 @@ jobs:
       with:
         fetch-depth: 0
 
-    - name: Set up Cygwin
-      uses: egor-tensin/setup-cygwin@v4
+    - name: Install Cygwin
+      uses: cygwin/cygwin-install-action@v5
       with:
         packages: python39 python39-pip python39-virtualenv git
+        add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
     - name: Arrange for verbose output
       run: |

From 4605dd690b6ebe4c0c07f3910607aae407d6070e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:30:50 -0500
Subject: [PATCH 912/994] Install pip in venv in separate step

---
 .github/workflows/cygwin-test.yml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index fbf9b8307..a2e7f6b2d 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -58,9 +58,13 @@ jobs:
 
     - name: Set up virtualenv
       run: |
-        python3.9 -m venv .venv
+        python3.9 -m venv --without-pip .venv
         echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV"
 
+    - name: Install pip in virtualenv
+      run: |
+        python -m ensurepip
+
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.

From a5fee410ec8d40264d7a53cc5c4fd9deb0c97dd5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:40:26 -0500
Subject: [PATCH 913/994] Install Cygwin package for wheel

---
 .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 a2e7f6b2d..d4aee4544 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Install Cygwin
       uses: cygwin/cygwin-install-action@v5
       with:
-        packages: python39 python39-pip python39-virtualenv git
+        packages: python39 python39-pip python39-virtualenv python39-wheel git
         add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
     - name: Arrange for verbose output

From a2b73cac7c252f3c35169dd8a752614614571d10 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:45:30 -0500
Subject: [PATCH 914/994] Use the pip bootstrap script instead

---
 .github/workflows/cygwin-test.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index d4aee4544..585c419aa 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Install Cygwin
       uses: cygwin/cygwin-install-action@v5
       with:
-        packages: python39 python39-pip python39-virtualenv python39-wheel git
+        packages: python39 python39-pip python39-virtualenv git
         add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
     - name: Arrange for verbose output
@@ -61,9 +61,9 @@ jobs:
         python3.9 -m venv --without-pip .venv
         echo 'BASH_ENV=.venv/bin/activate' >>"$GITHUB_ENV"
 
-    - name: Install pip in virtualenv
+    - name: Bootstrap pip in virtualenv
       run: |
-        python -m ensurepip
+        wget -qO- https://bootstrap.pypa.io/get-pip.py | python
 
     - name: Update PyPA packages
       run: |

From d597fc949c48726b34c2130424a044020960e5f2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 21 Feb 2025 05:51:25 -0500
Subject: [PATCH 915/994] Install wget for Cygwin job to use to get pip
 bootstrap script

---
 .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 585c419aa..278777907 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -30,7 +30,7 @@ jobs:
     - name: Install Cygwin
       uses: cygwin/cygwin-install-action@v5
       with:
-        packages: python39 python39-pip python39-virtualenv git
+        packages: python39 python39-pip python39-virtualenv git wget
         add-to-path: false  # No need to change $PATH outside the Cygwin environment.
 
     - name: Arrange for verbose output

From 1277baa811e5500cf4aaae1569137fe1e6b4c3cb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 24 Feb 2025 14:53:19 +0000
Subject: [PATCH 916/994] Bump Vampire/setup-wsl from 4.1.0 to 4.1.1

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 68b4fd8f9..4850f252c 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -47,7 +47,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: matrix.os-type == 'windows'
-      uses: Vampire/setup-wsl@v4.1.0
+      uses: Vampire/setup-wsl@v4.1.1
       with:
         distribution: Alpine
         additional-packages: bash

From 2ae697d02182d62ed1c17df370d7aa3a2b67cbce Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Wed, 5 Mar 2025 23:00:42 -0500
Subject: [PATCH 917/994] Mark `test_installation` xfail on Cygwin CI

Together with #2007, this works around #2004, allowing all tests to
pass on Cygwin CI.

In #2007, installation of the environment in which tests run was
fixed by downloading and running the `get-pip.py` bootstrap script.
If we were to modify our helper that sets up the (separate) virtual
environment in `test_installation` so that it does the same thing
(or conditionally does so on CI, since the problem does not seem to
happen in local installations), that would likely "fix" this more
thoroughly, allowing the test to pass.

But part of the goal of the installation test is to test that
installation works in a typical environment on the platform it runs
on. So it is not obivous that making it pass in that way would be
an improvement compared to marking it `xfail` with the exception
type that occurs due to #2004. So this just does that, for now.
---
 test/test_installation.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/test/test_installation.py b/test/test_installation.py
index ae6472e98..a35826bd0 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -4,11 +4,19 @@
 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 54e1c1bfd14fc055fb8f7324154fd0658ac0d16f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 12 Mar 2025 11:55:36 +0000
Subject: [PATCH 918/994] Bump Vampire/setup-wsl from 4.1.1 to 5.0.0

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 4.1.1 to 5.0.0.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v4.1.1...v5.0.0)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 4850f252c..039699af5 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -47,7 +47,7 @@ jobs:
 
     - name: Set up WSL (Windows)
       if: matrix.os-type == 'windows'
-      uses: Vampire/setup-wsl@v4.1.1
+      uses: Vampire/setup-wsl@v5.0.0
       with:
         distribution: Alpine
         additional-packages: bash

From a41a0de46d2fdbb34207161bb0c180bd72958c8b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 6 Mar 2025 19:06:46 -0500
Subject: [PATCH 919/994] Use WSL1 on CI

This avoids an occasional HTTP 403 error updating WSL for WSL2.

For details on that issue and possible approaches, see:
https://github.com/gitpython-developers/GitPython/pull/2008#pullrequestreview-2665805369
---
 .github/workflows/pythonpackage.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 039699af5..1a0210723 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -49,6 +49,7 @@ jobs:
       if: matrix.os-type == 'windows'
       uses: Vampire/setup-wsl@v5.0.0
       with:
+        wsl-version: 1
         distribution: Alpine
         additional-packages: bash
 

From c13d998c03ec5268b5b6c361fe0c65854041b684 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 6 Mar 2025 19:45:41 -0500
Subject: [PATCH 920/994] Test on free-threaded Python

See #2005. Right now, this does not limit by operating system, but
that is just to verify that there are no OS-specific 3.13t problems
we should know about right now; once that is verified, the macOS
and Windows jobs will be removed (excluded) for the time being.

The 3.13t jobs added here use `Quansight-Labs/setup-python`, not
`actions/setup-python`. The latter also has the ability to use
3.13t since https://github.com/actions/python-versions/pull/319
and https://github.com/actions/setup-python/pull/973 (see also
https://github.com/actions/setup-python/issues/771), but no version
tag includes this feature yet. It can be used by using `@main` or
`@...` where `...` is an OID. The former would risk pulling in
other untested features we're no trying to test with, while the
latter would not be easy to upgrade automatically as what we have
now (we would be deliberately keeping a hash not at any tag that is
already not the latest hash on any branch). In contrast, the
`Quansight-Labs/setup-python` fork adds this feature while staying
up to date with others. When `actions/setup-python` has a release
(stable or prerelease) with this feature, we can switch to it.

This could probably be done with less code duplication by using a
matrix variable for the action to use. Instead, the "Set up Python"
step is split in two, with opposite `if` conditions, so that each
is capable of being recognized and upgraded by Dependabot if a new
major version is released (in case this ends up remaining in place
longer than expected).
---
 .github/workflows/pythonpackage.yml | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 039699af5..b24c5cc57 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       matrix:
         os-type: [ubuntu, macos, windows]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"]
         exclude:
         - os-type: macos
           python-version: "3.7"  # Not available for the ARM-based macOS runners.
@@ -40,11 +40,20 @@ jobs:
         fetch-depth: 0
 
     - name: Set up Python ${{ matrix.python-version }}
+      if: |-
+        !endsWith(matrix.python-version, 't')
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
+    - name: Set up Python ${{ matrix.python-version }} (free-threaded)
+      if: endsWith(matrix.python-version, 't')
+      uses: Quansight-Labs/setup-python@v5
+      with:
+        python-version: ${{ matrix.python-version }}
+        allow-prereleases: ${{ matrix.experimental }}
+
     - name: Set up WSL (Windows)
       if: matrix.os-type == 'windows'
       uses: Vampire/setup-wsl@v5.0.0

From 56038c3e0382d87ccdb66d53964f038314c157fd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Thu, 6 Mar 2025 22:39:23 -0500
Subject: [PATCH 921/994] Only test free-threaded Python on Linux

For now, this omits macOS and Windows from the 3.13t ("threaded")
tests.

The plan in #2005 is to start without them, and no OS-specific
problems have been identified so far. In particular, in the
previous commit that adds 3.13t without excluding any operating
systems, all tests in the macOS job passed as expected, and the
Windows job had the same failure with the same message as in #1955,
with no XFAIL changed to XPASS (which, if present, would suggest
GC differences meriting further exploration of 3.13t on Windows).
---
 .github/workflows/pythonpackage.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index b24c5cc57..661df0693 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -17,8 +17,12 @@ jobs:
         exclude:
         - os-type: macos
           python-version: "3.7"  # Not available for the ARM-based macOS runners.
+        - os-type: macos
+          python-version: "3.13t"
         - os-type: windows
           python-version: "3.13"  # FIXME: Fix and enable Python 3.13 on Windows (#1955).
+        - os-type: windows
+          python-version: "3.13t"
         include:
         - os-ver: latest
         - os-type: ubuntu

From 11f7fafada8348c8e8f699c7ab621be6d26b00a5 Mon Sep 17 00:00:00 2001
From: Kamil Kozik <kamilkozik7@gmail.com>
Date: Fri, 7 Mar 2025 18:39:06 +0100
Subject: [PATCH 922/994] `IndexFile._to_relative_path` - fix case where
 absolute path gets stripped of trailing slash

---
 git/index/base.py  |  5 ++++-
 test/test_index.py | 22 ++++++++++++++++++++++
 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/git/index/base.py b/git/index/base.py
index 39cc9143c..65b1f9308 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -655,7 +655,10 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
             raise InvalidGitRepositoryError("require non-bare repository")
         if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
             raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
-        return os.path.relpath(path, self.repo.working_tree_dir)
+        result = os.path.relpath(path, self.repo.working_tree_dir)
+        if str(path).endswith(os.sep) and not result.endswith(os.sep):
+            result += os.sep
+        return result
 
     def _preprocess_add_items(
         self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
diff --git a/test/test_index.py b/test/test_index.py
index c586a0b5a..c42032e70 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -16,6 +16,7 @@
 import subprocess
 import sys
 import tempfile
+from unittest import mock
 
 from gitdb.base import IStream
 
@@ -1015,6 +1016,27 @@ class Mocked:
         rel = index._to_relative_path(path)
         self.assertEqual(rel, os.path.relpath(path, root))
 
+    def test__to_relative_path_absolute_trailing_slash(self):
+        repo_root = os.path.join(osp.abspath(os.sep), "directory1", "repo_root")
+
+        class Mocked:
+            bare = False
+            git_dir = repo_root
+            working_tree_dir = repo_root
+
+        repo = Mocked()
+        path = os.path.join(repo_root, f"directory2{os.sep}")
+        index = IndexFile(repo)
+
+        expected_path = f"directory2{os.sep}"
+        actual_path = index._to_relative_path(path)
+        self.assertEqual(expected_path, actual_path)
+
+        with mock.patch("git.index.base.os.path") as ospath_mock:
+            ospath_mock.relpath.return_value = f"directory2{os.sep}"
+            actual_path = index._to_relative_path(path)
+            self.assertEqual(expected_path, actual_path)
+
     @pytest.mark.xfail(
         type(_win_bash_status) is WinBashStatus.Absent,
         reason="Can't run a hook on Windows without bash.exe.",

From 94151aa2ca9f16491a0cf2344b4daa8bf7b41d70 Mon Sep 17 00:00:00 2001
From: Andrej730 <azhilenkov@gmail.com>
Date: Wed, 19 Mar 2025 13:17:46 +0500
Subject: [PATCH 923/994] Use property decorator to support typing

---
 git/refs/symbolic.py | 41 ++++++++++++++++++++++++-----------------
 git/repo/base.py     | 40 +++++++++++++++++++++-------------------
 2 files changed, 45 insertions(+), 36 deletions(-)

diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 510850b2e..1b90a3115 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -39,7 +39,6 @@
 if TYPE_CHECKING:
     from git.config import GitConfigParser
     from git.objects.commit import Actor
-    from git.refs import Head, TagReference, RemoteReference, Reference
     from git.refs.log import RefLogEntry
     from git.repo import Repo
 
@@ -387,17 +386,23 @@ def set_object(
         # set the commit on our reference
         return self._get_reference().set_object(object, logmsg)
 
-    commit = property(
-        _get_commit,
-        set_commit,  # type: ignore[arg-type]
-        doc="Query or set commits directly",
-    )
+    @property
+    def commit(self) -> "Commit":
+        """Query or set commits directly"""
+        return self._get_commit()
+
+    @commit.setter
+    def commit(self, commit: Union[Commit, "SymbolicReference", str]) -> "SymbolicReference":
+        return self.set_commit(commit)
+
+    @property
+    def object(self) -> AnyGitObject:
+        """Return the object our ref currently refers to"""
+        return self._get_object()
 
-    object = property(
-        _get_object,
-        set_object,  # type: ignore[arg-type]
-        doc="Return the object our ref currently refers to",
-    )
+    @object.setter
+    def object(self, object: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference":
+        return self.set_object(object)
 
     def _get_reference(self) -> "SymbolicReference":
         """
@@ -496,12 +501,14 @@ def set_reference(
         return self
 
     # Aliased reference
-    reference: Union["Head", "TagReference", "RemoteReference", "Reference"]
-    reference = property(  # type: ignore[assignment]
-        _get_reference,
-        set_reference,  # type: ignore[arg-type]
-        doc="Returns the Reference we point to",
-    )
+    @property
+    def reference(self) -> "SymbolicReference":
+        return self._get_reference()
+
+    @reference.setter
+    def reference(self, ref: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference":
+        return self.set_reference(ref)
+
     ref = reference
 
     def is_valid(self) -> bool:
diff --git a/git/repo/base.py b/git/repo/base.py
index db89cdf41..cbf54f222 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -354,21 +354,19 @@ def __ne__(self, rhs: object) -> bool:
     def __hash__(self) -> int:
         return hash(self.git_dir)
 
-    # Description property
-    def _get_description(self) -> str:
+    @property
+    def description(self) -> str:
+        """The project's description"""
         filename = osp.join(self.git_dir, "description")
         with open(filename, "rb") as fp:
             return fp.read().rstrip().decode(defenc)
 
-    def _set_description(self, descr: str) -> None:
+    @description.setter
+    def description(self, descr: str) -> None:
         filename = osp.join(self.git_dir, "description")
         with open(filename, "wb") as fp:
             fp.write((descr + "\n").encode(defenc))
 
-    description = property(_get_description, _set_description, doc="the project's description")
-    del _get_description
-    del _set_description
-
     @property
     def working_tree_dir(self) -> Optional[PathLike]:
         """
@@ -885,13 +883,14 @@ def _set_daemon_export(self, value: object) -> None:
         elif not value and fileexists:
             os.unlink(filename)
 
-    daemon_export = property(
-        _get_daemon_export,
-        _set_daemon_export,
-        doc="If True, git-daemon may export this repository",
-    )
-    del _get_daemon_export
-    del _set_daemon_export
+    @property
+    def daemon_export(self) -> bool:
+        """If True, git-daemon may export this repository"""
+        return self._get_daemon_export()
+
+    @daemon_export.setter
+    def daemon_export(self, value: object) -> None:
+        self._set_daemon_export(value)
 
     def _get_alternates(self) -> List[str]:
         """The list of alternates for this repo from which objects can be retrieved.
@@ -929,11 +928,14 @@ def _set_alternates(self, alts: List[str]) -> None:
             with open(alternates_path, "wb") as f:
                 f.write("\n".join(alts).encode(defenc))
 
-    alternates = property(
-        _get_alternates,
-        _set_alternates,
-        doc="Retrieve a list of alternates paths or set a list paths to be used as alternates",
-    )
+    @property
+    def alternates(self) -> List[str]:
+        """Retrieve a list of alternates paths or set a list paths to be used as alternates"""
+        return self._get_alternates()
+
+    @alternates.setter
+    def alternates(self, alts: List[str]) -> None:
+        self._set_alternates(alts)
 
     def is_dirty(
         self,

From 3d979a6c307b2a03f08d4bdbc7fb3716d8c17c94 Mon Sep 17 00:00:00 2001
From: Yuki Kobayashi <drsuaimqjgar@gmail.com>
Date: Tue, 18 Mar 2025 08:22:04 +0000
Subject: [PATCH 924/994] Fix some incorrect sphinx markups in the docstrings

Fixed some markups so
[the api reference](https://gitpython.readthedocs.io/en/stable/reference.html)
is rendered correctly.
---
 git/index/base.py     | 10 +++++-----
 git/objects/base.py   |  4 ++--
 git/objects/commit.py |  4 ++--
 git/repo/base.py      |  2 +-
 4 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index 65b1f9308..a95762dca 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -508,7 +508,7 @@ def iter_blobs(
 
         :param predicate:
             Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
-            the iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you
+            the iterator. A default filter, the :class:`~git.index.typ.BlobFilter`, allows you
             to yield blobs only if they match a given list of paths.
         """
         for entry in self.entries.values():
@@ -770,7 +770,7 @@ def add(
             - path string
 
                 Strings denote a relative or absolute path into the repository pointing
-                to an existing file, e.g., ``CHANGES``, `lib/myfile.ext``,
+                to an existing file, e.g., ``CHANGES``, ``lib/myfile.ext``,
                 ``/home/gitrepo/lib/myfile.ext``.
 
                 Absolute paths must start with working tree directory of this index's
@@ -789,7 +789,7 @@ def add(
 
                 They are added at stage 0.
 
-            - :class:~`git.objects.blob.Blob` or
+            - :class:`~git.objects.blob.Blob` or
               :class:`~git.objects.submodule.base.Submodule` object
 
                 Blobs are added as they are assuming a valid mode is set.
@@ -815,7 +815,7 @@ def add(
 
             - :class:`~git.index.typ.BaseIndexEntry` or type
 
-                Handling equals the one of :class:~`git.objects.blob.Blob` objects, but
+                Handling equals the one of :class:`~git.objects.blob.Blob` objects, but
                 the stage may be explicitly set. Please note that Index Entries require
                 binary sha's.
 
@@ -998,7 +998,7 @@ def remove(
 
                 The path string may include globs, such as ``*.c``.
 
-            - :class:~`git.objects.blob.Blob` object
+            - :class:`~git.objects.blob.Blob` object
 
                 Only the path portion is used in this case.
 
diff --git a/git/objects/base.py b/git/objects/base.py
index eeaebc09b..faf600c6b 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -122,7 +122,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
         :return:
             New :class:`Object` instance of a type appropriate to the object type behind
             `id`. The id of the newly created object will be a binsha even though the
-            input id may have been a `~git.refs.reference.Reference` or rev-spec.
+            input id may have been a :class:`~git.refs.reference.Reference` or rev-spec.
 
         :param id:
             :class:`~git.refs.reference.Reference`, rev-spec, or hexsha.
@@ -218,7 +218,7 @@ class IndexObject(Object):
     """Base for all objects that can be part of the index file.
 
     The classes representing git object types that can be part of the index file are
-    :class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition,
+    :class:`~git.objects.tree.Tree` and :class:`~git.objects.blob.Blob`. In addition,
     :class:`~git.objects.submodule.base.Submodule`, which is not really a git object
     type but can be part of an index file, is also a subclass.
     """
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 0ceb46609..fbe0ee9c0 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -289,7 +289,7 @@ def name_rev(self) -> str:
         """
         :return:
             String describing the commits hex sha based on the closest
-            `~git.refs.reference.Reference`.
+            :class:`~git.refs.reference.Reference`.
 
         :note:
             Mostly useful for UI purposes.
@@ -349,7 +349,7 @@ def iter_items(
         return cls._iter_from_process_or_stream(repo, proc)
 
     def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> Iterator["Commit"]:
-        R"""Iterate _all_ parents of this commit.
+        R"""Iterate *all* parents of this commit.
 
         :param paths:
             Optional path or list of paths limiting the :class:`Commit`\s to those that
diff --git a/git/repo/base.py b/git/repo/base.py
index cbf54f222..7e918df8c 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -512,7 +512,7 @@ def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
     def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
         """An iterator yielding Submodule instances.
 
-        See the `~git.objects.util.Traversable` interface for a description of `args`
+        See the :class:`~git.objects.util.Traversable` interface for a description of `args`
         and `kwargs`.
 
         :return:

From 1abb399b90994f61b81c1aa4be608a85b07b73c7 Mon Sep 17 00:00:00 2001
From: Nathan Goldbaum <nathan.goldbaum@gmail.com>
Date: Tue, 25 Mar 2025 09:20:48 -0600
Subject: [PATCH 925/994] replace quansight-labs/setup-python with
 actions/setup-python

---
 .github/workflows/pythonpackage.yml | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 5bedb6107..61088237d 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -44,20 +44,11 @@ jobs:
         fetch-depth: 0
 
     - name: Set up Python ${{ matrix.python-version }}
-      if: |-
-        !endsWith(matrix.python-version, 't')
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
         allow-prereleases: ${{ matrix.experimental }}
 
-    - name: Set up Python ${{ matrix.python-version }} (free-threaded)
-      if: endsWith(matrix.python-version, 't')
-      uses: Quansight-Labs/setup-python@v5
-      with:
-        python-version: ${{ matrix.python-version }}
-        allow-prereleases: ${{ matrix.experimental }}
-
     - name: Set up WSL (Windows)
       if: matrix.os-type == 'windows'
       uses: Vampire/setup-wsl@v5.0.0

From f2483a6151a99b7326b657fd945ec75f891e6462 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 May 2025 13:53:04 +0000
Subject: [PATCH 926/994] Bump Vampire/setup-wsl from 5.0.0 to 5.0.1

Bumps [Vampire/setup-wsl](https://github.com/vampire/setup-wsl) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/vampire/setup-wsl/releases)
- [Commits](https://github.com/vampire/setup-wsl/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: Vampire/setup-wsl
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .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 61088237d..9fd660c6b 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.0
+      uses: Vampire/setup-wsl@v5.0.1
       with:
         wsl-version: 1
         distribution: Alpine

From ec0087227ed37cf1df441e4f0f73daa25895c4b5 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 21 May 2025 15:48:34 -0400
Subject: [PATCH 927/994] add tests for is_cygwin_git

---
 test/test_util.py | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/test/test_util.py b/test/test_util.py
index dad2f3dcd..ed57dcbae 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -34,6 +34,7 @@
     LockFile,
     cygpath,
     decygpath,
+    is_cygwin_git,
     get_user_id,
     remove_password_if_present,
     rmtree,
@@ -349,6 +350,24 @@ def test_decygpath(self, wpath, cpath):
         assert wcpath == wpath.replace("/", "\\"), cpath
 
 
+class TestIsCygwinGit:
+    """Tests for :func:`is_cygwin_git`"""
+
+    def test_on_path_executable(self):
+        match sys.platform:
+            case "cygwin":
+                assert is_cygwin_git("git")
+            case _:
+                assert not is_cygwin_git("git")
+
+    def test_none_executable(self):
+        assert not is_cygwin_git(None)
+
+    def test_with_missing_uname(self):
+        """Test for handling when `uname` isn't in the same directory as `git`"""
+        assert not is_cygwin_git("/bogus_path/git")
+
+
 class _Member:
     """A member of an IterableList."""
 

From de5e57caf26e5f6772ac02a3944e58e0aba177f3 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 21 May 2025 15:49:07 -0400
Subject: [PATCH 928/994] check for the existence/execute bit on the uname
 command before trying to run it

---
 git/util.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/git/util.py b/git/util.py
index 9e8ac821d..f1a94c469 100644
--- a/git/util.py
+++ b/git/util.py
@@ -463,7 +463,13 @@ def _is_cygwin_git(git_executable: str) -> bool:
                 git_dir = osp.dirname(res[0]) if res else ""
 
             # Just a name given, not a real path.
+            # Let's see if the same path has uname
             uname_cmd = osp.join(git_dir, "uname")
+            if not (osp.isfile(uname_cmd) and os.access(uname_cmd, os.X_OK)):
+                _logger.debug(f"File {uname_cmd} either does not exist or is not executable.")
+                _is_cygwin_cache[git_executable] = is_cygwin
+                return is_cygwin
+
             process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
             uname_out, _ = process.communicate()
             # retcode = process.poll()

From 428be1a504e2b30c195786d5ca6708f4c39d90a7 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 21 May 2025 15:51:29 -0400
Subject: [PATCH 929/994] adding self to authors

---
 AUTHORS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/AUTHORS b/AUTHORS
index 45b14c961..b57113edd 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -55,5 +55,6 @@ Contributors are:
 -Eliah Kagan <eliah.kagan _at_ gmail.com>
 -Ethan Lin <et.repositories _at_ gmail.com>
 -Jonas Scharpf <jonas.scharpf _at_ checkmk.com>
+-Gordon Marx
 
 Portions derived from other open source works and are clearly marked.

From 58ff723e6757af3c508e2a9424d287e5016ac230 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 08:54:42 -0400
Subject: [PATCH 930/994] Revert "check for the existence/execute bit on the
 uname command before trying to run it"

This reverts commit de5e57caf26e5f6772ac02a3944e58e0aba177f3.
---
 git/util.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/git/util.py b/git/util.py
index f1a94c469..9e8ac821d 100644
--- a/git/util.py
+++ b/git/util.py
@@ -463,13 +463,7 @@ def _is_cygwin_git(git_executable: str) -> bool:
                 git_dir = osp.dirname(res[0]) if res else ""
 
             # Just a name given, not a real path.
-            # Let's see if the same path has uname
             uname_cmd = osp.join(git_dir, "uname")
-            if not (osp.isfile(uname_cmd) and os.access(uname_cmd, os.X_OK)):
-                _logger.debug(f"File {uname_cmd} either does not exist or is not executable.")
-                _is_cygwin_cache[git_executable] = is_cygwin
-                return is_cygwin
-
             process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
             uname_out, _ = process.communicate()
             # retcode = process.poll()

From 71879840b8a72890f63fcadf7a92694106610ef0 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 09:05:13 -0400
Subject: [PATCH 931/994] use pathlib.Path instead of os.path.isfile

---
 git/util.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/git/util.py b/git/util.py
index 9e8ac821d..3b6917fc4 100644
--- a/git/util.py
+++ b/git/util.py
@@ -464,6 +464,12 @@ def _is_cygwin_git(git_executable: str) -> bool:
 
             # Just a name given, not a real path.
             uname_cmd = osp.join(git_dir, "uname")
+
+            if not (pathlib.Path(uname_cmd).exists() and os.access(uname_cmd, os.X_OK)):
+                _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
+                _is_cygwin_cache[git_executable] = is_cygwin
+                return is_cygwin
+
             process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
             uname_out, _ = process.communicate()
             # retcode = process.poll()

From 3f5a942be8293bb8c663631756885f877afc22e7 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 09:06:02 -0400
Subject: [PATCH 932/994] don't keep checking if sys.platform isn't 'cygwin'

---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 3b6917fc4..989f9196c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -490,7 +490,7 @@ def is_cygwin_git(git_executable: PathLike) -> bool: ...
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if sys.platform == "win32":  # TODO: See if we can use `sys.platform != "cygwin"`.
+    if sys.platform != "cygwin":
         return False
     elif git_executable is None:
         return False

From 226f4ffcaf2deed4598b9ff53902a12f6483c069 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 09:15:50 -0400
Subject: [PATCH 933/994] check that uname_cmd points to a file; if uname_cmd
 were a directory, it could both exist and have os.X_OK but not work

---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 989f9196c..32a808c60 100644
--- a/git/util.py
+++ b/git/util.py
@@ -465,7 +465,7 @@ def _is_cygwin_git(git_executable: str) -> bool:
             # Just a name given, not a real path.
             uname_cmd = osp.join(git_dir, "uname")
 
-            if not (pathlib.Path(uname_cmd).exists() and os.access(uname_cmd, os.X_OK)):
+            if not (pathlib.Path(uname_cmd).isfile() and os.access(uname_cmd, os.X_OK)):
                 _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
                 _is_cygwin_cache[git_executable] = is_cygwin
                 return is_cygwin

From 22d6284b099e99716da101198ce113c96e35423a Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 09:24:37 -0400
Subject: [PATCH 934/994] don't use match-case, since it's a 3.10 feature

---
 test/test_util.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/test/test_util.py b/test/test_util.py
index ed57dcbae..de80ca3af 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -354,11 +354,10 @@ class TestIsCygwinGit:
     """Tests for :func:`is_cygwin_git`"""
 
     def test_on_path_executable(self):
-        match sys.platform:
-            case "cygwin":
-                assert is_cygwin_git("git")
-            case _:
-                assert not is_cygwin_git("git")
+        if sys.platform == "cygwin":
+            assert is_cygwin_git("git")
+        else:
+            assert not is_cygwin_git("git")
 
     def test_none_executable(self):
         assert not is_cygwin_git(None)

From b1289eeb3676e63f4261518655ee90808e9d3d1f Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 10:56:26 -0400
Subject: [PATCH 935/994] it's is_file(), not isfile()

---
 git/util.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 32a808c60..4071fdc14 100644
--- a/git/util.py
+++ b/git/util.py
@@ -465,7 +465,7 @@ def _is_cygwin_git(git_executable: str) -> bool:
             # Just a name given, not a real path.
             uname_cmd = osp.join(git_dir, "uname")
 
-            if not (pathlib.Path(uname_cmd).isfile() and os.access(uname_cmd, os.X_OK)):
+            if not (pathlib.Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
                 _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
                 _is_cygwin_cache[git_executable] = is_cygwin
                 return is_cygwin
@@ -490,6 +490,7 @@ def is_cygwin_git(git_executable: PathLike) -> bool: ...
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
+    _logger.debug(f"{sys.platform=}, {git_executable=}")
     if sys.platform != "cygwin":
         return False
     elif git_executable is None:

From cffa264bdc1dd09a13a85cc27417b85628273e14 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 11:11:48 -0400
Subject: [PATCH 936/994] turns out f-strings before 3.8 don't support
 {variable=} notation, take that out

---
 git/util.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 4071fdc14..66175ea33 100644
--- a/git/util.py
+++ b/git/util.py
@@ -490,7 +490,7 @@ def is_cygwin_git(git_executable: PathLike) -> bool: ...
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    _logger.debug(f"{sys.platform=}, {git_executable=}")
+    _logger.debug(f"sys.platform = {sys.platform}, git_executable = {git_executable}")
     if sys.platform != "cygwin":
         return False
     elif git_executable is None:

From 8dc75521bd42db19d41e8d92ce5204dfe1a594ac Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Thu, 22 May 2025 15:48:54 -0400
Subject: [PATCH 937/994] remove type assertions from util.py

---
 git/util.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/git/util.py b/git/util.py
index 9e8ac821d..0d09c3f09 100644
--- a/git/util.py
+++ b/git/util.py
@@ -1200,8 +1200,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[override]
-        assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str"
-
         if isinstance(index, int):
             return list.__getitem__(self, index)
         elif isinstance(index, slice):
@@ -1214,8 +1212,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)
         if not isinstance(index, int):
             delindex = -1

From 36a893bbd47a08df80e0266f6ce5182915511833 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Fri, 23 May 2025 07:24:43 -0400
Subject: [PATCH 938/994] add self to AUTHORS, add Emacs tempfiles to
 .gitignore

---
 .gitignore | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitignore b/.gitignore
index d85569405..eab294a65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,8 @@ __pycache__/
 # Transient editor files
 *.swp
 *~
+\#*#
+.#*#
 
 # Editor configuration
 nbproject

From c441316b7714207eaf2c13be5104d15ec69943bd Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Tue, 27 May 2025 10:58:24 -0400
Subject: [PATCH 939/994] use `strings` instead of `uname` to detect cygwin

---
 git/util.py | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)

diff --git a/git/util.py b/git/util.py
index 66175ea33..53475eeb3 100644
--- a/git/util.py
+++ b/git/util.py
@@ -457,23 +457,24 @@ def _is_cygwin_git(git_executable: str) -> bool:
     if is_cygwin is None:
         is_cygwin = False
         try:
-            git_dir = osp.dirname(git_executable)
+            git_cmd = pathlib.Path(git_executable)
+            git_dir = git_cmd.parent
+
             if not git_dir:
                 res = py_where(git_executable)
-                git_dir = osp.dirname(res[0]) if res else ""
+                git_dir = pathlib.Path(res[0]).parent if res else ""
 
-            # Just a name given, not a real path.
-            uname_cmd = osp.join(git_dir, "uname")
+            # If it's a cygwin git, it'll have cygwin in the output of `strings git`
+            strings_cmd = pathlib.Path(git_dir, "strings")
 
-            if not (pathlib.Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
-                _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
-                _is_cygwin_cache[git_executable] = is_cygwin
+            if not (pathlib.Path(strings_cmd).is_file() and os.access(strings_cmd, os.X_OK)):
+                _logger.debug(f"Failed checking if running in CYGWIN: {strings_cmd} is not an executable")
+                _is_cygwin_cache[git_executable] = False
                 return is_cygwin
 
-            process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
-            uname_out, _ = process.communicate()
-            # retcode = process.poll()
-            is_cygwin = "CYGWIN" in uname_out
+            process = subprocess.Popen([strings_cmd, git_cmd], stdout=subprocess.PIPE, text=True)
+            strings_output, _ = process.communicate()
+            is_cygwin = any(x for x in strings_output if "cygwin" in x.lower())
         except Exception as ex:
             _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
         _is_cygwin_cache[git_executable] = is_cygwin

From 0df08181f1012f0e591f998dcc57fd594e754d98 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Tue, 27 May 2025 11:32:23 -0400
Subject: [PATCH 940/994] debug printing

---
 git/util.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/git/util.py b/git/util.py
index 53475eeb3..81761f15c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -475,6 +475,8 @@ def _is_cygwin_git(git_executable: str) -> bool:
             process = subprocess.Popen([strings_cmd, git_cmd], stdout=subprocess.PIPE, text=True)
             strings_output, _ = process.communicate()
             is_cygwin = any(x for x in strings_output if "cygwin" in x.lower())
+            if not is_cygwin:
+                _logger.debug(f"is not cygwin: {strings_output}")
         except Exception as ex:
             _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
         _is_cygwin_cache[git_executable] = is_cygwin

From 58f77105f5163408a9bf323518b96646ab413561 Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 28 May 2025 11:15:06 -0400
Subject: [PATCH 941/994] Revert "debug printing"

This reverts commit 0df08181f1012f0e591f998dcc57fd594e754d98.
---
 git/util.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/git/util.py b/git/util.py
index 81761f15c..53475eeb3 100644
--- a/git/util.py
+++ b/git/util.py
@@ -475,8 +475,6 @@ def _is_cygwin_git(git_executable: str) -> bool:
             process = subprocess.Popen([strings_cmd, git_cmd], stdout=subprocess.PIPE, text=True)
             strings_output, _ = process.communicate()
             is_cygwin = any(x for x in strings_output if "cygwin" in x.lower())
-            if not is_cygwin:
-                _logger.debug(f"is not cygwin: {strings_output}")
         except Exception as ex:
             _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
         _is_cygwin_cache[git_executable] = is_cygwin

From 1731c1e377aca056d1a7acf2dd6df0e68a4e9e7e Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 28 May 2025 11:15:23 -0400
Subject: [PATCH 942/994] Revert "use `strings` instead of `uname` to detect
 cygwin"

This reverts commit c441316b7714207eaf2c13be5104d15ec69943bd.
---
 git/util.py | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/git/util.py b/git/util.py
index 53475eeb3..66175ea33 100644
--- a/git/util.py
+++ b/git/util.py
@@ -457,24 +457,23 @@ def _is_cygwin_git(git_executable: str) -> bool:
     if is_cygwin is None:
         is_cygwin = False
         try:
-            git_cmd = pathlib.Path(git_executable)
-            git_dir = git_cmd.parent
-
+            git_dir = osp.dirname(git_executable)
             if not git_dir:
                 res = py_where(git_executable)
-                git_dir = pathlib.Path(res[0]).parent if res else ""
+                git_dir = osp.dirname(res[0]) if res else ""
 
-            # If it's a cygwin git, it'll have cygwin in the output of `strings git`
-            strings_cmd = pathlib.Path(git_dir, "strings")
+            # Just a name given, not a real path.
+            uname_cmd = osp.join(git_dir, "uname")
 
-            if not (pathlib.Path(strings_cmd).is_file() and os.access(strings_cmd, os.X_OK)):
-                _logger.debug(f"Failed checking if running in CYGWIN: {strings_cmd} is not an executable")
-                _is_cygwin_cache[git_executable] = False
+            if not (pathlib.Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
+                _logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
+                _is_cygwin_cache[git_executable] = is_cygwin
                 return is_cygwin
 
-            process = subprocess.Popen([strings_cmd, git_cmd], stdout=subprocess.PIPE, text=True)
-            strings_output, _ = process.communicate()
-            is_cygwin = any(x for x in strings_output if "cygwin" in x.lower())
+            process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
+            uname_out, _ = process.communicate()
+            # retcode = process.poll()
+            is_cygwin = "CYGWIN" in uname_out
         except Exception as ex:
             _logger.debug("Failed checking if running in CYGWIN due to: %r", ex)
         _is_cygwin_cache[git_executable] = is_cygwin

From f3ab5d3810b1e8c1f0365d26eea3e91808d8e4af Mon Sep 17 00:00:00 2001
From: Gordon Marx <gcmarx@gmail.com>
Date: Wed, 28 May 2025 11:21:28 -0400
Subject: [PATCH 943/994] incorporate review feedback from @EliahKagan

---
 git/util.py       | 3 ++-
 test/test_util.py | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/git/util.py b/git/util.py
index 66175ea33..a10609185 100644
--- a/git/util.py
+++ b/git/util.py
@@ -490,7 +490,8 @@ def is_cygwin_git(git_executable: PathLike) -> bool: ...
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    _logger.debug(f"sys.platform = {sys.platform}, git_executable = {git_executable}")
+    # TODO: when py3.7 support is dropped, use the new interpolation f"{variable=}"
+    _logger.debug(f"sys.platform={sys.platform!r}, git_executable={git_executable!r}")
     if sys.platform != "cygwin":
         return False
     elif git_executable is None:
diff --git a/test/test_util.py b/test/test_util.py
index de80ca3af..000830f41 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -354,6 +354,7 @@ class TestIsCygwinGit:
     """Tests for :func:`is_cygwin_git`"""
 
     def test_on_path_executable(self):
+        # Currently we assume tests run on Cygwin use Cygwin git. See #533 and #1455 for background.
         if sys.platform == "cygwin":
             assert is_cygwin_git("git")
         else:

From b7ce712c631c0b59566af43e7a4b00cc1dbeba3b Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 12:36:37 -0400
Subject: [PATCH 944/994] Use newer ruff style

This updates `ruff` in `.pre-commit-config.yaml` from 0.6.0 to
0.11.12, changes its `id` from the legacy `ruff` alias to
`ruff-check` (which is better distinguished from `ruff-format`,
which we also have a hook for), and applies the few style changes
it newly recommends throughout the code. The style changes seem to
make things slightly clearer overall.

This also updates some other pre-commit hooks, but those don't
require any changes to the code.

Currently the `ruff` dependency in `requirements-dev.txt` doesn't
specify a version, so no change is needed there. This update may
be seen as bringing the `pre-commit` version in line with what
users will usually have locally with `pip install -e ".[test]"`.

The `pre-commit` hooks are how linting is currently done on CI, so
this is updating `ruff` for CI. That's the most significant effect
of this change. (`pre-commit` is run for linting on CI probably
much more often than it is used locally, to manage pre-commit
hooks or otherwise, in GitPython development.)
---
 .pre-commit-config.yaml | 10 +++++-----
 git/cmd.py              |  4 ++--
 git/refs/log.py         |  2 +-
 git/repo/fun.py         |  2 +-
 test/test_git.py        |  2 +-
 test/test_quick_doc.py  |  2 +-
 6 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 424cc5f37..737b56d45 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,15 +1,15 @@
 repos:
 - repo: https://github.com/codespell-project/codespell
-  rev: v2.3.0
+  rev: v2.4.1
   hooks:
   - id: codespell
     additional_dependencies: [tomli]
     exclude: ^test/fixtures/
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.6.0
+  rev: v0.11.12
   hooks:
-  - id: ruff
+  - id: ruff-check
     args: ["--fix"]
     exclude: ^git/ext/
   - id: ruff-format
@@ -23,7 +23,7 @@ repos:
     exclude: ^test/fixtures/polyglot$|^git/ext/
 
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.6.0
+  rev: v5.0.0
   hooks:
   - id: end-of-file-fixer
     exclude: ^test/fixtures/|COPYING|LICENSE
@@ -33,6 +33,6 @@ repos:
   - id: check-merge-conflict
 
 - repo: https://github.com/abravalheri/validate-pyproject
-  rev: v0.19
+  rev: v0.24.1
   hooks:
   - id: validate-pyproject
diff --git a/git/cmd.py b/git/cmd.py
index 2048a43fa..7f46edc8f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -207,7 +207,7 @@ def pump_stream(
                 )
             if stderr_handler:
                 error_str: Union[str, bytes] = (
-                    "error: process killed because it timed out." f" kill_after_timeout={kill_after_timeout} seconds"
+                    f"error: process killed because it timed out. kill_after_timeout={kill_after_timeout} seconds"
                 )
                 if not decode_streams and isinstance(p_stderr, BinaryIO):
                     # Assume stderr_handler needs binary input.
@@ -1319,7 +1319,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
                 out, err = proc.communicate()
                 watchdog.cancel()
                 if kill_check.is_set():
-                    err = 'Timeout: the command "%s" did not complete in %d ' "secs." % (
+                    err = 'Timeout: the command "%s" did not complete in %d secs.' % (
                         " ".join(redacted_command),
                         timeout,
                     )
diff --git a/git/refs/log.py b/git/refs/log.py
index 17e3a94b3..8f2f2cd38 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -126,7 +126,7 @@ def from_line(cls, line: bytes) -> "RefLogEntry":
         elif len(fields) == 2:
             info, msg = fields
         else:
-            raise ValueError("Line must have up to two TAB-separated fields." " Got %s" % repr(line_str))
+            raise ValueError("Line must have up to two TAB-separated fields. Got %s" % repr(line_str))
         # END handle first split
 
         oldhexsha = info[:40]
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 125ba5936..1c995c6c6 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -405,7 +405,7 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
             # END end handle tag
         except (IndexError, AttributeError) as e:
             raise BadName(
-                f"Invalid revision spec '{rev}' - not enough " f"parent commits to reach '{token}{int(num)}'"
+                f"Invalid revision spec '{rev}' - not enough parent commits to reach '{token}{int(num)}'"
             ) from e
         # END exception handling
     # END parse loop
diff --git a/test/test_git.py b/test/test_git.py
index 274511f8d..5bcf89bdd 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -747,7 +747,7 @@ def test_environment(self, rw_dir):
 
         path = osp.join(rw_dir, "failing-script.sh")
         with open(path, "wt") as stream:
-            stream.write("#!/usr/bin/env sh\n" "echo FOO\n")
+            stream.write("#!/usr/bin/env sh\necho FOO\n")
         os.chmod(path, 0o777)
 
         rw_repo = Repo.init(osp.join(rw_dir, "repo"))
diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py
index 4ef75f4aa..98658e02f 100644
--- a/test/test_quick_doc.py
+++ b/test/test_quick_doc.py
@@ -173,7 +173,7 @@ def test_cloned_repo_object(self, local_dir):
         # [15-test_cloned_repo_object]
         def print_files_from_git(root, level=0):
             for entry in root:
-                print(f'{"-" * 4 * level}| {entry.path}, {entry.type}')
+                print(f"{'-' * 4 * level}| {entry.path}, {entry.type}")
                 if entry.type == "tree":
                     print_files_from_git(entry, level + 1)
 

From 0c0fc7ee97c14088ef1705d756e04dadd9cfdd8a Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 14:37:03 -0400
Subject: [PATCH 945/994] Temporarily remove CodeQL workflow file

So GitHub can regenerate a fresh new one based on current defaults.
---
 .github/workflows/codeql.yml | 80 ------------------------------------
 1 file changed, 80 deletions(-)
 delete mode 100644 .github/workflows/codeql.yml

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index ae5241898..000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
-  push:
-  pull_request:
-  schedule:
-    - cron: '27 10 * * 3'
-
-jobs:
-  analyze:
-    name: Analyze
-    # Runner size impacts CodeQL analysis time. To learn more, please see:
-    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
-    #   - https://gh.io/supported-runners-and-hardware-resources
-    #   - https://gh.io/using-larger-runners
-    # Consider using larger runners for possible analysis time improvements.
-    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
-    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
-    permissions:
-      actions: read
-      contents: read
-      security-events: write
-
-    strategy:
-      fail-fast: false
-      matrix:
-        language: [ 'python' ]
-        # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
-        # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
-        # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
-        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@v4
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v3
-      with:
-        languages: ${{ matrix.language }}
-        setup-python-dependencies: false
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-
-        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
-        # queries: security-extended,security-and-quality
-
-
-    # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v3
-
-    # ℹ️ Command-line programs to run using the OS shell.
-    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
-    #   If the Autobuild fails above, remove it and uncomment the following three lines.
-    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
-    # - run: |
-    #     echo "Run, Build Application using script"
-    #     ./location_of_script_within_repo/buildscript.sh
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v3
-      with:
-        category: "/language:${{matrix.language}}"

From 847ead6948efa5d33a8798395220ed6b719a56f2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 14:40:38 -0400
Subject: [PATCH 946/994] Recreate `codeq.yml` with current defaults

This adds CodeQL scanning of GitHub Actions, while continuing to
scan Python as well.

This will subsequently be customized slightly to restore some
elements of the preivous custom workflow that we may prefer.
---
 .github/workflows/codeql.yml | 100 +++++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)
 create mode 100644 .github/workflows/codeql.yml

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..450166503
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,100 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL Advanced"
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+  schedule:
+    - cron: '36 18 * * 0'
+
+jobs:
+  analyze:
+    name: Analyze (${{ matrix.language }})
+    # Runner size impacts CodeQL analysis time. To learn more, please see:
+    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
+    #   - https://gh.io/supported-runners-and-hardware-resources
+    #   - https://gh.io/using-larger-runners (GitHub.com only)
+    # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+    permissions:
+      # required for all workflows
+      security-events: write
+
+      # required to fetch internal or private CodeQL packs
+      packages: read
+
+      # only required for workflows in private repositories
+      actions: read
+      contents: read
+
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+        - language: actions
+          build-mode: none
+        - language: python
+          build-mode: none
+        # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
+        # Use `c-cpp` to analyze code written in C, C++ or both
+        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+        # 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
+
+    # Add any setup steps before running the `github/codeql-action/init` action.
+    # This includes steps like installing compilers or runtimes (`actions/setup-node`
+    # or others). This is typically only required for manual builds.
+    # - name: Setup runtime (example)
+    #   uses: actions/setup-example@v1
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v3
+      with:
+        languages: ${{ matrix.language }}
+        build-mode: ${{ matrix.build-mode }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+
+        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+    # If the analyze step fails for one of the languages you are analyzing with
+    # "We were unable to automatically build your code", modify the matrix above
+    # to set the build mode to "manual" for that language. Then modify this step
+    # to build your code.
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+    - if: matrix.build-mode == 'manual'
+      shell: bash
+      run: |
+        echo 'If you are using a "manual" build mode for one or more of the' \
+          'languages you are analyzing, replace this with the commands to build' \
+          'your code, for example:'
+        echo '  make bootstrap'
+        echo '  make release'
+        exit 1
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v3
+      with:
+        category: "/language:${{matrix.language}}"

From d7ce09fa75a7a86ed089ecff330ee86333df5200 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 14:51:21 -0400
Subject: [PATCH 947/994] Keep running CodeQL on all branches, etc.

This restores three aspects of the previous `codeql.yml`:

- Run it on all branches, not just `main`.

- Run it on the previous schedule rather than the new one, since
  there's no reason to change the schedule (though there's no
  reason to be attached to the old schedule either).

- Use "CodeQL" rather than "CodeQL Advanced" as the workflow
  `name`, since this takes up less horizontal space when reading
  the reports from the checks.

Of these, only the first is really significant.
---
 .github/workflows/codeql.yml | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 450166503..3d273f9c7 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -9,15 +9,13 @@
 # the `language` matrix defined below to confirm you have the correct set of
 # supported CodeQL languages.
 #
-name: "CodeQL Advanced"
+name: "CodeQL"
 
 on:
   push:
-    branches: [ "main" ]
   pull_request:
-    branches: [ "main" ]
   schedule:
-    - cron: '36 18 * * 0'
+    - cron: '27 10 * * 3'
 
 jobs:
   analyze:

From 89dbd4a85ae78c272e5714e827f70783df04f924 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 14:58:09 -0400
Subject: [PATCH 948/994] Remove unnecessary permissions from `codeql.yml`

This is another change back to the way we had it before, but the
removals are based specifically on the guidance in the default
workflow comments about why each permission was given by default.
---
 .github/workflows/codeql.yml | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 3d273f9c7..2bee952af 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -27,16 +27,8 @@ jobs:
     # Consider using larger runners or machines with greater resources for possible analysis time improvements.
     runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
     permissions:
-      # required for all workflows
       security-events: write
 
-      # required to fetch internal or private CodeQL packs
-      packages: read
-
-      # only required for workflows in private repositories
-      actions: read
-      contents: read
-
     strategy:
       fail-fast: false
       matrix:

From a9833d635dd5201cd94cc9d061590e41e24ea0cc Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 30 May 2025 15:38:00 -0400
Subject: [PATCH 949/994] Specify explicit `contents: read` workflow
 permissions

Three CI workflows that need only `contents: read` permissions and
no other permissions did not have explicit permissions set, and
would therefore be given default permissions configured for the
repository, which might be more expansive than the workflows need.

It is recommended to set explicit workflow permissions [1]. This
does that, specifying permissions as `pythonpackage.yml` already
did, and closing three `actions/missing-workflow-permissions`
CodeQL alerts (new since #2032 enabled scanning of GHA workflows).

[1]: https://codeql.github.com/codeql-query-help/actions/actions-missing-workflow-permissions/
---
 .github/workflows/alpine-test.yml | 3 +++
 .github/workflows/cygwin-test.yml | 3 +++
 .github/workflows/lint.yml        | 3 +++
 3 files changed, 9 insertions(+)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index bd09a939b..513c65bb8 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -2,6 +2,9 @@ name: test-alpine
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   test:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 278777907..572a9197e 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -2,6 +2,9 @@ name: test-cygwin
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   test:
     runs-on: windows-latest
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a0e81a993..ceba0dd85 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,6 +2,9 @@ name: Lint
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   lint:
     runs-on: ubuntu-latest

From 00b46127e54c104ac86333150708acdccce98cb7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 03:22:44 +0000
Subject: [PATCH 950/994] Bump git/ext/gitdb from `f36c0cc` to `7e02fbd`

Bumps [git/ext/gitdb](https://github.com/gitpython-developers/gitdb) from `f36c0cc` to `7e02fbd`.
- [Release notes](https://github.com/gitpython-developers/gitdb/releases)
- [Commits](https://github.com/gitpython-developers/gitdb/compare/f36c0cc42ea2f529291e441073f74e920988d4d2...7e02fbde5fcfcd52f541995fbcde22e85535adef)

---
updated-dependencies:
- dependency-name: git/ext/gitdb
  dependency-version: 7e02fbde5fcfcd52f541995fbcde22e85535adef
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 git/ext/gitdb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/ext/gitdb b/git/ext/gitdb
index f36c0cc42..335c0f661 160000
--- a/git/ext/gitdb
+++ b/git/ext/gitdb
@@ -1 +1 @@
-Subproject commit f36c0cc42ea2f529291e441073f74e920988d4d2
+Subproject commit 335c0f66173eecdc7b2597c2b6c3d1fde795df30

From 67468a2396f56f792ac4c139f43c98745e93a2b5 Mon Sep 17 00:00:00 2001
From: betaboon <betaboon@0x80.ninja>
Date: Fri, 6 Jun 2025 22:41:28 +0200
Subject: [PATCH 951/994] Fix GitConfigParser not removing quotes from values

---
 git/config.py                        | 2 ++
 test/fixtures/git_config_with_quotes | 3 +++
 test/test_config.py                  | 8 +++++++-
 3 files changed, 12 insertions(+), 1 deletion(-)
 create mode 100644 test/fixtures/git_config_with_quotes

diff --git a/git/config.py b/git/config.py
index de3508360..2df99a753 100644
--- a/git/config.py
+++ b/git/config.py
@@ -508,6 +508,8 @@ def string_decode(v: str) -> str:
                     if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
                         is_multi_line = True
                         optval = string_decode(optval[1:])
+                    elif len(optval) > 1 and optval[0] == '"' and optval[-1] == '"':
+                        optval = optval[1:-1].strip()
                     # END handle multi-line
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)
diff --git a/test/fixtures/git_config_with_quotes b/test/fixtures/git_config_with_quotes
new file mode 100644
index 000000000..40e6710d9
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes
@@ -0,0 +1,3 @@
+[user]
+  name = "Cody Veal"
+  email = "cveal05@gmail.com"
diff --git a/test/test_config.py b/test/test_config.py
index 92997422d..886d5b136 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -391,7 +391,7 @@ def test_complex_aliases(self):
         with GitConfigParser(file_obj, read_only=False) as w_config:
             self.assertEqual(
                 w_config.get("alias", "rbi"),
-                '"!g() { git rebase -i origin/${1:-master} ; } ; g"',
+                "!g() { git rebase -i origin/${1:-master} ; } ; g",
             )
         self.assertEqual(
             file_obj.getvalue(),
@@ -406,6 +406,12 @@ def test_empty_config_value(self):
         with self.assertRaises(cp.NoOptionError):
             cr.get_value("color", "ui")
 
+    def test_config_with_quotes(self):
+        cr = GitConfigParser(fixture_path("git_config_with_quotes"), read_only=True)
+
+        self.assertEqual(cr.get("user", "name"), "Cody Veal")
+        self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")
+
     def test_get_values_works_without_requiring_any_other_calls_first(self):
         file_obj = self._to_memcache(fixture_path("git_config_multiple"))
         cr = GitConfigParser(file_obj, read_only=True)

From 5f303202ee0666f5298ff39a5a129162b69ff790 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 01:08:07 -0400
Subject: [PATCH 952/994] Test a quoted config var with meaningful edge
 whitespace

#2035 fixed issue #1923 by removing separate double quotation marks
appearing on a single-line configuration variable when parsing a
configuration file. However, it also stripped leading and trailing
whitespace from the string obtained by removing the quotes.

This adds a test case of a plausible scenario where such whitespace
needs to be preserved and where a user would almost certainly expect
it to preserve: setting a value like `# ` for `core.commentString`,
in order to be able to easily create commit messages like this one,
that contain a line that begins with a literal `#`, while still
letting `#` in the more common case that it is followed by a space
be interpreted as a comment.

The effect of  `git config --local core.commentString '# '` is to
add a `commentString = "# "` line in the `[core]` section of
`.git/config`. The changes in #2035 allow us to correctly parse
more quoted strings than before, and almost allow us to parse this,
but not quite, because of the `strip()` operation that turns `# `
into `#`.
---
 test/fixtures/git_config_with_quotes_whitespace | 2 ++
 test/test_config.py                             | 4 ++++
 2 files changed, 6 insertions(+)
 create mode 100644 test/fixtures/git_config_with_quotes_whitespace

diff --git a/test/fixtures/git_config_with_quotes_whitespace b/test/fixtures/git_config_with_quotes_whitespace
new file mode 100644
index 000000000..c6014cc61
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes_whitespace
@@ -0,0 +1,2 @@
+[core]
+  commentString = "# "
diff --git a/test/test_config.py b/test/test_config.py
index 886d5b136..671f34046 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -412,6 +412,10 @@ def test_config_with_quotes(self):
         self.assertEqual(cr.get("user", "name"), "Cody Veal")
         self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")
 
+    def test_config_with_quotes_with_literal_whitespace(self):
+        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace"), read_only=True)
+        self.assertEqual(cr.get("core", "commentString"), "# ")
+
     def test_get_values_works_without_requiring_any_other_calls_first(self):
         file_obj = self._to_memcache(fixture_path("git_config_multiple"))
         cr = GitConfigParser(file_obj, read_only=True)

From bd2b930503ad5d97322aa81d7610ab743d915f84 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 00:13:46 -0400
Subject: [PATCH 953/994] Preserve quoted leading and trailing single-line
 whitespace

At least in a single line, whitespace in a double-quoted value in a
configuration file, like `name = " abc def "`, would presumably be
intended. This removes the `strip()` call that is applied to text
`ConfigParser` obtained by removing the double quotes around it.

This slightly refines the changes in #2035 by dropping the `strip()`
call while continuing to remove opening and closing double quotes.
---
 git/config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git/config.py b/git/config.py
index 2df99a753..1d58db53a 100644
--- a/git/config.py
+++ b/git/config.py
@@ -509,7 +509,7 @@ def string_decode(v: str) -> str:
                         is_multi_line = True
                         optval = string_decode(optval[1:])
                     elif len(optval) > 1 and optval[0] == '"' and optval[-1] == '"':
-                        optval = optval[1:-1].strip()
+                        optval = optval[1:-1]
                     # END handle multi-line
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)

From 5a8a4059d646fd313e81365ef240562556d8bd3f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Fri, 6 Jun 2025 23:16:45 -0400
Subject: [PATCH 954/994] Refactor Git.{AutoInterrupt,CatFileContentStream}
 nesting

This makes `Git.AutoInterrupt` and `Git.CatFileContentStream`
transparent aliases to top-level nonpublic `_AutoInterrupt` and
`_CatFileContentStream` classes in the `cmd` module.

This does not change the "public" interface. It also does not
change metadata relevant to documentation: the `__name__` and
`__qualname__` attributes are set explicitly to the values they had
before when these classes were defined nested, so that Sphinx
continues to document them (and to do so in full) in `Git` and as
`Git.AutoInterrupt` and `Git.CatFileContentStream`.

The purpose of this is to increase readability. The `Git` class is
big and complex, with a number of long members and various forms of
nesting. Since these two classes can be understood even without
reading the code of the `Git` class, moving the definitions out of
the `Git` class into top-level nonpublic classes will hopefully
increase readability and help with maintenance.
---
 git/cmd.py | 440 +++++++++++++++++++++++++++--------------------------
 1 file changed, 226 insertions(+), 214 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7f46edc8f..a6195880d 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -308,6 +308,230 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 
 ## -- End Utilities -- @}
 
+
+class _AutoInterrupt:
+    """Process wrapper that terminates the wrapped process on finalization.
+
+    This kills/interrupts the stored process instance once this instance goes out of
+    scope. It is used to prevent processes piling up in case iterators stop reading.
+
+    All attributes are wired through to the contained process object.
+
+    The wait method is overridden to perform automatic status code checking and possibly
+    raise.
+    """
+
+    __slots__ = ("proc", "args", "status")
+
+    # If this is non-zero it will override any status code during _terminate, used
+    # to prevent race conditions in testing.
+    _status_code_if_terminate: int = 0
+
+    def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
+        self.proc = proc
+        self.args = args
+        self.status: Union[int, None] = None
+
+    def _terminate(self) -> None:
+        """Terminate the underlying process."""
+        if self.proc is None:
+            return
+
+        proc = self.proc
+        self.proc = None
+        if proc.stdin:
+            proc.stdin.close()
+        if proc.stdout:
+            proc.stdout.close()
+        if proc.stderr:
+            proc.stderr.close()
+        # Did the process finish already so we have a return code?
+        try:
+            if proc.poll() is not None:
+                self.status = self._status_code_if_terminate or proc.poll()
+                return
+        except OSError as ex:
+            _logger.info("Ignored error after process had died: %r", ex)
+
+        # It can be that nothing really exists anymore...
+        if os is None or getattr(os, "kill", None) is None:
+            return
+
+        # Try to kill it.
+        try:
+            proc.terminate()
+            status = proc.wait()  # Ensure the process goes away.
+
+            self.status = self._status_code_if_terminate or status
+        except OSError as ex:
+            _logger.info("Ignored error after process had died: %r", ex)
+        # END exception handling
+
+    def __del__(self) -> None:
+        self._terminate()
+
+    def __getattr__(self, attr: str) -> Any:
+        return getattr(self.proc, attr)
+
+    # TODO: Bad choice to mimic `proc.wait()` but with different args.
+    def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
+        """Wait for the process and return its status code.
+
+        :param stderr:
+            Previously read value of stderr, in case stderr is already closed.
+
+        :warn:
+            May deadlock if output or error pipes are used and not handled separately.
+
+        :raise git.exc.GitCommandError:
+            If the return status is not 0.
+        """
+        if stderr is None:
+            stderr_b = b""
+        stderr_b = force_bytes(data=stderr, encoding="utf-8")
+        status: Union[int, None]
+        if self.proc is not None:
+            status = self.proc.wait()
+            p_stderr = self.proc.stderr
+        else:  # Assume the underlying proc was killed earlier or never existed.
+            status = self.status
+            p_stderr = None
+
+        def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
+            if stream:
+                try:
+                    return stderr_b + force_bytes(stream.read())
+                except (OSError, ValueError):
+                    return stderr_b or b""
+            else:
+                return stderr_b or b""
+
+        # END status handling
+
+        if status != 0:
+            errstr = read_all_from_possibly_closed_stream(p_stderr)
+            _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
+            raise GitCommandError(remove_password_if_present(self.args), status, errstr)
+        return status
+
+
+_AutoInterrupt.__name__ = "AutoInterrupt"
+_AutoInterrupt.__qualname__ = "Git.AutoInterrupt"
+
+
+class _CatFileContentStream:
+    """Object representing a sized read-only stream returning the contents of
+    an object.
+
+    This behaves like a stream, but counts the data read and simulates an empty stream
+    once our sized content region is empty.
+
+    If not all data are read to the end of the object's lifetime, we read the rest to
+    ensure the underlying stream continues to work.
+    """
+
+    __slots__ = ("_stream", "_nbr", "_size")
+
+    def __init__(self, size: int, stream: IO[bytes]) -> None:
+        self._stream = stream
+        self._size = size
+        self._nbr = 0  # Number of bytes read.
+
+        # Special case: If the object is empty, has null bytes, get the final
+        # newline right away.
+        if size == 0:
+            stream.read(1)
+        # END handle empty streams
+
+    def read(self, size: int = -1) -> bytes:
+        bytes_left = self._size - self._nbr
+        if bytes_left == 0:
+            return b""
+        if size > -1:
+            # Ensure we don't try to read past our limit.
+            size = min(bytes_left, size)
+        else:
+            # They try to read all, make sure it's not more than what remains.
+            size = bytes_left
+        # END check early depletion
+        data = self._stream.read(size)
+        self._nbr += len(data)
+
+        # Check for depletion, read our final byte to make the stream usable by
+        # others.
+        if self._size - self._nbr == 0:
+            self._stream.read(1)  # final newline
+        # END finish reading
+        return data
+
+    def readline(self, size: int = -1) -> bytes:
+        if self._nbr == self._size:
+            return b""
+
+        # Clamp size to lowest allowed value.
+        bytes_left = self._size - self._nbr
+        if size > -1:
+            size = min(bytes_left, size)
+        else:
+            size = bytes_left
+        # END handle size
+
+        data = self._stream.readline(size)
+        self._nbr += len(data)
+
+        # Handle final byte.
+        if self._size - self._nbr == 0:
+            self._stream.read(1)
+        # END finish reading
+
+        return data
+
+    def readlines(self, size: int = -1) -> List[bytes]:
+        if self._nbr == self._size:
+            return []
+
+        # Leave all additional logic to our readline method, we just check the size.
+        out = []
+        nbr = 0
+        while True:
+            line = self.readline()
+            if not line:
+                break
+            out.append(line)
+            if size > -1:
+                nbr += len(line)
+                if nbr > size:
+                    break
+            # END handle size constraint
+        # END readline loop
+        return out
+
+    # skipcq: PYL-E0301
+    def __iter__(self) -> "Git.CatFileContentStream":
+        return self
+
+    def __next__(self) -> bytes:
+        line = self.readline()
+        if not line:
+            raise StopIteration
+
+        return line
+
+    next = __next__
+
+    def __del__(self) -> None:
+        bytes_left = self._size - self._nbr
+        if bytes_left:
+            # Read and discard - seeking is impossible within a stream.
+            # This includes any terminating newline.
+            self._stream.read(bytes_left + 1)
+        # END handle incomplete read
+
+
+_CatFileContentStream.__name__ = "CatFileContentStream"
+_CatFileContentStream.__qualname__ = "Git.CatFileContentStream"
+
+
 _USE_SHELL_DEFAULT_MESSAGE = (
     "Git.USE_SHELL is deprecated, because only its default value of False is safe. "
     "It will be removed in a future release."
@@ -728,221 +952,9 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
                         f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
                     )
 
-    class AutoInterrupt:
-        """Process wrapper that terminates the wrapped process on finalization.
-
-        This kills/interrupts the stored process instance once this instance goes out of
-        scope. It is used to prevent processes piling up in case iterators stop reading.
-
-        All attributes are wired through to the contained process object.
-
-        The wait method is overridden to perform automatic status code checking and
-        possibly raise.
-        """
-
-        __slots__ = ("proc", "args", "status")
-
-        # If this is non-zero it will override any status code during _terminate, used
-        # to prevent race conditions in testing.
-        _status_code_if_terminate: int = 0
-
-        def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None:
-            self.proc = proc
-            self.args = args
-            self.status: Union[int, None] = None
-
-        def _terminate(self) -> None:
-            """Terminate the underlying process."""
-            if self.proc is None:
-                return
-
-            proc = self.proc
-            self.proc = None
-            if proc.stdin:
-                proc.stdin.close()
-            if proc.stdout:
-                proc.stdout.close()
-            if proc.stderr:
-                proc.stderr.close()
-            # Did the process finish already so we have a return code?
-            try:
-                if proc.poll() is not None:
-                    self.status = self._status_code_if_terminate or proc.poll()
-                    return
-            except OSError as ex:
-                _logger.info("Ignored error after process had died: %r", ex)
-
-            # It can be that nothing really exists anymore...
-            if os is None or getattr(os, "kill", None) is None:
-                return
-
-            # Try to kill it.
-            try:
-                proc.terminate()
-                status = proc.wait()  # Ensure the process goes away.
-
-                self.status = self._status_code_if_terminate or status
-            except OSError as ex:
-                _logger.info("Ignored error after process had died: %r", ex)
-            # END exception handling
-
-        def __del__(self) -> None:
-            self._terminate()
-
-        def __getattr__(self, attr: str) -> Any:
-            return getattr(self.proc, attr)
-
-        # TODO: Bad choice to mimic `proc.wait()` but with different args.
-        def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
-            """Wait for the process and return its status code.
-
-            :param stderr:
-                Previously read value of stderr, in case stderr is already closed.
-
-            :warn:
-                May deadlock if output or error pipes are used and not handled
-                separately.
-
-            :raise git.exc.GitCommandError:
-                If the return status is not 0.
-            """
-            if stderr is None:
-                stderr_b = b""
-            stderr_b = force_bytes(data=stderr, encoding="utf-8")
-            status: Union[int, None]
-            if self.proc is not None:
-                status = self.proc.wait()
-                p_stderr = self.proc.stderr
-            else:  # Assume the underlying proc was killed earlier or never existed.
-                status = self.status
-                p_stderr = None
-
-            def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes:
-                if stream:
-                    try:
-                        return stderr_b + force_bytes(stream.read())
-                    except (OSError, ValueError):
-                        return stderr_b or b""
-                else:
-                    return stderr_b or b""
-
-            # END status handling
-
-            if status != 0:
-                errstr = read_all_from_possibly_closed_stream(p_stderr)
-                _logger.debug("AutoInterrupt wait stderr: %r" % (errstr,))
-                raise GitCommandError(remove_password_if_present(self.args), status, errstr)
-            return status
-
-    # END auto interrupt
-
-    class CatFileContentStream:
-        """Object representing a sized read-only stream returning the contents of
-        an object.
-
-        This behaves like a stream, but counts the data read and simulates an empty
-        stream once our sized content region is empty.
-
-        If not all data are read to the end of the object's lifetime, we read the
-        rest to ensure the underlying stream continues to work.
-        """
-
-        __slots__ = ("_stream", "_nbr", "_size")
-
-        def __init__(self, size: int, stream: IO[bytes]) -> None:
-            self._stream = stream
-            self._size = size
-            self._nbr = 0  # Number of bytes read.
-
-            # Special case: If the object is empty, has null bytes, get the final
-            # newline right away.
-            if size == 0:
-                stream.read(1)
-            # END handle empty streams
-
-        def read(self, size: int = -1) -> bytes:
-            bytes_left = self._size - self._nbr
-            if bytes_left == 0:
-                return b""
-            if size > -1:
-                # Ensure we don't try to read past our limit.
-                size = min(bytes_left, size)
-            else:
-                # They try to read all, make sure it's not more than what remains.
-                size = bytes_left
-            # END check early depletion
-            data = self._stream.read(size)
-            self._nbr += len(data)
-
-            # Check for depletion, read our final byte to make the stream usable by
-            # others.
-            if self._size - self._nbr == 0:
-                self._stream.read(1)  # final newline
-            # END finish reading
-            return data
-
-        def readline(self, size: int = -1) -> bytes:
-            if self._nbr == self._size:
-                return b""
-
-            # Clamp size to lowest allowed value.
-            bytes_left = self._size - self._nbr
-            if size > -1:
-                size = min(bytes_left, size)
-            else:
-                size = bytes_left
-            # END handle size
-
-            data = self._stream.readline(size)
-            self._nbr += len(data)
-
-            # Handle final byte.
-            if self._size - self._nbr == 0:
-                self._stream.read(1)
-            # END finish reading
-
-            return data
-
-        def readlines(self, size: int = -1) -> List[bytes]:
-            if self._nbr == self._size:
-                return []
-
-            # Leave all additional logic to our readline method, we just check the size.
-            out = []
-            nbr = 0
-            while True:
-                line = self.readline()
-                if not line:
-                    break
-                out.append(line)
-                if size > -1:
-                    nbr += len(line)
-                    if nbr > size:
-                        break
-                # END handle size constraint
-            # END readline loop
-            return out
-
-        # skipcq: PYL-E0301
-        def __iter__(self) -> "Git.CatFileContentStream":
-            return self
-
-        def __next__(self) -> bytes:
-            line = self.readline()
-            if not line:
-                raise StopIteration
-
-            return line
-
-        next = __next__
+    AutoInterrupt = _AutoInterrupt
 
-        def __del__(self) -> None:
-            bytes_left = self._size - self._nbr
-            if bytes_left:
-                # Read and discard - seeking is impossible within a stream.
-                # This includes any terminating newline.
-                self._stream.read(bytes_left + 1)
-            # END handle incomplete read
+    CatFileContentStream = _CatFileContentStream
 
     def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
         """Initialize this instance with:

From c6d16d01c54d45be20de40dedae4e9471b96c8ab Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 11:35:08 -0400
Subject: [PATCH 955/994] Start on fixing AutoInterrupt/CatFileContentStream
 aliases

This uses `TypeAlias` from the `typing` module, to make it so the
assignment statments introduced in #2037 (to set `Git.AutoInterrupt`
and `Git.CatFileContentStream` to nonpublic module-level
implementations `_AutoInterrupt` and `_CatFileContentStream`) are
treated by `mypy` as type aliases rather than as class variables.

For details on the problem this partially fixes, see #2038 and:
https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases

The fix won't work in this form, however, because it attempts to
import `TypeAlias` unconditionally from the standard-library
`typing` module, which only gained it in Python 3.10.
---
 git/cmd.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index a6195880d..7015d376f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -55,6 +55,7 @@
     TYPE_CHECKING,
     TextIO,
     Tuple,
+    TypeAlias,
     Union,
     cast,
     overload,
@@ -952,9 +953,9 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
                         f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
                     )
 
-    AutoInterrupt = _AutoInterrupt
+    AutoInterrupt: TypeAlias = _AutoInterrupt
 
-    CatFileContentStream = _CatFileContentStream
+    CatFileContentStream: TypeAlias = _CatFileContentStream
 
     def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
         """Initialize this instance with:

From c6c081230f4b96ca9efce4c9e2478396eaf348c9 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 11:52:36 -0400
Subject: [PATCH 956/994] Import `TypeAlias` from `typing_extensions` where
 needed

The standard library `typing` module introduced `TypeAlias` in
Python 3.10. This uses it from `typing_extensions` where neederd,
by making three changes:

- Change the version lower bound for `typing-extensions` from
  3.7.4.3 to 3.10.0.2, since 3.7.4.3 doesn't offer `TypeAlias`.

  (The reason not to go higher, to major version 4, is that
  it no longer supports versions of Python lower than 3.9, but
  we currently support Python 3.7 and Python 3.8.)

- Require the `typing-extensions` dependency when using Python
  versions lower than 3.10, rather than only lower than 3.7 as
  before.

- Conditionally import `TypeAlias` (in the `git.cmd` module) from
  either `typing` or `type_extensions` depending on the Python
  version, using a pattern that `mypy` and other type checkers
  recognize statically.

Together with the preceding commit, this fixes #2038. (This is
approach (2) described there.)
---
 git/cmd.py       | 6 +++++-
 requirements.txt | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 7015d376f..6deded04b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -55,12 +55,16 @@
     TYPE_CHECKING,
     TextIO,
     Tuple,
-    TypeAlias,
     Union,
     cast,
     overload,
 )
 
+if sys.version_info >= (3, 10):
+    from typing import TypeAlias
+else:
+    from typing_extensions import TypeAlias
+
 from git.types import Literal, PathLike, TBD
 
 if TYPE_CHECKING:
diff --git a/requirements.txt b/requirements.txt
index 7159416a9..61d8403b0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 gitdb>=4.0.1,<5
-typing-extensions>=3.7.4.3;python_version<"3.8"
+typing-extensions>=3.10.0.2;python_version<"3.10"

From 8905e9e8f0244414fbff90c19b144d02b74af3b3 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 12:33:56 -0400
Subject: [PATCH 957/994] Fix CI `mypy` command on free-threaded Python

When the version is represented as `3.13t`, the `--python-version`
option needs an operand of `3.13`, not `3.13t`.

(This, and the fix here, will automatically apply to later threaded
Pythons, such as 3.14t, too.)
---
 .github/workflows/pythonpackage.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 9fd660c6b..c4fbef48e 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -97,10 +97,11 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy --python-version=${{ matrix.python-version }}
+        mypy --python-version="${PYTHON_VERSION%t}"
       env:
         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.
       continue-on-error: true

From b45ae86aaaab25ccd07ee183382a0e78a92f65a2 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 13:10:47 -0400
Subject: [PATCH 958/994] Add an explanatory comment for omitting "t"

Since the `${var%pattern}` syntax may not be immediately obvious.
---
 .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 c4fbef48e..e7cb06cc0 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -97,7 +97,7 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy --python-version="${PYTHON_VERSION%t}"
+        mypy --python-version="${PYTHON_VERSION%t}"  # Version only, with no "t" for free-threaded.
       env:
         MYPY_FORCE_COLOR: "1"
         TERM: "xterm-256color"  # For color: https://github.com/python/mypy/issues/13817

From ba58f40d385e5f371c468470752a4d5aa0b16789 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 13:37:22 -0400
Subject: [PATCH 959/994] Split Cygwin CI into two test jobs

One job is for all tests except the `performance` tests, while the
other job is for only the `performance` tests.

The idea is to decrease the total time it takes for all CI jobs to
complete in most cases, by splitting the long-running (currently
usually about 13 minute) Cygwin job into two less-long jobs.
---
 .github/workflows/cygwin-test.yml | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 572a9197e..d73f0522e 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -10,6 +10,14 @@ jobs:
     runs-on: windows-latest
 
     strategy:
+      matrix:
+        selection: [fast, perf]
+        include:
+        - selection: fast
+          additional-pytest-args: --ignore=test/performance
+        - selection: perf
+          additional-pytest-args: test/performance/
+
       fail-fast: false
 
     env:
@@ -87,4 +95,4 @@ jobs:
 
     - name: Test with pytest
       run: |
-        pytest --color=yes -p no:sugar --instafail -vv
+        pytest --color=yes -p no:sugar --instafail -vv ${{ matrix.additional-pytest-args }}

From 414de64b095282ce39b9dd34876167e607b331cf Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 14:13:25 -0400
Subject: [PATCH 960/994] Show differing `pytest` arguments in step name

In the "Test with pytest" step of the Cygwin test jobs.

This is to distinguish the newly split jobs from each other more
clearly when glancing at their steps for a run.
---
 .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 d73f0522e..3c1eb3dc0 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -93,6 +93,6 @@ jobs:
         python --version
         python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")'
 
-    - name: Test with pytest
+    - name: Test with pytest (${{ matrix.additional-pytest-args }})
       run: |
         pytest --color=yes -p no:sugar --instafail -vv ${{ matrix.additional-pytest-args }}

From a6c623eecf12eb525ad115b0be75c7449c4f8efe Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 14:18:14 -0400
Subject: [PATCH 961/994] Small stylistic consistency tweak

This removes an unnecessary trailing slash that I had not used
consistently anyway.
---
 .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 3c1eb3dc0..7c3eeedca 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -16,7 +16,7 @@ jobs:
         - selection: fast
           additional-pytest-args: --ignore=test/performance
         - selection: perf
-          additional-pytest-args: test/performance/
+          additional-pytest-args: test/performance
 
       fail-fast: false
 

From 31e1c035e1af514d5d2a9ed6630f71663df7b265 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 15:01:21 -0400
Subject: [PATCH 962/994] Use static notation for `setuptools` in installation
 test

The `VirtualEnvironment` test helper is used in multiple places,
but it is only told to install/upgrade `pip` when used from
`test_installation`. It implements the functionality it would
ideally obtain by `upgrade_deps`, since the `upgrade_deps`
parameter is only avaiilable in `venv` when using Python 3.9 and
later.

When `pip` is installed, `upgrade_deps` would install `setuptools`
when using Python 3.11 or lower, but not when using Python 3.12 or
higher. `VirtualEnvironment` does the same. (The reason for this is
not just to avoid needlessly departing from what `upgrade_deps`
would do. Rather, it should not generally be necessary to have
`setuptools` installed for package management since Python 3.12,
and if it were necessary then this would a bug we would want to
detect while running tests.)

Previously this conditional specification of `setuptools` was done
by building different lists of package arguments to pass to `pip`,
by checking `sys.version_info` to decide whether to append the
string `setuptools`.

This commit changes how it is done, to use a static list of package
arguments instead. (The Python intepreter path continues to be
obtained dynamically, but all its positional arguments, including
those specifying packages, are now string literals.) The
conditional `setuptools` requirement is now expressed statically
using notation recognized by `pip`, as the string
`setuptools; python_version<"3.12"`.
---
 test/lib/helper.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/test/lib/helper.py b/test/lib/helper.py
index 5d91447ea..241d27341 100644
--- a/test/lib/helper.py
+++ b/test/lib/helper.py
@@ -415,9 +415,15 @@ def __init__(self, env_dir, *, with_pip):
 
         if with_pip:
             # The upgrade_deps parameter to venv.create is 3.9+ only, so do it this way.
-            command = [self.python, "-m", "pip", "install", "--upgrade", "pip"]
-            if sys.version_info < (3, 12):
-                command.append("setuptools")
+            command = [
+                self.python,
+                "-m",
+                "pip",
+                "install",
+                "--upgrade",
+                "pip",
+                'setuptools; python_version<"3.12"',
+            ]
             subprocess.check_output(command)
 
     @property

From 727f4e9e54362d0356066a26d0c22028a465cccd Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 15:21:00 -0400
Subject: [PATCH 963/994] Use static notation for `setuptools` in CI `pip`
 commands

`setuptools` may potentially be needed for installation to work
fully as desired prior to Python 3.12, so in those versions it is
installed automatically in any virtual environment that is created
with `pip` available. This is a behavior of the `venv` module that
is not specific to CI.

However, on CI we upgrade packages that are preinstalled in the
virtual environment, or that we may otherwise wish to be present.
This took the form of unconditionally installing/upgrading the
`pip` and `wheel` packages, but conditionally upgrading the
`setuptools` package only if we find that it is already installed.

This commit changes the behavior to statically specify the same
list of package specifications to `pip` in all environments and in
all versions of Python, but to use the static notation recognized
by `pip` to indicate that `setuptools` is to be instaled/upgraded
only if the Python version is strictly less than Python 3.12.

This seems to be more readable. It also avoids using unquoted shell
parameter expansion in a way that is potentially confusing (for
example, if we were running our CI script steps through ShellCheck,
then it would automatically balk at that construction). It is also
more consistent with how `test_installation` sets up its
environment (especially since 31e1c03, but actually even before
that, because it was still conditioning `setuptools` on the Python
version rather than whether it was already installed). Finally,
this behavior is what the preexisting comment already described.

This also adjusts the shell quoting style slightly in other related
commands (in the same workflows) that pass package specifications
to `pip`, for consistency.

(For now, `".[test]"` rather than `.[test]` remains written in the
readme because it works in `cmd.exe` as well as other shells, but
it may be changed there in the future too.)
---
 .github/workflows/alpine-test.yml   | 4 ++--
 .github/workflows/cygwin-test.yml   | 4 ++--
 .github/workflows/pythonpackage.yml | 6 +++---
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index 513c65bb8..a3361798d 100644
--- a/.github/workflows/alpine-test.yml
+++ b/.github/workflows/alpine-test.yml
@@ -55,12 +55,12 @@ jobs:
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
         . .venv/bin/activate
-        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
+        python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel
 
     - name: Install project and test dependencies
       run: |
         . .venv/bin/activate
-        pip install ".[test]"
+        pip install '.[test]'
 
     - name: Show version and platform information
       run: |
diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 7c3eeedca..6943db09c 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -79,11 +79,11 @@ jobs:
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
+        python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel
 
     - name: Install project and test dependencies
       run: |
-        pip install ".[test]"
+        pip install '.[test]'
 
     - name: Show version and platform information
       run: |
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index e7cb06cc0..c56d45df7 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -72,11 +72,11 @@ jobs:
     - name: Update PyPA packages
       run: |
         # Get the latest pip, wheel, and prior to Python 3.12, setuptools.
-        python -m pip install -U pip $(pip freeze --all | grep -ow ^setuptools) wheel
+        python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel
 
     - name: Install project and test dependencies
       run: |
-        pip install ".[test]"
+        pip install '.[test]'
 
     - name: Show version and platform information
       run: |
@@ -114,5 +114,5 @@ jobs:
     - name: Documentation
       if: matrix.python-version != '3.7'
       run: |
-        pip install ".[doc]"
+        pip install '.[doc]'
         make -C doc html

From 2b85cd1d7ccbbc596cf0d3149bdc854498ebe35e Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 16:35:08 -0400
Subject: [PATCH 964/994] Fix ambiguous `_safer_popen_windows` comment

This fixes some ambiguous wording in a comment in
`_safer_popen_wording`, where it was unclear if the secondary
problem -- where it would be possible to run a wrong `cmd.exe`-type
shell -- would happen under two separate circumstances, or only
when both circumstances occurred together. This adjusts its wording
to make clear that it is the latter.

This also fixes a minor typo in another `_safer_popen_windows`
comment.

This might be viewed as building on the improvements in b9d9e56
(#1859), but the changes here are to comments only.
---
 git/cmd.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 6deded04b..71096197c 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -273,12 +273,12 @@ def _safer_popen_windows(
         if shell:
             # The original may be immutable, or the caller may reuse it. Mutate a copy.
             env = {} if env is None else dict(env)
-            env["NoDefaultCurrentDirectoryInExePath"] = "1"  # The "1" can be an value.
+            env["NoDefaultCurrentDirectoryInExePath"] = "1"  # The "1" can be any value.
 
         # When not using a shell, the current process does the search in a
         # CreateProcessW API call, so the variable must be set in our environment. With
         # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283
-        # is patched. In Python versions where it is unpatched, and in the rare case the
+        # is patched. In Python versions where it is unpatched, in the rare case the
         # ComSpec environment variable is unset, the search for the shell itself is
         # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here,
         # is simpler and protects against that. (As above, the "1" can be any value.)

From 253099fe91744801c39b41527e126c85c17ec003 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 18:28:17 -0400
Subject: [PATCH 965/994] Clarify `USE_SHELL` warning helper signature

This is a minor refactor of how `_warn_use_shell` can be, and is,
invoked.

The `_warn_use_shell` helper function in `git.cmd` takes a single
`bool`-valued argument `extra_danger`, which is conceptually
associated with having a `True` value of `USE_SHELL`, but the
association is not necessarily obvious. Specifically:

- For the warning given when reading `USE_SHELL` on the `Git` class
  or through an instance, `extra_danger` is always `False`. This is
  so even if the `USE_SHELL` value is currently `True`, because the
  danger that arises from `True` occurs internally.

- For the warning given when writing `USE_SHELL`, which can only be
  done on the `Git` class and not on or through an instance,
  `extra_danger` is the value set for the attribute. This is
  because setting `USE_SHELL` to `True` incurs the danger described
  in #1896.

When reading the code, which passed `extra_danger` positionally,
the meaning of the parameter may not always have been obvious.

This makes the `extra_danger` parameter keyword-only, and passes
it by keyword in all invocations, so that its meaning is clearer.
---
 git/cmd.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/git/cmd.py b/git/cmd.py
index 71096197c..15d7820df 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -550,7 +550,7 @@ def __del__(self) -> None:
 )
 
 
-def _warn_use_shell(extra_danger: bool) -> None:
+def _warn_use_shell(*, extra_danger: bool) -> None:
     warnings.warn(
         _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
         DeprecationWarning,
@@ -566,12 +566,12 @@ class _GitMeta(type):
 
     def __getattribute(cls, name: str) -> Any:
         if name == "USE_SHELL":
-            _warn_use_shell(False)
+            _warn_use_shell(extra_danger=False)
         return super().__getattribute__(name)
 
     def __setattr(cls, name: str, value: Any) -> Any:
         if name == "USE_SHELL":
-            _warn_use_shell(value)
+            _warn_use_shell(extra_danger=value)
         super().__setattr__(name, value)
 
     if not TYPE_CHECKING:
@@ -988,7 +988,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
 
     def __getattribute__(self, name: str) -> Any:
         if name == "USE_SHELL":
-            _warn_use_shell(False)
+            _warn_use_shell(extra_danger=False)
         return super().__getattribute__(name)
 
     def __getattr__(self, name: str) -> Any:

From e5a2db58d4d7fee765bd1146647bf2c09b026ce8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 19:24:31 -0400
Subject: [PATCH 966/994] Test `ConfigParser` with whitespace outside the value

In both the:

- Unquoted case, where extra whitespace is at the edges of what can
  be parsed as the value.

- Quoted case, where extra whitespace is next to but outside of the
  quotes.

The case where the whitespace is at the edges of the quoted value
and *inside* the quotes, and thus part of the value, is already
covered in #2036. (That is merely renamed here, to distinguish it.)
---
 test/fixtures/git_config_with_extra_whitespace         |  2 ++
 ...espace => git_config_with_quotes_whitespace_inside} |  0
 .../fixtures/git_config_with_quotes_whitespace_outside |  2 ++
 test/test_config.py                                    | 10 +++++++++-
 4 files changed, 13 insertions(+), 1 deletion(-)
 create mode 100644 test/fixtures/git_config_with_extra_whitespace
 rename test/fixtures/{git_config_with_quotes_whitespace => git_config_with_quotes_whitespace_inside} (100%)
 create mode 100644 test/fixtures/git_config_with_quotes_whitespace_outside

diff --git a/test/fixtures/git_config_with_extra_whitespace b/test/fixtures/git_config_with_extra_whitespace
new file mode 100644
index 000000000..0f727cb5d
--- /dev/null
+++ b/test/fixtures/git_config_with_extra_whitespace
@@ -0,0 +1,2 @@
+[init]
+  defaultBranch =  trunk  
diff --git a/test/fixtures/git_config_with_quotes_whitespace b/test/fixtures/git_config_with_quotes_whitespace_inside
similarity index 100%
rename from test/fixtures/git_config_with_quotes_whitespace
rename to test/fixtures/git_config_with_quotes_whitespace_inside
diff --git a/test/fixtures/git_config_with_quotes_whitespace_outside b/test/fixtures/git_config_with_quotes_whitespace_outside
new file mode 100644
index 000000000..4b1615a51
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes_whitespace_outside
@@ -0,0 +1,2 @@
+[init]
+  defaultBranch =  "trunk"  
diff --git a/test/test_config.py b/test/test_config.py
index 671f34046..879b98365 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -398,6 +398,10 @@ def test_complex_aliases(self):
             self._to_memcache(fixture_path(".gitconfig")).getvalue(),
         )
 
+    def test_config_with_extra_whitespace(self):
+        cr = GitConfigParser(fixture_path("git_config_with_extra_whitespace"), read_only=True)
+        self.assertEqual(cr.get("init", "defaultBranch"), "trunk")
+
     def test_empty_config_value(self):
         cr = GitConfigParser(fixture_path("git_config_with_empty_value"), read_only=True)
 
@@ -413,9 +417,13 @@ def test_config_with_quotes(self):
         self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")
 
     def test_config_with_quotes_with_literal_whitespace(self):
-        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace"), read_only=True)
+        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_inside"), read_only=True)
         self.assertEqual(cr.get("core", "commentString"), "# ")
 
+    def test_config_with_quotes_with_whitespace_outside_value(self):
+        cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_outside"), read_only=True)
+        self.assertEqual(cr.get("init", "defaultBranch"), "trunk")
+
     def test_get_values_works_without_requiring_any_other_calls_first(self):
         file_obj = self._to_memcache(fixture_path("git_config_multiple"))
         cr = GitConfigParser(file_obj, read_only=True)

From 4ebe407493363a814f590a97f3734364e6c5c1e1 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 22:33:35 -0400
Subject: [PATCH 967/994] Test that ConfigParser treats empty quotes as an
 empty value

The ConfigParser has supported this for a long time, but it is now
done redundantly since #2035. This adds a test for it, both to make
clearer that it is intended to work and to allow verifying that it
continues to hold once the legacy special-casing for it is removed.
---
 test/fixtures/git_config_with_empty_quotes | 2 ++
 test/test_config.py                        | 4 ++++
 2 files changed, 6 insertions(+)
 create mode 100644 test/fixtures/git_config_with_empty_quotes

diff --git a/test/fixtures/git_config_with_empty_quotes b/test/fixtures/git_config_with_empty_quotes
new file mode 100644
index 000000000..f11fe4248
--- /dev/null
+++ b/test/fixtures/git_config_with_empty_quotes
@@ -0,0 +1,2 @@
+[core]
+  filemode = ""
diff --git a/test/test_config.py b/test/test_config.py
index 879b98365..76b918a54 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -416,6 +416,10 @@ def test_config_with_quotes(self):
         self.assertEqual(cr.get("user", "name"), "Cody Veal")
         self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")
 
+    def test_config_with_empty_quotes(self):
+        cr = GitConfigParser(fixture_path("git_config_with_empty_quotes"), read_only=True)
+        self.assertEqual(cr.get("core", "filemode"), "", "quotes can form a literal empty string as value")
+
     def test_config_with_quotes_with_literal_whitespace(self):
         cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_inside"), read_only=True)
         self.assertEqual(cr.get("core", "commentString"), "# ")

From 2f225244286567b72c865fc7c0219c7dc043575f Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 22:59:24 -0400
Subject: [PATCH 968/994] Remove explicit empty `""` handling in ConfigParser

Because literal `""` is a special case of `"..."` as parsed
since #2035.
---
 git/config.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/git/config.py b/git/config.py
index 1d58db53a..cdcc32e7b 100644
--- a/git/config.py
+++ b/git/config.py
@@ -501,9 +501,6 @@ def string_decode(v: str) -> str:
                         if pos != -1 and optval[pos - 1].isspace():
                             optval = optval[:pos]
                     optval = optval.strip()
-                    if optval == '""':
-                        optval = ""
-                    # END handle empty string
                     optname = self.optionxform(optname.rstrip())
                     if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
                         is_multi_line = True

From c8e4aa0f30d06ea2437539a5d56eede1ffa11432 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 23:15:05 -0400
Subject: [PATCH 969/994] Refactor quote parsing, to prepare for more checks

This refactors ConfigParser double-quote parsing near the single
line double-quoted value parsing code, so that:

- Code that parses the name is less intermixed with code that
  parses the value.

- Conditional logic is less duplicated.

- The `END` comment notation appears next to the code it describes.

- The final `else` can be turned into one or more `elif` followed
  by `else` to cover different cases of `"..."` differently. (But
  those are not added here. This commit is purely a refactoring.)

(The `pass` suite when `len(optval) < 2 or optval[0] != '"'` is
awkward and not really justified right now, but it looks like it
may be able to help with readabilty and help keep nesting down
when new `elif` cases are added.)
---
 git/config.py | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/git/config.py b/git/config.py
index cdcc32e7b..afec498f2 100644
--- a/git/config.py
+++ b/git/config.py
@@ -496,18 +496,23 @@ def string_decode(v: str) -> str:
                 if mo:
                     # We might just have handled the last line, which could contain a quotation we want to remove.
                     optname, vi, optval = mo.group("option", "vi", "value")
+                    optname = self.optionxform(optname.rstrip())
+
                     if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
                         pos = optval.find(";")
                         if pos != -1 and optval[pos - 1].isspace():
                             optval = optval[:pos]
                     optval = optval.strip()
-                    optname = self.optionxform(optname.rstrip())
-                    if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
+
+                    if len(optval) < 2 or optval[0] != '"':
+                        pass  # Nothing to treat as opening quotation.
+                    elif optval[-1] != '"':
                         is_multi_line = True
                         optval = string_decode(optval[1:])
-                    elif len(optval) > 1 and optval[0] == '"' and optval[-1] == '"':
-                        optval = optval[1:-1]
                     # END handle multi-line
+                    else:
+                        optval = optval[1:-1]
+
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)
                 else:

From 7bcea08873ca4052ab743f9e2f7cff42e7fe62d8 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sat, 7 Jun 2025 23:59:46 -0400
Subject: [PATCH 970/994] Add tests of ConfigParser with `"..."` with `"` or
 `\` inside

These are cases where just removing the outer quotes without doing
anything to the text inside does not give the correct result, and
where keeping the quotes may be preferable, in that it was the
long-standing behavior of `GitConfigParser`.

That this was the long-standing behavior may justify bringing it
back when the `"`-`"`-enclosed text contains such characters, but
it does not justify preserving it indefinitely: it will still be
better to parse the escape sequences, at least in the type case
that all of them in a value's representation are well-formed.
---
 test/fixtures/git_config_with_quotes_escapes |  9 +++++++++
 test/test_config.py                          | 20 ++++++++++++++++++++
 2 files changed, 29 insertions(+)
 create mode 100644 test/fixtures/git_config_with_quotes_escapes

diff --git a/test/fixtures/git_config_with_quotes_escapes b/test/fixtures/git_config_with_quotes_escapes
new file mode 100644
index 000000000..33332c221
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes_escapes
@@ -0,0 +1,9 @@
+[custom]
+  hasnewline = "first\nsecond"
+  hasbackslash = "foo\\bar"
+  hasquote = "ab\"cd"
+  hastrailingbackslash = "word\\"
+  hasunrecognized = "p\qrs"
+  hasunescapedquotes = "ab"cd"e"
+  ordinary = "hello world"
+  unquoted = good evening
diff --git a/test/test_config.py b/test/test_config.py
index 76b918a54..8e1007d9e 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -428,6 +428,26 @@ def test_config_with_quotes_with_whitespace_outside_value(self):
         cr = GitConfigParser(fixture_path("git_config_with_quotes_whitespace_outside"), read_only=True)
         self.assertEqual(cr.get("init", "defaultBranch"), "trunk")
 
+    def test_config_with_quotes_containing_escapes(self):
+        """For now just suppress quote removal. But it would be good to interpret most of these."""
+        cr = GitConfigParser(fixture_path("git_config_with_quotes_escapes"), read_only=True)
+
+        # These can eventually be supported by substituting the represented character.
+        self.assertEqual(cr.get("custom", "hasnewline"), R'"first\nsecond"')
+        self.assertEqual(cr.get("custom", "hasbackslash"), R'"foo\\bar"')
+        self.assertEqual(cr.get("custom", "hasquote"), R'"ab\"cd"')
+        self.assertEqual(cr.get("custom", "hastrailingbackslash"), R'"word\\"')
+        self.assertEqual(cr.get("custom", "hasunrecognized"), R'"p\qrs"')
+
+        # It is less obvious whether and what to eventually do with this.
+        self.assertEqual(cr.get("custom", "hasunescapedquotes"), '"ab"cd"e"')
+
+        # Cases where quote removal is clearly safe should happen even after those.
+        self.assertEqual(cr.get("custom", "ordinary"), "hello world")
+
+        # Cases without quotes should still parse correctly even after those, too.
+        self.assertEqual(cr.get("custom", "unquoted"), "good evening")
+
     def test_get_values_works_without_requiring_any_other_calls_first(self):
         file_obj = self._to_memcache(fixture_path("git_config_multiple"))
         cr = GitConfigParser(file_obj, read_only=True)

From f2b80410e96a256aed044fba0387eab0440a1525 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Jun 2025 00:09:49 -0400
Subject: [PATCH 971/994] Don't remove quotes if `\` or `"` are present inside

This is for single line quoting in the ConfigParser.

This leaves the changes in #2035 (as adjusted in #2036) intact for
the cases where it addressed #1923: when the `...` in `"..."`
(appearing in the value position on a single `{name} = {value}"`
line) has no occurrences of `\` or `"`, quote removal is enough.

But when `\` or `"` does appear, this suppresses quote removal.
This is with the idea that, while it would be better to interpret
such lines as Git does, we do not yet do that, so it is preferable
to return the same results we have in the past (which some programs
may already be handling themselves).

This should make the test introduced in the preceding commit pass.
But it will be even better to support more syntax, at least
well-formed escapes. As noted in the test, both the test and the
code under test can be adjusted for that.

(See comments in #2035 for context.)
---
 git/config.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/git/config.py b/git/config.py
index afec498f2..5fc099a27 100644
--- a/git/config.py
+++ b/git/config.py
@@ -505,13 +505,16 @@ def string_decode(v: str) -> str:
                     optval = optval.strip()
 
                     if len(optval) < 2 or optval[0] != '"':
-                        pass  # Nothing to treat as opening quotation.
+                        # Does not open quoting.
+                        pass
                     elif optval[-1] != '"':
+                        # Opens quoting and does not close: appears to start multi-line quoting.
                         is_multi_line = True
                         optval = string_decode(optval[1:])
-                    # END handle multi-line
-                    else:
+                    elif optval.find("\\", 1, -1) == -1 and optval.find('"', 1, -1) == -1:
+                        # Opens and closes quoting. Single line, and all we need is quote removal.
                         optval = optval[1:-1]
+                    # TODO: Handle other quoted content, especially well-formed backslash escapes.
 
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)

From 1b79d449c90cc6fca8220246e2de159d24666837 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Jun 2025 14:33:58 -0400
Subject: [PATCH 972/994] Improve line-ending consistency in requirements files

`requirements-dev.txt`, but none of the others, was tracked with
Windows-style (CRLF) line endings. This appears to have been the
case since it was introduced in a1b7634 (as `dev-requirements.txt`)
and not to be intentional.

This only changes how it is stored in the repository. This does not
change `.gitattributes` (it is not forced to have LF line endings
if automatic line-ending conversions are configured in Git).
---
 requirements-dev.txt | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index f626644af..01cb2d040 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,8 +1,8 @@
--r requirements.txt
--r test-requirements.txt
-
-# For additional local testing/linting - to be added elsewhere eventually.
-ruff
-shellcheck
-pytest-icdiff
-# pytest-profiling
+-r requirements.txt
+-r test-requirements.txt
+
+# For additional local testing/linting - to be added elsewhere eventually.
+ruff
+shellcheck
+pytest-icdiff
+# pytest-profiling

From 6f4f7f5137d63facb61eae2955e6c0801c71b7b5 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Jun 2025 15:05:03 -0400
Subject: [PATCH 973/994] Update Ruff configuration

This resolves two warnings about Ruff configuration, by:

- No longer setting `ignore-init-module-imports = true` explicitly,
  which was deprecated since `ruff` 0.4.4. We primarily use `ruff`
  via `pre-commit`, for which this deprecation has applied since we
  upgraded the version in `.pre-commit-config.yaml` from 0.4.3 to
  0.6.0 in d1582d1 (#1953).

  We continue to list `F401` ("Module imported but unused") as not
  automatically fixable, to avoid inadvertently removing imports
  that may be needed.

  See also:
  https://docs.astral.sh/ruff/settings/#lint_ignore-init-module-imports

- Rename the rule `TCH004` to `TC004`, since `TCH004` is the old
  name that may eventually be removed and that is deprecated since
  0.8.0. We upgraded `ruff` in `.pre-commit-config.yml` again in
  b7ce712 (#2031), from 0.6.0 to 0.11.12, at which point this
  deprecation applied.

  See also https://astral.sh/blog/ruff-v0.8.0.

These changes make those configuration-related warnings go away,
and no new diagnostics (errors/warnings) are produced when running
`ruff check` or `pre-commit run --all-files`. No F401-related
diagnostics are triggered when testing with explicit
`ignore-init-module-imports = false`, in preview mode or otherwise.

In addition, this commit makes two changes that are not needed to
resolve warnings:

- Stop excluding `E203` ("Whitespace before ':'"). That diagnostic
  is no longer failing with the current code here in the current
  version of `ruff`, and code changes that would cause it to fail
  would likely be accidentally mis-st

- Add the version lower bound `>=0.8` for `ruff` in
  `requirements-dev.txt`. That file is rarely used, as noted in
  a8a73ff7 (#1871), but as long as we have it, there may be a
  benefit to excluding dependency versions for which our
  configuration is no longer compatible. This is the only change in
  this commit outside of `pyproject.toml`.
---
 pyproject.toml       | 10 ++++------
 requirements-dev.txt |  2 +-
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 090972eed..0097e9951 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,16 +60,14 @@ lint.select = [
     # "UP",  # See: https://docs.astral.sh/ruff/rules/#pyupgrade-up
 ]
 lint.extend-select = [
-    # "A",     # See: https://pypi.org/project/flake8-builtins
-    "B",       # See: https://pypi.org/project/flake8-bugbear
-    "C4",      # See: https://pypi.org/project/flake8-comprehensions
-    "TCH004",  # See: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
+    # "A",    # See: https://pypi.org/project/flake8-builtins
+    "B",      # See: https://pypi.org/project/flake8-bugbear
+    "C4",     # See: https://pypi.org/project/flake8-comprehensions
+    "TC004",  # See: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
 ]
 lint.ignore = [
-    "E203",  # Whitespace before ':'
     "E731",  # Do not assign a `lambda` expression, use a `def`
 ]
-lint.ignore-init-module-imports = true
 lint.unfixable = [
     "F401",  # Module imported but unused
 ]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 01cb2d040..066b192b8 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,7 +2,7 @@
 -r test-requirements.txt
 
 # For additional local testing/linting - to be added elsewhere eventually.
-ruff
+ruff >=0.8
 shellcheck
 pytest-icdiff
 # pytest-profiling

From a36b8a5a726ee5a17cfd492e49c0b0b2a05b2136 Mon Sep 17 00:00:00 2001
From: Eliah Kagan <degeneracypressure@gmail.com>
Date: Sun, 8 Jun 2025 16:25:17 -0400
Subject: [PATCH 974/994] Always use a `def` instead of assigning a `lambda`

This stops listing Ruff rule `E731` ("Do not assign a `lambda`
expression, use a `def`") as ignored, and fixes all occurrences of
it:

- Spacing is manually adjusted so that readability is not harmed,
  while still satisfying the current formatting conventions.

- Although the affected test modules do not currently use type
  annotations, the non-test modules do. Some of the lambdas already
  had type annotations, by annotating the variable itself with an
  expression formed by subscripting `Callable`. This change
  preserves them, converting them to paramter and return type
  annotations in the resulting `def`. Where such type annotations
  were absent (in lambdas in non-test modules), or partly absent,
  all missing annotations are added to the `def`.

- Unused paramters are prefixed with a `_`.

- `IndexFile.checkout` assigned a lambda to `make_exc`, whose body
  was somewhat difficult to read. Separately from converting it to
  a `def`, this refactors the expression in the `return` statement
  to use code like `(x, *ys)` in place of `(x,) + tuple(ys)`.

This change does not appear to have introduced (nor fixed) any
`mypy` errors.

This only affects lambdas that were assigned directly to variables.
Other lambda expressions remain unchanged.
---
 git/index/base.py   | 19 +++++++++++++++----
 git/objects/tree.py |  4 +++-
 pyproject.toml      |  2 +-
 test/test_fun.py    |  6 +++++-
 test/test_index.py  |  5 ++++-
 test/test_tree.py   | 10 ++++++++--
 6 files changed, 36 insertions(+), 10 deletions(-)

diff --git a/git/index/base.py b/git/index/base.py
index a95762dca..7cc9d3ade 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -530,7 +530,10 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
             stage. That is, a file removed on the 'other' branch whose entries are at
             stage 3 will not have a stage 3 entry.
         """
-        is_unmerged_blob = lambda t: t[0] != 0
+
+        def is_unmerged_blob(t: Tuple[StageType, Blob]) -> bool:
+            return t[0] != 0
+
         path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
         for stage, blob in self.iter_blobs(is_unmerged_blob):
             path_map.setdefault(blob.path, []).append((stage, blob))
@@ -690,12 +693,17 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry
             This must be ensured in the calling code.
         """
         st = os.lstat(filepath)  # Handles non-symlinks as well.
+
         if S_ISLNK(st.st_mode):
             # In PY3, readlink is a string, but we need bytes.
             # In PY2, it was just OS encoded bytes, we assumed UTF-8.
-            open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
+            def open_stream() -> BinaryIO:
+                return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
         else:
-            open_stream = lambda: open(filepath, "rb")
+
+            def open_stream() -> BinaryIO:
+                return open(filepath, "rb")
+
         with open_stream() as stream:
             fprogress(filepath, False, filepath)
             istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
@@ -1336,8 +1344,11 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
             kwargs["as_process"] = True
             kwargs["istream"] = subprocess.PIPE
             proc = self.repo.git.checkout_index(args, **kwargs)
+
             # FIXME: Reading from GIL!
-            make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read())
+            def make_exc() -> GitCommandError:
+                return GitCommandError(("git-checkout-index", *args), 128, proc.stderr.read())
+
             checked_out_files: List[PathLike] = []
 
             for path in paths:
diff --git a/git/objects/tree.py b/git/objects/tree.py
index 09184a781..1845d0d0d 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -50,7 +50,9 @@
 
 # --------------------------------------------------------
 
-cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b)
+
+def cmp(a: str, b: str) -> int:
+    return (a > b) - (a < b)
 
 
 class TreeModifier:
diff --git a/pyproject.toml b/pyproject.toml
index 0097e9951..58ed81f17 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -66,7 +66,7 @@ lint.extend-select = [
     "TC004",  # See: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/
 ]
 lint.ignore = [
-    "E731",  # Do not assign a `lambda` expression, use a `def`
+    # If it becomes necessary to ignore any rules, list them here.
 ]
 lint.unfixable = [
     "F401",  # Module imported but unused
diff --git a/test/test_fun.py b/test/test_fun.py
index b8593b400..a456b8aab 100644
--- a/test/test_fun.py
+++ b/test/test_fun.py
@@ -243,6 +243,7 @@ def test_tree_traversal(self):
         B_old = self.rorepo.tree("1f66cfbbce58b4b552b041707a12d437cc5f400a")  # old base tree
 
         # Two very different trees.
+
         entries = traverse_trees_recursive(odb, [B_old.binsha, H.binsha], "")
         self._assert_tree_entries(entries, 2)
 
@@ -251,7 +252,10 @@ def test_tree_traversal(self):
         self._assert_tree_entries(oentries, 2)
 
         # Single tree.
-        is_no_tree = lambda i, d: i.type != "tree"
+
+        def is_no_tree(i, _d):
+            return i.type != "tree"
+
         entries = traverse_trees_recursive(odb, [B.binsha], "")
         assert len(entries) == len(list(B.traverse(predicate=is_no_tree)))
         self._assert_tree_entries(entries, 1)
diff --git a/test/test_index.py b/test/test_index.py
index c42032e70..cf3b90fa6 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -330,7 +330,10 @@ def test_index_file_from_tree(self, rw_repo):
         assert len([e for e in three_way_index.entries.values() if e.stage != 0])
 
         # ITERATE BLOBS
-        merge_required = lambda t: t[0] != 0
+
+        def merge_required(t):
+            return t[0] != 0
+
         merge_blobs = list(three_way_index.iter_blobs(merge_required))
         assert merge_blobs
         assert merge_blobs[0][0] in (1, 2, 3)
diff --git a/test/test_tree.py b/test/test_tree.py
index 73158113d..7ba93bd36 100644
--- a/test/test_tree.py
+++ b/test/test_tree.py
@@ -126,12 +126,18 @@ def test_traverse(self):
         assert len(list(root)) == len(list(root.traverse(depth=1)))
 
         # Only choose trees.
-        trees_only = lambda i, d: i.type == "tree"
+
+        def trees_only(i, _d):
+            return i.type == "tree"
+
         trees = list(root.traverse(predicate=trees_only))
         assert len(trees) == len([i for i in root.traverse() if trees_only(i, 0)])
 
         # Test prune.
-        lib_folder = lambda t, d: t.path == "lib"
+
+        def lib_folder(t, _d):
+            return t.path == "lib"
+
         pruned_trees = list(root.traverse(predicate=trees_only, prune=lib_folder))
         assert len(pruned_trees) < len(trees)
 

From 1d808917168ee292a9e4c5fb575a79133d253bb3 Mon Sep 17 00:00:00 2001
From: David <david0@users.noreply.github.com>
Date: Wed, 11 Jun 2025 14:05:31 +0200
Subject: [PATCH 975/994] 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 <degeneracypressure@gmail.com>
Date: Thu, 12 Jun 2025 02:39:11 -0400
Subject: [PATCH 976/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 14 Jun 2025 14:42:31 -0400
Subject: [PATCH 977/994] 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 <degeneracypressure@gmail.com>
Date: Sat, 14 Jun 2025 14:57:40 -0400
Subject: [PATCH 978/994] 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 <module>
        _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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 19:04:15 -0400
Subject: [PATCH 979/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 19:06:58 -0400
Subject: [PATCH 980/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 19:11:16 -0400
Subject: [PATCH 981/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 19:48:39 -0400
Subject: [PATCH 982/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 20:05:56 -0400
Subject: [PATCH 983/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 20:25:55 -0400
Subject: [PATCH 984/994] 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 <degeneracypressure@gmail.com>
Date: Sun, 15 Jun 2025 23:37:27 -0400
Subject: [PATCH 985/994] 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 <tombedor@gmail.com>
Date: Tue, 17 Jun 2025 09:03:52 -0700
Subject: [PATCH 986/994] 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 987/994] 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] <support@github.com>
---
 .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 988/994] 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] <support@github.com>
---
 .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 <niklasmertsch@web.de>
Date: Sun, 20 Jul 2025 14:17:49 +0200
Subject: [PATCH 989/994] 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 <niklasmertsch@web.de>
Date: Sun, 20 Jul 2025 14:17:58 +0200
Subject: [PATCH 990/994] 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 <t.c.w.webber@gmail.com>
Date: Tue, 22 Jul 2025 12:23:18 +0200
Subject: [PATCH 991/994] 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 <t.c.w.webber@gmail.com>
Date: Wed, 23 Jul 2025 07:12:27 +0200
Subject: [PATCH 992/994] 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 <sebastian.thiel@icloud.com>
Date: Thu, 24 Jul 2025 05:37:18 +0200
Subject: [PATCH 993/994] 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 994/994] 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] <support@github.com>
---
 .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
 
