As demonstrated by the previous example, there are instances when conflicting changes can’t be merged automatically.
Let’s create another scenario with a merge conflict to explore the tools Git provides to help resolve disparities. Starting with a common hello with just the contents “hello,” let’s create two different branches with two different variants of the file:
$git init
Initialized empty Git repository in /tmp/conflict/.git/ $echo hello > hello
$git add hello
$git commit -m"Initial hello file"
Created initial commit b8725ac: Initial hello file 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hello $git checkout -b alt
Switched to a new branch "alt" $echo world >> hello
$echo 'Yay!' >> hello
$git commit -a -m"One world"
Created commit d03e77f: One world 1 files changed, 2 insertions(+), 0 deletions(-) $git checkout master
$echo worlds >> hello
$echo 'Yay!' >> hello
$git commit -a -m"All worlds"
Created commit eddcb7d: All worlds 1 files changed, 2 insertions(+), 0 deletions(-)
One branch says world
, while the other says
worlds
—a deliberate difference.
As in the earlier example, if you check out
master
and try to merge the alt
branch into it, a conflict arises:
$ git merge alt
Auto-merged hello
CONFLICT (content): Merge conflict in hello
Automatic merge failed; fix conflicts and then commit the result.
As expected, Git warns you about the conflict found in the hello file.
But what if Git’s helpful directions scrolled off the screen or if there were many files with conflicts? Luckily, Git keeps track of problematic files by marking each one in the index as conflicted, or unmerged.
You can also use either the git status command or the git ls-files -u command to show the set of files that remain unmerged in your working tree:
$git status
hello: needs merge # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # # unmerged: hello # no changes added to commit (use "git add" and/or "git commit -a") $git ls-files -u
100644 ce013625030ba8dba906f756967f9e9ca394464a 1 hello 100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2 hello 100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3 hello
You can use git diff to show what’s not yet merged, but it will show all of the gory details, too!
When a conflict appears, the working directory copy of each conflicted file is enhanced with three-way diff or merge markers. Continuing from where the example left off, the resulting conflicted file now looks like this:
$ cat hello
hello
<<<<<<< HEAD:hello
worlds
=======
world
>>>>>>> 6ab5ed10d942878015e38e4bab333daff614b46e:hello
Yay!
The merge markers delineate the two possible versions of the conflicting chunk of the file. In the first version, the chunk says “worlds”; in the other version, it says “world.” You could simply choose one phrase or the other, remove the conflict markers, and then run git add and git commit, but let’s explore some of the other features Git offers to help resolve conflicts.
The three-way merge marker lines
(<<<<<<<<
,
========
, and
>>>>>>>>
) are
automatically generated, but they’re just meant to be read by you,
not necessarily by a program. You should delete them with your text
editor once you resolve the conflict.
Git has a special, merge-specific variant of git diff to display the changes made against both parents simultaneously. In the example, it looks like this:
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,7 @@@
hello
++<<<<<<< HEAD:hello
+worlds
++=======
+ world
++>>>>>>> alt:hello
Yay!
What does it all mean? It’s the simple combination of two
diffs: one versus the first parent, called HEAD
,
and one against the second parent, or alt
. (Don’t
be surprised if the second parent is an absolute SHA1 name
representing some unnamed commit from some other repository!) To
make things easier, Git also gives the second parent the special
name MERGE_HEAD
.
You can compare both the HEAD
and
MERGE_HEAD
versions against the working directory
(merged) version:
$ git diff HEAD
diff --git a/hello b/hello
index e63164d..4e4bc4e 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD:hello
worlds
+=======
+world
+>>>>>>> alt:hello
Yay!
And then this:
$ git diff MERGE_HEAD
diff --git a/hello b/hello
index 562080a..4e4bc4e 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD:hello
+worlds
+=======
world
+>>>>>>> alt:hello
Yay!
In newer versions of Git, git diff --ours is a synonym for git diff HEAD, because it shows the differences between “our” version and the merged version. Similarly, git diff MERGE_HEAD can be written as git diff --theirs. You can use git diff --base to see the combined set of changes since the merge base, which would otherwise be rather awkwardly written as:
git diff $(git merge-base HEAD MERGE_HEAD)
If you line up the two diffs side by side, all the text except
the +
columns are the same, so Git
prints the main text only once and prints the +
columns next to each other.
The conflict found by git diff has two
columns of information prepended to each line of output. A plus sign
in a column indicates a line addition, a minus sign indicates a line
removal, and a blank indicates a line with no change. The first
column shows what’s changing versus your version, and the second
column shows what’s changing versus the other version. The conflict
marker lines are new in both versions, so they get a
++
. The world
and
worlds
lines are new only in one version or the
other, so they have just a single +
in the
corresponding column.
If you edit the file to pick a third option, like this:
$ cat hello
hello
worldly ones
Yay!
the new git diff output looks this:
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,3 @@@
hello
- worlds
-world
++worldly ones
Yay!
Alternatively, you could choose one or the other original version, like this:
$ cat hello
hello
world
Yay!
The git diff output would then be:
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
Wait! Something strange happened there. Where’s the diff line
about world
, showing that it was added to the
second version, and worlds
, showing that it was
removed in the first version? In fact, Git omitted it deliberately,
because it thinks you probably don’t care about that section
anymore.
Using git diff on a conflicted file only shows you the sections that really have a conflict. In a large file with numerous changes scattered throughout, most of those changes don’t have a conflict; either one side of the merge changed a particular section or the other side did. When you’re trying to resolve a conflict, you rarely care about those sections, so git diff trims out uninteresting sections using a simple heuristic: if a section has changes versus only one side, that section isn’t shown.
This optimization has a slightly confusing side effect: once you resolve something that used to be a conflict by simply picking one side or the other, it stops showing up. That’s because you modified the section so that it only changes one side or the other (i.e., the side that you didn’t choose), so to Git it looks just like a section that was never conflicted at all.
This is really more a side effect of the implementation than an intentional feature, but you might consider it useful anyway: git diff shows you only those sections of the file that are still conflicted, so you can use it to keep track of the conflicts you haven’t fixed yet.
While you’re in the process of resolving a conflict, you can use some special git log options to help you figure out exactly where the changes came from and why. Try this:
$ git log --merge --left-right -p
commit <eddcb7dfe63258ae4695eb38d2bc22e726791227
Author: Jon Loeliger <[email protected]>
Date: Wed Oct 22 21:29:08 2008 -0500
All worlds
diff --git a/hello b/hello
index ce01362..e63164d 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
hello
+worlds
+Yay!
commit >d03e77f7183cde5659bbaeef4cb51281a9ecfc79
Author: Jon Loeliger <[email protected]>
Date: Wed Oct 22 21:27:38 2008 -0500
One world
diff --git a/hello b/hello
index ce01362..562080a 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
hello
+world
+Yay!
This command shows all the commits in both parts of the
history that affect conflicted files in your merge, along with the
actual changes each commit introduced. If you wondered when, why,
how, and by whom the line worlds
came to be added
to the file, you can see exactly which set of changes introduced
it.
The options provided to git log are as follows:
--merge
shows only commits related to files that produced
a conflict.
--left-right
displays <
if the
commit was from the “left” side of the
merge (“our” version, the one you started with), or
>
if the commit was from the
“right” side of the merge (“their”
version, the one you’re merging in).
-p
shows the commit message and the patch associated
with each commit.
If your repository were more complicated and several files had conflicts, you could also provide the exact filename(s) you’re interested in as a command line option, like this:
$ git log --merge --left-right -p hello
The examples here have been kept small for demonstration purposes. Of course, real-life situations are likely to be significantly larger and more complex. One technique to mitigate the pain of large merges with nasty, extended conflicts is to use several small commits with well-defined effects contained to individual concepts. Git handles small commits well, so there is no need to wait until the last minute to commit large, widespread changes. Smaller commits and more frequent merge cycles reduce the pain of conflict resolution.
How exactly does Git keep track of all the information about a conflicted merge? There are several parts:
.git/MERGE_HEAD contains the SHA1 of
the commit you’re merging in. You don’t really have to use the
SHA1 yourself; Git knows to look in that file whenever you talk
about MERGE_HEAD
.
.git/MERGE_MSG contains the default merge message used when you git commit after resolving the conflicts.
The Git index contains three copies of each conflicted file: the merge base, “our” version, and “their” version. These three copies are assigned stage numbers 1, 2, and 3, respectively.
The conflicted version (merge markers and all) is not stored in the index. Instead, it is stored in a file in your working directory. When you run git diff without any parameters, the comparison is always between what’s in the index and what’s in your working directory.
To see how the index entries are stored, you can use the git ls-files plumbing command as follows:
$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 1 hello
100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2 hello
100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3 hello
The -s
option to git ls-files shows
all the files with all
stages. If you want to see only the conflicted files, use the
-u
option instead.
In other words, the hello file is stored three times, and each has a different hash corresponding to the three different versions. You can look at a specific variant by using git cat-file:
$ git cat-file -p e63164d951
hello
worlds
Yay!
You can also use some special syntax with git diff to compare different versions of the file. For example, if you want to see what changed between the merge base and the version you’re merging in, you can do this:
$ git diff :1:hello :3:hello
diff --git a/:1:hello b/:3:hello
index ce01362..562080a 100644
--- a/:1:hello
+++ b/:3:hello
@@ -1 +1,3 @@
hello
+world
+Yay!
Starting with Git version 1.6.1, the git
checkout command accepts the --ours
or
--theirs
option as shorthand for simply checking
out a file from one side or the other of a conflicted merge; your
choice resolves the conflict. These two options can only be used
during a conflict resolution.
Using the stage numbers to name a version is different from git diff --theirs, which shows the differences between “their” version and the resulting, merged (or still conflicted) version in your working directory. The merged version is not yet in the index, so it doesn’t even have a number.
Because you fully edited and resolved the working copy version in favor of “their” version, there should be no difference now:
$cat hello
hello world Yay! $git diff --theirs
* Unmerged path hello
All that remains is an “unmerged path” reminder to add it to the index.
Let’s make one last change to the hello file before declaring it merged:
$ cat hello
hello
everyone
Yay!
Now that the file is fully merged and resolved, git add reduces the index to just a single copy of the hello file again:
$git add hello
$git ls-files -s
100644 ebc56522386c504db37db907882c9dbd0d05a0f0 0 hello
That lone 0
between the SHA1
and the pathname tells you that the stage number for a nonconflicted
file is zero.
You must work through all the conflicted files as recorded in the index. You cannot commit as long as there is an unresolved conflict. Therefore, as you fix the conflicts in a file, run git add (or git rm, git update-index, and so on) on the file to clear its conflict status.
Be careful not to git add files with lingering conflict markers. Although that will clear the conflict in the index and allow you to commit, your file won’t be correct.
Finally, you can git commit the end result and use git show to see the merge commit:
$cat .git/MERGE_MSG
Merge branch 'alt' Conflicts: hello $git commit
$git show
commit a274b3003fc705ad22445308bdfb172ff583f8ad Merge: eddcb7d... d03e77f... Author: Jon Loeliger <@example.com> Date: Wed Oct 22 23:04:18 2008 -0500 Merge branch 'alt' Conflicts: hello diff --cc hello index e63164d,562080a..ebc5652 --- a/hello +++ b/hello @@@ -1,3 -1,3 +1,3 @@@ hello - worlds -world ++everyone Yay!
You should notice three interesting things when you look at a merge commit:
There is a new, second line in the header that says
Merge:
. Normally there’s no need to show the
parent of a commit in git log or git show, since there is only one parent and it’s typically
the one that comes right after it in the log. But merge commits
typically have two (and sometimes more) parents, and those parents
are important to understanding the merge. Hence, git
log and git show always print the
SHA1 of each ancestor.
The automatically generated commit log message helpfully notes the list of files that are conflicted. This can be useful later if it turns out a particular problem was caused by your merge. Usually, problems caused by a merge are caused by the files that had to be merged by hand.
The diff of a merge commit is not a normal diff. It is always in the combined diff, or “conflicted merge,” format. A successful merge in Git is considered to be no change at all; it is simply the combination of other changes that already appeared in the history. Thus, showing the contents of a merge commit shows only the parts that are different from one of the merged branches, not the entire set of changes.
If you start a merge operation but then decide for some reason that you don’t want to complete it, Git provides an easy way to abort the operation. Prior to executing the final git commit on the merge commit, use:
$ git reset --hard HEAD
This command restores both your working directory and the index to the state immediately prior to the git merge command.
If you want to abort or discard the merge after it has finished (that is, after it’s introduced a new merge commit), use the command:
$ git reset --hard ORIG_HEAD
Prior to beginning the merge operation, Git saves your original
branch HEAD
in the ORIG_HEAD
ref
for just this sort of purpose.
You should be very careful here, though. If you did not start the merge with a clean working directory and index, you could get in trouble and lose any uncommitted changes you have in your directory.
You can initiate a git merge request with a
dirty working directory, but if you execute git reset
--hard, your dirty state prior to the merge is not fully
restored. Instead, the reset loses your dirty state in the working
directory area. In other words, you requested a
--hard
reset to the HEAD
state!
Starting with Git version 1.6.1, you have another choice. If you have botched a conflict resolution and want to return to the original conflict state before trying to resolve it again, you can use the command git checkout -m.
3.145.184.117