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?
TL;DR:
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.
$ git status # we are on master On branch master $ git checkout -b ts-setup # checkout from master $ npm i -D typescript && npx tsc --init $ git add . && git commit -m 'Setup TypeScript' $ git push -u origin ts-setup
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.
$ git status On branch ts-setup $ git checkout -b migrate-components # checkout from ts-setup $ mv components/Button.jsx components/Button.tsx $ git add . && git commit -m 'Migrate button to TS' $ git push -u origin migrate-components
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:
$ git checkout migrate-to-firebase $ git reset --soft develop $ git restore --staged .
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:
$ git branch -M setup-firebase
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowIn this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
SOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
3 Replies to "Using stacked pull requests in GitHub"
In your example to break up a large PR, you suggest:
$ git checkout migrate-to-firebase
$ git reset –soft develop
$ git restore –staged .
Is that not the same thing as:
$ git checkout migrate-to-firebase
$ git reset develop
Thanks.
You’re right, David. And you might require a `git rebase develop` before all that if `develop` has newer changes.
As of today, the Github feature no longer works for me. I had this stack of PRs: https://github.com/pypa/setuptools/pull/4103, https://github.com/pypa/setuptools/pull/4069. After merging 4103, 4069 reported that the target branch had been deleted and closed itself without the ability to re-open. Perhaps the “delete branch on merge” feature happens too soon and prevents the “Base automatically change” from taking effect?