Skip to content

Commit 9ef9ca6

Browse files
codexByron
authored andcommitted
refactor!: delegate index state and metadata decoding to Git and Gix
Python previously retained mutable index dictionaries and reconstructed index files from those entries. That duplicated Git semantics and allowed validation and publication to diverge. Keep deferred and virtual indexes as opaque bytes; materialize a private file before Git edits it, verify requested entries survived, and publish through the existing atomic lock. Replace `IndexFile.entries` with `iter_entries()` and `entry()` and migrate submodule, tutorial and test callers. Preserve deferred add/remove and conflict resolution, alternate publication, index versions and flags, split snapshots, checkout/diff visibility and hook edits. Failed ordinary edits retain pending and published state. Reject NULs before encoding stdin records. Keep source containment checks because Python reads those paths before Git validates the recorded destination. Use GixPython's structured commit/tag decoders, including signature headers and encoding, instead of parsing native object bytes. Retain raw CLI commit/tag metadata decoding where formatted Git output cannot preserve those fields. Delegate textual date syntax and trailer discovery to Git. Record Gix's unsafe index insertion, missing date parser and missing strict tag validation as `GIX-26` through `GIX-28`; private index edits and tag storage use CLI fallbacks. A native byte round trip alone does not prove Git-compatible validation. Remove the deprecated `GitDB` selection, API aliases, global shell switch, automatic environment expansion and Windows error-hiding switches. Keep explicit shell selection and tilde expansion. Document replacements and changed timezone and trailer behavior in `doc/source/changes.rst`. Delete obsolete parser/deprecation tests, repeated rejection matrices and historical timing-only tests. Retain happy paths, Python glue/security failures, atomicity and demonstrated compatibility regressions. The maintained benchmark continues to check parity and CLI process budgets. Replace Python-built repository and filesystem initial states across the maintained tests with Bash scripts executed by `gix.testtools`. Git CLI creates the states and defines expected behavior; both backends exercise the same fixtures. Read-only calls share cached script output. Mutators receive native writable copies, with explicit execution only for symlinks, permissions, virtual environments or metadata that embeds its own absolute location. Keep immutable parser byte samples and operations under test in Python. Move repository seeding for discovery, refs, objects, index, configuration, diffs, submodules, remotes, tutorials, installation and filesystem tests into scripts. Preserve the original assertions, including literal filenames and Git's large-integer configuration behavior from the updated `main` base. Serialization, raw object writes and optional index-refreshing commands now use private fixtures instead of modifying the shared read-only repository. Use the supported versioned fixture API to avoid GixPython 0.2.0 memoization collisions when arguments contain absolute paths. Sanitize fixture errors that otherwise expose inherited environment variables. Preserve best-effort cleanup and failed-test artifacts around the native temporary owner, including a backup when a locked directory cannot be renamed. Record these upstream limitations in `doc/gix-backend.md`. Generated fixture data and archives stay out of the source distribution; no archives are required. Document the Python 3.11+ fixture-suite requirement and keep Cygwin Python 3.9 as an import smoke job. Validation with CPython 3.12 on macOS, Git 2.54.0 and GixPython 0.2.0: - Full CLI suite: 1,691 passed, 151 skipped, one existing expected failure, and 44 subtests passed. Full Gix suite: 1,793 passed, 51 skipped, one existing expected failure, and 44 subtests passed. - Final cleanup/cache and shared-write corrections: 29 passed and one platform skip on each backend, after the full runs. - SWE-bench's four local storage-format checks passed on each backend. - All 14 benchmark result digests matched and existing Gix CLI-process ceilings passed; single-value runs establish no timing conclusion. - `pre-commit`, Ruff, `mypy`, `basedpyright`, ShellCheck, actionlint, warnings-as-errors Sphinx and `git diff --check` passed. Hosted CI, Windows, Alpine, Cygwin and older interpreters were not executed locally. Removed APIs and corrected date/trailer behavior require migration.
1 parent 0792f41 commit 9ef9ca6

115 files changed

Lines changed: 5127 additions & 6983 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 & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -228,24 +228,6 @@
228228
}
229229
}
230230
],
231-
"./git/remote.py": [
232-
{
233-
"code": "reportAttributeAccessIssue",
234-
"range": {
235-
"startColumn": 26,
236-
"endColumn": 38,
237-
"lineCount": 1
238-
}
239-
},
240-
{
241-
"code": "reportAttributeAccessIssue",
242-
"range": {
243-
"startColumn": 26,
244-
"endColumn": 38,
245-
"lineCount": 1
246-
}
247-
}
248-
],
249231
"./git/repo/base.py": [
250232
{
251233
"code": "reportTypedDictNotRequiredAccess",
@@ -311,16 +293,6 @@
311293
"lineCount": 1
312294
}
313295
}
314-
],
315-
"./test/deprecation/test_basic.py": [
316-
{
317-
"code": "reportUnusedExpression",
318-
"range": {
319-
"startColumn": 12,
320-
"endColumn": 62,
321-
"lineCount": 1
322-
}
323-
}
324296
]
325297
}
326298
}

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

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: test-cygwin
1+
name: import-cygwin
22

33
on:
44
push:
@@ -11,22 +11,12 @@ permissions:
1111

1212
jobs:
1313
test:
14-
strategy:
15-
matrix:
16-
selection: [fast, perf]
17-
include:
18-
- selection: fast
19-
additional-pytest-args: --ignore=test/performance
20-
- selection: perf
21-
additional-pytest-args: test/performance
22-
23-
fail-fast: false
24-
2514
runs-on: windows-latest
2615

2716
env:
2817
CHERE_INVOKING: "1"
2918
CYGWIN_NOWINPATH: "1"
19+
GIT_PYTHON_BACKEND: cli
3020

3121
defaults:
3222
run:
@@ -94,9 +84,9 @@ jobs:
9484
run: |
9585
python -m pip install -U 'setuptools; python_version<"3.12"' wheel
9686
97-
- name: Install project and test dependencies
87+
- name: Install project
9888
run: |
99-
pip install ./smmap ./gitdb '.[test]'
89+
pip install ./smmap ./gitdb .
10090
10191
- name: Verify Cygwin Git detection
10292
run: python -c 'from git import Git; assert Git.is_cygwin()'
@@ -149,6 +139,5 @@ jobs:
149139
python --version
150140
python -c 'import os, sys; print(f"{sys.platform=}, {os.name=}")'
151141
152-
- name: Test with pytest (${{ matrix.additional-pytest-args }})
153-
run: |
154-
pytest --color=yes -p no:sugar --instafail -vv ${{ matrix.additional-pytest-args }}
142+
- name: Smoke check the installed package on Python 3.9
143+
run: python -I -c "import git; from git import _backend; assert _backend.name == 'cli'; print(git.__version__)"

‎CONTRIBUTING.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,19 @@ origin.
4040

4141
## Temporary test directories
4242

43-
Use `test.cleanup.TemporaryDirectory` for isolated temporary directories and
44-
`test.cleanup.cleanup_directory` when disposing of directories created by test
45-
fixtures. Close repository handles before removing their files. Cleanup is
43+
Tests require Python 3.11+, Git 2.52+, Bash, and the `test` extra, which installs
44+
GixPython even when testing the CLI backend. Select `GIT_PYTHON_BACKEND=cli` or
45+
`GIT_PYTHON_BACKEND=gix` before running pytest.
46+
47+
Put initial repository and filesystem states in `tests/fixtures/*.sh` and use
48+
`test.lib.fixtures` to run them through `gix.testtools`. Read-only tests share
49+
cached output; mutating tests use writable copies. Use `execute=True` only when
50+
the fixture cannot be copied, and explain the reason at its call site. Keep
51+
operations under test in Python. Immutable parser byte samples remain in
52+
`test/fixtures`; generated caches and archives are ignored.
53+
54+
Use `test.cleanup.cleanup_directory` for cleanup outside the shared fixture
55+
owners. Close repository handles before removing their files. Cleanup is
4656
best-effort: it logs filesystem errors, removes whatever it can, and leaves
4757
locked files behind without failing or skipping a test. The shared writable
4858
repository decorators still keep failed tests' directories for debugging.

‎MANIFEST.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ include git/py.typed
1111

1212
recursive-include doc *
1313
recursive-exclude test *
14+
prune tests
1415

1516
global-exclude __pycache__ *.pyc

‎doc/gix-backend.md‎

Lines changed: 115 additions & 13 deletions
Large diffs are not rendered by default.

‎doc/source/changes.rst‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ API changes
5151
* ``GitCmdObjectDB`` no longer inherits ``LooseObjectDB``. Its object reads,
5252
writes, existence checks, and enumeration use Git, including packed objects.
5353
Precompressed input streams and custom object-output writers are unsupported.
54-
The deprecated ``GitDB`` remains explicitly selectable with its existing
55-
warning and limitations; ``gitdb`` remains a dependency for shared types.
54+
``GitDB`` exports and backend selection are removed. ``odbt`` accepts only
55+
``GitCmdObjectDB`` and its subclasses, rejecting unsupported types before
56+
initialization or cloning. Remove ``odbt=GitDB`` or select ``GitCmdObjectDB``.
57+
``gitdb`` remains a dependency for shared types and utilities.
5658
* ``Repo.object_format`` and ``Repo.ref_format`` report Git's storage formats.
5759
``Repo.alternates`` is read-only and reports effective absolute alternate
5860
directories, including environment and transitive alternates. Direct editing
@@ -99,6 +101,52 @@ API changes
99101
Fetch/pull results are derived from command output rather than ``FETCH_HEAD``
100102
file parsing. Revision strings follow Git's native revision grammar.
101103

104+
* ``IndexFile.entries`` is removed. Use ``iter_entries()`` for immutable query
105+
records, ``entry(path, stage=0)`` for exact lookup (missing entries raise
106+
``KeyError``), ``add()``, and ``remove()`` for edits. Python retains only opaque
107+
index-file bytes for deferred/virtual indexes. Git edits private files and
108+
validates readback before atomic publication. ``add(write=False)`` and
109+
``resolve_blobs()`` retain deferred edits; ``remove(..., write=False)`` now does
110+
too, unless ``working_tree=True``. ``write(file_path)`` publishes to an alternate
111+
path. Checkout and diff use pending bytes. Git owns versions, flags, sparse and
112+
split indexes; deferred split snapshots are made standalone through Git.
113+
Ordinary failed index edits preserve pending and published state. Worktree,
114+
object storage and hook side effects are not rolled back. Commit hooks see the
115+
materialized index; their index edits survive failures without advancing HEAD.
116+
Binary-parser tests and duplicate filename-rejection matrices are replaced by
117+
happy paths, Python glue failures and representative compatibility regressions.
118+
Historical timing-only tests are removed; the maintained backend benchmark
119+
checks result parity and CLI budgets.
120+
* Commit/tag metadata uses structured Gix decoders when the Gix backend supports
121+
the repository. The CLI backend retains raw decoding because Git has no faithful
122+
formatted query for all commit headers or arbitrary tag objects. ``gpgsig``
123+
remains readable. The small ``Actor.from_string()`` and
124+
``parse_actor_and_date()`` identity helpers remain supported.
125+
* ``parse_date()`` delegates text syntax and timezone interpretation to Git;
126+
aware datetimes retain direct conversion and invalid inputs raise ``ValueError``.
127+
ISO/RFC dates now apply their timezone to the UTC timestamp, correcting the
128+
previous behavior. Dates without a zone use Git's local timezone, and accepted
129+
date syntax follows the installed Git version. Use an explicit timezone for
130+
reproducible results. ``co_authors`` now uses Git's final trailer block rather
131+
than scanning arbitrary message lines; use a valid trailer block after a blank
132+
line.
133+
* Removed deprecated APIs: ``Git.USE_SHELL`` (explicit ``Git.execute(shell=...)``
134+
remains), ``Diff.renamed`` (use ``renamed_file``), ``Commit.trailers`` (use
135+
``trailers_list`` or ``trailers_dict``), ``Actor.name_email_regex`` (use
136+
``Actor.from_string``), ``git.util.Iterable`` (use ``IterableObj``),
137+
``git.compat.is_win/is_posix/is_darwin`` (use ``os.name``/``sys.platform``), and
138+
``git.types.Lit_commit_ish`` (use ``Literal["commit", "tag"]`` or
139+
``GitObjectTypeString``). Top-level typing exports and private module aliases
140+
are removed; import from ``typing`` or the owning module. ``git.util`` now
141+
exposes the actual utility module. Abstract ``Traversable.traverse`` and
142+
``list_traverse`` raise ``NotImplementedError``; use concrete implementations.
143+
* Paths and URLs no longer expand ``$VAR``/``%VAR%`` automatically. Removed
144+
``expand_vars`` switches from ``Repo``, ``Repo.init``, ``expand_path``,
145+
``cygpath`` and ``Git.polish_url``. Expand explicitly with
146+
``os.path.expandvars`` if wanted; initial ``~`` expansion remains.
147+
``HIDE_WINDOWS_KNOWN_ERRORS`` and ``HIDE_WINDOWS_FREEZE_ERRORS`` are removed.
148+
``rmtree`` propagates filesystem errors rather than raising ``SkipTest``.
149+
102150
3.2.1
103151
=====
104152

‎doc/source/intro.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Overview / Install
66

77
GitPython is a python library used to interact with git repositories, high-level like git-porcelain, or low-level like git-plumbing.
88

9-
It provides Python objects for repository data and delegates Git operations to the Git executable. The default backend supports both SHA-1 and SHA-256 object IDs and both files and reftable reference storage. The legacy pure-Python GitDB backend remains available but is deprecated.
9+
It provides Python objects for repository data and delegates Git operations to the Git executable. The default backend supports both SHA-1 and SHA-256 object IDs and both files and reftable reference storage. The legacy pure-Python GitDB backend is removed.
1010

1111
The object database implementation is optimized for handling large quantities of objects and large datasets, which is achieved by using low-level structures and data streaming.
1212

@@ -15,7 +15,7 @@ Requirements
1515

1616
* `Python`_ >= 3.8
1717
* `Git`_ 2.52 or newer
18-
* `GitDB`_ - shared data types and the deprecated legacy object database
18+
* `GitDB`_ - shared data types and utilities
1919
* `typing_extensions`_ >= 3.7.3.4 (if python < 3.10)
2020

2121
.. _Python: https://www.python.org

‎doc/source/tutorial.rst‎

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ As trees allow direct access to their intermediate child entries only, use the t
341341

342342
The Index Object
343343
****************
344-
The git index is the stage containing changes to be written with the next commit or where merges finally have to take place. You may access and manipulate semantic entries (mode, object ID, path, and stage) using the :class:`IndexFile <git.index.base.IndexFile>` object. Git reads and writes the underlying index; raw stat fields and binary index extensions are not exposed.
344+
The git index is the stage containing changes to be written with the next commit or where merges finally have to take place. Use :meth:`IndexFile.iter_entries <git.index.base.IndexFile.iter_entries>` and :meth:`IndexFile.entry <git.index.base.IndexFile.entry>` to query immutable entry records (mode, object ID, path, and stage); use ``add`` and ``remove`` to edit them. Deferred edits retain opaque index-file bytes. Git reads and writes the underlying index; raw stat fields and binary index extensions are not exposed.
345345
Modify the index with ease
346346

347347
.. literalinclude:: ../../test/test_docs.py
@@ -522,20 +522,9 @@ resolves abbreviated object IDs through persistent ``git cat-file`` processes::
522522
# Equivalent explicit selection:
523523
repo = Repo("path/to/repo", odbt=GitCmdObjectDB)
524524

525-
GitDB
526-
=====
527-
.. warning::
528-
The pure-Python ``GitDB`` backend is deprecated due to security and performance
529-
issues. Its object parsers can exhaust resources or return incorrect object
530-
data when processing untrusted repositories. Do not use it for untrusted data.
531-
532-
Selecting ``odbt=GitDB`` (including a subclass) emits a ``DeprecationWarning``.
533-
To migrate, remove ``odbt=GitDB`` or replace it with ``odbt=GitCmdObjectDB`` when
534-
opening, initializing, or cloning a repository. The deprecated backend remains
535-
available for compatibility; deprecation does not fix its parsing issues.
536-
537-
The ``gitdb`` package remains a dependency because GitPython still uses its shared
538-
types and utilities.
525+
Only ``GitCmdObjectDB`` and subclasses can be selected through ``odbt``. The
526+
legacy ``GitDB`` backend is removed. The ``gitdb`` package remains a dependency
527+
for shared types and utilities.
539528

540529
Git Command Debugging and Customization
541530
***************************************

‎git/__init__.py‎

Lines changed: 3 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"GitCommandError",
3030
"GitCommandNotFound",
3131
"GitConfigParser",
32-
"GitDB",
3332
"GitError",
3433
"HEAD",
3534
"Head",
@@ -40,13 +39,11 @@
4039
"IndexObject",
4140
"InvalidDBRoot",
4241
"InvalidGitRepositoryError",
43-
"List", # Deprecated - import this from `typing` instead.
4442
"LockFile",
4543
"NULL_TREE",
4644
"NoSuchPathError",
4745
"ODBError",
4846
"Object",
49-
"Optional", # Deprecated - import this from `typing` instead.
5047
"ParseError",
5148
"PathLike",
5249
"PushInfo",
@@ -60,19 +57,15 @@
6057
"RepositoryDirtyError",
6158
"RootModule",
6259
"RootUpdateProgress",
63-
"Sequence", # Deprecated - import from `typing`, or `collections.abc` in 3.9+.
6460
"StageType",
6561
"Stats",
6662
"Submodule",
6763
"SymbolicReference",
68-
"TYPE_CHECKING", # Deprecated - import this from `typing` instead.
6964
"Tag",
7065
"TagObject",
7166
"TagReference",
7267
"Tree",
7368
"TreeModifier",
74-
"Tuple", # Deprecated - import this from `typing` instead.
75-
"Union", # Deprecated - import this from `typing` instead.
7669
"UnmergedEntriesError",
7770
"UnsafeOptionError",
7871
"UnsafeProtocolError",
@@ -88,12 +81,7 @@
8881

8982
__version__ = "git"
9083

91-
from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92-
93-
if TYPE_CHECKING:
94-
from types import ModuleType
95-
96-
import warnings
84+
from typing import Optional as _Optional
9785

9886
from git.util import to_hex_sha
9987

@@ -158,7 +146,7 @@
158146
DiffIndex,
159147
Diffable,
160148
)
161-
from git.db import GitCmdObjectDB, GitDB # @NoMove
149+
from git.db import GitCmdObjectDB # @NoMove
162150
from git.cmd import Git # @NoMove
163151
from git.repo import Repo # @NoMove
164152
from git.remote import FetchInfo, PushInfo, Remote, RemoteProgress # @NoMove
@@ -169,9 +157,6 @@
169157
IndexEntry,
170158
IndexFile,
171159
StageType,
172-
# NOTE: This tells type checkers what util resolves to. We delete it, and it is
173-
# really resolved by __getattr__, which warns. See below on what to use instead.
174-
util,
175160
)
176161
from git.util import ( # @NoMove
177162
Actor,
@@ -185,82 +170,12 @@
185170
raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
186171

187172

188-
def _warned_import(message: str, fullname: str) -> "ModuleType":
189-
import importlib
190-
191-
warnings.warn(message, DeprecationWarning, stacklevel=3)
192-
return importlib.import_module(fullname)
193-
194-
195-
def _getattr(name: str) -> Any:
196-
# TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
197-
198-
if name == "util":
199-
return _warned_import(
200-
"The expression `git.util` and the import `from git import util` actually "
201-
"reference git.index.util, and not the git.util module accessed in "
202-
'`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
203-
"confusing behavior is currently preserved for compatibility, but may be "
204-
"changed in the future and should not be relied on.",
205-
fullname="git.index.util",
206-
)
207-
208-
for names, prefix in (
209-
({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
210-
({"base", "fun", "typ"}, "git.index"),
211-
):
212-
if name not in names:
213-
continue
214-
215-
fullname = f"{prefix}.{name}"
216-
217-
return _warned_import(
218-
f"{__name__}.{name} is a private alias of {fullname} and subject to "
219-
f"immediate removal. Use {fullname} instead.",
220-
fullname=fullname,
221-
)
222-
223-
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
224-
225-
226-
if not TYPE_CHECKING:
227-
# NOTE: The expression `git.util` gives git.index.util and `from git import util`
228-
# imports git.index.util, NOT git.util. It may not be feasible to change this until
229-
# the next major version, to avoid breaking code inadvertently relying on it.
230-
#
231-
# - If git.index.util *is* what you want, use (or import from) that, to avoid
232-
# confusion.
233-
#
234-
# - To use the "real" git.util module, write `from git.util import ...`, or if
235-
# necessary access it as `sys.modules["git.util"]`.
236-
#
237-
# Note also that `import git.util` technically imports the "real" git.util... but
238-
# the *expression* `git.util` after doing so is still git.index.util!
239-
#
240-
# (This situation differs from that of other indirect-submodule imports that are
241-
# unambiguously non-public and subject to immediate removal. Here, the public
242-
# git.util module, though different, makes less discoverable that the expression
243-
# `git.util` refers to a non-public attribute of the git module.)
244-
#
245-
# This had originally come about by a wildcard import. Now that all intended imports
246-
# are explicit, the intuitive but potentially incompatible binding occurs due to the
247-
# usual rules for Python submodule bindings. So for now we replace that binding with
248-
# git.index.util, delete that, and let __getattr__ handle it and issue a warning.
249-
#
250-
# For the same runtime behavior, it would be enough to forgo importing util, and
251-
# delete util as created naturally; __getattr__ would behave the same. But type
252-
# checkers would not know what util refers to when accessed as an attribute of git.
253-
del util
254-
255-
# This is "hidden" to preserve static checking for undefined/misspelled attributes.
256-
__getattr__ = _getattr
257-
258173
# { Initialize git executable path
259174

260175
GIT_OK = None
261176

262177

263-
def refresh(path: Optional[PathLike] = None) -> None:
178+
def refresh(path: _Optional[PathLike] = None) -> None:
264179
"""Convenience method for setting the git executable path.
265180
266181
:param path:

0 commit comments

Comments
 (0)