May 26, 2016 · git github

Stacked Pull Requests: Keeping GitHub Diffs Small

Code reviews aren't just for catching bugs. They also help teams:

  1. Build trust and create a shared coding standard
  2. Force the discussion and vetting of code design
  3. Prevent broken windows

But if a pull request gets too big, it causes code review fatigue:

500 lines of code = "looks fine."

What you'll learn from this post:

1. Create a WIP Pull Request

As with most Git workflows, start your work in a feature branch:

$ git checkout -b somefeature
$ git commit -am "first change"

Now, before you've gone too far, open a WIP PR. Why?

The following uses GitHub CLI tool hub, although you could just use the site directly:

$ hub pull-request

WIP - Adding somefeature

- [x] Do the thing
- [ ] Do the other thing

Now hack away, committing early and often. Keep your team updated by pushing and checking off the PR to-dos.

Once the WIP is ready to ship, close the PR, we're ready to start our stacked PRs.

Tip: Don't delete the branch just yet. It's handy to have a remote backup just in case.

2. Create Stacked Pull Requests

Start by creating a new branch, somefeature1, with all of the changes from somefeature unstaged:

$ git checkout -b somefeature1 somefeature
$ git reset master

Running git status now shows all the changes we made in somefeature copied over to somefeature1 in an uncommitted state. From here we can start creating the commits that will make up our stacked PR.

Commit 1

Reach for git add --patch to create our first logical commit in the somefeature1 branch:

$ git add --patch

Once the changes have been staged, stash everything else and verify things still work:

$ git stash --include-untracked --keep-index
$ make test

Tests pass. Time to create our first commit and PR in the stack:

$ git commit -m "[somefeature1 - PART1] Make some change to x"
$ git push origin somefeature1
$ hub pull-request

[somefeature - PART1] Make some change to x

Commits 2 through N

For the next commit, start by creating a new branch, somefeature2 with our stash popped:

$ git checkout -b somefeature2
$ git stash pop

Now back to git add --patch to create and test our next commit:

$ git add --patch
$ git stash --include-untracked --keep-index
$ make test

$ git commit -m "[somefeature - PART2] Make change to y"
$ git push origin somefeature2

When creating the PR, make sure to base it off of the previous branch with the -b flag:

$ hub pull-request -b somefeature1

Repeat this process until you've made it all the way through your stash.

The Finished Product

The result is a stack of focused, reviewable PRs.

Your Git graph should look a bit like so, with each branch depending on the previous one:

            * somefeature3
          * somefeature2
        * somefeature1
------* master
        * somefeature

Your stacked PRs are now ready for review.

3. Feedback, Beautiful Feedback

Now that you've pinged the team for review, comments should start coming in.

Notice how much feedback stacked PRs provoke:

Compare that with a rather large PR that garnered very little feedback:

~60 comments difference—that's huge.

Now address feedback and push until you have enough stamps to merge.

Tip: Merge Up, Squash Down

When making additional commits to your branches, be sure to propagate them up the stack via git merge, not git rebase:

$ git co myfeature2
$ git commit -am "addressing this feedback"
$ git commit -am "addressing that feedback"
$ git push origin myfeature2

$ git co myfeature3
$ git merge myfeature2 # integrate changes from myfeature2
$ make test

Rebasing focuses on keeping later branches tipped to the front of earlier branches. This is a noble goal, but it forces you to resolve merge conflicts 1 commit at a time! In the case of stacked PRs, it's just not necessary because we'll be squashing our commits when we merge the stack anyways.

Make your life easier and just use git merge. If multiple commits have conflicts, you'll be able to resolve them in one go.

The same goes for staying up to date with master: consider merging it in instead of rebasing.

4. Landing the Stack

At this point you've collected a "LGTM" for each PR in the stack, and it's time to merge.

It can be tempting to merge PART1 of the stack in, since that PR will most likely be stamped first. Resist the urge; merging the first PR will cause all following PRs to become unmergeable! This is because GitHub does not support updating the target branch on PRs.

Instead, go through the stack from LAST to FIRST, and merge your stack into master:

The result is a single, well-reviewed commit on master:

5. Measure the Difference

Stacked PRs lead to more thoroughly reviewed code shipping faster.

My experience? Very positive. My last stack -

~2000 lines of code, over 60 comments of feedback, 20+ commits addressing feedback

Compare your next stack to a large and long running PR to see the difference.