spr is a command line tool for using a stacked-diff workflow with GitHub.
The idea behind spr is that your local branch management should not be dictated by your code-review tool. You should be able to send out code for review in individual commits, not branches. You make branches only when you want to, not because you have to for every code review.
If you’ve used Phabricator and its command-line tool
arc, you’ll find spr very familiar.
In spr’s workflow, you send out individual commits for review, not entire branches. This is the most basic version:
Make your change as a single commit, directly on your local
spr diffto send out your commit for review on GitHub.
If you need to make updates in response to feedback, amend your commit, and run
spr diffagain to send those updates to GitHub.
Similarly, you can rebase onto newer upstream
spr diffto reflect any resulting changes to your commit.
Once reviewers have approved, run
spr land. This will put your commit on top of the latest
mainand push it upstream.
In practice, you’re likely to have more complex situations: multiple commits being reviewed, and possibly in-review commits that depend on others. You may need to make updates to any of these commits, or land them in any order.
spr can handle all of that, without requiring any particular way of organizing your local repo. See the guides in the “How To” section for instructions on using spr in those situations:
- Simple PRs: no more than one review in flight on any branch.
- Stacked PRs: multiple reviews in flight at once on your local
The reason to use spr is that it allows you to use whatever local branching scheme you want, instead of being forced to create a branch for every review. In particular, you can commit everything directly on your local
main. This greatly simplifies rebasing: rather than rebasing every review branch individually, you can simply rebase your local
main onto upstream
You can make branches locally if you want, and it’s not uncommon for spr users to do so. You could even make a branch for every review if you don’t want to use the stacked-PR workflow. It doesn’t matter to spr.
One reasonable position is to make small changes directly on
main, but make branches for larger, more complex changes. The branch keeps the work-in-progress isolated while you get it to a reviewable state, making lots of small commits that aren’t individually reviewable. Once the branch as a whole is reviewable, you can squash it down to a single commit, which you can send out for review (either from the branch or cherry-picked onto
The principle behind spr is one commit per logical change. Each commit should be able to stand on its own: it should have a coherent thesis and be a complete change in and of itself. It should have a clear summary, description, and test plan. It should leave the codebase in a consistent state: building and passing tests, etc.
In addition, ideally, it shouldn’t be possible to further split a commit into multiple commits that each stand on their own. If you can split a commit that way, you should.
What follows from those principles is the idea that commits, not branches, should be the unit of code review. The above description of a commit also describes the ideal code review: a single, well-described change that leaves the codebase in a consistent state, and that cannot be subdivided further.
If the commit is the unit of code review, then, why should the code review tool require that you make branches? spr’s answer is: it shouldn’t.
Following the one-commit-per-change principle maintains the invariant that checking out any commit on
main gives you a codebase that has been reviewed in that state, and that builds and passes tests, etc. This makes it easy to revert changes, and to bisect.
Git’s default branch name is
master, but GitHub’s is now
main, so we’ll use
main throughout this documentation.
brew install spr
nix-channel --update && nix-env -i spr
If you have Cargo installed (the Rust build tool), you can install spr by running
cargo install spr.
spr is written in Rust. You need a Rust toolchain to build from source. See rustup.rs for information on how to install Rust if you have not got a Rust toolchain on your system already.
With Rust all set up, clone this repository and run
cargo build --release. The spr binary will be in the
In the repo you want to use spr in, run
spr init; this will ask you several questions.
You’ll need to provide a GitHub personal access token (PAT) as the first step. See the GitHub docs on how to create one.
spr init will tell you which scopes the token must have; make sure to set them correctly when creating the token.
The rest of the settings that
spr init asks for have sensible defaults, so almost all users can simply accept the defaults. The most common situation where you would need to diverge from the defaults is if the remote representing GitHub is not called
See the Configuration reference page for full details about the available settings.
After initial setup, you can update your settings in several ways:
spr init. The defaults it suggests will be your existing settings, so you can easily change only what you need to.
git config --set(docs here).
This section details the process of putting a single commit up for review, and landing it (pushing it upstream). It assumes you don’t have multiple reviews in flight at the same time. That situation is covered in another guide, but you should be familiar with this single-review workflow before reading that one.
mainfrom upstream, and check it out.
Make your change, and run
git commit. See this guide for what to put in your commit message.
spr diff. This will create a PR for your HEAD commit.
Wait for reviewers to approve. If you need to make changes:
Make whatever changes you need in your working copy.
Amend them into your HEAD commit with
git commit --amend.
spr diff. If you changed the commit message in the previous step, you will need to add the flag
--update-message; see this guide for more detail.
This will update the PR with the new version of your HEAD commit. spr will prompt you for a short message that describes what you changed. You can also pass the update message on the command line using the
Once your PR is approved, run
spr landto push it upstream.
The above instructions have you committing directly to your local
main. Doing so will keep things simpler when you have multiple reviews in flight. However, spr does not require that you commit directly to
main. You can make branches if you prefer.
spr land will always push your commit to upstream
main, regardless of which local branch it was on. Note that
spr land won’t delete your feature branch.
When you run
spr diff to update an existing PR, your update will be added to the PR as a new commit, so that reviewers can see exactly what changed. The new commit’s message will be what you entered in step 4.3 of the instructions above.
The individual commits that you see in the PR are solely for the benefit of reviewers; they will not be reflected in the commit history when the PR is landed. The commit that eventually lands on upstream
main will always be a single commit, whose message is the title and description from the PR.
If you amend your local commit before landing, you must run
spr diff to update the PR before landing, or else
spr land will fail.
This is because
spr land checks to make sure that the following two operations result in exactly the same tree:
- Merging the PR directly into upstream
- Cherry-picking your HEAD commit onto upstream
This check prevents
spr land from either landing or silently dropping unreviewed changes.
spr land may fail with conflicts; for example, there may have been new changes pushed to upstream
main since you last rebased, and those changes conflict with your PR. In this case:
Rebase your PR onto latest upstream
main, resolving conflicts in the process.
spr diffto update the PR.
Note that even if your local commit (and your PR) is not based on the latest upstream
main, landing will still succeed as long as there are no conflicts with the actual latest upstream
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.
You should format your commit messages like this:
One-line title Then a description, which may be multiple lines long. This describes the change you are making with this commit. Test Plan: how to test the change in this commit. The test plan can also be several lines long. Reviewers: github-username-a, github-username-b
The first line will be the title of the PR created by
spr diff, and the rest of the lines except for the
Reviewers line will be the PR description (i.e. the content of the first comment). The GitHub users named on the
Reviewers line will be added to the PR as reviewers.
Test Plan section is required to be present by default;
spr diff will fail with an error if it isn’t.
You can disable this in the configuration.
When you create a PR with
spr diff, the PR becomes the source of truth for the title and description. When you land a commit with
spr land, its commit message will be amended to match the PR’s title and description, regardless of what is in your local repo.
If you want to update the title or description, there are two ways to do so:
Modify the PR through GitHub’s UI.
Amend the commit message locally, then run
spr diff --update-message. Note that this does not update reviewers; that must be done in the GitHub UI. If you amend the commit message but don’t include the
--update-messageflag, you’ll get an error.
If you want to go the other way — that is, make your local commit message match the PR’s title and description — you can run
At various stages of a commit’s lifecycle,
spr will add lines to the commit message:
After first creating a PR,
spr diffwill amend the commit message to include a line like this at the end:
Pull Request: https://github.com/example/project/pull/123
The presence or absence of this line is how
spr diffknows whether a commit already has a PR created for it, and thus whether it should create a new PR or update an existing one.
spr landwill amend the commit message to exactly match the title/description of the PR (just as
spr amenddoes), as well as adding a line like this:
Reviewed By: github-username-a
This line names the GitHub users who approved the PR.
This is what a commit message should look like when you first commit it, before running
spr at all:
Add feature This is a really cool feature! It's going to be great. Test Plan: - Run tests - Use the feature Reviewers: user-a, coworker-b
spr diff to create a PR, the local commit message will be amended to include a link to the PR:
Add feature This is a really cool feature! It's going to be great. Test Plan: - Run tests - Use the feature Reviewers: user-a, coworker-b Pull Request: https://github.com/example/my-thing/pull/123
In this state, running
spr diff again will update PR 123.
spr land will amend the commit message to have the exact title/description of PR 123, add the list of users who approved the PR, then land the commit. In this case, suppose only
Add feature This is a really cool feature! It's going to be great. Test Plan: - Run tests - Use the feature Reviewers: user-a, coworker-b Reviewed By: coworker-b Pull Request: https://github.com/example/my-thing/pull/123
spr is fairly permissive in parsing your commit message: it is case-insensitive, and it mostly ignores whitespace. You can run
spr format to rewrite your HEAD commit’s message to be in a canonical format.
This command does not touch GitHub; it doesn’t matter whether the commit has a PR created for it or not.
spr land will write the message of the commit it lands in the canonical format; you don’t need to do so yourself before landing.
While reviewing someone else’s pull request, it may be useful to pull their changes to your local repo, so you can run their code, or view it in your editor/IDE, etc.
To do so, get the number of the PR you want to pull, and run
spr patch <number>. This creates a local branch named
PR-<number>, and checks it out.
The head of this new local branch is the PR commit itself. The branch is based on the
main commit that was closest to the PR commit in the creator’s local repo. In between:
If the PR commit was directly on top of a
maincommit, then the PR commit will be the only one on the branch.
If there were commits between the PR commit and the nearest
maincommit, they will be squashed into a single commit in your new local branch.
Thus, the new local branch always has either one or two commits on it, before joining
You can amend the head commit of the
PR-<number> branch locally, and run
spr diff to update the PR; it doesn’t matter that you didn’t create the PR. However, doing so will overwrite the contents of the PR on GitHub with what you have locally. You should coordinate with the PR creator before doing so.
The recommended way to configure spr is to run
spr init, rather than setting config values manually. You can rerun
spr init to update config at any time.
spr uses the following Git configuration values:
|config key||CLI flag||description||default1||default in |
|The GitHub PAT (personal authentication token) to use for accessing the GitHub API.|
|Name of the git remote in this local repository that corresponds to GitHub|
|Name of repository on github.com in ||extracted from the URL of the GitHub remote|
|The name of the centrally shared branch into which the pull requests are merged||taken from repository configuration on GitHub|
|String used to prefix autogenerated names of pull request branches|
|If true, ||false|
|If true, ||true|
The config keys are all in the
sprsection; for example,
Values passed on the command line take precedence over values set in Git configuration.
Values are read from Git configuration as if by
git config --get, and thus follow its order of precedence in reading from local and global config files. See the git-config docs for dteails.
spr initwrites configured values into
.git/configin the local repo. (It must be run inside a Git repo.)
Value used by
spr if not set in configuration.
Value suggested by
spr init if not previously configured.
Be careful using this: your auth token will be in your shell history.