Bernd Haug, Senior Software Engineer
Integrating Your Work With git (part 2) – Rebase
Another option for integrating changes is a Rebase. In a rebase, we typically stay on the branch that we want to merge when we’re done implementing it and rebase it onto the branch into which we want to merge it. Let’s try it and see what happens:
As we can see, git comes back to us that it found a conflict when merging the first commit of the branch that we are rebasing.
Here, we see the most important difference between merge and rebase: While merge brings together two branch heads and all the changes they build on, rebase takes the branch we are rebasing, and applies it, commit for commit, to the branch we are rebasing onto.
If you think about it, it does exactly what it says on the label: Rebase takes a branch from its existing “base” (where its branch history first diverged from the rebase target) and puts it onto a new one.
Let’s see what
git status can tell us:
git’s message is fairly self-explanatory: We are in a rebase, there are conflicts that we need to resolve. git also tells us how we can proceed.
Let’s forge ahead. We run
git mergetool -y just as with merge; when we’re done we get the following
git status output:
We have fixed the conflicts that the first commit of the branch we are merging,
feature-1, had with the branch we are rebasing it onto,
master. We can now do
git rebase --continue and git will perform the commit of the new, deconflicted contents that we have created in our merge tool, and immediately continue on with applying the other commits in
feature-1. At this point, as in any other point during a rebase, we could also run
git rebase --abort to abandon the rebase (which would end up with
master looking exactly as they did when we started the process with
git rebase master).
We can see that the first change got applied without conflicts after we resolved them, but we get another conflict on the next commit, which we have to resolve in turn.
This is both a curse and a blessing of rebasing: When we rebase, we have to resolve potential conflicts for each change that we need to apply, meaning that we may have to resolve more conflicts (or at least, in more passes) than with a direct merge. The corresponding benefit is that we don’t have to merge a lot of stuff at once, but can take it one change at a time.
In the end, nothing determines how well rebase works for you as much as the quality of the branch histories that you’re integrating: The more each commit implements one whole intention, the mentally easier it will be to merge. If you don’t have feature branches that don’t touch half of your source tree, you’ll have fewer opportunities for conflicting changes. If there’s few back-and-forth experimental commits in your branch, there are also fewer risks of having to resolve conflicts in one location multiple times.
Much of this can be “fixed in post” when using git, in any case. We will look at the ways git can help you achieve a clean history for easier conflict resolution in the next section.
Before that, let’s take a look at a git history where multiple branches have been integrated through
git rebase. For that, we have to rebase all the feature branches and finally make the
master branch point to the newest commit in the linear history we produce. The easiest way to achieve this in our demo is to rebase each feature (or fix) branch onto the previous one that we have finished rebasing without touching master in the meantime, and only in the end
checking out master and merging the last feature branch that we have rebased:
What we get is a “Fast-forward” merge: git has determined that the
master branch into which we want to merge
feature-3 is a direct ancestor of
feature-3, so no further merging is necessary; the
master reference is just switched to point to the same commit that is the head of
feature-3. Finally, git prints a summary of the changes that happened between the old and new head commits of master.
What we have achieved with this is the following picture:
I would say it’s much easier to understand what happened when than in a history built using “normal” merge.