Repository navigation
fix(switch): track a remote base only when the names match - #3913
Conversation
worktrunk-bot
left a comment
There was a problem hiding this comment.
The behavior claim is right — switch.rs unsets the upstream whenever --create's base is a remote-tracking ref (the is_remote_tracking_branch(base) arm, citing #713) — and I reproduced the push.autoSetupRemote = true path exactly as written. One inline suggestion on the rationale sentence, which doesn't hold under git's default config.
One thing the paragraph under-describes: resolve_base_ref promotes a bare base to origin/<base> when the name exists on exactly one remote and not locally (the remotes.len() == 1 arm in src/commands/worktree/switch.rs), so --base release against a remote-only release lands in the same no-upstream state as the explicit --base origin/release the paragraph shows. Worth a few words if it fits, though the explicit form is the one #3912 hit.
Repro for the inline suggestion (git 2.55.0, default config)
$ git config --get push.default # unset → "simple"
$ git worktree add -b feature ../ft origin/release
$ git branch -vv
* feature 48b8215 [origin/release] r1
$ git push
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:releaseUnder push.default = upstream it does what the paragraph says:
$ git -c push.default=upstream push --dry-run
To ../origin.git
48b8215..5c1fec7 feature -> releaseSo the unset protects the upstream configuration specifically; under simple both states refuse the bare push, just with different messages.
Also covers the bare `--base release` that resolve_base_ref promotes to `origin/release`, and corrects the same overstatement in the code comment above unset_upstream().
|
I'm confused though: if we switch to an upstream branch, don't we make a local branch and then that local branch has an upstream? or I'm thinking about it wrongly? |
The paragraph sat next to the sentence about switching to a remote branch creating a local *tracking* branch, without saying the two cases differ. Name --create as the exception and say what the inherited tracking would point at.
|
You're thinking about it right — that's a different case from the one the paragraph covers, and the paragraph didn't say so.
The difference is what the inherited tracking would point at. Switching to Pushed c51e0bb rewording the paragraph to name Verified against a scratch repo (debug
|
|
ok, does this act differently to git? do we need to document it?! |
|
Yes — this is the one place Git sets the upstream on every form that creates a branch from a remote-tracking ref — The caveat is that the deviation only buys something under One thing docs can't do: the surprise arrives at Repro (git 2.55.0, default config, no
|
|
so should we just copy git's behavior? |
…tream `wt switch --create <name> --base origin/<branch>` ran `git branch --unset-upstream` afterwards so the new branch could not push to the base (#713). Git ships that exact rule as `branch.autoSetupMerge = simple`: tracking is set only when the start point is a remote-tracking branch and the new branch has the same name as the remote branch. Default to it for the `--create` paths rather than undoing git's `true` after the fact. Three things follow. A branch that shares its base's name — `--create release --base origin/release` — keeps the tracking, which was always correct and was being unset too. An explicit `branch.autoSetupMerge` is honoured, so `wt` picks a different default rather than overriding the setting. And `wt switch --create` no longer fails outright for anyone whose config left git no upstream to unset: under `branch.autoSetupMerge = false` the command exited 128 with "fatal: branch 'feature' has no upstream information", after the branch and worktree had been created. The DWIM paths are unaffected: they create `feature` from `origin/feature`, where `simple` and `true` agree.
|
Not git's default — that's the config #713 reported an accidental push to Pushed 61c66de switching to it —
The docs paragraph stays but shrinks to naming the config, since the behavior is now a git setting rather than a Verified against scratch repos (git 2.55.0, debug
|
|
Thanks for the repro in #3937 — it ran unmodified against builds of both On The flip side is that an explicit |
…nfig says (#3950) ## 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](#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. <details> <summary>Three reproduced defects in the explicit-flags version</summary> **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: ```console $ 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: ```console $ 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`. </details> 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_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
worktrunk 0.76.0 Created-by: HarmonybrewBot Commit-by: HarmonybrewBot Merged-by: HarmonybrewBot Description: Created by `brew bump` --- Created with `brew bump-formula-pr`.<details> <summary>release notes</summary> <pre>## Release Notes ### Improved - **`wt switch --execute` takes a program, not a shell string**: `-x` names one program, with everything after `--` passed as literal argv. Worktrunk spawns it as a child rather than running it in your interactive shell, so shell functions and its `cd` no longer reach you. `-x sh -- -c '…'` recovers shell syntax, not functions. (Breaking: existing `-x` strings, plus `WORKTRUNK_DIRECTIVE_EXEC_FILE` and `WORKTRUNK_SHELL`.) ([#3977](max-sixty/worktrunk#3977), closes [#2860](max-sixty/worktrunk#2860), fixes [#3944](max-sixty/worktrunk#3944), thanks @omgreenfield for testing the migration path) - **Worktrunk decides tracking for the branches it creates**: a new branch gets an upstream only when its name matches the remote branch it starts from, whatever `branch.autoSetupMerge` says: `--create release --base origin/release` tracks, `--create feature --base origin/release` does not. Under `autoSetupMerge = false` this previously exited 128 after creating the worktree. ([#3913](max-sixty/worktrunk#3913), [#3950](max-sixty/worktrunk#3950), fixes [#3937](max-sixty/worktrunk#3937), thanks @mjakl for reporting and diagnosing) - **Retired config keys are no longer read**: `commit.generation.template-file` and `squash-template-file` are gone — put the file's contents in `template` or `squash-template`. There is no migration: the key stays, warns as unknown on every load, and the prompt reverts to the built-in default. Setting both `template` and `template-file` used to fail the load; it now loads `template`. `switch.picker.timeout-ms` likewise warns rather than being stripped. (Breaking.) ([#3949](max-sixty/worktrunk#3949)) - **Project aliases and hooks can use `wt switch --execute`**: their bodies previously refused it. Worktrunk now starts the program itself instead of handing shell text to your shell, and the command-approval gate remains the control on project-defined commands. ([#3977](max-sixty/worktrunk#3977)) - **Established automation and LLM customization interfaces are now stable**: `wt step eval`, `wt step for-each`, `wt step prune`, LLM branch summaries, `wt config state vars`, and `commit.generation.template-append` are no longer marked experimental. ([#3949](max-sixty/worktrunk#3949)) - **`wt step prune` stages removals concurrently again**: only the `git worktree remove` teardowns serialize, so dirty checks, fsmonitor shutdown, and the trash rename overlap. The `prune_e2e/live` benchmark median went from 377 ms to 222 ms. ([#3954](max-sixty/worktrunk#3954)) - **`wt config approvals add` reads as a review rather than a warning**: it opens with a cyan `Review 1 command for repo:` instead of a yellow `▲ … needs approval to execute`. The execution-time gate is unchanged. ([#3953](max-sixty/worktrunk#3953)) ### Fixed - **The `wt switch` picker survives a preview longer than 65,535 lines**: skim keeps the pane's line count in a `u16`, so a larger diff aborted `wt` with exit 101 seconds after it painted — on screen the picker looked like it closed by itself. Panes now cap at 60,000 lines and say so. ([#3959](max-sixty/worktrunk#3959), fixes [#3958](max-sixty/worktrunk#3958), thanks @sandertammesoo for reporting and diagnosing) - **The nushell wrapper propagates exit codes without a POSIX shell**: it reported a failing `wt` by spawning `sh`, so on Windows every failing command printed a ``Command `sh` not found`` trace and exit 1 whatever the real code was. Nushell's wrapper is a static file, so rerun `wt config shell install` to pick this up. ([#3945](max-sixty/worktrunk#3945)) - **The picker no longer shows one detached worktree's diff for another**: previews were keyed by branch name, so every detached row shared the key `(detached)`. Rows now key by canonical identity, which also lets `wt remove feature ~/repo.feature` plan that worktree once instead of failing on the second. ([#3926](max-sixty/worktrunk#3926)) - **`wt switch pr:<n>` gives `pre-switch` hooks the real branch**: `branch` and `target` held the literal `pr:3933`, and `target_worktree_path` was unset even when that branch had a worktree. `pr_number` and `pr_url` are now set for same-repo PRs, not only forks. ([#3941](max-sixty/worktrunk#3941), fixes [#3934](max-sixty/worktrunk#3934), thanks @robsonpeixoto for reporting) - **`wt config shell install` no longer replaces an rc file created while it was checking**: the missing-file path refuses to clobber and fails with a rerun hint. A dangling rc symlink is preserved and rejected rather than overwritten. ([#3929](max-sixty/worktrunk#3929)) - **`wt switch --create X --base pr:<n>` works when the PR branch has no local branch**: the fetch writes only the remote-tracking ref, and git won't expand a bare name to one, so the command failed with `No branch, tag, or commit named …`. The base is now named under its remote. ([#3951](max-sixty/worktrunk#3951)) - **`wt switch --create` streams `git worktree add` instead of stalling**: where a sandbox denied the timed wait's pipe or `sigaction`, the command waited out git's entire run and then reported `Failed to wait for command` for work that had already succeeded. ([#3881](max-sixty/worktrunk#3881)) - **Claude Code activity markers stay on the session's launch worktree**: the plugin's marker hooks pass `-C "$CLAUDE_PROJECT_DIR"`, so a `cd` mid-session no longer marks another repository. A session launched outside a repository gets no marker. ([#3956](max-sixty/worktrunk#3956)) ### Documentation - **`--execute` documents how to get a variable into a shell body**: each position is substituted verbatim, so a variable spliced into `sh -c` text is re-parsed by that shell and splits on spaces. The help now shows passing it as a separate argument and referencing it positionally. ([#3977](max-sixty/worktrunk#3977)) - **The clone-local default-branch override is documented**: `wt config state default-branch set` writes `worktrunk.default-branch` to the clone's local git config — machine-local, never committed, and shared by every linked worktree. `set` warns when the branch isn't checked out locally, and `wt list` skips its comparisons until it is. ([#3947](max-sixty/worktrunk#3947), [#3948](max-sixty/worktrunk#3948), fixes [#3946](max-sixty/worktrunk#3946), thanks @danielo515 for the request) - **The docs site renders code with the CLI's own colors**: terminal examples reproduce each snapshot's exact ANSI roles, while command references and the homepage command comparison get clap and shell syntax color from paired light/dark themes. File excerpts gain a path tab, and code wraps instead of scrolling on mobile. ([#3936](max-sixty/worktrunk#3936), [#3938](max-sixty/worktrunk#3938), [#3939](max-sixty/worktrunk#3939), [#3942](max-sixty/worktrunk#3942)) - **The `pre-start` upstream example uses `--base`**: it showed `wt switch --create feature origin/feature`, which `wt switch` rejects. ([#3955](max-sixty/worktrunk#3955)) ### Internal - **Library API rework** (Breaking library API): `cargo-semver-checks` fails ten lints — `RemoteRefProvider` and its provider structs give way to `ForgeKind` dispatch, the `--execute` rework retires the fish and PowerShell `ShellEscapeMode` escapes and `WORKTRUNK_SHELL_ENV_VAR`, `BranchRef` swaps `full_ref`/`worktree_path` for `id`, and accessors including `Branch::unset_upstream` and `ProjectConfig::ci_platform` are gone. ([#3913](max-sixty/worktrunk#3913), [#3926](max-sixty/worktrunk#3926), [#3949](max-sixty/worktrunk#3949), [#3963](max-sixty/worktrunk#3963), [#3977](max-sixty/worktrunk#3977)) - **Hook pipelines and `wt list` collection lost two indirection layers**: the background pipeline carries `PreparedStep` directly, and 15 marker task types collapse into one exhaustive dispatch. The synthesized-`--force` submodule path keeps its dirty re-check and destructive command under one lock, so prune's narrower locking doesn't widen that window. ([#3964](max-sixty/worktrunk#3964), [#3954](max-sixty/worktrunk#3954)) ## Install worktrunk 0.76.0 ### Install prebuilt binaries via shell script ```sh curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.76.0/worktrunk-installer.sh | sh && wt config shell install ``` ### Install prebuilt binaries via powershell script ```sh powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.76.0/worktrunk-installer.ps1 | iex"; git-wt config shell install ``` ### Install prebuilt binaries via Homebrew ```sh brew install worktrunk && wt config shell install ``` ## Download worktrunk 0.76.0 | File | Platform | Checksum | |--------|----------|----------| | [worktrunk-aarch64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.76.0/worktrunk-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.76.0/worktrunk-aarch64-apple-darwin.tar.xz.sha256) | | [worktrunk-x86_64-ap See merge request: Harmonybrew/homebrew-core!18482
Problem
wt switch --create <name> --base origin/<branch>rangit branch --unset-upstreamafterwards, so the new branch could not push to the base (#713). Nothing said so, and #3912 was the third thread to arrive at the manualgit push --set-upstream origin <branch>step. Reviewing the docs for it (@max-sixty, comment) surfaced the better question: should this just copy git?Solution
Git already ships the rule the unset was approximating —
branch.autoSetupMerge = simplesets tracking only when the start point is a remote-tracking branch and the new branch has the same name as the remote branch.wtnow defaults to it on the--createpaths instead of undoing git'strueafter the fact, and documents it in one paragraph ofwt switch'safter_long_help.Three things follow:
--create release --base origin/releasewas getting unset too, though the tracking it lost was correct.branch.autoSetupMergeis honoured.wtpicks a different default; it no longer overrides the setting.--createoff a remote base no longer fails outright for anyone whose config left git no upstream to unset. Underbranch.autoSetupMerge = falsethe command exited 128 —fatal: branch 'feature' has no upstream information— after creating the branch and the worktree.The DWIM paths (
wt switch releasewhen onlyorigin/releaseexists) are unaffected: they createfeaturefromorigin/feature, wheresimpleandtrueagree.simpleneeds git ≥ 2.37;MINIMUM_GIT_VERSIONis 2.43.Testing
test_switch_create_from_remote_base_no_upstreambecomestest_switch_create_from_remote_base_upstream, covering the same #713 property plus the three cases above. Full suite viacargo run -- hook pre-merge --yes.Verification (git 2.55.0, debug
wt, bareoriginwithmainandrelease)Behavior, before and after, in a clone with no local
release:The failure this removes, on the pre-change binary:
branch.autoSetupMergesemantics, straight from git, nowtinvolved:And that
git worktree add -b— the invocationwtactually runs — honours it the same way:Refs #3912, #713 — automated triage