From 476efaa00e377cb814763d87451fba427bbb7ee9 Mon Sep 17 00:00:00 2001 From: Edmund Kohlwey Date: Tue, 2 Jun 2026 18:30:41 -0400 Subject: [PATCH 1/3] submodule: gs repo init --recurse-submodules --- .../unreleased/Added-20260514-074144.yaml | 3 + doc/includes/cli-reference.md | 76 +-------- internal/git/submodule.go | 52 +++++- internal/git/submodule_test.go | 9 +- repo_init.go | 158 +++++++++++++++++- testdata/help/repo_init.txt | 11 +- testdata/script/repo_init_no_recurse.txt | 29 ++++ testdata/script/repo_init_recurse.txt | 32 ++++ 8 files changed, 289 insertions(+), 81 deletions(-) create mode 100644 .changes/unreleased/Added-20260514-074144.yaml create mode 100644 testdata/script/repo_init_no_recurse.txt create mode 100644 testdata/script/repo_init_recurse.txt diff --git a/.changes/unreleased/Added-20260514-074144.yaml b/.changes/unreleased/Added-20260514-074144.yaml new file mode 100644 index 000000000..9f8894aa3 --- /dev/null +++ b/.changes/unreleased/Added-20260514-074144.yaml @@ -0,0 +1,3 @@ +kind: Added +body: 'submodule: ''gs repo init'' offers to initialize tracked submodules recursively (--recurse-submodules/--no-recurse-submodules), resolving each sub''s trunk via .gitmodules branch, HEAD upstream, or the guesser. On opt-in, ''spice.submodule.recurse=true'' is written so daily ops will recurse.' +time: 2026-05-14T07:41:44.8119-04:00 diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index a5bc2c1f0..697fddf62 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -140,6 +140,7 @@ and untrack all branches. * `--remote=NAME`: Name of the remote to push submitted branches to * `--upstream=NAME`: Name of the remote to open change requests against * `--reset`: Forget all information about the repository +* `--[no-]recurse-submodules`: Also initialize tracked submodules. Prompts when unset and submodules are present. ### git-spice repo sync {#gs-repo-sync} @@ -286,48 +287,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} ``` @@ -657,11 +616,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) @@ -1119,33 +1078,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} ``` diff --git a/internal/git/submodule.go b/internal/git/submodule.go index 587a4564d..6cb2c5541 100644 --- a/internal/git/submodule.go +++ b/internal/git/submodule.go @@ -19,6 +19,12 @@ type Submodule struct { // URL is the configured remote URL. URL string + + // Branch is the upstream-tracking branch declared via + // `submodule..branch` in .gitmodules, or "" if unset. + // Used by `git submodule update --remote` and consumed by + // git-spice as a trunk hint during recursive init. + Branch string } // Submodules lists all submodules in the worktree. @@ -84,9 +90,19 @@ func submoduleFromConfigKey( url = "" // URL may be missing for local submodules. } + branchKey := "submodule." + name + ".branch" + branch, err := w.gitCmd(ctx, + "config", "--file", ".gitmodules", + "--get", branchKey, + ).OutputChomp() + if err != nil { + branch = "" // .gitmodules branch is optional. + } + return Submodule{ - Path: strings.TrimSpace(path), - URL: strings.TrimSpace(url), + Path: strings.TrimSpace(path), + URL: strings.TrimSpace(url), + Branch: strings.TrimSpace(branch), }, nil } @@ -257,6 +273,38 @@ func (w *Worktree) SubmoduleHasGsStore( return true, nil } +// GitConfigGet reads a single value from the worktree's local +// git-config. Returns the empty string and a nil error if unset. +func (w *Worktree) GitConfigGet( + ctx context.Context, key string, +) (string, error) { + out, err := w.gitCmd(ctx, + "config", "--get", key, + ).OutputChomp() + if err != nil { + // `git config --get` exits 1 when the key is missing; + // treat that as the empty string. + var exitErr *xec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 { + return "", nil + } + return "", fmt.Errorf("git config --get %s: %w", key, err) + } + return out, nil +} + +// GitConfigSet writes a value to the worktree's local git-config. +func (w *Worktree) GitConfigSet( + ctx context.Context, key, value string, +) error { + if err := w.gitCmd(ctx, + "config", "--local", key, value, + ).Run(); err != nil { + return fmt.Errorf("git config %s: %w", key, err) + } + return nil +} + // AddUpdate stages updates to all tracked files in the worktree // (`git add -u`). Untracked files are not affected. func (w *Worktree) AddUpdate(ctx context.Context) error { diff --git a/internal/git/submodule_test.go b/internal/git/submodule_test.go index d8cbcb108..715dd0a09 100644 --- a/internal/git/submodule_test.go +++ b/internal/git/submodule_test.go @@ -50,9 +50,14 @@ func TestSubmodules(t *testing.T) { t.Run("SubmoduleWorktree", func(t *testing.T) { subWt, err := parentWt.SubmoduleWorktree(ctx, "libs/core") require.NoError(t, err) + // git rev-parse --show-toplevel returns forward slashes + // on all platforms, including Windows. + // Normalize both sides for comparison. assert.Equal(t, - joinSlash(parentWt.RootDir(), "libs", "core"), - joinSlash(subWt.RootDir()), + filepath.ToSlash( + filepath.Join(parentWt.RootDir(), "libs", "core"), + ), + filepath.ToSlash(subWt.RootDir()), ) }) } diff --git a/repo_init.go b/repo_init.go index 79ba052ff..7ce88ba3a 100644 --- a/repo_init.go +++ b/repo_init.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "strings" "go.abhg.dev/gs/internal/cli" "go.abhg.dev/gs/internal/git" @@ -27,7 +28,8 @@ type repoInitCmd struct { Remote string `placeholder:"NAME" predictor:"remotes" help:"Name of the remote to push submitted branches to"` Upstream string `placeholder:"NAME" predictor:"remotes" help:"Name of the remote to open change requests against"` - Reset bool `help:"Forget all information about the repository"` + Reset bool `help:"Forget all information about the repository"` + Recurse *bool `name:"recurse-submodules" negatable:"" help:"Also initialize tracked submodules. Prompts when unset and submodules are present."` } func (*repoInitCmd) Help() string { @@ -110,9 +112,163 @@ func (cmd *repoInitCmd) Run( } log.Info("Initialized repository", "trunk", cmd.Trunk) + + if err := cmd.maybeRecurseSubmodules(ctx, log, view, wt); err != nil { + // Recursive init is opportunistic: surface as a warning, + // not a hard failure of the parent init. + log.Warn("Recursive submodule init failed", "error", err) + } + + return nil +} + +// maybeRecurseSubmodules optionally initializes git-spice in each +// tracked submodule and persists `spice.submodule.recurse=true` for +// future ops to pick up. +func (cmd *repoInitCmd) maybeRecurseSubmodules( + ctx context.Context, + log *silog.Logger, + view ui.View, + wt *git.Worktree, +) error { + subs, err := wt.Submodules(ctx) + if err != nil { + return fmt.Errorf("list submodules: %w", err) + } + if len(subs) == 0 { + return nil + } + + // Resolve whether to recurse: + // - explicit flag wins; + // - interactive: prompt with default Yes; + // - non-interactive: default Yes for non-interactive scripts. + var recurse bool + switch { + case cmd.Recurse != nil: + recurse = *cmd.Recurse + case ui.Interactive(view): + recurse = true + prompt := ui.NewConfirm(). + WithTitle(fmt.Sprintf( + "Initialize %d submodule(s) with git-spice too?", + len(subs))). + WithDescription( + "You can rerun 'gs repo init --no-recurse-submodules' to skip later."). + WithValue(&recurse) + if err := ui.Run(view, prompt); err != nil { + return fmt.Errorf("submodule prompt: %w", err) + } + default: + recurse = true + } + + if !recurse { + return nil + } + + for _, sub := range subs { + if err := cmd.initOneSubmodule(ctx, log, view, wt, sub); err != nil { + log.Warn("Submodule init failed", + "path", sub.Path, "error", err) + continue + } + } + + // Persist the recurse preference for future ops, if unset. + if err := writeRecurseConfigIfUnset(ctx, wt, log); err != nil { + log.Warn("Could not set spice.submodule.recurse", + "error", err) + } + + return nil +} + +// writeRecurseConfigIfUnset writes spice.submodule.recurse=true to +// the worktree's local git-config only when the key is absent so +// an explicit user opt-out is never overridden. +func writeRecurseConfigIfUnset( + ctx context.Context, wt *git.Worktree, log *silog.Logger, +) error { + if existing, err := wt.GitConfigGet(ctx, "spice.submodule.recurse"); err == nil && existing != "" { + // Already explicitly set — never override. + return nil + } + if err := wt.GitConfigSet(ctx, + "spice.submodule.recurse", "true"); err != nil { + return err + } + log.Info( + "Submodule recursion enabled. " + + "Use 'git config spice.submodule.recurse false' to opt out.", + ) return nil } +// initOneSubmodule initializes git-spice inside a single submodule. +// Trunk is resolved by precedence: +// 1. submodule..branch in .gitmodules (tracking branch); +// 2. submodule HEAD's upstream branch; +// 3. submodule's repo guesser (interactive prompt or auto-detect). +func (cmd *repoInitCmd) initOneSubmodule( + ctx context.Context, + log *silog.Logger, + view ui.View, + parentWT *git.Worktree, + sub git.Submodule, +) error { + has, err := parentWT.SubmoduleHasGsStore(ctx, sub.Path) + if err != nil { + return fmt.Errorf("check gs store: %w", err) + } + if has { + log.Info("Submodule already initialized", + "path", sub.Path) + return nil + } + + subWT, err := parentWT.SubmoduleWorktree(ctx, sub.Path) + if err != nil { + return fmt.Errorf("open submodule worktree: %w", err) + } + subRepo := subWT.Repository() + + trunk, src := resolveSubmoduleTrunk(ctx, subWT, sub) + log.Info("Initializing submodule", + "path", sub.Path, + "trunk", trunk, + "source", src) + + return (&repoInitCmd{ + Trunk: trunk, + Reset: cmd.Reset, + }).Run(ctx, log, view, subRepo, subWT) +} + +// resolveSubmoduleTrunk picks the trunk branch for a submodule init +// based on .gitmodules, HEAD's upstream, or guesser fallback. +func resolveSubmoduleTrunk( + ctx context.Context, + subWT *git.Worktree, + sub git.Submodule, +) (trunk string, source string) { + if sub.Branch != "" && subWT.Repository().BranchExists(ctx, sub.Branch) { + return sub.Branch, ".gitmodules" + } + cur, err := subWT.CurrentBranch(ctx) + if err == nil { + if upstream, err := subWT.Repository().BranchUpstream(ctx, cur); err == nil { + // upstream is e.g. "origin/main"; strip the remote prefix. + if _, branch, ok := strings.Cut(upstream, "/"); ok { + return branch, "HEAD upstream" + } + } + return cur, "current branch" + } + // Falls back to empty so the child repoInitCmd guesses interactively. + return "", "guess" +} + // repoInitRemoteGuesser guesses remotes for repository initialization. type repoInitRemoteGuesser interface { GuessUpstreamRemote(context.Context, spice.GitRepository) (string, error) diff --git a/testdata/help/repo_init.txt b/testdata/help/repo_init.txt index 7de6ed72f..75e95fdd8 100644 --- a/testdata/help/repo_init.txt +++ b/testdata/help/repo_init.txt @@ -20,10 +20,13 @@ stacked on the old trunk will be updated to point to the new trunk. Re-run with --reset to discard all stored information and untrack all branches. Flags: - --trunk=BRANCH Name of the trunk branch - --remote=NAME Name of the remote to push submitted branches to - --upstream=NAME Name of the remote to open change requests against - --reset Forget all information about the repository + --trunk=BRANCH Name of the trunk branch + --remote=NAME Name of the remote to push submitted branches to + --upstream=NAME Name of the remote to open change requests + against + --reset Forget all information about the repository + --[no-]recurse-submodules Also initialize tracked submodules. Prompts when + unset and submodules are present. Global Flags: -h, --help Show help for the command diff --git a/testdata/script/repo_init_no_recurse.txt b/testdata/script/repo_init_no_recurse.txt new file mode 100644 index 000000000..5ac641c84 --- /dev/null +++ b/testdata/script/repo_init_no_recurse.txt @@ -0,0 +1,29 @@ +# 'gs repo init --no-recurse-submodules' skips submodule initialization +# and does not set spice.submodule.recurse. + +as 'Test ' +at '2026-03-11T15:00:00Z' + +cd sub-repo +git init +git commit --allow-empty -m 'Initial commit' + +cd $WORK/repo +git init +git commit --allow-empty -m 'Initial commit' +git -c protocol.file.allow=always submodule add $WORK/sub-repo libs/core +git commit -m 'Add submodule' + +gs repo init --trunk=main --no-recurse-submodules + +# Parent initialized. +git rev-parse --verify refs/spice/data + +# Sub NOT initialized. +! git -C libs/core rev-parse --verify refs/spice/data + +# Config not set. +! git config --local spice.submodule.recurse + +-- repo/.keep -- +-- sub-repo/.keep -- diff --git a/testdata/script/repo_init_recurse.txt b/testdata/script/repo_init_recurse.txt new file mode 100644 index 000000000..f5d1a9e80 --- /dev/null +++ b/testdata/script/repo_init_recurse.txt @@ -0,0 +1,32 @@ +# 'gs repo init --recurse-submodules' initializes git-spice in +# each submodule and sets spice.submodule.recurse=true. + +as 'Test ' +at '2026-03-11T15:00:00Z' + +# Sub repo (no gs init). +cd sub-repo +git init +git commit --allow-empty -m 'Initial commit' + +# Parent repo with submodule added but no init yet. +cd $WORK/repo +git init +git commit --allow-empty -m 'Initial commit' +git -c protocol.file.allow=always submodule add $WORK/sub-repo libs/core +git commit -m 'Add submodule' + +# Initialize parent with recurse-submodules. +gs repo init --trunk=main --recurse-submodules + +# Both repos should be initialized. +git -C libs/core rev-parse --verify refs/spice/data +git rev-parse --verify refs/spice/data + +# The local config should be set. +git config --local spice.submodule.recurse +stdout 'true' + +-- sub-repo/feat.txt -- +sub feature +-- repo/.keep -- From 4f82ef2e5398259b5c7c89795f6b16683cc7355e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:01:45 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- doc/includes/cli-reference.md | 75 +++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index 697fddf62..2acadd6a2 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -287,6 +287,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} ``` @@ -616,11 +658,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) @@ -1078,6 +1120,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} ``` From 83ab9054e5192b45ea863a609a37fa322e7f7822 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:33:37 +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 2acadd6a2..228161668 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -311,9 +311,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. @@ -322,12 +324,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} @@ -625,7 +627,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: @@ -643,13 +645,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 @@ -659,12 +663,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} @@ -1136,16 +1140,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}