Skip to content

Merge Stacked PRs with merge command#307

Draft
skarim wants to merge 6 commits into
mainfrom
skarim/merge-cmd
Draft

Merge Stacked PRs with merge command#307
skarim wants to merge 6 commits into
mainfrom
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 detected and rejected. If the base branch uses a merge queue, the command stops before doing anything and points you to gh pr merge — the stack merge lands directly, not through a queue. Admin bypass is likewise not applied by the stack-merge API.

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 / failed); plus RepoMergeConfig (allowed methods and last-used default, via GraphQL), BaseBranchPolicy (merge-queue detection), 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 the merge-queue limitation.

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).
  • Live-validated against a real repo: headless and interactive merges, partial (up-to-a-PR) merges, atomic rollback on a failing/conflicting PR, and merge-queue rejection.
  • 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/docs-content-updates branch from 379fd43 to ace18c0 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
@skarim
skarim force-pushed the skarim/docs-content-updates branch from bdc11e1 to 094328f Compare July 26, 2026 13:41
Base automatically changed from skarim/docs-content-updates to main July 26, 2026 15:26
skarim added 6 commits July 26, 2026 11:29
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 0ff36e1 to f0ec7d5 Compare July 26, 2026 15:29
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.

1 participant