GitHub Stacked Pull Requests in Public Preview: A Practical Test with gh-stack
GitHub now offers native Stacked Pull Requests in Public Preview. A hands-on test with the gh-stack CLI extension and what the workflow actually changes in practice.
A large pull request is convenient for the author and punishing for the reviewer. When a feature is built in one sweep, with data model, API and UI piled into the same mountain of commits, the reviewer has to understand everything at once before giving useful feedback.
The obvious alternative is to split the change into several small pull requests that build on one another. That is exactly what Stacked Pull Requests are: PR 2 is based on PR 1, PR 3 on PR 2, and so on. Every layer can be reviewed separately without waiting for the rest. Until now, the difficult part has been the mechanics. Without tooling, a stack means constant manual rebasing, incorrect base branches in the PR interface and a merge order that somebody has to keep track of manually.
Tools such as Graphite, git-spice and Sapling have solved that problem for years, but as an external layer on top of GitHub. Since 30 July 2026 GitHub has supported the workflow natively: Stacked Pull Requests are in Public Preview and are rolling out to all repositories over the following days. I tested the feature on a real small project with four pull requests that genuinely depended on one another and were merged as a stack.
What stacks solve in daily work
The obvious benefit is diff size. The two effects behind it are more interesting.
The first is that the author no longer has to stop. Without a stack, opening a PR leaves two options: wait for the review to finish, or keep working and push the next change into the same PR, making it larger. Neither is good. With a stack, the next layer can be placed on top while the lower one is still under review. The team’s review latency no longer determines the author’s pace of work.
The second effect is fault isolation. When a change reaches main as four thematically separated commits instead of one large one, unexpected behaviour can be assigned much more precisely and reverted selectively. A revert then affects the layer that introduced the problem rather than the complete feature.
There is also an organisational detail that is easy to miss: different layers often need different reviewers. The database migration should be reviewed by somebody different from the UI component. In one large PR, both have to scroll through the complete diff and find the part that concerns them. In a stack, each reviewer receives exactly the layer for which he is responsible, and both can work in parallel.
It is worth being precise about claims around review quality. The common recommendation to keep a PR below a few hundred lines comes from general code-review literature rather than measurements of stacked workflows specifically. The overview at Awesome Code Reviews notes that studies on decomposing changes found fewer false-positive findings, but did not consistently find more defects or faster reviews. The defensible benefit is therefore less “better reviews” and more reduced blocking and better attribution. That is still valuable, but it is a different claim.
The gh-stack extension
According to GitHub, stacks can be managed directly on github.com, in the mobile app or through Coding Agents such as Copilot. I tested the path that fits most naturally into an existing Git workflow: the CLI extension.
gh extension install github/gh-stack
gh stack adds its own command group. init starts a new stack, add adds another layer, submit pushes all branches and creates or updates the corresponding pull requests, sync reconciles the local state with GitHub, and merge performs the atomic merge.
A stack with four layers
For the test I built a tiny Python project, wordstats, which counts word frequencies in a text file. The functionality was deliberately split into four layers, each building on the previous one:
- Core logic:
count_words()counts words after normal text normalisation. - CLI: an
argparsewrapper exposes the core logic on the command line. - A
--topflag limits output to the N most frequent words. - Tests for the core logic and CLI.
The stack starts with init and then grows through add:
gh stack init 01-core-counting
# ... core.py schreiben ...
gh stack add -Am "Core: Worthaeufigkeit zaehlen"
# ... cli.py schreiben ...
gh stack add -Am "CLI: wordstats <datei> gibt Worthaeufigkeiten aus" 02-cli-interface
# ... --top-Flag ergaenzen ...
gh stack add -Am "CLI: --top begrenzt die Ausgabe auf die N haeufigsten Woerter" 03-top-n-flag
# ... Tests schreiben ...
gh stack add -Am "Tests: core und CLI" 04-tests
There is a small peculiarity in the first add: gh stack reported that branch 01-core-counting did not have any commits yet and attached the commit directly to that branch instead of creating another one. That is not an error but the expected case when init is followed immediately by add. It is still worth reading the message rather than dismissing it automatically.
gh stack view then shows the local state as a tree:
● 04-tests (current)
│ cad75de · Tests: core und CLI
│
○ 03-top-n-flag
│ 31c657d · CLI: --top begrenzt die Ausgabe auf die N haeufigsten Woerter
│
○ 02-cli-interface
│ 3e4ab48 · CLI: wordstats <datei> gibt Worthaeufigkeiten aus
│
○ 01-core-counting
│ cc9e8db · Core: Worthaeufigkeit zaehlen
│
└ main
gh stack submit --auto --open pushes all four branches and creates four pull requests. Each PR uses the previous layer as its base branch rather than main:
✓ Created PR #1 for 01-core-counting
✓ Created PR #2 for 02-cli-interface
✓ Created PR #3 for 03-top-n-flag
✓ Created PR #4 for 04-tests
✓ Stack created on GitHub with 4 PRs (stack #5)
Review isolation and the stack map
The actual benefit becomes visible when opening the individual pull requests. PR #2 shows a 2/4 badge in the header together with dprinz wants to merge 1 commit into 01-core-counting from 02-cli-interface. The PR diff contains only the CLI file, even though the branch also contains the core logic inherited from PR #1. GitHub shows the difference to the layer below, not the cumulative change since main.
For the reviewer this means that reviewing PR #2 shows only the CLI code, not the core logic that has already been reviewed in PR #1. Different reviewers can work on different layers at the same time without blocking one another.
Partial merge and automatic retargeting
The part that creates the most manual work without native support is retargeting base branches after a merge. I tested this deliberately by merging only the bottom two layers:
gh stack merge 2 --yes --squash
Merging #1, #2 into main via squash...
✓ Merged #1, #2 into main (a83468b)
Before the merge, PR #3 used 02-cli-interface as the base for 03-top-n-flag. Immediately afterwards, without any additional action, it showed:
{"baseRefName": "main", "headRefName": "03-top-n-flag", "state": "OPEN"}
The base branch of PR #3 had been retargeted to main automatically. Running gh stack sync then rebased the local state accordingly:
✓ Rebased 03-top-n-flag onto main (adjusted for merged PR)
✓ Rebased 04-tests onto 03-top-n-flag (adjusted for merged PR)
The addition adjusted for merged PR matters more than it looks. This is exactly where manual stacks usually become awkward. If the lower PR lands on main as a squash commit, Git sees the same changes again in the child branch as apparent conflicts even though the content has already landed. Dave Pacheco has documented exactly that procedure and needs three successive manual merges, including a git merge -X ours against the squash commit. In my test, gh stack sync resolved the situation after the squash merge automatically.
The rest of the stack could then be merged with one command:
gh stack merge 4 --yes --squash
Merging #3, #4 into main via squash...
✓ Merged #3, #4 into main (cc7bd1a)
According to the documentation, both merge operations are atomic: either all included pull requests are merged or none of them are. Existing branch protections and required checks continue to apply; gh stack merge does not bypass them.
How to cut a useful stack
The tool removes mechanical work, not the design decision. A badly divided stack is more cumbersome than one large PR because the reviewer has to reconstruct context across several pull requests.
The most important rule is to stack only changes that genuinely depend on one another. Two independent changes belong in two independent pull requests against main, not in a stack. A stack expresses an ordering. If that order is arbitrary, it creates artificial dependencies and blocks the upper layer for no reason.
Splitting along architectural boundaries rather than file boundaries has worked well for me: first the data model, then business logic, then the outward-facing interface. That is exactly what happened in the example above. Every layer should establish a meaningful state on its own and ideally remain independently mergeable. A useful test is whether the title of the layer can be stated in one sentence without using “and”.
Depth should stay manageable as well. Three to five layers are easy to understand. With ten layers, the ordering becomes the main problem and the top layer may be waiting on several reviews below it. A stack that deep usually indicates that an independent piece near the bottom should have landed earlier on its own.
It also helps to push the stack early, even as drafts. Reviewers can then see the planned structure before the upper layers are finished. That is often the point where a poor split can still be corrected cheaply with gh stack modify rather than by restructuring a completed feature.
And layers that are already reviewed and mergeable should be merged instead of waiting for the entire stack. That is the point of the workflow. If a stack lands only as one closed block, its remaining advantage over a large PR is mostly the cleaner commit history.
Limits of the preview
A few points qualify the convenience:
- Merge Queue support for stacks is, according to the changelog, rolling out gradually over the following weeks. Teams already using a Merge Queue should verify that it works with stacks in their repository before relying on the combination.
- This is a Public Preview. Behaviour and CLI commands may still change before general availability.
- I tested only the CLI workflow through
gh stack. I did not test management directly in the GitHub web interface or mobile app. - A stack still consists of Git branches. Manually using
git rebaseorgit push --forcein the middle of those branches can create a mismatch between whatgh stackexpects and what actually exists on GitHub.gh stack synchandles this well in practice, but it is not a substitute for understanding the state of the stack.
One objection remains even with perfect tooling, and it is social rather than technical: a stack is harder for outsiders to read than a single PR. Someone who is not working on the feature and gets assigned PR #3 of four first has to understand where that layer fits into the complete change. The stack map helps, but it does not eliminate the need for context. In repositories with many casual reviewers, that costs attention.
Result
The four-PR stack behaved in practice exactly as the changelog described: isolated diffs per layer, visible stack status in the PR interface, automatic retargeting after a partial merge and an atomic merge across several PRs. None of this was impossible with plain Git before, but it required disciplined manual work or an external tool.
The biggest difference from a manually maintained chain of feature branches is not that the stack exists but that GitHub knows it exists. A reviewer can immediately see how many layers there are and where a specific PR sits, without reconstructing the structure from base branches and PR descriptions. For smaller teams that do not already use a tool such as Graphite, that is a substantial improvement without adding another layer to the workflow.
My assessment after the test is that the benefit depends on reviews actually moving. Stacks remove waiting for a review as a blocker to continued development, but they do not replace a review culture. Where pull requests already sit for days, a stack quickly becomes a deep chain of waiting changes, which is more unpleasant than one waiting PR.
Anyone who wants to reproduce the test can find the complete example project, including all four merged pull requests, at dprinz/stacked-pull-requests.