Skip to content

Commit 1af8db5

Browse files
codexByron
authored andcommitted
perf(gix): discover gitfiles through canonical native repositories
Use the shared native discovery path for standalone storage-directory and gitfile helpers. Preserve native validation, storage/environment guards, and Git fallback for rejected candidates or unsupported layouts. After Gix opens a gitfile, reopen its Git directory through Gix with a canonical input path and the same strict trust/configuration options. Return the second handle's actual metadata. This resolves symlink targets and also avoids treating an arbitrary gitfile's location as the worktree. The extra native open is an accepted temporary cost; no Git storage files or worktree locations are interpreted in Python. Each supported gitfile helper now needs zero CLI launches, including symlink targets that previously needed one. `Repo` retains the reopened handle and its separate format-validation query. Failed reopening selects fallback and is never recorded as native success. Preserve native worktree classification from the preceding correction; do not restore the linked-bare fallback. Tests compare Git-directory and worktree locations for ordinary, `..` and symlink targets, verify that both opens keep the same options, and inject a failure into the second open. Discovery/worktree/submodule cases passed with Gix first: 61 other cases plus all 8 gitfile cases, 14 subtests and 2 skips across the focused runs. The matching CLI selection passed 54 cases and 14 subtests, with 3 skips, in 38.48s. Ruff lint/format and `git diff --check` passed. Direct helper-output comparisons use Git's raw path spelling, preserving Windows separators independently of `Repo`'s PathLike properties. The final path-format selection passed with Gix first (9 passed in 1.22s), then CLI (1 passed, 1 skipped).
1 parent 0e952e0 commit 1af8db5

4 files changed

Lines changed: 63 additions & 2 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ The historical full-suite counts above predate this correction. A constant
130130
| `Git.stream_object_data`, ODB `stream` | Object bytes in an independent stream | Objects larger than 8 MiB; GIX-2 |
131131
| Commit/tree serialization readback | Existing ODB stream; no separate `cat-file` process | The same storage and large-object fallbacks as ODB reads |
132132
| `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 |
133+
| 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 |
133134
| `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 |
134135
| Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options |
135136
| Reference object reads | One exact native lookup for direct, symbolic and absent references | Partial names and native decoding errors |
@@ -415,7 +416,7 @@ below use the supplied `pygix` source and its pinned Gitoxide revision.
415416
| GIX-11 | Inexact and ambiguous rename pairing/scores have not been established as Git-compatible. This is a conservative guard, not a claimed native bug. | Verified pairing, scoring, tie-breaking and option parity. Exact unambiguous renames are native. |
416417
| GIX-12 | Config bindings lack standalone parsing, ordered section/key enumeration, multivars, unset/remove-section and source-scoped queries. | Those operations for `GitConfigParser` and remote/branch configuration. Native merged getters cannot replace repository-only config readers. |
417418
| GIX-13 | Native index writing normalizes v4 to v2, offers no version setter, and expands split indexes. | Version and split-index preservation. Tests now assert that a split index remains split after an update. |
418-
| GIX-14 | Gix correctly exposes linked worktree paths, including those of bare main repositories. Its documented `is_bare()` reflects configuration and can be true alongside a worktree; opening and inventory now use both pieces of native metadata. Actual gaps remain: discovery tolerates undecodable HEADs and ignores dangling `commondir` symlinks, while Python flattens native failure kinds into `gix.Error`. | Opening forces native HEAD decoding and retains CLI validation for remaining layout/diagnostic gaps. Strict trust checks do not replace layout validation. Canonical inputs can be reopened through Gix, as already done for the inventory's main repository; no classification fix is required upstream. Do not fabricate rejection from Python `HEAD` existence checks or infer a missing worktree root. |
419+
| GIX-14 | Gix correctly exposes linked worktree paths, including those of bare main repositories. Its documented `is_bare()` reflects configuration and can be true alongside a worktree; opening and inventory use both pieces of native metadata. Gitfiles are reopened at the canonical native Git directory, which also prevents an arbitrary gitfile location from becoming the worktree. Actual gaps remain: discovery tolerates undecodable HEADs and ignores dangling `commondir` symlinks, while Python flattens native failure kinds into `gix.Error`. | Opening forces native HEAD decoding and retains CLI validation for remaining layout/diagnostic gaps. Strict trust checks do not replace layout validation. Reopening uses the same strict options and returns actual native metadata; no upstream classification fix or Python worktree inference is needed. A native canonical-opening option could avoid the extra open later. |
419420
| GIX-15 | Native blame disagrees with Git even with Myers and rewrite tracking selected. In the fixture's `README.md`, lines 150 and 158 are attributed to the opposite commits. Incremental order also differs. | Attribution and incremental-output parity before replacing `Repo.blame` / `blame_incremental`. |
420421
| GIX-16 | Native archive streaming takes a tree rather than a commit. Its TAR omits Git's global PAX commit comment, leaves `export-subst` placeholders literal, and writes ordinary modes as `0644` where Git uses `0664`. | Commit-aware export substitution, metadata and permission parity. Both engines respected `export-ignore` in the probe. |
421422
| GIX-17 | Revision parsing accepts abbreviated IDs with `-dirty`, prefers the OID suffix over an exact describe-shaped tag, and treats escaped braces in message searches differently. | Git-compatible revision grammar and regex semantics. Existing `test_rev_parse.py` cases reproduce all three. |

‎git/_backend.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ def discover_repository(path: str, environment: Dict[str, Any]) -> Any:
7070
)
7171
try:
7272
repo = gix.open_opts(path, options)
73+
if os.path.isfile(path):
74+
# Reopening also makes Gix derive the worktree from its Git directory,
75+
# rather than retaining an arbitrary gitfile's location as a worktree.
76+
repo = gix.open_opts(os.path.realpath(repo.git_dir()), options)
7377
snapshot = repo.config_snapshot()
7478
if snapshot.string("extensions.refStorage") == b"reftable":
7579
return _fallback("Repo.open", "reftable (GIX-1)")

‎git/repo/fun.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from gitdb.exc import BadName, BadObject
2727

28+
from git import _backend
2829
from git.cmd import Git
2930
from git.compat import defenc
3031
from git.exc import GitCommandError
@@ -47,11 +48,14 @@ def touch(filename: str) -> str:
4748

4849

4950
def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
50-
"""Resolve a repository directory or gitfile using Git."""
51+
"""Resolve a repository directory or gitfile using the selected backend."""
5152
path = osp.abspath(os.fspath(d))
5253
if not osp.exists(path):
5354
return None
5455
try:
56+
native = _backend.discover_repository(path, {})
57+
if native is not NotImplemented:
58+
return to_native_path_linux(os.fspath(native.git_dir()))
5559
return Git()._call_process_safe("rev_parse", "--resolve-git-dir", to_native_path_linux(path))
5660
except GitCommandError:
5761
return None

‎test/test_gix_backend.py‎

Lines changed: 52 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.repo.fun import find_submodule_git_dir, find_worktree_git_dir, is_git_dir
1415
from gitdb import IStream
1516

1617
gix = pytest.importorskip("gix")
@@ -122,6 +123,18 @@ def test_opening_and_discovery_use_gix_with_explicit_fallbacks(repo, tmp_path):
122123
bare_linked_native = gix.open(bare_linked)
123124
assert bare_linked_native.is_bare()
124125
assert bare_linked_native.workdir() == bare_linked
126+
gitfile = tmp_path / "gitfile"
127+
gitfile.write_text("gitdir: repo/.git\n")
128+
candidates = [repo.git_dir, bare.git_dir, linked / ".git", linked_admin, gitfile]
129+
with patch.object(_backend, "gix", None):
130+
expected = [find_submodule_git_dir(path) for path in candidates]
131+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call")):
132+
assert [find_submodule_git_dir(path) for path in candidates] == expected
133+
assert find_worktree_git_dir(gitfile) == expected[-1]
134+
assert is_git_dir(repo.git_dir)
135+
with patch.object(Git, "execute", autospec=True, side_effect=Git.execute) as cli:
136+
assert find_submodule_git_dir(nested) is None
137+
assert sum("--resolve-git-dir" in call.args[1] for call in cli.call_args_list) == 1
125138
for path, search, expected_bare, rejected_candidates in [
126139
(root, False, False, 0),
127140
(repo.git_dir, False, False, 0),
@@ -165,6 +178,45 @@ def test_discovery_asks_gix_before_falling_back(tmp_path):
165178
native_open.assert_called_once()
166179

167180

181+
@pytest.mark.parametrize("target", ["repo/.git", "repo/../repo/.git", "alias/.git"])
182+
def test_gitfile_reopens_canonical_native_repository(repo, tmp_path, target):
183+
if target == "alias/.git":
184+
try:
185+
(tmp_path / "alias").symlink_to(repo.working_dir, target_is_directory=True)
186+
except OSError as exc:
187+
pytest.skip(f"symlinks unavailable: {exc}")
188+
gitfile = tmp_path / "gitfile"
189+
gitfile.write_text(f"gitdir: {target}\n")
190+
with patch.object(_backend, "gix", None), Repo(gitfile) as control:
191+
expected = (control.git_dir, control.working_tree_dir, control.bare)
192+
expected_git_path = repo.git.rev_parse("--resolve-git-dir", str(gitfile))
193+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call")):
194+
with patch.object(gix, "open_opts", wraps=gix.open_opts) as native_open:
195+
native = _backend.discover_repository(str(gitfile), {})
196+
assert (str(native.git_dir()), str(native.workdir()), native.is_bare()) == expected
197+
assert native_open.call_count == 2
198+
assert native_open.call_args.args[0] == expected[0]
199+
assert native_open.call_args.args[1] is native_open.call_args_list[0].args[1]
200+
assert find_submodule_git_dir(gitfile) == expected_git_path
201+
assert find_worktree_git_dir(gitfile) == expected_git_path
202+
with patch.object(Git, "execute", autospec=True, side_effect=Git.execute) as cli:
203+
with Repo(gitfile) as opened:
204+
assert (opened.git_dir, str(opened.working_tree_dir), opened.bare) == expected
205+
assert isinstance(opened._gix_repository, gix.Repository)
206+
assert all("version" in call.args[1] or "--show-ref-format" in call.args[1] for call in cli.call_args_list)
207+
208+
209+
def test_gitfile_reopen_failure_selects_fallback(repo, tmp_path):
210+
gitfile = tmp_path / "gitfile"
211+
gitfile.write_text("gitdir: repo/.git\n")
212+
outcomes = [gix.open(repo.git_dir), gix.Error("canonical reopen failed")]
213+
before = _backend.statistics().get(("Repo.open", "native"), 0)
214+
with patch.object(gix, "open_opts", side_effect=outcomes) as native_open:
215+
assert _backend.discover_repository(str(gitfile), {}) is NotImplemented
216+
assert native_open.call_count == 2
217+
assert _backend.statistics().get(("Repo.open", "native"), 0) == before
218+
219+
168220
def test_native_objects_trees_commits_and_index_reads(repo):
169221
actor = Actor("Example", "example@example.invalid")
170222
paths = ["file", "dir/with space", "dir/unicode-é", "--option"]

0 commit comments

Comments
 (0)