Skip to content

Commit 143df53

Browse files
codexByron
authored andcommitted
feat(gix): write index trees with the native editor
Write `IndexFile.write_tree` through the native tree editor after the shared complete index validation. Keep unmerged-entry rejection and reuse the existing Tree result type. Missing non-gitlink children retain Git's `--missing-ok` behavior through fallback (GIX-7). The staged version passes Ruff and Python syntax checks. Its SHA-1/SHA-256 native smoke and focused pytest selection pass: 48 passed in 16.38s.
1 parent ea2769e commit 143df53

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

‎git/_backend.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,49 @@ def _hash_object(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
397397
return str(oid).encode("ascii") + b"\n"
398398

399399

400+
_ENTRY_KINDS = {0o100644: "blob", 0o100755: "exe", 0o120000: "link", 0o160000: "commit", 0o40000: "tree"}
401+
402+
403+
def _write_tree(repo: Any, entries: Sequence[Tuple[bytes, int, str]]) -> str:
404+
names = {name for name, _mode, _oid in entries}
405+
if len(names) != len(entries):
406+
raise _Unsupported("duplicate or overlapping tree paths")
407+
for name in names:
408+
while b"/" in name:
409+
name = name.rsplit(b"/", 1)[0]
410+
if name in names:
411+
raise _Unsupported("duplicate or overlapping tree paths")
412+
for _path, mode, oid in entries:
413+
object_id = gix.ObjectId(oid)
414+
if object_id.is_null():
415+
raise _Unsupported("null tree object IDs")
416+
header = repo.try_find_header(object_id)
417+
if header is None:
418+
if mode != 0o160000:
419+
raise _Unsupported("missing tree children (GIX-7)")
420+
elif header.kind() != ("tree" if mode == 0o40000 else "commit" if mode == 0o160000 else "blob"):
421+
raise _Unsupported("tree child object kind")
422+
with repo.empty_tree().edit() as editor:
423+
for path, mode, oid in entries:
424+
editor.upsert(path, _ENTRY_KINDS[mode], gix.ObjectId(oid))
425+
return str(_write("write_tree", editor.write))
426+
427+
428+
def write_tree(command: Any, entries: Sequence[Tuple[bytes, int, str]]) -> Any:
429+
if gix is None:
430+
return NotImplemented
431+
command._require_version()
432+
try:
433+
result = _write_tree(_repository(command, {}), entries)
434+
except _Unsupported as exc:
435+
return _fallback("IndexFile.write_tree", str(exc))
436+
except gix.Error as exc:
437+
_logger.debug("write_tree native preparation: %s", exc)
438+
return _fallback("IndexFile.write_tree", "native preparation diagnostics")
439+
record("IndexFile.write_tree", "native")
440+
return result
441+
442+
400443
_HANDLERS: Dict[str, Callable[[Any, List[str], Dict[str, Any]], bytes]] = {
401444
"rev_parse": _rev_parse,
402445
"ls_tree": _ls_tree,

‎git/index/base.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from gitdb.base import IStream
2323

24+
from git import _backend
2425
from git.compat import defenc, force_bytes, safe_decode
2526
from git.cmd import Git
2627
import git.diff as git_diff
@@ -169,9 +170,8 @@ def version(self) -> int:
169170
)
170171
)
171172

172-
@contextlib.contextmanager
173-
def _materialized_index(self) -> Generator[str, None, None]:
174-
"""Apply the in-memory entries to a private, Git-managed index."""
173+
def _validated_entries(self) -> Dict[Tuple[PathLike, StageType], IndexEntry]:
174+
"""Validate the complete update before either backend can write."""
175175
desired: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
176176
for key, entry in self.entries.items():
177177
path = os.fspath(entry.path)
@@ -181,6 +181,12 @@ def _materialized_index(self) -> Generator[str, None, None]:
181181
if len(entry.binsha) != self.repo._oid_size or entry.mode not in (0o100644, 0o100755, 0o120000, 0o160000):
182182
raise ValueError("Invalid index object ID or mode")
183183
desired[(path, entry.stage)] = entry
184+
return desired
185+
186+
@contextlib.contextmanager
187+
def _materialized_index(self) -> Generator[str, None, None]:
188+
"""Apply the in-memory entries to a private, Git-managed index."""
189+
desired = self._validated_entries()
184190
with tempfile.TemporaryDirectory(prefix="gitpython-index-") as directory:
185191
path = osp.join(directory, "index")
186192
env = {"GIT_INDEX_FILE": path}
@@ -670,6 +676,12 @@ def write_tree(self) -> Tree:
670676
for entry in self.entries.values():
671677
if entry.stage:
672678
raise UnmergedEntriesError(entry)
679+
entries = self._validated_entries()
680+
native = _backend.write_tree(
681+
self.repo.git, [(os.fsencode(entry.path), entry.mode, entry.hexsha) for entry in entries.values()]
682+
)
683+
if native is not NotImplemented:
684+
return Tree(self.repo, bytes.fromhex(native), path="")
673685
with self._materialized_index() as path:
674686
oid = self.repo.git._call_process_safe("write_tree", "--missing-ok", env={"GIT_INDEX_FILE": path})
675687
return Tree(self.repo, bytes.fromhex(oid), path="")

0 commit comments

Comments
 (0)