Skip to content

fix(git): resolve repo_path through the separate-git-dir backlink - #4236

Merged
max-sixty merged 3 commits into
mainfrom
fix/issue-4235
Sep 22, 2026
Merged

max-sixty merged 3 commits into
mainfrom
fix/issue-4235

Conversation

@worktrunk-bot

@worktrunk-bot worktrunk-bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

repo_path() resolved a non-bare repository to parent(git_common_dir), which is the work tree only when the git directory sits inside it. With git init --separate-git-dir the 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 and wt list rendered the main worktree's path as the store. repo_path() now consults the gitdir backlink git keeps next to the common dir, so both surfaces name the work tree.

wt remove failed 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 on fatal: 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 list through the correction in list_worktrees that already existed for submodules: git's get_main_worktree() derives its main-worktree entry by stripping a trailing /.git from the common dir, and a separate git dir has no such suffix, so git reports the store there too. That correction defers to repo_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 under worktree.useRelativePaths and resolves against the directory the file sits in. So it is read through the existing registration_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_at helper. A backlink left by a work tree that has since moved or been replaced falls through to the old parent(git_common_dir) answer rather than to a path that no longer holds the repository.

What this leaves. Only git worktree repair writes the backlink — git init --separate-git-dir and git clone --separate-git-dir leave the store without one, and a repository that has never been repaired still records its work tree nowhere for wt to find. Running git worktree repair from 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 from wt's own discovery path, but only when wt runs in the main work tree, so using it would make {{ repo_path }} depend on where the command was invoked. The alternatives — having wt write the backlink itself, or caching the work tree in git config the way worktrunk.default-branch is — both put a write on a read path.

Reproduction and verification

The four tests added to src/git/repository/tests.rs build the layout with git init --separate-git-dir plus git worktree repair. Both repo_path and list_worktrees tests fail before the change with .../store against 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 sets worktree.useRelativePaths before the repair, so git writes ../../work/.git, and pins that repo_path still 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:

# before `git worktree repair` — unchanged, the gap described above
{"action":"created","branch":"probe","path":".../e2e/store.probe",...}
# after `git worktree repair`
{"action":"created","branch":"probe2","path":".../e2e/work.probe2",...}

wt list then renders the main worktree as . rather than the store.

One adjacent surface this PR does not touch: try_repo_at in src/git/recover.rs admits a directory only when its .git is 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

@worktrunk-bot worktrunk-bot added the automated-fix Automated CI fix label Sep 22, 2026
`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 worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-dir and git clone --separate-git-dir both leave the store with no gitdir entry; git worktree repair run from the main work tree writes it, and run from a linked worktree does not.
  • A normal repository's .git and a submodule's .git/modules/<name> hold no gitdir file 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_tree call site (each reporting the store's parent), and the stale-backlink test fails if the round-trip comparison is replaced with Some(work_tree).

@max-sixty
max-sixty merged commit a56e6d9 into main Sep 22, 2026
38 checks passed
@max-sixty
max-sixty deleted the fix/issue-4235 branch September 22, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated-fix Automated CI fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants