Why I prefer rebase over merge (and everything else)
Early humans were not able to answer this important question, probably because they were too busy drawing animals on cave walls and certainly not because git was created dozens of millenniums later.
Fast-forward tens of thousands of years, and here we are, working with git for the last twenty-ish years, so we finally have a definitive, completely unambiguous answer!
Altamira Cave paintings: still better than Picasso
Well, to be completely honest with you, I’m lying. There is no clear answer, only a preference. What I like to do is already suggested in the title, but a preference is not necessarily a completely subjective concept – arguments do come into play, and here is why I prefer rebasing as an option.
Why rebase over merge?
History Cleanliness and Simplicity
Linear History
No matter how many times you use it, rebase creates a linear and cleaner project history, making it easier to follow changes. This is in contrast with the often messy and complex commit history that results from merging changes.
Easier Debugging
Linear history simplifies the process of tracking down bugs and understanding the progression of changes, as there are no unnecessary merge commits cluttering/bloating the history.
Review Process
Streamlined Code Reviews / Focused changes
A rebased branch with a linear history is easier to review as the reviewers can see a clear progression of RELEVANT changes. For example, when merging, each merge commit is considered a new commit in a feature branch, meaning it will be shown as part of the changes in that branch.
Do we really care about what was merged? Personally, I don’t, so I only want to be able to see the changes that are actually relevant for a specific feature.
Conflict Resolution
Simpler Conflict Context
During a rebase, conflicts are resolved step-by-step for each conflicting commit, meaning the context is smaller as it is contained. When merging, especially when there are a lot of changes to merge, resolving conflicts can be a tedious process as there are many of them.
Interactive rebase
If you are thinking about tearing your face off after reading the things above, I strongly suggest that you close this article immediately because what’s coming is even worse.
And, just a note before proceeding: don’t use interactive rebase if you are not familiar with the concept of rebasing, as you can easily brick your branch. Rebase is a destructive process as it changes git history/commit SHAs in most cases. Yes, git reflog can be useful for restoring in those kind of situations, but it’s best to avoid them completely.
Rebase is my personal favorite, but adding -i opens up a whole range of new possibilities of commit editing (reword, squash, move, delete, amend etc.). How far back interactive rebase goes depends on the argument passed. For instance, git rebase -i HEAD~3 will take into account the last 3 commits starting from the HEAD, which is the most recent one.
I’ve found it useful in several cases:
- Removing commits which introduced bugs
- Instead of adding separate revert commit, you can just pick commits that will be thrown away like they never existed in the first place
- Amending commits
- git commit –amend allows amending the last commit, but what if you need to amend several of them, e.g. first and third?
- Reordering commits
Why do it on your main branch?
Our mantra in Infobip is that we are humble engineers, so my very humble opinion is that the main branch should contain only one type of commit – working ones.
But we all know that mistakes are inevitable (guess the reference, hint – the purple villain that annoys the hell out of the Avengers), and they do happen, more often than you think. Commits containing app-breaking bugs end up in the main branch. Sometimes even working commits end up on top of them. And what value do those commits have? None.
You should be able to checkout whatever commit there is and run the app as it was at that point in time (excluding other dependencies, such as DB on purpose). That’s why I’m advocating for those commits to be removed entirely. Not reverted by a revert commit but erased from the history of that branch. This is exactly where rebasing comes in and wipes the floor with [INSERT_ANY_OTHER_SOLUTION].
Let’s say you have a situation described above. App breaking commit ended up in the main branch, and other commits were added afterward. Well, let’s remove that no-good piece of code.
git rebase -i HEAD~N
Using even the most basic text editor, you will get a pretty great overview of the commits in question and all the available actions.
VS Code – TBH, not really a “basic” text editor
Interactively rebasing, we are able to drop the specific commit from the branch. At this point, you are probably asking yourself, what about all the current branches that were created from the main before this action? Well, since all the commits above the one(s) removed are getting new SHA, this introduces a mismatch between the interactively rebased branch (main in this case) and all those other live branches. To get them on the same page, the easiest thing to do is:
git checkout [feature_branch]
git rebase main
git rebase -i HEAD~N
where N is the number of commits between the HEAD of the branch and the last shared commit (same SHA). All the dropped commits in main will be positioned between the HEAD and the last shared commit in your feature branch, so you can easily drop them here as well.
If you’re an adventurous person and you want to solve this case using merge to pull changes from main, it will cause conflicts that you’ve already resolved previously, if this is not your first merge, to reappear (depending on the commit’s SHA), so prepare for manual conflict resolution and a few raised eyebrows along the way.
And since I started with an image, let’s also end with one.
A very bad meme, but you get the point.