From c41eaac6043cb33d5975378117f15d083c9dffe4 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:12:28 +0000 Subject: [PATCH 1/4] docs(switch): say why a branch off a remote base has no upstream Creating from a remote base deliberately unsets the tracking git would otherwise set to the base (#713), so a bare `git push` cannot push the new work to the base branch. Nothing said so, leaving the branch with no upstream and no stated way to publish it. Refs #3912 --- docs/src/content/docs/switch.md | 2 ++ plugins/worktrunk/skills/worktrunk/reference/switch.md | 2 ++ skills/worktrunk/reference/switch.md | 2 ++ src/cli/mod.rs | 2 ++ .../integration__integration_tests__help__help_switch_long.snap | 2 ++ 5 files changed, 10 insertions(+) diff --git a/docs/src/content/docs/switch.md b/docs/src/content/docs/switch.md index 3736f2b87..114a73005 100644 --- a/docs/src/content/docs/switch.md +++ b/docs/src/content/docs/switch.md @@ -32,6 +32,8 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. +A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. + ## Creating worktrees If the branch already has a worktree, `wt switch` changes directories to it. Otherwise, it creates one: diff --git a/plugins/worktrunk/skills/worktrunk/reference/switch.md b/plugins/worktrunk/skills/worktrunk/reference/switch.md index afaa82b39..1d879e1e7 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/switch.md +++ b/plugins/worktrunk/skills/worktrunk/reference/switch.md @@ -19,6 +19,8 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. +A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. + ## Creating worktrees If the branch already has a worktree, `wt switch` changes directories to it. Otherwise, it creates one: diff --git a/skills/worktrunk/reference/switch.md b/skills/worktrunk/reference/switch.md index afaa82b39..1d879e1e7 100644 --- a/skills/worktrunk/reference/switch.md +++ b/skills/worktrunk/reference/switch.md @@ -19,6 +19,8 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. +A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. + ## Creating worktrees If the branch already has a worktree, `wt switch` changes directories to it. Otherwise, it creates one: diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6a460250c..b0a6ef547 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -627,6 +627,8 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. +A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. + ## Creating worktrees If the branch already has a worktree, `wt switch` changes directories to it. Otherwise, it creates one: diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index 5f4d14a05..1d2f566f0 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -148,6 +148,8 @@ Worktrees are addressed by branch name; paths are computed from a configurable t The --create flag creates a new branch from --base — the default branch unless specified. Without --create, the branch must already exist. Switching to a remote branch (e.g., wt switch feature when only origin/feature exists) creates a local tracking branch. +A branch created from a remote base — --base origin/release — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare git push would push the new work to release. Publish it with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after which a bare git push from the new worktree publishes the branch and configures its tracking. + Creating worktrees If the branch already has a worktree, wt switch changes directories to it. Otherwise, it creates one: From 29bb2078a9250002518b0226c978b41e5a37986b Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:26:05 +0000 Subject: [PATCH 2/4] docs(switch): scope the bare-push consequence to push.default = upstream 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(). --- docs/src/content/docs/switch.md | 2 +- plugins/worktrunk/skills/worktrunk/reference/switch.md | 2 +- skills/worktrunk/reference/switch.md | 2 +- src/cli/mod.rs | 2 +- src/commands/worktree/switch.rs | 5 +++-- ...tegration__integration_tests__help__help_switch_long.snap | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/switch.md b/docs/src/content/docs/switch.md index 114a73005..17e5a3e00 100644 --- a/docs/src/content/docs/switch.md +++ b/docs/src/content/docs/switch.md @@ -32,7 +32,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. ## Creating worktrees diff --git a/plugins/worktrunk/skills/worktrunk/reference/switch.md b/plugins/worktrunk/skills/worktrunk/reference/switch.md index 1d879e1e7..f1b283f96 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/switch.md +++ b/plugins/worktrunk/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. ## Creating worktrees diff --git a/skills/worktrunk/reference/switch.md b/skills/worktrunk/reference/switch.md index 1d879e1e7..f1b283f96 100644 --- a/skills/worktrunk/reference/switch.md +++ b/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. ## Creating worktrees diff --git a/src/cli/mod.rs b/src/cli/mod.rs index b0a6ef547..e61557f6b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -627,7 +627,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release` — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. ## Creating worktrees diff --git a/src/commands/worktree/switch.rs b/src/commands/worktree/switch.rs index 493652186..31dac8c24 100644 --- a/src/commands/worktree/switch.rs +++ b/src/commands/worktree/switch.rs @@ -1030,8 +1030,9 @@ fn execute_switch( // Safety: unset unsafe upstream when creating a new branch from a remote // tracking branch. When `git worktree add -b feature origin/main` runs, - // git sets feature to track origin/main. This is dangerous because - // `git push` would push to main instead of the feature branch. + // git sets feature to track origin/main. That is dangerous under + // `push.default = upstream`, where a bare `git push` pushes the new work + // to main; under the default `simple` it refuses the push instead. // See: https://github.com/max-sixty/worktrunk/issues/713 if *create_branch && let Some(base) = base_branch diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index 1d2f566f0..ce0ea8ee9 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -148,7 +148,7 @@ Worktrees are addressed by branch name; paths are computed from a configurable t The --create flag creates a new branch from --base — the default branch unless specified. Without --create, the branch must already exist. Switching to a remote branch (e.g., wt switch feature when only origin/feature exists) creates a local tracking branch. -A branch created from a remote base — --base origin/release — is deliberately left with no upstream. Git would otherwise have it track the base, so a bare git push would push the new work to release. Publish it with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after which a bare git push from the new worktree publishes the branch and configures its tracking. +A branch created from a remote base — --base origin/release, or a bare --base release that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under push.default = upstream a bare git push would push the new work to release. Publish it with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after which a bare git push from the new worktree publishes the branch and configures its tracking. Creating worktrees From c51e0bbd8754a2787d69d96cfdb1b5d5c8adcbd3 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:58:30 +0000 Subject: [PATCH 3/4] docs(switch): mark the no-upstream case as --create's exception 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. --- docs/src/content/docs/switch.md | 2 +- plugins/worktrunk/skills/worktrunk/reference/switch.md | 2 +- skills/worktrunk/reference/switch.md | 2 +- src/cli/mod.rs | 2 +- ...integration__integration_tests__help__help_switch_long.snap | 3 ++- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/switch.md b/docs/src/content/docs/switch.md index 17e5a3e00..25b2f7eb4 100644 --- a/docs/src/content/docs/switch.md +++ b/docs/src/content/docs/switch.md @@ -32,7 +32,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/plugins/worktrunk/skills/worktrunk/reference/switch.md b/plugins/worktrunk/skills/worktrunk/reference/switch.md index f1b283f96..ee2c41f42 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/switch.md +++ b/plugins/worktrunk/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/skills/worktrunk/reference/switch.md b/skills/worktrunk/reference/switch.md index f1b283f96..ee2c41f42 100644 --- a/skills/worktrunk/reference/switch.md +++ b/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/src/cli/mod.rs b/src/cli/mod.rs index e61557f6b..f125e7c18 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -627,7 +627,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -A branch created from a remote base — `--base origin/release`, or a bare `--base release` that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish it with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes the branch and configures its tracking. +`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index ce0ea8ee9..38b804c67 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -148,7 +148,8 @@ Worktrees are addressed by branch name; paths are computed from a configurable t The --create flag creates a new branch from --base — the default branch unless specified. Without --create, the branch must already exist. Switching to a remote branch (e.g., wt switch feature when only origin/feature exists) creates a local tracking branch. -A branch created from a remote base — --base origin/release, or a bare --base release that exists only on a remote — is deliberately left with no upstream. Git would otherwise have it track the base, so under push.default = upstream a bare git push would push the new work to release. Publish it with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after which a bare git push from the new worktree publishes the branch and configures its tracking. +--create from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — origin/release for --base origin/release, and the same for a bare --base release that exists only on a remote — so under push.default = upstream a bare git push would push the new work to release. Publish the branch with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after + which a bare git push from the new worktree publishes it and configures its tracking. Creating worktrees From 61c66de11685af52ca47d4cc40891f43b7eec419 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:20:34 +0000 Subject: [PATCH 4/4] fix(switch): use git's autoSetupMerge=simple instead of unsetting upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wt switch --create --base origin/` 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. --- docs/src/content/docs/switch.md | 2 +- .../skills/worktrunk/reference/switch.md | 2 +- skills/worktrunk/reference/switch.md | 2 +- src/cli/mod.rs | 2 +- src/commands/worktree/switch.rs | 37 ++++--- src/git/repository/branch.rs | 12 --- src/git/repository/remotes.rs | 15 --- tests/integration_tests/switch.rs | 97 +++++++++++++------ ...gration_tests__help__help_switch_long.snap | 4 +- 9 files changed, 95 insertions(+), 78 deletions(-) diff --git a/docs/src/content/docs/switch.md b/docs/src/content/docs/switch.md index 25b2f7eb4..a0d279705 100644 --- a/docs/src/content/docs/switch.md +++ b/docs/src/content/docs/switch.md @@ -32,7 +32,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. +`--create` from a remote base is the exception. For the branch it creates, `wt` defaults git's `branch.autoSetupMerge` to `simple` instead of git's own `true`, so the new branch tracks its base only when the two share a name: `--create release --base origin/release` tracks `origin/release`, while `--create feature --base origin/release` — and the bare `--base release` that resolves to it — gets no upstream. Git's default would have `feature` track `origin/release`, so under `push.default = upstream` a bare `git push` would push the new work to `release`. A `branch.autoSetupMerge` set in git config takes precedence over that default. Publishing such a branch takes `git push --set-upstream origin `, or git's `push.autoSetupRemote = true` set once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/plugins/worktrunk/skills/worktrunk/reference/switch.md b/plugins/worktrunk/skills/worktrunk/reference/switch.md index ee2c41f42..b6a83744c 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/switch.md +++ b/plugins/worktrunk/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. +`--create` from a remote base is the exception. For the branch it creates, `wt` defaults git's `branch.autoSetupMerge` to `simple` instead of git's own `true`, so the new branch tracks its base only when the two share a name: `--create release --base origin/release` tracks `origin/release`, while `--create feature --base origin/release` — and the bare `--base release` that resolves to it — gets no upstream. Git's default would have `feature` track `origin/release`, so under `push.default = upstream` a bare `git push` would push the new work to `release`. A `branch.autoSetupMerge` set in git config takes precedence over that default. Publishing such a branch takes `git push --set-upstream origin `, or git's `push.autoSetupRemote = true` set once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/skills/worktrunk/reference/switch.md b/skills/worktrunk/reference/switch.md index ee2c41f42..b6a83744c 100644 --- a/skills/worktrunk/reference/switch.md +++ b/skills/worktrunk/reference/switch.md @@ -19,7 +19,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. +`--create` from a remote base is the exception. For the branch it creates, `wt` defaults git's `branch.autoSetupMerge` to `simple` instead of git's own `true`, so the new branch tracks its base only when the two share a name: `--create release --base origin/release` tracks `origin/release`, while `--create feature --base origin/release` — and the bare `--base release` that resolves to it — gets no upstream. Git's default would have `feature` track `origin/release`, so under `push.default = upstream` a bare `git push` would push the new work to `release`. A `branch.autoSetupMerge` set in git config takes precedence over that default. Publishing such a branch takes `git push --set-upstream origin `, or git's `push.autoSetupRemote = true` set once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f125e7c18..0ac5f4ba2 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -627,7 +627,7 @@ $ wt switch https://github.com/owner/repo/pull/123 # ...or paste the PR's URL The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch. -`--create` from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — `origin/release` for `--base origin/release`, and the same for a bare `--base release` that exists only on a remote — so under `push.default = upstream` a bare `git push` would push the new work to `release`. Publish the branch with `git push --set-upstream origin `, or set git's `push.autoSetupRemote = true` once, after which a bare `git push` from the new worktree publishes it and configures its tracking. +`--create` from a remote base is the exception. For the branch it creates, `wt` defaults git's `branch.autoSetupMerge` to `simple` instead of git's own `true`, so the new branch tracks its base only when the two share a name: `--create release --base origin/release` tracks `origin/release`, while `--create feature --base origin/release` — and the bare `--base release` that resolves to it — gets no upstream. Git's default would have `feature` track `origin/release`, so under `push.default = upstream` a bare `git push` would push the new work to `release`. A `branch.autoSetupMerge` set in git config takes precedence over that default. Publishing such a branch takes `git push --set-upstream origin `, or git's `push.autoSetupRemote = true` set once, after which a bare `git push` from the new worktree publishes it and configures its tracking. ## Creating worktrees diff --git a/src/commands/worktree/switch.rs b/src/commands/worktree/switch.rs index 31dac8c24..a11c30785 100644 --- a/src/commands/worktree/switch.rs +++ b/src/commands/worktree/switch.rs @@ -959,7 +959,28 @@ fn execute_switch( // branch as the *value* of `-b`, which is safe even when // the branch name starts with `-`. let worktree_path_str = worktree_path.to_string_lossy(); - let mut args: Vec<&str> = vec!["worktree", "add"]; + let mut args: Vec<&str> = Vec::new(); + + // Safety: for an explicitly requested branch, default + // `branch.autoSetupMerge` to `simple` rather than git's + // `true`. Under `true`, `git worktree add -b feature + // origin/main` sets `feature` to track `origin/main`, so a + // bare `git push` under `push.default = upstream` pushes the + // new work to `main` (#713). `simple` is git's own narrower + // mode: it sets tracking only when the new branch's name + // matches the remote branch's, which is exactly the case + // where inherited tracking is correct. An explicit setting + // wins — `wt` picks a different default, it does not override + // the user's configuration. + // + // Only the `--create` paths need it. The DWIM paths below + // create `feature` from `origin/feature`, where the names + // match and `simple` and `true` agree. + if *create_branch && repo.config_value("branch.autoSetupMerge")?.is_none() { + args.extend(["-c", "branch.autoSetupMerge=simple"]); + } + + args.extend(["worktree", "add"]); // For DWIM fallback: when the branch doesn't exist locally, // git worktree add relies on DWIM to auto-create it from a @@ -1028,20 +1049,6 @@ fn execute_switch( .into()); } - // Safety: unset unsafe upstream when creating a new branch from a remote - // tracking branch. When `git worktree add -b feature origin/main` runs, - // git sets feature to track origin/main. That is dangerous under - // `push.default = upstream`, where a bare `git push` pushes the new work - // to main; under the default `simple` it refuses the push instead. - // See: https://github.com/max-sixty/worktrunk/issues/713 - if *create_branch - && let Some(base) = base_branch - && repo.is_remote_tracking_branch(base) - { - // Unset the upstream to prevent accidental pushes - branch_handle.unset_upstream()?; - } - // `--base pr:N` / `--base mr:N` against a same-repo PR/MR: the // user asked for a custom local name pointing at an existing // remote branch — wire up tracking so `git push` from the new diff --git a/src/git/repository/branch.rs b/src/git/repository/branch.rs index c8a514e0c..bc9900e8e 100644 --- a/src/git/repository/branch.rs +++ b/src/git/repository/branch.rs @@ -126,18 +126,6 @@ impl<'a> Branch<'a> { .and_then(|b| b.upstream_short.clone())) } - /// Unset the upstream tracking branch for this branch. - /// - /// This removes the tracking relationship, preventing accidental pushes - /// to the wrong branch (e.g., when a feature branch was created from origin/main). - pub fn unset_upstream(&self) -> anyhow::Result<()> { - // `--` separates the option from the positional branch name so a - // hyphen-prefixed branch cannot be misread as a flag. - self.repo - .run_command(&["branch", "--unset-upstream", "--", &self.name])?; - Ok(()) - } - /// Get the URL of the remote where this branch would be pushed. /// /// Uses `%(push:remotename)` which returns either a remote name or URL directly diff --git a/src/git/repository/remotes.rs b/src/git/repository/remotes.rs index ddecd9d9c..b5a0d9843 100644 --- a/src/git/repository/remotes.rs +++ b/src/git/repository/remotes.rs @@ -427,21 +427,6 @@ impl Repository { .and_then(|config| config.list.url) } - /// Check if a ref is a remote tracking branch. - /// - /// Returns true if the ref appears in the remote-branch inventory - /// (e.g., `origin/main`). Returns false for local branches, tags, SHAs, - /// non-existent refs, and `/HEAD` symrefs (which the inventory - /// excludes). - /// - /// Resolved from the remote-branch inventory — no subprocess calls once - /// it's populated. - pub fn is_remote_tracking_branch(&self, ref_name: &str) -> bool { - self.remote_branches() - .ok() - .is_some_and(|branches| branches.iter().any(|r| r.short_name == ref_name)) - } - /// Strip the remote prefix from a remote-tracking branch name. /// /// Given a name like `origin/username/feature-1`, returns `Some("username/feature-1")` diff --git a/tests/integration_tests/switch.rs b/tests/integration_tests/switch.rs index 7c06b6621..51738df9f 100644 --- a/tests/integration_tests/switch.rs +++ b/tests/integration_tests/switch.rs @@ -358,7 +358,7 @@ fn test_switch_create_with_remote_only_base(#[from(repo_with_remote)] repo: Test ); // The new branch must exist and must NOT track the remote base - // (same safety property as test_switch_create_from_remote_base_no_upstream). + // (same safety property as test_switch_create_from_remote_base_upstream). let branch_output = repo.git_output(&["branch", "--list", "new-wt"]); assert!(branch_output.contains("new-wt"), "branch should be created"); @@ -373,38 +373,75 @@ fn test_switch_create_with_remote_only_base(#[from(repo_with_remote)] repo: Test ); } -/// When creating a new branch from a remote tracking branch (e.g., origin/main), -/// the new branch should NOT track the remote base branch. -/// This prevents accidental `git push` to the base branch (e.g., pushing to main). -/// This is the bug fix for GitHub issue #713. -#[rstest] -fn test_switch_create_from_remote_base_no_upstream(#[from(repo_with_remote)] repo: TestRepo) { - // Create a new branch with --base pointing to a remote tracking branch - let output = repo - .wt_command() - .args(["switch", "--create", "my-feature", "--base=origin/main"]) - .output() - .unwrap(); - assert!(output.status.success(), "switch should succeed"); +/// `--create` from a remote tracking branch defaults `branch.autoSetupMerge` to +/// git's `simple` rather than git's own `true`, so the new branch inherits the +/// base's upstream only when the two share a name. Under `true`, a branch +/// created from `origin/release` tracks `origin/release`, and a bare `git push` +/// under `push.default = upstream` pushes the new work to `release` — GitHub +/// issue #713. +/// +/// An explicit `branch.autoSetupMerge` wins: `wt` picks a different default, it +/// does not override the setting. The previous implementation — a post-hoc +/// `git branch --unset-upstream` — overrode every setting, and failed the whole +/// command with exit 128 whenever the user's config meant git had set no +/// upstream for it to unset. +#[rstest] +fn test_switch_create_from_remote_base_upstream(#[from(repo_with_remote)] repo: TestRepo) { + // `release` on origin only, so it can serve as a remote base whose name a + // new branch either shares or doesn't. + repo.run_git(&["push", "origin", "main:release"]); + repo.run_git(&["fetch", "origin"]); + + let create = |branch: &str| { + let output = repo + .wt_command() + .args(["switch", "--create", branch, "--base=origin/release"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "switch --create {branch} should succeed; stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let branches = repo.git_output(&["branch", "--list", branch]); + assert!(branches.contains(branch), "{branch} should be created"); + }; + let upstream = |branch: &str| -> Option { + let output = repo + .git_command() + .args([ + "rev-parse", + "--abbrev-ref", + &format!("{branch}@{{upstream}}"), + ]) + .run() + .unwrap(); + output + .status + .success() + .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) + }; - // Verify the branch was created - let branch_output = repo.git_output(&["branch", "--list", "my-feature"]); - assert!( - branch_output.contains("my-feature"), - "branch should be created" - ); + // Different name: no upstream, so a bare `git push` cannot reach `release`. + create("my-feature"); + assert_eq!(upstream("my-feature"), None); - // Verify the branch does NOT have an upstream (no tracking) - // Using rev-parse to check for upstream - should fail for untracked branches - let upstream_check = repo - .git_command() - .args(["rev-parse", "--abbrev-ref", "my-feature@{upstream}"]) - .run() - .unwrap(); + // Same name: the tracking git would set points at the branch's own remote + // counterpart, which is what tracking is for — it stays. + create("release"); + assert_eq!(upstream("release").as_deref(), Some("origin/release")); - assert!( - !upstream_check.status.success(), - "branch should NOT have upstream tracking (to prevent accidental push to origin/main)" + // `false` means git sets no upstream at all; nothing to undo, nothing to fail. + repo.run_git(&["config", "branch.autoSetupMerge", "false"]); + create("no-auto-setup"); + assert_eq!(upstream("no-auto-setup"), None); + + // `always` is the user asking for git's inheriting behaviour explicitly. + repo.run_git(&["config", "branch.autoSetupMerge", "always"]); + create("explicit-always"); + assert_eq!( + upstream("explicit-always").as_deref(), + Some("origin/release") ); } diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index 38b804c67..6141422dc 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -148,8 +148,8 @@ Worktrees are addressed by branch name; paths are computed from a configurable t The --create flag creates a new branch from --base — the default branch unless specified. Without --create, the branch must already exist. Switching to a remote branch (e.g., wt switch feature when only origin/feature exists) creates a local tracking branch. ---create from a remote base is the exception: that branch is deliberately left with no upstream. Git would otherwise have it track the base rather than a remote branch of its own name — origin/release for --base origin/release, and the same for a bare --base release that exists only on a remote — so under push.default = upstream a bare git push would push the new work to release. Publish the branch with git push --set-upstream origin , or set git's push.autoSetupRemote = true once, after - which a bare git push from the new worktree publishes it and configures its tracking. +--create from a remote base is the exception. For the branch it creates, wt defaults git's branch.autoSetupMerge to simple instead of git's own true, so the new branch tracks its base only when the two share a name: --create release --base origin/release tracks origin/release, while --create feature --base origin/release — and the bare --base release that resolves to it — gets no upstream. Git's default would have feature track origin/release, so under push.default = upstream a bare git push +would push the new work to release. A branch.autoSetupMerge set in git config takes precedence over that default. Publishing such a branch takes git push --set-upstream origin , or git's push.autoSetupRemote = true set once, after which a bare git push from the new worktree publishes it and configures its tracking. Creating worktrees