Building large features with small PRs

Original illustration by Sagar

At Funding Societies | Modalku, we are generally encouraged to keep our pull requests small and focussed — so that they can be easily reviewed and merged. Even for new features, the idea is to integrate well-tested code changes early and control their visibility in production using feature flags (control variables that we can toggle from the backend to manage a given user’s access to a given feature)

However, the reality is that pull requests take time to review and this often blocks engineers from being able to push more changes. In this post, I describe a process to build a seemingly large feature through a chain of small PRs while also keeping the VCS history readable.

Motivation

  • Reviewer Empathy: As the author of a PR, it is our responsibility to make it easy to review. With small PRs, reviewing is simple and straight-forward.
  • Progress Tracking: More often than not, a feature is broken down into tasks and tracked on project management tools like JIRA. By splitting the feature into multiple PRs, we can create a one-to-one relationship between the project management tasks and source code pull requests and track the progress of the entire feature to completion easily.

Process

Let’s start by creating a branch for the feature (note that this can be master if you follow trunk-based development) and another for the first task under the feature.

(master) $ git checkout -b feature
(feature) $ git checkout -b feature/task-1

Now we can start pushing commits to the feature/task-1 branch and create a PR upon completion with the feature branch as the base.

Lifecycle of any feature/task-* branch. It is eventually squash-merged into the main feature branch.

While we wait for the PR to be reviewed, we can start working on the next task of this feature.

If the next task is not dependent on the previous one, we can simply branch off from the feature branch and create a PR back to the feature branch.

If the next task is dependent on the previous one, we branch off from feature/task-1 to create the feature/task-2 branch.

(feature) $ git checkout feature/task-1
(task-1) $ git checkout -b feature/task-2

Now we can start pushing commits to the feature/task-2 branch.

task-2 is branched off from the task-1 branch at some commit (often the latest one when the PR was opened)

Meanwhile, if there is any feedback on the feature/task-1 PR, we can make those changes and keep updating the branch until it eventually gets approved.

It is highly recommended to regularly rebase the feature/task-2 branch against feature/task-1 since there are changes going into the latter in parallel. While rebasing, if there are any conflicts, they will have to be resolved. You can learn more about Git rebasing from here.

(task-1) $ git checkout feature/task-2
(task-2) $ git rebase feature/task-1
task-2 branch (after it is rebased against task-1)

Note: We won’t be able to create a PR for task-2 until the PR for task-1 is merged. But this shouldn’t stop us from working on task-3 using the same approach.

When the pull request from feature/task-1 is squash-merged into feature, we will have a single commit on the latter that comprises of all the changes in the feature/task-1 branch.

Now ensure that the feature/task-2 branch has all the commits from the feature/task-1 branch by rebasing. Before we can create a PR to feature, we need to update the base of this branch such that the PR contains only commits from feature/task-2. This can be done using the rebasing against feature but with the --onto option (this is where the real magic happens)

(task-2) $ git rebase --onto feature feature/task-1 feature/task-2
task-2 branch (after it is rebased against the main feature branch with the squashed task-1 commit)

With the command above, we are asking git to update the base of feature/task-2 branch to feature branch and apply only the commits from feature/task-2 that are not in feature/task-1. Now the feature/task-2 branch should have the 1 squash-merged commit on feature followed by its own commits. We are now ready to open our second pull request!

This process can be repeated multiple times for all the tasks that make up the feature.

Conclusion

The process that I have described above has the following benefits:

  • Developers are not blocked by PRs that are pending review
  • Small and incremental PRs that are easy to review
  • A sane and clean VCS (i.e. Git here) history

--

--