Skip to content

Commit b48f13c

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 e5a36fe commit b48f13c

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
@@ -361,6 +362,70 @@ def _index_from_tree(repo: Any, tree: Any) -> Any:
361362
return repo.index_from_tree(tree)
362363

363364

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