diff -pruN 3.1.44-1/.github/workflows/alpine-test.yml 3.1.45-1/.github/workflows/alpine-test.yml
--- 3.1.44-1/.github/workflows/alpine-test.yml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.github/workflows/alpine-test.yml	2025-07-24 03:40:24.000000000 +0000
@@ -2,8 +2,11 @@ name: test-alpine
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
-  build:
+  test:
     runs-on: ubuntu-latest
 
     container:
@@ -16,10 +19,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-virtualenv
         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.
 
@@ -44,23 +47,23 @@ 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
-        . .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
+        . .venv/bin/activate
+        python -m pip install -U pip 'setuptools; python_version<"3.12"' wheel
 
     - name: Install project and test dependencies
       run: |
-        pip install ".[test]"
+        . .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 +72,5 @@ jobs:
 
     - name: Test with pytest
       run: |
+        . .venv/bin/activate
         pytest --color=yes -p no:sugar --instafail -vv
diff -pruN 3.1.44-1/.github/workflows/codeql.yml 3.1.45-1/.github/workflows/codeql.yml
--- 3.1.44-1/.github/workflows/codeql.yml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.github/workflows/codeql.yml	2025-07-24 03:40:24.000000000 +0000
@@ -19,38 +19,48 @@ on:
 
 jobs:
   analyze:
-    name: 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
-    # Consider using larger runners for possible analysis time improvements.
+    #   - 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' }}
-    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
-
+        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 }}
-        setup-python-dependencies: false
+        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.
@@ -58,21 +68,21 @@ jobs:
         # 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
-
+    # 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 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
+    - 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
diff -pruN 3.1.44-1/.github/workflows/cygwin-test.yml 3.1.45-1/.github/workflows/cygwin-test.yml
--- 3.1.44-1/.github/workflows/cygwin-test.yml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.github/workflows/cygwin-test.yml	2025-07-24 03:40:24.000000000 +0000
@@ -2,35 +2,47 @@ name: test-cygwin
 
 on: [push, pull_request, workflow_dispatch]
 
-jobs:
-  build:
-    runs-on: windows-latest
+permissions:
+  contents: read
 
+jobs:
+  test:
     strategy:
+      matrix:
+        selection: [fast, perf]
+        include:
+        - selection: fast
+          additional-pytest-args: --ignore=test/performance
+        - selection: perf
+          additional-pytest-args: test/performance
+
       fail-fast: false
 
+    runs-on: windows-latest
+
     env:
       CHERE_INVOKING: "1"
       CYGWIN_NOWINPATH: "1"
 
     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
       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:
         fetch-depth: 0
 
-    - name: Set up Cygwin
-      uses: egor-tensin/setup-cygwin@v4
+    - name: Install Cygwin
+      uses: cygwin/cygwin-install-action@v6
       with:
-        packages: python39=3.9.16-1 python39-pip python39-virtualenv git
+        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
       run: |
@@ -55,19 +67,18 @@ jobs:
         # and cause subsequent tests to fail
         cat test/fixtures/.gitconfig >> ~/.gitconfig
 
-    - name: Ensure the "pip" command is available
+    - name: Set up virtual environment
       run: |
-        # This is used unless, and before, an updated pip is installed.
-        ln -s pip3 /usr/bin/pip
+        python3.9 -m venv .venv
+        echo 'BASH_ENV=.venv/bin/activate' >>"$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
+        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: |
@@ -77,6 +88,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
+        pytest --color=yes -p no:sugar --instafail -vv ${{ matrix.additional-pytest-args }}
diff -pruN 3.1.44-1/.github/workflows/lint.yml 3.1.45-1/.github/workflows/lint.yml
--- 3.1.44-1/.github/workflows/lint.yml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.github/workflows/lint.yml	2025-07-24 03:40:24.000000000 +0000
@@ -2,6 +2,9 @@ name: Lint
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   lint:
     runs-on: ubuntu-latest
diff -pruN 3.1.44-1/.github/workflows/pythonpackage.yml 3.1.45-1/.github/workflows/pythonpackage.yml
--- 3.1.44-1/.github/workflows/pythonpackage.yml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.github/workflows/pythonpackage.yml	2025-07-24 03:40:24.000000000 +0000
@@ -9,19 +9,30 @@ permissions:
   contents: read
 
 jobs:
-  build:
+  test:
     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"]
+        os-type: [ubuntu, macos, windows]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"]
         exclude:
-        - os: "macos-latest"
-          python-version: "3.7"
+        - 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
+          python-version: "3.7"
+          os-ver: "22.04"
         - experimental: false
 
-    runs-on: ${{ matrix.os }}
+      fail-fast: false
+
+    runs-on: ${{ matrix.os-type }}-${{ matrix.os-ver }}
 
     defaults:
       run:
@@ -39,9 +50,10 @@ jobs:
         allow-prereleases: ${{ matrix.experimental }}
 
     - name: Set up WSL (Windows)
-      if: startsWith(matrix.os, 'windows')
-      uses: Vampire/setup-wsl@v3.1.1
+      if: matrix.os-type == 'windows'
+      uses: Vampire/setup-wsl@v6.0.0
       with:
+        wsl-version: 1
         distribution: Alpine
         additional-packages: bash
 
@@ -59,12 +71,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: |
@@ -76,7 +87,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'
@@ -85,10 +96,11 @@ jobs:
 
     - name: Check types with mypy
       run: |
-        mypy --python-version=${{ matrix.python-version }}
+        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
+        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
@@ -101,5 +113,5 @@ jobs:
     - name: Documentation
       if: matrix.python-version != '3.7'
       run: |
-        pip install ".[doc]"
+        pip install '.[doc]'
         make -C doc html
diff -pruN 3.1.44-1/.gitignore 3.1.45-1/.gitignore
--- 3.1.44-1/.gitignore	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.gitignore	2025-07-24 03:40:24.000000000 +0000
@@ -10,6 +10,8 @@ __pycache__/
 # Transient editor files
 *.swp
 *~
+\#*#
+.#*#
 
 # Editor configuration
 nbproject
diff -pruN 3.1.44-1/.pre-commit-config.yaml 3.1.45-1/.pre-commit-config.yaml
--- 3.1.44-1/.pre-commit-config.yaml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/.pre-commit-config.yaml	2025-07-24 03:40:24.000000000 +0000
@@ -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 -pruN 3.1.44-1/AUTHORS 3.1.45-1/AUTHORS
--- 3.1.44-1/AUTHORS	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/AUTHORS	2025-07-24 03:40:24.000000000 +0000
@@ -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.
diff -pruN 3.1.44-1/SECURITY.md 3.1.45-1/SECURITY.md
--- 3.1.44-1/SECURITY.md	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/SECURITY.md	2025-07-24 03:40:24.000000000 +0000
@@ -11,4 +11,6 @@ Only the latest version of GitPython can
 
 ## 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/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.
diff -pruN 3.1.44-1/VERSION 3.1.45-1/VERSION
--- 3.1.44-1/VERSION	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/VERSION	2025-07-24 03:40:24.000000000 +0000
@@ -1 +1 @@
-3.1.44
+3.1.45
diff -pruN 3.1.44-1/debian/changelog 3.1.45-1/debian/changelog
--- 3.1.44-1/debian/changelog	2025-03-05 18:51:18.000000000 +0000
+++ 3.1.45-1/debian/changelog	2025-09-03 00:25:03.000000000 +0000
@@ -1,3 +1,16 @@
+python-git (3.1.45-1) unstable; urgency=medium
+
+  * Team upload.
+
+  [ Geovanna Maciel ]
+  * Update Standards-Version to 4.7.0.
+
+  [ Colin Watson ]
+  * New upstream release.
+  * Standards-Version: 4.7.2.
+
+ -- Colin Watson <cjwatson@debian.org>  Wed, 03 Sep 2025 01:25:03 +0100
+
 python-git (3.1.44-1) unstable; urgency=medium
 
   * New upstream version 3.1.44
@@ -7,7 +20,7 @@ python-git (3.1.44-1) unstable; urgency=
 python-git (3.1.37-3) unstable; urgency=medium
 
   * Team upload.
-  * Source-only upload. 
+  * Source-only upload.
 
  -- Jérémy Lal <kapouer@melix.org>  Fri, 12 Jan 2024 16:11:11 +0100
 
diff -pruN 3.1.44-1/debian/control 3.1.45-1/debian/control
--- 3.1.44-1/debian/control	2025-03-05 18:42:06.000000000 +0000
+++ 3.1.45-1/debian/control	2025-09-03 00:25:03.000000000 +0000
@@ -17,7 +17,7 @@ Build-Depends: debhelper-compat (= 13),
                python3-pytest <!nocheck>,
                python3-ddt
 Build-Depends-Indep: python3-gitdb,
-Standards-Version: 4.6.2
+Standards-Version: 4.7.2
 Vcs-Git: https://salsa.debian.org/python-team/packages/python-git.git
 Vcs-Browser: https://salsa.debian.org/python-team/packages/python-git
 Homepage: https://github.com/gitpython-developers/GitPython
diff -pruN 3.1.44-1/doc/Makefile 3.1.45-1/doc/Makefile
--- 3.1.44-1/doc/Makefile	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/doc/Makefile	2025-07-24 03:40:24.000000000 +0000
@@ -3,7 +3,7 @@
 
 # You can set these variables from the command line.
 BUILDDIR      = build
-SPHINXOPTS    = -W
+SPHINXOPTS    =
 SPHINXBUILD   = sphinx-build
 PAPER         =
 
diff -pruN 3.1.44-1/doc/source/changes.rst 3.1.45-1/doc/source/changes.rst
--- 3.1.44-1/doc/source/changes.rst	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/doc/source/changes.rst	2025-07-24 03:40:24.000000000 +0000
@@ -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
 ======
 
diff -pruN 3.1.44-1/fuzzing/fuzz-targets/fuzz_submodule.py 3.1.45-1/fuzzing/fuzz-targets/fuzz_submodule.py
--- 3.1.44-1/fuzzing/fuzz-targets/fuzz_submodule.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/fuzzing/fuzz-targets/fuzz_submodule.py	2025-07-24 03:40:24.000000000 +0000
@@ -9,11 +9,17 @@ from utils import (
     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():
diff -pruN 3.1.44-1/git/cmd.py 3.1.45-1/git/cmd.py
--- 3.1.44-1/git/cmd.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/cmd.py	2025-07-24 03:40:24.000000000 +0000
@@ -60,6 +60,11 @@ from typing import (
     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:
@@ -207,7 +212,7 @@ def handle_process_output(
                 )
             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.
@@ -268,12 +273,12 @@ if sys.platform == "win32":
         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.)
@@ -308,6 +313,230 @@ def dict_to_slots_and__excluded_are_none
 
 ## -- 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."
@@ -321,7 +550,7 @@ _USE_SHELL_DANGER_MESSAGE = (
 )
 
 
-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,
@@ -337,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:
@@ -728,221 +957,9 @@ class Git(metaclass=_GitMeta):
                         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: TypeAlias = _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: TypeAlias = _CatFileContentStream
 
     def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
         """Initialize this instance with:
@@ -971,7 +988,7 @@ class Git(metaclass=_GitMeta):
 
     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:
@@ -1319,7 +1336,7 @@ class Git(metaclass=_GitMeta):
                 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 -pruN 3.1.44-1/git/config.py 3.1.45-1/git/config.py
--- 3.1.44-1/git/config.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/config.py	2025-07-24 03:40:24.000000000 +0000
@@ -87,15 +87,15 @@ class MetaParserBuilder(abc.ABCMeta):  #
             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
@@ -496,19 +496,26 @@ class GitConfigParser(cp.RawConfigParser
                 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()
-                    if optval == '""':
-                        optval = ""
-                    # END handle empty string
-                    optname = self.optionxform(optname.rstrip())
-                    if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
+
+                    if len(optval) < 2 or optval[0] != '"':
+                        # 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
+                    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)
                 else:
diff -pruN 3.1.44-1/git/index/base.py 3.1.45-1/git/index/base.py
--- 3.1.44-1/git/index/base.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/index/base.py	2025-07-24 03:40:24.000000000 +0000
@@ -508,7 +508,7 @@ class IndexFile(LazyMixin, git_diff.Diff
 
         :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():
@@ -530,7 +530,10 @@ class IndexFile(LazyMixin, git_diff.Diff
             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))
@@ -655,7 +658,10 @@ class IndexFile(LazyMixin, git_diff.Diff
             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"]]]
@@ -687,12 +693,17 @@ class IndexFile(LazyMixin, git_diff.Diff
             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))
@@ -767,7 +778,7 @@ class IndexFile(LazyMixin, git_diff.Diff
             - 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
@@ -786,7 +797,7 @@ class IndexFile(LazyMixin, git_diff.Diff
 
                 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.
@@ -812,7 +823,7 @@ class IndexFile(LazyMixin, git_diff.Diff
 
             - :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.
 
@@ -995,7 +1006,7 @@ class IndexFile(LazyMixin, git_diff.Diff
 
                 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.
 
@@ -1333,8 +1344,11 @@ class IndexFile(LazyMixin, git_diff.Diff
             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 -pruN 3.1.44-1/git/index/fun.py 3.1.45-1/git/index/fun.py
--- 3.1.44-1/git/index/fun.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/index/fun.py	2025-07-24 03:40:24.000000000 +0000
@@ -207,7 +207,7 @@ def read_header(stream: IO[bytes]) -> Tu
     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
 
 
diff -pruN 3.1.44-1/git/objects/base.py 3.1.45-1/git/objects/base.py
--- 3.1.44-1/git/objects/base.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/objects/base.py	2025-07-24 03:40:24.000000000 +0000
@@ -122,7 +122,7 @@ class Object(LazyMixin):
         :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 -pruN 3.1.44-1/git/objects/commit.py 3.1.45-1/git/objects/commit.py
--- 3.1.44-1/git/objects/commit.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/objects/commit.py	2025-07-24 03:40:24.000000000 +0000
@@ -289,7 +289,7 @@ class Commit(base.Object, TraversableIte
         """
         :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 @@ class Commit(base.Object, TraversableIte
         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 -pruN 3.1.44-1/git/objects/submodule/base.py 3.1.45-1/git/objects/submodule/base.py
--- 3.1.44-1/git/objects/submodule/base.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/objects/submodule/base.py	2025-07-24 03:40:24.000000000 +0000
@@ -11,6 +11,7 @@ import os.path as osp
 import stat
 import sys
 import uuid
+import urllib
 
 import git
 from git.cmd import Git
@@ -353,6 +354,11 @@ class Submodule(IndexObject, Traversable
                 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,
@@ -794,9 +800,13 @@ class Submodule(IndexObject, Traversable
                     + "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,
diff -pruN 3.1.44-1/git/objects/tree.py 3.1.45-1/git/objects/tree.py
--- 3.1.44-1/git/objects/tree.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/objects/tree.py	2025-07-24 03:40:24.000000000 +0000
@@ -50,7 +50,9 @@ TraversedTreeTup = Union[Tuple[Union["Tr
 
 # --------------------------------------------------------
 
-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 -pruN 3.1.44-1/git/refs/log.py 3.1.45-1/git/refs/log.py
--- 3.1.44-1/git/refs/log.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/refs/log.py	2025-07-24 03:40:24.000000000 +0000
@@ -126,7 +126,7 @@ class RefLogEntry(Tuple[str, str, Actor,
         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 -pruN 3.1.44-1/git/refs/symbolic.py 3.1.45-1/git/refs/symbolic.py
--- 3.1.44-1/git/refs/symbolic.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/refs/symbolic.py	2025-07-24 03:40:24.000000000 +0000
@@ -39,7 +39,6 @@ from git.types import AnyGitObject, Path
 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 @@ class SymbolicReference:
         # 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",
-    )
-
-    object = property(
-        _get_object,
-        set_object,  # type: ignore[arg-type]
-        doc="Return the object our ref currently refers to",
-    )
+    @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.setter
+    def object(self, object: Union[AnyGitObject, "SymbolicReference", str]) -> "SymbolicReference":
+        return self.set_object(object)
 
     def _get_reference(self) -> "SymbolicReference":
         """
@@ -496,12 +501,14 @@ class SymbolicReference:
         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 -pruN 3.1.44-1/git/repo/base.py 3.1.45-1/git/repo/base.py
--- 3.1.44-1/git/repo/base.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/repo/base.py	2025-07-24 03:40:24.000000000 +0000
@@ -354,21 +354,19 @@ class Repo:
     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]:
         """
@@ -514,7 +512,7 @@ class Repo:
     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:
@@ -885,13 +883,14 @@ class Repo:
         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 @@ class Repo:
             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,
diff -pruN 3.1.44-1/git/repo/fun.py 3.1.45-1/git/repo/fun.py
--- 3.1.44-1/git/repo/fun.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/repo/fun.py	2025-07-24 03:40:24.000000000 +0000
@@ -301,7 +301,13 @@ def rev_parse(repo: "Repo", rev: str) ->
 
             # 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)
@@ -399,7 +405,7 @@ def rev_parse(repo: "Repo", rev: str) ->
             # 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 -pruN 3.1.44-1/git/util.py 3.1.45-1/git/util.py
--- 3.1.44-1/git/util.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/git/util.py	2025-07-24 03:40:24.000000000 +0000
@@ -464,6 +464,12 @@ def _is_cygwin_git(git_executable: str)
 
             # Just a name given, not a real path.
             uname_cmd = osp.join(git_dir, "uname")
+
+            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([uname_cmd], stdout=subprocess.PIPE, universal_newlines=True)
             uname_out, _ = process.communicate()
             # retcode = process.poll()
@@ -484,7 +490,9 @@ def is_cygwin_git(git_executable: PathLi
 
 
 def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool:
-    if sys.platform == "win32":  # TODO: See if we can use `sys.platform != "cygwin"`.
+    # 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:
         return False
@@ -1200,8 +1208,6 @@ class IterableList(List[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 +1220,6 @@ class IterableList(List[T_IterableObj]):
         # 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
diff -pruN 3.1.44-1/pyproject.toml 3.1.45-1/pyproject.toml
--- 3.1.44-1/pyproject.toml	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/pyproject.toml	2025-07-24 03:40:24.000000000 +0000
@@ -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`
+    # If it becomes necessary to ignore any rules, list them here.
 ]
-lint.ignore-init-module-imports = true
 lint.unfixable = [
     "F401",  # Module imported but unused
 ]
diff -pruN 3.1.44-1/requirements-dev.txt 3.1.45-1/requirements-dev.txt
--- 3.1.44-1/requirements-dev.txt	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/requirements-dev.txt	2025-07-24 03:40:24.000000000 +0000
@@ -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 >=0.8
+shellcheck
+pytest-icdiff
+# pytest-profiling
diff -pruN 3.1.44-1/requirements.txt 3.1.45-1/requirements.txt
--- 3.1.44-1/requirements.txt	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/requirements.txt	2025-07-24 03:40:24.000000000 +0000
@@ -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"
diff -pruN 3.1.44-1/setup.py 3.1.45-1/setup.py
--- 3.1.44-1/setup.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/setup.py	2025-07-24 03:40:24.000000000 +0000
@@ -95,7 +95,6 @@ setup(
         # "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",
diff -pruN 3.1.44-1/test/fixtures/git_config_with_empty_quotes 3.1.45-1/test/fixtures/git_config_with_empty_quotes
--- 3.1.44-1/test/fixtures/git_config_with_empty_quotes	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_empty_quotes	2025-07-24 03:40:24.000000000 +0000
@@ -0,0 +1,2 @@
+[core]
+  filemode = ""
diff -pruN 3.1.44-1/test/fixtures/git_config_with_extra_whitespace 3.1.45-1/test/fixtures/git_config_with_extra_whitespace
--- 3.1.44-1/test/fixtures/git_config_with_extra_whitespace	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_extra_whitespace	2025-07-24 03:40:24.000000000 +0000
@@ -0,0 +1,2 @@
+[init]
+  defaultBranch =  trunk  
diff -pruN 3.1.44-1/test/fixtures/git_config_with_quotes 3.1.45-1/test/fixtures/git_config_with_quotes
--- 3.1.44-1/test/fixtures/git_config_with_quotes	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_quotes	2025-07-24 03:40:24.000000000 +0000
@@ -0,0 +1,3 @@
+[user]
+  name = "Cody Veal"
+  email = "cveal05@gmail.com"
diff -pruN 3.1.44-1/test/fixtures/git_config_with_quotes_escapes 3.1.45-1/test/fixtures/git_config_with_quotes_escapes
--- 3.1.44-1/test/fixtures/git_config_with_quotes_escapes	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_quotes_escapes	2025-07-24 03:40:24.000000000 +0000
@@ -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 -pruN 3.1.44-1/test/fixtures/git_config_with_quotes_whitespace_inside 3.1.45-1/test/fixtures/git_config_with_quotes_whitespace_inside
--- 3.1.44-1/test/fixtures/git_config_with_quotes_whitespace_inside	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_quotes_whitespace_inside	2025-07-24 03:40:24.000000000 +0000
@@ -0,0 +1,2 @@
+[core]
+  commentString = "# "
diff -pruN 3.1.44-1/test/fixtures/git_config_with_quotes_whitespace_outside 3.1.45-1/test/fixtures/git_config_with_quotes_whitespace_outside
--- 3.1.44-1/test/fixtures/git_config_with_quotes_whitespace_outside	1970-01-01 00:00:00.000000000 +0000
+++ 3.1.45-1/test/fixtures/git_config_with_quotes_whitespace_outside	2025-07-24 03:40:24.000000000 +0000
@@ -0,0 +1,2 @@
+[init]
+  defaultBranch =  "trunk"  
diff -pruN 3.1.44-1/test/lib/helper.py 3.1.45-1/test/lib/helper.py
--- 3.1.44-1/test/lib/helper.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/lib/helper.py	2025-07-24 03:40:24.000000000 +0000
@@ -415,9 +415,15 @@ class VirtualEnvironment:
 
         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
diff -pruN 3.1.44-1/test/test_config.py 3.1.45-1/test/test_config.py
--- 3.1.44-1/test/test_config.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_config.py	2025-07-24 03:40:24.000000000 +0000
@@ -391,13 +391,17 @@ class TestBase(TestCase):
         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(),
             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)
 
@@ -406,6 +410,44 @@ class TestBase(TestCase):
         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_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"), "# ")
+
+    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)
diff -pruN 3.1.44-1/test/test_fun.py 3.1.45-1/test/test_fun.py
--- 3.1.44-1/test/test_fun.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_fun.py	2025-07-24 03:40:24.000000000 +0000
@@ -243,6 +243,7 @@ class TestFun(TestBase):
         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 @@ class TestFun(TestBase):
         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 -pruN 3.1.44-1/test/test_git.py 3.1.45-1/test/test_git.py
--- 3.1.44-1/test/test_git.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_git.py	2025-07-24 03:40:24.000000000 +0000
@@ -747,7 +747,7 @@ class TestGit(TestBase):
 
         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"))
@@ -762,17 +762,18 @@ class TestGit(TestBase):
     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
+        actual_lines = [None, [], []]
 
-        def counter_stdout(line):
-            count[1] += 1
+        def stdout_handler(line):
+            actual_lines[1].append(line)
 
-        def counter_stderr(line):
-            count[2] += 1
+        def stderr_handler(line):
+            actual_lines[2].append(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")),
         ]
@@ -784,10 +785,10 @@ class TestGit(TestBase):
             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(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()
diff -pruN 3.1.44-1/test/test_index.py 3.1.45-1/test/test_index.py
--- 3.1.44-1/test/test_index.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_index.py	2025-07-24 03:40:24.000000000 +0000
@@ -16,6 +16,7 @@ from stat import S_ISLNK, ST_MODE
 import subprocess
 import sys
 import tempfile
+from unittest import mock
 
 from gitdb.base import IStream
 
@@ -329,7 +330,10 @@ class TestIndex(TestBase):
         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)
@@ -1015,6 +1019,27 @@ class TestIndex(TestBase):
         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.",
diff -pruN 3.1.44-1/test/test_installation.py 3.1.45-1/test/test_installation.py
--- 3.1.44-1/test/test_installation.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_installation.py	2025-07-24 03:40:24.000000000 +0000
@@ -2,6 +2,7 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ast
+import functools
 import os
 import subprocess
 
@@ -11,50 +12,22 @@ from test.lib import TestBase, VirtualEn
 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,
-        )
-        self.assertEqual(
-            0,
-            result.returncode,
-            msg=result.stderr or result.stdout or "Can't install project",
-        )
+        result = run([venv.pip, "install", "."])
+        self._check_result(result, "Can't install project")
 
-        result = subprocess.run(
-            [venv.python, "-c", "import git"],
-            stdout=subprocess.PIPE,
-            cwd=venv.sources,
-        )
-        self.assertEqual(
-            0,
-            result.returncode,
-            msg=result.stderr or result.stdout or "Self-test failed",
-        )
+        result = run([venv.python, "-c", "import git"])
+        self._check_result(result, "Self-test failed")
 
-        result = subprocess.run(
-            [venv.python, "-c", "import gitdb; import smmap"],
-            stdout=subprocess.PIPE,
-            cwd=venv.sources,
-        )
-        self.assertEqual(
-            0,
-            result.returncode,
-            msg=result.stderr or result.stdout or "Dependencies not installed",
-        )
+        result = run([venv.python, "-c", "import gitdb; import smmap"])
+        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 = subprocess.run(
-            [venv.python, "-c", "import sys; import git; print(sys.path)"],
-            stdout=subprocess.PIPE,
-            cwd=venv.sources,
-        )
-        syspath = result.stdout.decode("utf-8").splitlines()[0]
+        result = run([venv.python, "-c", "import sys; import git; print(sys.path)"])
+        syspath = result.stdout.splitlines()[0]
         syspath = ast.literal_eval(syspath)
         self.assertEqual(
             "",
@@ -64,10 +37,37 @@ class TestInstallation(TestBase):
 
     @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
+
+        # Create a convenience function to run commands in it.
+        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,
+            msg=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}"
diff -pruN 3.1.44-1/test/test_quick_doc.py 3.1.45-1/test/test_quick_doc.py
--- 3.1.44-1/test/test_quick_doc.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_quick_doc.py	2025-07-24 03:40:24.000000000 +0000
@@ -173,7 +173,7 @@ class QuickDoc(TestBase):
         # [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)
 
diff -pruN 3.1.44-1/test/test_repo.py 3.1.45-1/test/test_repo.py
--- 3.1.44-1/test/test_repo.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_repo.py	2025-07-24 03:40:24.000000000 +0000
@@ -1064,9 +1064,9 @@ class TestRepo(TestBase):
         # 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
 
diff -pruN 3.1.44-1/test/test_submodule.py 3.1.45-1/test/test_submodule.py
--- 3.1.44-1/test/test_submodule.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_submodule.py	2025-07-24 03:40:24.000000000 +0000
@@ -755,6 +755,22 @@ class TestSubmodule(TestBase):
 
     @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):
         repo_path = osp.join(rwdir, "parent")
         repo = git.Repo.init(repo_path)
@@ -1334,3 +1350,23 @@ class TestSubmodule(TestBase):
             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"
diff -pruN 3.1.44-1/test/test_tree.py 3.1.45-1/test/test_tree.py
--- 3.1.44-1/test/test_tree.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_tree.py	2025-07-24 03:40:24.000000000 +0000
@@ -126,12 +126,18 @@ class TestTree(TestBase):
         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)
 
diff -pruN 3.1.44-1/test/test_util.py 3.1.45-1/test/test_util.py
--- 3.1.44-1/test/test_util.py	2025-01-02 07:27:54.000000000 +0000
+++ 3.1.45-1/test/test_util.py	2025-07-24 03:40:24.000000000 +0000
@@ -34,6 +34,7 @@ from git.util import (
     LockFile,
     cygpath,
     decygpath,
+    is_cygwin_git,
     get_user_id,
     remove_password_if_present,
     rmtree,
@@ -349,6 +350,24 @@ class TestCygpath:
         assert wcpath == wpath.replace("/", "\\"), cpath
 
 
+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:
+            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."""
 
