diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml
index bd09a939b..a9c29117e 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
@@ -23,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
 
@@ -44,20 +47,19 @@ 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 $(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/codeql.yml b/.github/workflows/codeql.yml
index ae5241898..9191471c3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -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
+      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`
+    # 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 --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml
index 278777907..5c42c8583 100644
--- a/.github/workflows/cygwin-test.yml
+++ b/.github/workflows/cygwin-test.yml
@@ -2,13 +2,24 @@ name: test-cygwin
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   test:
-    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
 
+    runs-on: windows-latest
+
     env:
       CHERE_INVOKING: "1"
       CYGWIN_NOWINPATH: "1"
@@ -21,16 +32,16 @@ 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
+    - uses: actions/checkout@v5
       with:
         fetch-depth: 0
 
     - name: Install Cygwin
-      uses: cygwin/cygwin-install-action@v5
+      uses: cygwin/cygwin-install-action@v6
       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
@@ -56,23 +67,18 @@ 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
+        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: |
-        # 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: |
@@ -82,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 --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a0e81a993..16978f9a8 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,12 +2,15 @@ name: Lint
 
 on: [push, pull_request, workflow_dispatch]
 
+permissions:
+  contents: read
+
 jobs:
   lint:
     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 5bedb6107..4e5d82a55 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -39,28 +39,19 @@ jobs:
         shell: bash --noprofile --norc -exo pipefail {0}
 
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
       with:
         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
+      uses: Vampire/setup-wsl@v6.0.0
       with:
         wsl-version: 1
         distribution: Alpine
@@ -80,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: |
@@ -106,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
@@ -122,5 +113,5 @@ jobs:
     - name: Documentation
       if: matrix.python-version != '3.7'
       run: |
-        pip install ".[doc]"
+        pip install '.[doc]'
         make -C doc html
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
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/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.
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/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         =
 
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
 ======
 
diff --git a/git/cmd.py b/git/cmd.py
index 2048a43fa..15d7820df 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -60,6 +60,11 @@
     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 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.
@@ -268,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.)
@@ -308,6 +313,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."
@@ -321,7 +550,7 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
 )
 
 
-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 @@ 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: 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 @@ 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:
@@ -1319,7 +1336,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/config.py b/git/config.py
index de3508360..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
@@ -496,19 +496,26 @@ 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()
-                    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 --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
diff --git a/git/index/base.py b/git/index/base.py
index 65b1f9308..7cc9d3ade 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():
@@ -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))
@@ -770,7 +778,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 +797,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 +823,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 +1006,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.
 
@@ -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/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
 
 
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/objects/submodule/base.py b/git/objects/submodule/base.py
index fa60bcdaf..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
@@ -353,6 +354,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,
@@ -794,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,
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/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/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..7e918df8c 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]:
         """
@@ -514,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:
@@ -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,
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/git/util.py b/git/util.py
index 9e8ac821d..0aff5eb64 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).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: 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"`.
+    # 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 @@ 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 +1220,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
diff --git a/pyproject.toml b/pyproject.toml
index 090972eed..58ed81f17 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`
+    # 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 --git a/requirements-dev.txt b/requirements-dev.txt
index f626644af..066b192b8 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 >=0.8
+shellcheck
+pytest-icdiff
+# pytest-profiling
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"
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",
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/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 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/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/fixtures/git_config_with_quotes_whitespace_inside b/test/fixtures/git_config_with_quotes_whitespace_inside
new file mode 100644
index 000000000..c6014cc61
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes_whitespace_inside
@@ -0,0 +1,2 @@
+[core]
+  commentString = "# "
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/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
diff --git a/test/test_config.py b/test/test_config.py
index 92997422d..8e1007d9e 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -391,13 +391,17 @@ 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(),
             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 @@ 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_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 --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_git.py b/test/test_git.py
index 274511f8d..4a54d0d9b 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"))
@@ -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")),
         ]
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_installation.py b/test/test_installation.py
index a35826bd0..7c82bd403 100644
--- a/test/test_installation.py
+++ b/test/test_installation.py
@@ -2,67 +2,32 @@
 # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
 
 import ast
+import functools
 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)
+        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(
             "",
@@ -72,10 +37,37 @@ 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
+
+        # 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 --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)
 
diff --git a/test/test_submodule.py b/test/test_submodule.py
index d88f9dab0..4a248eb60 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):
@@ -1334,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"
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)
 
diff --git a/test/test_util.py b/test/test_util.py
index dad2f3dcd..000830f41 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):
+        # 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."""
 
