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 diff
on each commit starting fromHEAD
and going to back to the first commit that is part of upstreammain
. 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 throwaway
Modify it to look like this1:
pick 0a0a0a Commit A fixup 2c2c2c throwaway exec spr diff pick 1b1b1b Commit B
This 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 diff
on 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.) To land commit A, you would:
-
Run
git rebase --interactive
. The editor will start with this:pick 3a3a3a Commit A pick 4b4b4b Commit B
Modify 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 --all
at the beginning, you could run plainspr diff
right 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 B
The
squash
command will open an editor, where you can edit the message of the combined commit. The--update-message
flag 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
, but rather a synthetic branch that contains the difference between main
and B’s parent. This is so that the PR for B only shows the changes in B itself, rather than the entire difference between main
and B.
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 has a synthetic base branch that contains 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
main
as before, and runspr diff
. -
Make commit B on top of A as before, and run
spr diff --cherry-pick
. The flag causesspr diff
to create the PR as if B were cherry-picked onto upstreammain
, rather than creating the synthetic base branch. (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 land
refuses 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.
-
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 --rebase
onmain
, resolving conflicts along the way as needed. -
Run
spr diff --all
.
You can shorten exec
to x
, fixup
to f
, and squash
to s
; they are spelled out here for clarity.