Git Merge vs. Rebase
As a programmer one encounters varying levels of git-prowess. 95% of all use-cases are covered by commit, checkout and merge, and when something goes wrong the occasional arcane shell invocation from Stack Overflow does the rest.
Investing the time to understand the tools used on a daily basis is a worthwhile investment. Analogous to Clean Code, keeping a sane git graph makes things faster, easier to parse visually and let’s you see who did what exactly, why and when much more precise.
That is why I much prefer the Rebase Flow to the Merge Flow. In my opinion unnecessary merges clutter the graph and needlessly mix up unrelated changes.
Sample Scenario
[Source]
Suppose we have the following graph: We branched out from main at commit b
, and then did work on c
and d
, while on the main something else was fixed, say a colleague found a bug in commit b
that was fixed in commit e
that is now conflicting with our changes in c
.
Rebase flow
When we now rebase feature on top of main we get something that looks like this:
[Source]
We essentially history rewrote the feature branch by changing the branching out point of the feature branch from b
to f
and reapplying the changes of c
and d
.
Since our new branching point is f
we automatically get the bugfix from commit e
, but have to fix the conflicts when applying commit c
, making it c'
. And because commit d
was based on c
(d
is a set of changes to be applied to c
to reach the desired state at d
) it now becomes d'
.
History Rewriting! This should only be done on clean branches that aren’t shared between lots of devs, high traffic branches, or semantic branches (like release)!
Once the rebase is done, we have two options Fast-Forward Merge or Non Fast-Forward Merge. Different people and teams handle this differently. I use both depending on the shape of the feature branch. If it is only a single commit with few changed lines, I go for the fast-forward variant, otherwise non-fast-forward is always a good option to keep the mainline clean.
Fast-Forward Merge
[Source]
Since we rewrote our feature branch to contain the changes of c
and d
but with the conflicts resolved (c'
and d'
) it will cleanly apply on top of f
using a fast-forward merge.
Non Fast-Forward Merge
[Source]
Same goes for the non-ff-merge, it will cleanly non-fast-forward merge, but it will now have its own merge commit that allows us to document the feature and some clients, like my beloved Sublime Merge do support collapsing of merge commits (and showing the combined diff of the entire branch when selected), which in the best case makes the mainline a series of standalone feature merge commits.
Merge Flow
The merge flow also allows us to integrate the bugfix of commit e
into our feature branch. But remember the changes in e
and d
are conflicting. That means we do have to solve the conflicts all the same.
[Source]
Then when it comes to merging the thing back, depending on the complexity of our conflict resolution of c
and e
, we may now have to solve new conflicts that arise from e
already being on the mainline and us trying to merge a modified version of e
(e
plus conflict resolution changes) back into main. These conflicts are easily solved most of the time, mostly preferring using the fixed variant from the feature branch. But not always, code is messy.
Oh, and now that we have merged main into the feature branch, this effectively blocks the feature branch from being rebased. It technically is possible, but really painful and hard to get right.
[Source]
After the merge back to main, it looks like this. We now have persisted the conflict and its resolution in full to the mainline.
Observations
Comparing the difference in simplicity between the final graphs of the two methods, one rewrote the history to a cleaner state, only keeping what will be of interest in the future (namely that the bugfix happened in e
and that the feature branch contained the commits c'
and d'
, with proper author attribution and commit messages), while the other was more work and left the graph in a messier state.