Skip to content

Commit 5919290

Browse files
codexByron
authored andcommitted
perf(gix): resolve revision path metadata without cat-file
`Repo.rev_parse` resolved an object natively but still launched `cat-file` to find its tree/index mode, then probed revision prefixes to locate its path. Use `Repository.rev_parse` to obtain the ID and path/mode together. Preserve path normalization and the absent metadata for root-tree specifications. Share the existing revision-grammar guard. Index revisions also check the existing custom/sparse-index capability guard before native parsing, including OID lookup on the fallback path, so an alternate index is never silently replaced by the repository's default index. The fixed `HEAD:file` probe drops from 1 CLI launch to 0. Regression assertions compare root/directory/file, executable, symlink and index-stage results with Git in both object formats, prohibit native CLI launches, and exercise an alternate index. The earlier full inventory had 170 mode-query launches. Validation: revision, backend and positional-argument tests passed with Gix first (101 tests, 15.60s), then CLI (54 tests, one skipped, 12.65s). Ruff lint/format, mypy, basedpyright and `git diff --check` passed.
1 parent 9fbab72 commit 5919290

4 files changed

Lines changed: 77 additions & 6 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ The historical full-suite counts above predate this correction. A constant
134134
| `Repo` opening/discovery | Native storage, worktree, object format and empty-tree metadata | Repository-format validation, unsupported layouts/environment and native validation gaps; GIX-1/14/19 |
135135
| Standalone discovery helpers | Native storage directories and gitfiles reopened at their canonical native Git directory | Rejected candidates and the same layout/environment guards as `Repo` opening; GIX-14 |
136136
| `Repo.rev_parse`, `rev_parse` metadata queries | Revision lookup, common directory, object format, bare flag, worktree root, fixed `modules`/`COMMIT_EDITMSG` locations | Repository-format validation, other metadata paths, symlinked metadata leaves, message searches and describe forms; GIX-1/14/17/22 |
137+
| Revision path/mode metadata | Native revision specification, including tree paths and index stages | Custom/sparse indexes, unsupported revision grammar or path normalization |
137138
| Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options |
138139
| Reference object reads | One exact native lookup for direct, symbolic and absent references | Partial names and native decoding errors |
139140
| Reference-name validation | Native full-name validation without opening a repository | Standalone names outside the native grammar; GIX-20 |

‎git/_backend.py‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,18 @@ def _canonical_repository(repo: Any) -> Any:
194194
return gix.open_opts(canonical, repo.open_options()) if path != canonical else repo
195195

196196

197-
def _oid(repo: Any, ref: Any) -> Any:
197+
def _check_revision_grammar(ref: str) -> None:
198+
if ref.startswith(":/") or "^{/" in ref or "-dirty" in ref or re.search(r"-\d+-g[0-9a-fA-F]+", ref):
199+
raise _Unsupported("revision grammar differences (GIX-17)")
200+
201+
202+
def _oid(repo: Any, ref: Any, env: Optional[Dict[str, Any]] = None) -> Any:
198203
ref = safe_decode(ref) if isinstance(ref, bytes) else str(ref)
199204
if re.fullmatch(r"[a-fA-F0-9]{40}|[a-fA-F0-9]{64}", ref):
200205
return gix.ObjectId(ref)
201-
if ref.startswith(":/") or "^{/" in ref or "-dirty" in ref or re.search(r"-\d+-g[0-9a-fA-F]+", ref):
202-
raise _Unsupported("revision grammar differences (GIX-17)")
206+
_check_revision_grammar(ref)
207+
if ref.startswith(":"):
208+
_index(repo, {"env": env or {}})
203209
if not hasattr(repo, "rev_parse_single"):
204210
raise _Unsupported("revision feature disabled")
205211
return repo.rev_parse_single(ref)
@@ -212,7 +218,7 @@ def object_data(command: Any, ref: bytes, *, stream: bool = False) -> Any:
212218
method = "stream_object_data" if stream else "get_object_header"
213219
try:
214220
repo = _repository(command, {})
215-
oid = _oid(repo, ref)
221+
oid = _oid(repo, ref, command.environment())
216222
header = repo.try_find_header(oid)
217223
if header is None:
218224
raise ValueError("SHA %s could not be resolved" % oid)
@@ -231,6 +237,29 @@ def object_data(command: Any, ref: bytes, *, stream: bool = False) -> Any:
231237
return result
232238

233239

240+
def revision_info(command: Any, ref: str) -> Any:
241+
"""Resolve an object and its tree/index path metadata in one native parse."""
242+
if gix is None:
243+
return NotImplemented
244+
try:
245+
_check_revision_grammar(ref)
246+
repo = _repository(command, {})
247+
if ref.startswith(":"):
248+
_index(repo, {"env": command.environment()})
249+
spec = repo.rev_parse(ref)
250+
oid = spec.single()
251+
if oid is None:
252+
raise _Unsupported("multiple revisions")
253+
metadata = spec.path_and_mode()
254+
except _Unsupported as exc:
255+
return _fallback("Repo.rev_parse", str(exc))
256+
except gix.Error as exc:
257+
_logger.debug("native revision metadata: %s", exc)
258+
return _fallback("Repo.rev_parse", "native read diagnostics")
259+
record("Repo.rev_parse", "native")
260+
return str(oid), metadata
261+
262+
234263
def _rev_parse(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
235264
if args[:2] == ["--path-format=absolute", "--git-path"]:
236265
from git.util import to_native_path_linux
@@ -259,7 +288,7 @@ def _rev_parse(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
259288
raise _Unsupported("discovery or revision options")
260289
if args[:-2] not in (["--verify"], ["--verify", "--quiet"]):
261290
raise _Unsupported("revision options")
262-
return str(_oid(repo, args[-1])).encode("ascii") + b"\n"
291+
return str(_oid(repo, args[-1], kwargs.get("env"))).encode("ascii") + b"\n"
263292

264293

265294
def _ls_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:

‎git/repo/fun.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,13 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
129129
revisions and embedded command delimiters are rejected before invoking Git.
130130
"""
131131
rev = Git._check_operand(rev, "revision")
132+
native = _backend.revision_info(repo.git, rev) if ":" in rev else NotImplemented
132133
try:
133-
oid = repo.git._call_process_safe("rev_parse", "--verify", "--quiet", "--end-of-options", rev)
134+
oid = (
135+
native[0]
136+
if native is not NotImplemented
137+
else repo.git._call_process_safe("rev_parse", "--verify", "--quiet", "--end-of-options", rev)
138+
)
134139
except GitCommandError as exc:
135140
if exc.status != 1 and "Invalid regular expression" not in exc.stderr:
136141
raise
@@ -139,6 +144,12 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
139144
raise BadName(rev)
140145
obj = Object.new_from_sha(repo, hex_to_bin(oid))
141146
if isinstance(obj, IndexObject) and ":" in rev:
147+
if native is not NotImplemented:
148+
if native[1] is not None:
149+
path_bytes, obj.mode = native[1]
150+
path = path_bytes.decode(defenc, "surrogateescape")
151+
obj.path = posixpath.normpath(path) if path else ""
152+
return obj
142153
# Git resolves the mode, including index stages and executable/symlink
143154
# entries. No object storage or revision grammar is decoded in Python.
144155
with tempfile.TemporaryFile() as stream:

‎test/test_gix_backend.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from git import Actor, Commit, Git, Reference, Repo, SymbolicReference, _backend
1313
from git.exc import GitCommandError, InvalidGitRepositoryError
14+
from git.index.typ import BaseIndexEntry
1415
from git.repo.fun import find_submodule_git_dir, find_worktree_git_dir, is_git_dir
1516
from gitdb import IStream
1617

@@ -306,6 +307,35 @@ def test_native_tree_and_commit_bytes_match_cli(repo):
306307
assert commit.replace(message="a changed message").hexsha == native.hexsha
307308

308309

310+
def test_native_revision_path_and_mode_match_cli(repo, tmp_path):
311+
blob = repo.odb.store(IStream("blob", 4, BytesIO(b"data")))
312+
index = repo.index
313+
index.add(
314+
[
315+
BaseIndexEntry((mode, blob.binsha, 0, path))
316+
for mode, path in ((0o100644, "dir/file"), (0o100755, "executable"), (0o120000, "link"))
317+
]
318+
)
319+
index.commit("paths", skip_hooks=True)
320+
revisions = ["HEAD:", "HEAD:dir/", "HEAD:dir/file", "HEAD:executable", "HEAD:link", ":executable", ":0:link"]
321+
322+
def resolve(revision):
323+
obj = repo.rev_parse(revision)
324+
return obj.hexsha, getattr(obj, "path", None), getattr(obj, "mode", None)
325+
326+
with patch.object(_backend, "gix", None):
327+
expected = [resolve(rev) for rev in revisions]
328+
count = _backend.statistics().get(("Git.execute", "CLI process"), 0)
329+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected revision metadata CLI call")):
330+
assert [resolve(rev) for rev in revisions] == expected
331+
assert _backend.statistics().get(("Git.execute", "CLI process"), 0) == count
332+
333+
with repo.git.custom_environment(GIT_INDEX_FILE=str(tmp_path / "alternate-index")):
334+
repo.git.read_tree("--empty")
335+
repo.git.update_index("--add", "--cacheinfo", "100755", blob.binsha.hex(), "alternate")
336+
assert resolve(":alternate") == (blob.binsha.hex(), "alternate", 0o100755)
337+
338+
309339
def test_native_reference_name_validation_without_repository():
310340
SymbolicReference._check_ref_name_native.cache_clear()
311341
count = _backend.statistics().get(("Git.execute", "CLI process"), 0)

0 commit comments

Comments
 (0)