Introduction
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.
To get started, see the installation instructions, and the first-time setup. (You’ll need to go through setup in each repo where you want to use spr.)
Workflow overview
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
main
1 branch. -
Run
spr diff
to send out your commit for review on GitHub. -
If you need to make updates in response to feedback, amend your commit, and run
spr diff
again to send those updates to GitHub.Similarly, you can rebase onto newer upstream
main
and runspr diff
to reflect any resulting changes to your commit. -
Once reviewers have approved, run
spr land
. This will put your commit on top of the latestmain
and 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
main
.
Rationale
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 main
.
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 main
).
Why Review Commits?
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.
Installation
Binary Installation
Using Homebrew
brew install spr
Using Nix
nix-channel --update && nix-env -i spr
Using Cargo
If you have Cargo installed (the Rust build tool), you can install spr by running cargo install spr
.
Install from Source
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 target/release
directory.
Set up spr
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 origin
.
See the Configuration reference page for full details about the available settings.
After initial setup, you can update your settings in several ways:
-
Simply rerun
spr init
. The defaults it suggests will be your existing settings, so you can easily change only what you need to. -
Use
git config --set
(docs here). -
Edit the
[spr]
section of.git/config
directly.
Create and Land a Simple PR
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.
-
Pull
main
from upstream, and check it out. -
Make your change, and run
git commit
. See this guide for what to put in your commit message. -
Run
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
. -
Run
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
--message
/-m
flag ofspr diff
.
-
-
Once your PR is approved, run
spr land
to 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 update
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.
Updating before landing
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
main
. - Cherry-picking your HEAD commit onto upstream
main
.
This check prevents spr land
from either landing or silently dropping unreviewed changes.
Conflicts on landing
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. -
Run
spr diff
to update the PR. -
Run
spr land
again.
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 main
.
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.
Format and Update Commit Messages
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.
The 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.
Updating the commit message
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-message
flag, 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 spr amend
.
Further information
Fields added by spr
At various stages of a commit’s lifecycle, spr
will add lines to the commit message:
-
After first creating a PR,
spr diff
will 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 diff
knows 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 land
will amend the commit message to exactly match the title/description of the PR (just asspr amend
does), as well as adding a line like this:Reviewed By: github-username-a
This line names the GitHub users who approved the PR.
Example commit message lifecycle
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
After running 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.
Running 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 coworker-b
approved:
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
Reformatting the commit message
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.
Note that 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.
Check Out Someone Else’s PR
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
main
commit, then the PR commit will be the only one on the branch. -
If there were commits between the PR commit and the nearest
main
commit, 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 main
.
Updating the PR
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.
Configuration
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 spr init 2 |
---|---|---|---|---|
githubAuthToken | --github-auth-token 3 | The GitHub PAT (personal authentication token) to use for accessing the GitHub API. | ||
githubRemoteName | Name of the git remote in this local repository that corresponds to GitHub | origin | origin | |
githubRepository | --github-repository | Name of repository on github.com in owner/repo format | extracted from the URL of the GitHub remote | |
githubMasterBranch | The name of the centrally shared branch into which the pull requests are merged | master | taken from repository configuration on GitHub | |
branchPrefix | --branch-prefix | String used to prefix autogenerated names of pull request branches | spr/GITHUB_USERNAME/ | |
requireApproval | If true, spr land will refuse to land a pull request that is not accepted | false | ||
requireTestPlan | If true, spr diff will refuse to process a commit without a test plan | true |
-
The config keys are all in the
spr
section; for example,spr.githubAuthToken
. -
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 init
writes configured values into.git/config
in 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.