Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20260601-113312.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: '''branch checkout'': Treat the integration branch name as a shortcut to the integration branch, with a one-line throwaway warning instead of an untracked-branch prompt.'
time: 2026-06-01T11:33:12.704475-04:00
20 changes: 20 additions & 0 deletions branch_checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package main
import (
"context"
"encoding"
"errors"
"fmt"
"strings"

"go.abhg.dev/gs/internal/cli"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/handler/checkout"
"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/spice/state"
"go.abhg.dev/gs/internal/text"
"go.abhg.dev/gs/internal/ui"
)
Expand Down Expand Up @@ -146,6 +148,7 @@ func (cmd *branchCheckoutCmd) Run(
ctx context.Context,
log *silog.Logger,
view ui.View,
store *state.Store,
handler CheckoutHandler,
) error {
mode := cmd.TrackUntracked
Expand All @@ -162,10 +165,27 @@ func (cmd *branchCheckoutCmd) Run(
log.Warnf("Please use spice.branchCheckout.trackUntracked=%v instead", mode.String())
}

// The integration branch is a deliberately-untracked singleton; the
// generic "branch not tracked" prompt is confusing for it. Detect
// the case once up front so ShouldTrack can short-circuit and the
// user gets a single, targeted warning that the branch is
// throwaway.
isIntegration := false
if info, err := store.Integration(ctx); err == nil && info.Name == cmd.Branch {
isIntegration = true
log.Warnf("%v: integration branch is throwaway and not tracked by git-spice", cmd.Branch)
} else if err != nil && !errors.Is(err, state.ErrNotExist) {
log.Warn("Could not load integration branch configuration", "error", err)
}

return handler.CheckoutBranch(ctx, &checkout.Request{
Branch: cmd.Branch,
Options: &cmd.Options,
ShouldTrack: func(branch string) (bool, error) {
if isIntegration {
return false, nil
}

switch mode {
case trackUntrackedAlways:
log.Infof("%v: automatically tracking branch", branch)
Expand Down
36 changes: 21 additions & 15 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,11 @@ Before merging, the stack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
before failing if checks are not ready.
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
before failing if merge readiness is not reached.

By default, a branch failure skips that branch's upstack descendants,
but independent sibling branches continue.
Expand All @@ -321,12 +323,12 @@ Use --fail-fast to stop the queue after the first branch failure.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-branch-check`: Skip stale base validation before merging.
* `--fail-fast`: Stop the merge queue after the first branch failure.
* `--branch=NAME`: Branch whose stack to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice stack restack {#gs-stack-restack}

Expand Down Expand Up @@ -624,7 +626,7 @@ This command acts as a local merge queue:
it merges one Change Request,
waits for that merge to finish,
restacks and updates the next Change Request,
waits for its CI checks to pass,
waits for merge readiness on the updated Change Request,
and then repeats the process.

For a stack like this:
Expand All @@ -642,13 +644,15 @@ Before merging, the downstack is checked for branches
whose base PR was already merged on the forge.
Use --no-branch-check to skip this validation.

Before each merge, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait
Before each merge, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait
(default: 30m, 0 means fail immediately if not ready).

Between merges, the command waits for each merge
to complete, restacks and updates the next PR,
waits for CI checks on the updated PR,
waits for merge readiness on the updated PR,
and syncs merged branch cleanup.

Use --no-wait for single branch merging
Expand All @@ -658,12 +662,12 @@ when you don't want to wait for the merge to propagate.
**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--no-wait`: Skip polling for a single branch merge to propagate.
* `--no-branch-check`: Skip stale base validation before merging.
* `--branch=NAME`: Branch to start merging from

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice downstack edit {#gs-downstack-edit}

Expand Down Expand Up @@ -1133,16 +1137,18 @@ Use --branch to merge a different branch.
The branch must be based directly on trunk.
To merge a stacked branch, use 'gs downstack merge'.

Before merging, waits for CI checks to pass.
Use --build-timeout to configure the maximum wait.
Before merging, waits for merge readiness:
the forge must observe the pushed head
and report that the CR is ready to merge.
Use --ready-timeout to configure the maximum wait.

**Flags**

* `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'.
* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once.
* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once.
* `--branch=NAME`: Branch to merge

**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod)
**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout)

### git-spice branch submit {#gs-branch-submit}

Expand Down
38 changes: 38 additions & 0 deletions testdata/script/branch_checkout_integration.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Verifies 'gs branch checkout <integration>' is a no-friction
# shortcut to the integration branch: it switches without prompting
# to track the branch and warns once that the branch is throwaway.

as 'Test <test@example.com>'
at '2025-06-01T00:00:00Z'

cd repo
git init
git commit --allow-empty -m 'Initial commit'
gs repo init

git add feat-a.txt
gs bc feat-a -m 'Add feat-a'

gs trunk
gs integration create preview --tip feat-a
gs integration rebuild

# After rebuild, the worktree is back on the previously checked-out
# branch (main). Switching to the integration branch by name should
# just work, without the 'branch not tracked' prompt path.
gs bco preview
stderr 'integration branch is throwaway'
stderr 'switched to branch: preview'

# Sanity: HEAD now points at the integration branch.
git symbolic-ref HEAD
cmp stdout $WORK/golden/head.txt

# Sanity: tracking is not implicitly added.
! gs branch track preview
stderr 'cannot track integration branch'

-- repo/feat-a.txt --
feature a
-- golden/head.txt --
refs/heads/preview
Loading