简体   繁体   中英

Merge conflict in git parallel branches

I'm trying to achieve a scenario in git. I started with a text file with four lines and then made 4 branches each changes one line in the text file and they work in parallel with each one having a copy of the original file as in the picture. 在此处输入图片说明 When I merge the branches, the first merge always succeeds like this:

Updating 0b18c93..274ba8c
Fast-forward
 t.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

but I get merge conflicts in the later merges.

Automatic merge failed; fix conflicts and then commit the result.

Is there a method to achieve this scenario without getting merge conflicts?

To add to jthill's answer (which is correct and I've upvoted it): You need to realize that your first merge isn't a merge at all. That's why Git says:

 Fast-forward 

The merge process, as jthill said:

finds (and in some cases Git's merge will actually construct) the common base and compares diffs from that base to each branch tip. Any differences in overlapping ranges constitutes a conflict.

But for the first git merge command, the common base is one of the branch tips.

The first one is free

Let's see how that is:

$ mkdir tt; cd tt; git init
Initialized empty Git repository in ...
$ cat << END > myfile.txt
01
02
03
04
END
$ git add myfile.txt
$ git commit -m initial
[master (root-commit) b1a22ca] initial
 1 file changed, 4 insertions(+)
 create mode 100644 myfile.txt
$ git checkout -b br1
Switched to a new branch 'br1'
$ ed myfile.txt
12
1s/$/ one/
w
16
q
$ git add myfile.txt
$ git commit -m 'change line 1'
[br1 b31f04a] change line 1
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --all --decorate --oneline --graph
* b31f04a (HEAD -> br1) change line 1
* b1a22ca (master) initial

(If you're not familiar with the ed editor, it is a rather minimal text-only editor that prints the number of bytes in the input file when started up, and has commands of the form: line-number(s) operation . So 1s/$/ one/ means "replace the end of line 1 with one . The w command writes the file back, and q quits the editor.)

We do not have to create the extra branches br2 , br3 , and br4 yet, but let's go ahead and do that anyway, creating them so that they point to commit b1a22ca :

$ git checkout master
Switched to branch 'master'
$ git branch br2 && git branch br3 && git branch br4
$ git log --all --decorate --oneline --graph
* b31f04a (br1) change line 1
* b1a22ca (HEAD -> master, br4, br3, br2) initial

At this point, we have a graph that looks like this, if we draw it horizontally instead of vertically the way Git does:

b1a22ca   <-- master (HEAD), br2, br3, br4
    \
   b31f04a   <-- br1

That is, the branch name br1 indicates that its tip commit is b31f04a , while the branch names master and the other three br s all indicate that their tip commit is b1a22ca .

Let's go ahead and create a new commit on br2 now:

$ git checkout br2
Switched to branch 'br2'
$ ed myfile.txt
12
1,$p
01
02
03
04
2s/$/ two/
w
16
q
$ git add myfile.txt 
$ git commit -m 'change line 2'
[br2 805ea58] change line 2
 1 file changed, 1 insertion(+), 1 deletion(-)

and see how git log --all --decorate --oneline --graph draws it:

$ git log --all --decorate --oneline --graph
* 805ea58 (HEAD -> br2) change line 2
| * b31f04a (br1) change line 1
|/  
* b1a22ca (master, br4, br3) initial

In my preferred horizontal graph drawing—where newer commits are to the right, rather than above—we have:

   805ea58   <-- br2 (HEAD)
    /
b1a22ca   <-- master, br3, br4
    \
   b31f04a   <-- br1

If we now run git checkout master && git merge br1 , Git will locate, for us, the merge base commit. That's the "nearest" commit that's on both branches br1 and master .

Note that at this point, commit b31f04a is only on br1 , and commit 805ea58 is only on br2 , but commit b1a22ca is on every branch. This is a key thing in Git: branches "contain" commits, and any given commit can be on many branches simultaneously.

Having found the merge base, Git now must combine two sets of changes:

  • the changes from the merge base b1a22ca to the tip of master : b1a22ca ;
  • the changes from the merge base b1a22ca to the tip of br1 : b31f04a .

But b1a22ca is b1a22ca . There cannot possibly be any changes here!

What Git does by default for this case is to do a fast forward . A fast forward is not a merge at all! It just amounts to switching to the other commit, in this case b31f04a , and dragging the name master forward to point to that commit:

   805ea58   <-- br2
    /
b1a22ca   <-- br3, br4
    \
   b31f04a   <-- master (HEAD), br1

No new commits get added, during this operation: the only thing that changes is that your current commit is now b31f04a instead of b1a22ca , and that the branch label master points to b31f04a . (The word HEAD moved along with master because HEAD is "attached to" master . Git shows this as HEAD -> master in the git log output.)

You can, if you like, run git merge --no-ff br1 instead. This forces Git to make a real merge. However, there are still no changes to find when Git compares b1a22ca to itself, so there are no merge conflicts. If you do this, you get a new commit, with two parents. You didn't, so I'll just run with the fast-forward:

$ git checkout master
Switched to branch 'master'
$ git merge br1
Updating b1a22ca..b31f04a
Fast-forward
 myfile.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --all --decorate --oneline --graph
* 805ea58 (br2) change line 2
| * b31f04a (HEAD -> master, br1) change line 1
|/  
* b1a22ca (br4, br3) initial

The second merge is conflicted

When we get to the second merge, though, things are different indeed. This time, the first commit that is shared between master (now b31f04a ) and br2 ( 805ea58 ) is b1a22ca . So Git will run:

$ git diff --find-renames b1a22ca b31f04a   # what happened on master
diff --git a/myfile.txt b/myfile.txt
index cb3ff6d..8bc2250 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1,4 +1,4 @@
-01
+01 one
 02
 03
 04

Then Git will run:

$ git diff --find-renames b1a22ca 805ea58   # what happened on br2
diff --git a/myfile.txt b/myfile.txt
index cb3ff6d..8bfe146 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1,4 +1,4 @@
 01
-02
+02 two
 03
 04

Git now goes to combine these two changes. But in Git's eyes, the two changes overlap: the first one touches line 1, and the second touches line 2, and lines 1 and 2 touch each other: that's an overlap. The two changes are not identical , so they cannot be collapsed down to a single change that changes both lines. The change is therefore a merge conflict , which is what you see.

The same thing happens for the remaining merges, once branches br3 and br4 have appropriate tip commits.

Based on the questions you're asking in comments, here is some additional explanation.

First you have a starting state with just numbers on each line; we'll call that commit A . That is where master points.

A <--(master)
            ^HEAD

Then you have four branches. Each branch changes a different line of the code. We'll keep HEAD at master in preparation for the merges.

A <-(master)
|\         ^HEAD
| B <-(branch1)
|\
| C <-(branch2)
|\
| D <-(branch3)
 \
  E <-(branch4)

So you do your first merge

git merge branch1

Now git is going to look for a merge base between "what you're merging" ( branch1 = B ) and the HEAD ( master = A ). The merge base is typically (and in this case) just the most recent commit reachable from both sides (ie from master and branch1 ). Of course A is reachable from master , and A is also reachable from branch1 because A is B 's parent.

So we've identified 3 versions. "ours" (what we're merging changes into) is A . "theirs" (where changes are being merged from) is B . "base" (the merge base between "ours" and "theirs") is A , which happens to be the same as "ours".

If we did a full merge, the next step would be to compare "ours" to "base" and also compare "theirs" to "base" to create two patches. Then we combine those patches. As long as the patches don't affect the same hunks of code, we can combine them trivially ("we changed hunk A from xxx to yyy; they changed hunk B from zzz to www; so the merged patch does both of those things"). That's roughly how a non-conflicting merge works. And in this case it's very simple; because "ours" and "base" are the same thing, "we changed nothing"; so the merged patch is equal to the patch between "base" and "theirs".

And in fact, by default git takes a shortcut here. As soon as it realizes that "ours" is an ancestor of "theirs", it knows it can do a "fast forward" instead of a true merge. It can just move master to where branch1 is and not merge anything at all. (You can prevent this by giving the --no-ff option to the merge command.)

But the point is, even without a fast-forward, this will not conflict because a conflict by definition means "we" changed the same hunk of code "they" changed , and in this case "we" didn't change anything.

So now after the fast-forward you have

A -- B <-(branch1)(master)
|\                       ^HEAD
| C <-(branch2)
|\
| D <-(branch3)
 \
  E <-(branch4)

Notice that master is no longer an ancestor of C , D , or E ; the first merge moved master to a commit unreachable from those other branches. This would be true of a true merge as well; in that case master would've moved to a new "merge commit". Because of the fast-forward, it just moved to B , but the effect is the same.

So now you say

git merge branch2

When we look for a merge base between "ours" ( master = B ) and "theirs" ( branch2 = C ) we'll find A . So again base is A , but this time "our changes" are "line 1 changed from '01' to '01 one'". "Their changes" are "line 2 changed from '02' to '02 two'".

Since the conditions for a fast-forward are not met, we must do the full merge using those two patches. And you might think "I can combine those patches without conflict" resulting in "line 1 changed from '01' to '01 one' and line 2 changed frmo '02' to '02 two'". If patches were compared strictly line-by-line, that would be true; but they aren't.

There's a reason they aren't. The purpose of git is to maintain versions of program source code. The odds are the code on line 1 is related to the code on line 2. If changes aren't separated by a respectable distance (no, I don't know what that distance is[1]), they are considered to affect the same hunk.

So rather than assume that these changes are independently correct and don't interfere with one another, git flags it as a conflict and asks you to decide what the final result should be. Even if you ended up having to needlessly resolve 100 such conflicts, the cost would still be lower than one instance where git assumes it knows what to do and is wrong.

And the analysis for the 3rd and 4th merges will be the same; in each case you have two patches that affect the same hunk of code, so manual intervention is desired.

Now, if you happen to be source controlling some special type of file where you are highly confident (actually, probably you should have to be absolutely sure ) that changes to one line are independent from changes to the next line, then you could write your own merge driver. It's not trivial, keeping in mind that you have to know what to do when lines are added, when lines are removed, when two versions of the file have deviated much more wildly than what you've shown in your example, etc.

The odds are it's better just to accept that sometimes you have to resolve a conflict. If you want do compose "toy" tests that simulate non-conflicting merges, the easiest way is to have each branch change a different file; but within a file, changes just can't be too close together or conflicts are going to happen.


[1] UPDATE FROM COMMENTS - According to torek, having even one intervening line that nobody changed is sufficient. I didn't think that was correct, but as a rule if torek tells me something I think is wrong, I run another test; and based on that test, I would agree with him. So in fact it seems you could merge branch1 and branch3 without conflict, for example.

Is there a method to achieve this scenario without getting merge conflicts?

tl;dr: No.

Merge finds (and in some cases Git's merge will actually construct) the common base and compares diffs from that base to each branch tip. Any difference in changes to overlapping ranges constitutes a conflict.

Nobody's ever found a way to resolve those overlaps automatically that works. We've got vast quantities of existing history to work with¸so you can try any method you come up with on the merges in linux or vim or libreoffice or git itself, or what have you, it wouldn't be the first time everybody who's ever looked at a problem missed something, but I'd bet an awful lot on the "can't be done" answer.

The cost of a bad automerge is very high, it's the "pain" in the pain/gain metric. The gain is simply convenience: many conflicts, like yours, are easy for humans to resolve correctly. Get good with the tools, easy cases take seconds. So Git's properly cautious.

我发现有帮助的是将分支1和2合并到主服务器后,检出到分支3并从主服务器中拉出并解决冲突,然后推送到主服务器,然后分支4可以从主服务器中拉出所有已解决的冲突并合并然后再推送到主服务器。

Is there a method to achieve this scenario without getting merge conflicts?

No (as explained in other answers), but it is possible with the right tooling to have them automatically resolved.

The following builds on all the commands in Torek's answer .

$ git merge br2
Auto-merging myfile.txt
CONFLICT (content): Merge conflict in myfile.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   myfile.txt

no changes added to commit (use "git add" and/or "git commit -a")
$ git ls-files -u
100644 cb3ff6d73dab0ac586b87f6c5f222e37b85dd32d 1       myfile.txt
100644 8bc2250b70fd06d951fd6ae1ea7ebf0b421ce2e3 2       myfile.txt
100644 8bfe14645e97d71dd4036b5a51d73e29020cba55 3       myfile.txt
$

So merging br2 results in a conflict (and you can inspect which versions that are involved with ls-files ).

Git's internal merge handling fails when two lines next to each other are modified while if using KDiff3 to do the merging instead it will not (which is usually what you want but there is a higher risk of this being incorrect than for lines further apart, so there is a trade off).

Using KDiff3 is simple with the mergetool command;

$ git config merge.tool kdiff3
$ git mergetool
Merging:
myfile.txt

Normal merge conflict for 'myfile.txt':
  {local}: modified file
  {remote}: modified file
$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

        modified:   myfile.txt

Untracked files:
$ ..." to include in what will be committed)

        myfile.txt.orig

$ rm myfile.txt.orig
$ git diff --cached
diff --git a/myfile.txt b/myfile.txt
index 8bc2250..73e9fe0 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1,4 +1,4 @@
 01 one
-02
+02 two
 03
 04
$ 

KDiff3 is a graphical 3-way merge program, but in this case when it resolved things automatically it did not display a window. If there were conflicts that had to be resolved manually it would have.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM