Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
222 views
in Technique[技术] by (71.8m points)

git - Add line to differnent position cause conflict by cherry-pick

My branch structure is:

0x1---->0x2---->0x3
/              /
|               |
master          dev

Common ancestors is 0x1.

I cherry-pick a feature to master.

Scene 1:

Master branch have a.txt file.
0x1 first commit.
a.txt content:
1

Then I create a branch dev, I add "2" to a.txt,
0x2 second commit
0x1 first commit

a.txt content:
1
2

Then I add "3" to a.txt
0x3 third commit
0x2 second commit
0x1 first commit

a.txt content:
3
1
2

I cherry-pick 0x3 to master:
master> git cherry-pick 0x3

It has no conflict.

Scene 2: But I modify added position.

0x3 third commit
0x2 second commit
0x1 first commit

a.txt content:
1
2
3

I apply 0x3 to master. It will have a conflict.

What's the difference between Scene 1 and Scene 2? I'm confused!

question from:https://stackoverflow.com/questions/65842371/add-line-to-differnent-position-cause-conflict-by-cherry-pick

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Your cherry-pick gets a merge conflict because the same merge would get a merge conflict. That is because a cherry-pick is a merge operation. It's just that the merge base for the cherry-pick is chosen specially, and the final commit is an ordinary single-parent commit, rather than a two-parent merge commit.

The conflict itself is, I think, easier to explain when it is done with git merge. Suppose we have the following series of commits:

          I--J   <-- branch1
         /
...--G--H
         
          K--L   <-- branch2

We pick some commit, such as commit J, to be the current commit by picking one branch name, such as branch1, to be the current branch. Git will extract the snapshot in commit J into our working tree:

git checkout branch1

I like to denote this situation by attaching the special name HEAD to the chosen branch name, like this:

          I--J   <-- branch1
         /
...--G--H
         
          K--L   <-- branch2

If we now run git merge branch2, Git will activate its merge machinery, so as to perform the merging process. This merge machinery needs three commits:

  • One of the commits is our current commit J.
  • One of the commits is the one we name with git merge: in this case, branch2 selects commit L.
  • The third—or, in a sense, first, because Git has to use it first—commit is one that the merge process locates. The merge command finds the best shared commit: a commit that is on both branches, and is in some sense "closest" to the two branch tip commits. Here, that commit is clearly commit H.

To perform the actual merge work, Git now:

  • compares the snapshot in the base (H) to that in the current commit J; and
  • compares the snapshot in the base to that in the other commit L.

This produces two "diffs": two recipes for changing the files that appear in the merge-base commit. The first recipe says that if you do various things—such as add and/or delete particular lines of particular files—to the files in H, you'll get the files in J. The second recipe says that if you do whatever other various things, you'll get the files in L.

The merge engine now combines two diffs. This combining can produce merge conflicts.

The question of when Git finds a merge conflict is the one that's a little odd. There's one case that's clear enough. Suppose the base version of the file, in commit H, reads, e.g.:

The quick brown fox
jumps over
the lazy dog.

Suppose that the J and L versions read:

The quick brown fox
sometimes jumps over
the lazy dog.

and:

The quick brown fox
has often jumped over
the lazy dog.

Both diffs changed line 2, in incompatible ways. Git doesn't know which change to take, so it takes neither, or both, depending on how you look at it, and then declares a merge conflict and makes you clean up the mess.

Your cherry-picks

[Note: this has been revised to use the commit graph in the updated question. We now create the repository with:

mkdir tcherry && cd tcherry && git init
echo 1 > a.txt && git add a.txt && git commit -m 0x1
git checkout -b dev
echo 2 >> a.txt && git add a.txt && git commit -m 0x2

and then either insert 3 at the top of a.txt or add it to the end of a.txt to generate the setup for the two scenarios:

printf "3
1
2
" > a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev

or:

echo 3 >> a.txt && git add a.txt && git commit -m 0x3
git checkout master
git cherry-pick dev

The first one works without conflict; the second gives a conflict.]

In your case, you aren't using git merge, you're using git cherry-pick. But a cherry-pick is still a merge. Instead of:

          I--J   <-- branch1
         /
...--G--H
         
          K--L   <-- branch2

you have:

  B--C   <-- dev
 /
A   <-- master (HEAD)

and you run git cherry-pick dev. Note that dev selects commit C, while your current branch is master and your current commit is A.

The cherry-pick command invokes Git's merge engine, but this time, the merge base is forced to be the parent of the commit you're cherry-picking. So the merge base here is commit B. Git will now:

  • diff B vs A, to get "our" changes; and
  • diff B vs C, to get "their" changes.

The difference from B to A is that you removed the line reading 2 at the end of a.txt. The following (which uses a sneaky kind of git rev-parse to find the right commits; see the description of :/<text> in the gitrevisions documentation) shows this:

$ git diff :/0x2 :/0x1
diff --git a/a.txt b/a.txt
index 1191247..d00491f 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1 @@
 1
-2

The difference from A to C depends on which of these two scenarios you're using.

Here's the diff for the non-conflict case:

$ git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..0571a2e 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
+3
 1
 2

This says to add 3 before the line reading 1, which was line 1 and becomes line 2. So Git is supposed to delete the line reading 2, which was line 2 in the merge base version. Git can safely do this because it's known where to do this, and line 1 itself is not touched: only lines from 2 to end-of-file are affected. Git is also supposed to insert the line reading 3 before the line reading 1. This affects from the beginning of the file ("line 0") through line 1. The affected line ranges do not overlap, and the line reading 1 sits between them, making them not "touch" each other either.

Now let's look at the case that does get a merge conflict:

$ git reset --hard HEAD^          # discard the cherry-pick
HEAD is now at 2b4180d 0x1
$ git checkout -q dev
$ git reset --hard HEAD^          # discard commit 0x3
HEAD is now at 1160130 0x2
$ echo 3 >> a.txt && git add a.txt && git commit -m 0x3
[dev c546f74] 0x3
 1 file changed, 1 insertion(+)
$ cat a.txt
1
2
3
$ git checkout master
Switched to branch 'master'

Again, we're on commit A now, and we'll instruct Git to cherry-pick commit C (subject 0x3) whose parent is B. The diff from B to A is still "remove the line reading 2" (which is on line 2). Let's look at the diff from B to C this time:

git diff :/0x2 :/0x3
diff --git a/a.txt b/a.txt
index 1191247..01e79c3 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
 1
 2
+3

Git is now supposed to combine the deletion of the line reading 2 with the addition of the line reading 3. The first one touches line 2 (by removing it entirely). The second one adds a line after line 2.

Git certainly could combine these, but the "is this a conflict" algorithm doesn't like the fact that both diffs affect line 2. These diffs don't have a line in between the two changes, separating the changes from each other. So we get a conflict:

$ git cherry-pick dev
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
error: could not apply c546f74... 0x3
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

Examining the a.txt left behind, we see:

$ cat a.txt
1
<<<<<<< HEAD
||||||| parent of c546f74... 0x3
2
=======
2
3
>>>>>>> c546f74... 0x3

Note that I have merge.conflictStyle set to diff3, so I get the ||||||| parent of ... line, showing what the conflict is about. Compare that to the default style:

$ git checkout -m --conflict merge a.txt
Recreated 1 merge conflict
$ cat a.txt
1
<<<<<<< ours
=======
2
3
>>>>>>> theirs

which I personally find more mysterious. In the case of cherry-picks like this one, the fact that the diff from B to A goes "backwards" means we have a deletion, of line 2. This forms the ours part of the conflict. It's not at all obvious from the merge style conflict; in the diff3 style, I can look at this and see that the merge base version has a line reading 2, which I've deleted. They kept their line 2 when they added their line 3.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...