spr 1.3.7

Submit pull requests for individual, amendable, rebaseable commits to GitHub
Documentation
# 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](./simple.md).

You'll use Git's [interactive rebase](https://git-scm.com/docs/git-rebase#_interactive_mode) 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 this[^rebase-cmds]:

      ```
      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]#cherry-picking 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]./commit-message.md 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`.

[^rebase-cmds]: You can shorten `exec` to `x`, `fixup` to `f`, and `squash` to `s`; they are spelled out here for clarity.