FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(index): restore read-only `entries` compatibility · gitpython-developers/GitPython@b3a4ba8 · GitHub

Repository navigation

Commit b3a4ba8

Browse files
authored andcommitted
fix(index): restore read-only entries compatibility
dlt 1.31.0's `test_init_code_update_no_conflict` reads `repo.index.entries` before staging an update. Removing that property raises `AttributeError` in this branch, while the unmodified test passes with released GitPython 3.2.1. Renku's repository file listing uses the same mapping keys at runtime. Expose an immutable snapshot keyed by `(path, stage)` through the existing `iter_entries()` query. Every access reflects the published or deferred index, and earlier snapshots retain their values. This restores read compatibility without retaining an editable Python index or implementing Git storage semantics independently. Default-index queries continue through GixPython's supported index API where available; other queries retain their Git fallback. Direct mapping mutation remains unsupported and is documented accordingly. Add a Bash fixture and regressions for mapping lookup/iteration, merge stages, snapshot immutability, and pending versus published edits across SHA-1/SHA-256 and files/reftable storage. All four regressions failed before the fix. The regression and adjacent index suites passed 163 tests with two skips on each backend, CLI first, with Python 3.12.15, Git 2.53.0 and GixPython 0.2.0. Ruff lint/format, ShellCheck and mypy for `git/index/base.py` passed. The original dlt regression also passed with the restored property on Gix; the broader downstream audit remains in progress.
1 parent aa136c6 commit b3a4ba8

7 files changed

Lines changed: 67 additions & 4 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ fallbacks.
4646
## Index and object metadata lifecycle
4747

4848
`IndexFile` queries immutable entry records; it does not retain a parsed index.
49+
The compatibility `entries` property projects those queries into a read-only
50+
snapshot keyed by path and stage, preserving readers in dlt and Renku. It uses
51+
the same Gix-backed default-index query or documented Git fallback as
52+
`iter_entries()`; snapshots do not become editable index state.
4953
Deferred/virtual state consists of opaque file bytes, materialized before Git
5054
operates and read back only after success. Private edits use Git because native
5155
bindings cannot load an arbitrary existing index (GIX-3) or validate a checked

‎doc/source/changes.rst‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,11 @@ API changes
101101
Fetch/pull results are derived from command output rather than ``FETCH_HEAD``
102102
file parsing. Revision strings follow Git's native revision grammar.
103103

104-
* ``IndexFile.entries`` is removed. Use ``iter_entries()`` for immutable query
105-
records, ``entry(path, stage=0)`` for exact lookup (missing entries raise
104+
* ``IndexFile.entries`` returns an immutable snapshot keyed by ``(path, stage)``
105+
for compatibility with existing readers. Each access queries the current
106+
on-disk or pending index; previously returned snapshots retain their values.
107+
Direct mutation of the mapping is removed. Use ``iter_entries()`` for immutable
108+
query records, ``entry(path, stage=0)`` for exact lookup (missing entries raise
106109
``KeyError``), ``add()``, and ``remove()`` for edits. Python retains only opaque
107110
index-file bytes for deferred/virtual indexes. Git edits private files and
108111
validates readback before atomic publication. ``add(write=False)`` and

‎doc/source/tutorial.rst‎

Lines changed: 1 addition & 1 deletion
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. 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.
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). The ``entries`` property provides a read-only snapshot keyed by ``(path, stage)`` for existing readers; use ``add`` and ``remove`` to edit the index. 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

‎git/index/base.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import subprocess
1919
import shutil
2020
import tempfile
21+
from types import MappingProxyType
2122

2223
from gitdb.base import IStream
2324

@@ -60,6 +61,7 @@
6061
Iterable,
6162
Iterator,
6263
List,
64+
Mapping,
6365
NoReturn,
6466
Sequence,
6567
TYPE_CHECKING,
@@ -148,6 +150,15 @@ def iter_entries(self) -> Iterator[IndexEntry]:
148150
with self._materialized_index() as path:
149151
yield from self._read_entries(path)
150152

153+
@property
154+
def entries(self) -> Mapping[Tuple[PathLike, StageType], IndexEntry]:
155+
"""Return an immutable snapshot keyed by path and merge stage.
156+
157+
Each access queries the current on-disk or pending index. Use :meth:`add`
158+
and :meth:`remove` to edit the index; the snapshot is never written back.
159+
"""
160+
return MappingProxyType({entry_key(entry): entry for entry in self.iter_entries()})
161+
151162
def entry(self, path: PathLike, stage: StageType = 0) -> IndexEntry:
152163
"""Return the exact path/stage entry; raise ``KeyError`` if absent."""
153164
for entry in self.iter_entries():

‎test/test_cli_objects_index.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def test_pending_index_queries_removal_and_failed_update(repo):
180180
index.add(["file"], write=False)
181181
pending = index.entry("file")
182182
assert pending.binsha != original.binsha
183-
assert not hasattr(index, "entries")
183+
assert index.entries[("file", 0)] == pending
184184
with pytest.raises(ValueError, match="Git did not retain"):
185185
index.add([BaseIndexEntry((0o120000, pending.binsha, 0, ".gitmodules"))], write=False)
186186
with pytest.raises(ValueError, match="Invalid index path"):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Public GitPython queries used by released downstream projects."""
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from test.lib.fixtures import fixture_repo
8+
9+
10+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
11+
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
12+
def test_index_entries_readers_retain_pending_and_published_snapshots(object_format, ref_format):
13+
with fixture_repo("downstream-index.sh", object_format, ref_format, write=True) as repo:
14+
index = repo.index
15+
published_bytes = Path(index.path).read_bytes()
16+
entries = index.entries
17+
assert len(entries) == 4
18+
assert set(entries.keys()) == {("tracked", 0), *(("conflict", stage) for stage in (1, 2, 3))}
19+
assert [str(key[0]) for key in entries].count("conflict") == 3
20+
assert entries[("tracked", 0)].binsha == repo.head.commit.tree["tracked"].binsha
21+
assert all(entry.path == path and entry.stage == stage for (path, stage), entry in entries.items())
22+
with pytest.raises(KeyError):
23+
entries[("missing", 0)]
24+
with pytest.raises(TypeError):
25+
entries[("tracked", 0)] = entries[("conflict", 1)]
26+
index.add(["tracked"], write=False)
27+
pending = index.entries
28+
assert pending[("tracked", 0)].binsha != entries[("tracked", 0)].binsha
29+
assert repo.index.entries[("tracked", 0)] == entries[("tracked", 0)]
30+
assert Path(index.path).read_bytes() == published_bytes
31+
index.write()
32+
assert repo.index.entries == pending
33+
assert entries[("tracked", 0)].binsha == repo.head.commit.tree["tracked"].binsha

‎tests/fixtures/downstream-index.sh‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init -q --object-format="$1" --ref-format="$2" repo
4+
cd repo
5+
printf initial > tracked
6+
git add tracked
7+
git commit -qm initial
8+
oid=$(git rev-parse HEAD:tracked)
9+
for stage in 1 2 3; do
10+
printf '100644 %s %s\tconflict\n' "$oid" "$stage"
11+
done | git update-index --index-info
12+
printf changed > tracked

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL