Software Engineering · Code Review
Stacked Changes and the Mechanics of Incremental Review
How a chain of dependent changes lets each diff be read against its own base, what happens when the bottom of the stack changes, and why restacking is the expensive step.
A stack is a sequence of changes in which each is computed against the previous one rather than against the mainline. The format is old — the kernel’s patch series predate distributed version control — and it exists because of a property of review that no amount of tooling removes: the reviewer’s cost is superlinear in the size of a diff, while the author’s work is often naturally sequential. A stack lets the author keep working while the reviewer consumes one unit at a time, and it lets each unit be read against a base that already contains the reasoning of the units below it.
Each diff in the stack has its own base
The defining mechanic is that the diff for change $n$ is computed against change $n-1$, not against the mainline. The consequence is that the diff is small and its context is correct: a call to a function introduced in change $n-1$ appears in change $n$ as a call to an existing function, not as a call to something undefined.
This is what makes the review tractable. If all the changes were squashed into one diff against the mainline, the reader would have to hold the whole feature in mind to judge any part of it, because every part depends on every other. With a stack, the reader holds one unit plus the already-approved units below it, and the approved units can be treated as a fixed, correct substrate. The reasoning is compositional: review each step, then trust the composition.
The price is that the diffs are not independently meaningful. Change $n$ does not compile against the mainline; it compiles only on top of $n-1$. This is not a defect of the format but its essential property, and it is why a stack cannot be landed out of order and why partial landing requires either squashing or a synthetic base. Review systems that present each change as if it were against the mainline have to compute a synthetic diff — change $n$ against the mainline — and that diff contains the union of everything below it, which reintroduces exactly the size problem the stack was meant to avoid.
Reviewing the delta between revisions
A stack makes a second kind of incremental review natural: within one change, revisions arrive as the author responds to comments. The diff between revision 3 and revision 4 is usually far smaller than either revision against the base, and it is the only material the reviewer has not seen. Reviewing that delta is the correct economy, and the tool support matters: a reviewer who must diff two revisions by hand will instead re-read the whole change, which wastes their attention and, worse, lowers their attention on the delta because it is embedded in familiar text.
The failure of delta review is that a change to a comment’s subject can invalidate an approval given on the earlier revision even when the delta is small. A revision that alters a function signature in one line and updates four call sites in the same line is small by line count and large by consequence. The delta view does not distinguish; it shows five changed lines. There is no substitute for the reviewer reading the delta and judging whether it touches anything their earlier approval relied on.
Restacking is where the cost concentrates
The bottom of a stack is against a base that moves. When the mainline advances, the whole stack must be rebased onto the new base, and the rebase is performed change by change. The first change is replayed onto the new base; if it conflicts, the conflict is resolved by hand, and the resolution is new code that no one has reviewed. The second change is replayed onto the rebased first change, and now the conflict may be different from what it would have been against the original base, because the first change’s rebased form differs from its original form.
The compounding is the real cost. A stack of eight changes rebased after a base move may require eight resolutions, and the resolution of change five can depend on how change two was resolved. An author who resolves change two one way and change five another way can produce a stack that is internally consistent but semantically different from the reviewed one. Nothing in the tool detects this, because each resolution is textually valid.
The practical mitigations are procedural rather than mechanical. Restack early and often, so that each rebase is against a base that has moved little and the resolutions are small. Review the resolutions as changes in their own right, which means the rebased revision is a new revision requiring at least a look. And when a base move makes the stack’s premise obsolete — the mainline solved the same problem differently — abandon the stack rather than resolving conflicts to preserve work that is now redundant.
Choosing the unit boundaries in advance
The hard design decision in a stack is where the boundaries go, and it must be made before the work is done, because boundaries chosen afterwards are boundaries chosen by diff, which is a weaker criterion than intent.
A boundary that works has three properties. The change below it is approved and unlikely to be revised, because revising a lower change forces restacking of everything above. The change above it can be described without referring to the specifics of the change below beyond its public surface, because a reviewer reading the upper change should not need the lower change’s internals. And the upper change adds something a reader can evaluate on its own terms, typically a behaviour or a capability rather than a mechanical step.
The common bad boundary is the “plumbing first” split: change one adds an interface with no callers, change two adds callers. Change one is unreviewable because nothing uses it, so its design cannot be judged; change two is where the design’s adequacy becomes apparent, but by then the interface is approved and the reviewer’s objection arrives one change too late. A better boundary places the interface and its first caller together, so the interface is reviewed in the context of a use.
Landing a stack without a merge queue
Landing requires that the merged tree contain the whole stack, and the tree that was reviewed is the stack applied to a specific base. If the base has moved since the top of the stack was tested, the merged result is a tree no one has built.
Two strategies exist. The first is to land the changes in order, each verified against the mainline at the moment of landing. This requires each change to be re-verified after the one below lands, which is a serial process and is slow when the mainline is busy. The second is to test the whole stack against the prospective base and land it atomically, which requires the merge mechanism to accept a sequence rather than a single commit. The second is faster and demands more from the merge infrastructure; the first is universally available and degrades gracefully.
Neither strategy solves the semantic conflict between the stack and an unrelated change that landed concurrently. Both changes pass their own tests; the combination is broken. That failure is the motivation for testing the merge result rather than the branch, which is the merge queue’s job.
Tool support and its gaps
The mechanics above are old and the tools vary in which parts they automate. A patch-series tool represents the stack as a sequence of patches applied on top of a base, with operations to push, pop, and refresh a patch; the representation is explicit and the rebase is a manual refresh of each patch. A code-review system that models changes as a stack represents the dependency in its data model, so that the review of change $n$ displays the correct base and the landing order is enforced. A branch per change with a merge target that names the branch below achieves the same effect using the version control system’s own graph.
The gaps are consistent across all three. None of them detects that a restack changed semantics. None of them can tell a reviewer that the delta they are reading alters an assumption of an approval given earlier. And none of them makes the cost of reviewing a resolution proportional to the resolution’s size, because the resolution’s significance is not a function of its line count.
# restack: replay each change onto the updated base, one at a time
git rebase --onto mainline old-base feature-tip
# after each conflict, the resolution is new code and needs a look
git range-diff old-base..old-tip mainline..feature-tip
The range-diff output is the closest thing to a restack-aware review view: it pairs each commit
before and after the rebase and shows what changed within each. Reading it is the only way to see that
a resolution altered more than the conflict markers, and it is the step most often skipped because the
rebase “succeeded”.
When a stack is the wrong shape
A stack is a tool for a change whose parts are genuinely ordered. When the parts are independent — three unrelated bug fixes, four new endpoints — the stack imposes an order that does not exist, and the reviewer of change three must wait for changes one and two to be approved for no reason. In that case the right shape is a set of independent changes, each against the mainline, landed in any order.
The distinction is whether the dependency is real or incidental. A dependency is real when the upper change does not build or does not make sense without the lower one. It is incidental when the author happened to work on them in sequence. Sorting the two apart is the same judgement as choosing review units, applied one level up: the unit of review is the smallest thing that is coherent alone, and the stack is the graph of those units.
References
- Gerrit Code Review — Working with Changes — Gerrit project
- git-rebase(1) Manual Page — Git project
- Pro Git — Git Branching: Rebasing — Chacon & Straub, Apress