Skip to content

Commit 02e30df

Browse files
codexByron
authored andcommitted
feat(gix): list worktree metadata natively
Build the managed porcelain inventory from native main/linked repositories, including branch, detached, unborn and lock metadata. Keep prunable entries on Git for its diagnostics. Support worktrees of bare main repositories using Gix's actual worktree path. `Repository.is_bare()` intentionally reflects configuration and can be true alongside a linked worktree. A record is bare only when that configuration has no native worktree. The available metadata is sufficient; no upstream classification fix or Python filesystem inference is needed. Gix's `main_repo()` can retain `worktrees/linked/../..` in its Git-directory path. Reopen that repository through Gix on a canonical input path, preserving its native open options, instead of returning repaired metadata. This helper can also handle gitfile targets. The SHA-1/SHA-256 regression compares locked, unborn linked worktree inventories from both main and linked repositories. Each supported inventory now avoids one CLI launch, including bare-main inventories that previously required one. The regression forbids CLI execution after setup and executable validation. Validation: worktree/reftable/extension selection passed with Gix first (10 passed, 1 skipped in 2.97s), then CLI (4 passed, 2 skipped in 2.47s). Ruff lint/format and `git diff --check` passed.
1 parent bd41042 commit 02e30df

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎git/_backend.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ def _repository(command: Any, env: Dict[str, Any], *, query_config: bool = False
8686
return repo
8787

8888

89+
def _canonical_repository(repo: Any) -> Any:
90+
"""Have Gix resolve repository metadata again from a canonical input path."""
91+
path = os.fspath(repo.git_dir())
92+
canonical = os.path.realpath(path)
93+
return gix.open_opts(canonical, repo.open_options()) if path != canonical else repo
94+
95+
8996
def _oid(repo: Any, ref: Any) -> Any:
9097
ref = safe_decode(ref) if isinstance(ref, bytes) else str(ref)
9198
if re.fullmatch(r"[a-fA-F0-9]{40}|[a-fA-F0-9]{64}", ref):
@@ -202,12 +209,46 @@ def _config(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
202209
raise _Unsupported("config file parsing, enumeration, or mutation (GIX-12)")
203210

204211

212+
def _worktree(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
213+
if args != ["list", "--porcelain", "-z"] or not hasattr(repo, "worktrees"):
214+
raise _Unsupported("worktree operation or feature disabled")
215+
proxies = repo.worktrees()
216+
if any(proxy.is_prunable() for proxy in proxies):
217+
raise _Unsupported("prunable worktree diagnostics")
218+
main = _canonical_repository(repo.main_repo())
219+
220+
def describe(local: Any) -> List[bytes]:
221+
workdir = local.workdir()
222+
fields = [b"worktree " + os.fsencode(workdir or local.git_dir())]
223+
# Gix's is_bare() reflects configuration, even for a linked worktree.
224+
if local.is_bare() and workdir is None:
225+
fields.append(b"bare")
226+
else:
227+
head = local.head()
228+
oid = head.id()
229+
fields.append(
230+
b"HEAD " + (str(oid).encode("ascii") if oid is not None else b"0" * local.object_hash().len_in_hex())
231+
)
232+
fields.append(b"detached" if head.is_detached() else b"branch " + head.referent_name())
233+
return fields
234+
235+
records = [b"\0".join(describe(main)) + b"\0\0"]
236+
for proxy in sorted(proxies, key=lambda proxy: os.fsencode(proxy.base())):
237+
fields = describe(proxy.into_repo())
238+
if proxy.is_locked():
239+
reason = proxy.lock_reason()
240+
fields.append(b"locked" + (b" " + reason if reason else b""))
241+
records.append(b"\0".join(fields) + b"\0\0")
242+
return b"".join(records)
243+
244+
205245
_HANDLERS: Dict[str, Callable[[Any, List[str], Dict[str, Any]], bytes]] = {
206246
"rev_parse": _rev_parse,
207247
"ls_tree": _ls_tree,
208248
"symbolic_ref": _symbolic_ref,
209249
"for_each_ref": _for_each_ref,
210250
"config": _config,
251+
"worktree": _worktree,
211252
}
212253

213254

‎test/test_gix_backend.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ def test_native_head_does_not_validate_repository_extensions(repo):
4040
Repo(repo.git_dir)
4141

4242

43+
def test_native_worktree_inventory_includes_bare_main(repo, tmp_path):
44+
with Repo.init(tmp_path / "bare", bare=True, object_format=repo.object_format) as bare:
45+
linked_path = tmp_path / "linked"
46+
bare.git.worktree("add", "--orphan", "-b", "linked", str(linked_path))
47+
bare.git.worktree("lock", "--reason", "keep", str(linked_path))
48+
with Repo(linked_path) as linked:
49+
for current in (bare, linked):
50+
with patch.object(_backend, "gix", None):
51+
expected = current.git._call_process_safe("worktree", "list", "--porcelain", "-z")
52+
with patch.object(Git, "execute", side_effect=AssertionError("unexpected CLI call")):
53+
assert current.git._call_process_safe("worktree", "list", "--porcelain", "-z") == expected
54+
55+
4356
def test_unknown_command_and_storage_environment_use_cli(repo):
4457
before = _backend.statistics()
4558
assert repo.git._call_process_safe("status", "--porcelain") == ""

0 commit comments

Comments
 (0)