如何解决 "added by us" Git Cherry-pick 冲突?

3
我遇到了一个关于git的问题,在cherry-pick之后进行冲突解决时。问题涉及在之前的提交中git mv的文件上进行的修改。
以下是复现该问题的完整命令示例:
mkdir git_repository && cd git_repository
git init
echo "foo" > myFooFile
git add myFooFile 
git commit -m "First commit add myFooFile"

git checkout -b branch-a
rm myFooFile
echo "bar" > myBarFile
git add -A
git commit -m "rm myFooFile add myBarFile"

git checkout master
git mv myFooFile myBarFile
git add -u
git commit -m "git mv myFooFile myBarFile"

现在让我们在分支a上进行修改。
git cherry-pick $(git show-ref branch-a)
>error: could not apply 70c80f3... rm myFooFile add myBarFile
>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'

以下是冲突内容:

git status
>On branch master
>You are currently cherry-picking commit 70c80f3.
>  (fix conflicts and run "git cherry-pick --continue")
>  (use "git cherry-pick --abort" to cancel the cherry-pick operation)
>
>Unmerged paths:
>  (use "git add <file>..." to mark resolution)
>
>   added by us:     myBarFile
>
>no changes added to commit (use "git add" and/or "git commit -a")

很不幸,只使用git add myBarFile然后执行git cherry-pick --continue不是解决方案,因为它会创建一个空提交。
在冲突解决期间使用git rm myBarFile也不好,因为它会创建一个删除myBarFile中foo的提交。
如何正确解决由我们添加的cherry-pick冲突,并最终使我在我的主分支上拥有这3个提交?
  • rm myFooFile add myBarFile
  • git mv myFooFile myBarFile
  • 第一次提交添加myFooFile
其中myBarFile包含bar?
注意:我知道在Git冲突解决过程中可以使用git checkout branch-a -- myBarFile,但这不是我要寻找的解决方案,因为我认为这不是Git的做法。
1个回答

1

小建议:不要这样做:

git cherry-pick $(git show-ref branch-a)

请这样做:
git cherry-pick branch-a

The cherry-pick 命令接受任何可以标识修订版本的内容,甚至是修订版本范围,如 gitrevisions 文档 中所述。git show-ref 命令输出哈希 ID 和名称,因此它会尝试两次执行 cherry-pick 操作。(幸运的是,git cherry-pick 足够聪明,可以消除多余的操作。)
解决冲突的高级视图
没有一种绝对正确的方法来解决冲突。您可以使用任何您喜欢的方法。这里有几个重要的事情需要记住:
  • Cherry-picking is essentially a three-way merge a la git merge, except that instead of finding the actual merge base, Git just uses the parent of the commit you are cherry-picking as the merge base (and of course, the final commit is a regular non-merge commit).

  • Git doesn't track file name changes. Instead, when doing a merge operation—including a cherry-pick—Git in essence runs two git diff commands:

    git diff --find-renames <merge-base> HEAD     # figure out what we did
    git diff --find-renames <merge-base> <other>  # figure out what they did
    

    If the result is a merge conflict, Git leaves all three "interesting" files in the index—but one or two such files can be missing (as is the case here). The git status command shows these as "unmerged", but you can find the full details using git ls-files --stage. I'll show details in a moment.

  • Your job, at this point, is simply to arrange, in the index, the files you want in the final commit to be made as a result of the merge (or cherry-pick, in this case). These files need to be at stage zero, which is where normal, un-conflicted files live in the index. Any entries at stage 1 represent the version of the file in the merge base, any at stage 2 represent the version of the file in the HEAD commit, and any at stage 3 represent the version of the file in the other commit you're merging with (or cherry-picking).

冲突本身

那么,让我们来看看 git diff --find-renames 所找到的内容,知道合并基础是被挑选的提交的父级。我们可以使用名称 branch-a 来确定挑选的提交。因此,它的父级是 branch-a^(再次参见 gitrevisions 文档)。

$ git diff --find-renames branch-a^ HEAD
diff --git a/myFooFile b/myBarFile
similarity index 100%
rename from myFooFile
rename to myBarFile

这是“我们所更改的内容”:一次重命名操作。
$ git diff --find-renames branch-a^ branch-a
diff --git a/myBarFile b/myBarFile
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/myBarFile
@@ -0,0 +1 @@
+bar
diff --git a/myFooFile b/myFooFile
deleted file mode 100644
index 257cc56..0000000
--- a/myFooFile
+++ /dev/null
@@ -1 +0,0 @@
-foo

这是“他们所做的更改”:删除一个文件,添加另一个。
现在Git负责将myFooFile重命名为myBarFile(内容为foo),同时删除myFooFile并创建带有内容barmyBarFile。它不可能同时完成两个操作,因此我们会遇到冲突。与此同时,它可以执行我们所做的重命名,因此它只在工作区域中执行了该操作,使得myBarFile包含foo
您想知道:
如何正确修复我们添加的cherry-pick冲突,并最终使myBarFile包含bar
Git所需要的就是让您向工作区写入一个包含所需内容的myBarFile版本,然后调整索引以使其条目处于第零阶段。目前,索引中的内容是:
$ git ls-files --stage
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 2      myBarFile

并且myBarFile的内容包含您不想要的内容。

注意:我知道在Git冲突解决期间可以使用git checkout branch-a -- myBarFile...

是的:这会从由branch-a标识的提交中提取myBarFile的版本,将其写入阶段零的索引中,删除阶段2中的条目,并准备好提交所有内容。(作为奖励,它还将文件写入工作树,以便您可以看到它,尽管Git实际上在这一点上并不关心工作树版本。)因此,这是实现所需结果的一种好而快速的方法。我们可以看看另一种方法,虽然在某些情况下可能有用:

[但]我认为这不是Git的做法。

Git没有一个(单一)方法。Git是一种工具。按照您的喜好使用它。如果您喜欢在未触及索引的情况下调整工作树内容,则可以使用git show提取文件的branch-a版本:

$ git show branch-a:myBarFile > tmp
$ cat tmp
bar
$ mv tmp myBarFile
$ git add myBarFile

如果我们检查实际的暂存区,我们会发现:

$ git ls-files --stage
100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0       myBarFile

这就是我们想要的,因此现在我们可以使用 git cherry-pick --continuegit commit 完成此次 cherry-pick:

$ git cherry-pick --continue

此时,您首选的编辑器应打开一个包含主题 rm myFooFile add myBarFile 和其他数据(因为这是您要挑选的提交)的消息文件。将其写出如下:
[master 1837c17] rm myFooFile add myBarFile                   
 Date: Fri Jun 15 22:21:48 2018 -0700
 1 file changed, 1 insertion(+), 1 deletion(-)

"

git log --oneline则显示你需要的内容:

"
1837c17 (HEAD -> master) rm myFooFile add myBarFile
a30b874 git mv myFooFile myBarFile
bde7df5 First commit add myFooFile

(注:我在我的配置中有log.decorate = auto。)

非常感谢您详细的回答,但对我来说仍然不清楚。您说:“它不可能同时完成两个操作,因此我们会遇到冲突。与此同时,它可以执行我们所做的重命名操作,因此它确实在工作树中执行了该操作(仅限于工作树),使得myBarFile包含foo。”之后,为什么Git不能尝试将“bar”添加到“myBarFile”,然后在“myBarFile”中创建带有尖括号“<<<<<<< foo ======= bar >>>>>>>”的差异。此外,在解决冲突时为什么“git show :3:myBarFile”无法工作,我期望在这一步中看到“bar”。 - Hugo y
它无法这样做,因为这些是不同的文件,它们只是恰好具有相同的名称。这是相同的问题,以稍微不同的形式出现,就像在Windows系统上尝试拥有README.TXTReadMe.txt一样(您可以在Linux框中拥有两个文件,但是当您在Windows或MacOS上检查此类提交时,Git无法执行它,您会得到一个混乱)。Git理论上可以为名称在操作系统级别冲突的文件制定新名称,但实际上并没有这样做。 - torek
好的,但是为什么在冲突解决期间 git show :3:myBarFile 不起作用,我希望在这一步中能看到 bar。同样的原因吗? - Hugo y
不,这是因为它在第二阶段而不是第三阶段——此时第三阶段为空。请参见我从git ls-files --stage获得的输出。 (我认为在所有三个阶段中保留文件可能是合理的,但Git不这样做。) - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接