Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git merge conflict saying deleted, but it's not

I'm trying to resolve a conflict in a git merge (git version 2.9) - the conflict is suggesting the file in the 'other' branch is deleted, but it's not. Here's a (fairly) short reproducible recipe:

cd /some/new/folder/somewhere
git init

echo 'a on master' > a
git add a
git commit -m 'add a on master'

mkdir f
git mv a f/
git commit -m 'move a to f on master'

git checkout -b hotfix HEAD^

echo 'new content a on hotfix' > a
mkdir f
git add a
git mv a f/
git commit -m 'move a to f on hotfix with different content'

git checkout master
git merge hotfix

Git is now suggesting that f/a was deleted in the hotfix branch, but that's clearly not the case?

CONFLICT (rename/delete): f/a deleted in hotfix and renamed in HEAD. Version HEAD of f/a left in tree.
Automatic merge failed; fix conflicts and then commit the result.

I would be expecting a 'normal' conflict for f/a like this:

<<<<<<< HEAD
a on master
=======
new content a on hotfix
>>>>>>> hotfix

I can't understand why git is suggesting the conflict as rename/delete conflict?

Thanks for any help.

like image 552
Steve Folly Avatar asked Nov 18 '16 06:11

Steve Folly


2 Answers

I can't understand why git is suggesting the conflict as rename/delete conflict?

Git has detected the rename. More precisely, it detected one, but not the other.

I used your example script and got the same result you did:

CONFLICT (rename/delete): f/a deleted in hotfix and renamed in HEAD.
 Version HEAD of f/a left in tree.
Automatic merge failed; fix conflicts and then commit the result.

There are two keys here: how git merge performs merges, and how git diff works. Let's start with the first one first.

How merge (as a verb) works

The goal of a merge is to combine two sets of changes on two different lines-of-development (typically made by two different people, but in our case, made by one person "wearing two different hats", one at a time, as it were). These changes must necessarily start from some common starting-point, which Git calls the merge base.

To perform this merge, then, Git must find the merge base. For regular merges like this, the merge base is the commit that is shared between HEAD (the current commit) and the target commit. In my particular case the target, named hotfix, resolves to commit hash b45a155...:

$ git rev-parse hotfix
b45a15547101d836d84dbdf4758d71dc91c93353

while HEAD is 2ca7d2d...:

$ git rev-parse HEAD
2ca7d2d15d4d537edb828a7f3bfff3a2182630ec

The merge base of these two commits is the initial commit d763d32 add a on master:

$ git merge-base --all HEAD hotfix
d763d32af0cafdb0378b96b25e56fd70d63213d1
$ git log --graph --decorate --oneline --all
* b45a155 (hotfix) move a to f on hotfix with different content
| * 2ca7d2d (HEAD -> master) move a to f on master
|/  
* d763d32 add a on master

We don't actually need all these hashes, but it's nice to see them in concrete form sometimes. The point is, we have two different sets of changes: "what we did" going from d763d32 to 2ca7d2d, and "what they did" going from d763d32 to b45a155.

We can find the first of these things by running git diff:

$ git diff d763d32 2ca7d2d    # using raw IDs
$ git diff hotfix...master    # using the special "..." syntax

That's "what we did". I'll show it in a moment.

Next, we (or Git) can find the second of these things by running git diff again:

$ git diff d763d32 b45a155    # using raw IDs
$ git diff master...hotfix    # using the syntax again

How git diff performs diffs

This explanation gets long and convoluted in fine detail when it matters, and eventually it really matters quite a lot. Let's outsource it to another StackOverflow answer. In summary, though, Git will try to detect renames. Whether it can detect them, depends on many details.

In our particular case, though, what is happening is that Git is detecting the rename when going from merge-base to tip-of-master:

$ git diff hotfix...master
diff --git a/a b/f/a
similarity index 100%
rename from a
rename to f/a

The three-dot syntax here tells git diff to find the merge base of the two specified commits (tip of hotfix, and tip of master), then diff that merge base against the second commit, i.e., the tip of master. It detected the rename: the original file a was 100% identical to the new file f/a. Hence, we have a rename.

The second diff, though ... ah, there is a problem:

$ git diff master...hotfix
diff --git a/a b/a
deleted file mode 100644
index 81d07e3..0000000
--- a/a
+++ /dev/null
@@ -1 +0,0 @@
-a on master
diff --git a/f/a b/f/a
new file mode 100644
index 0000000..158795c
--- /dev/null
+++ b/f/a
@@ -0,0 +1 @@
+new content a on hotfix

The old content of a in the merge base is far too different from the new content of f/a in the tip commit. Git not only did not find the rename, it is never going to find the rename: the file is far too different. It does not resemble the original in the slightest.

Rename detection usually works better than this

In practice, when files are renamed, they tend to retain much or even all of their original content, just as happened in your merge-base-to-master-tip change. If they do not retain "enough", however, Git won't detect the rename.

Fortunately, Git has been around for a long time since I wrote this answer back in January 2012. It still applies today—you can use -X rename-threshold=number to tweak the rename detection threshold level during a merge—and almost everyone has a Git newer than 1.7.4. However, its drawbacks still apply. You may also want to read this other answer that I wrote in April 2016.

If you have many files and need automated rename detection, you may need to be fancy. If you have only one file, you can simply merge the file manually, doing your own "rename detection", using git merge-file to produce the merge result. Just extract the three revisions (base, HEAD, and other) into temporary files and use git merge-file to merge those three to produce your desired result. Replace Git's somewhat lame version with the correct one, and git add it and you are good to go.

like image 51
torek Avatar answered Nov 11 '22 07:11

torek


Nicely formulated question.

There is one shortcoming in your example :

file a consists of one single line, so editing this line means "100% difference between two files".
git renaming detection algorithm will never detect two 100% different files as a renaming.

If you change your example :

  • writing 4 lines in your initial file a
  • editing only the first one when modifying its content on hotfix

the merge does not trigger any conflict, and results in a f/a which includes the hotfix modification.


Hopefully, this means that in your real scenario, the number of cases where git says created vs deleted are limited.

If it is feasable to handle them manually, here would be a way to see your usual 3-way merge :

 # find the merge-base :
 $ git merge-base master hotfix
 3e12717099f3dc7b83b3d267e5cbd580ec8e69a1

 # get the content you expect for "ours", "base" and "theirs" :
 $ git show master:f/a > f/a.ours
 $ git show hotfix:f/a > f/a.theirs

 # here is the manual part : I don't know how to have an automatic detection
 # of the correct base file :
 $ git show 3e12717:a  > f/a.base

 # start a 3-way merge :
 $ meld f/a.ours f/a.base f/a.theirs

 # if you are satisfied with the content in a.base,
 # and want to use it as your new a :
 $ mv f/a.base f/a
 $ git add f/a
like image 40
LeGEC Avatar answered Nov 11 '22 09:11

LeGEC