Working with Merge Conflicts

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.

Locating Conflicted Files

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!

Inspecting Conflicts

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.

Tip

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 diff with conflicts

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!

Tip

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.

git log with conflicts

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 Git Keeps Track of Conflicts

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!

Tip

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.

Finishing Up a Conflict Resolution

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.

Warning

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.

Aborting or Restarting a Merge

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.184.117