Skip to content

Commit 95cf3e5

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 a7d813e commit 95cf3e5

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
@@ -90,6 +90,13 @@ def _repository(command: Any, env: Dict[str, Any], *, query_config: bool = False
9090
return repo
9191

9292

93+
def _canonical_repository(repo: Any) -> Any:
94+
"""Have Gix resolve repository metadata again from a canonical input path."""
95+
path = os.fspath(repo.git_dir())
96+
canonical = os.path.realpath(path)
97+
return gix.open_opts(canonical, repo.open_options()) if path != canonical else repo
98+
99+
93100
def _oid(repo: Any, ref: Any) -> Any:
94101
ref = safe_decode(ref) if isinstance(ref, bytes) else str(ref)
95102
if re.fullmatch(r"[a-fA-F0-9]{40}|[a-fA-F0-9]{64}", ref):
@@ -206,12 +213,46 @@ def _config(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
206213
raise _Unsupported("config file parsing, enumeration, or mutation (GIX-12)")
207214

208215

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

217258

‎test/test_gix_backend.py‎

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

4343

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

0 commit comments

Comments
 (0)