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.
spr diff --all. This is equivalent to calling
spr diffon each commit starting from
HEADand going to back to the first commit that is part of upstream
main. 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.
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 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.) To land commit A, you would:
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 --allat the beginning, you could run plain
spr 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 B
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.
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
- Cherry-picking the local commit onto upstream
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
mainas before, and run
Make commit B on top of A as before, and run
spr diff --cherry-pick. The flag causes
spr diffto create the PR as if B were cherry-picked onto upstream
main, rather than creating the synthetic base branch. (This step will fail if B does not cherry-pick cleanly onto upstream
main, which would imply that A and B are not truly independent.)
Once B is ready to land, you can do one of two things:
spr land --cherry-pick. (By default,
spr landrefuses to land a commit whose parent is not on upstream
main; the flag makes it skip that check.)
Do an interactive rebase that puts B directly on top of upstream
main, then runs
spr land, then puts A on top of B.
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:
git pull --rebaseon
main, resolving conflicts along the way as needed.
spr diff --all.
You can shorten
s; they are spelled out here for clarity.