Skip to content

Commit 8385ecc

Browse files
codexByron
authored andcommitted
feat(gix): hash and store objects natively
Implement managed object hashing/writing for blobs, trees, commits and tags. Validate the native serialization in an in-memory ODB before persisting bytes, and preserve the input position on fallback. Limit native input to 8 MiB. Once a native write starts, convert its error without retrying via Git. The staged version passes Ruff and Python syntax checks. Its SHA-1/SHA-256 native smoke and focused pytest selection pass: 8 passed in 1.20s.
1 parent 3bb7fab commit 8385ecc

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

‎git/_backend.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,53 @@ def _update_index(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
354354
return str(_index(repo, kwargs).version()).encode("ascii") + b"\n"
355355

356356

357+
def _write(method: str, function: Callable[[], Any]) -> Any:
358+
"""Once a write starts, an error must not cause a second attempt through Git."""
359+
try:
360+
return function()
361+
except gix.Error as exc:
362+
raise GitCommandError(["gix", method], 128, str(exc)) from exc
363+
364+
365+
def _input(kwargs: Dict[str, Any]) -> bytes:
366+
stream = kwargs.get("istream")
367+
if stream is None or not hasattr(stream, "seekable") or not stream.seekable():
368+
raise _Unsupported("nonseekable input")
369+
start = stream.tell()
370+
try:
371+
stream.seek(0, 2)
372+
if stream.tell() - start > 8 * 1024 * 1024:
373+
raise _Unsupported("large object streaming (GIX-2)")
374+
stream.seek(start)
375+
data = stream.read()
376+
finally:
377+
stream.seek(start)
378+
if not isinstance(data, bytes):
379+
raise _Unsupported("nonbinary input")
380+
return data
381+
382+
383+
def _hash_object(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
384+
if len(args) not in (3, 4) or args[0] != "-t" or args[-1] != "--stdin":
385+
raise _Unsupported("object hashing options")
386+
write = args[2:-1] == ["-w"]
387+
if args[2:-1] not in ([], ["-w"]):
388+
raise _Unsupported("object hashing options")
389+
kind = args[1]
390+
if kind not in ("blob", "tree", "commit", "tag"):
391+
raise _Unsupported("object kind")
392+
data = _input(kwargs)
393+
# Validate and check the encoded bytes in memory before touching object storage.
394+
memory = repo.with_object_memory()
395+
oid = memory.write_object(kind, data)
396+
if memory.find_object(oid).data != data:
397+
raise _Unsupported("object serialization changes bytes (GIX-5)")
398+
if write:
399+
oid = _write("hash_object", lambda: repo.write_object(kind, data))
400+
kwargs["istream"].seek(len(data), 1)
401+
return str(oid).encode("ascii") + b"\n"
402+
403+
357404
_HANDLERS: Dict[str, Callable[[Any, List[str], Dict[str, Any]], bytes]] = {
358405
"rev_parse": _rev_parse,
359406
"ls_tree": _ls_tree,
@@ -362,6 +409,7 @@ def _update_index(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
362409
"merge_base": _merge_base,
363410
"ls_files": _ls_files,
364411
"update_index": _update_index,
412+
"hash_object": _hash_object,
365413
"config": _config,
366414
"worktree": _worktree,
367415
}

‎test/test_gix_backend.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88

99
from git import Git, Repo, _backend
10-
from git.exc import InvalidGitRepositoryError
10+
from git.exc import GitCommandError, InvalidGitRepositoryError
1111
from gitdb import IStream
1212

1313
gix = pytest.importorskip("gix")
@@ -70,6 +70,23 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
7070
assert cli.call_count == 3
7171

7272

73+
def test_a_native_write_failure_is_not_retried(repo, monkeypatch):
74+
attempts = []
75+
76+
def fail():
77+
attempts.append("write")
78+
raise gix.Error("write failed after it began")
79+
80+
def handler(native, args, kwargs):
81+
return _backend._write("test_write", fail)
82+
83+
monkeypatch.setitem(_backend._HANDLERS, "test_write", handler)
84+
with patch.object(Git, "execute", side_effect=AssertionError("must not retry through CLI")):
85+
with pytest.raises(GitCommandError, match="write failed after it began"):
86+
repo.git._call_process_safe("test_write")
87+
assert attempts == ["write"]
88+
89+
7390
def test_partial_native_stream_does_not_corrupt_next_read(repo):
7491
one = repo.odb.store(IStream("blob", 6, BytesIO(b"abcdef"))).binsha
7592
two = repo.odb.store(IStream("blob", 3, BytesIO(b"xyz"))).binsha

0 commit comments

Comments
 (0)