Skip to content

Commit bb2b59a

Browse files
codexByron
authored andcommitted
feat(gix): persist index edits through native snapshots
Materialize in-memory index changes in a private native index and publish it through the existing atomic lock path. Preserve metadata for unchanged entries and validate before writes. Keep custom, sparse, split, non-v2, unmerged, overlapping and null-ID cases on Git. Add an assertion that split indexes remain split after editing. The staged version passes Ruff and Python syntax checks. Its SHA-1/SHA-256 native smoke and focused pytest selection pass: 54 passed in 14.32s.
1 parent e96aece commit bb2b59a

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

‎git/_backend.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
from collections import Counter
9+
from glob import glob
910
from importlib import import_module
1011
import io
1112
from itertools import islice
@@ -365,6 +366,70 @@ def _index_from_tree(repo: Any, tree: Any) -> Any:
365366
return repo.index_from_tree(tree)
366367

367368

369+
def materialize_index(command: Any, source: Any, destination: str, desired: Dict[Any, Any], dirty: Any) -> Any:
370+
"""Write a private index, retaining native metadata for unchanged entries."""
371+
if gix is None:
372+
return NotImplemented
373+
method = "IndexFile.write"
374+
command._require_version()
375+
try:
376+
if any(stage for _path, stage in desired):
377+
raise _Unsupported("unmerged index editing")
378+
if any(not entry.binsha.strip(b"\0") for entry in desired.values()):
379+
raise _Unsupported("null index object IDs")
380+
names = {os.fsencode(path) for path, _stage in desired}
381+
for path in names:
382+
while b"/" in path:
383+
path = path.rsplit(b"/", 1)[0]
384+
if path in names:
385+
raise _Unsupported("overlapping index paths")
386+
repo = _repository(command, {})
387+
if os.path.exists(source):
388+
if repo.config_snapshot().boolean("core.splitIndex") or glob(
389+
os.path.join(os.path.dirname(os.fspath(source)), "sharedindex.*")
390+
):
391+
raise _Unsupported("split index preservation (GIX-13)")
392+
index = _index(repo, {"env": {"GIT_INDEX_FILE": os.fspath(source)}})
393+
if index.version() != 2:
394+
raise _Unsupported("index version preservation (GIX-13)")
395+
else:
396+
index = _index_from_tree(repo, repo.empty_tree())
397+
# Keep path-independent flags that GitPython exposes; changed paths get fresh stat data.
398+
mask = (3 << 12) | (1 << 15) | (1 << 30)
399+
changed = {os.fsencode(path) for path in dirty}
400+
with index.entries() as entries:
401+
current = list(entries)
402+
for entry in current:
403+
wanted = desired.get((safe_decode(entry.path()), entry.stage()))
404+
if wanted is None or (entry.mode, str(entry.id), entry.flags & mask) != (
405+
wanted.mode,
406+
wanted.hexsha,
407+
wanted.flags & mask,
408+
):
409+
changed.add(entry.path())
410+
changed.update(names - {entry.path() for entry in current})
411+
# ponytail: per-entry removals shift a vector; use a bulk binding for very large edits.
412+
for position, entry in reversed(list(enumerate(current))):
413+
if entry.path() in changed:
414+
index.remove_entry_at_index(position)
415+
for (path, _stage), entry in desired.items():
416+
if os.fsencode(path) in changed:
417+
index.dangerously_push_entry(
418+
gix.IndexStat(), gix.ObjectId(entry.hexsha), entry.flags & mask, entry.mode, os.fsencode(path)
419+
)
420+
index.sort_entries()
421+
index.verify_entries()
422+
index.set_path(destination)
423+
_write("index", index.write)
424+
except _Unsupported as exc:
425+
return _fallback(method, str(exc))
426+
except gix.Error as exc:
427+
_logger.debug("materialize_index native preparation: %s", exc)
428+
return _fallback(method, "native preparation diagnostics")
429+
record(method, "native")
430+
return None
431+
432+
368433
def _write(method: str, function: Callable[[], Any]) -> Any:
369434
"""Once a write starts, an error must not cause a second attempt through Git."""
370435
try:

‎git/index/base.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ def _materialized_index(self) -> Generator[str, None, None]:
189189
desired = self._validated_entries()
190190
with tempfile.TemporaryDirectory(prefix="gitpython-index-") as directory:
191191
path = osp.join(directory, "index")
192+
native = _backend.materialize_index(self.repo.git, self.path, path, desired, self._dirty_paths)
193+
if native is not NotImplemented:
194+
yield path
195+
return
192196
env = {"GIT_INDEX_FILE": path}
193197
if osp.isfile(self.path):
194198
shutil.copyfile(self.path, path)

‎test/test_cli_objects_index.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ def test_git_index_storage_variants(repo, storage):
184184
assert index.write_tree()["inside/file"].data_stream.read() == b"changed"
185185
if storage == "v4":
186186
assert index.version == 4
187+
elif storage == "split":
188+
assert repo.git.rev_parse("--shared-index-path")
187189

188190

189191
def test_trailer_commands_are_not_executed(repo):

0 commit comments

Comments
 (0)