spr

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:

  1. Make your change as a single commit, directly on your local main1 branch.

  2. Run spr diff to send out your commit for review on GitHub.

  3. 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 run spr diff to reflect any resulting changes to your commit.

  4. Once reviewers have approved, run spr land. This will put your commit on top of the latest main 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.

1

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.

  1. Pull main from upstream, and check it out.

  2. Make your change, and run git commit. See this guide for what to put in your commit message.

  3. Run spr diff. This will create a PR for your HEAD commit.

  4. Wait for reviewers to approve. If you need to make changes:

    1. Make whatever changes you need in your working copy.

    2. Amend them into your HEAD commit with git commit --amend.

    3. 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 of spr diff.

  5. 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:

  1. Rebase your PR onto latest upstream main, resolving conflicts in the process.

  2. Run spr diff to update the PR.

  3. 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.

  1. Make a change and commit it on main. We’ll call this commit A.

  2. Make another change and commit it on top of commit A. We’ll call this commit B.

  3. Run spr diff --all. This is equivalent to calling spr diff on each commit starting from HEAD and 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.

  4. Suppose you need to update commit A in response to review feedback. You would:

    1. Make the change and commit it on top of commit B, with a throwaway message.

    2. 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.

  5. 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:

    1. 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
      
  6. 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 plain spr 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:

  1. Make commit A on top of main as before, and run spr diff.

  2. Make commit B on top of A as before, and run spr diff --cherry-pick. The flag causes spr diff to 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.)

  3. 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 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.

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:

  1. Run git pull --rebase on main, resolving conflicts along the way as needed.

  2. Run spr diff --all.

1

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 as spr 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.

Diagram of the branching scheme

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 keyCLI flagdescriptiondefault1default in spr init2
githubAuthToken--github-auth-token3The GitHub PAT (personal authentication token) to use for accessing the GitHub API.
githubRemoteNameName of the git remote in this local repository that corresponds to GitHuboriginorigin
githubRepository--github-repositoryName of repository on github.com in owner/repo formatextracted from the URL of the GitHub remote
githubMasterBranchThe name of the centrally shared branch into which the pull requests are mergedmastertaken from repository configuration on GitHub
branchPrefix--branch-prefixString used to prefix autogenerated names of pull request branchesspr/GITHUB_USERNAME/
requireApprovalIf true, spr land will refuse to land a pull request that is not acceptedfalse
requireTestPlanIf true, spr diff will refuse to process a commit without a test plantrue
  • 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.)

1

Value used by spr if not set in configuration.

2

Value suggested by spr init if not previously configured.

3

Be careful using this: your auth token will be in your shell history.