Skip to content
Published on

The Complete Guide to GitHub Stacked PRs — What Native Stacking Solved, and What It Left Standing

Share
Authors

Introduction — a Request to Review a 1,400-Line PR

A review request lands with 47 files changed and 1,400 lines added. A migration, a repository layer, API handlers, and a frontend form are all sitting in one PR. A reviewer effectively has three options: clear out an entire day, rubber-stamp it, or reply "please split this up." Pick the third, and now the author is stuck — cutting a set of dependent changes across git into multiple PRs means rebasing everything above whenever review feedback lands on a piece underneath.

On July 30, 2026, GitHub shipped Stacked Pull Requests into public preview. It's rolling out sequentially to every repository over a few days, and it works across web, mobile, and CLI. The Hacker News thread drew hundreds of comments, split between "finally" and "this is what Phabricator and Gerrit were doing ten years ago." Exact vote counts shift over time, so I won't quote a number here.

This post isn't a summary of the announcement — it's a field guide to the stacking workflow itself. It walks through, in order, the problem stacking actually solves, what GitHub's implementation does and doesn't do, and the rebase cascade that's stacking's real cost. General advice on splitting PRs is covered in Writing PRs and Commits That Get Merged; here the focus stays on stacking as a structure.

What Stacking Solves — Cutting Review Units Along Dependency Order

The advice to split a large change into small ones is old, but it fails often in practice. The reason is simple: you can't just split a change that has dependencies. The repository layer won't compile without the schema migration, and the API handler won't work without the repository. Split into three branches, and the second branch has to base off the first branch instead of main — and the moment it does, the PR view mixes in every change from the first branch too.

Stacking solves this with one rule: base each layer off the layer below, not off trunk.

main
 └── feat/schema        PR #1   base: main
      └── feat/repo     PR #2   base: feat/schema
           └── feat/api PR #3   base: feat/repo
                └── feat/ui PR #4  base: feat/api

This structure gives you three things.

  • A smaller review diff. Open PR #3 and you see only the API handler changes. The two layers below are already part of the base, so they drop out of the diff.
  • Parallel review. A DBA looks at #1, a backend reviewer looks at #2 and #3, and a frontend reviewer looks at #4, all at the same time. Sequential waiting disappears.
  • Partial landing. Once #1 and #2 are approved, they can merge before review on the layers above finishes. Review delays don't hold up the whole stack.

Stacking also creates one thing in return: every time a lower layer changes, everything above it has to be realigned. That's covered separately below.

What GitHub's Native Stack Actually Does

Per the changelog and the gh-stack repository, what this preview provides on the server side breaks down into four things.

First, a stack map. A map showing which layer of the stack a given PR sits at, and what's above and below it, appears at the top of every PR. A reviewer checks "where does this change sit in the whole picture" from the UI rather than a hand-maintained checklist in the PR description. What stacking tools used to fake by auto-inserting a markdown list into the PR body has become a server-side feature.

Second, per-layer diffs. When a reviewer opens a PR, only that layer's changes show. This actually already happens naturally when the base branch is set to the layer below — what's different is that the server keeps that state consistent even after a lower layer merges or gets rebased.

Third, two merge modes.

  • Merging the top, ready PR lands every unmerged layer beneath it as a single operation.
  • Merging only a lower layer first causes the PRs above it to be automatically rebased and re-based on the new target. This used to be done by hand or offloaded to a CLI tool.

Fourth, compatibility with existing protection rules. Branch protection, required checks, and review rules all still apply to every PR in the stack as-is. Merge queue support is described only as "rolling out gradually over the coming weeks," so as of this writing you can't assume it works in every repository yet.

There's something worth stating plainly here: GitHub's stacking is branch-level. It is not commit-level review. The single most repeated criticism in the HN thread was exactly this point — if you were expecting a model like Phabricator's or Gerrit's, where a single commit is the review unit and you can view an interdiff, this preview isn't that. You still have to create a branch per layer, and GitHub's existing behavior — where review comments vanish when a force push changes the diff — is unchanged.

gh-stack — a Command-Level Workflow

The CLI extension handles the local side. Stack metadata is stored as JSON at .git/gh-stack and isn't committed. In other words, the stack's ordering information is local state, and the server recognizes the stack through PR base relationships.

# Install (gh 2.0 or later)
gh extension install github/gh-stack

# 1) Start the stack from trunk
git switch main && git pull --ff-only
gh stack init feat/schema
git commit -am "add nullable columns for new pricing model"

# 2) Stack a layer — create a new branch on top of the current one
gh stack add feat/repo
git commit -am "repository reads new columns behind a flag"
gh stack add feat/api
git commit -am "expose pricing endpoint"

# 3) Check the current stack
gh stack view

# 4) Turn all three layers into PRs and link them together
gh stack submit

The real substance of stacking shows up when review feedback lands on a lower layer.

# Go down two levels and fix
gh stack down
gh stack down
git commit -am "address review: keep columns nullable for one release"

# fetch -> cascading rebase -> push -> sync PR state, all in one shot
gh stack sync

# to run just the rebase on its own
gh stack rebase

# reorder or combine layers (requires linear history)
gh stack modify

# atomically merge up to the point that's ready
gh stack merge

Having dedicated movement commands turns out to matter more than expected in practice. gh stack up / down / top / bottom / trunk move you between layers, and gh stack checkout jumps straight to a specific PR number or branch. Point at a branch that belongs to multiple stacks at once, and the command signals ambiguity with exit code 6, which you can branch on in a script.

A few constraints in the documentation are worth knowing ahead of time. gh stack push is not atomic. It pushes branch by branch with --force-with-lease, so if one branch gets rejected midway through, only that branch is left in a failed state and needs to be re-run. When the local stack and the remote have diverged significantly, sync only asks how to resolve it in interactive mode — so it's not something you run unattended in CI.

The Rebase Cascade — Stacking's Real Cost

Stacking's cost isn't the effort of creating multiple branches. It's the structural fact that when the bottom moves, everything above it shakes.

In a stack of height n, modifying the bottom layer once means you have to rebase the n-1 branches above it, in order. It's common for the same conflict to reappear at every layer — change a function signature at the bottom, and every layer above that uses that function has to resolve the same shape of conflict all over again. What the tooling automates is the execution of the rebase, not the judgment of the conflict.

There are three ways to cut this cost, and they're tool-independent.

# 1) Don't resolve the same conflict twice — rerere records resolutions and replays them
git config --global rerere.enabled true

# 2) Have rebase move intermediate branch refs along with it (Git 2.38+)
git config --global rebase.updateRefs true

# 3) Precise transplantation when the base has changed — name the range explicitly
git rebase --onto origin/main <previous-base-commit> feat/api

rerere is the single setting with the biggest felt difference in a stacking workflow. It eliminates re-typing the same conflict resolution at every layer. rebase.updateRefs, introduced in Git 2.38, advances every branch pointer in the stack together with a single rebase — effectively minimal stack support with plain git alone.

And every time a cascade fires, CI reruns on every PR above it. Fix the bottom of a height-5 stack three times, and CI runs 15 times. Add a merge queue on top of that, and it can run once more when entering the queue. The fact that stacking cuts review time but raises CI cost is worth calculating before you adopt it.

One last collaboration trap. Stacked branches force-push routinely. If someone else added a commit onto the same branch, that commit can vanish. --force-with-lease is the default line of defense, but it's not foolproof, so it's safer for a team to explicitly declare that a stacked branch is owned by a single author.

How It Differs From Existing Tools

Stacking isn't a concept GitHub invented. If anything, this space had an oversupply of tooling already.

ToolReview unitWhere state livesCascade automationServer-side integrationCost
GitHub native + gh-stackBranchLocal .git/gh-stack + PR base relationships on the serversync/rebase commands; server auto-realigns on a lower mergeStack map, per-layer diffs, atomic mergeFree during preview
GraphiteBranchIts own service + local CLIAutomatic via gt sync/restackOwn web review UI, merge queuePaid SaaS (free tier available)
ghstackCommitAn identifier embedded in the commit messagePushes the whole commit stack back in as a unitNone (only creates PRs)Open source
git-branchlessCommitA local event logMoves subtrees via git move, git syncNoneOpen source
SaplingCommitIts own VCS stateStack editing is the default behaviorSupports creating GitHub PRsOpen source
Plain gitBranchNoneOnly rebase.updateRefsNoneFree

The core difference reduces to two lines. Commit-level tools (ghstack, git-branchless, Sapling, and jj as their spiritual successor) take the model where one commit equals one review, and hide branches from the user. Branch-level tools (Graphite, GitHub native) leave GitHub's PR model exactly as it is and layer the stacking concept on top.

GitHub choosing branch-level appears to be the only option that could layer on without conflicting with existing protection rules, required checks, and CODEOWNERS. In exchange, it doesn't get the advantages of commit-level review — interdiffs, preserved commit history, review comments that survive a rewrite. Most of the HN thread's criticism was about exactly this trade-off.

If you're already on Graphite, there's not much reason to switch right now. Both do automatic rebasing and stack visualization, and Graphite is more mature on merge queue and review UI. Conversely, if you've been getting by with plain git and no tooling at all, native stacking is the lowest-friction option to adopt — no new account, no new web UI, no new permission approval required.

When Stacking Actually Hurts

Stacking isn't free, and it isn't a win for every team. In the following cases, you're better off not using it.

When the changes aren't actually dependent. If you have three genuinely independent fixes, just make three PRs, each directly off main. Bundle them into a stack and you create an ordering dependency that never existed — if the bottom PR stalls in review, everything else gets stuck along with it.

When the stack's height goes well past 3. Cascade cost scales with height, and so does CI cost. A height-8 stack is usually not "a change that got split well" — it's "something that should have been cut apart at the design stage, and is instead being cut apart at review time." It's often better to put it behind a feature flag and land it directly on main.

When there's a single reviewer with plenty of slack. A big share of stacking's benefit is parallel review. If one person is going to look at everything in sequence regardless, splitting into layers doesn't reduce total review time — it just adds to the author's rebase burden.

When a branch is shared by multiple people. As noted above, stacking assumes force-pushing. In a pairing culture, or one where several people commit to the same branch, a long-lived feature branch with periodic merges is safer than stacking.

When you squash-merge exclusively but commit history matters to the project. GitHub's stacking merges each layer as a separate PR, so layer-level history survives, but commits within a layer disappear under a squash policy. If you need commit-level archiving, look at Sapling or the jj family instead.

To sum up, the conditions where stacking works well are narrow — dependency order is clear, the stack is 2 to 4 layers deep, there are multiple reviewers, and each layer is independently deployable on its own. Move away from these conditions and the rebase cost outgrows the benefit.

Conclusion — the Tool Automated the Execution of a Rebase, Not the Design

GitHub's native stacking took things third-party tools used to imitate — the stack map, per-layer diffs, automatic realignment after a lower-layer merge, atomic merges — and turned them into server features. The fact that you can use it with no separate service signup alone lowers the barrier to adoption substantially.

  • Check right now: install with gh extension install github/gh-stack and try a height-3 stack on your next big change. Merge queue integration is still rolling out, so confirm actual behavior in your team's repository first.
  • Definitely turn on: rerere.enabled and rebase.updateRefs. Whatever tool you use, these cut the cascade cost the most.
  • Don't expect: commit-level review or interdiffs. This preview is a branch-level model, and it doesn't reproduce the Phabricator/Gerrit review experience on top of it.

Stacking doesn't make a large change small. It only reduces the friction of splitting a change that could already be split cleanly. Finding the right place to cut is still the job of design.

References