Stack Multiple PRs
The differences between spr’s commit-based workflow and GitHub’s default branch-based workflow are most apparent when you have multiple reviews in flight at the same time.
This guide assumes you’re already familiar with the workflow for simple, non-stacked PRs.
You’ll use Git’s interactive rebase quite often in managing stacked-PR situations. It’s a very powerful tool for reordering and combining commits in a series.
This is the workflow for creating multiple PRs at the same time. This example only creates two, but the workflow works for arbitrarily deep stacks.
-
Make a change and commit it on
main. We’ll call this commit A. -
Make another change and commit it on top of commit A. We’ll call this commit B.
-
Run
spr diff --all. This is equivalent to callingspr diffon each commit of your local branch that isn’t on upstreammainyet, starting with the bottom one and going up toHEAD. Thus, it will create a PR for each of commits A and B. -
Suppose you need to update commit A in response to review feedback. You would:
-
Make the change and commit it on top of commit B, with a throwaway message.
-
Run
git rebase --interactive. This will bring up an editor that looks like this:pick 0a0a0a Commit A pick 1b1b1b Commit B pick 2c2c2c throwawayModify it to look like this1:
pick 0a0a0a Commit A fixup 2c2c2c throwaway exec spr diff pick 1b1b1b Commit BThis will (1) amend your latest commit into commit A, discarding the throwaway message and using commit A’s message for the combined result; (2) run
spr diffon the combined result; and (3) put commit B on top of the combined result.
-
-
You must land commit A before commit B. (See the next section for what to do if you want to be able to land B first. With the
github-stackstacking mode, runningspr landon B lands A and B together.) To land commit A, you would:-
Run
git rebase --interactive. The editor will start with this:pick 3a3a3a Commit A pick 4b4b4b Commit BModify it to look like this:
pick 3a3a3a Commit A exec spr land pick 4b4b4b Commit B
-
-
Now you’re left with just commit B on top of upstream
main, and you can use the non-stacked workflow to update and land it.
There are a few possible variations to note:
-
Instead of a single run of
spr diff --allat the beginning, you could run plainspr diffright after making each commit. -
Instead of step 4, you could use interactive rebase to swap the order of commits A and B (as long as B doesn’t depend on A), and then simply use the non-stacked workflow to amend A and update the PR.
-
In step 4.2, if you want to update the commit message of commit A, you could instead do the following interactive rebase:
pick 0a0a0a Commit A squash 2c2c2c throwaway exec spr diff --update-message pick 1b1b1b Commit BThe
squashcommand will open an editor, where you can edit the message of the combined commit. The--update-messageflag on the next line is important; see this guide for more detail.
Cherry-picking
In the above example, you would not be able to land commit B before landing commit A, even if they were totally independent of each other.
First, some behind-the-scenes explanation. When you create the PR for commit B, spr diff will create a PR whose base branch is not main, so that the PR for B only shows the changes in B itself, rather than the entire difference between main and B. Depending on the stacking mode, that base is a synthetic branch that contains the difference between main and B’s parent, or the PR branch of commit A.
When you run spr land, it checks that each of these two operations would produce exactly the same tree:
- Merging the PR directly into upstream
main. - Cherry-picking the local commit onto upstream
main.
If those operations wouldn’t result in the same tree, spr land fails. This is to prevent you from landing a commit whose contents aren’t the same as what reviewers have seen.
In the above example, then, the PR for commit B is based on the changes in commit A. Thus, if you tried to land B before A, spr land’s “merge PR vs. cherry-pick” check would fail.
If you want to be able to land commit B before A, do this:
-
Make commit A on top of
mainas before, and runspr diff. -
Make commit B on top of A as before, and run
spr diff --cherry-pick. The flag causesspr diffto create the PR as if B were cherry-picked onto upstreammain, targetingmaindirectly. (This step will fail if B does not cherry-pick cleanly onto upstreammain, which would imply that A and B are not truly independent.) -
Once B is ready to land, you can do one of two things:
-
Run
spr land --cherry-pick. (By default,spr landrefuses to land a commit whose parent is not on upstreammain; the flag makes it skip that check.) -
Do an interactive rebase that puts B directly on top of upstream
main, then runsspr land, then puts A on top of B.
-
Stacking modes
The spr.stackingMode setting determines how the PRs of stacked commits are set up. Choose a Merge Method and Stacking Mode explains the options and their trade-offs; How it works - Stacked PRs describes what happens behind the scenes. In day-to-day use, these are the differences:
-
base-branches(the default with squash-merging): each PR of a stacked commit targets a synthetic base branch. Don’t merge such PRs in the GitHub UI, as that would merge them into their base branch; usespr land. Once the commits below have landed and you have rebased your local branch,spr diffchanges the PR to targetmaindirectly. -
chain(the default with merge commits): each PR of a stacked commit targets the PR branch of its parent commit. The PR of the parent commit must be up to date before you can runspr diffon a commit;spr diff --allupdates the whole stack from the bottom up, so this is taken care of.spr landlands the bottom PR, and the PRs stacked on it are changed to targetmain. -
github-stack: likechain, and the PRs are linked as a stack on GitHub.spr diffcreates and updates the stack.spr landlands the PR of the current commit together with all PRs below it, so to land a whole stack, runspr landon its top commit.
Reordering commits in a stack works in all modes. But spr never rewrites PR branches, so in the chain and github-stack modes, a PR branch that once had another PR’s changes merged in keeps them in its history, even though they are no longer part of its changes. With merge commits, those commits end up in the history of main (their changes don’t).
You can switch between the stacking modes at any time. spr diff changes existing PRs over to the configured mode as it updates them.
Rebasing the whole stack
One of the major advantages of committing everything to local main is that rebasing your work onto new upstream main commits is much simpler than if you had a branch for every in-flight review. The difference is especially pronounced if some of your reviews depend on others, which would entail dependent feature branches in a branch-based workflow.
Rebasing all your in-flight reviews and updating their PRs is as simple as:
-
Run
git pull --rebaseonmain, resolving conflicts along the way as needed. -
Run
spr diff --all.
-
You can shorten
exectox,fixuptof, andsquashtos; they are spelled out here for clarity. ↩