Smaller & Faster Code Review with Stacked Merge Request
Background
Let’s face the truth, code review is important but kinda boring. Just like the meme above, people tend to approve huge MR because it’s hard to navigate the files and understand the logic, especially from a web browser.
If huge MR is bad, then why people keep doing it anyway? In their defense, creating small MR will block their workflow, thus prolong the development time. That statement is partially correct. In a simple create-from-master branch workflow, each branch needs to get reviewed & merged first before the next branch can use it.
Is it possible to create a small MR without blocking our workflow? The answer is yes, and this guide will help you create your first stacked MR!
Creating New Branches
Let’s say we want to implement the code above. The code was tightly related, but it’s possible to split our commit and branch by each function we want to implement.
Our aim right now is to create 2 branches, branch_1
will implement do_first()
and branch_2
will implement do_second()
.
First Branch
First, create branch_1
from master
branch and implement our do_first()
function there.
// make sure you're in master branch$ git checkout -b branch_1
Switched to a new branch 'branch_1'
After that, commit and push our changes, then continue to the next branch.
$ git add main.py
$ git commit -m "implement first function"
[branch_1 71f1a7f] implement first function
1 file changed, 2 insertions(+)
create mode 100644 main.py
$ git push --set-upstream origin branch_1
...
Branch 'branch_1' set up to track remote branch 'branch_1' from 'origin'.
Second Branch
On the second branch, we need changes from branch_1
to implement do_second()
function. To do that, we need to create branch_2
from branch_1
.
// make sure you're in branch_1 branch$ git checkout -b branch_2
Switched to a new branch 'branch_2'
After that, commit and push our changes, and we’re done with main.py
file implementation.
$ git add main.py
$ git commit -m "implement second function"
[branch_2 d795b58] implement second function
1 file changed, 5 insertions(+)
$ git push --set-upstream origin branch_2
...
Branch 'branch_2' set up to track remote branch 'branch_2' from 'origin'.
End Result
Check your git log
, the result should look like this.
// make sure you're in branch_2 branch$ git log
commit ... (HEAD -> branch_2, origin/branch_2)
Author: ...
Date: ...implement second functioncommit ... (origin/branch_1, branch_1)
Author: ...
Date: ...implement first functioncommit ... (origin/master, master)
Author: ...
Date: ......
Creating New Merge Requests
Once the implementation is done, we need to create MR for others to review. Our aim right now is to create MR for branch_1
and branch_2
on GitLab.
First Branch
Navigate to the new merge request page. Since our branch_1
is branched from master
, we need to set the source to branch_1
and the target to master
.
After submitting, you should be able to see do_first()
implementation in the changes tab.
Second Branch
Next, we need to repeat the same procedure for the second branch. Since branch_2
is branched from branch_1
, we need to set the source to branch_2
and the target to branch_1
.
After submitting, you should be able to see do_second()
implementation in the changes tab.
Adding New Changes
As the review goes on, we might need to modify the existing implementation. This is not straightforward in stacked MR, and that’s why we need to learn the right strategy to do it.
There are several ways to do this, but I prefer to use 1-commit-per-branch strategy in my stacked MR. Every branch/MR must only contain one commit, and a newer commit should be squashed into the first commit.
The rationale is that our changes are small enough that there was no need to have multiple commits in one branch. In my opinion, lots of commits bring lots of conflicts, so I prefer to avoid them.
Adding New Changes to First Branch
Let’s say we want to modify do_first()
function on branch_1
, what do we need to do? First, we switch to branch_1
and implement our modification.
$ git checkout branch_1
Switched to branch 'branch_1'
Your branch is up to date with 'origin/branch_1'.
We want to modify the last commit instead of creating a new one. To do this, we can use --amend
option in git commit
.
$ git add main.py
$ git commit --amend -m "implement modified first function"
[branch_1 5a10a72] implement modified first function
Date: Mon Mar 22 14:30:17 2021 +0700
1 file changed, 2 insertions(+)
create mode 100644 main.py
Next, we want to show these new changes in branch_1
MR. Due to commit modification, our local and remote branch now are in conflict. We need to override our remote with the local branch, and to do this, we can use --force
option in git push
.
$ git push --force
...
+ b33d7cc...5a10a72 branch_1 -> branch_1 (forced update)
After push, you should be able to see the new changes in branch_1
MR.
Propagate Changes to Second Branch
Try open branch_2
MR and you will notice that our second branch is still using the old do_first()
function. This happens because we did not propagate changes in branch_1
to branch_2
.
Our branch_2
now has diverged from branch_1
. We need to override commit 3
with commit 5
as shown in the figure above. To do this, we can use git rebase
command.
Now, switch to branch_2
and rebase onto branch_1
. To remove a commit, set the action to drop
at the commit we want to remove.
$ git checkout branch_2
Switched to branch 'branch_2'
Your branch is up to date with 'origin/branch_2'.$ git rebase branch_1 --interactive// git interactive editor
drop b33d7cc implement first function
pick c6d6abe implement second function
[detached HEAD 3001b78] implement second function
1 file changed, 5 insertions(+)
Successfully rebased and updated refs/heads/branch_2.
The next thing to do is to push our changes to remote branch_2
.
$ git push --force
...
+ c6d6abe...3001b78 branch_2 -> branch_2 (forced update)
Now you should be able to see the new changes from branch_1
in branch_2
MR.
Merging to Master
There are several ways to do this, but I prefer to merge each of my stacked MR directly into master
. The rationale is it’s fast, as you have the option to merge all of your MR sequentially and immediately without waiting for master
pipeline to finish.
First Branch
The first branch is straightforward, just click merge
button and we’re done.
Second Branch
In the second branch, you will notice that GitLab is not able to merge your MR. This happens because we choose to delete branch_1
during the previous merge. This is intentional because we’re supposed to merge branch_2
into master
instead of branch_1
.
We need to switch the target branch from branch_1
to master
in the edit merge request page. Once done, we can merge branch_2
MR directly into master
.
Conclusion
While not exhaustive, you may found this guide to be applicable in most git repositories. It’s also possible to stack an infinite number of MR by repeating the same procedure.
Stacked MR may not be easy to manage, but in my opinion, the benefit from a parallel development-and-review, less painful review, and better review quality really outweigh the effort.