fix(switch): decide branch tracking for --create, whatever the git config says - #3950
Conversation
…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
left a comment
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
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 intendedAnd 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'…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
left a comment
There was a problem hiding this comment.
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.xThe 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-featureThe 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
|
Right on both counts, and thanks for re-running the check — fixed in 62aa46d. The docstring now says what the commit message did: |
|
Confirmed on |
Problem
wt switch --create <name> --base origin/<branch>must not leave the new branch tracking a differently-named base: underpush.default = upstreama baregit pushthen 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:trueandalwaystrack a differently-named base (the #713 footgun is still live), whilefalseandinheritdeny a same-named branch the tracking that is the point of it — including the DWIMwt switch featurefromorigin/feature, which the docs promise is a tracking branch.Solution
Force the
-cinstead of defaulting it, on everygit worktree addwt switchruns.-coutranks 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;--createfrom a differently-named base gets no upstream.Tests:
test_switch_create_from_remote_base_upstreambecomes a matrix over all sixbranch.autoSetupMergevalues (unset, simple, false, inherit, true, always) × three base spellings (origin/release, the barereleasethat resolves to it, andrefs/remotes/origin/staging);test_switch_dwim_from_remote_trackspins the DWIM half over the values that used to decline.Why not
--track/--no-trackThe obvious alternative is for
wtto 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_prefixsplits<remote>/<branch>at the first slash, but git maps a remote-tracking ref back to its branch through the fetch refspec. With a remote namedteam/fork, the two disagree and the verdict inverts both ways:A bare
git pushunderpush.default = upstreamthen lands onrelease— #713, reintroduced. The same base with--create releasegot 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.--trackis a hard demand wheresimpleis 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: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/releaseand--base remotes/origin/releasename the same ref asorigin/releasebut don't matchshort_name, so they got--no-track.All three come from
wtcomputing the name match itself, which it cannot do correctly — only git knows the refspec mapping. Sowtdecides 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--trackfails the suite rather than shipping.Verification
Measured end-to-end on scratch clones across all six
branch.autoSetupMergevalues: a differing name gets no upstream and a matching name tracks, identically in every one.