Skip to content

fix(switch): decide branch tracking for --create, whatever the git config says - #3950

Merged
max-sixty merged 5 commits into
mainfrom
switch-explicit-track-flags
Aug 29, 2026
Merged

fix(switch): decide branch tracking for --create, whatever the git config says#3950
max-sixty merged 5 commits into
mainfrom
switch-explicit-track-flags

Conversation

@max-sixty

@max-sixty max-sixty commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Problem

wt switch --create <name> --base origin/<branch> must not leave the new branch tracking a differently-named base: under push.default = upstream a bare git push then pushes the new work onto the base branch (#713).

Since #3913 that came from -c branch.autoSetupMerge=simple, injected only when the user had not set the key. So the outcome still depended on the user's git config, and two of git's five values reach a wrong answer: true and always track a differently-named base (the #713 footgun is still live), while false and inherit deny a same-named branch the tracking that is the point of it — including the DWIM wt switch feature from origin/feature, which the docs promise is a tracking branch.

Solution

Force the -c instead of defaulting it, on every git worktree add wt switch runs. -c outranks every config file, so one rule now decides the upstream whatever the user has configured: a new branch tracks the remote branch it starts from only when the two share a name. DWIM always shares it, so it always tracks; --create from a differently-named base gets no upstream.

Tests: test_switch_create_from_remote_base_upstream becomes a matrix over all six branch.autoSetupMerge values (unset, simple, false, inherit, true, always) × three base spellings (origin/release, the bare release that resolves to it, and refs/remotes/origin/staging); test_switch_dwim_from_remote_tracks pins the DWIM half over the values that used to decline.

Why not --track / --no-track

The obvious alternative is for wt to pick git's explicit flags from a name comparison of its own. I built that first; it passed the full suite, and it is wrong twice over.

Three reproduced defects in the explicit-flags version

The predicate is wrong. strip_remote_prefix splits <remote>/<branch> at the first slash, but git maps a remote-tracking ref back to its branch through the fetch refspec. With a remote named team/fork, the two disagree and the verdict inverts both ways:

$ git remote add team/fork <url> && git fetch team/fork
$ wt switch --create fork/release --base team/fork/release
$ git config branch.fork/release.merge
refs/heads/release

A bare git push under push.default = upstream then lands on release#713, reintroduced. The same base with --create release got no upstream, the opposite error. A refspec renaming into a sub-namespace (+refs/heads/*:refs/remotes/origin/mirror/*) does the same with no unusual remote name.

--track is a hard demand where simple is best-effort. In a single-branch clone holding a hand-fetched ref, it fails the whole command — after the branch name has already been taken:

$ git clone --single-branch -b main <url> && git fetch origin release:refs/remotes/origin/release
$ wt switch --create release --base origin/release
fatal: cannot set up tracking information; starting point 'origin/release' is not a branch

That is the matching-name case, i.e. exactly what the feature exists for, and it is the same "fails outright" class #3913 had just removed.

Qualified spellings lose tracking. --base refs/remotes/origin/release and --base remotes/origin/release name the same ref as origin/release but don't match short_name, so they got --no-track.

All three come from wt computing the name match itself, which it cannot do correctly — only git knows the refspec mapping. So wt decides the rule and git applies it. The first two are now regression tests (test_switch_create_base_on_remote_with_slash, test_switch_create_base_outside_fetch_refspec), so a future simplification to --track fails the suite rather than shipping.

Verification

Measured end-to-end on scratch clones across all six branch.autoSetupMerge values: a differing name gets no upstream and a matching name tracks, identically in every one.

This was written by Claude Code on behalf of max-sixty

max-sixty and others added 2 commits August 28, 2026 16:04
…nfig says

`wt switch --create <name> --base origin/<branch>` must not leave the new
branch tracking a differently-named base: under `push.default = upstream` a
bare `git push` would then push the new work onto the base (#713). It got
that from `-c branch.autoSetupMerge=simple`, injected only when the user had
not set the key — so `true` and `always` still reached the footgun, and
`false` and `inherit` denied a same-named branch the tracking that is the
point of it.

Force the `-c` instead. It outranks every config file, so `--create`
produces the same tracking on every machine.

Explicit `--track` / `--no-track` chosen from a name comparison was the
other candidate, and it is wrong twice over. `strip_remote_prefix` splits
`<remote>/<branch>` at the first slash, but git maps a remote-tracking ref
back to its branch through the fetch refspec: with a remote named
`team/fork`, `--create fork/release --base team/fork/release` got
`merge = refs/heads/release`, reintroducing #713, while `--create release`
from the same base got no upstream at all. And `--track` is a hard demand
where `simple` is best-effort — in a single-branch clone holding a
hand-fetched ref it failed the whole command with "starting point
'origin/release' is not a branch", after the branch name was taken. Both are
now regression tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBxMUZo7awheuY85PNXCjE

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The -c approach holds up, and the refspec argument against --track/--no-track is right — strip_remote_prefix genuinely can't reproduce git's refspec mapping, and the two regression tests pin that so a later "simplification" fails the suite. Two observations, neither blocking.

The forced -c isn't scoped to remote bases, but the documentation is. if *create_branch fires on every --create, local base included, so branch.autoSetupMerge = always and inherit change behaviour there too: wt switch --create feature-2 --base some-local-branch previously tracked the local base under always, or copied its upstream under inherit, and now gets neither. The inherit case looks like a genuine extra win — inheriting origin/feature onto feature-2 is the same #713 shape, a bare push under push.default = upstream landing on feature — but the help text opens with "--create from a remote base is the exception" and never mentions a local base, so a user who set always deliberately has no way to learn from --help why their new branch stopped tracking. A clause covering it would make after_long_help describe what the code now does.

--create and the DWIM path now disagree under false. The comment justifies excluding DWIM because "the names match and simple and true agree" — true of true, but not of false or inherit, which are exactly the values this PR otherwise stopped trusting. wt switch feature where only origin/feature exists runs git worktree add -b feature -- <path> origin/feature (the single-remote branch a few lines below), so under branch.autoSetupMerge = false it produces a branch with no upstream, while wt switch --create feature --base origin/feature now always sets one. Those are the same logical operation, and the sentence immediately above the changed paragraph — "Switching to a remote branch (e.g., wt switch feature when only origin/feature exists) creates a local tracking branch" — states it unconditionally. Forcing the same -c on that path, or saying why it's left to config, would close it.

One inline nit on the test setup comment.

Comment thread tests/integration_tests/switch.rs Outdated
Review feedback on #3950: the forced `-c` was scoped to `--create` while the
help text described one rule, and the DWIM path was left on the user's
`branch.autoSetupMerge`. Under `false` or `inherit`, `wt switch feature` from
`origin/feature` then produced an untracked branch, though the docs promise a
tracking one — and the same values `--create` had just stopped trusting.

Drop the condition. One rule now decides every upstream `wt switch` sets: a
new branch tracks the remote branch it starts from only when the two share a
name. DWIM always shares it; `--create` from a differently-named base does
not. The help text says that once instead of naming `--create` an exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBxMUZo7awheuY85PNXCjE

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The -c forcing is the right shape, and I checked the git semantics it rests on against git 2.55 in scratch clones rather than taking the PR body's word for it: simple tracks a same-named base and declines a differently-named one identically whatever branch.autoSetupMerge says, and it declines a base the fetch refspec doesn't map instead of failing the command — so the --track alternative really is worse, and the two regression tests are pinning something real. The earlier observations are both addressed.

Four non-blocking suggestions, all comment/test polish. The substantive one: the two new comments both assume git worktree add can't resolve a bare remote-only branch name to its remote-tracking ref. It can — that resolution is worktree add's own rather than rev-parse's, which is why git rev-parse --verify release^{commit} fails on a remote-only release (making resolve_base_ref's ref_exists branch fire) while git -c branch.autoSetupMerge=simple worktree add -b release -- <path> release prints branch 'release' set up to track 'origin/release'. It bites on the base_pr_upstream comment, which describes the one path that reaches worktree add with a bare name: wt switch --create <PR's own head name> --base pr:N does get tracking from simple, and the explicit set_config then rewrites the same two values. The outcome is unchanged either way — the explicit config is still load-bearing for the differing-name case, which is what --base pr:N exists for — but the mechanism as written isn't what happens.

Verification transcript (git 2.55)
$ # remote-only `release`, no local branch
$ git for-each-ref --format='%(refname)' refs/remotes
refs/remotes/origin/main
refs/remotes/origin/release

$ git rev-parse --verify --end-of-options 'release^{commit}'
fatal: Needed a single revision            # -> ref_exists() == false, so resolve_base_ref qualifies

$ git -c branch.autoSetupMerge=simple worktree add -b release -- /tmp/w release
Preparing worktree (new branch 'release')
branch 'release' set up to track 'origin/release'.   # bare name, still read as a remote branch

$ git -c branch.autoSetupMerge=simple worktree add -b mywork -- /tmp/w1 release
$ git config branch.mywork.remote        # differing name: nothing, as intended

And the refspec case the new regression test covers, confirming simple declines rather than fails:

$ git config remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'
$ git fetch origin refs/heads/release:refs/remotes/origin/release
$ git -c branch.autoSetupMerge=simple worktree add -b release -- /tmp/w origin/release
Preparing worktree (new branch 'release')
HEAD is now at 134b220 init               # no "set up to track" line
$ git rev-parse --abbrev-ref 'release@{upstream}'
fatal: no upstream configured for branch 'release'

Comment thread src/commands/worktree/types.rs Outdated
Comment thread src/commands/worktree/switch.rs Outdated
Comment thread tests/integration_tests/switch.rs
Comment thread tests/integration_tests/switch.rs
…t simple declines

Review polish on #3950. Both docstrings asserted that a bare remote-only branch
name is invisible to `git worktree add`. It isn't: `worktree add` DWIMs the
bare form even where `rev-parse` won't resolve it — with `-b` given, git's DWIM
discards it and creates the same-named branch instead. So the remote
qualification `resolve_base_ref` applies is load-bearing for
`resolve_switch_target`'s validation, not for what the creation can see, and
`base_pr_upstream` exists because the PR's source branch may carry any name,
not because the name is unreadable.

Also assert what `test_switch_create_base_outside_fetch_refspec` claims — that
`simple` declines the tracking rather than guessing — and scope the DWIM
matrix's docstring to the values it enumerates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBxMUZo7awheuY85PNXCjE

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One correction on the docstring rewrite — non-blocking, and partly my own to make.

resolve_base_ref's new conclusion inverts the one the commit message reached. The docstring now ends "so the qualification is load-bearing for the validation, not for the creation." It is load-bearing for both, and more sharply for the creation: git worktree add -b <name> -- <path> <bare-remote-only-branch> doesn't merely resolve the bare form, it discards -b and creates the branch under the remote branch's own name. The commit message has this right ("with -b given, git's DWIM discards it and creates the same-named branch instead"); the docstring keeps only the half that reads as "the qualification is redundant for creation", which is the sentence that would license removing it.

test_switch_create_with_remote_only_base is what that removal would break — and not by failing to create a branch, but by creating one under the wrong name: wt switch --create new-wt --base releases/4.x.x would leave you on releases/4.x.x, not new-wt. Suggestion inline.

My earlier transcript was wrong, which is how the premise got here. The approval on b46f12c claimed worktree add -b mywork -- <path> release gave "differing name: nothing, as intended". That run had created a local release one command earlier in the same clone, so the bare name resolved to that local branch and the DWIM never fired. In a clean clone it comes out the other way. Flagging it because that transcript is what this commit's premise was checked against.

The base_pr_upstream paragraph reads correctly once #3951's <remote>/<branch> fallback lands, so nothing to add there.

Verification transcript (git 2.55, clean clone each time)

The distinguishing detail is that the local branch must not already exist — that is what my earlier transcript got wrong.

$ git branch --list          # remote-only `releases/4.x.x`, nothing local but main
* main

$ git -c branch.autoSetupMerge=simple worktree add -b new-wt -- /tmp/o releases/4.x.x
Preparing worktree (new branch 'releases/4.x.x')
branch 'releases/4.x.x' set up to track 'origin/releases/4.x.x'.
HEAD is now at be0590c i

$ git branch --list          # `-b new-wt` silently discarded
* main
+ releases/4.x.x

The qualified spelling keeps -b:

$ git -c branch.autoSetupMerge=simple worktree add -b mywork2 -- /tmp/o2 origin/release
Preparing worktree (new branch 'mywork2')
$ git rev-parse --abbrev-ref 'mywork2@{upstream}'
fatal: no upstream configured for branch 'mywork2'

And wt on this head does the right thing, because resolve_base_ref qualifies first:

$ wt switch --create my-feature --base release --no-cd
✓ Created branch my-feature from origin/release and worktree @ …/work.my-feature
$ git branch --list
* main
+ my-feature

Comment thread src/commands/worktree/switch.rs Outdated
The previous commit inverted it. `git worktree add -b <name> -- <path>
<bare-remote-only-branch>` doesn't merely resolve the bare name — it drops the
`-b` and creates the remote branch's own name instead. So qualifying in
`resolve_base_ref` is what keeps `--create new-wt --base remote-only-branch` on
`new-wt`, which matters more for the creation than for the validation it also
serves. `test_switch_create_with_remote_only_base` pins it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBxMUZo7awheuY85PNXCjE
@max-sixty

Copy link
Copy Markdown
Owner Author

Right on both counts, and thanks for re-running the check — fixed in 62aa46d.

The docstring now says what the commit message did: git worktree add -b <name> -- <path> <bare-remote-only-branch> drops the -b and creates the remote branch's own name, so qualifying in resolve_base_ref is what keeps --create new-wt --base remote-only-branch on new-wt. It names test_switch_create_with_remote_only_base as what pins it.

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Confirmed on 62aa46df, and the citation holds in both directions: drop the qualification and --create new-wt --base releases/4.x.x first fails resolve_switch_target's pre-validation, so test_switch_create_with_remote_only_base trips at its output.status.success() assertion; if that validation ever moved, the branch --list new-wt assertion behind it catches the wrong-name creation. Nothing outstanding from my side — the four suggestions on the approved review all landed in e29b43e2.

@max-sixty
max-sixty merged commit 1b4c492 into main Aug 29, 2026
41 checks passed
@max-sixty
max-sixty deleted the switch-explicit-track-flags branch August 29, 2026 00:25
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.

2 participants