Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions impact_gate/core/gitplumb.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class GitRepo:
def __init__(self, path: str):
self.path = path
self._batch: subprocess.Popen | None = None
self._batch_z: bool | None = None

def _run(self, *args: str) -> str:
out = subprocess.run(["git", "-C", self.path, "-c", "diff.mnemonicprefix=false", *args],
Expand All @@ -51,11 +52,26 @@ def diff(self, old: str, new: str) -> list[FileDiff]:
return parse_diff(text)

# ---- blob streaming via a persistent cat-file --batch -----------------
def _supports_batch_z(self) -> bool:
# cat-file --batch -Z (NUL-delimited I/O) needs git >= 2.42; older git
# exits non-zero on the unknown option. Probe once and fall back to the
# newline protocol so older environments keep working (a filename with a
# literal newline is then the only case that stays broken).
if self._batch_z is None:
probe = subprocess.run(
["git", "-C", self.path, "cat-file", "--batch", "-Z"],
input=b"", capture_output=True,
)
self._batch_z = probe.returncode == 0
return self._batch_z

def _ensure_batch(self) -> subprocess.Popen:
if self._batch is None or self._batch.poll() is not None:
args = ["git", "-C", self.path, "cat-file", "--batch"]
if self._supports_batch_z():
args.append("-Z")
self._batch = subprocess.Popen(
["git", "-C", self.path, "cat-file", "--batch", "-Z"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
)
return self._batch

Expand All @@ -77,9 +93,13 @@ def blob(self, rev: str, path: str) -> tuple[str, bytes] | None:
"""
proc = self._ensure_batch()
assert proc.stdin and proc.stdout
proc.stdin.write(f"{rev}:{path}\0".encode())
z = self._supports_batch_z()
proc.stdin.write(f"{rev}:{path}".encode() + (b"\0" if z else b"\n"))
proc.stdin.flush()
header = self._read_until_nul(proc.stdout).decode("utf-8", errors="replace")
if z:
header = self._read_until_nul(proc.stdout).decode("utf-8", errors="replace")
else:
header = proc.stdout.readline().decode("utf-8", errors="replace").strip()
if not header or header.endswith(("missing", "ambiguous")):
return None
parts = header.split()
Expand All @@ -89,7 +109,7 @@ def blob(self, rev: str, path: str) -> tuple[str, bytes] | None:
except ValueError:
return None
data = proc.stdout.read(size)
proc.stdout.read(1) # trailing NUL (-Z mode)
proc.stdout.read(1) # trailing NUL (-Z) or newline
return oid, data

def close(self) -> None:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_gitplumb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""GitRepo blob streaming: the persistent cat-file --batch protocol."""
import pytest

from impact_gate.core.gitplumb import GitRepo
from gitutil import commit, write


def test_blob_reads_file_with_newline_in_name(repo):
write(repo, "plain.py", "x = 1\n")
write(repo, "a\nb.py", "y = 2\n")
commit(repo, "base")
r = GitRepo(str(repo))
if not r._supports_batch_z():
r.close()
pytest.skip("git cat-file --batch -Z unsupported (git < 2.42)")
try:
assert r.blob("HEAD", "a\nb.py")[1] == b"y = 2\n"
assert r.blob("HEAD", "plain.py")[1] == b"x = 1\n"
finally:
r.close()


def test_blob_falls_back_to_newline_protocol_without_batch_z(repo):
write(repo, "plain.py", "x = 1\n")
commit(repo, "base")
r = GitRepo(str(repo))
r._batch_z = False
try:
assert "-Z" not in r._ensure_batch().args
assert r.blob("HEAD", "plain.py")[1] == b"x = 1\n"
finally:
r.close()