Skip to content

Commit 721eed1

Browse files
codexByron
authored andcommitted
perf(gix): read absent references without CLI fallbacks
Reference reads previously tried `symbolic-ref` and then `rev-parse`, falling back to Git twice for an ordinary missing reference. Use one native exact-name lookup for `HEAD` and full `refs/` names, returning the existing direct or symbolic target or the same missing-reference `ValueError`. Keep filesystem containment checks on both the requested name and symbolic target. Existing traversal limits still reject cycles and overly deep chains. Partial-name lookups, unsupported storage/environment, and native decoding errors retain the CLI path and its diagnostics. The fixed missing-reference probe drops from 2 CLI launches to 0. A regression compares direct, packed and dangling symbolic reference reads with CLI results and asserts no CLI launches for native reads. The earlier full inventory had 473 adapter-identified missing-reference fallbacks, before secondary probes. Validation: Gix reference, reflog and backend tests passed first (107 tests and 24 subtests, 26.90s), followed by CLI (63 tests, one skipped and 24 subtests, 37.18s). Ruff lint/format, mypy, basedpyright and `git diff --check` passed.
1 parent 726df13 commit 721eed1

4 files changed

Lines changed: 56 additions & 2 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ 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
| `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 |
136136
| Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options |
137-
| Symbolic reference lookup, `symbolic_ref` | Nonrecursive target lookup | Mutation, missing-reference diagnostics |
137+
| Reference object reads | One exact native lookup for direct, symbolic and absent references | Partial names and native decoding errors |
138+
| Managed `symbolic_ref` | Nonrecursive target lookup | Mutation, command-level missing-reference diagnostics |
138139
| Reference enumeration, `for_each_ref` | Sorted reference names and literal prefixes, preserving symbolic aliases | Dangling symbolic refs, root refs, glob patterns, other formats/options |
139140
| Managed `config --get KEY` | Merged repository config snapshot | Files/streams, enumeration, mutation; GIX-12 |
140141
| Worktree inventory, `worktree list` | Main and linked worktree metadata, including bare main repositories and locks | Prunable entries; GIX-14 |

‎git/_backend.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,31 @@ def _ls_tree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
274274
)
275275

276276

277+
def reference_info(command: Any, path: str) -> Any:
278+
"""Read an exact reference target, including an absent reference, in one lookup."""
279+
if gix is None:
280+
return NotImplemented
281+
try:
282+
if path != "HEAD" and not path.startswith("refs/"):
283+
raise _Unsupported("partial reference names")
284+
repo = _repository(command, {})
285+
reference = repo.try_find_reference(path)
286+
result = None
287+
if reference is not None:
288+
if reference.name() != os.fsencode(path):
289+
raise _Unsupported("partial reference lookup")
290+
target = reference.target()
291+
name = target.try_name()
292+
result = (None, safe_decode(name)) if name is not None else (str(target.try_id()), None)
293+
except _Unsupported as exc:
294+
return _fallback("Reference.read", str(exc))
295+
except gix.Error as exc:
296+
_logger.debug("native reference read: %s", exc)
297+
return _fallback("Reference.read", "native read diagnostics")
298+
record("Reference.read", "native")
299+
return result
300+
301+
277302
def _symbolic_ref(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
278303
if len(args) != 4 or args[:3] != ["--quiet", "--no-recurse", "--"]:
279304
raise _Unsupported("reference mutation or options")

‎git/refs/symbolic.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from gitdb.exc import BadName, BadObject
1111

12+
from git import _backend
1213
from git.cmd import Git
1314
from git.exc import GitCommandError, UnsafeOptionError
1415
from git.objects.base import Object
@@ -179,6 +180,13 @@ def _get_ref_info_helper(
179180
raise ValueError("Reference does not exist")
180181
cls._get_validated_ref_path(repo, ref_path)
181182
path = os.fspath(ref_path)
183+
info = _backend.reference_info(repo.git, path)
184+
if info is None:
185+
raise ValueError("Reference at %r does not exist or is invalid" % path)
186+
if info is not NotImplemented:
187+
if info[1] is not None:
188+
cls._get_validated_ref_path(repo, info[1])
189+
return info
182190
try:
183191
target = repo.git._call_process_safe("symbolic_ref", "--quiet", "--no-recurse", "--", path)
184192
cls._get_validated_ref_path(repo, target)

‎test/test_gix_backend.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import pytest
1111

12-
from git import Actor, Commit, Git, Reference, Repo, _backend
12+
from git import Actor, Commit, Git, Reference, Repo, SymbolicReference, _backend
1313
from git.exc import GitCommandError, InvalidGitRepositoryError
1414
from gitdb import IStream
1515

@@ -254,6 +254,26 @@ def test_native_tree_and_commit_bytes_match_cli(repo):
254254
assert commit.replace(message="a changed message").hexsha == native.hexsha
255255

256256

257+
def test_native_reference_reads_include_missing_and_dangling_targets(repo):
258+
repo.index.commit("initial", skip_hooks=True)
259+
repo.git.pack_refs("--all", "--prune")
260+
repo.git.symbolic_ref("refs/heads/alias", "refs/heads/absent")
261+
paths = ["HEAD", "refs/heads/main", "refs/heads/alias"]
262+
with patch.object(_backend, "gix", None):
263+
expected = [SymbolicReference._get_ref_info(repo, path) for path in paths]
264+
SymbolicReference._check_ref_name_valid("refs/heads/absent")
265+
with pytest.raises(ValueError) as missing:
266+
SymbolicReference._get_ref_info(repo, "refs/heads/absent")
267+
count = _backend.statistics().get(("Git.execute", "CLI process"), 0)
268+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected reference CLI call")):
269+
assert [SymbolicReference._get_ref_info(repo, path) for path in paths] == expected
270+
with pytest.raises(ValueError) as native_missing:
271+
SymbolicReference._get_ref_info(repo, "refs/heads/absent")
272+
assert str(native_missing.value) == str(missing.value)
273+
assert not Reference(repo, "refs/heads/alias").is_valid()
274+
assert _backend.statistics().get(("Git.execute", "CLI process"), 0) == count
275+
276+
257277
def test_unknown_command_and_storage_environment_use_cli(repo):
258278
before = _backend.statistics()
259279
assert repo.git._call_process_safe("status", "--porcelain") == ""

0 commit comments

Comments
 (0)