Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the reasons and cases that cause git merge conflicts?

Tags:

git

  1. What are

    • the necessary and sufficient conditions, and/or

    • all the cases or some common cases

    that can cause git merge report merge conflict?

    How does git merge determine whether a line or some lines contain merge conflict(s)?

  2. For example, I sometimes see cases like the following, where either Part 1 or Part 2 is empty in

    <<<<<<< HEAD
    (Part 1)
    =======
    (Part 2)
    >>>>>>> some-other-branch
    

    It looks unlikely to have a merge conflict to me. So what are some possible reasons that cases like that have merge conflicts?

  3. Comparing the merge conflicts reported by git merge and the differences reported by git diff, is it correct that

    • the differences reported by git diff might not necessarily be in the places of the merge conflicts reported by git merge, and

    • the merge conflicts reported by git merge might not necessarily be in the places of the differences reported by git diff?

Thanks.

like image 817
Tim Avatar asked Jun 04 '17 22:06

Tim


People also ask

How do I resolve merge conflicts in Git?

You can only resolve merge conflicts on GitHub that are caused by competing line changes, such as when people make different changes to the same line of the same file on different branches in your Git repository. For all other types of merge conflicts, you must resolve the conflict locally on the command line.


1 Answers

There are multiple parts to a complete, correct answer. First, we have to get to a normal three way merge in the first place (which in Git requires using -s recursive or -s resolve, and if using -s recursive, finding a single merge base and two other commits). You may, however, want to skip right down to the third section.

The elements required for a normal three-way merge

To do any merge at all, you need:

  • a merge base commit B,1
  • a current commit L (left side, aka HEAD) that has B as an ancestor,
  • another commit R that also has B as an ancestor.

Since "is ancestor" allows node equality in the commit graph (technically it's a predecessor-or-equal ≼ comparison), it's possible to have B = L and/or B = R. If this is the case, however, then no merging is ever required, and if you do force a merge (using git merge --no-ff—this implies B = L and L ≺ R; the two unforced cases are "fast forward" not-actually-a-merge and a "nothing to merge" error) there will be no merge conflicts. So we may as well assume that the merge base precedes both sides of the merge.


1Using --allow-unrelated-histories, you can have Git substitute in the empty tree for an actual base commit, if L and R have no lowest common ancestor nodes. This, however, causes all identified files to be add/add conflicts.


Given a merge, the elements required for a conflict

Next, to get a conflict in some file path, you need either what I call a "high level" conflict or a "low level" conflict (or both). For this to occur, Git must identify (i.e., match together) files in B, L, and R. Due to the ability to add new files, rename files, and delete files, this does not require that the file have the same name in all three commits. In particular:

  • If path P exists in all three of B, L, and R, the three files with path P are identified together. (That is, there's a path PB, such as path/to/foo.txt, that has a matching PL path/to/foo.txt and PR path/to/foo.txt. Obviously this file is "the same file" all throughout the merge, so Git identifies the three paths as one file.)
  • Or, there may be up to three different paths, PB path/to/basename, PL path2/to2/left, and PR path3/to3/right. One or more of these may not even exist. These result in: add/add conflict, if ∄ PB and PL = PR; a rename/delete conflict, if PB equals one of the left or right paths, but the other does not exist; or a rename/rename conflict, if PB ≠ PL ≠ PR.

If there is not already a high level conflict (add/add, rename/rename, or rename/delete), there may still be a rename/modify or rename/delete conflict, as long as the hash IDs of the blobs (file contents) named by the two or three path names do not match.

All of these "high level" conflicts cause a merge conflict, but do not by themselves result in any conflict markers. To get that, we must now actually merge the base file with the two side files (which means all three files must exist, or for the add/add case, we take a trivial empty file as the base version).

This merge may or may not have its own conflicts. If the merge adds low level conflicts, we will get conflict markers. If there is no high level conflict (the path is the same in all three commits) but a full merge is required (the hashes all differ), we may get a low level conflict with conflict markers.

What Git requires for a conflict within a file

To get a merge conflict within one file in the work-tree, Git must see the same line changed by both left and right side versions, but changed in different ways. (Remember, all three hashes must differ, so that Git is combining a set of changes from, in effect, diff PB PL with those from diff PB PR.)

The most obvious and least confusing cases

The most obvious case occurs for one side (let's choose the left side first) to say:

 unchanged context
-changed line
+replacement 1
 more unchanged context

and the other to say:

 unchanged context
-changed line
+replacement 2
 more unchanged context

Here one or more lines are deleted and in their stead, one or more lines are inserted, but the inserted lines do not match. In this case the merge conflict style presents this as:

unchanged context
<<<<<<< left-label
replacement 1
=======
replacement 2
>>>>>>> right-label
more unchanged context

The diff3 context style presents this instead as:

unchanged context
<<<<<<< left-label
replacement 1
||||||| merged common ancestors
changed line
=======
replacement 2
>>>>>>> right-label
more unchanged context

If we merely add text, but add different text, at the same line, we also get a merge conflict. Again, the added text from each side shows up in the conflict markers. (If we add the same text to both sides—either as new text, or as replacement text for a changed line, Git takes one copy of this addition and there is no conflict.)

The somewhat confusing cases

If one but not both of the replacement lines is empty—i.e., if the left or right side diff reads:

 unchanged context
-changed line
 more unchanged context

then one but not both of the replacement lines in the merge or diff3 style marked-up file is missing. (If both diffs simply delete the original lines, there is no conflict: Git takes one deletion.)

Similarly, if one side adds a line above or below a line that the other side deletes, you get a conflict. This time the conflict shows that the side that retained-and-then-added a has all the lines, and other other side has no lines. For instance:

 some merge conflict.
 Line that will conflict.
+add line below it
 Rest of the

vs:

 some merge conflict.
-Line that will conflict.
 Rest of the

(and the same occurs if you add the line above instead of below).

This is where the diff3 conflict style is very helpful. Here's the entire merged file for the one of these cases:

We need a base file
in which to make
some merge conflict.
<<<<<<< HEAD
||||||| merged common ancestors
Line that will conflict.
=======
Change the line that will conflict.
>>>>>>> b2
Rest of the
base file for the
merge conflict example.

Note that it's now obvious—or at least, less mysterious—that there was a line that read Line that will conflict. in the base version, which I deleted entirely from the left side HEAD version and replaced with a different line in the right side b2 version.

like image 120
torek Avatar answered Sep 22 '22 05:09

torek