GitHub's Stacked PRs

matijash1 pts0 comments

Stacked PRs explainer - Carlos' Dev Stream - pckt

Stacked PRs explainer - Carlos' Dev Stream - pckt

Stacked PRs explainer

Carlos PreciosoJul 21, 2026

Subscribe

Stacked PRs are the new GitHub implementation of an already-existing concept: stacked changes or stacked diffs.

Stacked changes are a method for split a big change into independent, incremental pieces.

The typical example is a new feature that requires refactoring an existing system. We'd like the refactor to be reviewed by itself for correctness, and the new feature to build on top of if, but simplify review by not having the refactoring changes dirtying focused work on the new feature.

Therefore, instead of doing a big change, you split it into multiple ones in series, each dependent on the previous one but with somewhat independent focus, and send each change for review independently. This is not limited to two changes, btw. For example, at Wasp we've recently done a stack of 5 PRs for the rewrite of a large system.

In GitHub, splitting a change into multiple PRs is easy enough, just remember to create a branch for each and set the target of the PR to the previous branch in the chain:

$ gh pr create --title "(1) Foundation refactor" \<br>--base main --head my-feature-pt1<br>$ gh pr create --title "(2) Write the new feature" \<br>--base my-feature-pt1 --head my-feature-pt2<br>$ gh pr create --title "(3) Start using the new feature" \<br>--base my-feature-pt2 --head my-feature-pt3

Voilà, you have now stacked some PRs, and they can be reviewed independently! And you didn't even need to be on GitHub's Stacked PRs private beta for it.

The problem is the workflow

Trouble starts coming when review goes as it usually does: you need to do some changes in the initial refactor, and now GitHub complains that every other PR on top has merge conflicts.

The problem lies originally in Git itself: semantically, each commit represents a full snapshot of the repo, not the difference over the previous one.

You see this:

commit 1a7fb56bf6409a20a7a71f895472fc7ef829b04a

README.md:<br>@@ -21,7 +21,7<br>The current maintainers of the project are:<br>* foo<br>-* bar<br>+* cprecioso<br>* baz

But what Git actually works with is this:

commit 62e44588bc8c158878059c71ebf22249374a96fc

README.md:<br>[All the content above is also part of the snapshot!]

The current maintainers of the project are:<br>* foo<br>* bar<br>* baz

[Also all the other files in the repo]

commit 1a7fb56bf6409a20a7a71f895472fc7ef829b04a<br>parent 62e44588bc8c158878059c71ebf22249374a96fc

README.md:<br>[All the content above is also part of the snapshot!]

The current maintainers of the project are:<br>* foo<br>* cprecioso<br>* baz

[Also all the other files in the repo]

We usually work with Git in terms of commit-over-commit changes, so we don't usually realize that e.g. git merge has to calculate diffs and reapply them, instead of just changing the duplicating a commit with a new parent.

So, when a PR changes, all the ones on top have to be recalculated, and the bigger the change, the more probable is that merge conflicts show up. Even though the diff, logically, could be re-applied cleanly, git merge is working with different bases from which to calculate that diff; so it might confuse where does each line belong.

The common solutions

On GitHub, the most common solution is to rebase each branch on top of the previous one, and push --force everything:

$ git switch my-feature-pt1<br># ... We do some change here ...<br>$ git commit -m "Address feedback"<br>$ git push

# And now we rebase everything else<br>$ git switch my-feature-pt2<br>$ git rebase my-feature-pt1<br>$ git push --force-with-lease

$ git switch my-feature-pt3<br>$ git rebase my-feature-pt2<br>$ git push --force-with-lease

This process is annoying and error-prone, and conflicts do still appear even in simple changes, as each PR further down the chain is diverging more from the new rebased branches. Moreover, rebasing and force-pushing can make GitHub lose track of which files have been actually reviewed, or where comments of a review were placed.

It's clear a more automatic process is needed. There are some tools that can help out: ranging from local commands (git-stack-cli, gitstack), GUIs (GitButler), paid SaaS services (Graphite), or even full re-thinks of what Git should be (GitLess, jj).

The problem can also be solved on the side of forges, which is what Phabricator (RIP) or Gerrit do, by having a first-class stacked changes concept that they can track and be smart about, so Git doesn't choke on them.

GitHub's solution

GitHub has decided to do a similar solution to the aforementioned Graphite, by shipping a bit each: a local helper program, specific server-side support, and interface changes to accommodate stacks.

How does it work in practice...? Underwhelmingly so. It seems that instead of extracting diffs and tracking those between code versions, they've just officially-supported hooks into the existing rebase workflow explained above.

They can get some smarts from tracking each PR...

feature stacked changes github commit change

Related Articles