Skip to content

Commit 4bf5be6

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 512f2a6 commit 4bf5be6

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
@@ -350,6 +350,53 @@ def _update_index(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
350350
return str(_index(repo, kwargs).version()).encode("ascii") + b"\n"
351351

352352

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

‎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")
@@ -69,6 +69,23 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
6969
assert cli.call_count == 3
7070

7171

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

0 commit comments

Comments
 (0)