Skip to content

Git3 Compat + gix speedup for GitPython - #2274

Merged
Byron merged 22 commits into
mainfrom
gix-backend
Oct 24, 2026
Merged

Byron merged 22 commits into
mainfrom
gix-backend

Conversation

@Byron

@Byron Byron commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Tasks

  • refackiew - no need actually, but perusing I shall
  • Update to latest GixPython
  • update readme to talk about GixPython
Details

Created by Codex on behalf of Byron. Byron will review before this is ready to merge.

Summary

[!IMPORTANT]
This PR will become GitPython 3.3 some weeks before Git 3 is officially released in March.
Users can already test and run their software against this branch. Please report compatibility problems and provide feedback here in this PR.

To try it with Git 2.52 or newer installed:

python -m pip install --upgrade "git+https://github.com/gitpython-developers/GitPython.git@gix-backend"

GitPython now delegates repository discovery, configuration, references, reflogs, object storage, tree construction, index operations, and revision parsing to guarded Git commands. The default backend supports SHA-1 and SHA-256 objects with either files or reftable reference storage, without interpreting Git's binary storage formats in Python.

The Python GitDB storage implementation is removed; git.GitDB and git.db.GitDB remain compatibility aliases for the managed GitCmdObjectDB backend. Raw repo.git command access remains available. The migration notes in changes.rst describe the API changes and differences in Git CLI behavior.

Changes

1. Git CLI implementation of the GitPython API surface

The ordinary installation implements the supported GitPython API through guarded git.cmd.Git calls. Git owns repository storage and format handling, including SHA-1/SHA-256 objects and files/reftable references. This removes Python storage-format parsing from the default backend while retaining common documented workflows and raw repo.git access. Removed low-level APIs have replacements documented in the changelog. The existing context and migration notes below describe safety controls and low-level API changes.

2. GixPython implementation for better performance

The optional GitPython[gix] installation uses the published GixPython==0.2.0 release on CPython 3.11+. GitPython selects it automatically when the gix module is importable; the ordinary installation continues to support Python 3.8+. Set GIT_PYTHON_BACKEND=cli or GIT_PYTHON_BACKEND=gix before importing GitPython to select a backend explicitly. To try the native backend:

python -m pip install --upgrade "GitPython[gix] @ git+https://github.com/gitpython-developers/GitPython.git@gix-backend"

Supported library-managed operations use Gitoxide through GixPython's gix API for object reads/writes, revision and graph queries, tree enumeration and construction, index reads, references and reflogs, commit creation/statistics, tree diffs, status, untracked files, and ignore matching. Unsupported formats and options retain the guarded CLI implementation. In particular, reftable and extensions.compatObjectFormat repositories currently use Git. Native mutation failures are reported without attempting a second CLI mutation. Private index edits and tag object storage use Git until Gix exposes strict validation; index state in Python consists only of opaque file bytes. Commit/tag reads use structured Gix decoders where compatible; commit text requiring encoding conversion uses Git. Public repo.git commands retain their CLI behavior.

Historical local measurements, using official GixPython 0.1.0 on CPython 3.12.14, macOS arm64, before the 0.2.0 fixture migration:

Workload Git CLI GixPython Observed speedup
Full optimized suite, with coverage 755.78 s (12m36s) 404.61 s (6m45s) 1.87×
Top-level submodule tests, without coverage 178.75 s 88.63 s 2.02×
Tree tests, without coverage 3.86 s 1.66 s 2.33×

These are individual local validation runs, not statistical benchmarks or a guarantee for every application. The full suites collect different backend-specific tests; the focused selections overlap. See the backend guide and performance journal for conversion coverage, remaining CLI cases, reproduction details, and the GixPython/Gitoxide follow-up ledger (GIX-1 through GIX-30). Repository handles are now retained and refreshed through the shared accessor; the historical timings above predate that work.

3. Scripted test fixtures and CI

Both backend test suites now require GixPython 0.2.0 for gix.testtools. Initial repository and filesystem states live in Bash scripts under tests/fixtures. Read-only tests reuse cached output; mutating tests receive independent writable copies. Scripts re-execute only when their contents cannot be copied or relocated correctly, including link/permission cases and private absolute paths. Operations under test remain in Python, and Git CLI behavior defines expectations for both backends. Generated fixture caches and archives are ignored; no generated archives are checked in.

Full fixture suites run on Python 3.11+ with Bash and Git 2.52+. Python 3.8–3.10 retain installation/import smoke coverage, including Cygwin's Python 3.9 job. CI installs and imports gix.testtools for both backend selections, explicitly selects the backend, and prepares Rust and build caches where GixPython must be built from its released source distribution.

Historical fixture measurements with GixPython 0.1.0: on the same local coverage-enabled suite, fixture optimizations reduced wall time from 757.85 s (12m38s) to 404.61 s (6m45s): 353.24 s saved, 46.6% less time, about 1.87× faster. CLI fallback decisions decreased from 39,002 to 21,402; these counters do not count every Git subprocess. This is the before/after fixture improvement, separate from the backend comparison above. The changes reduce repeated work in the CI suites; the quoted timings are local measurements, not a hosted GitHub Actions before/after benchmark. No matching pre-optimization full CLI run was recorded.

The branch also adds released downstream compatibility checks for LangChain, MLflow, Bandit, SWE-bench, and DataHub, plus reproducible local test execution and backend-operation reports.

Context

The goal is to support Git's object and reference formats while reducing the attack surface of Python implementations and limiting argument injection and unintended CLI side effects.

  • Managed calls use git.cmd.Git, existing unsafe option/protocol checks, validated operands, and framed stdin records. Shell execution and option reordering past protective flags are rejected. Persistent cat-file requests use NUL framing.
  • Managed plumbing suppresses implicit hooks, filesystem monitors, maintenance, and lazy fetching. Diffs disable external diff and text conversion; blame disables text conversion by default. Explicit commit hooks use git hook run. Signing/editor options and custom archive format commands require an unsafe-options opt-in.
  • Existing Git worktree conversions remain intentional: status and worktree diffs can run clean filters; checkout, clone, and archive can run smudge filters. Index staging continues to store raw content.
  • Common documented workflows remain. Git manages semantic index entries and temporary indexes, references and reflogs, configuration syntax/locking, remote results, and submodule lifecycle operations. Discovery and cloning respect worktree storage boundaries, and submodule reconnection preserves the child repository's object and reference formats.
  • Known command outcomes retain established exceptions where distinguishable; other failures expose GitCommandError with Git's status and diagnostics.
  • Low-level APIs that cannot be represented faithfully through Git are removed or restricted. These include binary index/tree helpers, raw reflog rewriting and oldhexsha, precompressed object streams and custom object-output writers, Submodule.rename(), and direct Repo.alternates mutation. Configuration follows Git syntax and no longer subclasses RawConfigParser. Detailed replacements and limitations are in the changelog.

The change also updates documentation, format/backend and injection regressions, the minimum-Git CI job, and fuzz harnesses. Obsolete binary-parser, deprecation and timing-only tests have been removed. Happy paths, Python glue failures and representative compatibility regressions remain; the maintained backend benchmark checks parity and CLI budgets. The release announcement above describes the planned release; this PR does not change VERSION yet.

CI follow-up handles platform path conventions and rejects unsupported index filenames before changing the original index. Submodule updates now close their internally opened repositories, preventing retained log records from keeping Windows checkouts open through Git processes. Cygwin builds the supported Git version in its normal installation layout and verifies detection before installation/import smoke checks.

Migration details and historical validation

The index lifecycle now materializes deferred bytes into private files, verifies that Git retained requested entries, and publishes atomically. Failed ordinary edits preserve pending/published state; hook edits survive failures without advancing HEAD. IndexFile.entries remains available as an immutable read snapshot; use iter_entries(), entry() and explicit index edits. Direct mapping mutation remains unsupported. Textual date parsing and final trailer extraction use Git. See the migration notes for removed environment-expansion/error-hiding switches and changed date/trailer behavior.

Historical validation at earlier revisions: on macOS with Git 2.54.0, the retained suite passed with 1,767 tests on Gix and 1,672 on CLI, plus focused regressions on both. All five downstream profiles passed on both backends. All 14 benchmark result digests matched and existing Gix CLI ceilings passed; concurrent fast timing samples do not establish a speed change. Lint, typing and documentation checks passed locally. CI follow-up fixes the Bash 3.2 condition on macOS, removes the empty historical Cygwin performance split, makes path and command fixtures respect Windows configuration, closes a test-owned submodule parent before removal, and isolates partial-write failure injection to locked publication. Focused regressions passed locally with both backends. All 64 hosted checks passed on 17a316c, including the complete Windows matrix, Gix on Ubuntu/macOS/Windows, downstream compatibility, backend parity and CLI ceilings, and Cygwin (1,691 passed, 20 skipped, 11 xfailed).

Earlier CI validation

All 64 hosted checks passed for 781c620 on October 15, 2026. This includes the complete Windows matrix, Gix on Ubuntu/macOS/Windows, Python 3.8–3.10 installation/import checks, Cygwin smoke coverage, Alpine, the minimum-Git job, all five downstream compatibility profiles, backend parity and CLI-process ceilings, and the configured quality checks.

Windows Python 3.12 passed 1,674 CLI tests and 1,772 Gix tests, with 61 passing subtests on each backend. The final fixture correction also passed all 40 commit tests and 21 subtests locally on CLI, then Gix, plus the changed files' pre-commit hooks. Git remains the text baseline: malformed UTF-16 fixtures now force conversion failure, with raw-size assertions, rather than assuming stable output after conversion erases Git's header delimiters. Production behavior is unchanged by that correction.

The measurements in earlier sections remain historical; this CI run does not establish a new timing claim. Python 3.8–3.10 receive smoke coverage rather than the GixPython-dependent fixture suites.

Expanded downstream audit and current head

46303c88 consolidates the four audit records into one top commit, above the feature and twelve compatibility fixes. The audit index links reports and JSON manifests for 115 distinct projects, covering every identified current declared consumer within the September 2026 top-15,000 PyPI snapshot. The covered 348,530,835 monthly distribution requests include extras and repeat downloads; they do not measure GitPython installations or unique users. Original known-user additions and missing metadata prevent a strict popularity top-115 claim. Selected paths, upstream mocks, isolated helpers, omitted application stacks and CLI fallbacks remain explicitly disclosed.

The fixes restore immutable index readers, global -C selection, separate-Git-directory checkout context, the managed GitDB selector, local tracking branches, revision tree/path diagnostics, explicit literal-path construction, editable/build version behavior, historical Union/Optional imports and nonempty config section assignment. Current CI repairs use the Python 3.8-compatible typing.Mapping, make the guaranteed first discovery-loop iteration explicit, and compare Windows test checkout paths by filesystem identity.

Remaining actionable gaps are documented: Kas's empty-section creation needs a native section-edit API (downstream can use set_value() directly); sbxloop's constructor subclass needs supported metadata/include controls or migration; index mapping writes remain unsupported; ambiguous bare/mirror fetch results need full destination identities from Git's fetch --porcelain. Gix fetch remains a CLI fallback until its binding exposes compatible per-ref outcomes. Other retained downstream failures are distinguished from new GitPython regressions by released-baseline comparisons.

Local validation for the CI repairs passed: Python 3.8 import and evaluated annotations, 90 config/downstream tests and 146 repository/downstream tests plus 14 subtests on each backend, followed by 40 final downstream regressions per backend; CLI ran first. Ruff lint/format, mypy and basedpyright pass. Full suites and platform validation are running in hosted CI for this head; the earlier counts above remain historical.

@Byron Byron mentioned this pull request Oct 7, 2026
1 task
@Byron
Byron force-pushed the gix-backend branch 4 times, most recently from 594e7dd to 105114d Compare October 7, 2026 13:25
Byron pushed a commit that referenced this pull request Oct 7, 2026
PR #2274's Python 3.12 CI jobs spent 57m 25s in Windows `pytest` versus 5m 2s
on Ubuntu, with 85,777 and 82,683 `Git.execute` launches respectively. A local
profile of the file-handle and recursive submodule regressions spent about 65 of
77 seconds executing Git, including 44 seconds in repository construction.
Repeated process startup makes the separate metadata probes expensive.

Query reference format, object format, bare status and the absolute common
directory in one `git rev-parse` call on the CLI construction path. Keep the
path last and split only the three fixed scalar fields so embedded newlines
remain intact. Git still computes every value. Supported Gix discovery keeps
its native metadata and existing CLI format-validation query, with unchanged
native launch ceilings and fallbacks.

The focused selection drops from 2,447 to 2,009 launches: three saved for
each of 146 repository opens. Three paired Windows measurements of 20 opens
each reduce the median from 300.74 ms to 221.18 ms per open, with matching
metadata and 11 versus eight launches. Add `--durations=30` to CI so future
slow tests are visible, and record the profiling evidence and reproduction
requirements in `doc/gix-backend.md`.

Cover both object formats, both reference formats, bare and non-bare repos,
spaces and embedded newlines. The eight applicable Windows cases fail before
the fix and pass afterward; all 16 cases pass on Ubuntu WSL. The optional
Windows Gix selection reproduces the same 17 failures on the original and
patched sources. Document the demonstrated path-formatting and undecodable
`commondir` differences under `GIX-14`, distinguishing them from Windows test
assumptions. Keep these bugs open pending compatible upstream behavior.

Validation: the full Windows CLI suite passes with 1,662 passed, 60 skipped,
nine expected failures, two unexpected passes and 40 passing subtests. It
records 77,732 launches in 3,715.16s. The initial baseline records 86,032 in
3,990.28s; its only two failures pass on unchanged code after restoring CI's
`core.autocrlf=true` and placing `pytest`'s temporary directory outside a Git
checkout. The overlapping full runs are not a controlled elapsed-time
comparison. Repository-wide Ruff lint/format, all pinned pre-commit hooks,
`mypy` (47 files), `basedpyright` (zero errors or warnings), the Sphinx HTML
build and `git diff --check` pass.
Byron pushed a commit that referenced this pull request Oct 8, 2026
PR #2274's Python 3.12 CI jobs spent 57m 25s in Windows `pytest` versus 5m 2s
on Ubuntu, with 85,777 and 82,683 `Git.execute` launches respectively. A local
profile of the file-handle and recursive submodule regressions spent about 65 of
77 seconds executing Git, including 44 seconds in repository construction.
Repeated process startup makes the separate metadata probes expensive.

Query reference format, object format, bare status and the absolute common
directory in one `git rev-parse` call on the CLI construction path. Keep the
path last and split only the three fixed scalar fields so embedded newlines
remain intact. Git still computes every value. Supported Gix discovery keeps
its native metadata and existing CLI format-validation query, with unchanged
native launch ceilings and fallbacks.

The focused selection drops from 2,447 to 2,009 launches: three saved for
each of 146 repository opens. Three paired Windows measurements of 20 opens
each reduce the median from 300.74 ms to 221.18 ms per open, with matching
metadata and 11 versus eight launches. Add `--durations=30` to CI so future
slow tests are visible, and record the profiling evidence and reproduction
requirements in `doc/gix-backend.md`.

Cover both object formats, both reference formats, bare and non-bare repos,
spaces and embedded newlines. The eight applicable Windows cases fail before
the fix and pass afterward; all 16 cases pass on Ubuntu WSL. The optional
Windows Gix selection reproduces the same 17 failures on the original and
patched sources. Document the demonstrated path-formatting and undecodable
`commondir` differences under `GIX-14`, distinguishing them from Windows test
assumptions. Keep these bugs open pending compatible upstream behavior.

Validation: the full Windows CLI suite passes with 1,662 passed, 60 skipped,
nine expected failures, two unexpected passes and 40 passing subtests. It
records 77,732 launches in 3,715.16s. The initial baseline records 86,032 in
3,990.28s; its only two failures pass on unchanged code after restoring CI's
`core.autocrlf=true` and placing `pytest`'s temporary directory outside a Git
checkout. The overlapping full runs are not a controlled elapsed-time
comparison. Repository-wide Ruff lint/format, all pinned pre-commit hooks,
`mypy` (47 files), `basedpyright` (zero errors or warnings), the Sphinx HTML
build and `git diff --check` pass.
Byron pushed a commit that referenced this pull request Oct 8, 2026
PR #2274's Python 3.12 CI jobs spent 57m 25s in Windows `pytest` versus 5m 2s
on Ubuntu, with 85,777 and 82,683 `Git.execute` launches respectively. A local
profile of the file-handle and recursive submodule regressions spent about 65 of
77 seconds executing Git, including 44 seconds in repository construction.
Repeated process startup makes the separate metadata probes expensive.

Query reference format, object format, bare status and the absolute common
directory in one `git rev-parse` call on the CLI construction path. Keep the
path last and split only the three fixed scalar fields so embedded newlines
remain intact. Git still computes every value. Supported Gix discovery keeps
its native metadata and existing CLI format-validation query, with unchanged
native launch ceilings and fallbacks.

The focused selection drops from 2,447 to 2,009 launches: three saved for
each of 146 repository opens. Three paired Windows measurements of 20 opens
each reduce the median from 300.74 ms to 221.18 ms per open, with matching
metadata and 11 versus eight launches. Add `--durations=30` to CI so future
slow tests are visible, and record the profiling evidence and reproduction
requirements in `doc/gix-backend.md`.

Cover both object formats, both reference formats, bare and non-bare repos,
spaces and embedded newlines. The eight applicable Windows cases fail before
the fix and pass afterward; all 16 cases pass on Ubuntu WSL. The optional
Windows Gix selection reproduces the same 17 failures on the original and
patched sources. Document the demonstrated path-formatting and undecodable
`commondir` differences under `GIX-14`, distinguishing them from Windows test
assumptions. Keep these bugs open pending compatible upstream behavior.

Validation: the full Windows CLI suite passes with 1,662 passed, 60 skipped,
nine expected failures, two unexpected passes and 40 passing subtests. It
records 77,732 launches in 3,715.16s. The initial baseline records 86,032 in
3,990.28s; its only two failures pass on unchanged code after restoring CI's
`core.autocrlf=true` and placing `pytest`'s temporary directory outside a Git
checkout. The overlapping full runs are not a controlled elapsed-time
comparison. Repository-wide Ruff lint/format, all pinned pre-commit hooks,
`mypy` (47 files), `basedpyright` (zero errors or warnings), the Sphinx HTML
build and `git diff --check` pass.
@Byron
Byron force-pushed the gix-backend branch 4 times, most recently from af1de6a to c3a2832 Compare October 8, 2026 07:14
Byron pushed a commit that referenced this pull request Oct 9, 2026
PR #2274's Python 3.12 CI jobs spent 57m 25s in Windows `pytest` versus 5m 2s
on Ubuntu, with 85,777 and 82,683 `Git.execute` launches respectively. A local
profile of the file-handle and recursive submodule regressions spent about 65 of
77 seconds executing Git, including 44 seconds in repository construction.
Repeated process startup makes the separate metadata probes expensive.

Query reference format, object format, bare status and the absolute common
directory in one `git rev-parse` call on the CLI construction path. Keep the
path last and split only the three fixed scalar fields so embedded newlines
remain intact. Git still computes every value. Supported Gix discovery keeps
its native metadata and existing CLI format-validation query, with unchanged
native launch ceilings and fallbacks.

The focused selection drops from 2,447 to 2,009 launches: three saved for
each of 146 repository opens. Three paired Windows measurements of 20 opens
each reduce the median from 300.74 ms to 221.18 ms per open, with matching
metadata and 11 versus eight launches. Add `--durations=30` to CI so future
slow tests are visible, and record the profiling evidence and reproduction
requirements in `doc/gix-backend.md`.

Cover both object formats, both reference formats, bare and non-bare repos,
spaces and embedded newlines. The eight applicable Windows cases fail before
the fix and pass afterward; all 16 cases pass on Ubuntu WSL. The optional
Windows Gix selection reproduces the same 17 failures on the original and
patched sources. Document the demonstrated path-formatting and undecodable
`commondir` differences under `GIX-14`, distinguishing them from Windows test
assumptions. Keep these bugs open pending compatible upstream behavior.

Validation: the full Windows CLI suite passes with 1,662 passed, 60 skipped,
nine expected failures, two unexpected passes and 40 passing subtests. It
records 77,732 launches in 3,715.16s. The initial baseline records 86,032 in
3,990.28s; its only two failures pass on unchanged code after restoring CI's
`core.autocrlf=true` and placing `pytest`'s temporary directory outside a Git
checkout. The overlapping full runs are not a controlled elapsed-time
comparison. Repository-wide Ruff lint/format, all pinned pre-commit hooks,
`mypy` (47 files), `basedpyright` (zero errors or warnings), the Sphinx HTML
build and `git diff --check` pass.
@Byron
Byron force-pushed the gix-backend branch 4 times, most recently from 75ebc93 to 6881712 Compare October 9, 2026 08:01
Byron pushed a commit that referenced this pull request Oct 9, 2026
The Windows Python 3.14 job for PR #2274 failed in
`TestSubmodule.test_base_rw` when forced module-only removal called
`rmtree`: Windows reported `WinError 32` for `git/ext/gitdb`.
The legacy test retains that checkout's `Repo` through `csm.repo` and
starts its persistent `cat-file` process while reading parent commits.
Closing only the fresh handle created by `Submodule.remove()` cannot
release this caller-owned process.

Close the retained test-owned parent before deleting its checkout. Keep
strict removal and all existing happy-path assertions, and remove the
obsolete commented-out skip that claimed removal ignored this error.
The original test predates the PR's base, so record this as a follow-up
rather than rewriting shared history.

The focused `test_base_rw` passed locally with both CLI and Gix.
Relevant pre-commit hooks passed. Hosted Windows validation remains
part of the PR CI loop.
Byron pushed a commit that referenced this pull request Oct 9, 2026
The Windows Python 3.14 job for PR #2274 failed in
`TestSubmodule.test_base_rw` when forced module-only removal called
`rmtree`: Windows reported `WinError 32` for `git/ext/gitdb`.
The legacy test retains that checkout's `Repo` through `csm.repo` and
starts its persistent `cat-file` process while reading parent commits.
Closing only the fresh handle created by `Submodule.remove()` cannot
release this caller-owned process.

Close the retained test-owned parent before deleting its checkout. Keep
strict removal and all existing happy-path assertions, and remove the
obsolete commented-out skip that claimed removal ignored this error.
The original test predates the PR's base, so record this as a follow-up
rather than rewriting shared history.

The focused `test_base_rw` passed locally with both CLI and Gix.
Relevant pre-commit hooks passed. Hosted Windows validation remains
part of the PR CI loop.
@Byron
Byron force-pushed the gix-backend branch 2 times, most recently from 17a316c to a6c7dba Compare October 14, 2026 09:31
Byron pushed a commit that referenced this pull request Oct 14, 2026
PR #2274's Python 3.12 CI jobs spent 57m 25s in Windows `pytest` versus 5m 2s
on Ubuntu, with 85,777 and 82,683 `Git.execute` launches respectively. A local
profile of the file-handle and recursive submodule regressions spent about 65 of
77 seconds executing Git, including 44 seconds in repository construction.
Repeated process startup makes the separate metadata probes expensive.

Query reference format, object format, bare status and the absolute common
directory in one `git rev-parse` call on the CLI construction path. Keep the
path last and split only the three fixed scalar fields so embedded newlines
remain intact. Git still computes every value. Supported Gix discovery keeps
its native metadata and existing CLI format-validation query, with unchanged
native launch ceilings and fallbacks.

The focused selection drops from 2,447 to 2,009 launches: three saved for
each of 146 repository opens. Three paired Windows measurements of 20 opens
each reduce the median from 300.74 ms to 221.18 ms per open, with matching
metadata and 11 versus eight launches. Add `--durations=30` to CI so future
slow tests are visible, and record the profiling evidence and reproduction
requirements in `doc/gix-backend.md`.

Cover both object formats, both reference formats, bare and non-bare repos,
spaces and embedded newlines. The eight applicable Windows cases fail before
the fix and pass afterward; all 16 cases pass on Ubuntu WSL. The optional
Windows Gix selection reproduces the same 17 failures on the original and
patched sources. Document the demonstrated path-formatting and undecodable
`commondir` differences under `GIX-14`, distinguishing them from Windows test
assumptions. Keep these bugs open pending compatible upstream behavior.

Validation: the full Windows CLI suite passes with 1,662 passed, 60 skipped,
nine expected failures, two unexpected passes and 40 passing subtests. It
records 77,732 launches in 3,715.16s. The initial baseline records 86,032 in
3,990.28s; its only two failures pass on unchanged code after restoring CI's
`core.autocrlf=true` and placing `pytest`'s temporary directory outside a Git
checkout. The overlapping full runs are not a controlled elapsed-time
comparison. Repository-wide Ruff lint/format, all pinned pre-commit hooks,
`mypy` (47 files), `basedpyright` (zero errors or warnings), the Sphinx HTML
build and `git diff --check` pass.
Byron pushed a commit that referenced this pull request Oct 14, 2026
The Windows Python 3.14 job for PR #2274 failed in
`TestSubmodule.test_base_rw` when forced module-only removal called
`rmtree`: Windows reported `WinError 32` for `git/ext/gitdb`.
The legacy test retains that checkout's `Repo` through `csm.repo` and
starts its persistent `cat-file` process while reading parent commits.
Closing only the fresh handle created by `Submodule.remove()` cannot
release this caller-owned process.

Close the retained test-owned parent before deleting its checkout. Keep
strict removal and all existing happy-path assertions, and remove the
obsolete commented-out skip that claimed removal ignored this error.
The original test predates the PR's base, so record this as a follow-up
rather than rewriting shared history.

The focused `test_base_rw` passed locally with both CLI and Gix.
Relevant pre-commit hooks passed. Hosted Windows validation remains
part of the PR CI loop.
Copilot AI mentioned this pull request Oct 14, 2026
@Byron
Byron force-pushed the gix-backend branch 4 times, most recently from 13202ce to 781c620 Compare October 15, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved object-decoding and branch-renaming errors can return incomplete data or references to the wrong branch.

3 open issues
What changed in this PR

GitHub Copilot review: This PR prepares GitPython for newer Git storage formats and adds an optional GixPython backend, supported by shared fixtures, compatibility checks, and benchmarks.

Changes:

  • Delegate storage operations to guarded Git commands or supported Gix APIs, with updated object IDs and index interfaces.
  • Introduce reusable scripted fixtures, backend reporting, downstream checks, and benchmark parity enforcement.
  • Update installation guidance, dependencies, CI, and fuzzing for Git 2.52+.
File Description
tox.ini Select backends and add older-runtime smoke checks.
tests/​fixtures/​util.sh Prepare lock and symlink scenarios.
tests/​fixtures/​tree-order.sh Capture Git tree ordering.
tests/​fixtures/​submodule-transport.sh Prepare submodule transport comparisons.
tests/​fixtures/​submodule-symlinks.sh Prepare checkout and metadata symlinks.
tests/​fixtures/​submodule-special.sh Cover special submodule layouts and names.
tests/​fixtures/​submodule-source.sh Create a reusable submodule source.
tests/​fixtures/​submodule-relative.sh Prepare relative-URL scenarios.
tests/​fixtures/​submodule-no-fetch.sh Prepare offline submodule updates.
tests/​fixtures/​submodule-layout.sh Build varied submodule layouts.
tests/​fixtures/​submodule-cases.sh Prepare submodule lifecycle and safety cases.
tests/​fixtures/​safety.sh Prepare command and object safety cases.
tests/​fixtures/​rmtree.sh Prepare deletion and permission scenarios.
tests/​fixtures/​rev-parse.sh Build revision-parsing histories.
tests/​fixtures/​remote-timeout.sh Create a timeout-test repository.
tests/​fixtures/​remote-cli.sh Prepare fetch and quoted-name scenarios.
tests/​fixtures/​refs.sh Prepare reference safety cases.
tests/​fixtures/​refs-input.sh Create option-injection inputs.
tests/​fixtures/​reflog.sh Prepare reflog and signing scenarios.
tests/​fixtures/​reference-security.sh Build cyclic and external reference cases.
tests/​fixtures/​quick-doc.sh Replace remote tutorial setup locally.
tests/​fixtures/​positional.sh Prepare option-shaped operand cases.
tests/​fixtures/​objects-index.sh Prepare object, index, and hook scenarios.
tests/​fixtures/​local-environment.sh Generate isolated test configuration.
tests/​fixtures/​installation.sh Prepare installation-test environments.
tests/​fixtures/​index.sh Build index and hook fixtures.
tests/​fixtures/​hook-ownership.sh Prepare hook ownership layouts.
tests/​fixtures/​history.sh Create historical repository layouts.
tests/​fixtures/​git-command.sh Prepare executable and environment cases.
tests/​fixtures/​fun.sh Construct merge-tree inputs.
tests/​fixtures/​fixture-lifecycle.sh Support fixture lifecycle checks.
tests/​fixtures/​empty.sh Supply empty directories or repositories.
tests/​fixtures/​downstream-worktree.sh Prepare downstream worktree cases.
tests/​fixtures/​downstream-swebench.sh Create local SWE-bench clone sources.
tests/​fixtures/​downstream-separate-git-dir.sh Create relocatable separate metadata.
tests/​fixtures/​downstream-revision-path.sh Prepare revision-path compatibility cases.
tests/​fixtures/​downstream-local-upstream.sh Configure local tracking branches.
tests/​fixtures/​downstream-literal-repo.sh Preserve literal variable-shaped paths.
tests/​fixtures/​downstream-index.sh Prepare immutable index-reader cases.
tests/​fixtures/​docs.sh Isolate tutorial push destinations.
tests/​fixtures/​diff.sh Prepare diff formats and edge cases.
tests/​fixtures/​dependencies.sh Create historical dependency repositories.
tests/​fixtures/​config.sh Prepare configuration parsing cases.
tests/​fixtures/​commit.sh Prepare commit metadata and encoding cases.
tests/​fixtures/​command-guards.sh Prepare guarded-command regressions.
tests/​fixtures/​clone.sh Prepare clone compatibility cases.
tests/​fixtures/​cleanup.sh Prepare cleanup failure scenarios.
tests/​fixtures/​benchmark.sh Create the pinned benchmark repository.
tests/​fixtures/​benchmark-comparison.sh Generate comparison validation inputs.
test/​test_typing.py Update semantic index-entry checks.
test/​test_remote_progress.py Check progress override return typing.
test/​test_remote_cli.py Check command-derived fetch results.
test/​test_quick_doc.py Use local tutorial fixtures.
test/​test_installation.py Check installation, backend, and version behavior.
test/​test_imports.py Check the actual utility-module binding.
test/​test_fixture_health.py Check fixture reuse, isolation, and errors.
test/​test_exc.py Remove unnecessary repository setup.
test/​test_docs.py Isolate tutorials and update index examples.
test/​test_cli_process_counts.py Check subprocess counters and ceilings.
test/​test_blob_filter.py Simplify blob-filter inputs.
test/​test_base.py Move setup into shared fixtures.
test/​test_backend_selection.py Check backend selection and fixture availability.
test/​test_actor.py Remove obsolete deprecation checks.
test/​run-local.py Add isolated local test execution.
test/​performance/​test_odb.py Remove legacy object-store timing tests.
test/​performance/​test_commit.py Remove legacy commit timing tests.
test/​performance/​test_benchmark_comparison.py Check benchmark parity enforcement.
test/​performance/​README.md Document benchmark execution and interpretation.
test/​performance/​lib.py Remove legacy performance helpers.
test/​performance/​compare_backends.py Validate parity and report measurements.
test/​performance/​cli-budget.json Define backend subprocess ceilings.
test/​performance/​benchmark-requirements.txt Declare benchmark dependencies.
test/​lib/​fixtures.py Manage cached and writable scripted fixtures.
test/​downstream/​projects.json Define downstream compatibility profiles.
test/​downstream/​checks/​swebench.py Exercise real SWE-bench GitPython callers.
test/​deprecation/​test_types.py Remove obsolete type-alias deprecation tests.
test/​deprecation/​test_compat.py Remove platform-alias deprecation tests.
test/​deprecation/​test_basic.py Remove obsolete deprecation coverage.
test/​deprecation/​lib.py Remove deprecation test helpers.
test/​deprecation/​__init__.py Remove the deprecation test package.
test/​conftest.py Report backend operations and fixture sources.
test/​cleanup.py Add best-effort test-directory cleanup.
test-requirements.txt Add GixPython fixture tooling.
README.md Explain Gix activation and prerequisites.
pyproject.toml Add the Gix extra and adjust typing coverage.
MANIFEST.in Package Gix requirements and exclude fixtures.
gix-requirements.txt Pin the optional GixPython dependency.
git/​types.py Remove the deprecated commit-type alias.
git/​refs/​tag.py Guard tag options and validate operands.
git/​refs/​remote.py Delegate reference deletion to Git.
git/​refs/​reference.py Let Git maintain HEAD reflogs.
git/​refs/​head.py Guard branch operations and support local upstreams.
git/​objects/​util.py Delegate textual date parsing to Git.
git/​objects/​tree.py Delegate tree loading and construction to Git.
git/​objects/​tag.py Add native tag metadata decoding.
git/​objects/​submodule/​util.py Use shared configuration-section escaping.
git/​objects/​submodule/​root.py Update reflog lookup and transport handling.
git/​objects/​base.py Support repository-specific object ID widths.
git/​index/​util.py Remove obsolete binary-index helpers.
git/​index/​typ.py Represent semantic index entries without raw metadata.
git/​diff.py Add native dispatch and guarded CLI diffs.
git/​compat.py Remove deprecated platform aliases.
git/​__init__.py Update version loading and compatibility exports.
fuzzing/​README.md Document Git-backed fuzzing requirements.
fuzzing/​oss-fuzz-scripts/​container-environment-bootstrap.sh Enforce the minimum Git version.
fuzzing/​oss-fuzz-scripts/​build.sh Validate and bundle the selected Git executable.
fuzzing/​local-dev-helpers/​Dockerfile Build the minimum supported Git.
fuzzing/​fuzz-targets/​fuzz_repo.py Exercise Git-backed repository workflows.
fuzzing/​fuzz-targets/​fuzz_diff.py Adapt object IDs and expected errors.
fuzzing/​fuzz-targets/​fuzz_config.py Handle expected Git configuration rejection.
fuzzing/​fuzz-targets/​fuzz_blob.py Respect repository object ID widths.
doc/​source/​tutorial.rst Update object, index, and reflog guidance.
doc/​source/​intro.rst Describe storage support and new requirements.
CONTRIBUTING.md Document fixture and cleanup conventions.
AGENTS.md Define Gix implementation and compatibility requirements.
.gitignore Ignore generated fixture caches and archives.
.github/​workflows/​git-cli.yml Test minimum-Git format and safety contracts.
.github/​workflows/​downstream.yml Run downstream profiles on both backends.
.github/​workflows/​cygwin-test.yml Build supported Git and smoke-test installation.
.github/​workflows/​backend-benchmark.yml Measure backends and enforce parity budgets.
.github/​workflows/​alpine-test.yml Install fixture tooling and select CLI explicitly.
.basedpyright/​baseline.json Remove obsolete typing suppressions.

Review effort: Balanced

Comment thread git/objects/tag.py
Comment thread git/objects/tree.py
Comment thread git/refs/head.py
@Byron
Byron merged commit f1fb0bd into main Oct 24, 2026
65 checks passed
@Byron
Byron deleted the gix-backend branch October 25, 2026 07:20
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "reading them raises `AttributeError`" when native decoding has
no tagger, and requests preserving initialized fields through a CLI fallback.
Four SHA-1/SHA-256 regressions reproduce the native failure. The two cases
with a message also expose CLI loss of the first message line, so falling
back alone would not restore complete metadata.

Adapt `Tag.tagger()` returning `None` to the existing empty `Actor` and zero
date/offset defaults through `parse_actor_and_date("")`. Gix already exposes
the absence correctly; no new Git semantics or fallback is required. Make
the CLI parser recognize the optional tagger header and preserve the entire
message. Git 2.54.0's `tag.c::parse_tag_buffer()` accepts an absent tagger
and sets its date to zero. The existing GIX-28 strict writer gap remains open.

All four regressions now pass on both backends; the native cases forbid
`Git.execute()` during metadata access. Reference and CLI object/index tests
pass on CLI (105 tests, 24 subtests) and Gix (105 tests, 24 subtests).
Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "subsequent iteration or lookup silently accepts that truncated
tree" after an entry fails validation. Stored trees containing a `.git`
entry reproduce this on both backends: the first iteration rejects the name,
but the second accepts an empty tree or the valid prefix.

Build the entries in a local list and attach `_cache` only after every record
passes parsing and validation. Failed reads leave the lazy cache unset, so
later accesses retry and raise the same error instead of exposing partial data.
No backend behavior or tree validation policy changes. Git 2.54.0's `mktree`
and `ls-tree` accept and emit the fixture's `.git` entry; GitPython applies
its existing metadata-path guard while consuming that output.

Four regressions cover invalid entries first and last in SHA-1/SHA-256 trees.
All fail before the fix and pass afterwards on CLI and native Gix `ls_tree`.
All 97 tree tests pass on each backend. Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "the returned head points to `refs/heads/topic`, which may be
missing or identify a different branch" after renaming to the short branch
name `refs/heads/topic`. A repository with an existing `topic` at commit A
and a source branch at commit B reproduces the incorrect returned ref on
both backends.

Compute the destination by prepending `refs/heads/` to the literal name
passed to `git branch`. Validate that same destination and retain it only
after Git successfully renames the branch. Git 2.54.0's
`builtin/branch.c::copy_or_rename_branch()` uses `refs.c::check_branch_ref()`,
which always prepends that namespace. The general `to_full_path()` helper
continues to normalize already-qualified references for its other callers.

The SHA-1/SHA-256 regressions fail before this fix and pass afterwards.
They check the returned path, name and commit, current `HEAD`, and that the
original `topic` still points to commit A. All 50 reference tests and 24
subtests pass on each backend, including ordinary and forced renames.
Ruff, ShellCheck, mypy and basedpyright pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "reading them raises `AttributeError`" when native decoding has
no tagger, and requests preserving initialized fields through a CLI fallback.
Four SHA-1/SHA-256 regressions reproduce the native failure. The two cases
with a message also expose CLI loss of the first message line, so falling
back alone would not restore complete metadata.

Adapt `Tag.tagger()` returning `None` to the existing empty `Actor` and zero
date/offset defaults through `parse_actor_and_date("")`. Gix already exposes
the absence correctly; no new Git semantics or fallback is required. Make
the CLI parser recognize the optional tagger header and preserve the entire
message. Git 2.54.0's `tag.c::parse_tag_buffer()` accepts an absent tagger
and sets its date to zero. The existing GIX-28 strict writer gap remains open.

All four regressions now pass on both backends; the native cases forbid
`Git.execute()` during metadata access. Reference and CLI object/index tests
pass on CLI (105 tests, 24 subtests) and Gix (105 tests, 24 subtests).
Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "subsequent iteration or lookup silently accepts that truncated
tree" after an entry fails validation. Stored trees containing a `.git`
entry reproduce this on both backends: the first iteration rejects the name,
but the second accepts an empty tree or the valid prefix.

Build the entries in a local list and attach `_cache` only after every record
passes parsing and validation. Failed reads leave the lazy cache unset, so
later accesses retry and raise the same error instead of exposing partial data.
No backend behavior or tree validation policy changes. Git 2.54.0's `mktree`
and `ls-tree` accept and emit the fixture's `.git` entry; GitPython applies
its existing metadata-path guard while consuming that output.

Four regressions cover invalid entries first and last in SHA-1/SHA-256 trees.
All fail before the fix and pass afterwards on CLI and native Gix `ls_tree`.
All 97 tree tests pass on each backend. Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "the returned head points to `refs/heads/topic`, which may be
missing or identify a different branch" after renaming to the short branch
name `refs/heads/topic`. A repository with an existing `topic` at commit A
and a source branch at commit B reproduces the incorrect returned ref on
both backends.

Compute the destination by prepending `refs/heads/` to the literal name
passed to `git branch`. Validate that same destination and retain it only
after Git successfully renames the branch. Git 2.54.0's
`builtin/branch.c::copy_or_rename_branch()` uses `refs.c::check_branch_ref()`,
which always prepends that namespace. The general `to_full_path()` helper
continues to normalize already-qualified references for its other callers.

The SHA-1/SHA-256 regressions fail before this fix and pass afterwards.
They check the returned path, name and commit, current `HEAD`, and that the
original `topic` still points to commit A. All 50 reference tests and 24
subtests pass on each backend, including ordinary and forced renames.
Ruff, ShellCheck, mypy and basedpyright pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "reading them raises `AttributeError`" when native decoding has
no tagger, and requests preserving initialized fields through a CLI fallback.
Four SHA-1/SHA-256 regressions reproduce the native failure. The two cases
with a message also expose CLI loss of the first message line, so falling
back alone would not restore complete metadata.

Adapt `Tag.tagger()` returning `None` to the existing empty `Actor` and zero
date/offset defaults through `parse_actor_and_date("")`. Gix already exposes
the absence correctly; no new Git semantics or fallback is required. Make
the CLI parser recognize the optional tagger header and preserve the entire
message. Git 2.54.0's `tag.c::parse_tag_buffer()` accepts an absent tagger
and sets its date to zero. The existing GIX-28 strict writer gap remains open.

All four regressions now pass on both backends; the native cases forbid
`Git.execute()` during metadata access. Reference and CLI object/index tests
pass on CLI (105 tests, 24 subtests) and Gix (105 tests, 24 subtests).
Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "subsequent iteration or lookup silently accepts that truncated
tree" after an entry fails validation. Stored trees containing a `.git`
entry reproduce this on both backends: the first iteration rejects the name,
but the second accepts an empty tree or the valid prefix.

Build the entries in a local list and attach `_cache` only after every record
passes parsing and validation. Failed reads leave the lazy cache unset, so
later accesses retry and raise the same error instead of exposing partial data.
No backend behavior or tree validation policy changes. Git 2.54.0's `mktree`
and `ls-tree` accept and emit the fixture's `.git` entry; GitPython applies
its existing metadata-path guard while consuming that output.

Four regressions cover invalid entries first and last in SHA-1/SHA-256 trees.
All fail before the fix and pass afterwards on CLI and native Gix `ls_tree`.
All 97 tree tests pass on each backend. Ruff, ShellCheck and mypy pass.
Byron pushed a commit that referenced this pull request Oct 25, 2026
Review item #2274 (comment)
reports that "the returned head points to `refs/heads/topic`, which may be
missing or identify a different branch" after renaming to the short branch
name `refs/heads/topic`. A repository with an existing `topic` at commit A
and a source branch at commit B reproduces the incorrect returned ref on
both backends.

Compute the destination by prepending `refs/heads/` to the literal name
passed to `git branch`. Validate that same destination and retain it only
after Git successfully renames the branch. Git 2.54.0's
`builtin/branch.c::copy_or_rename_branch()` uses `refs.c::check_branch_ref()`,
which always prepends that namespace. The general `to_full_path()` helper
continues to normalize already-qualified references for its other callers.

The SHA-1/SHA-256 regressions fail before this fix and pass afterwards.
They check the returned path, name and commit, current `HEAD`, and that the
original `topic` still points to commit A. All 50 reference tests and 24
subtests pass on each backend, including ordinary and forced renames.
Ruff, ShellCheck, mypy and basedpyright pass.
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
Since 212573b (gitpython-developers#2274), GitPython requires Git 2.52 or newer for
library-managed operations, and the tests require it too. The README
says that Git must be "available in `PATH` or selected with
`GIT_PYTHON_GIT_EXECUTABLE`", which lets a developer whose default
`git` is older point GitPython at a newer one without changing
`PATH`.

tox did not support the second way. Its environments only receive the
variables tox passes by default plus those in `pass_env`, which listed
just `SSH_*`. So `GIT_PYTHON_GIT_EXECUTABLE` set in the shell was
silently dropped, and `tox` ran the tests against whatever `git` came
first in `PATH`, even when that was too old.

Add `GIT_PYTHON_GIT_EXECUTABLE` to `pass_env` in `[testenv]`. Every
environment inherits it, including `gix` and the `py38` through
`py310` import smoke checks, none of which override `pass_env`.
`GIT_PYTHON_BACKEND` is still not passed through, since `[testenv]`
and `gix` each set it explicitly.

Checked locally with tox 4.67.1 by running an environment with its
command overridden to print the variable, with
`GIT_PYTHON_GIT_EXECUTABLE` set in the calling shell. Before this
change it printed `None`; after it, the value that was set.
`tox config` shows the variable in `pass_env` for `py311`, `gix`, and
`mypy`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
The tox `ruff`, `format`, and `misc` environments only run tools on
the source tree: `ruff check`, `ruff format --check`, and the
remaining pre-commit hooks. But they did not set `skip_install`, so
they inherited the default of installing GitPython with
`extras = test` from `[testenv]`. That has always been unneeded work,
but it became a real obstacle when 212573b (gitpython-developers#2274) added GixPython to
the `test` extra for Python 3.11 and later. GixPython 0.3.0 publishes
wheels only for macOS, so on Linux and Windows installing it builds
its source distribution, which needs Rust 1.89 or newer and a linker.
So running Ruff through tox on those platforms now needed a Rust
toolchain.

Set `skip_install = true` in all three environments. `ruff` and
`format` already list `ruff` in `deps`. `misc` got `pre-commit` from
the `test` extra, so list it in `deps` instead. Like the `test`
extra's entry, it is unversioned. The pre-commit hooks themselves are
installed by pre-commit into its own environments as before.

Run locally with tox 4.67.1, each environment now installs only
`ruff` or `pre-commit` and passes. `ruff` reports "All checks
passed!", `format` reports 112 files already formatted, and `misc`
runs every non-Ruff hook successfully. `ruff` and `format` each took
about 1.5 seconds, compared with about 12 seconds for `ruff` before,
most of which went to installing the `test` extra.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
Since 212573b (gitpython-developers#2274), GitPython requires Git 2.52 or newer for
library-managed operations, and the tests require it too. The README
says that Git must be "available in `PATH` or selected with
`GIT_PYTHON_GIT_EXECUTABLE`", which lets a developer whose default
`git` is older point GitPython at a newer one without changing
`PATH`.

tox did not support the second way. Its environments only receive the
variables tox passes by default plus those in `pass_env`, which listed
just `SSH_*`. So `GIT_PYTHON_GIT_EXECUTABLE` set in the shell was
silently dropped, and `tox` ran the tests against whatever `git` came
first in `PATH`, even when that was too old.

Add `GIT_PYTHON_GIT_EXECUTABLE` to `pass_env` in `[testenv]`. Every
environment inherits it, including `gix` and the `py38` through
`py310` import smoke checks, none of which override `pass_env`.
`GIT_PYTHON_BACKEND` is still not passed through, since `[testenv]`
and `gix` each set it explicitly.

Checked locally with tox 4.67.1 by running an environment with its
command overridden to print the variable, with
`GIT_PYTHON_GIT_EXECUTABLE` set in the calling shell. Before this
change it printed `None`; after it, the value that was set.
`tox config` shows the variable in `pass_env` for `py311`, `gix`, and
`mypy`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
The tox `ruff`, `format`, and `misc` environments only run tools on
the source tree: `ruff check`, `ruff format --check`, and the
remaining pre-commit hooks. But they did not set `skip_install`, so
they inherited the default of installing GitPython with
`extras = test` from `[testenv]`. That has always been unneeded work,
but it became a real obstacle when 212573b (gitpython-developers#2274) added GixPython to
the `test` extra for Python 3.11 and later. GixPython 0.3.0 publishes
wheels only for macOS, so on Linux and Windows installing it builds
its source distribution, which needs Rust 1.89 or newer and a linker.
So on those platforms, running Ruff through tox needed a Rust
toolchain whenever 3.10 was not found but 3.11 or 3.12 was, since tox
then picked one of those for these environments.

Set `skip_install = true` in all three environments. `ruff` and
`format` already list `ruff` in `deps`. `misc` got `pre-commit` from
the `test` extra, so list it in `deps` instead. Like the `test`
extra's entry, it is unversioned. The pre-commit hooks themselves are
installed by pre-commit into its own environments as before.

Run locally with tox 4.67.1, each environment now installs only
`ruff` or `pre-commit` and passes. `ruff` reports "All checks
passed!", `format` reports 112 files already formatted, and `misc`
runs every non-Ruff hook successfully. `ruff` and `format` each took
about 1.5 seconds, compared with about 12 seconds for `ruff` before,
most of which went to installing the `test` extra.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
Since 212573b (gitpython-developers#2274), GitPython requires Git 2.52 or newer for
library-managed operations, and the tests require it too. The README
says that Git must be "available in `PATH` or selected with
`GIT_PYTHON_GIT_EXECUTABLE`", which lets a developer whose default
`git` is older point GitPython at a newer one without changing
`PATH`.

tox did not support the second way. Its environments only receive the
variables tox passes by default plus those in `pass_env`, which listed
just `SSH_*`. So `GIT_PYTHON_GIT_EXECUTABLE` set in the shell was
silently dropped, and `tox` ran the tests against whatever `git` came
first in `PATH`, even when that was too old.

Add `GIT_PYTHON_GIT_EXECUTABLE` to `pass_env` in `[testenv]`. Every
environment inherits it, including `gix` and the `py38` through
`py310` import smoke checks, none of which override `pass_env`.
`GIT_PYTHON_BACKEND` is still not passed through, since `[testenv]`
and `gix` each set it explicitly.

Checked locally with tox 4.67.1 by running an environment with its
command overridden to print the variable, with
`GIT_PYTHON_GIT_EXECUTABLE` set in the calling shell. Before this
change it printed `None`; after it, the value that was set.
`tox config` shows the variable in `pass_env` for `py311`, `gix`, and
`mypy`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
EliahKagan added a commit to EliahKagan/GitPython that referenced this pull request Oct 25, 2026
The tox `ruff`, `format`, and `misc` environments only run tools on
the source tree: `ruff check`, `ruff format --check`, and the
remaining pre-commit hooks. But they did not set `skip_install`, so
they inherited the default of installing GitPython with
`extras = test` from `[testenv]`. That has always been unneeded work,
but it became a real obstacle when 212573b (gitpython-developers#2274) added GixPython to
the `test` extra for Python 3.11 and later. GixPython 0.3.0 publishes
wheels only for macOS, so on Linux and Windows installing it builds
its source distribution, which needs Rust 1.89 or newer and a linker.
So on those platforms, running Ruff through tox needed a Rust
toolchain whenever 3.10 was not found but 3.11 or 3.12 was, since tox
then picked one of those for these environments.

Set `skip_install = true` in all three environments. `ruff` and
`format` already list `ruff` in `deps`. `misc` got `pre-commit` from
the `test` extra, so list it in `deps` instead. Like the `test`
extra's entry, it is unversioned. The pre-commit hooks themselves are
installed by pre-commit into its own environments as before.

Run locally with tox 4.67.1, each environment now installs only
`ruff` or `pre-commit` and passes. `ruff` reports "All checks
passed!", `format` reports 114 files already formatted, and `misc`
runs every non-Ruff hook successfully. `ruff` and `format` each took
about 1.5 seconds, compared with about 12 seconds for `ruff` before,
most of which went to installing the `test` extra.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants