Skip to content

Commit 47afff2

Browse files
codexByron
authored andcommitted
perf(gix): resolve fixed metadata paths from the native Git directory
GitPython asks `--git-path` for `modules` and `COMMIT_EDITMSG`. Git's `path.c` keeps these fixed leaves in the private Git directory, including linked worktrees; the location is already available from Gix. Resolve the repository's canonical Git directory through Gix and append only those two leaf names. Use the existing `to_native_path_linux()` formatter for Git's forward-slash output on Windows. This is the native-location adaptation permitted by `AGENTS.md`. Keep arbitrary metadata names on Git, since objects, indexes, hooks and other paths have separate configuration/environment or common-directory rules. Symlinked metadata leaves also retain CLI canonicalization, including dangling targets. Do not implement the general Git path resolver in Python. Each ordinary fixed-path lookup drops from one CLI launch to zero. Regression tests forbid launches for both names across normal, bare and linked worktrees, including a bare main repository, and compare all returned paths with Git. Additional cases require CLI for metadata symlinks and an unconverted path. Validation: affected metadata/submodule/commit-message tests passed with Gix first (87 passed in 27.87s), plus 2 fallback cases in 0.16s, then CLI (81 passed, 1 skipped in 62.10s). After applying the shared path formatter, the focused checks passed again with Gix first (9 passed in 1.58s), then CLI (3 passed, 1 skipped in 0.94s). Ruff lint/format and `git diff --check` passed. The ledger now identifies only the remaining general-resolution requirements.
1 parent 6165116 commit 47afff2

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ The historical full-suite counts above predate this correction. A constant
129129
| `Git.get_object_header`, ODB `info` | Object resolution and native header lookup | Custom storage, unsupported revision grammar |
130130
| `Git.stream_object_data`, ODB `stream` | Object bytes in an independent stream | Objects larger than 8 MiB; GIX-2 |
131131
| `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 |
132-
| `Repo.rev_parse`, `rev_parse` metadata queries | Revision lookup, common directory, object format, bare flag, worktree root | Repository-format validation, message searches and describe forms; GIX-1/14/17 |
132+
| `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 |
133133
| Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options |
134134
| Symbolic reference lookup, `symbolic_ref` | Nonrecursive target lookup | Mutation, missing-reference diagnostics |
135135
| Reference enumeration, `for_each_ref` | Sorted reference names and literal prefixes, preserving symbolic aliases | Dangling symbolic refs, root refs, glob patterns, other formats/options |
@@ -419,6 +419,7 @@ below use the supplied `pygix` source and its pinned Gitoxide revision.
419419
| GIX-18 | The tree-diff resource cache reads attributes from the index; Git also consults worktree attributes. Native binary classification with textconv configured can also differ even in `to_git` mode. | Independent attribute-source control and `--no-textconv` parity. Statistics currently fall back for any effective `diff` attribute. |
420420
| GIX-19 | GixPython opens repositories with `extensions.compatObjectFormat`, but translation and object-write compatibility have not been validated. The installed Git reports `compatibility hash algorithm support requires Rust` when asked to translate an ID. This is an unvalidated capability guard, not evidence of missing native object mappings. | Verify compatibility-format reads, writes and OID translation against a Git build that supports them. All operations use CLI in the meantime. |
421421
| GIX-21 | Native snapshot overrides for `committer.name`, `committer.email` and `gitoxide.commit.committerDate` resolve correctly. Applying them to the retained repository would expose command-specific identity to other threads. GixPython 0.1.0 has no general independent repository clone: Python `copy`/`deepcopy` fail, while the binding's internal `RepoHandle::clone()` shares its `Arc` state. | Bind a cheap independent native clone with isolated config so native identity resolution can apply per-command overrides, including removal semantics. Reopening isolates state but repeats setup; `with_object_memory()` also changes object-write behavior and is not a general clone API. Keep native process/config identity and CLI for differing command overrides. Do not mutate process environment/shared snapshots or resolve identity fields in Python. Explicit canonical commit inputs may still be adapted to `gix.Signature`. |
422+
| GIX-22 | The native Git directory is sufficient for the two fixed metadata leaves used here: `modules` and `COMMIT_EDITMSG`. Git's `path.c` applies no special relocation to either, including in linked worktrees. The adapter appends only these names to the canonical native Git directory. | No new binding is needed for these ordinary paths. Other `--git-path` names and symlinked metadata leaves retain CLI resolution; a general resolver must handle config/environment overrides, common/private storage and canonical targets. `Repository.modules_path()` means `.gitmodules`, not the `modules` storage directory. |
422423

423424
For GIX-8, at fixture commit `44e0a8ec55c42559dfcdf5117710b26261a7c937`,
424425
compare `git rev-list HEAD` with

‎git/_backend.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ def object_data(command: Any, ref: bytes, *, stream: bool = False) -> Any:
224224

225225

226226
def _rev_parse(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
227+
if args[:2] == ["--path-format=absolute", "--git-path"]:
228+
from git.util import to_native_path_linux
229+
230+
if len(args) != 3 or args[2] not in ("modules", "COMMIT_EDITMSG"):
231+
raise _Unsupported("Git metadata path resolution (GIX-22)")
232+
# Git keeps these fixed leaves in the private Git directory, including
233+
# linked worktrees. Other names have their own config/environment rules.
234+
path = os.path.join(_canonical_repository(repo).git_dir(), args[2])
235+
if os.path.islink(path):
236+
raise _Unsupported("symlinked metadata path (GIX-22)")
237+
return os.fsencode(to_native_path_linux(path)) + b"\n"
227238
if args == ["--path-format=absolute", "--git-common-dir"]:
228239
return os.fsencode(os.path.abspath(repo.common_dir())) + b"\n"
229240
if args == ["--is-bare-repository"]:

‎test/test_gix_backend.py‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,44 @@ def test_native_objects_trees_commits_and_index_reads(repo):
191191
assert commit.replace(message="changed").message == "changed"
192192

193193

194+
def test_native_metadata_paths_match_git_in_each_worktree(repo, tmp_path):
195+
repo.index.commit("initial", skip_hooks=True)
196+
linked_path = tmp_path / "linked"
197+
repo.git.worktree("add", "--detach", str(linked_path))
198+
with Repo(linked_path) as linked, Repo.init(tmp_path / "bare", bare=True, object_format=repo.object_format) as bare:
199+
bare_linked_path = tmp_path / "bare-linked"
200+
bare.git.worktree("add", "--orphan", "-b", "main", str(bare_linked_path))
201+
with Repo(bare_linked_path) as bare_linked:
202+
for current in (repo, linked, bare, bare_linked):
203+
queries = [
204+
("rev_parse", "--path-format=absolute", "--git-path", name)
205+
for name in ("modules", "COMMIT_EDITMSG")
206+
]
207+
with patch.object(_backend, "gix", None):
208+
expected = [current.git._call_process_safe(*query) for query in queries]
209+
count = _backend.statistics().get(("Git.execute", "CLI process"), 0)
210+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call")):
211+
assert [current.git._call_process_safe(*query) for query in queries] == expected
212+
assert _backend.statistics().get(("Git.execute", "CLI process"), 0) == count
213+
assert Path(linked.git_dir, "modules") != Path(repo.common_dir, "modules")
214+
215+
216+
def test_metadata_symlinks_retain_git_canonicalization(repo, tmp_path):
217+
for name in ("modules", "COMMIT_EDITMSG"):
218+
target = tmp_path / name
219+
try:
220+
Path(repo.git_dir, name).symlink_to(target, target_is_directory=name == "modules")
221+
except OSError as exc:
222+
pytest.skip(f"symlinks unavailable: {exc}")
223+
query = ("rev_parse", "--path-format=absolute", "--git-path", name)
224+
with patch.object(_backend, "gix", None):
225+
expected = repo.git._call_process_safe(*query)
226+
assert Path(expected) == target
227+
with patch.object(Git, "execute", autospec=True, side_effect=Git.execute) as cli:
228+
assert repo.git._call_process_safe(*query) == expected
229+
cli.assert_called_once()
230+
231+
194232
def test_native_tree_and_commit_bytes_match_cli(repo):
195233
actor = Actor("Example", "example@example.invalid")
196234
Path(repo.working_dir, "file").write_bytes(b"payload\n")
@@ -217,7 +255,8 @@ def test_unknown_command_and_storage_environment_use_cli(repo):
217255
with repo.git.custom_environment(GIT_INDEX_FILE=".git/index"):
218256
assert repo.git._call_process_safe("ls_files", "--stage", "-v", "-z", "--full-name") == "fallback"
219257
assert repo.git._call_process_safe("rev_parse", "--show-ref-format") == "fallback"
220-
assert cli.call_count == 3
258+
assert repo.git._call_process_safe("rev_parse", "--path-format=absolute", "--git-path", "objects") == "fallback"
259+
assert cli.call_count == 4
221260

222261
command = Git(repo.working_dir)
223262
command._repo = repo.git._repo

0 commit comments

Comments
 (0)