Skip to content

Commit 781c620

Browse files
codexByron
andcommitted
feat!: delegate repository storage to Git and Gix
GitPython's Python implementations of repository discovery, configuration, references, object storage and indexes assume storage details that do not hold for SHA-256 objects or reftable references. Delegate these operations to Git 2.52+ and offer GixPython 0.2.0 for supported operations, using the Git CLI as the behavioral baseline. Keep library-managed commands behind argument and stdin validation, suppress implicit hooks, external diff programs, maintenance and lazy fetches where appropriate, and preserve explicit unsafe-operation opt-ins. Support SHA-1 and SHA-256 with files or reftable reference storage. Close owned repository handles before filesystem cleanup and account for Windows and Cygwin path restrictions. The optional `gix` extra selects Gitoxide through supported GixPython APIs. `GIT_PYTHON_BACKEND=cli|gix` makes selection explicit even when both backends are installed. Retain native repository handles and adapt native object, reference, index, graph, configuration, diff and status results where they match Git. Keep CLI fallbacks for missing capabilities and incompatible behavior; record reproductions, affected adapters and upstream requirements in the compatibility ledger in `doc/gix-backend.md`. Native execution never means reimplementing Git semantics in Python. Read declared commit encodings according to Git's first-header and whole-commit conversion behavior, including conversion failure and signature headers. Keep UTF-8 metadata native; use Git's text projection for other encodings and the open Gix header-order compatibility bug. Validate these cases with read-only Bash fixtures and direct Git comparisons. Configuration failure regressions exercise Git's per-mutation locks rather than the removed constructor lock. Breaking API changes follow the storage migration. Remove the Python `GitDB` backend and binary index/tree and raw reflog mutation interfaces. Replace `IndexFile.entries` with immutable `iter_entries()`/`entry()` queries and explicit edits; publish deferred index bytes only after Git validates them. Configuration uses Git's syntax and locking, reflogs expose Git's commit-reflog view, and repository object IDs have format-dependent widths. Remove deprecated exports and implicit environment-variable path expansion. Document the affected APIs, exceptions, hook behavior and replacements in `doc/source/changes.rst`. Move generated repository and filesystem initial states into Bash scripts run through `gix.testtools`. Read-only tests share prepared fixtures; mutation tests copy them by default, with explicit execution only for fixtures that cannot be relocated. Keep generated caches and archives out of version control. Tests require GixPython with either backend: CI runs fixture suites on Python 3.11+ and retains installation/import smoke checks on Python 3.8-3.10. Update the normal, minimum-Git, Alpine and Cygwin environments and the source packaging. Add downstream release checks for Bandit, MLflow, LangChain, SWE-bench and DataHub with both backends. Add a read-only repository benchmark that compares result digests, records native/fallback decisions and actual Git launches, and enforces CLI-process ceilings. Preserve historical measurements and their original revision references as evidence, including superseded results. Make scripted tests portable to Git for Windows. Define literal submodule names inside Bash from simple scenario identifiers, because MSYS expands metacharacters in native arguments and disabling that expansion breaks GixPython's quoted script launcher. Retain Windows directory-symlink types for dangling targets and build trailing-space filenames with Git's `mktree` instead of the Windows-restricted index. Compare escaped log paths and native fixture locations correctly. Keep exact Git-recorded submodule URLs while separately checking path identity, including a portable `/./` regression. Use Git's actual text projection for platform-dependent `iconv` declarations. Alpine's musl recognizes `utf-16-be` while macOS rejects that spelling. Keep Git as the text baseline; independently assert the first codec, raw dates/timezones, and native/fallback decisions. Keep malformed UTF-16 fixtures odd-sized, including the portable `UTF-16BE` spelling, so conversion fails without erasing Git's header delimiters. Assert the raw object size to prevent fixture edits from reintroducing unstable pretty output. Record these platform limitations in `doc/gix-backend.md`. Before the Windows fixture repairs, macOS validation used CPython 3.12.14, Git 2.54.0 and GixPython 0.2.0. The full CLI suite passed 1,745 tests with 151 skips, and the full Gix suite passed 1,845 tests with 51 skips. Both passed 58 subtests and retained one expected failure. CLI ran first. All 14 benchmark result digests and the existing CLI-process ceilings matched; the two-sample smoke run establishes no timing conclusion. Ruff lint/format, mypy, basedpyright, ShellCheck and Sphinx with warnings fatal passed. Hosted CI, other platforms and older interpreters were not run locally. The first Windows fixture repair passed 471 tests and 20 subtests on each backend locally but exposed a native Windows launcher failure in CI. The follow-up removes the `MSYS=noglob` override and preserves all literal-name assertions through scripted initial states. It also fixes Alpine's encoding expectation rather than changing production Git behavior. The focused commit, cleanup, fixture-health and literal-name tests passed 70 tests and 21 subtests per backend locally, CLI first, with one Windows-only skip. Repository pre-commit hooks passed. On `13202ce`, 63 of 64 hosted checks passed, including Alpine and Windows Gix. Windows CLI on Python 3.12 exposed one remaining fixture expectation: direct Git returned `x` instead of an empty message after successful UTF-16 recoding removed every newline. The failure preceded the backend comparison. Force conversion failure for those malformed fixtures and remove the unsupported empty-output assertion while retaining exact Git comparisons. No production behavior changes for this repair. The final commit module passed 40 tests and 21 subtests per backend locally, CLI first, and all hooks for the changed files passed. Hosted verification of that final fixture correction is left to the next CI run. Co-authored-by: GPT-6 Astra <codex@openai.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Byron <sebastian.thiel@icloud.com>
1 parent 563cb1f commit 781c620

162 files changed

Lines changed: 13677 additions & 13024 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.basedpyright/baseline.json‎

Lines changed: 0 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -8,75 +8,9 @@
88
"endColumn": 45,
99
"lineCount": 1
1010
}
11-
},
12-
{
13-
"code": "reportArgumentType",
14-
"range": {
15-
"startColumn": 41,
16-
"endColumn": 48,
17-
"lineCount": 1
18-
}
19-
},
20-
{
21-
"code": "reportArgumentType",
22-
"range": {
23-
"startColumn": 32,
24-
"endColumn": 39,
25-
"lineCount": 1
26-
}
27-
},
28-
{
29-
"code": "reportCallIssue",
30-
"range": {
31-
"startColumn": 25,
32-
"endColumn": 46,
33-
"lineCount": 1
34-
}
35-
},
36-
{
37-
"code": "reportArgumentType",
38-
"range": {
39-
"startColumn": 30,
40-
"endColumn": 39,
41-
"lineCount": 1
42-
}
43-
}
44-
],
45-
"./git/db.py": [
46-
{
47-
"code": "reportIncompatibleMethodOverride",
48-
"range": {
49-
"startColumn": 8,
50-
"endColumn": 12,
51-
"lineCount": 1
52-
}
53-
},
54-
{
55-
"code": "reportIncompatibleMethodOverride",
56-
"range": {
57-
"startColumn": 8,
58-
"endColumn": 14,
59-
"lineCount": 1
60-
}
6111
}
6212
],
6313
"./git/index/base.py": [
64-
{
65-
"code": "reportArgumentType",
66-
"range": {
67-
"startColumn": 30,
68-
"endColumn": 36,
69-
"lineCount": 1
70-
}
71-
},
72-
{
73-
"code": "reportArgumentType",
74-
"range": {
75-
"startColumn": 28,
76-
"endColumn": 34,
77-
"lineCount": 1
78-
}
79-
},
8014
{
8115
"code": "reportArgumentType",
8216
"range": {
@@ -186,14 +120,6 @@
186120
"endColumn": 25,
187121
"lineCount": 1
188122
}
189-
},
190-
{
191-
"code": "reportArgumentType",
192-
"range": {
193-
"startColumn": 52,
194-
"endColumn": 84,
195-
"lineCount": 1
196-
}
197123
}
198124
],
199125
"./git/objects/tag.py": [
@@ -274,32 +200,6 @@
274200
}
275201
}
276202
],
277-
"./git/refs/log.py": [
278-
{
279-
"code": "reportArgumentType",
280-
"range": {
281-
"startColumn": 30,
282-
"endColumn": 34,
283-
"lineCount": 1
284-
}
285-
},
286-
{
287-
"code": "reportAttributeAccessIssue",
288-
"range": {
289-
"startColumn": 17,
290-
"endColumn": 22,
291-
"lineCount": 1
292-
}
293-
},
294-
{
295-
"code": "reportArgumentType",
296-
"range": {
297-
"startColumn": 28,
298-
"endColumn": 30,
299-
"lineCount": 1
300-
}
301-
}
302-
],
303203
"./git/refs/reference.py": [
304204
{
305205
"code": "reportIncompatibleVariableOverride",
@@ -310,16 +210,6 @@
310210
}
311211
}
312212
],
313-
"./git/refs/symbolic.py": [
314-
{
315-
"code": "reportAttributeAccessIssue",
316-
"range": {
317-
"startColumn": 15,
318-
"endColumn": 20,
319-
"lineCount": 1
320-
}
321-
}
322-
],
323213
"./git/refs/tag.py": [
324214
{
325215
"code": "reportIncompatibleMethodOverride",
@@ -338,49 +228,7 @@
338228
}
339229
}
340230
],
341-
"./git/remote.py": [
342-
{
343-
"code": "reportAttributeAccessIssue",
344-
"range": {
345-
"startColumn": 26,
346-
"endColumn": 38,
347-
"lineCount": 1
348-
}
349-
},
350-
{
351-
"code": "reportAttributeAccessIssue",
352-
"range": {
353-
"startColumn": 26,
354-
"endColumn": 38,
355-
"lineCount": 1
356-
}
357-
},
358-
{
359-
"code": "reportAttributeAccessIssue",
360-
"range": {
361-
"startColumn": 26,
362-
"endColumn": 38,
363-
"lineCount": 1
364-
}
365-
},
366-
{
367-
"code": "reportAttributeAccessIssue",
368-
"range": {
369-
"startColumn": 26,
370-
"endColumn": 38,
371-
"lineCount": 1
372-
}
373-
}
374-
],
375231
"./git/repo/base.py": [
376-
{
377-
"code": "reportReturnType",
378-
"range": {
379-
"startColumn": 15,
380-
"endColumn": 28,
381-
"lineCount": 1
382-
}
383-
},
384232
{
385233
"code": "reportTypedDictNotRequiredAccess",
386234
"range": {
@@ -445,26 +293,6 @@
445293
"lineCount": 1
446294
}
447295
}
448-
],
449-
"./git/repo/fun.py": [
450-
{
451-
"code": "reportReturnType",
452-
"range": {
453-
"startColumn": 11,
454-
"endColumn": 20,
455-
"lineCount": 1
456-
}
457-
}
458-
],
459-
"./test/deprecation/test_basic.py": [
460-
{
461-
"code": "reportUnusedExpression",
462-
"range": {
463-
"startColumn": 12,
464-
"endColumn": 62,
465-
"lineCount": 1
466-
}
467-
}
468296
]
469297
}
470298
}

‎.github/workflows/alpine-test.yml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ permissions:
1212
jobs:
1313
test:
1414
runs-on: ubuntu-latest
15+
env:
16+
GIT_PYTHON_BACKEND: cli
1517

1618
container:
1719
image: alpine:latest
@@ -23,8 +25,8 @@ jobs:
2325
steps:
2426
- name: Prepare Alpine Linux
2527
run: |
26-
apk add sudo git git-daemon python3 py3-pip py3-virtualenv
27-
echo 'Defaults env_keep += "CI GITHUB_* RUNNER_*"' >/etc/sudoers.d/ci_env
28+
apk add sudo git git-daemon bash rust cargo build-base python3 python3-dev py3-pip py3-virtualenv
29+
echo 'Defaults env_keep += "CI GITHUB_* RUNNER_* GIT_PYTHON_BACKEND"' >/etc/sudoers.d/ci_env
2830
addgroup -g 127 docker
2931
adduser -D -u 1001 runner # TODO: Check if this still works on GHA as intended.
3032
adduser runner docker
@@ -64,6 +66,7 @@ jobs:
6466
run: |
6567
. .venv/bin/activate
6668
pip install ./smmap ./gitdb '.[test]'
69+
python -c 'from gix import testtools'
6770
6871
- name: Show POSIX file ownership
6972
run: |
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Backend benchmark
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
compare-backends:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 30
16+
steps:
17+
- uses: actions/checkout@v7
18+
with:
19+
fetch-depth: 0
20+
persist-credentials: false
21+
- uses: actions/setup-python@v7
22+
with:
23+
python-version: "3.12"
24+
- name: Ensure Git 2.52 or newer
25+
run: |
26+
if ! dpkg --compare-versions "$(git version | awk '{print $3}')" ge 2.52; then
27+
sudo add-apt-repository --yes ppa:git-core/ppa
28+
sudo apt-get update
29+
sudo apt-get install --yes git
30+
fi
31+
- name: Prepare Rust for GixPython
32+
run: |
33+
rustup update stable
34+
rustup default stable
35+
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/gix-target" >> "$GITHUB_ENV"
36+
- name: Cache GixPython builds
37+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
38+
with:
39+
path: |
40+
~/.cargo/registry
41+
~/.cargo/git
42+
${{ runner.temp }}/gix-target
43+
~/.cache/pip
44+
key: benchmark-gix-${{ runner.os }}-${{ hashFiles('gix-requirements.txt') }}
45+
- name: Install separate backends with the same interpreter
46+
run: |
47+
python -m venv .bench-cli
48+
python -m venv .bench-gix
49+
.bench-cli/bin/python -m pip install . -r test/performance/benchmark-requirements.txt
50+
.bench-gix/bin/python -m pip install '.[gix]' -r test/performance/benchmark-requirements.txt
51+
- name: Prepare the shared read-only benchmark fixture
52+
run: |
53+
.bench-cli/bin/python -c 'from pathlib import Path; from gix import testtools; print("BENCH_REPO=" + str((testtools.scripted_fixture_read_only_with_args("benchmark.sh", [str(Path.cwd())]) / "repo").resolve()))' >> "$GITHUB_ENV"
54+
mkdir benchmark-results
55+
- name: Measure GixPython operations, opening and discovery first
56+
env:
57+
GIT_PYTHON_BACKEND: gix
58+
run: |
59+
.bench-gix/bin/python -m test.performance.bench_repository \
60+
--repo "$BENCH_REPO" --expect-backend gix -o benchmark-results/gix.json
61+
- name: Measure CLI operations, opening and discovery
62+
env:
63+
GIT_PYTHON_BACKEND: cli
64+
run: |
65+
.bench-cli/bin/python -m test.performance.bench_repository \
66+
--repo "$BENCH_REPO" --expect-backend cli -o benchmark-results/cli.json
67+
- name: Check parity and publish comparison
68+
run: |
69+
.bench-gix/bin/python -m test.performance.compare_backends \
70+
benchmark-results/cli.json benchmark-results/gix.json \
71+
--cli-budget test/performance/cli-budget.json > benchmark-results/comparison.md
72+
cat benchmark-results/comparison.md >> "$GITHUB_STEP_SUMMARY"
73+
.bench-gix/bin/python -m pyperf compare_to \
74+
benchmark-results/cli.json benchmark-results/gix.json --table --table-format md \
75+
>> "$GITHUB_STEP_SUMMARY"
76+
- name: Retain raw timings and comparison
77+
if: always()
78+
uses: actions/upload-artifact@v7
79+
with:
80+
name: backend-benchmark
81+
path: benchmark-results/

0 commit comments

Comments
 (0)