Repository navigation
fix(git): resolve repo_path through the separate-git-dir backlink - #4236
Conversation
…er the relative-backlink reject
`worktree.useRelativePaths` makes git write the main worktree's backlink relative to the store (`../../work/.git`) and resolve it against the directory the file sits in, exactly as it does for a linked worktree's registration. Declining a non-absolute backlink left #4235 unfixed for anyone with that setting on, and the test added alongside it pinned the reject rather than the behaviour. `registration_worktree_path` already implements git's rule for the same file format, so read the backlink through it instead of re-deriving the parse.
worktrunk-bot
left a comment
There was a problem hiding this comment.
The resolution only fires once git worktree repair has written <store>/gitdir. Against git 2.55, neither git init --separate-git-dir nor git clone --separate-git-dir writes one, so the layout as anyone actually creates it still resolves to the store's parent and #4235's reported symptom is unchanged for a fresh repository. The body names that gap; what I'd put on the record is a fourth option it doesn't weigh.
The three alternatives listed all try to resolve the work tree without the backlink, and each pays for it — invocation-dependence, or a write on a read path. Detecting the case costs neither, and the predicate is the confirmation this PR already ships, pointed the other way: where parent(git_common_dir) holds no .git that resolves back to git_common_dir, the fallback answer is known-wrong rather than merely unverified. wt could name git worktree repair there instead of silently placing the worktree beside the store.
Two reasons I'd rather have a decision on that than push it as a follow-up. A submodule that reaches this same fallback (core.worktree set, but the rev-parse --show-toplevel probe failed) has .git/modules as its parent and trips the predicate too, so the message has to read sanely on a false positive. And repo_path() is on the wt list read path, so where a hint surfaces and how it dedups is a product decision under the CLI-surface bar in CLAUDE.md, not a mechanical one.
None of that blocks the change itself, which resolves correctly and distrusts the one-way link by the right amount.
What I verified against git 2.55
Relevant to the option above, and to anyone picking it up later:
git init --separate-git-dirandgit clone --separate-git-dirboth leave the store with nogitdirentry;git worktree repairrun from the main work tree writes it, and run from a linked worktree does not.- A normal repository's
.gitand a submodule's.git/modules/<name>hold nogitdirfile at all, so the new branch has nothing to misfire on there even before the round trip rejects it. - The three new positive tests fail without the
separate_git_dir_work_treecall site (each reporting the store's parent), and the stale-backlink test fails if the round-trip comparison is replaced withSome(work_tree).
repo_path()resolved a non-bare repository toparent(git_common_dir), which is the work tree only when the git directory sits inside it. Withgit init --separate-git-dirthe git directory is a store elsewhere and that parent names the store's neighbourhood instead, so{{ repo_path }}.{{ branch }}placed every new worktree beside the store andwt listrendered the main worktree's path as the store.repo_path()now consults thegitdirbacklink git keeps next to the common dir, so both surfaces name the work tree.wt removefailed outright in this layout rather than misplacing anything — it runs git from the resolved repo path, and the store's parent is not a repository, so the command died onfatal: not a git repository. That is the same root cause and is fixed here too; the issue reports it as a second symptom.The same wrong path reached
wt listthrough the correction inlist_worktreesthat already existed for submodules: git'sget_main_worktree()derives its main-worktree entry by stripping a trailing/.gitfrom the common dir, and a separate git dir has no such suffix, so git reports the store there too. That correction defers torepo_path(), so this one change fixes both.The backlink is the same file format as a linked worktree's
.git/worktrees/<name>/gitdir, including the relative form git writes underworktree.useRelativePathsand resolves against the directory the file sits in. So it is read through the existingregistration_worktree_path, which already implements that rule, rather than through a second parse — a re-derived rule here would be correct on the absolute form and silently wrong for anyone with that setting on.The backlink is one-way, so it is confirmed rather than trusted: the work tree it names must point back at this common dir, via the existing
git_dir_athelper. A backlink left by a work tree that has since moved or been replaced falls through to the oldparent(git_common_dir)answer rather than to a path that no longer holds the repository.What this leaves. Only
git worktree repairwrites the backlink —git init --separate-git-dirandgit clone --separate-git-dirleave the store without one, and a repository that has never been repaired still records its work tree nowhere forwtto find. Runninggit worktree repairfrom the work tree is now an effective remedy where before it changed nothing, but a freshly created repository of this shape still resolves to the store's parent until it is run. Closing that gap needs a decision this PR doesn't take: the work tree is derivable fromwt's own discovery path, but only whenwtruns in the main work tree, so using it would make{{ repo_path }}depend on where the command was invoked. The alternatives — havingwtwrite the backlink itself, or caching the work tree in git config the wayworktrunk.default-branchis — both put a write on a read path.Reproduction and verification
The four tests added to
src/git/repository/tests.rsbuild the layout withgit init --separate-git-dirplusgit worktree repair. Bothrepo_pathandlist_worktreestests fail before the change with.../storeagainst an expected.../work. A third plants an unrelated repository where the work tree used to be and pins the fall-through — it fails if the round-trip check is dropped. The fourth setsworktree.useRelativePathsbefore the repair, so git writes../../work/.git, and pins thatrepo_pathstill names the work tree; it accepts the absolute form too, since git honors the setting only from 2.48 and worktrunk supports 2.43.End to end with the built binary, matching the issue's repro:
wt listthen renders the main worktree as.rather than the store.One adjacent surface this PR does not touch:
try_repo_atinsrc/git/recover.rsadmits a directory only when its.gitis a directory, which is never true of a separate-git-dir work tree. Worth a look separately; no user-visible symptom verified here.Refs #4235 — automated triage