# GixPython backend This backend is unreleased. Install `.[gix]` from this checkout to make GitPython use GixPython for supported operations. GixPython is the distribution name; `gix` is its import name. The extra uses the official PyPI release `GixPython==0.3.0` on CPython 3.11 or newer. On older Python versions, the dependency is omitted and GitPython uses the CLI backend. The ordinary GitPython installation still supports Python 3.8 or newer. To install from this checkout with an existing interpreter: ```sh uv venv --python .venv/bin/python .tox/gix uv pip install --python .tox/gix/bin/python --editable '.[test,gix]' ``` `tox -e gix` also resolves the published GixPython release from the package index. GixPython 0.3.0 publishes macOS wheels. On Windows and Linux, installation builds the released source distribution and requires Rust 1.89 or newer and a system linker. Its default build uses pure Rust hashing, compression and HTTPS; no C/C++ compiler is required for those dependencies. The release uses Gitoxide's `gix` 0.90.0 and `gix-testtools` 0.22.0 from crates.io. The fixture suites for both backends need GixPython. CI prepares Rust for source builds and caches built wheels, Cargo dependencies and compilation output on the main matrix. The 0.3.0 upgrade probes native discovery, HEAD/commit lookup and the scripted fixture APIs against Git on CPython 3.12. Reftable remains unsupported: on the `empty.sh sha1 reftable` fixture, Git reports `reftable` and resolves HEAD to `master`, while `gix.open().head()` raises an unsupported-storage error. The existing GIX-1 CLI fallback preserves that repository's behavior. Historical measurements and compatibility findings below retain their original versions; the release upgrade alone does not close them. Python extras add dependencies; they do not leave a runtime feature bit. Set `GIT_PYTHON_BACKEND=cli` or `GIT_PYTHON_BACKEND=gix` before importing GitPython. Explicit Gix selection fails if `gix` cannot be imported; an invalid selection also fails. With the variable unset, GitPython selects Gix when it can be imported and otherwise uses CLI. Test and benchmark runners select a backend explicitly, because both installations contain GixPython for fixtures. Git 2.52 or newer is required. Unsupported operations and call options use the existing CLI implementation. Public `repo.git.()` calls retain their command-line behavior. Native dispatch applies to library-managed calls. Here and in coverage reports, **native means Gitoxide execution through GixPython's `gix` API**. It never means Python-implemented Git behavior. Python glue validates inputs, adapts Gix results and manages repository handles. For example, object reads call `Repository.find_object()` and reference edits call `Repository.edit_references_as()`. The two fixed metadata paths use `Repository.git_dir()` plus the requested basename; that adaptation does not provide general Git-path resolution. A `native` counter must represent a Gix-backed operation, and backend timings include its Python glue and any CLI fallbacks. ## Index and object metadata lifecycle `Repo(..., expand_vars=False)` accepts literal paths for callers such as sbxloop 2.1.36 that explicitly require literal paths. The default also keeps environment variables literal; `expand_vars=True` is unsupported. Callers wanting expansion must perform it before passing the path. Discovery still uses Gix/Git after ordinary input-path canonicalization. Repository construction validates storage and configuration through Gix/Git; overriding `Repo.config_reader()` does not replace that native discovery parser. sbxloop 2.1.36's `_MetadataRepo` subclass relied on the released constructor's permissive Python discovery and overridden no-includes reader. Five upstream tests still differ: malformed includes prevent opening, malformed local config is rejected before its custom `ProvisionError`, and version-0 repositories with version-1 storage extensions are rejected before its format check. Released GitPython 3.2.1 passes those five tests. This is a breaking subclass contract, not a demonstrated Gix-versus-Git mismatch: both backends reject the metadata. GixPython `OpenOptions` has strict/lossy configuration controls and overrides, but no supported option to open a repository while omitting its local include graph. A replacement must expose separately controlled metadata discovery and literal local configuration access with Git-compatible format validation. Do not bypass validation or reconstruct discovery/storage in Python to restore the old constructor. Downstream must construct its trusted metadata view before opening it with `Repo`, and handle native discovery errors explicitly; its original tests remain failed in the audit. `git.GitDB` and `git.db.GitDB` are compatibility aliases for `GitCmdObjectDB`, so legacy `Repo(..., odbt=GitDB)` selectors use the same managed backend. Aider 0.86.2 uses that selector for every repository open. These aliases select managed storage; gitdb's Python object database and its specialized APIs are unsupported. Object queries continue through `object_data()` and the supported `Repository.try_find_header()`/`find_object()` APIs, with the existing large-object and diagnostic CLI fallbacks. Storage uses the existing `hash_object` dispatch. `Head.tracking_branch()` asks Git's `rev-parse --symbolic-full-name` to resolve local upstreams configured with `branch..remote=.`. GixPython does not expose the corresponding Git-compatible upstream configuration resolver; this path retains CLI. Released GitPython 3.2.1 returns an invalid remote reference for these upstreams, preventing angr 10.0.4.post1 from reporting a valid local tracking branch. A native replacement must expose Git's local-upstream resolution, including its validation and diagnostics. Revision path results retain the root tree's empty path so `Repo.rev_parse("HEAD:")` can be iterated, as required by MCP Server Git 2026.10.19. Git's `objectmode` atom and Gix's `RevisionSpec.path_and_mode()` omit metadata for that root; the adapter supplies its fixed empty path after successful tree resolution. Missing tree paths retain GitPython's `Tree.join()` `KeyError` diagnostics. Git validates the tree-ish prefix, including colons inside commit-message searches, and existing backend-managed tree queries identify the missing component. This error path uses a CLI prefix resolver; GixPython would need to expose compatible failed-path context and component diagnostics to replace it. `IndexFile` queries immutable entry records; it does not retain a parsed index. The compatibility `entries` property projects those queries into a read-only snapshot keyed by path and stage, preserving readers in dlt and Renku. It uses the same Gix-backed default-index query or documented Git fallback as `iter_entries()`; snapshots do not become editable index state. Discovery distinguishes a checkout's `.git` file from an arbitrary gitfile operand. Gix reopens the canonical checkout gitfile through `gix.open_opts()` and returns `Repository.workdir()` for separate Git directories. Reopening only the administrative directory loses that checkout context. Standalone gitfile helpers still reopen the canonical native Git directory to avoid the known arbitrary-gitfile mismatch (GIX-14). The CLI path uses `rev-parse --show-toplevel` from the supplied checkout. Mistral Vibe 3.0.1 and scripted SHA-1/SHA-256, files/reftable regressions verify this adapter correction with Git 2.53.0 and GixPython 0.2.0; it does not close GIX-14's upstream compatibility bugs. Deferred/virtual state consists of opaque file bytes, materialized before Git operates and read back only after success. Private edits use Git because native bindings cannot load an arbitrary existing index (GIX-3) or validate a checked batch of paths (GIX-26). Split snapshots become standalone through Git and regain split publication through Git. Native default-index queries, fresh v2 indexes and verified tree construction remain supported. Backend coverage must count these editing fallbacks. Commit metadata uses `Repository.find_commit().decode()` plus structured `Commit.author()`/`committer()` signatures. Tag metadata uses `Repository.find_tag().decode()` and `Tag.tagger()`. A missing native tagger maps to GitPython's empty actor and zero date/offset; tag reads remain native. Decoded extra headers retain `gpgsig`. Raw Gix object bytes are used only for object streams. If repository capabilities require CLI fallback, metadata uses `git cat-file`: general formatted Git queries cannot faithfully expose all signature headers or arbitrary tags. The public identity helpers use Python glue. Human date parsing uses Git until GIX-27 is exposed; trailer extraction already uses Git. Benchmark remeasurement on 2026-10-09 used the same fixed SHA-1/files fixture `6ba2c0a2f9ee7feffd7e079621c4845820180c9a`. All 14 result digests match between CLI and Gix. Gix still uses 1 CLI process for the complete read journey, 0 for index reads and commit history, 1 for patch diffs/opening, and 3 for discovery. The CLI journey now uses 73 launches, including 26 for the 25-commit history; raw metadata queries preserve exact bytes. Existing Gix budgets remain valid. The `--fast` timing samples ran alongside validation and have substantial jitter; use their digests/counts, not their wall times, as evidence for this migration. These read-only budgets make no claim that index editing is native. ## Install and test without package indexes Prepare `.venv` and `.tox/gix` with GixPython 0.3.0. Select the backend explicitly when running against scripted fixtures: ```sh GIT_PYTHON_BACKEND=cli .venv/bin/python test/run-local.py --no-cov -q GIT_PYTHON_BACKEND=gix .tox/gix/bin/python test/run-local.py --no-cov -q \ --backend-report=.cache/gix-coverage.json ``` On Windows, use Git Bash, as CI does, and the environments' `Scripts` directories. To retain CI's coverage and pytest options: ```sh GIT_PYTHON_BACKEND=cli .venv/Scripts/python.exe test/run-local.py --color=yes -p no:sugar --instafail -vv --durations=30 \ --junitxml=.cache/windows-cli.xml --backend-report=.cache/windows-cli.json GIT_PYTHON_BACKEND=gix .tox/gix/Scripts/python.exe test/run-local.py --color=yes -p no:sugar --instafail -vv --durations=30 \ --junitxml=.cache/windows-gix.xml --backend-report=.cache/windows-gix.json ``` The Windows runner resolves `git.exe` from `PATH` to an absolute path so GitPython can share minimum-version checks. An explicit `GIT_PYTHON_GIT_EXECUTABLE` override is preserved for GitPython calls. Native fixture scripts run `git` from `PATH`, which must select the intended Git version too. Git Bash normally selects `mingw64/bin/git.exe`; the `cmd/git.exe` and `bin/git.exe` launchers add overhead to local measurements. The private config retains CI's `core.autocrlf=true` on Windows and appends the test aliases directly, matching CI without introducing config includes. The runner uses local version tags, creates an isolated Git configuration, prepares the historical test fixture through cached Bash scripts, gives pytest a separate temporary root for each run, and disables package-index access. Cleanup of these isolated directories is best-effort, so a locked leftover cannot change pytest's exit status. Tests use local repositories, including the tutorial example. The suite needs loopback sockets for its Git daemon and permission to inspect its own child processes. It does not need a remote Git server. Missing local tags or packages are errors, not invitations to download. The ignored `.cache/gix-wheels` directory contains the published GixPython wheel and the build/test dependencies available on this machine. To recreate an environment with an already-installed CPython interpreter: ```sh uv --cache-dir .cache/uv venv --offline --python .venv/bin/python .tox/gix uv --cache-dir .cache/uv pip install --offline --no-index \ --find-links .cache/gix-wheels --python .tox/gix/bin/python \ --editable '.[test,gix]' ``` For another machine, populate that directory from PyPI or its package cache before running the offline installation tests. Select wheels compatible with the interpreter and platform; ordinary and free-threaded CPython use different wheels. On platforms without a published wheel, first build one from the released source distribution and retain it in the wheelhouse. ## Scripted test fixtures The `test` extra requires GixPython 0.3.0 on Python 3.11+. Full fixture suites run on Python 3.11 and newer with either backend. Python 3.8–3.10 retain installation/import smoke coverage, including the Cygwin Python 3.9 job. Running tests also requires Bash and Git 2.52+; `gix` here is GixPython's module. Initial repository and filesystem states live in `tests/fixtures/*.sh`. `test/lib/fixtures.py` calls `gix.testtools` with an explicit lifetime: - `read_only_fixture()` and the default `fixture_repo()` reuse cached output. Tests must not mutate these paths, including object storage during serialization. - `writable_fixture()`, `fixture_repo(write=True)`, and the existing writable decorators copy cached output with `Creation.CopyFromReadOnly`. - `execute=True` selects `Creation.Execute` only for states that cannot move or copy correctly, such as symlinks, unreadable files, virtual environments, or repository metadata containing absolute paths inside its own temporary root. Operations under test remain in Python. Scripts prepare their inputs through Git CLI, whose behavior defines the expectations for both backends. Test-only configuration, malformed metadata and safety sentinels are scripted too. Pure in-memory parser inputs remain ordinary test data. The framework closes repository handles before best-effort cleanup, and the writable decorators retain failed tests' directories for inspection. Gix testtools caches script results under `tests/fixtures/generated-do-not-edit/`. This directory and generated `.tar` archives are ignored; no generated archives are checked in or required. GixPython provides deterministic fixture identities and dates and isolates Git configuration while the scripts run. Changes to sourced data/scripts must also change a fixture argument or version, because only the entry script is hashed. ### GixPython 0.2.0 testtools limitations These are test-infrastructure bugs, separate from the Git-operation ledger: - **Argument memoization:** absolute arguments reset the native path used as an in-process cache key, discarding the script name and preceding arguments. Distinct scenarios with the same absolute source path can return the first scenario's output. `test_fixture_cache_keeps_arguments_before_absolute_paths_distinct` reproduces this. The shared helper uses the supported versioned postprocessing API, with a version derived from the full script/argument tuple and an empty callback. Upstream must include each argument and the script in the key. - **Copy semantics:** `Creation.CopyFromReadOnly` follows symlinks and changes directory permissions (a mode `0500` directory becomes `0755`), and cannot copy dangling links or unreadable files. Link and permission tests explicitly use `Creation.Execute`; upstream needs a mode that preserves links and metadata without reading inaccessible files. - **Error diagnostics:** failed fixture scripts include the inherited environment in `gix.Error`, potentially exposing credentials. The helper suppresses this error text while retaining captured script output. Upstream should omit or redact the environment in command failures. - **Windows script arguments:** MSYS globbing alters literal arguments from the native fixture launcher: `prefix-${GITPYTHON_TEST_SECRET}-suffix` reached the script as `prefix-$GITPYTHON_TEST_SECRET-suffix` in Windows CI. Setting `MSYS=noglob` instead broke every fixture before its script ran, reporting `$1 : No such file or directory`. GixPython 0.2.0 pins Gitoxide commit `09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d`, whose [fixture launcher](https://github.com/GitoxideLabs/gitoxide/blob/09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d/tests/tools/src/lib.rs#L1805-L1816) passes a quoted `bash -c` program through `std::process::Command`. Literal-name fixtures use plain scenario identifiers and define their quoted names inside Bash; the submodule regression still asserts each exact name before and after the operation. Upstream must preserve both launcher quoting and literal argument contents on Windows. Do not override MSYS argument parsing in the shared helper. - **Cleanup:** native `TempDir.__exit__()` raises on removal errors. The Python owner preserves this project's best-effort cleanup contract for locked files, so cleanup cannot replace a test's actual result. The binding has no `keep()`/`into_path()` operation to release ownership without deleting files. Failed-test directories are moved aside while the native owner closes; if a locked directory cannot move, the helper tries to copy its artifacts with symlinks preserved and logs the retained path. Copy failures are logged too and cannot replace the original test failure. Upstream needs a way to detach temporary-directory ownership. These observations use the official 0.2.0 wheel and its source tag `v0.2.0` (`5bf4247facad8caf3981bd19aa260cd657611322`). The adapters remain necessary until the corresponding upstream behavior has been verified fixed. ## Conversion coverage The pytest header names the selected backend. Native runs print operation counts and fallback reasons, and `--backend-report=PATH` saves the same records as JSON. A record looks like: ```json {"method": "IndexFile.write", "outcome": "CLI: split index preservation (GIX-13)", "count": 2} ``` Search the report for `not converted` to find remaining command adapters, and for `GIX-` to find upstream limitations. A high-level method can use several native and CLI operations; counts describe operations, not entire user calls. The `git.backend` DEBUG logger reports these decisions outside pytest. `git._backend.statistics()` returns a snapshot of the process-local counters. Zero observed calls means untested in that run, not unsupported. Fixture-script Git launches happen outside `Git.execute` and its counters. Moving setup into Bash does not represent native backend conversion. Successful `Git.execute` launches also appear as `CLI process` records. This includes raw `repo.git` calls and failed Git commands that started a process, but excludes failed process creation, direct subprocesses in test helpers, other Python processes and Git's own child processes. A persistent `cat-file` launch counts once; subsequent requests through it do not. These counts apply to both installations and are separate from native/fallback decisions. Pytest prints session launch totals split into setup, call, teardown and collection/session work. `--backend-report` keeps its existing record-list format and adds `pytest.*` phase records. The `Git.execute` record is process-lifetime cumulative (including import-time probes); `pytest.session_total` is the current session's total. Optional ceilings fail the test session on an increase while allowing reductions: ```sh GIT_PYTHON_BACKEND=gix .tox/gix/bin/python test/run-local.py --no-cov -q \ --backend-report=.cache/gix-processes.json \ --max-cli-processes=TOTAL --max-cli-test-processes=CALLS ``` Replace `TOTAL`/`CALLS` with baselines for the same selected tests and platform. Setup is deliberately counted separately because fixture improvements and test additions can change it independently of native backend capability. These are pytest phases: `unittest.TestCase.setUp()`/`tearDown()` run inside the call phase, so that column can still include their fixture work. Counts cover the current Python process; they are not aggregated across xdist workers. For a stable CI assertion, the pinned repository benchmark uses per-measurement ceilings in `test/performance/cli-budget.json`: the warm Gix journey launches one CLI process, opening launches one, and nested discovery launches three. Reduce these ceilings as conversions land. The other operation rows have zero ceilings except the one-process patch diff. Before the Gix-only audit, this instrumentation on CPython 3.12.14/macOS arm64 and official GixPython 0.1.0 recorded a full suite without coverage passing 1,674 tests and 38 subtests (79 skipped, one xfailed) in 408.81 seconds. It recorded **30,898 `Git.execute` launches**: 6,523 in pytest setup and 24,375 in call phases, with zero in teardown or collection/session phases. The process-lifetime counter is 30,899 because the import-time Git probe precedes pytest session accounting. The same run recorded 21,409 fallback decisions. These totals include explicit CLI tests and fixture commands, including non-Git commands passed directly to `Git.execute`; they are a local baseline, not a count of only fallback calls. The expanded warm benchmark instead isolates a user journey: 43 CLI launches versus one Gix launch, opening nine versus one, and nested discovery eleven versus three, including the reference-format fallback and shared successful version checks. The historical full-suite counts above include Python reference-format inference and predate the current fallback; they do not establish native coverage. `GIX-1` records the native repository-format validation gap. | GitPython entry point / managed command | Gix implementation | Remaining CLI cases | | --- | --- | --- | | `Git.get_object_header`, ODB `info` | Object resolution and native header lookup | Custom storage, unsupported revision grammar | | `Git.stream_object_data`, ODB `stream` | Object bytes in an independent stream | Objects larger than 8 MiB; GIX-2 | | Commit metadata | Structured `Commit.decode()`, `author()` and `committer()` for UTF-8/default encoding | Other declared encodings and encoding headers in unexpected order use Git's text projection; GIX-29/30 | | Commit/tree serialization readback | Existing ODB stream; no separate `cat-file` process | The same storage and large-object fallbacks as ODB reads | | `Repo` opening/discovery | Native storage, worktree, object format and empty-tree metadata | Repository-format validation, unsupported layouts/environment and native validation gaps; GIX-1/14/19 | | Standalone discovery helpers | Native storage directories and gitfiles reopened at their canonical native Git directory | Rejected candidates and the same layout/environment guards as `Repo` opening; GIX-14 | | `Repo.rev_parse`, `rev_parse` metadata queries | Revision lookup, common directory, object format, bare flag, worktree root, fixed `modules`/`COMMIT_EDITMSG` locations | Repository-format validation, other metadata paths, symlinked metadata leaves, message searches and describe forms; GIX-1/14/17/22 | | Revision path/mode metadata | Native revision specification, including tree paths and index stages | Custom/sparse indexes, unsupported revision grammar or path normalization | | Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options | | Reference object reads | One exact native lookup for direct, symbolic and absent references | Partial names and native decoding errors | | Reference-name validation | Native full-name validation without opening a repository | Standalone names outside the native grammar; GIX-20 | | Managed `symbolic_ref` | Nonrecursive target lookup | Mutation, command-level missing-reference diagnostics | | Reference enumeration, `for_each_ref` | Sorted reference names and literal prefixes, preserving symbolic aliases | Dangling symbolic refs, root refs, glob patterns, other formats/options; GIX-25 | | Managed `config --get KEY` | Merged repository config snapshot | Files/streams, enumeration, mutation; GIX-12 | | Submodule enumeration and cached fields | Dedicated native `.gitmodules` parser over the existing worktree/blob source, with raw path/URL/branch values | Non-UTF-8 subsection names, other/duplicate sections, ambiguous brackets, missing/implicit fields and parser errors; GIX-12 | | Commit hooks | No bound native lookup/execution API | All hooks, including absent hooks; GIX-23 | | Worktree inventory, `worktree list` | Main and linked worktree metadata, including bare main repositories and locks | Prunable entries; GIX-14 | | `Repo.merge_base`, `Repo.is_ancestor` | Native graph queries for two revisions | Octopus/fork-point and other options | | `Commit.count` | Reachable commit count, skip/limit/first-parent | Path filters and other revision options | | `Commit.iter_items`, `Repo.iter_commits` | First-parent walks and a single tip | Reflog walks, general history ordering; GIX-8 | | Index entry reads, `ls_files` | Stage/mode/OID/path and exposed index flags | Custom and sparse indexes; GIX-3/4 | | `IndexFile.version`, `update_index` query | Native index version | Actual `update-index` mutations | | Index editing and `IndexFile.write` | Opaque byte snapshots; atomic locked publication | Private-file edits always use Git: arbitrary native index loading and strict pathname validation are missing; GIX-3/4/13/26 | | ODB `store`, managed `hash_object` | Native object hashing/writing with byte-fidelity preflight | Tags, large/nonseekable input and changed serialization; GIX-2/5/28 | | `IndexFile.write_tree` | Native tree editor with child-kind and null-ID validation | Missing non-gitlink children, invalid entries; GIX-7/24 | | Tree serialization, `mktree` | Native tree editor with child-kind and null-ID validation | Missing children, unsupported or invalid entries; GIX-7/24 | | Fresh index preparation, `read_tree` | Empty index or index from one tree | Existing index, merges, non-v2 selection; GIX-13 | | `Commit.create_from_tree`, `commit_tree` | Explicit author/committer and unsigned UTF-8 commit | Identity cleanup, object-kind validation, other encodings, signature formats, signing/options; GIX-6/21/24 | | Reference log reads, `reflog` | Existence and GitPython's reflog format | Orphan logs, arbitrary formats, writes; GIX-10 | | Committer identity, `var` | Native process/config identity from `Repository.committer()` | Per-command identity overrides, other variables and normalization/identity errors; GIX-21 | | Selected `set_object` / reference deletion, `update_ref` | Native ref edits with GitPython's existing validation | Strict create/CAS, branch-target validation, symbolic aliases other than HEAD, symbolic detachment, active-branch HEAD log, unchanged targets and log-message cleanup; GIX-9/10/21/24 | | Tree/commit `Diffable.diff` | Raw change records and unambiguous exact renames | Patches, paths, index/worktree/root diff, inexact/ambiguous renames; GIX-11 | | `Commit.stats` | Native Myers line counts, binary counts, first-parent tree comparison | Diff attributes, other algorithms, gitlinks, non-ASCII or quoted filenames, large blobs; GIX-2/18 | | `Repo.is_dirty` | Index/tree/worktree status, configured submodule checks and pathspecs | Custom/sparse indexes and non-root command directories | | `Repo.untracked_files` | Native directory walk | Extra status options and non-root command directories | | `Repo.ignored` | Native excludes with tracked-file suppression | Symlink/submodule traversal, unsupported path normalization, and unavailable path metadata | Revision APIs preserve a reference object's validated full name. The shared `Git._check_revision()` first uses the exact reference reader to verify existence and the symbolic-reference chain; it passes the full name rather than the resolved object ID. Strings retain Git's name guessing, and commit objects retain their IDs. This prevents same-named tags from replacing branches without losing the ref identity needed by reflog walks, fork-point lookups and remote archives. `merge_base()` validates nested list/tuple operands before string conversion. `exact-refs.sh` covers these paths with shadowing tags and missing references in SHA-1/SHA-256 and files/reftable repositories on both backends. These name-sensitive operations retain CLI fallback. GixPython 0.3.0's `Repository.rev_walk()` has no Git-compatible reflog-walk mode, `Repository.merge_base()` exposes graph merge bases rather than reflog-based fork-point selection, and no upload-archive client API is exposed. The regressions compare Git 2.54.0's reflog history, old fork point after an upstream rewind, and default upload-archive server behavior, which rejects literal object IDs. Native replacements must expose matching reflog walking and fork-point selection and the upload-archive protocol; no Python implementation substitutes for them. Reference reads require an exact name, unlike revision APIs that intentionally apply Git's name guessing. `reference_info()` uses `Repository.try_find_reference()` and verifies the returned name for `HEAD` and fully qualified refs. Other root names, including `REBASE_HEAD`, retain the CLI fallback: GixPython's lookup accepts partial names, including symbolic lookalike tags for missing root names (GIX-33). Native `symbolic_ref` dispatch verifies the returned name and falls back for a lookalike. A native root-reference conversion needs verified existence and error behavior, including Git's distinction between an absent and an invalid reference. `Repo.currently_rebasing_on()` probes `git show-ref --exists` and then uses the shared exact reference reader; the existence probe remains CLI on both backends. `exact-refs.sh` tests missing names beside fetched tags with both object and reference formats. Reftable still uses GIX-1; this change claims no new native coverage. The symbolic lookalike mismatch remains open upstream as GIX-33. Reference names and revisions are encoded with `surrogateescape` before calling GixPython's byte-capable `Target.Symbolic`, `Repository.try_find_reference`, `Repository.rev_parse_single`, `Repository.rev_parse` and `RefEdit` APIs. Status and untracked-file filters likewise pass byte patterns to `StatusPlatform.into_iter()` and `Repository.dirwalk_iter()`. Their regression checks matching and nonmatching patterns containing undecodable bytes, and prohibits CLI fallback in Gix mode. This preserves non-UTF-8 packed tag names without interpreting reference storage in Python. `test_non_utf8_tag_names_preserve_bytes` covers lightweight and annotated tag enumeration in SHA-1 and SHA-256 repositories on both backends and all platforms. `test_non_utf8_tag_names_resolve` forbids CLI fallback during native resolution. Resolution requires POSIX: Windows command-line arguments cannot preserve the original non-UTF-8 bytes, and native Windows lookup has GIX-32. Windows CI with Git 2.56.0.windows.2 and CPython 3.15.1 free-threaded enumerates both packed names, but resolving the surrogate-escaped `annotated-\udce3` name through `git rev-parse --verify` exits 128 with `fatal: Needed a single revision`. Enumeration remains tested on Windows on both backends, along with the GIX-32 native diagnostic fallback. This corrects adapter input conversion; GixPython 0.3.0 accepts the original bytes through those APIs on POSIX. Repository-backed native operations fall back for reftable (GIX-1) and repositories with `extensions.compatObjectFormat` (GIX-19). Process-returning calls, timeouts, custom storage/environment, global Git options, and unhandled command options also retain their CLI contracts. A broken installed extension is reported as an import error unless CLI was explicitly selected. With automatic selection, only an absent top-level `gix` selects CLI mode. ## Performance work All measurements in this document describe unreleased GitPython development revisions, using the GixPython and Git versions stated for each run. They are validation evidence, not a history of changes between GitPython releases. Use the [measurements after the Gix-only audit](#measurements-after-the-gix-only-audit) for the corrected implementation. Older snapshots retain their original workloads and include shortcuts that were subsequently removed. ### Shared minimum-version checks Both installations reuse successful minimum-Git-version checks across fresh `Git` wrappers with the same resolved executable, executable metadata, working directory, effective environment and `Git.refresh()` generation. Two extra checks in the fixed warm probe now launch zero processes instead of two; two equivalent cold wrappers launch one probe. Public `version_info` caching remains per instance, and old versions still fail before repository creation. This is shared CLI housekeeping: it caches an actual Git answer and records no native success. It is not a Gix implementation of a missing capability. The pinned warm Gix opening/discovery ceilings fall from 2/4 to 1/3 here; the reference-format query and rejected-candidate diagnostics remain real CLI calls. Cold contexts can still require a version probe. Global Git options and ambiguous Windows executable search retain per-instance probes; explicit absolute executables can share checks on Windows. The shared cache is bounded to 128 contexts. Call `Git.refresh()` if a launcher's reported version changes through external state without changing its executable or environment metadata. ### Existing-repository benchmark [`test/performance/README.md`](../test/performance/README.md) describes the `pyperf` harness and fixed GitPython 3.1.45 fixture. It measures eleven public-API operations and their complete journey on one already-open `git.Repo`, plus separate direct opening and discovery from `git/objects`. Fresh high-level wrappers preserve the cost of actual operations; imports, fixture preparation and parity preflight are outside timing. Native repository opening remains inside operation timing. Historical retained-handle results below included refresh costs instead. Submodule inventory, revision path/mode lookup and raw commit/tree readback extend the already-open journey, each with a zero-CLI ceiling. The readback measurement reads existing bytes, including the signed fixture commit, without rewriting objects. Historical tables below retain their original workloads. At `244e418da6cc43de129cbe2908d11be4c5ad457a`, using official GixPython 0.1.0 and the same existing CPython 3.12.14/macOS arm64 interpreter for both installations, with Git 2.54.0 (Apple Git-157), six worker processes with three values each produced the following means and standard deviations. GixPython ran first, then CLI; all eleven result digests matched. The fixture has one branch, one untracked file and an ignored directory, with ambient Git configuration disabled. | Measurement | CLI (ms) | GixPython (ms) | CLI / Gix | | --- | ---: | ---: | ---: | | Already-open journey | 230.90 ± 6.87 | 260.14 ± 13.37 | 0.89× | | Reference inventory | 22.77 ± 1.15 | 1.17 ± 0.26 | 19.52× | | 25 first-parent commits with metadata | 9.24 ± 0.46 | 73.61 ± 7.24 | 0.13× | | Root tree and README blob | 16.02 ± 1.07 | 2.40 ± 0.46 | 6.66× | | Index entries | 8.43 ± 0.57 | 2.00 ± 0.38 | 4.21× | | Commit count and ancestry queries | 82.28 ± 2.27 | 36.19 ± 0.84 | 2.27× | | Latest diff and commit statistics | 29.98 ± 1.66 | 60.98 ± 2.45 | 0.49× | | Patch diff (includes CLI fallback) | 17.87 ± 1.28 | 10.86 ± 1.01 | 1.64× | | Dirty/untracked/ignored paths | 68.56 ± 3.18 | 64.74 ± 2.00 | 1.06× | | Direct opening and close | 95.08 ± 4.64 | 35.84 ± 2.02 | 2.65× | | Nested discovery and close | 112.58 ± 5.38 | 49.20 ± 4.16 | 2.29× | Ratios above one favor GixPython. This workload's complete journey is about 13% slower with GixPython: metadata reads and commit statistics offset gains elsewhere. These are warm-cache measurements on one machine, with `pyperf` stability warnings for several samples; they do not establish a general speedup. Full-suite times below also include setup and coverage. The separate `Backend benchmark` CI job runs both installations on one runner, publishes every measurement and `pyperf` significance reporting, and retains raw JSON and comparison artifacts. Results include revisions and per-operation native/fallback decisions. Adding a `MEASUREMENTS` entry extends the journey, individual timings and parity checks together. Opening/discovery remain separate lifecycle measurements. CI fails on execution/parity errors; timing ratios are observational on shared runners. ### Historical retained-handle and opening measurements These results predate the Gix-only audit. The measured implementation inferred reference format, repaired linked-worktree metadata and rejected some discovery candidates in Python. Its zero-CLI counts and associated opening speedups do not establish Gix coverage and are superseded by the corrected implementation. Keep the raw observations below only as a historical record. At `c49bbec0afb806df66a33fa9c5d76e21ac8f0314`, the same pinned fixture, official GixPython 0.1.0 and existing CPython 3.12.14/macOS arm64 interpreter were measured sequentially, Gix first and CLI second, after tests finished. Each backend used three worker processes, three values per worker, four loops per value and one warmup. All eleven result digests matched and the checked-in CLI ceilings passed. Raw local results are `/private/tmp/gitpython-retained-final-{gix,cli}.json`; this table is a journal snapshot, while CI continues to publish fresh measurements. | Measurement | CLI mean ± stdev (ms) | GixPython mean ± stdev (ms) | CLI / Gix | Native / fallback | CLI launches: CLI / Gix | | --- | ---: | ---: | ---: | ---: | ---: | | journey | 209.44 ± 17.65 | 263.25 ± 5.52 | 0.80× | 55 / 2 | 23 / 1 | | inventory | 19.76 ± 0.78 | 0.89 ± 0.03 | 22.30× | 3 / 0 | 3 / 0 | | history_25 | 7.49 ± 0.17 | 79.02 ± 9.67 | 0.09× | 26 / 0 | 1 / 0 | | browse_tree_and_blob | 13.55 ± 0.39 | 2.06 ± 0.41 | 6.59× | 5 / 0 | 2 / 0 | | read_index | 6.96 ± 0.08 | 2.59 ± 0.47 | 2.69× | 1 / 0 | 1 / 0 | | revision_graph | 63.27 ± 2.57 | 32.72 ± 0.22 | 1.93× | 9 / 0 | 6 / 0 | | diff_and_stats | 20.78 ± 0.37 | 65.33 ± 1.25 | 0.32× | 5 / 0 | 3 / 0 | | patch_diff | 14.06 ± 0.39 | 8.61 ± 0.08 | 1.63× | 3 / 2 | 2 / 1 | | worktree_status | 56.61 ± 1.43 | 70.68 ± 2.18 | 0.80× | 3 / 0 | 5 / 0 | | open_repository | 69.28 ± 0.59 | 1.00 ± 0.07 | 69.20× | 1 / 0 | 11 / 0 | | discover_repository | 81.68 ± 0.68 | 0.90 ± 0.19 | 90.52× | 1 / 0 | 13 / 0 | The pre-audit implementation launched no CLI processes for these opening and discovery probes. It measured about 69× and 91× faster lifecycle operations, while the complete already-open journey was about 26% slower with Gix. These lifecycle ratios include the shortcuts removed by the audit. Retaining a Python handle alone does not resolve the existing history/statistics costs. Several rows have `pyperf` sample-size or variability warnings, so timings describe this machine and workload rather than a general speed guarantee. The full Gix suite without coverage passed **1,678 tests and 38 subtests**, with 79 skips and one expected failure, in **337.51 seconds (5m 37.51s)**. It recorded **22,319 `Git.execute` launches**: 3,833 setup, 18,486 call, zero teardown or collection/session. The process-lifetime count is 22,320 including the import probe. Against the preceding 408.81-second/30,898-launch baseline, this local run took 17.4% less time and launched 27.8% fewer CLI processes; the suite has four additional regressions, and the timing comparison is observational. Affected tests ran with Gix before CLI: 304/264 repository, command, backend and safety tests; 136 command-guard tests per backend; 31 positional tests per backend; 13 process-count/budget tests per backend. Repository suites also passed 14 subtests, with three skips. Ruff lint/format, mypy and basedpyright passed. ### Measurements after the Gix-only audit At `5ef5c62f2e2dafe3e7ddb0deed6998fd7092d360`, the adapters used Gix for supported operations and retained CLI fallback for missing native capabilities. The audit removed Python reference-format inference, metadata-path construction, committer resolution, reflog cleanup, discovery repairs and absent-hook success. Its findings were recorded in GIX-1/10/14/21/22/23; the capability follow-up below refines those requirements. Retained native repository state and conversions that actually use Gix APIs remain in place. The full-suite and timed benchmark figures in this section describe that snapshot, before the subsequent metadata and discovery changes. The full Gix suite without coverage passed **1,710 tests and 38 subtests**, with 79 skips and one expected failure. It took **342.36 pytest seconds / 342.80 seconds including the runner (5m 42.80s)** on existing CPython 3.12.14/macOS arm64, official GixPython 0.1.0 and Git 2.54.0. This includes CLI-inventory and Trace2 overhead. The corrected run recorded **22,694 `Git.execute` launches**. The earlier 12,394-launch result included the removed Python shortcuts and is superseded as a coverage claim. The corrected run also contains 19 additional regression cases; these are observed totals for their respective revisions. | Launch source | Before audit (`d2e788a1`) | Corrected (`5ef5c62f`) | | --- | ---: | ---: | | Managed operations with Gix enabled | 8,836 | 16,871 | | Managed operations with Gix explicitly disabled | 309 | 339 | | Minimum-version probes | 2,483 | 4,698 | | Raw `Git.execute` calls | 766 | 786 | | **Total pytest launches** | **12,394** | **22,694** | The corrected total splits into 4,464 setup and 18,230 call-phase launches, with zero in teardown or collection/session. Call phases include `unittest` fixture work. The Python inventory reconciles exactly with the pytest counter; the process-lifetime backend count is 22,695 including its import probe. Trace2 additionally identifies 3,685 Git child processes and 37 processes outside the captured wrapper. Its total overlaps the Python inventory, so the two totals must not be added together. The benchmark used the same pinned GitPython 3.1.45 repository and interpreter for both backends, sequentially after tests, Gix first and CLI second. Each used three worker processes, three values per worker, four loops per value and one warmup. All **14 result digests matched**, and every corrected CLI ceiling passed. The eleven operations form one journey on an already-open `Repo`; opening and discovery remain separate lifecycle measurements. | Measurement | CLI mean ± stdev (ms) | GixPython mean ± stdev (ms) | CLI / Gix | Native / fallback | CLI launches: CLI / Gix | | --- | ---: | ---: | ---: | ---: | ---: | | journey | 314.35 ± 2.33 | 264.94 ± 3.30 | 1.19× | 79 / 2 | 43 / 1 | | inventory | 19.50 ± 0.31 | 0.99 ± 0.08 | 19.68× | 3 / 0 | 3 / 0 | | history_25 | 7.59 ± 0.12 | 73.32 ± 2.17 | 0.10× | 26 / 0 | 1 / 0 | | browse_tree_and_blob | 13.45 ± 0.21 | 1.78 ± 0.22 | 7.55× | 5 / 0 | 2 / 0 | | read_index | 7.00 ± 0.11 | 2.09 ± 0.27 | 3.35× | 1 / 0 | 1 / 0 | | submodule_inventory | 72.10 ± 0.71 | 3.21 ± 0.20 | 22.44× | 13 / 0 | 11 / 0 | | revision_paths | 55.27 ± 0.42 | 1.41 ± 0.12 | 39.12× | 6 / 0 | 8 / 0 | | object_readback | 6.60 ± 0.16 | 1.22 ± 0.10 | 5.43× | 5 / 0 | 1 / 0 | | revision_graph | 62.26 ± 1.15 | 33.25 ± 0.36 | 1.87× | 9 / 0 | 6 / 0 | | diff_and_stats | 21.43 ± 1.28 | 67.83 ± 7.58 | 0.32× | 5 / 0 | 3 / 0 | | patch_diff | 13.96 ± 0.12 | 8.97 ± 0.23 | 1.56× | 3 / 2 | 2 / 1 | | worktree_status | 36.59 ± 1.01 | 70.55 ± 4.64 | 0.52× | 3 / 0 | 5 / 0 | | open_repository | 58.47 ± 1.12 | 7.71 ± 0.25 | 7.58× | 1 / 1 | 9 / 1 | | discover_repository | 70.72 ± 0.68 | 20.49 ± 0.38 | 3.45× | 1 / 5 | 11 / 3 | The expanded warm journey is **1.19× faster** with Gix in this sample, with 43 CLI launches reduced to the single patch-diff launch. Opening is **7.58× faster** and retains one reference-format query; discovery is **3.45× faster** and adds two CLI queries for rejected native candidates. Its five fallback decisions include both discovery and command-adapter decisions, while only three processes are launched. Cold contexts can also need a version probe. The previous zero-launch opening/discovery results included Python substitutes and are historical only. History, diff/statistics and worktree-status measurements remain slower with Gix despite making no CLI launches. They need separate profiling. Several rows have `pyperf` sample-size or variability warnings; these measurements describe this machine and workload rather than a general speed guarantee. Every affected selection passed with Gix before CLI. The final full suite above used Gix; CLI validation used the affected selections. Repository-wide Ruff lint/format, mypy and basedpyright passed. The benchmark comparison independently checked result parity, fixture/source identity and all process ceilings. Generic configuration and standalone reference-name gaps remain documented under GIX-12/20. That snapshot retained native repository state; the current accessor opens a fresh handle for each operation, as documented below. Raw benchmarks, comparison tables and logs from that snapshot are under `.cache/gix-only-audit/benchmark-*` and `.cache/gix-only-audit/pyperf-comparison.md`. The joined inventory, per-call CSV/JSONL, grouped commands, passing full-suite log and runner timing are in `.cache/gix-only-audit/full-inventory-5ef5c62/`. The older `.cache/gix-conversions/benchmark-{gix,cli}.json` and `final-inventory-d2e788a/` remain historical evidence. CI continues to publish fresh measurements and enforce the checked-in operation-specific ceilings. ### Native capability follow-up The follow-up used the same official GixPython 0.1.0 and existing CPython 3.12.14, with runtime and test code at `ee947c98540396d5644f3a7fea7c807633f4b5b8`, checked with Gix before CLI. - Reference storage can already be identified through native config. Direct SHA-1 and SHA-256 regressions confirm that reftable `HEAD` access raises `gix.Error` with an unsupported-storage cause; the binding does not expose a separate `Unsupported` exception. Strict native opening and `HEAD` access still accept unknown repository extensions that Git rejects, so the CLI format query remains necessary for validation (GIX-1). - Native `git_dir()` supplies the locations for `modules` and `COMMIT_EDITMSG`, including linked worktrees. The adapter now adds only those fixed leaf names, after reopening noncanonical Git paths through Gix. Each ordinary lookup drops from one CLI launch to zero. Other metadata names and symlinked leaves retain Git's path resolution (GIX-22). - Gix already exposes the worktree of a bare main repository. Combining its configured `is_bare()` flag with `workdir()` fixes opening and inventory in the adapter; the original classification difference remains tracked as an upstream compatibility bug. Gitfiles are reopened at their canonical native Git directory with the same strict options; supported symlinked-target helpers now need zero CLI launches instead of one. This also prevents an arbitrary gitfile location from becoming the worktree. Dangling `commondir` handling and typed failure information remain gaps; rejected candidates keep Git diagnostics (GIX-14). - Native snapshot overrides resolve committer name, email and date. A cheap independent repository clone is needed to isolate per-command overrides: Python copying is unsupported and the internal binding clone shares state. Shared snapshots and process environment remain untouched (GIX-21). - `LogChange` already configures each ref edit's message and log policy. A direct native regression preserves whitespace where Git normalizes it, so message cleanup still needs a native helper or policy (GIX-10). Final focused validation passed **120 Gix tests and 14 subtests** (one skip) in 22.93 seconds, followed by **34 CLI tests and 14 subtests** (two skips, including the native-only module) in 6.14 seconds. This covered the complete native-backend, launch-counter and benchmark-comparison modules plus the affected shared discovery, gitfile, worktree and repository-construction tests. Repository-wide Ruff lint/format, mypy and basedpyright passed. An untimed benchmark preflight, Gix first and CLI second, matched all **14 result digests** and passed the existing launch ceilings. The warm journey remains CLI/Gix **43/1** launches, opening **9/1**, and nested discovery **11/3**; the new fixed-path and gitfile-helper reductions are asserted by their focused tests. No full-suite rerun or fresh timed benchmark is claimed for this follow-up. Logs, backend reports and the count comparison are under `.cache/gix-capability-followup/` (`15-final-*` and `16-benchmark-*`). ### Windows CI process overhead The Python 3.12 jobs in [PR #2274's Python package run](https://github.com/gitpython-developers/GitPython/actions/runs/37628358853) spent 57m 25s in pytest on Windows and 5m 2s on Ubuntu. Their process reports counted 85,777 and 82,683 `Git.execute` launches respectively. The Windows submodule test modules accounted for approximately 36 minutes, based on the timestamps of their test results; the delay was spread across many operations. A local Windows profile of `test_file_handle_leaks` and `test_update_no_fetch_is_recursive[root-no-fetch]`, with CI's coverage and pytest options, counted 2,447 launches. About 65 of 77 seconds were spent in Git command execution, including 44 seconds in repository construction; forced garbage collection accounted for about three seconds. Combining `--show-ref-format`, `--show-object-format`, `--is-bare-repository` and `--git-common-dir` in one `rev-parse` invocation reduced the same selection to 2,009 launches: three saved for each of 146 repository opens. Git still computes all four values. The common-directory path is last and split only after the three scalar fields, preserving paths containing newlines. Three paired measurements of 20 ordinary repository opens on Windows gave median times of 300.74 ms before and 221.18 ms after, with matching metadata and 11 versus eight launches per open. These use CPython 3.12.13 and Git 2.55.0.windows.3; timing is observational, while the launch reduction is checked by the regression tests. A 50-call `git version` probe on the same host measured median times of 17.49 ms per call on Windows and 1.27 ms in Ubuntu WSL, which uses CPython 3.14.4 and Git 2.53.0. Even trivial Git commands carry appreciable startup cost on Windows. This optimizes the CLI backend's construction path. Supported Gix discovery still uses its native metadata and the separate Git format-validation query; the Gix launch ceilings and open compatibility bugs remain unchanged. CI now includes `--durations=30` so subsequent slow tests are visible directly. Local Windows reproduction must retain CI's `core.autocrlf=true` setting and place pytest's temporary directories outside a Git checkout. Isolating all Git configuration without restoring that setting changes the newline test; placing `--basetemp` under this checkout lets empty-directory Git probes discover the parent repository instead. The full local baseline at `105114db` recorded 86,032 launches in 3,990.28s; the two submodule modules accounted for 2,421.59s. It had 1,652 passing tests and the two harness failures described above, both of which passed unchanged after correcting the setup. The patched run exited successfully with 1,662 passed, 60 skipped, nine expected failures, two unexpected passes and 40 passing subtests. It recorded 77,732 launches in 3,715.16s. These runs overlapped, so their wall times are not a controlled comparison. The paired repository-open benchmark above measures the affected operation separately. Logs, profiles, JUnit results and backend reports are retained locally under `.cache/ci-performance/`. ### Submodule fixture process overhead on Windows Windows CI and the local runner now resolve `git.exe` from `PATH` to an absolute `GIT_PYTHON_GIT_EXECUTABLE` when no override was supplied. This enables the existing minimum-version cache without changing the library's executable lookup or cache invalidation. Submodule fixtures also pass `skip_hooks=True` when committing their test history; the library default and dedicated hook tests continue to run hooks. A serial Windows comparison on CPython 3.12.13, Git 2.55.0.windows.5 and GixPython 0.1.0 selected the Windows destination-name rejection tests, `test_update_no_fetch_restores_deinitialized_submodule`, and `test_update_no_fetch_is_recursive`. Both runs used the direct Git executable on `PATH`, CI's flat alias config and `core.autocrlf=true`, with profiling enabled and coverage disabled. All 37 cases passed in both runs. | Test invocation and fixtures | Pytest time | Git launches | Version probes | Hook launches | | --- | ---: | ---: | ---: | ---: | | `GIT_PYTHON_GIT_EXECUTABLE=git`, fixture hooks enabled | 87.10s | 1,437 | 403 | 177 | | Absolute executable, fixture hooks skipped | 77.20s | 1,128 | 271 | 0 | The 309 eliminated launches comprise 132 redundant version probes and three hook checks for each of 59 fixture commits. This sample was about 11% faster; it is not a full-suite speedup estimate. Git Bash CI already selects `mingw64/bin/git.exe` and appends the alias config directly, so correcting the local runner's launcher/config differences is not an additional CI gain. Logs, profiles, JUnit results and operation reports are retained locally under `.cache/test-cleanup/windows-submodule-{baseline,optimized}*`. ### Test-suite setup measurements The fixture optimizations below are implemented as separate commits, each validated with GixPython before CLI. The original local measurements use official GixPython 0.1.0 and existing CPython 3.12.14 on macOS arm64 at `89c609cf92335e75652d7725682e215a9ea080e5`. The full suite with coverage took 757.85 seconds wall-clock (757.04 seconds reported by pytest): 1,649 tests and 38 subtests passed, 79 skipped, and one expected failure. Operation reporting counted 114,800 native operations and 39,002 CLI fallback decisions. Those decisions are not a complete subprocess count: raw `repo.git` calls and subprocesses started by Git are not all counted. ### Open fresh native repository state Managed operations access Gix through `Repo._get_gix_repository()`, which opens a fresh `gix.Repository` for each call. Standalone commands use the same opening path. No native handle, environment/file fingerprint or refresh lock is cached on `Repo`; configuration, includes and storage metadata are read by Gix again on each open. Concurrent operations therefore receive independent handles. Execution handles override hooks, fsmonitor and automatic maintenance. Configuration queries omit those overrides so synthetic settings do not appear as user settings. Storage overrides, reftable and compatibility object formats retain their existing CLI guards. Reopening itself adds no Git CLI calls. `Repo.close()` releases persistent CLI object readers. Native handles remain local to operations; callers holding separately opened handles or iterators must release them before deleting their repository, especially on Windows. Pickling restores the command's weak owner reference without native resources. This replaces the retained-handle policy and its best-effort invalidation via file metadata, environment changes and CLI launches. Reopening repeats native initialization work but avoids fingerprint construction and unconditional reloads for configurations with includes. An earlier 1,000-call benchmark without coverage measured an object read using one retained native handle at 0.046 ms per call, compared with 0.635 ms through the fresh-open stream adapter. Opening and validating a native repository alone took 0.418 ms per call. Those historical measurements do not describe the current accessor or estimate whole-suite speedup; rerun the backend harness to measure it. ### Reuse prepared test fixtures (implemented) A 19-case submodule rejection sample without coverage took 15.62 seconds: 1,258 `Git.execute()` calls accounted for 11.86 seconds, while 1,995 native repository-open/validation attempts accounted for 0.84 seconds. Most work was fixture setup. This sample supports prioritizing repeated setup subprocesses; it is not a profile of the entire suite. The original collection found 1,729 parameterized cases from 703 distinct source bodies. Source inspection identified the following conservative set of 422 cases whose assertions do not require changing repository state after preparation. The remaining cases have not been exhaustively classified; these are reuse candidates, not proof that a shared fixture is safe in every execution order. | Original cases | Repeated setup | Implemented change | | --- | --- | --- | | 166 | `TExc` (157) and `TestActor` (9) inherit repository-building `TestBase`. | Use the existing `TestCase` base without repository setup. | | 182 | Three submodule rejection bodies repeatedly build `movable_submodule`, then check snapshots for no mutation. | Prepare logical-name baselines once and copy the parent per case, retaining fresh wrappers and independent writable files. | | 51 | Six submodule rejection bodies prepare nested metadata, separate metadata, intermediate/leaf symlinks, or retained metadata before checking rejection. | Cache ten prepared layouts and restore complete copies at their original paths, preserving absolute Git links and symlinks. Cleanup is best-effort; a locked active copy invalidates the layout so the next case rebuilds at a fresh path. | | 15 | Eight revision-query bodies rebuild the same four-commit graph, refs, index, and reflogs through `rev_parse_repo`. | Prepare the graph once and copy it for every consumer, including mutating cases; recreate repository, branch and commit wrappers. | | 8 | Tree lookup bodies clone and check out `0.3.2.1` through `with_rw_repo`. | Read the historical tree directly through the existing class repository, removing clones and checkouts. | The `movable_submodule` baseline also serves mutating cases: all writable refs, index, config, objects, worktree and module metadata are filesystem copies, while the local clone source remains shared and immutable. The original 322 consumers no longer repeat repository initialization and submodule cloning. The original 68 `local_submodule` cases copy both source and parent from one prepared two-commit layout because these tests also mutate the source. The fixture relocates all source URLs and records the private URL in parent history for `RootModule` comparisons. No mutable Git metadata is hard-linked. Historical dependency sources are now lazy session fixtures. Originally all 25 `TestBase` classes reconstructed both sources (50 builds), although only four classes called the URL helpers. The suite now prepares each needed source once, with consuming tests retaining independent writable clones. Four additional checks exercise isolation: edits and refs in movable copies, restoration after deliberate mutation with an absolute symlink, private source commits, and revision-graph changes. Existing security no-side-effect snapshots remain in place. Native repository lifetime and refresh limitations are documented above. Per-change affected tests on existing CPython 3.12.14/macOS arm64 with official GixPython 0.1.0, without coverage (wall-clock seconds including runner setup): | Change | Passed cases | GixPython first | CLI second | | --- | --- | --- | --- | | Repository-free actor/exception tests | 166 | 0.47 s | 0.49 s | | Lazy historical sources, all consumers | 206, plus 14 subtests; 6 skipped, 1 xfailed | 90.89 s | 160.82 s | | Movable baseline, top-level submodule tests | 333 | 88.63 s | 178.75 s | | Prepared rejection variants | 51 | 12.34 s | 26.23 s | | Layout restoration check | 1 | 1.33 s | 1.62 s | | Prepared revision graph | 23 | 4.39 s | 7.57 s | | Historical tree lookups, whole tree module | 22 | 1.66 s | 3.86 s | | Prepared no-fetch source and parent | 69 | 56.27 s | 117.52 s | These selections overlap, and their timings are validation records rather than isolated before/after benchmarks. The full-suite measurements below provide the broader comparison. Only the existing interpreter was used. At `c8ee26c7da373e28cc7ede17ef2eaadd763fcdfc`, the full GixPython suite with coverage passed in **404.61 seconds wall-clock (6m45s)**, with pytest reporting 404.12 seconds: 1,653 passed, 79 skipped, one expected failure, and 38 subtests passed. Coverage remains 90%. Compared with the original 757.85-second run, this saved 353.24 seconds (46.6%, about 1.87 times faster). This is one local before/after run per revision, not a statistical benchmark. The same operation counters now record 102,581 native operations and 21,402 CLI fallback decisions, down from 114,800 and 39,002 respectively. Raw Git calls and child processes remain outside those counters. The subsequent full CLI run with coverage passed in **755.78 seconds wall-clock (12m36s)**, with pytest reporting 755.30 seconds: 1,617 passed, 80 skipped, one expected failure, and 38 subtests passed. CLI coverage is 82%; backend-specific tests account for different collection and coverage. This CLI run validates the optimized suite; there is no matching pre-change CLI full-run measurement here. Repository-wide Ruff lint/format, mypy and pyright passed. ## GixPython / Gitoxide follow-up ledger These are local findings, not filed issues. **Every observed divergence from the equivalent Git operation is a compatibility bug**, including intentional differences. Gix must offer matching behavior, at least through an API or mode as strict as Git. This covers acceptance/rejection, validation, returned data, ordering, paths and mutation side effects. `strict_config(True)` and `bail_if_untrusted(True)` alone do not provide that guarantee (GIX-1/14). The status distinguishes reproduced bugs from missing APIs and unverified coverage. Low-level primitives can remain available, but a compatible API or mode is still required. An adapter workaround, including reopening through Gix, does not close an upstream compatibility bug. Retain its expected/actual behavior, version and reproduction until the upstream fix or compatible mode has been verified. Python must not implement the missing Git behavior. Unless stated otherwise, the evidence uses official GixPython 0.1.0, its pinned Gitoxide revision `f819565c2c4c56619c4888acef6cf3b8144cbccb`, and Git 2.54.0 on macOS arm64 with existing CPython 3.12.14. The repository and commands below identify the historical fixture where needed. | ID | Status | Finding and evidence | Required Gix behavior/API | | --- | --- | --- | --- | | GIX-1 | Bug; missing API | Native config exposes `extensions.refStorage`; reading `HEAD` in SHA-1/SHA-256 reftable repositories raises `gix.Error` with an unsupported-storage cause. However, strict native opening plus a successful `HEAD` read accepts unknown version-1 extensions that Git rejects. It also accepts `extensions.refStorage=files` with repository version 0, which Git rejects. | Git-compatible repository-format/extension validation, typed native errors, and eventual reftable support. A dedicated format getter is not needed just to identify storage. Keep the CLI format query because it also validates the repository, with regressions for both the unsupported-HEAD signal and unknown extensions. | | GIX-2 | Missing API | Object lookup buffers full contents. `write_blob_stream` also reads its input into memory. | Bounded-memory object readers/writers usable by Python. The adapter currently caps native blob transfers at 8 MiB. | | GIX-3 | Missing API | Index bindings open the repository's index but cannot load an arbitrary existing index file. `set_path()` only chooses its write destination. | An index loader accepting a path, including relative-path semantics. | | GIX-4 | Missing API | Sparse index expansion is not exposed. | Expansion preserving skip-worktree and other metadata before editing. | | GIX-5 | Unverified | Structured-object writers may decode/re-encode supplied bytes. This is a compatibility guard, not a demonstrated corruption bug. | A raw-object writer, or a guaranteed byte-preserving contract. The current adapter checks the round trip in an in-memory ODB first. | | GIX-6 | Missing API | Native commit construction accepts a string message. GitPython also supports arbitrary message bytes and other declared encodings. | Byte-message commit construction with explicit encoding/signature semantics. | | GIX-7 | Missing API | Native tree editing requires existing non-gitlink child objects. Git's `mktree --missing` / `write-tree --missing-ok` permit missing children. | Missing-object tree construction with equivalent validation. | | GIX-8 | Bug | General revision iteration differs from Git's default ordering. The same fixture enumerated 5,439 commits in both engines, with the first ordering difference at position 178. Its 1,923-commit first-parent chain matched. | A Git-compatible walk order; counts and first-parent walks already work. See the reproduction notes below. | | GIX-9 | Bug | `PreviousValue.MustNotExist` accepts an existing ref already at the requested target. Native tests explicitly permit this; Git's strict create/CAS rejects it. | A strict native expectation, enforced under the same ref lock. A Python existence precheck would race. | | GIX-10 | Bug; missing API | `LogChange` preserves ` keep\t spaces ` where Git stores `keep spaces`. Updating the checked-out branch directly or through an alias omits the HEAD log entry Git appends. Updating that branch to its unchanged target also omits Git's HEAD log entry. Detaching symbolic HEAD records a null old OID instead of Git's resolved previous commit. `RefLog.Only` cannot express an independent arbitrary old OID, and reference-bound readers cannot access orphan logs. | Fix message normalization, secondary HEAD logging and old-OID selection in Gix, at least in a Git-strict transaction mode; expose raw reflog append and orphan-log access. `LogChange` already configures each edit's message, mode and force-create flag; `core.logAllRefUpdates` controls creation, not cleanup. Keep the existing `_update_ref` and `_reflog` fallbacks before mutation; do not normalize messages in Python. | | GIX-11 | Unverified | Inexact and ambiguous rename pairing/scores have not been established as Git-compatible. This is a conservative guard, not a claimed native bug. | Verified pairing, scoring, tie-breaking and option parity. Exact unambiguous renames are native. | | GIX-12 | Missing API | Generic config bindings lack standalone parsing, ordered section/key enumeration, multivars, unset/remove-section and source-scoped queries. Dedicated `.gitmodules` parsing is available and now used for supported submodule reads. | Those operations for `GitConfigParser` and remote/branch configuration. Native merged getters cannot replace repository-only config readers. | | GIX-13 | Bug; missing API | Native index writing normalizes v4 to v2, offers no version setter, and expands split indexes. | Version and split-index preservation. Tests now assert that a split index remains split after an update. | | GIX-14 | Bug; missing API | For a linked worktree of a bare main repository, `is_bare()` returns true while `git rev-parse --is-bare-repository` returns false; `workdir()` does expose the worktree. Initial gitfile opening can retain a noncanonical target and use an arbitrary gitfile's location as the worktree, unlike Git. Discovery accepts undecodable HEADs until explicit `head()` decoding and ignores dangling `commondir` symlinks that Git rejects. Bindings flatten failure kinds into `gix.Error`. Windows also exposes adapter path-formatting differences and a worker panic on an undecodable `commondir`; see the Windows reproduction below. | Provide Git-compatible classification, canonical gitfile locations and layout validation, plus typed errors instead of panics. Format native locations compatibly with Git's command output. The adapter combines Gix metadata, reopens through Gix and forces HEAD decoding, retaining CLI for remaining validation/diagnostic gaps. Those workarounds do not close these compatibility bugs. A Git-strict mode must cover these cases; strict config and trust settings alone do not. | | GIX-15 | Bug | Native blame disagrees with Git even with Myers and rewrite tracking selected. In the fixture's `README.md`, lines 150 and 158 are attributed to the opposite commits. Incremental order also differs. | Attribution and incremental-output parity before replacing `Repo.blame` / `blame_incremental`. | | GIX-16 | Bug; missing API | Native archive streaming takes a tree rather than a commit. Its TAR omits Git's global PAX commit comment, leaves `export-subst` placeholders literal, and writes ordinary modes as `0644` where Git uses `0664`. | Commit-aware export substitution, metadata and permission parity. Both engines respected `export-ignore` in the probe. | | GIX-17 | Bug (open) | Revision parsing accepts abbreviated IDs with `-dirty`, prefers the OID suffix over an exact describe-shaped tag, and treats escaped braces in message searches differently. With GixPython 0.3.0 and Git 2.54.0, `2313-supersede` resolves to blob `23136a1d1dd1f02de7cde0f7b55f49d76ac77d55` through Gix while Git resolves the existing branch's commit. The deterministic `rev-parse.sh hex-prefix-collision` fixture creates a branch named `-topic`: Gix returns the blob and also accepts `-missing`, which Git rejects. | Provide Git-compatible revision grammar and regex semantics. `_check_revision_grammar()` retains CLI for hexadecimal prefixes followed by a dash, as well as the existing guarded forms; this adapter workaround does not close the upstream bug. `test_rev_parse.py` reproduces these differences, including `test_rev_parse_hex_prefix_with_arbitrary_suffix`. | | GIX-18 | Bug; missing API | The tree-diff resource cache reads attributes from the index; Git also consults worktree attributes. Native binary classification with textconv configured can also differ even in `to_git` mode. | Independent attribute-source control and `--no-textconv` parity. Statistics currently fall back for any effective `diff` attribute. | | GIX-19 | Unverified | GixPython opens repositories with `extensions.compatObjectFormat`, but translation and object-write compatibility have not been validated. The installed Git reports `compatibility hash algorithm support requires Rust` when asked to translate an ID. This is an unvalidated capability guard, not evidence of missing native object mappings. | Verify compatibility-format reads, writes and OID translation against a Git build that supports them. All operations use CLI in the meantime. | | GIX-20 | Bug; missing API | `Target.Symbolic(name)` validates full names but rejects standalone names accepted by `git check-ref-format --allow-onelevel`. Four of 430 audited calls differed: `refs` twice, `hellothere`, and `valid_one_level_refname`. Further probes include digits, punctuation and Unicode (`1`, `A1`, `HEAD_1`, `A-B`, `A.B`, `Ä`). | Expose Git-compatible partial/one-level name validation, or correct the native restriction where appropriate. This is a binding/behavior shortcoming to revisit upstream; standalone names rejected natively retain CLI validation. | | GIX-21 | Bug; missing API | `Repository.committer()` preserves a process identity of ` Name. ` / ` email@example.invalid ` while `git var GIT_COMMITTER_IDENT` returns `Name.` / `email@example.invalid`. Native snapshot overrides for name, email and date work, but modifying the retained snapshot would leak per-command identity across threads. GixPython 0.1.0 has no general independent repository clone: Python copying fails and internal `RepoHandle::clone()` shares its `Arc` state. | Expose Git-compatible identity cleanup and a cheap independent native clone with isolated config, including per-command removal semantics. Reopening isolates state but repeats setup; `with_object_memory()` changes object-write behavior and is not a general clone. Keep `_checked_signature` validation and CLI fallback for cleanup/overrides. Never clean identity fields in Python or mutate shared snapshots/process environment to handle a command. | | GIX-22 | Missing API | The native Git directory is sufficient for the two fixed metadata leaves used here: `modules` and `COMMIT_EDITMSG`. Git's `path.c` applies no special relocation to either, including in linked worktrees. The adapter appends only these names to the canonical native Git directory. | No new binding is needed for these ordinary paths. Other `--git-path` names and symlinked metadata leaves retain CLI resolution; a general resolver must handle config/environment overrides, common/private storage and canonical targets. `Repository.modules_path()` means `.gitmodules`, not the `modules` storage directory. | | GIX-23 | Missing API | No hook lookup or execution API is bound in GixPython 0.1.0. | Bind native hook lookup with configured/default path, linked-worktree, missing/nonexecutable-hook and diagnostic semantics, plus execution where needed. Until then all managed hook calls use Git, including `--ignore-missing` no-ops, preceded by a Git discovery/ownership check that removes only `Repo`'s automatic environment bindings. Each valid hook request therefore makes two CLI calls. Explicit caller overrides retain Git's behavior. A Python filesystem check is not a Gix implementation. | | GIX-24 | Bug | In both object formats, `new_commit_as()` accepts a blob as the tree or a parent where `git commit-tree` rejects it. Tree-editor `upsert()`/`write()` accepts object IDs whose actual kinds disagree with blob/tree/gitlink modes; `git mktree --missing` rejects all three tested mismatches. `edit_references_as()` accepts a blob target under `refs/heads/`, rejected by `git update-ref`. | Provide checked commit/tree construction and reference edits, at least in Git-strict mode. Match Git's kind and direct-branch target validation before writing and under the required ref locks, including its distinct handling of symbolic aliases. Existing `_commit_tree`, `_write_tree` and `_update_ref` guards query Gix headers and select CLI on invalid inputs; they do not replace Gix writes with Python. | | GIX-25 | Bug; missing API | After `git symbolic-ref refs/heads/dangling refs/heads/missing`, `Repository.references().all()` enumerates the dangling name, while `git for-each-ref --format=%(refname)` omits it. Valid symbolic aliases must remain present. Both SHA-1 and SHA-256 probes reproduce this difference. | Expose Git-compatible enumeration with the same dangling-reference and diagnostic behavior, retaining the raw iterator for callers that need it. `_for_each_ref` forces Gix to resolve symbolic targets and falls back to Git on failure; keep that fallback until a compatible Gix API/mode is verified. | | GIX-26 | Bug; missing API | `dangerously_push_entry(IndexStat(), oid, 0, 0o120000, b".gitmodules")`, followed by `sort_entries()`, `verify_entries()` and `write()`, succeeds and retains the unsafe symlink. Protected Git `update-index -z --index-info` ignores it with exit 0 and `Ignoring path .gitmodules`; protected `read-tree` rejects it. The native fresh `index_from_tree` also rejects it. | A Git-strict checked index batch editor that validates names/modes, preserves metadata and reports rejected records. `verify_entries()` verifies ordering, not pathname safety. All private-index edits use Git with `core.protectHFS=true` and `core.protectNTFS=true`, followed by requested-entry readback; no Python dictionary reconstruction or native dangerous insertion remains. | | GIX-27 | Missing API | `gix.Time` exposes seconds/offset but no human date parser in GixPython 0.1.0. | Bind Git-compatible textual date parsing with timezone semantics. `parse_date()` uses `git var GIT_AUTHOR_IDENT` with fixed identity and per-command date, then adapts the normalized timestamp/offset. | | GIX-28 | Bug; missing API | GixPython 0.1.0 on Gitoxide `f819565c2c4c56619c4888acef6cf3b8144cbccb` stores `object \ntype blob\ntag invalid\n\nmessage\n` through `write_object("tag", data)`. Git 2.54.0 rejects the same payload through `hash-object -t tag -w --stdin` with `missingTaggerEntry`. Byte-preserving native readback does not establish Git's validation. | Expose a Git-strict tag writer or validation API. Managed tag hashing/storage uses Git before any native write; `test_tag_storage_rejects_missing_tagger` verifies failure and absence from storage. Keep the bug open until upstream validation is verified. | | GIX-29 | Missing API | GixPython 0.2.0 exposes raw identity/message/signature bytes without a Git-compatible text reencoder. Git 2.54.0 recodes the complete commit or retains all bytes when conversion fails; per-field Python conversion differs. The Latin-1, unknown/non-text codecs, malformed UTF-16 and signed-header cases in `test_deserialization_declared_encoding` reproduce the required behavior. | Expose whole-commit text conversion, including failed/unknown-encoding behavior. `object_metadata()` retains CLI for non-UTF-8 declarations. The fallback obtains identities/message from Git's fixed UTF-8 text projection and signatures from its recoded raw header block, retaining the original declaration separately. | | GIX-30 | Bug (open) | GixPython 0.2.0 `Commit.decode()` reports `encoding=None` when `encoding ISO-8859-1` follows `x-extra value`; Git 2.54.0 selects `ISO-8859-1` and renders Latin-1 identity/message bytes correctly. Gix retains `(b"encoding", b"ISO-8859-1")` in `extra_headers`, but its encoding field disagrees with Git. The `late-latin1` read-only `commit.sh` fixture in `test_deserialization_declared_encoding` reproduces this and asserts the fallback. | Recognize encoding headers in Git-compatible order. `object_metadata()` conservatively retains CLI when an encoding appears in `extra_headers`; it does not select the header in Python. Keep this bug open until an upstream fix or compatible mode is verified. | | GIX-31 | Bug (open) | GixPython 0.2.0 `Commit.decode()` rejects commit headers containing a CR-only or tab-only line with `object parsing failed, at gix-object/src/commit/decode.rs:20`. Git 2.54.0 accepts both objects with `fsck --strict`, continues through their header block and renders the later Latin-1 declaration and message correctly. A space-only continuation line decodes natively but retains the late encoding declaration in `extra_headers` (GIX-30). `test_deserialization_headers_end_at_empty_line` reproduces all three using the `commit.sh whitespace-headers` fixtures and checks Git's projection. | Accept Git-compatible header lines and end headers only at an empty line. The existing `object_metadata()` native decoder diagnostics fallback retains Git for CR/tab inputs; the existing GIX-30 fallback covers the space input. Neither fallback closes these bugs. | | GIX-32 | Missing capability (open); no demonstrated Git compatibility mismatch | Windows CI on GixPython 0.3.0, Git 2.56.0.windows.2 and CPython 3.12.10 enumerates the non-UTF-8 packed names in `legacy-refs.sh`, but exact byte-name lookup selects `Reference.read: CLI: native read diagnostics`; the no-CLI guard fails for SHA-1 and SHA-256. POSIX lookup succeeds. Git's Windows CLI likewise cannot address those original bytes through UTF-16 arguments, so there is no demonstrated equivalent Windows CLI operand. Git enumeration does preserve both names. | Provide native Windows packed-reference lookup for byte names without first converting the name to a loose-reference filesystem path. Keep `reference_info()`'s existing diagnostic fallback; `test_non_utf8_tag_names_preserve_bytes` checks it on Windows. Resolution coverage and native claims remain POSIX-only. The upstream lookup source attempts loose-path conversion before consulting packed refs; CI demonstrates the fallback, but does not capture the underlying Gix error text. | | GIX-33 | Bug (open); missing exact-name mode | With Git 2.54.0 and GixPython 0.3.0, `exact-refs.sh sha1 files symbolic` makes `refs/tags/REBASE_HEAD` a symbolic ref to another tag while the root `REBASE_HEAD` is absent. `Repository.try_find_reference("REBASE_HEAD")` returns that tag; native `_symbolic_ref` formerly returned its target, whereas `git symbolic-ref --quiet --no-recurse -- REBASE_HEAD` exits 1. GitPython's root-reference object read consequently returned a commit through Gix and raised `ValueError` through CLI. SHA-256 reproduces the same mismatch. | Expose an exact-name lookup mode, including root references, whose absence and error behavior matches Git. `_symbolic_ref()` compares the native reference's name with the requested bytes and selects CLI for a lookalike. `test_missing_root_reference_does_not_follow_symbolic_lookalike_tag` verifies parity and forced CLI fallback. This adapter guard does not close the upstream compatibility bug. | | GIX-34 | Bug (open) | With Git 2.54.0 and GixPython 0.3.0, an author, committer or tagger line with fields after its time zone, such as `A 1112591163 +0200 1700000000 -0500`, decodes through `Commit.author()`/`committer()` and `Tag.tagger()` with the correct seconds but an offset of 0. Git reads only the first date and time zone after the last `>` (`split_ident_line`) and reports `1112591163 +0200` through `git show --format=%ad --date=raw` and `%(taggerdate:raw)`. `git fsck` reports `badTimezone` for such objects, but the default `transfer.fsckObjects=false` accepts them. The `trailing-date` read-only `commit.sh` fixture in `test_deserialization_reads_date_after_email` reproduces both object kinds. | Report the time zone Git reads for identity lines with trailing fields, or reject them consistently. `object_metadata()` selects CLI when the decoded signature does not reproduce the end of its raw header line. This adapter guard does not close the upstream compatibility bug. | GIX-14 also covers configured worktree symlink resolution. On macOS with GixPython 0.3.0, CPython 3.12.14 and Git 2.54.0, set a submodule's `core.worktree` to `parent/redirect`, where `redirect` links to `outside`. `Repository.workdir()` retains `parent/redirect`; Git's `rev-parse --show-toplevel` returns `outside`. A trailing `/worktree` reproduces the same difference for an intermediate symlink. Opening the canonical gitfile or reopening the canonical native Git directory does not resolve it. `test_submodule_writes_use_checked_worktree` exercises both layouts: submodule handles now pass the validated checkout as `GIT_WORK_TREE` to prevent redirected writes in either backend. General native discovery still needs Git-compatible configured-worktree resolution; this guard does not close GIX-14. The PR #2306 regression exposed an adapter ownership-check bypass in both backends: `Repo`'s automatic `GIT_DIR` made Git 2.54.0 treat hook execution as explicitly selecting the repository, so it ran a hook where an ordinary `git commit` rejected dubious ownership. `run_commit_hook()` now asks Git to rediscover the repository from its original location before executing hooks. `test_pre_commit_hook_not_run_in_repository_git_refuses` uses Git's `GIT_TEST_ASSUME_DIFFERENT_OWNER` switch and verifies rejection, `safe.directory` exemptions and explicit command-environment behavior across ordinary, bare, linked, separate-directory and configured-worktree layouts. This fixes the adapter bug without claiming a native Gix trust check; GIX-23 remains open. The October 22 Windows Gix CI run on Git 2.56.0.windows.2, CPython 3.12.10, and GixPython 0.2.0 exposed an adapter failure in `test_ignored_items_reported_verbatim`: Python's path metadata lookup raises `WinError 123` for `q"uote.env`, while Git's `check-ignore --stdin -z` accepts the queried path without creating it. The adapter now retains CLI when path metadata is unavailable, including permission errors. This is an adapter compatibility bug, not a demonstrated mismatch in Gix's excludes matching. GixPython would need to expose Git-compatible ignore queries with native path-kind and traversal handling to remove this metadata-dependent fallback. `test_ignore_metadata_errors_retain_cli` reproduces it on every host; the original verbatim-path regression continues to cover Windows directly. Git selects the first encoding header, recodes the complete commit, and retains all original bytes if conversion fails. Codec aliases vary by platform: Git 2.54.0 on macOS rejects `utf-16-be`, while Alpine's musl 1.2.6 iconv recognizes it. The malformed UTF-16 fixtures use odd byte counts so conversion fails even with the portable `UTF-16BE` spelling. A later `encoding ISO-8859-1` header cannot rescue that failure. Tests assert the odd byte counts and compare text directly with Git. Earlier even-sized fixtures converted successfully but erased every ASCII newline. Their Git text projection is unstable: Git 2.56.0.windows.2 returned `x` as the message in one CI job instead of the empty message observed elsewhere. Git's pretty formatter assumes a header delimiter after scanning the recoded buffer; these malformed inputs provide none. They do not provide a reliable text oracle. Successful conversion remains covered by ASCII-compatible encodings. The native UTF-8/default path remains in use; other declarations use a fixed UTF-8 Git text projection. `Commit.encoding` remains the first declared Python text codec when valid, or `Commit.default_encoding` for unknown, non-text or malformed codec names. That choice for subsequent serialization does not control Git's text conversion: Git's iconv can recognize codecs Python cannot, such as `ISO-2022-CN`. The fixture compares against the installed Git, whose iconv support can vary by platform. Windows reproduction for GIX-14 uses CPython 3.12.13, Git 2.55.0.windows.3 and the released GixPython 0.1.0 source distribution with the same pinned Gitoxide revision above. Both `105114db` and the CLI metadata batching change (`f6779dbb`) reproduce the following existing failures: - `test_native_command_queries_match_cli` and `test_native_worktree_inventory_includes_bare_main` return backslashes in the adapter's native path text where Git returns forward slashes, in both object formats. The locations agree; the command-output formatting does not. - The `commondir=b"\xff"` subtest of `test_repo_discovery_rejects_invalid_metadata` raises `RuntimeError: native worker panicked` instead of the CLI backend's `InvalidGitRepositoryError`. The binding must reject this input through its normal native error contract so discovery can take the existing fallback. The adapters now format locations returned by `Repository.common_dir()`, `workdir()` and `git_dir()` with the existing Windows-only path separator conversion, including `Repo.common_dir` and gitfile worktree paths. Gix still resolves every location. Ordinary, bare and linked repository metadata and worktree-list queries compare exactly against Git without additional CLI calls. POSIX filenames containing literal backslashes are unaffected. Discovery catches only the binding's exact `RuntimeError("native worker panicked")` and selects Git's existing validation path. The malformed metadata regression then raises `InvalidGitRepositoryError`. Unrelated runtime errors still propagate. Both normal native write errors and worker panics after a mutation begins are tested to ensure they never trigger a CLI retry. GIX-14 remains open: these adapter changes do not fix the upstream panic or the other discovery compatibility bugs. The tests now pass gitfile operands in Git's Windows-compatible spelling and compare symlink resolution against the current platform's Git output. Status and ignore parity runs with a portable filename everywhere; only the extra newline-filename variant is skipped on Windows. Blob-reference updates also compare against Git: a direct branch rejects the blob, while a symbolic alias outside `refs/heads/` accepts it on Git 2.55.0.windows.3, in both object formats. That alias behavior is not evidence of a Gix mismatch and does not invalidate GIX-24's separate direct-branch comparison. For GIX-8, at fixture commit `44e0a8ec55c42559dfcdf5117710b26261a7c937`, compare `git rev-list HEAD` with `repo.rev_walk([repo.head_id()]).sorting("newest_first").all()`. The first different IDs were Git's `5a53ae6d68e318a85be78fb5fcee4d3aa9dfbb48` and gix's `ee854dcb62220adeae4feb59bd10185e7ac02957`. Native walk builders return new values: retain the result of `first_parent_only()` and other builder methods. For GIX-15, compare `git blame --line-porcelain HEAD -- README.md` with `repo.blame_file("README.md", repo.head_id(), gix.BlameOptions(diff_algorithm="myers", rewrites=gix.Rewrites()))` at that same fixture commit. The differing commits are `2ddd5e5ef89da7f1e3b3a7d081fbc7f5c46ac11c` and `3aacb3717ad78ec40e5b168a7ce8109aee6f156e`. `VERSION` and `git/objects/commit.py` matched in the comparison. For GIX-18, set `file diff=forced` in `.gitattributes`, `diff.forced.binary=true`, and `diff.forced.textconv=false`. Change a NUL-containing line in `file`: the native `to_git` cache reports one insertion and one removal, while Git with `--no-textconv --numstat` reports `-\t-\tfile` (binary). For GIX-24, use a repository containing an empty tree, a commit and a blob. Pass the blob ID as the tree or a parent to `new_commit_as()` and compare `git commit-tree BLOB` or `git commit-tree TREE -p BLOB`. Insert a commit ID under the `blob` or `tree` kind with `Tree.edit().upsert()`, or a blob ID under `commit`, then `write()`; Git's corresponding `mktree --missing` entry rejects each mismatched kind. Finally, submit a `RefEdit.update_with_log()` for `refs/heads/bad` with `Target.Object(blob_id)` and `PreviousValue.Any`; `git update-ref refs/heads/bad BLOB` rejects that branch target. The GIX-10 transaction probes start with HEAD pointing to `refs/heads/main` at a first commit. Apply `RefEdit.update_with_log()` with `PreviousValue.Any`, `LogChange.force_create_reflog=True` and a fixed message, comparing `git update-ref --create-reflog -m MESSAGE`. Test advancing the branch, writing its current target again, updating through a symbolic alias, and detaching HEAD with `with_deref(False)` / `--no-deref`. Compare both the branch and HEAD logs, including the previous OID, as recorded in the ledger. The latest direct comparisons covered 12 cases in each of SHA-1 and SHA-256: six object/branch type mismatches, identity cleanup, dangling enumeration and four reflog transactions. Each case called Gix first and Git second. The local probe and captured results are in `.cache/gix-compatibility-ledger/{probe-contracts.py,contracts.json}`. Existing [backend regressions](../test/test_gix_backend.py) protect the adapter fallbacks, including `test_tree_writes_validate_child_object_kinds`, `test_branch_blob_updates_match_git`, `test_commit_identity_cleanup_matches_git` and `test_reference_enumeration_preserves_aliases_and_skips_dangling_refs`. This ledger update changes no runtime code or CLI budgets; the earlier runtime validation and timings remain attributed to their measured snapshots. ### Remaining configuration reads (GIX-12) Native `.gitmodules` reads preserve arbitrary bytes in paths, URLs and branch values through `surrogateescape`. Non-UTF-8 subsection names use the CLI fallback: GixPython 0.3.0's `ConfigFile.string()` and `.boolean()` require text keys. Although `ModulesFile.path()` and `.url()` accept byte names, `.branch()` interprets the value: an empty value becomes `HEAD`, and missing and implicit values both become `None`. A byte-key raw configuration accessor is needed to retain GitPython's configuration contract natively. The scripted `submodule-bytes.sh` regressions cover values, names and cached-field rereads; the native value test prohibits CLI execution. The October 7 inventory identified 951 remote-enumeration reads, 674 actor lookups, 262 remote-property reads, 202 tracking-branch lookups and 111 fetch refspec checks. These are historical call-site counts, not additive savings. The dedicated `.gitmodules` parser now covers supported submodule reads; the other consumers still need generic config binding additions. Local probes with official GixPython 0.1.0 and the existing CPython 3.12.14 confirmed these contracts against both GitPython backends: | Consumer | GitPython behavior to preserve | Why the existing native API is insufficient | | --- | --- | --- | | Remote enumeration | Repository-local declaration order: `z-last`, `a-first` | `remote_names()` returned the merged, sorted `a-first`, `global-only`, `z-last`. | | Remote URL/properties | The cached raw `url` property returned the last value, `../second`; `urls` yielded both `../first` and `../second`. | Native `Remote.url(Fetch)` selected only `../first`. Generic multivalue enumeration and raw snapshot semantics are still needed. | | Actor lookup | A supplied file/stream reader returned `Reader Actor`, independent of the repository's `Repository Actor`. | Repository identity lookup cannot honor arbitrary source lists, reader snapshots, or GitPython's identity fallback rules. | | Tracking branch | `branch.main.remote=z-last` and `merge=refs/heads/topic` produced `refs/remotes/z-last/topic`. | Native tracking lookup applied the configured fetch mapping and returned `refs/foreign/topic`. Replacing this API requires the original scalar config values. | | Fetch refspec presence | The cached local reader tests whether a value exists before invoking fetch. | Native remote creation/refspec access applies URL/refspec parsing and merged configuration; it does not expose this raw, source-scoped presence check. | | General config readers | Explicit files/streams, their order, included files, duplicate values and cached reader state | `ConfigFile` exposes only `boolean`, `integer`, `string`, `set_raw_value` and `to_bstring`. `OpenOptions.isolated()` also disables includes: a local included value read as `yes` through GitPython was absent natively. | The needed additions are standalone generic parsing, ordered section/key and multivalue access, source selection, and mutation/removal primitives. The submodule adapter's conservative section guards can be removed once those bindings exist. No generic configuration parser is implemented in Python. The fixed general-config probe remains **2 CLI launches before and after** this investigation; this item does not claim a process reduction. The October 22 rebase regressions compare unquoted escapes and continued values directly with Git 2.54.0, using CPython 3.12.14 and GixPython 0.2.0. Git rejects unknown escapes such as `\q` and the unescaped backslashes in `C:\Users`, including in continued values. It preserves whitespace before a dangling EOF continuation when reading. However, `git config set` appends a new section header into that continuation: writing `other.value=updated` after `k = value \` changes `a.k` to `value [other]` and writes `a.value`. The tests assert Git's actual reads and serialized bytes rather than the former Python parser's repair of that input. General config files and mutations still use the CLI on both backends under GIX-12; these observations do not demonstrate a native Gix mismatch or eliminate any CLI calls. The October 24 ports of PRs #2305 and #2307 also compare home-relative `includeIf` conditions and inline section headers with Git 2.54.0. The conditional-include fixture uses a real repository and platform-native `HOME`/`USERPROFILE` paths, retaining the Windows regression case; the local run covered macOS only. Git accepts options after headers, quoted `]` in subsections, and multiple headers on one line. It rejects unbalanced quoted headers, missing closing brackets, stray brackets, and malformed options or sections. `git config list` omits empty sections, so the facade's enumeration also omits the original PR's empty `e` and `f` sections; their inspection is still unsupported. These regressions pass through the existing GIX-12 CLI fallback on both backends with GixPython 0.2.0. They establish no native compatibility bug or reduction in CLI calls. ## Other remaining operations | Operation family | Why it still uses Git | | --- | --- | | `Repo.init`, clone | The different bundled initialization template set remains a Git compatibility bug; a compatible mode must reproduce Git's effective templates. Git's template/reinitialization options are not exposed. Clone also needs GitPython's environment, local-copy, progress and cleanup contracts. | | Fetch, push, pull, remote pruning | Fetch outcomes do not expose the per-ref old IDs/status/notes needed for `FetchInfo`. Push/pull porcelain is absent. Network mutations cannot be retried after a partial native attempt. | | Checkout, reset, index checkout/merge, move/remove | No bound equivalent of checkout-index/unpack-trees preserving these worktree/index semantics. Native tree/commit merge is a different operation. | | Branch/tag porcelain and symbolic-ref mutation | Strict creation, reflogs, config movement, signing, message cleanup and safety contracts need more than a raw ref edit. | | Object enumeration and alternate-directory listing | No binding for complete ODB object iteration or alternate-store enumeration; `cat-file --batch-all-objects` / `count-objects` remain. | | Name-rev, trailer parsing, cherry | No equivalent bound operation. Native describe is not name-rev. | | Config-backed remote/submodule maintenance | Missing config enumeration/multivar/removal operations (GIX-12), plus filesystem and worktree lifecycle requirements. | | Hook execution | Every explicit hook request, including an absent hook, retains Git-managed lookup, execution and diagnostics until GIX-23 is addressed. Native operations retain GitPython's default hook/fsmonitor/maintenance restrictions. | ### Submodule transport policy Submodule clone and fetch stay on the CLI and pass `GIT_PROTOCOL_FROM_USER=0` for URLs from `.gitmodules`, matching `git submodule update --init` and `git submodule update --remote`. A `protocol.file.allow=user` policy therefore rejects the file transport, while `always` permits it. The existing explicit `allow_unsafe_protocols=True` opt-in preserves the caller's environment policy; `user` is accepted only when `GIT_PROTOCOL_FROM_USER` is unset or permits it. The regression compares both backends with Git 2.54.0, including unchanged checkouts after denied transfers. GixPython 0.2.0 still needs the clone/environment and fetch-result contracts listed above before these calls can move to Gix. This is a shared GitPython policy bug, not a demonstrated Gix compatibility mismatch. ### Downstream mirror-fetch result gap Airflow Git provider 1.0.1's bare-cache refspecs `+refs/heads/*:refs/heads/*` and `+refs/tags/*:refs/tags/*` expose an existing GitPython result-decoding problem when a branch and tag share a name. With Git 2.53.0, both refs update successfully, but verbose output abbreviates their names. The local CLI and Gix selections omit the ambiguous branch result; released GitPython 3.2.1 instead returns a nonexistent `refs/remotes/main` reference whose commit cannot be read. The same problem occurs with `+refs/*:refs/*`. Two preserved real-repository supplemental tests fail with all three selections; Airflow's 175 unchanged upstream tests pass. This is a demonstrated GitPython adapter gap, not a demonstrated Gitoxide mismatch. Git's `fetch --porcelain` reports full destination refnames and old/new IDs, providing native evidence for a future adapter correction. Fetch remains a CLI fallback: GixPython 0.2.0 must expose per-ref identities, status and notes before its fetch outcome can satisfy `FetchInfo`. Do not infer namespaces or reimplement fetch semantics in Python. Reproduction and baseline evidence are retained in the [downstream audit](../test/downstream/audit.md#airflow-ambiguous-mirror-fetch-results). ### Downstream empty configuration sections Kas 5.5's three unchanged SSH key validation tests call `GitConfigParser.add_section('gpg "ssh"')` before assigning `allowedSignersFile` through a section proxy. They pass with released GitPython 3.2.1 and fail with both local backends because empty sections and the inherited ConfigParser section API were removed. Git 2.53.0's config mutation interface and GixPython 0.2.0 do not expose creating an empty section. This is a missing capability and an intentional, demonstrated GitPython API break; it is not a demonstrated Gitoxide mismatch. No successful empty-section result is invented or serialized in Python. GixPython needs a supported native configuration edit that creates sections before this contract can be restored. Kas's separate identity cleanup uses nonempty section assignment, which is supported through Git config remove/set operations under GIX-12. Values are validated before mutation; individual Git operations lock independently, so replacement does not claim transaction-wide atomicity. Downstream can avoid the empty-section API by calling `set_value('gpg "ssh"', 'allowedSignersFile', path)` directly. The three upstream failures remain in the audit. ## Maintaining the adapters `git/_backend.py` contains the capability checks. Managed command adapters return the same bytes/text as the existing parser expects; direct adapters return the existing GitPython object types. Keep operand validation before dispatch. Gix APIs must provide Git behavior. Python glue may validate inputs, format native results and select fallback; it must not implement missing Git semantics or fabricate successful answers to lower CLI counts. Document missing or unclear capabilities here and retain the CLI path until Gix provides them. Name the actual Gix calls behind each conversion. Record every observed Git/Gix mismatch as a compatibility bug with expected/actual behavior, version and a reproduction or regression. A Gix-based workaround does not resolve that bug; require an upstream fix or a verified mode as strict as Git before closing it. Decide whether to fall back before mutating anything. Read/preparation failures can use Git to preserve its public diagnostics. Once `_write()` begins, `gix.Error` failures become `GitCommandError`; other exceptions propagate. Neither may trigger a second CLI mutation. Access native repository state through `Repo._get_gix_repository()` for bound commands. Each access opens a fresh handle through Gix. Close native iterators when partially consumed. Add each conversion with a no-subprocess check and a Git parity check where practical, in its own commit. Update this ledger when a limitation changes; the runtime report's reason should point to the corresponding entry. ## CI coverage The [Python package workflow](../.github/workflows/pythonpackage.yml) adds one Python 3.12 Gix job each on Ubuntu, macOS and Windows, preserving all 28 CLI combinations and their existing check names. It installs `.[test,gix]`, verifies the selected backend and runs the full suite with the same coverage and pytest options. Every job retains JUnit results and `--backend-report` operation counts under its `tests-OS-PYTHON-BACKEND` artifact, and prints the 30 slowest test durations. The extra Gix jobs do not duplicate the documentation build. Their check names include `gix`, such as `test (ubuntu, 3.12, gix)`. On platforms without a matching wheel, pip builds the official source release. `CARGO_TARGET_DIR` keeps compilation output outside pip's temporary source directory. The Cargo cache distinguishes OS, architecture, Python and Rust versions and `gix-requirements.txt`; a release change can reuse dependencies from the same platform and toolchain. ## Windows validation Local Windows checks use CPython 3.12.13, Git 2.55.0.windows.3 and GixPython 0.1.0 built from the released source distribution. The installed extension matches the cached Windows ABI3 wheel byte-for-byte; that wheel's SHA-256 is `1a0addd5f569e2ff7fb2b45c38be1093f6cabbead176c779533b983b86307dc2`. The full baseline at `f6779dbb` reproduced all 17 failures described above: 16 ordinary failures and the subtest with malformed `commondir`. The full patched suite passed with **1,760 tests and 40 subtests**, 58 skipped, nine expected failures and three unexpected passes in 2,242.66 seconds. The three unexpected passes also appeared in the baseline. Ruff, mypy, basedpyright and the pinned pre-commit checks passed. The baseline recorded 21,598 CLI launches in 2,279.10 seconds; the patched run recorded 21,826 CLI launches and 86,936 native operations. Totals include the additional regressions and tests that previously stopped at failing assertions. No CLI ceilings were lowered. Both runs used isolated snapshots and CI's coverage, Git configuration and pytest options. They overlapped, so the elapsed times are not a controlled speed comparison. Logs, JUnit results and operation reports are retained locally in `.cache/gix-windows/`. The CLI repository/discovery regressions passed with 138 tests, 14 subtests, nine skips and one expected failure. The changed native path, discovery, reference and mutation-error cases also passed under Ubuntu WSL: 43 tests and 14 subtests, using CPython 3.14.4, Git 2.53.0 and a separate released-source GixPython build. This includes the POSIX newline-filename cases. ## GixPython 0.1.0 artifact validation The official Apple Silicon ABI3 wheel for GixPython 0.1.0 has SHA-256 `6b68758cb54a90c8eb893ac7815e74ccd9b48b197aaecec951366a6326168f52`. It was downloaded from PyPI and verified against the published digest. At the backend-selection commit, its SHA-1/SHA-256 smoke checks and fresh extra-installation check passed (3 tests) on the existing CPython 3.12.14/macOS environment. The dependency commit and all 31 descendants passed the fast QA profile: Ruff lint/format, pre-commit, mypy, basedpyright, universal dependency resolution, and fatal lint checks for the bundled dependencies. The full published-wheel suite subsequently passed with coverage: 1,649 tests and 38 subtests passed, 79 skipped, and one expected failure, in 757.85 seconds wall-clock on the same interpreter/platform. The performance notes above record the tested revision and future optimization candidates. ## Historical local artifact validation The original integration was validated with GixPython 0.1.0 from local checkout `c8a9fabc03c326ad7b0afdf03d0bda4e62fdff52`, with Gitoxide revision `f819565c2c4c56619c4888acef6cf3b8144cbccb` and its existing vendored fixes. The ordinary Apple Silicon ABI3 wheel in `pygix/dist` has SHA-256 `62a03fe63e0e043c89f8271c10540cb00d31a15cf81fc1f45d3e2feb82a44e9f`. The installed extension was compared byte-for-byte with that wheel. Validation uses CPython 3.12.14 on macOS. The focused backend tests cover both SHA-1 and SHA-256. Platform-specific skipped tests are not claims of validation on other operating systems or free-threaded Python. The full suite and operation report can be reproduced with the commands above. | Check | Result | | --- | --- | | Full CLI installation | 1,360 passed, 80 skipped, 1 expected failure; 38 subtests passed. | | Full GixPython installation | 1,396 passed, 79 skipped, 1 expected failure; 38 subtests passed. | | Expanded native parity/fallback checks | 36 passed, including object-kind validation, identity cleanup, symbolic aliases/dangling refs, and compatibility-format guards. | | Ruff lint/format, mypy, basedpyright | Passed. | | Offline package build | Built an sdist and rebuilt its wheel; the archive includes `gix-requirements.txt` and the wheel declares the `gix` extra. | The wheel directory also supported a fresh offline `.[test,gix]` installation. The full installation test checks that its new environment selects the same backend as the environment running the tests. Tox itself was not installed on this machine; the underlying interpreter-based command was used directly.