When working on a team, one of the most inherently difficult and more involved processes is code reviews. To review a large pull request (PR), you need time and context as well as the energy to parse and hold your mental boundaries in focus.
The same cannot be said for small PRs, however, where it’s much easier to consider changes and to suggest changes of our own. So how can we introduce large changes while avoiding cognitively overloading our teammates? This is where stacked pull requests come into play.
In this article, we will cover what stacked pull requests are, when and how to use them, and how to convert a monolithic PR into a stacked one.
Stacked PRs, also know as dependent, incremental, or chained PRs, are pull requests that branch off from other pull requests. In git terms, they are feature branches that are checked out from other feature branches to build small and coherent units to represent changes.
When working with stacked PRs, it’s helpful to think of your branches as a similar layer of code-change organization to git commits. You have the choice between pushing all your changes as a single big commit and organizing your code in separate commits. Having multiple commits is the better practice. So, what good is there in doing the same with your branches?
As previously stated, stacked PRs are useful when wanting to split large pull requests. The other situation where stacked PRs really shine is when you want to use a particular change in two or more branches.
For instance, imagine wanting to migrate a codebase to TypeScript where you rewrite the pages in TS while your teammate rewrites the components. The TypeScript setup (dependency installation, tsconfig.json, etc.) would have to be shared between the two of you, either by eagerly committing the setup to the master (or develop) branch, or by stacking your PRs on top of a ts-setup feature branch.
This would allow the two branches, migrate-pages and migrate-components, to share the TypeScript configuration in a master-like relationship with the ts-setup branch. This means that if a change occurs in ts-setup, migrate-pages would have to merge or rebase ts-setup.
If page migration is dependent on components migration, you can stack the branches even further.
This is especially handy when two people are trying to collaborate on the same feature. Stacking two branches is easier to handle than working on the same branch.
To stack two PRs, checkout the first branch from your base master (or develop) and push your changes.
In your GitHub repository, you’ll be prompted to create a pull request from ts-setup:
Create the PR with the base as master.
Then, checkout the second branch from the first.
This effectively turns ts-setup and migrate-components into stacked branches ready to become stacked PRs.
Notice that while master is set as the base of our PR, the changes from ts-setup (“Setup TypeScript” commit) are present, and our commit count is at two.
Changing the base branch to ts-setup removes overlapping commits, bringing our commit count to just one.
Make sure to clearly state that a PR is stacked on top of another. A label might also help.
The worst-case scenario is someone merging a PR, pulling master, not finding the changes, and getting confused, which begs the question, how do you merge stacked PRs?
The only restriction you have on merging while working with stacked PRs is that you cannot “squash and merge” or “rebase and merge.” You must merge directly. This restriction does not apply to the last PR on a given PR chain.
This is due to how git’s history works. Git tracks changes through commits by commit hashes. If you recall, changing the base from master to ts-setup shaved off the common commit between ts-setup and migrate-components.
Git knew to do so because it saw a commit with the same metadata on the two branches. Squashing and rebasing both overwrites Git’s history (albeit in different ways), removing the overlap that deemed the two branches continuous.
TL;DR: All orders are valid. It depends on how you want the merge commits to look on master.
The order in which we should merge or stacked PRs is completely subjective. If we merge ts-setup with a commit message of “Setup TypeScript” and delete the PR branch, GitHub will automatically pick up on this and change the base of our migrate-components PR to master.
This will give us the chance to merge with master with a separate merge commit message, “Migrate Components to TS.”
Alternatively, we can first merge migrate-components into ts-setup, then merge ts-setup with master with a single merge commit message to master of “Setup and migrate components to TS.”
Let’s say we’re trying to merge a large migrate-to-firebase branch with develop. The PR affects tens of files and has proven to be difficult to review. To split it into multiple PRs, locally, we do the following:
First, we checkout the branch and then we uncommit all the changes that do not exist on develop without removing the changes themselves. This results in all the changes being staged as git status would indicate, so we unstage them by running git restore --staged ..
You can rename the branch to give an accurate account of what it actually does by running:
You then can start adding, committing, and checking out new branches, forming a chain.
One of the issues you encounter while using the Gitflow workflow is being unable to selectively push feature branches from develop in a given release. For example, if you have a redesign coming up that you wish to work on but not release yet, you can scope that redesign in a parent feature branch that smaller feature branches can be stacked on top of, then merge that parent branch with develop once it’s finished.
In this article, we learned about stacked PRs, why and when to use them, and how to create and merge them. We also talked about how they can enhance the Gitflow workflow. Thanks for reading!