System Design · Issue 10
How GitHub Engineered Stacked Pull Requests for Millions of Repositories
Splitting a big change into a chain of small PRs used to mean rebasing branches by hand. Here is how GitHub made the stack a first-class object.

The problem
One giant PR, or a rebase treadmill

Every reviewer knows the bind. You either open one enormous pull request that takes forever to review, or you split the work across several dependent branches you then have to keep manually rebasing as things shift underneath you.
One big PR
slow, sprawling review
Stack of small PRs
reviewable, but manual to maintain
GitHub's own docs name three specific pains: rebasing dependent branches by hand is tedious and error-prone, reviewing a change out of context hurts review quality, and worst of all, branch protection rules and CI checks often fire only for the bottom PR in the chain.
That last one is the real technical gap. Without native support, only the PR targeting main gets full CI and branch-protection treatment, so every PR above it in the stack is a second-class citizen whose true status you cannot trust.
Anatomy
What a stack actually is

Stack — Two or more PRs in the same repo where the bottom one targets the stack's trunk (the repo default branch, e.g. main), and each PR above it targets the branch of the PR directly below.
main
└─ PR #1 feature-a → targets main (bottom)
└─ PR #2 feature-b → targets feature-a
└─ PR #3 feature-c → targets feature-b (top)You drive it from a gh stack CLI extension, installed with gh extension install github/gh-stack. Commands like init, add, and submit track branches in dependency order and push them up as a linked stack on GitHub in one go.
The key decision
Evaluate every PR against the final target

The clever move that fixes the second-class-PR problem is quiet but load-bearing. Every PR in a stack is checked for branch protection rules and required CI against its final target branch, not its literal base branch.
literal base evaluated against PR #3 → feature-b ⇒ main (CI + CODEOWNERS) PR #2 → feature-a ⇒ main (CI + CODEOWNERS) PR #1 → main ⇒ main (CI + CODEOWNERS)
So a CODEOWNERS approval rule or a CI workflow configured for main now triggers correctly on a PR three layers up, even though its direct base is feature-b. The status you see for the middle of the stack is finally the status you would get if that change landed on trunk.
The stack stops being a stack of guesses. Every layer is graded against where it is actually going.
Merging
Merge one, land everything below it

Clicking Merge on any PR in the stack merges that PR and every unmerged PR below it, bottom-up, as a single operation. A direct merge is atomic: either the whole group lands, or on any failure nothing merges and it rolls back.
- Merge PR #2 (you click here)
- PR #1 lands first (it is below)
- PR #2 lands (onto the new trunk)
- PR #3 stays open (auto-rebased onto main)
Land only part of the stack and the PRs above stay open and automatically retarget onto the new base. All three merge methods work, and the resulting history is stated to match merging each PR individually from the bottom.
Through the merge queue the behaviour is subtler: PRs enter together but are evaluated one at a time from the bottom, and if one fails it and all its descendants are ejected while the lower PRs that already passed are untouched.
The hard part
Cascading rebase, and the squash trap

Keeping a stack current is the genuinely hard problem, so GitHub moved it server-side. A Rebase Stack button (or gh stack rebase) replays the entire stack onto the latest trunk, updates every unmerged branch, and force-pushes the results in one cascade.
main advances ──▶ ▓▓▓ new trunk
│ Rebase Stack (server-side)
▼
replay #1 → #2 → #3, force-push each branchThe subtle win is squash-merge handling. Squash and rebase merges rewrite commit hashes, which historically broke the git-history continuity stacked PRs rely on to compute diffs. GitHub's rebase engine replays your unique commits on top of the squashed base, avoiding the artificial conflicts that used to make this the standard objection to stacking.
Rollout
Preview first, merge queue later

The feature shipped in stages. A waitlist-gated private preview of the gh-stack CLI extension opened in April 2026, covered by InfoQ later that month.
Apr 2026 (private preview)Jul 2026 (public preview)weeks later (merge queue support)
Public preview arrived on July 30, 2026, rolling out to all repositories over the following days, while merge queue support for stacks rolled out progressively over the coming weeks, lagging the basic feature.
The launch drew a large Hacker News thread, though the reported numbers diverge across snapshots: HN itself showed around 900 points and 528 comments, while InfoQ cited 516 points and 282 comments for the discussion.
The tradeoff
Stacking, without the git bookkeeping
Why it matters
Stacked diffs were a Meta and Google workflow that tools like Graphite bolted onto GitHub from the outside. Bringing it inside the platform changes what mid-stack PRs mean: evaluating each one against its final target branch is the fix that makes CI and CODEOWNERS trustworthy for the whole chain, not just the bottom. For teams that already split work into dependent branches, native support removes the manual rebasing and the squash-merge breakage that made the pattern painful on GitHub.
The short version
- A **stack** is dependent PRs where the bottom targets `main` and each one above targets the branch below it.
- The key decision is evaluating every PR against its **final target branch**, so CI and branch protection fire on mid-stack PRs, not just the bottom one.
- Merging any PR lands it and everything below atomically, and PRs above **auto-rebase** onto the new base.
- A **server-side cascading rebase** replays the whole stack and safely handles squash merges, the old dealbreaker for stacking on GitHub.
- Public preview landed July 30, 2026 across all repos, with **merge queue** support trailing by weeks.