To stack it or To break it: managing big changes in PRs
stacking PRs on top of one other or breaking them down in separate self-contained PRs for better code review and debugging
Breaking big PRs into smaller ones can help in the review process and manage big changes in code files especially when working in teams and collaborating with fellow developers.
However, when working on big features we don’t really plan out our changes and keep committing the changes in a single branch as we go. This can get more trickier in cases where commits are not organized.
Remember, as a rule, try not to make too many changes in a single commit. Keep commits organized and focused to facilitate code review or debugging at a later stage.
In case you wanna organize your commits, you can use git reset to open interactive rebase to rebase your changes.
Now let’s say you have set up a single branch for the new feature with all the commits there and a large number of changes. How would you break the changes to make it easy to code review?
Let’s draw a scenario here -
Mayank Doe is working on the payments feature in the branch `feat/payments-module’. Over 2k+ loc has been made with 8 commits (5 related to feat and the remaining 3 for tests). Commit History-
commit 8fbf3e12a4b1 (HEAD -> feat/payments-module, origin/feat/payments-module)
Author: Mayank Doe <mayank.doe@example.com>
Date: Mon Jan 13 15:00:00 2025 +0530
test: add integration tests for payment gateway interactions
commit 9c23b1a5d6e4
Author: Mayank Doe <mayank.doe@example.com>
Date: Sun Jan 12 23:45:33 2025 +0530
test: implement unit tests for payment validation logic
commit 7e12f4c5a3d9
Author: Mayank Doe <mayank.doe@example.com>
Date: Sun Jan 12 18:30:20 2025 +0530
test: write E2E tests for subscription flow with payments
commit 5f12ac78d4e1
Author: Mayank Doe <mayank.doe@example.com>
Date: Sat Jan 11 16:10:05 2025 +0530
feat: add support for multiple payment methods in checkout
commit 3c9ed5b1f2d8
Author: Mayank Doe <mayank.doe@example.com>
Date: Sat Jan 11 11:45:15 2025 +0530
feat: integrate Stripe API for payment processing
commit 1b9ec3f4a2d7
Author: Mayank Doe <mayank.doe@example.com>
Date: Fri Jan 10 20:30:10 2025 +0530
feat: design payment modal with validation rules
commit 2c1ab5f6d3e8
Author: Mayank Doe <mayank.doe@example.com>
Date: Fri Jan 10 14:25:00 2025 +0530
feat: add backend API endpoint for initiating payments
commit 6f3e1b2d4f9c
Author: Mayank Doe <mayank.doe@example.com>
Date: Thu Jan 9 22:00:25 2025 +0530
feat: create models and database schema for payment transactions
Strategies covered in the blog to use in this scenario -
Stacking the PR - like a stack on top of each other
Breaking the PR - like an atom separate from each other
Stacking the PR
To stack the PR, we will batch the commits that belong together and divide them into multiple PRs.
For the above scenario, we will batch
first three commits (creating db models and backend API endpoint for payments)
then the next two commits (integrating Stripe API and extending support for multiple methods)
and the last three commits (unit, E2E, integration tests for payments).
We will create three separate branches (diff from feat/payments-module) and cherry-pick related commits.
Here are the steps in detail:
Checkout to main or develop branch and use that as a base branch to create another branch
git checkout develop Switched to branch 'develop' Your branch is up to date with 'origin/develop'.git checkout -b feat/payments-module-1 Switched to branch 'feat/payments-module-1'
Now, cherry-pick commits for the first batch
git cherry-pick <commit-id-1> <commit-id-2> <commit-id-3>git cherry-pick 6f3e1b2d4f9c 2c1ab5f6d3e8 1b9ec3f4a2d7Resolve any merge conflicts that may arise manually. Use `git cherry-pick --abort` to abort the operation.
Once you have the commits, you can push the changes and raise a PR on GitHub or your preferred platform pointing to the base branch, `origin/develop` in this scenario. (PR#1)
git push origin feat/payments-module-1 origin/develop <-> feat/payments-module-1After raising the PR, continue with the next steps.
Create a diff branch using the first branch, `payments-module-1` as base branch.
git checkout -b feat/payments-module-2 Switched to a new branch 'feat/payments-module-2'(Note:- using prev branch as the base branch is the key here.)
Now, similarly to first, cherry-pick the commits for the second batch
git cherry-pick 3c9ed5b1f2d8 5f12ac78d4e1After commits for the second batch have been moved, you will have the changes from the first and second batches. Now move on to creating a second PR pointing to prev PR’s branch (feat/payment-module-1) (PR#2)
git push origin feat/payments-module-2 feat/payments-module-1 <-> feat/payments-module-2Since the second PR branch points to the first PRs, only the changes new in the second will be highlighted making review easier.
Similarly, now, checkout from the second branch to the third branch and cherry-pick for the third and final batch
git checkout -b feat/payments-module-test Switched to a new branch 'feat/payments-module-test' git cherry-pick 7e12f4c5a3d9 9c23b1a5d6e4 8fbf3e12a4b1Resolve any merge conflicts that may arise. By now, you should have all the commits including the ones from prev branches.
Now move on to creating a new PR pointing to the second PR’s branch
git push origin feat/payments-module-test feat/payments-module-2 <-> feat/payments-module-test
This way, you will have 3 PRs stacked on top of each other (origin/develop <-> feat/payments-module-1 <-> feat/payments-module-2 <-> feat/payments-module-test) and will make it easier to navigate and review the changes.
Creating a Break PR
Another strategy is to break into two different PRs however unlike stacking where the previous branch becomes the base branch, the base branch remains the same i.e., the main or develop branch and PRs will be separated from each other.
If you were to create a break PR for the above scenario, the only change you would have to do is to use origin/develop as your base branch. Checkout to develop branch create a new branch, cherry-pick the changes and raise the PR pointing to origin/develop in each case.
git checkout develop
Switched to branch 'develop' Your branch is up to date with 'origin/develop'.git checkout -b feat/payments-module-1
Switched to branch 'feat/payments-module-1'git cherry-pick 6f3e1b2d4f9c 2c1ab5f6d3e8 1b9ec3f4a2d7
git push origin feat/payments-module-1(Raise the first PR pointing to ‘origin/develop‘)
origin/develop <-> feat/payments-module-1git checkout develop
git checkout -b feat/payments-module-2
Switched to branch 'feat/payments-module-2'git cherry-pick 3c9ed5b1f2d8 5f12ac78d4e1
git push origin feat/payments-module-2(Raise the first PR pointing to ‘origin/develop‘)
origin/develop <-> feat/payments-module-2Similarly, for third PR, checkout to develop first, then create a new branch and cherry-pick the commits.
git checkout develop
git checkout -b feat/payments-module-test
Switched to branch 'feat/payments-module-test'git cherry-pick 7e12f4c5a3d9 9c23b1a5d6e4 8fbf3e12a4b1
git push origin feat/payments-module-testRaise the third PR again pointing to develop base branch.
origin/develop <-> feat/payments-module-testThe idea behind creating a breakdown is to have separate PRs along with the branches. Instead of stacking by pointing to prev branch, a single branch is used as the base branch.
What will be the Merge Strategy?
Having a clear merge strategy helps to ensure stability, minimize conflicts, and maintain a clean project history.
For Stack PR-
The best way is to go from bottom-to-top. Merging the last branch in the stack all the way to the top and then the final merge to the main/develop branch will consist of all the commits and can be finally squash-and-merge or rebased depending on the preference.
In the scenario used above, PR#3 will be first to get merged into PR#2 then that into PR#1 and finally PR#1 into `origin/develop`.
For Break PR-
Generally, break PRs are self-contained, independent of other PRs. Hence, they can be simply merged in any order. The preference might change in case changes are dependent or commits needs to be maintained in a certain order.
In above scenario, changes are dependent on each other and hence, the preference will be to merge in a particular order, i.e., PR#1 followed by PR#2 and PR#3.
when to Stack it and when to Break it?
It depends, obviously. Generally, in case changes are similar in nature and depend on each other, it is better to stack the PR as changes unravel with each PR without missing/breaking the logic from previous PRs.
There is no hard-core restriction though, both strategies are swappable and can be used as per need and convenience. As for the scenario used, both strategies have been used. However, the stack PR approach would be more effective as all the commits are part of one feature and it makes more sense to stack them together.
Remember, at the end what’s important is to ensure clutter-free coding and review process. The more effort made while writing and testing code intially reduces the need to debug at later stage.
To conclude for creating Stack or Break PR,
divide the changes into batches and use cherry-pick to only pick the commits you want
if stack PR, point to prev branch and keep stacking on top of the previous ones
if break PR, point to a single develop/main branch
no restriction on merge strategy for either, however, it’s better to merge from bottom—to-top in stack approach
stack it in case changes are dependent, break it in case changes are self-contained or can be pointed to a single branch
these were some of my learnings while managing PRs and dealing with big changes. try to explore and come up with your own strategies and techniques.
until next time,
mayank bansal
signing off…
