Skip to content

fix: keep core.worktree-derived worktree paths in the caller's path namespace#2697

Draft
Mattias Granlund (mtsgrd) wants to merge 1 commit into
GitoxideLabs:mainfrom
mtsgrd:fix/keep-worktree-path-namespace
Draft

fix: keep core.worktree-derived worktree paths in the caller's path namespace#2697
Mattias Granlund (mtsgrd) wants to merge 1 commit into
GitoxideLabs:mainfrom
mtsgrd:fix/keep-worktree-path-namespace

Conversation

@mtsgrd

@mtsgrd Mattias Granlund (mtsgrd) commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.worktree from repo-owned config gets resolved against the symlink-resolved git dir whenever realpath(git_dir) differs from the logical git dir. Makes sense when .git itself is a symlink. But it also fires when merely some ancestor is a symlink — e.g. macOS, where TMPDIR lives under the symlinked /var/folders.

The fallout: open a submodule via Submodule::open() through such a path and workdir() comes back canonicalized (/private/var/…) while path() and everything else stays in the namespace you opened with (/var/…). So they no longer relate:

let repo = submodule.open()?.expect("exists");
repo.path().strip_prefix(parent)     // ok:  /var/folders/…
repo.workdir()?.strip_prefix(parent) // Err: /private/var/folders/…

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-toplevel goes through getcwd), so this isn't about matching Git — it's about gix's own path()/workdir() staying in one namespace, which is the existing behavior everywhere except this one spot.

Tests

Two regression tests, each builds its own link -> fixture symlink so it runs anywhere (not relying on macOS's TMPDIR), both fail before the change:

  • direct core.worktree via the existing relative-worktree fixture through a symlinked ancestor
  • submodule via a new with-submodule-uninitialized-checkout fixture — 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 .git file and this path isn't hit)

Verified downstream too: with gix patched to this branch, GitButler's failing but-workspace tests 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.

… 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +550 to +552
.and_then(|normalized| realpath(&normalized, current_dir))
.zip(realpath(&resolved, current_dir))
.is_some_and(|(symlink_preserving, resolved)| symlink_preserving == resolved);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@mtsgrd Mattias Granlund (mtsgrd) marked this pull request as draft July 6, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant