Building large features with small PRs
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.
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.
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
Note: We won’t be able to create a PR for
task-2
until the PR fortask-1
is merged. But this shouldn’t stop us from working ontask-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
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