Skip to content

Commit 419181d

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. Disable MSYS argument globbing only during fixture preparation, 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 empty encoding labels, whose `iconv` behavior differs across platforms. 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. After the Windows fixture repairs, the affected commit, cleanup, fixture, standalone submodule and no-fetch modules passed 471 tests and 20 subtests on each backend locally, CLI first. The three platform-specific skips remain. A final four-case CLI rerun covers literal submodule names after the shared MSYS change. All repository pre-commit hooks passed. These repairs address the 19 failures in Windows job `116278342906` of run `38691545608`; native Windows execution remains pending. No production Git behavior changed. 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 419181d

162 files changed

Lines changed: 13639 additions & 13023 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)