September 13, 2010
In my day job, we often deal with a multitude of git branches - whether we’re keeping branches for maintenance releases, supporting deprecated APIs for certain customers, or working with experimental features. Although git’s model entices you to create more and more branches, it brings the burden of keeping them up to date through periodic merging of branches.
While merges are important for keeping code up to date, errors in merge commits are more common and more impactful than in normal commits. Firstly, merges have multiple parents, which makes it very hard to see, from history, what the programmer actually did to resolve merge conflicts. Secondly, reverting a bad merge can turn into a headache in itself. Thirdly, a large proportion of merge conflicts happen when dealing with someone else's code because by nature, branching is a multiplayer game. In essense, merge errors are easy to make, hard to fix, and hard to find, so it really does pay to improve the merge process.
Yet, I have found that the tools and interfaces available for performing merges
do not equip programmers sufficiently to do them effectively. Often, a
programmer will simply run git merge
and hope that git will take care of the
large majority of the hunks. Of the hunks that conflict, the usual merge
strategy of the human at the helm is to use the surrounding context to roughly
guess what the intended program is supposed to look like.
This article will hopefully demonstrate that there can be a much more measured process to the resolution of merge conflicts which takes the guesswork out of this risky operation.
Let’s say your team has been assigned the task of writing a repository of poems
(what a nightmare!), and your nightmare has just been compounded with the task
of merging the recent fixes from the master
branch into the beta
branch. So
you switch to the beta
branch and run:
git merge master
Auto-merging roses.txt
CONFLICT (content): Merge conflict in roses.txt
Automatic merge failed; fix conflicts and then commit the result.
A merge conflict. So you have a look at the file in question to see what the conflict is:
cat roses.txt
<<<<<<< HEAD
roses are #ff0000
violets are #0000ff
all my base
are belong to you
=======
Roses are red,
Violets are blue,
All of my base
Are belong to you.
>>>>>>> master
(Listing 1)
Great! The whole file conflicts. Which is the correct one to take? They both look equally valid. The top one is written in hacker style with HTML-style colour codes and the hardcore, minimalist aura of pure lowercase letters. The bottom one has more correct punctuation and capitalisation, but also seems a bit boring, comparatively.
If this was your project, you could just pick one and be done with it. But the problem is, this isn’t your poem, you’ve never seen the poem before, you weren’t responsible for writing or editing it, and basically, you don’t want to risk destroying someone’s hard work. You have only been assigned the task of merging these branches. So what do you do?
The trick is that Listing 1 doesn't give you all the information you need to complete the merge correctly. In fact, there are four important pieces of information involved in a merge, three of which are necessary to resolve the merge. In this case, git has only given you two of the required pieces. The following diagram illustrates the four states:
States B and C correspond to the heads of the master
and beta
branches
respectively, and these are the fragments that git has shown in the failed merge
above. D is the state you want to generate (in most cases, git will compute D
automatically). State A at the top represents the merge base of the master
and
beta
branches. The merge base is the last common ancestor of the two branches,
and for now, we will assume it is unique. As we will see later, this state plays
a crucial role in resolving the merges. In the diagram, I have also marked out
the deltas 1 and 2, which represent the change between state A and B, and A and
C respectively. Given states A, B and C, deltas 1 and 2 can be derived. Note
that deltas 1 and 2 may represent more than 1 commit. But for our purposes, we
can just treat them as monolithic deltas.
To understand how to get to D, you need to understand what the merge operation
is trying to achieve. D should represent the combination of changes made in the
master
branch and the changes made in the beta
branch. That is, it is the
combination of deltas 1 and 2. The idea is simple - and most of the time, it's
so simple that the VCS can combine the deltas with no human intervention. It is
only when the deltas affect proximal parts of the file that it will ask for
human assistance. Your job as the human assistant is to continue the job of the
machine, by determining what the intents of deltas 1 and 2 are and implementing
both intents to produce D, the merged result.
To find the changes that went into each branch, we need to know what the merge
base (state A) looks like. The most basic mechanism by which git allows us to do
so is to set the merge.conflictstyle
option to diff3
by executing the
command:
git config merge.conflictstyle diff3
After setting this, retry the merge (git reset --hard; git merge master
) and
examine the conflicting file again:
cat roses.txt
<<<<<<< HEAD
roses are #ff0000
violets are #0000ff
all my base
are belong to you
|||||||
roses are red
violets are blue
all my base
are belong to you
=======
Roses are red,
Violets are blue,
All of my base
Are belong to you.
>>>>>>> master
There is now a third fragment shown in the middle, which corresponds to the
merge base. At this point, we can do an eye-diff to find that in HEAD
(the
beta
branch) the colour names were changed to the nerdy HTML codes, while the
master
branch introduced capitalisation and punctuation. Based on this
knowledge, we now know that the result should include captalisation, punctuation
and HTML-style colours.
It is at this point that this article could end, having reached a solution. However, there is a better way.
While staring at plain-text merge output can get the job done in the simple cases, there are times when the conflicts are more drastic and some more sophisticated, graphical tooling can help. My diff and merge tool of choice is a simple Python program called meld, but anything that visualises merges with three panes will do. To use it for merge resolution, have it installed and, while in an unresolved-merge in git, type:
git mergetool
It will ask you to choose the merge program, so type meld
and hit Enter. Here
is what the window might look like (assuming merge.conflictstyle
has not been
set):
While it is now presented in side-by-side view, this doesn't actually present
any more helpful information than Listing 1. That is because, as before, we are
not given the state of the file from the merge base. We are presented with
roses.txt.LOCAL.2760.txt
on the left and roses.txt.REMOTE.2760.txt
on the
right, and the file in the middle is a failed merge. We are given states B, C
and an attempt at D, but state A is missing... Or is it? If you fire up a new
terminal (without exiting meld) and look in the directory, you'll find some
extra files:
ls -1 roses.txt roses.txt.BACKUP.2760.txt roses.txt.BASE.2760.txt
roses.txt.LOCAL.2760.txt roses.txt.REMOTE.2760.txt
The interesting file here is roses.txt.BASE.2760.txt
. It is the merge base
that we are looking for! The trick now is to find the changes introduced by the
master
and beta
branches, in relation to the BASE
. We can do that with two
separate invocations of meld:
meld roses.txt.LOCAL.2760.txt roses.txt.BASE.2760 &
meld roses.txt.BASE.2760 roses.txt.REMOTE.2760.txt &
(Some might find it makes more sense to swap the order of the arguments in the first command so that the original file is on the left in both cases, but I like to keep the order the same as in the three-paned window.) The two resultant windows are:
Reading the first window from right to left and the second window from left to
right, it is now clear as day what changes occurred in each branch. Because meld
has even presented a diff within each line, it's now much easier to spot the
differences and with this view, it's likely that you'll find changes that you
would not have noticed had you just stared at the text files (eg. did you notice
before the addition of the word "of" in the REMOTE
branch?).
Armed with this knowledge, we can now go back to the three-paned view and make
the changes. My edit strategy for performing the manual merge is to take the
text from the branch that contained the more extensive changes (in this case,
the master/REMOTE
version), and then insert the edits made in the other
branch. Here is the result for this little example:
Hopefully, you'll find the three-window merge conflict resolution strategy
demonstrated above to be as useful as I have. But you might, like me, also find
it inconvenient to have to manually open up new instances of meld to see the two
diff views. The solution to this is to configure git to open up all three
windows when the mergetool is requested. To do this, paste the following into
the an executable script in the system PATH
(eg. $HOME/bin/gitmerge
):
#!/bin/sh
meld $2 $1 &
sleep 0.5
meld $1 $3 &
sleep 0.5
meld $2 $4 $3
And add the following to your ~/.gitconfig
file:
[merge]
tool = mymeld
[mergetool "mymeld"]
cmd = $HOME/bin/gitmerge $BASE $LOCAL $REMOTE $MERGED
Now, the next time you type git mergetool, for each file to be resolved, you can
choose the mymeld option to have all three windows opened - one for the diff
from BASE
to LOCAL
, one for the diff from BASE
to REMOTE
and one for the
three-pane view.
After you get used to using these three views to do the merge conflict resolution, you'll find that the process becomes very methodical and mechanical. In most cases, there is no longer the need to read and understand the chunks of code from each branch to understand which elements from which are correct. You'll find that you're employing much less guesswork and you'll be much more confident that what you're commiting is correct. And because of this confidence, you might even find that merging becomes fun, not dreaded.