Skip to content

Merge Stacked PRs with merge command - #307

Open
skarim wants to merge 7 commits into
skarim/docs-minor-updatesfrom
skarim/merge-cmd
Open

Merge Stacked PRs with merge command#307
skarim wants to merge 7 commits into
skarim/docs-minor-updatesfrom
skarim/merge-cmd

Conversation

@skarim

@skarim skarim commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

This adds a new gh stack merge command built on GitHub's new async merge API, which can land multiple stacked PRs atomically. It merges some or all of a stack in one atomic operation: every PR up to and including the one you choose is merged into the base branch, and if any PR can't be merged, none are.

What's new

  • Three ways to target a merge. No argument merges the current active local stack; a stack number merges a stack you don't have checked out (a purely remote operation); a PR number merges directly up to that PR.
  • Interactive wizard. In a terminal, a three-step wizard lets you choose how far up the stack to merge (a bottom-anchored checkbox list, so everything below your selection is always included), pick the merge method (only the methods the repo allows, defaulting to your last-used one), and confirm — then a live view polls the merge to completion.
  • Non-interactive / agent-friendly. With --yes or no TTY, the whole stack (or everything up to the given PR) merges without prompting, using your last-used merge method unless one is given via --merge / --squash / --rebase / --merge-method.
  • Light pre-checks only. PRs are checked for being open and non-draft; GitHub evaluates branch protection and repository rules at merge time and reports the failure reason back. Because the merge is atomic, a failure lands nothing.
  • Merge queues are handled automatically. The merge is submitted with merge_action: "default", so GitHub either merges the stack directly or, when the base branch uses a merge queue, adds it to the queue (reported back as an enqueued result). Bypassing merge requirements with admin privileges is not supported for stacks.

Changes

The command

  • cmd/merge.go — the new gh stack merge command: resolves the target stack (current branch / stack number / PR number), computes the mergeable candidates (open, non-draft, contiguous from the bottom), and runs either the interactive wizard or the headless submit-and-poll path. Registered in cmd/root.go.

GitHub client

  • internal/github/merge_async.go — an async stack-merge client over the REST API: MergeStackAsync (submit) and GetAsyncMergeResult (poll) on go-gh's REST client, returning a status enum (pending / merged / enqueued / failed); plus RepoMergeConfig (allowed methods and last-used default, via GraphQL) and PRTitles (batch PR titles for the picker). Added to ClientOps and MockClient.

Wizard TUI

  • internal/tui/mergeview/ (new package) — the Bubble Tea wizard: a stepper banner, the bottom-anchored PR picker with a scrolling viewport, the allowed-method picker, the confirm step, and the live progress view.

Docs

  • README.md, docs/ CLI reference and workflows guide, skills/gh-stack/SKILL.md, and AGENTS.md — document the command, its targeting and merge-method behavior, and how merges route to a direct merge or the merge queue.

Testing

  • Unit tests for the command paths (cmd/merge_test.go), the async-merge client including status decoding and error mapping (internal/github/merge_async_test.go), and the wizard model (internal/tui/mergeview/model_test.go).
  • go test -race -count=1 ./... and go vet ./... are clean, and the changed files pass gofmt.

Stack created with GitHub Stacks CLIGive Feedback 💬

@skarim
skarim force-pushed the skarim/merge-cmd branch 2 times, most recently from a14ba2a to c8efa62 Compare July 25, 2026 17:06
@skarim
skarim force-pushed the skarim/merge-cmd branch 2 times, most recently from d69a1b4 to 0ff36e1 Compare July 26, 2026 00:33
Base automatically changed from skarim/docs-content-updates to main July 26, 2026 15:26
@skarim
skarim force-pushed the skarim/merge-cmd branch from 0ff36e1 to f0ec7d5 Compare July 26, 2026 15:29
@skarim
skarim marked this pull request as ready for review July 28, 2026 21:08
Copilot AI review requested due to automatic review settings July 28, 2026 21:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds gh stack merge for atomic stacked-PR merges through GitHub’s async merge API.

Changes:

  • Adds local, stack-number, and PR-number merge targeting.
  • Adds interactive and headless merge workflows.
  • Extends GitHub clients, tests, and documentation.
Show a summary per file
File Description
cmd/merge.go Implements merge command behavior.
cmd/merge_test.go Tests merge command paths.
cmd/root.go Registers the command.
cmd/root_test.go Tests command registration.
internal/github/merge_async.go Adds async merge API support.
internal/github/merge_async_test.go Tests async merge handling.
internal/github/client_interface.go Extends the client interface.
internal/github/mock_client.go Extends the mock client.
internal/tui/mergeview/model.go Implements wizard state and polling.
internal/tui/mergeview/model_test.go Tests wizard behavior.
internal/tui/mergeview/types.go Defines wizard types.
internal/tui/mergeview/view.go Renders the wizard.
README.md Documents the command.
docs/src/content/docs/reference/cli.md Adds CLI reference material.
docs/src/content/docs/guides/workflows.md Adds merge workflow guidance.
skills/gh-stack/SKILL.md Adds agent-facing merge guidance.
AGENTS.md Updates architecture documentation.
.github/copilot-instructions.md Updates Copilot repository guidance.

Review details

Comments suppressed due to low confidence (1)

internal/tui/mergeview/model.go:292

  • A polling transport/API error does not mean the server-side merge failed—the request may still complete—but this marks it failed and the command reports merge failed. That can make users retry an in-flight atomic merge or believe nothing landed. Preserve an unknown/watch-stopped outcome and report that only status polling stopped.
func (m Model) handlePollDone(msg pollDoneMsg) (tea.Model, tea.Cmd) {
	if msg.err != nil {
		m.err = msg.err
		m.failed = true
		m.message = msg.err.Error()
		return m.finish()
  • Files reviewed: 18/18 changed files
  • Comments generated: 13
  • Review effort level: Medium

Comment thread cmd/merge.go
Comment on lines +180 to +183
// Try as a stack number first (mirrors `gh stack checkout`).
rs, err := client.GetStack(n)
if err == nil && rs != nil {
return rs, mergeTarget{}, nil
Comment on lines +388 to +394
if w >= width-1 {
b.WriteString("…")
b.WriteString("\x1b[0m")
break
}
b.WriteRune(r)
w++
Comment on lines +152 to +154
// The merge_action is always "default", which lets the server route the stack
// to a direct merge or the base branch's merge queue automatically, based on
// the repository's rules and configuration.
Comment on lines +185 to +189
func (m Model) visibleItems() int {
n := len(m.opts.PRs)
if m.height <= 0 {
return n
}
Comment thread cmd/merge.go
Comment on lines +151 to +155
// Non-interactive (or --yes): merge the whole stack (or up to the given PR)
// without prompting.
if !target.hasPR {
targetPR = candidates[len(candidates)-1].Number
}

Only basic pull request state is checked before merging (open and not a draft); GitHub evaluates branch protection and repository rules when the merge runs, so any such failure is reported back to you. **Bypassing merge requirements is not supported** for stacked PR merges.

If the base branch uses a merge queue, the stack is added to the queue and merges once the queue processes it; otherwise it's merged directly.

In an interactive terminal, a short wizard lets you choose how far up the stack to merge, pick the merge method (only the ones your repository allows, defaulting to your last-used method), and confirm — then shows live progress. In a non-interactive terminal, or with `--yes`, the whole stack (or everything up to the given PR) is merged without prompting. After merging, run `gh stack sync` to update your local branches.

If the base branch uses a merge queue, `gh stack merge` adds the stack to the queue instead of merging directly — it merges once the queue processes it.
Comment thread skills/gh-stack/SKILL.md
7. **Use standard `git add` and `git commit` for staging and committing.** This gives you full control over which changes go into each branch. The `-Am` shortcut is available but should not be the default approach—stacked PRs are most effective when each branch contains a deliberate, logical set of changes.
8. **Navigate down the stack when you need to change a lower layer.** If you're working on a frontend branch and realize you need API changes, don't hack around it at the current layer. Navigate to the appropriate branch (`gh stack down`, `gh stack checkout`, or `gh stack bottom`), make and commit the changes there, run `gh stack rebase --upstack`, then navigate back up to continue.
9. **Use `gh stack link` for external tool workflows.** When branches are managed by an external tool (jj, Sapling, etc.), use `gh stack link branch-a branch-b`. `link` does not rely on local tracking state and is intended for API-driven PR and stack management. Provide at least two branches/PRs to create or update a stack, or a stack number followed by the new branches/PRs to append them to the top of an existing stack (e.g. `gh stack link 7 branch-c`).
10. **Use `gh stack merge --yes` to merge stacked PRs.** `gh pr merge` does not work with stacked PRs. In a non-interactive terminal `gh stack merge` runs without prompting and merges the entire stack (bottom to top) atomically; pass `--yes` to be explicit. Scope the merge by passing a pull request number (`gh stack merge 42 --yes` merges everything up to and including PR #42) or a stack number (`gh stack merge 7 --yes`, which needs no local checkout). Choose the method with `--squash`, `--rebase`, `--merge`, or `--merge-method <method>`; without one, the last-used method is used. The merge is all-or-nothing — if any PR can't be merged, none are, and the failure reason is reported. Only basic pull request state is checked before merging (open and not a draft); bypassing merge requirements is not supported for stacks. If the base branch uses a merge queue, the stack is added to the queue and merges once the queue processes it; otherwise it's merged directly.
Comment thread cmd/merge.go
Comment on lines +69 to +70
If the base branch uses a merge queue, the stack is added to the queue and merges
once the queue processes it; otherwise it is merged directly.`,
- `cmd/`: One Cobra command per file. Each exports `<Name>Cmd(cfg *config.Config)` with logic in `run<Name>()`.
- `internal/git/`: `Ops` interface (52 methods) wrapping git CLI. `MockOps` for tests. Package-level functions delegate to swappable `ops` variable.
- `internal/github/`: `ClientOps` interface (13 methods) for GitHub API. `MockClient` for tests. Stack operations use the public Stacks REST API (`/repos/{owner}/{repo}/stacks`).
- `internal/github/`: `ClientOps` interface (17 methods) for GitHub API. `MockClient` for tests. Stack operations use the public Stacks REST API (`/repos/{owner}/{repo}/stacks`); merges use the async merge API (`/repos/{owner}/{repo}/pulls/{n}/merge-async`), which routes to a direct merge or the base branch's merge queue automatically.
@skarim
skarim changed the base branch from main to skarim/docs-minor-updates July 28, 2026 21:33
skarim added 7 commits July 28, 2026 17:34
Follow-up polish for `gh stack merge` (the command itself landed in the
previous commit). These changes refine the interactive wizard, enrich the
PR picker, and replace the merge client's bespoke HTTP handling with the
standard go-gh REST client.

Wizard and stepper:
- Redesign the top stepper as a segmented bar: completed steps are green,
  the active step is the brightest, and upcoming steps are dimmed. Steps
  are separated by a Powerline arrow that blends into the shading, with a
  graceful fallback to abutting segments on terminals that lack the glyph
  (e.g. Apple Terminal). Set GH_STACK_POWERLINE=1/0 to override detection.
- Show the stack number in the header ("Merge stack #123").
- Hide the header and stepper once the merge is submitted so the live
  progress view stands on its own.

PR picker:
- Render each pull request on two lines: the title (white/black, a touch
  bolder when selected) above its "#number • branch" (gray, fainter when
  deselected). Titles are fetched in one batched GraphQL query (PRTitles)
  and fall back to the branch name.
- Scroll long stacks in a fixed 10-item window with persistent "N more"
  indicators, so the list no longer jumps as those hints appear and
  disappear. Add shift+up / shift+down to jump to the top or bottom.

Progress and outcome:
- Always render a status line ("Submitting merge request...") so it does
  not pop in later and shift the view, and normalize messages to end in an
  ellipsis.
- Print the final result from the command layer rather than the TUI: a
  success line that includes the merge commit SHA
  ("Merged #1, #2 into main (abc1234)"), an atomic-rollback note on
  failure, a distinct message when the user stops watching an in-flight
  merge, and "Cancelled operation, nothing merged" on cancel.
- Clamp every rendered line to the terminal width so resizing no longer
  leaves duplicated header lines behind, and make truncation ANSI-aware.

Async-merge client:
- Use the go-gh REST client (c.rest.Put / c.rest.Get) for both the submit
  and poll endpoints, removing the bespoke http.Client, base-URL helper,
  and manual response decoding. The REST client discards non-2xx bodies,
  but that only costs the rare 400 message and 409 UUID: real merge
  failures still surface through the 200 poll body, and the in-range PRs
  are validated open, non-draft, and non-merged before submitting.
- Add classifyAsyncMergeError to map status codes to clear errors (404
  unavailable, 409 already exists, 400 no longer mergeable) and drop the
  now-unused AsyncMergeResult.StatusCode field. Rework the client tests to
  drive the REST client through a stub http.RoundTripper.
@skarim
skarim force-pushed the skarim/merge-cmd branch from 5847fdd to a4ae722 Compare July 28, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants