fix: keep core.worktree-derived worktree paths in the caller's path namespace#2697
Conversation
… namespace Since b1c1cce, relative `core.worktree` values from repository-owned configuration were resolved against the symlink-resolved git dir whenever that differed from the logical git dir. That is correct when the `.git` directory itself is reached through a symlink (GitoxideLabs#2052), but it also fired when merely an ancestor directory is a symlink - like `/var` on macOS, where `TMPDIR` defaults to a path inside the symlinked `/var/folders`. In that case a submodule repository opened through the symlinked path would return a canonicalized `workdir()` while `Repository::path()` and all paths the caller holds remain in the symlinked namespace, breaking prefix-based path logic such as `repo.workdir().strip_prefix(parent_worktree_dir)`. Now the symlink-resolved base is only used when resolving the relative worktree path against the logical and the resolved git dir leads to different directories on disk. If both denote the same directory, the logical path is kept, so all paths of the opened repository remain consistent with the path it was opened with.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 293865e247
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".
| .and_then(|normalized| realpath(&normalized, current_dir)) | ||
| .zip(realpath(&resolved, current_dir)) | ||
| .is_some_and(|(symlink_preserving, resolved)| symlink_preserving == resolved); |
There was a problem hiding this comment.
Preserve namespace for missing worktree targets
When core.worktree points at a checkout that does not exist yet (for example the new with-submodule-uninitialized-checkout fixture's m2, where .git/modules/m2 exists but m2 was removed), this realpath call returns None, making denotes_same_directory false. That sends the code to resolved, so a repo opened through a symlinked ancestor still reports a canonicalized workdir even though the two candidate paths only differ by that ancestor symlink; prefix logic against the caller's worktree path then fails for deinitialized/unchecked-out submodules.
Useful? React with 👍 / 👎.
Hey, discovered a problem in GitButler repo that resolves with this patch. If it immediately makes sense to you, great, and if not, I'll tell you more shortly.
What's wrong (AI)
Since b1c1cce (#2671, fixing #2052), a relative
core.worktreefrom repo-owned config gets resolved against the symlink-resolved git dir wheneverrealpath(git_dir)differs from the logical git dir. Makes sense when.gititself is a symlink. But it also fires when merely some ancestor is a symlink — e.g. macOS, whereTMPDIRlives under the symlinked/var/folders.The fallout: open a submodule via
Submodule::open()through such a path andworkdir()comes back canonicalized (/private/var/…) whilepath()and everything else stays in the namespace you opened with (/var/…). So they no longer relate:Found this downstream in GitButler bumping gix 0.80 → 0.85 — 4 macOS test failures, all
prefix not found.What this does
Only fall back to the symlink-resolved git dir when it actually changes where the relative worktree lands on disk (the #2052 case, where
..diverges between the symlink and its target). If both resolve to the same directory, keep the symlink-preserving path so all of the repo's paths stay consistent with what you opened it with. Both #2052 tests still pass.Worth calling out: Git does canonicalize the worktree it reports (
rev-parse --show-toplevelgoes throughgetcwd), so this isn't about matching Git — it's about gix's ownpath()/workdir()staying in one namespace, which is the existing behavior everywhere except this one spot.Tests
Two regression tests, each builds its own
link -> fixturesymlink so it runs anywhere (not relying on macOS'sTMPDIR), both fail before the change:core.worktreevia the existingrelative-worktreefixture through a symlinked ancestorwith-submodule-uninitialized-checkoutfixture — module clone present, checkout not yet there (exactly the state just before a fresh checkout; with a checkout present the git dir comes from the worktree's.gitfile and this path isn't hit)Verified downstream too: with gix patched to this branch, GitButler's failing
but-workspacetests go green (4 failed → 21 passed).Alternative you might prefer
If you'd rather gix canonicalize everything for consistency instead of preserving the caller's namespace, that's a bigger call with more blast radius — happy to go that way instead if you think it's more correct.