Skip to content

Commit 6835bb2

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 a8af8ab commit 6835bb2

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
@@ -131,7 +131,7 @@ The historical full-suite counts above predate this correction. A constant
131131
| `Git.get_object_header`, ODB `info` | Object resolution and native header lookup | Custom storage, unsupported revision grammar |
132132
| `Git.stream_object_data`, ODB `stream` | Object bytes in an independent stream | Objects larger than 8 MiB; GIX-2 |
133133
| `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 |
134-
| `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 |
134+
| `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 |
135135
| Tree enumeration, `ls_tree` | Native tree entries with modes, names, and object IDs | Other command options |
136136
| Symbolic reference lookup, `symbolic_ref` | Nonrecursive target lookup | Mutation, missing-reference diagnostics |
137137
| Reference enumeration, `for_each_ref` | Sorted reference names and literal prefixes, preserving symbolic aliases | Dangling symbolic refs, root refs, glob patterns, other formats/options |
@@ -422,6 +422,7 @@ below use the supplied `pygix` source and its pinned Gitoxide revision.
422422
| 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. |
423423
| 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. |
424424
| 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`. |
425+
| 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. |
425426

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

‎git/_backend.py‎

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

229229

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

‎test/test_gix_backend.py‎

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

194194

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

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

0 commit comments

Comments
 (0)