diff --git a/.changes/unreleased/Added-20260601-113312.yaml b/.changes/unreleased/Added-20260601-113312.yaml new file mode 100644 index 000000000..ca049daeb --- /dev/null +++ b/.changes/unreleased/Added-20260601-113312.yaml @@ -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 diff --git a/branch_checkout.go b/branch_checkout.go index 7c7233905..f9c279130 100644 --- a/branch_checkout.go +++ b/branch_checkout.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding" + "errors" "fmt" "strings" @@ -10,6 +11,7 @@ import ( "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" ) @@ -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 @@ -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) diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index ecad060ec..7ea9b9192 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -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. @@ -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} @@ -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: @@ -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 @@ -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} @@ -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} diff --git a/testdata/script/branch_checkout_integration.txt b/testdata/script/branch_checkout_integration.txt new file mode 100644 index 000000000..fb7dd8c18 --- /dev/null +++ b/testdata/script/branch_checkout_integration.txt @@ -0,0 +1,38 @@ +# Verifies 'gs branch checkout ' 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 ' +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