From 422ea5bf398ce557a9557b66eb2bee4742809c68 Mon Sep 17 00:00:00 2001 From: Edmund Kohlwey Date: Mon, 1 Jun 2026 12:59:40 -0400 Subject: [PATCH 1/3] integration: tip advance moves tips to upstack leaves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extending a stack above a configured tip leaves the tip "behind" — the integration branch keeps merging the old tip and ignores the new top of the stack. The workaround so far has been 'tip remove old' plus 'tip add new', which gets tedious when several tips drift together. Add 'gs integration tip advance' that walks the upstack tree of each configured tip via graph.Tops, swaps the tip for its leaves, and dedups across tips. Linear case ('low -> middle -> high' with tip 'low') collapses to a single replacement: 'low' becomes 'high'. Forked case ('base -> {fork-a, fork-b}' with tip 'base') expands to both leaves. Already-at-the-top tips are left alone with no log spam. An empty advance reports "No tips to advance". The optional positional argument scopes the operation to a subset of configured tips, so 'tip advance fork-a' walks only that tip. Unrecognized names abort before any mutation so we never leave the tip list half-updated. --- .../unreleased/Added-20260601-125928.yaml | 3 + doc/includes/cli-reference.md | 101 +++------ integration_tip.go | 201 +++++++++++++++++- testdata/help/gs.txt | 4 +- testdata/help/integration_tip_advance.txt | 25 +++ testdata/script/integration_tip_advance.txt | 90 ++++++++ 6 files changed, 346 insertions(+), 78 deletions(-) create mode 100644 .changes/unreleased/Added-20260601-125928.yaml create mode 100644 testdata/help/integration_tip_advance.txt create mode 100644 testdata/script/integration_tip_advance.txt diff --git a/.changes/unreleased/Added-20260601-125928.yaml b/.changes/unreleased/Added-20260601-125928.yaml new file mode 100644 index 00000000..ce053f3a --- /dev/null +++ b/.changes/unreleased/Added-20260601-125928.yaml @@ -0,0 +1,3 @@ +kind: Added +body: '''integration tip advance'': Move each configured tip to the leaves of its upstack, optionally scoped to specific tips.' +time: 2026-06-01T12:59:28.111325-04:00 diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index d5dd4fcd..c45f2ce5 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -293,48 +293,6 @@ only if there are multiple CRs in the stack. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.labels](/cli/config.md#spicesubmitlabels), [spice.submit.labels.addWhen](/cli/config.md#spicesubmitlabelsaddwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.updateOnly](/cli/config.md#spicesubmitupdateonly), [spice.submit.web](/cli/config.md#spicesubmitweb) -### git-spice stack merge {#gs-stack-merge} - -``` -gs stack (s) merge (m) [flags] -``` - -:material-test-tube:{ title="Experimental" }[merge](/cli/experiments.md#merge) - -Merge a stack - -Merges the CRs for the current branch's stack into trunk. -Use --branch to merge a different branch's stack. - -The stack includes the selected branch, -its downstack branches down to trunk, -and every upstack branch. - -Already-merged branches are skipped automatically. -Branches must have an open Change Request to be merged. - -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. - -By default, a branch failure skips that branch's upstack descendants, -but independent sibling branches continue. -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. -* `--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) - ### git-spice stack restack {#gs-stack-restack} ``` @@ -664,11 +622,11 @@ 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. +* `--branch=NAME`: Branch to start merging from * `--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 +* `--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. **Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) @@ -1124,33 +1082,6 @@ Use --branch to target a different branch. * `--branch=NAME`: Branch to diff -### git-spice branch merge {#gs-branch-merge} - -``` -gs branch (b) merge (m) [flags] -``` - -:material-test-tube:{ title="Experimental" }[merge](/cli/experiments.md#merge) - -Merge a branch into trunk - -Merges the CR for the current branch into trunk. -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. - -**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. -* `--branch=NAME`: Branch to merge - -**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) - ### git-spice branch submit {#gs-branch-submit} ``` @@ -1619,6 +1550,32 @@ subsumes it. A second run is a no-op once nothing remains to prune. Existing tips with no upstack-tip relationship are left alone. +### git-spice integration tip advance {#gs-integration-tip-advance} + +``` +gs integration (int) tip advance [ ...] +``` + +Move tips to the topmost branches of their upstacks + +For each configured tip (or only the named tips, if any are +given as arguments), replaces the tip with the topmost +branch(es) in its upstack — the leaves of the tree above it. + +If a tip already has no branches above it, it is left alone. +If a tip's upstack forks, the tip expands to every leaf of +the fork. Branches that would become duplicates of other +configured tips are deduplicated. + +Useful as a one-shot maintenance step after extending a stack +above a configured tip: instead of 'tip remove old' + 'tip +add new', a single 'tip advance' walks each tip to its +current top. + +**Arguments** + +* `branches`: Tips to advance; defaults to all configured tips + ## Rebase ### git-spice rebase continue {#gs-rebase-continue} diff --git a/integration_tip.go b/integration_tip.go index 25fb044e..7a2d3f7e 100644 --- a/integration_tip.go +++ b/integration_tip.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "strings" "github.com/alecthomas/kong" "go.abhg.dev/gs/internal/handler/integration" @@ -12,10 +13,11 @@ import ( ) type integrationTipCmd struct { - Add integrationTipAddCmd `cmd:"" aliases:"a" help:"Add a branch to the integration tip list"` - Remove integrationTipRemoveCmd `cmd:"" aliases:"r,rm" help:"Remove a branch from the integration tip list"` - List integrationTipListCmd `cmd:"" aliases:"l,ls" help:"List the configured integration tips"` - Clean integrationTipCleanCmd `cmd:"" aliases:"prune" help:"Remove tips whose upstack already contains another tip"` + Add integrationTipAddCmd `cmd:"" aliases:"a" help:"Add a branch to the integration tip list"` + Remove integrationTipRemoveCmd `cmd:"" aliases:"r,rm" help:"Remove a branch from the integration tip list"` + List integrationTipListCmd `cmd:"" aliases:"l,ls" help:"List the configured integration tips"` + Clean integrationTipCleanCmd `cmd:"" aliases:"prune" help:"Remove tips whose upstack already contains another tip"` + Advance integrationTipAdvanceCmd `cmd:"" help:"Move tips to the topmost branches of their upstacks"` } type integrationTipAddCmd struct { @@ -130,6 +132,197 @@ func (cmd *integrationTipCleanCmd) Run( return nil } +type integrationTipAdvanceCmd struct { + Branches []string `arg:"" optional:"" predictor:"integrationTips" help:"Tips to advance; defaults to all configured tips"` +} + +func (*integrationTipAdvanceCmd) Help() string { + return text.Dedent(` + For each configured tip (or only the named tips, if any are + given as arguments), replaces the tip with the topmost + branch(es) in its upstack — the leaves of the tree above it. + + If a tip already has no branches above it, it is left alone. + If a tip's upstack forks, the tip expands to every leaf of + the fork. Branches that would become duplicates of other + configured tips are deduplicated. + + Useful as a one-shot maintenance step after extending a stack + above a configured tip: instead of 'tip remove old' + 'tip + add new', a single 'tip advance' walks each tip to its + current top. + `) +} + +func (cmd *integrationTipAdvanceCmd) Run( + ctx context.Context, + log *silog.Logger, + svc *spice.Service, + handler IntegrationHandler, +) error { + status, err := handler.Show(ctx) + if err != nil { + return err + } + + graph, err := svc.BranchGraph(ctx, nil) + if err != nil { + return fmt.Errorf("build branch graph: %w", err) + } + + scope := scopeOfTips(status.Tips, cmd.Branches) + if err := validateAdvanceScope(scope, cmd.Branches); err != nil { + return err + } + + plan := advanceTips(graph, status.Tips, scope) + if len(plan) == 0 { + log.Info("No tips to advance.") + return nil + } + + for _, step := range plan { + if err := handler.RemoveTip(ctx, step.from); err != nil { + return fmt.Errorf("remove tip %q: %w", step.from, err) + } + for _, leaf := range step.to { + if err := handler.AddTip(ctx, leaf); err != nil { + return fmt.Errorf("add tip %q: %w", leaf, err) + } + } + log.Infof("Advanced %q to %s.", step.from, formatLeaves(step.to)) + } + return nil +} + +// scopeOfTips returns the set of tip names to advance. An empty +// requested slice selects all currently-configured tips. +func scopeOfTips(tips []integration.TipStatus, requested []string) map[string]struct{} { + scope := make(map[string]struct{}) + if len(requested) == 0 { + for _, t := range tips { + scope[t.Name] = struct{}{} + } + return scope + } + for _, r := range requested { + scope[r] = struct{}{} + } + return scope +} + +// validateAdvanceScope ensures every name in requested is actually a +// configured tip; an unrecognized name aborts before any mutation. +func validateAdvanceScope(scope map[string]struct{}, requested []string) error { + if len(requested) == 0 { + return nil + } + configured := make(map[string]struct{}, len(scope)) + for k := range scope { + configured[k] = struct{}{} + } + for _, r := range requested { + if _, ok := configured[r]; !ok { + return fmt.Errorf("tip %q is not configured", r) + } + } + return nil +} + +// advanceStep records that a configured tip should be replaced with +// the listed leaves. +type advanceStep struct { + from string + to []string +} + +// advanceTips computes the replacement plan. For each tip in scope: +// - If the tip is missing from the graph, skip it. +// - Collect the leaves of its upstack via [BranchGraph.Tops]. +// - If the only leaf is the tip itself, the tip is already at +// the top and is left alone. +// - Otherwise replace it with all leaves, dropping leaves that +// are already configured as other tips (or would duplicate +// leaves already emitted by an earlier step in the plan). +// +// The plan preserves the configured tip order so output is stable. +func advanceTips( + graph *spice.BranchGraph, + tips []integration.TipStatus, + scope map[string]struct{}, +) []advanceStep { + existing := make(map[string]struct{}, len(tips)) + for _, t := range tips { + existing[t.Name] = struct{}{} + } + + var plan []advanceStep + for _, t := range tips { + if _, ok := scope[t.Name]; !ok { + continue + } + if t.Missing { + continue + } + var leaves []string + seen := map[string]struct{}{} + for leaf := range graph.Tops(t.Name) { + if _, dup := seen[leaf]; dup { + continue + } + seen[leaf] = struct{}{} + leaves = append(leaves, leaf) + } + if len(leaves) == 0 { + continue + } + if len(leaves) == 1 && leaves[0] == t.Name { + continue // already at the top + } + // Filter out leaves that would duplicate other configured + // tips. Removing t and adding such a leaf would just collide + // with an existing entry. + filtered := leaves[:0] + for _, leaf := range leaves { + if leaf == t.Name { + continue + } + if _, ok := existing[leaf]; ok { + continue + } + filtered = append(filtered, leaf) + } + if len(filtered) == 0 { + continue + } + // Update existing so subsequent steps don't emit duplicates. + delete(existing, t.Name) + for _, leaf := range filtered { + existing[leaf] = struct{}{} + } + plan = append(plan, advanceStep{from: t.Name, to: filtered}) + } + return plan +} + +// formatLeaves renders a leaf list for the log line. +func formatLeaves(leaves []string) string { + switch len(leaves) { + case 0: + return "" + case 1: + return fmt.Sprintf("%q", leaves[0]) + } + var sb strings.Builder + for i, leaf := range leaves { + if i > 0 { + sb.WriteString(", ") + } + fmt.Fprintf(&sb, "%q", leaf) + } + return sb.String() +} + // subsumption records that tip is fully contained in the upstack of // subsumer, both of which are configured integration tips. type subsumption struct { diff --git a/testdata/help/gs.txt b/testdata/help/gs.txt index 062531cf..3d2b3066 100644 --- a/testdata/help/gs.txt +++ b/testdata/help/gs.txt @@ -31,7 +31,6 @@ Log Stack stack (s) submit (s) Submit a stack - stack (s) merge (m) Merge a stack stack (s) restack (r) Restack a stack stack (s) edit (e) Edit the order of branches in a stack stack (s) delete (d) Delete all branches in a stack @@ -59,7 +58,6 @@ Branch branch (b) restack (r) Restack a branch branch (b) onto (on) Move a branch onto another branch branch (b) diff (di) Show diff between a branch and its base - branch (b) merge (m) Merge a branch into trunk branch (b) submit (s) Submit a branch Commit @@ -89,6 +87,8 @@ Integration integration (int) tip clean (prune) Remove tips whose upstack already contains another tip + integration (int) tip advance Move tips to the topmost branches of their + upstacks Rebase rebase (rb) continue (c) Continue an interrupted operation diff --git a/testdata/help/integration_tip_advance.txt b/testdata/help/integration_tip_advance.txt new file mode 100644 index 00000000..ba6e52f8 --- /dev/null +++ b/testdata/help/integration_tip_advance.txt @@ -0,0 +1,25 @@ +Usage: gs integration (int) tip advance [ ...] + +Move tips to the topmost branches of their upstacks + +For each configured tip (or only the named tips, if any are given as arguments), +replaces the tip with the topmost branch(es) in its upstack — the leaves of the +tree above it. + +If a tip already has no branches above it, it is left alone. If a tip's upstack +forks, the tip expands to every leaf of the fork. Branches that would become +duplicates of other configured tips are deduplicated. + +Useful as a one-shot maintenance step after extending a stack above a configured +tip: instead of 'tip remove old' + 'tip add new', a single 'tip advance' walks +each tip to its current top. + +Arguments: + [ ...] Tips to advance; defaults to all configured tips + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/script/integration_tip_advance.txt b/testdata/script/integration_tip_advance.txt new file mode 100644 index 00000000..d5d1ac18 --- /dev/null +++ b/testdata/script/integration_tip_advance.txt @@ -0,0 +1,90 @@ +# Verifies 'gs integration tip advance' moves each configured tip +# to the topmost branch(es) in its upstack — i.e., the leaf branches +# that have no branches above them. This is the natural maintenance +# operation after adding new work above a tip: instead of asking the +# user to manually 'tip remove' + 'tip add', the same effect is +# achieved in one step. + +as 'Test ' +at '2025-06-01T00:00:00Z' + +cd repo +git init +git commit --allow-empty -m 'Initial commit' +gs repo init + +# Linear stack: main -> low -> middle -> high +git add low.txt +gs branch create low -m 'Add low' +git add middle.txt +gs branch create middle -m 'Add middle' +git add high.txt +gs branch create high -m 'Add high' + +# Forked stack: main -> base -> fork-a +# -> fork-b +gs trunk +git add base.txt +gs branch create base -m 'Add base' +git add fork-a.txt +gs branch create fork-a -m 'Add fork-a' + +gs branch checkout base +git add fork-b.txt +gs branch create fork-b -m 'Add fork-b' + +# Configure only the lower branches as tips: 'low' (linear) and +# 'base' (forked). After advance: +# - 'low' moves up to 'high' (single leaf) +# - 'base' expands to 'fork-a' and 'fork-b' (two leaves) +gs trunk +gs integration create preview --tip low --tip base + +gs integration tip advance +stderr 'Advanced "low" to "high"' +stderr 'Advanced "base" to "fork-a", "fork-b"' + +# The new tip list is the union of all leaves, in encounter order +# (the configured order of source tips, then per-tip leaves). +gs integration tip list +cmp stdout $WORK/golden/tips-after.txt + +# A second run is a no-op: each tip is already a leaf. +gs integration tip advance +stderr 'No tips to advance' + +# Advance can be scoped to a single tip with the optional argument. +# Add a new branch above fork-a, then advance only fork-a. +gs branch checkout fork-a +git add above-a.txt +gs branch create above-a -m 'Add above-a' + +gs integration tip advance fork-a +stderr 'Advanced "fork-a" to "above-a"' +! stderr 'Advanced "fork-b"' + +gs integration tip list +cmp stdout $WORK/golden/tips-after-scoped.txt + +-- repo/low.txt -- +low +-- repo/middle.txt -- +middle +-- repo/high.txt -- +high +-- repo/base.txt -- +base +-- repo/fork-a.txt -- +fork a +-- repo/fork-b.txt -- +fork b +-- repo/above-a.txt -- +above a +-- golden/tips-after.txt -- +high +fork-a +fork-b +-- golden/tips-after-scoped.txt -- +high +fork-b +above-a From 3e7d04a7f62365ac009d58ff734311dc2b8af94e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:56:47 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- doc/includes/cli-reference.md | 75 +++++++++++++++++++++++++++++++++-- testdata/help/gs.txt | 2 + 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index c45f2ce5..a501b3c1 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -293,6 +293,48 @@ only if there are multiple CRs in the stack. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.labels](/cli/config.md#spicesubmitlabels), [spice.submit.labels.addWhen](/cli/config.md#spicesubmitlabelsaddwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.updateOnly](/cli/config.md#spicesubmitupdateonly), [spice.submit.web](/cli/config.md#spicesubmitweb) +### git-spice stack merge {#gs-stack-merge} + +``` +gs stack (s) merge (m) [flags] +``` + +:material-test-tube:{ title="Experimental" }[merge](/cli/experiments.md#merge) + +Merge a stack + +Merges the CRs for the current branch's stack into trunk. +Use --branch to merge a different branch's stack. + +The stack includes the selected branch, +its downstack branches down to trunk, +and every upstack branch. + +Already-merged branches are skipped automatically. +Branches must have an open Change Request to be merged. + +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. + +By default, a branch failure skips that branch's upstack descendants, +but independent sibling branches continue. +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. +* `--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) + ### git-spice stack restack {#gs-stack-restack} ``` @@ -622,11 +664,11 @@ when you don't want to wait for the merge to propagate. **Flags** -* `--branch=NAME`: Branch to start merging from -* `--no-wait`: Skip polling for a single branch merge to propagate. -* `--no-branch-check`: Skip stale base validation before merging. * `--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. +* `--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) @@ -1082,6 +1124,33 @@ Use --branch to target a different branch. * `--branch=NAME`: Branch to diff +### git-spice branch merge {#gs-branch-merge} + +``` +gs branch (b) merge (m) [flags] +``` + +:material-test-tube:{ title="Experimental" }[merge](/cli/experiments.md#merge) + +Merge a branch into trunk + +Merges the CR for the current branch into trunk. +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. + +**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. +* `--branch=NAME`: Branch to merge + +**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) + ### git-spice branch submit {#gs-branch-submit} ``` diff --git a/testdata/help/gs.txt b/testdata/help/gs.txt index 3d2b3066..82897235 100644 --- a/testdata/help/gs.txt +++ b/testdata/help/gs.txt @@ -31,6 +31,7 @@ Log Stack stack (s) submit (s) Submit a stack + stack (s) merge (m) Merge a stack stack (s) restack (r) Restack a stack stack (s) edit (e) Edit the order of branches in a stack stack (s) delete (d) Delete all branches in a stack @@ -58,6 +59,7 @@ Branch branch (b) restack (r) Restack a branch branch (b) onto (on) Move a branch onto another branch branch (b) diff (di) Show diff between a branch and its base + branch (b) merge (m) Merge a branch into trunk branch (b) submit (s) Submit a branch Commit From cfef01c16ad060274e2daea9759702777481c785 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:38:36 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- doc/includes/cli-reference.md | 36 ++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index a501b3c1..89bbde47 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -317,9 +317,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. @@ -328,12 +330,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} @@ -631,7 +633,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: @@ -649,13 +651,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 @@ -665,12 +669,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} @@ -1140,16 +1144,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}