Skip to content

Commit 43d02e6

Browse files
codexByron
authored andcommitted
feat(gix): stream small objects from native storage
Read object contents through GixPython and return independent byte streams from `Git.stream_object_data`. Partially reading one object must not corrupt another read. Retain CLI streaming above 8 MiB because the current native lookup buffers an entire object (GIX-2). The staged version passes Ruff and Python syntax checks. Its SHA-1/SHA-256 native smoke and focused pytest selection pass: 9 passed in 2.40s.
1 parent 3ab125b commit 43d02e6

3 files changed

Lines changed: 23 additions & 2 deletions

File tree

‎git/_backend.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from collections import Counter
99
from importlib import import_module
10+
import io
1011
import logging
1112
import os
1213
import re
@@ -96,11 +97,11 @@ def _oid(repo: Any, ref: Any) -> Any:
9697
return repo.rev_parse_single(ref)
9798

9899

99-
def object_data(command: Any, ref: bytes) -> Any:
100+
def object_data(command: Any, ref: bytes, *, stream: bool = False) -> Any:
100101
"""Use native object reads without changing the public cat-file interface."""
101102
if gix is None:
102103
return NotImplemented
103-
method = "get_object_header"
104+
method = "stream_object_data" if stream else "get_object_header"
104105
command._require_version()
105106
try:
106107
repo = _repository(command, {})
@@ -109,6 +110,11 @@ def object_data(command: Any, ref: bytes) -> Any:
109110
if header is None:
110111
raise ValueError("SHA %s could not be resolved" % oid)
111112
result: Tuple[Any, ...] = (str(oid), header.kind(), header.size())
113+
if stream:
114+
# ponytail: native lookup buffers the object; use CLI above 8 MiB until GIX-2 provides streaming.
115+
if header.size() > 8 * 1024 * 1024:
116+
raise _Unsupported("large object streaming (GIX-2)")
117+
result += (io.BytesIO(repo.find_object(oid).data),)
112118
except _Unsupported as exc:
113119
return _fallback(method, str(exc))
114120
except gix.Error as exc:

‎git/cmd.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,9 @@ def stream_object_data(self, ref: Union[str, bytes]) -> Tuple[str, str, int, "Gi
20932093
instance per thread to be safe!
20942094
"""
20952095
request = self._prepare_ref(ref)
2096+
native = _backend.object_data(self, request[:-1], stream=True)
2097+
if native is not NotImplemented:
2098+
return native
20962099
cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True, Z=True)
20972100
hexsha, typename, size = self.__get_object_header(cmd, request)
20982101
cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO()

‎test/test_gix_backend.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Exercise the installed backend, including proof that converted calls avoid Git."""
22

3+
from io import BytesIO
34
from pathlib import Path
45
from unittest.mock import patch
56

67
import pytest
78

89
from git import Git, Repo, _backend
10+
from gitdb import IStream
911

1012
gix = pytest.importorskip("gix")
1113

@@ -29,3 +31,13 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
2931
with repo.git.custom_environment(GIT_INDEX_FILE=".git/index"):
3032
assert repo.git._call_process_safe("ls_files", "--stage", "-v", "-z", "--full-name") == "fallback"
3133
assert cli.call_count == 2
34+
35+
36+
def test_partial_native_stream_does_not_corrupt_next_read(repo):
37+
one = repo.odb.store(IStream("blob", 6, BytesIO(b"abcdef"))).binsha
38+
two = repo.odb.store(IStream("blob", 3, BytesIO(b"xyz"))).binsha
39+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call")):
40+
stream = repo.odb.stream(one)
41+
assert stream.read(1) == b"a"
42+
assert repo.odb.stream(two).read() == b"xyz"
43+
assert stream.read() == b"bcdef"

0 commit comments

Comments
 (0)