Merge Stacked PRs with merge command - #307
Open
skarim wants to merge 7 commits into
Open
Conversation
skarim
force-pushed
the
skarim/merge-cmd
branch
2 times, most recently
from
July 25, 2026 17:06
a14ba2a to
c8efa62
Compare
skarim
force-pushed
the
skarim/merge-cmd
branch
2 times, most recently
from
July 26, 2026 00:33
d69a1b4 to
0ff36e1
Compare
skarim
marked this pull request as ready for review
July 28, 2026 21:08
Contributor
There was a problem hiding this comment.
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 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 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. |
| 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 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. |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a new
gh stack mergecommand 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
--yesor 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.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 anenqueuedresult). Bypassing merge requirements with admin privileges is not supported for stacks.Changes
The command
cmd/merge.go— the newgh stack mergecommand: 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 incmd/root.go.GitHub client
internal/github/merge_async.go— an async stack-merge client over the REST API:MergeStackAsync(submit) andGetAsyncMergeResult(poll) on go-gh's REST client, returning astatusenum (pending/merged/enqueued/failed); plusRepoMergeConfig(allowed methods and last-used default, via GraphQL) andPRTitles(batch PR titles for the picker). Added toClientOpsandMockClient.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, andAGENTS.md— document the command, its targeting and merge-method behavior, and how merges route to a direct merge or the merge queue.Testing
cmd/merge_test.go), the async-merge client includingstatusdecoding and error mapping (internal/github/merge_async_test.go), and the wizard model (internal/tui/mergeview/model_test.go).go test -race -count=1 ./...andgo vet ./...are clean, and the changed files passgofmt.Stack created with GitHub Stacks CLI • Give Feedback 💬