Smaller & Faster Code Review with Stacked Merge Request

Irsyad Rizaldi
Inside Bukalapak
Published in
7 min readMar 25, 2021
how code review is usually done

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.

new branch_2 changes

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.

--

--