Stacked diffs: commits as review units
Treat each commit (or tiny branch) as the unit of review, not a large feature branch. Smaller reviews, less waiting, better commit hygiene — and a non-trivial rebase tax to pay.
On this page
The default Git workflow says “one branch, one PR, one review.” Stacked diffs say “one commit (or very small branch) is the unit of review” — and your local work is a queue of dependent changes ahead of main, each reviewable independently while you keep building on top.
This is the workflow Meta popularised on Phabricator, Google practises in chained reviews, and modern tools (Graphite, ghstack, Sapling, spr) have brought to GitHub. It’s a force multiplier when it works. It’s also unforgiving when commit discipline isn’t there.
The mental shift
main: A---B---C
\
feature: D---E---F---G---H (one big PR)Reviewer reads 600 lines across 5 concerns. Author waits for review to start the next feature.
main: A---B---C
\
D---E---F---G
^ ^ ^ ^
PR1 PR2 PR3 PR4Each commit is its own PR; PR 2 stacks on PR 1, PR 3 on PR 2, and so on. The author keeps coding on top while PR 1 is in review.
The shift isn’t about Git mechanics — it’s about engineering granularity. Each commit has one thesis, is independently understandable, and is small enough that review feedback arrives in hours, not days.
Why teams adopt it
Branch-per-PR forces dependent work to either block on review (no parallelism) or pile into one large PR (no review signal). Large PRs are harder to review well, hide risk, and accumulate “address feedback” commits that erode history quality.
Split a larger initiative into a stack of small, dependent, reviewable commits. Publish each as its own PR. Keep coding on top while lower layers are in review. When feedback lands on a lower layer, rebase the stack to replay the upper layers on the updated base.
Reviews are faster and higher-signal. Authors stay in flow. git log becomes useful: each commit is a coherent change, blame is meaningful, bisect actually works. The cost is paid in rebase frequency and tooling investment — without those, the stack becomes a tower of conflicts.
The author loop
-
Start from updated `main`
git fetch origin && git switch -c stack-base origin/main. The stack is built on a known base. -
Make one coherent change, commit, publish
The commit has a thesis, a body, a test plan. Publish as a PR with whatever stack tool you use (
gt submit,ghstack,spr diff). -
Keep building the next layer on top
Don’t wait. Commit the next dependent change. Publish it; the tool creates PR 2 stacked on PR 1’s branch.
-
When feedback arrives on a lower layer
git rebase -ito edit the relevant commit. Git replays the upper layers automatically. Resolve conflicts at each layer. Re-publish. -
Land in dependency order
PR 1 first, then PR 2, then PR 3. Most stack tools handle the rebase-after-land cycle automatically.
The feedback ripple
This is the operation that defines whether stacking feels good or painful:
git rebase --update-refs (Git 2.38+) updates dependent branch refs during a rebase, which removes a big chunk of the manual choreography for stacked-branch tooling.
The tooling landscape (2026)
| Tool | Approach | Where it lives |
|---|---|---|
| Graphite | Commercial CLI + web + VS Code. gt create, gt submit, gt modify, gt sync. Adds a merge queue and CI optimisation tuned for stacks. | GitHub-native |
| ghstack | OSS CLI. One PR per commit using synthetic branches. Land via ghstack land (not the GitHub merge button). | GitHub-native |
| spr | OSS CLI. Phabricator-flavoured. spr init, spr diff, spr land. Heavy emphasis on commit-as-review-unit. | GitHub-native |
| Sapling | Source-control system with stacks as a first-class concept. GitHub integration plus a stack-aware review UI. | Alternative SCM |
| ReviewStack | Stack-aware review UI sitting on top of GitHub PRs. | GitHub-native |
| Gerrit | Patch-set review server. Commit-centric review long before “stacked diffs” was a term. | Self-hosted |
Pick one tool — mixing two stack CLIs on the same repo is a fast path to a confused team.
CI implications (don’t underestimate this)
Stacking multiplies PR count, which multiplies CI triggers. The interesting moves:
- Selective CI by stack position. Run on the bottom and top of every stack; skip intermediates unless they touch CI-relevant files.
- Dependency-aware test selection. Bazel/Buck/Turborepo-style — only run tests reachable from changed files.
- Merge queue batching. Amortise validation across multiple stack items in one batch run.
- Fail-open optimisation gates. If the CI optimiser fails to answer, default to running CI — never default to skipping.
Stacking improves throughput fastest when CI policy is adapted alongside the workflow. Without that, the workflow saves engineer time and burns CI budget at the same rate.
When stacking is the wrong choice
Stacked diffs assume a few things hold in your team. When they don’t, the workflow makes things worse, not better.
- Changes are already small and independent. A team that ships 50-line PRs and gets review in an hour doesn’t have the problem stacking solves.
- Compliance demands monolithic review artifacts. Some regulatory regimes want one signed-off artifact per change. Stacking fragments that audit trail.
- The team can’t run force-push workflows safely. Stack maintenance is rebase + force-push. If branch protection forbids force-push on integration branches, the tool will fight you at every step.
- The team lacks rebase fluency. This is the real adoption blocker. The first time the stack needs a rebase across three layers with conflicts, anyone who hasn’t internalised
git rebase -iwill abandon the workflow.
Anti-patterns
A stack of 5 PRs, each 400 lines, all touching the same module.
The author adopted the workflow without internalising “one thesis per commit.” The stack is shaped like a single feature — just chopped into arbitrary slices.
The unit isn’t “a PR-sized chunk of the feature” — it’s one coherent change. Infra → API → UI is a stack. “Lines 1-200, lines 201-400, lines 401-600 of the same file” is not.
Stack commits with messages like “wip”, “address feedback”, “fix tests” — interspersed with substantive commits.
The author treats the stack like a branch they’ll squash at the end. Stacked tools generally don’t squash — each commit becomes a permanent PR.
Each commit must carry the message it would have if it were the only commit in the PR. If a commit is genuinely a fixup, use git commit --fixup=<sha> and git rebase --autosquash to fold it into its target before publishing. See 0026 Three ways to squash, and when each is right · Git for the squash variants.
Adoption — what actually works
Pilot with one team that has high PR latency pain. Pick one tool. Cap initial stack depth at two layers while the team learns. Introduce a commit-message template (thesis, context, test plan). Add CI optimisation only once you have baseline metrics — otherwise you can’t tell whether the optimisation helped.
Four metrics to track during adoption:
- Median PR size (lines changed). Should fall sharply.
- Review turnaround (open → first review). Should fall.
- Time-to-merge (open → land). Should fall, but watch for stalled lower layers.
- Conflict/rework time. This is the canary. Rising rework time means the team isn’t keeping up with rebase hygiene — slow down before scaling.
Key takeaway
Stacked diffs are less about branch topology and more about engineering granularity plus review ergonomics. The biggest wins come when commit-level quality, stack-aware tooling, and CI strategy align. Without those three together, stacking is painful theatre. With them, it’s the difference between shipping at the speed of code review and shipping at the speed of code.
Related: 0030 Branching strategies: Git Flow, GitHub Flow, trunk-based · Git for how stacked diffs fit (or don’t) into common branching strategies.
Related
- Git· mutualGit worktrees for parallel branch workHow and when to use git worktree to keep multiple branches checked out at once, sharing one object store, without the disk cost or coordination headaches of multiple clones.
- Git· relatedGit rebase without losing workWhat rebase actually does to your commits, how to drive the interactive form, and the recovery moves that keep a bad rebase from becoming a lost-work incident.