首页 > 解决方案 > 如何为合并和冲突解决提供不同的提交

问题描述

我将开发分支合并到我的功能分支中,这导致在解决我提交和推送的内容后出现合并冲突,现在问题是合并和冲突解决更改都在一次提交中,很难找到解决冲突的方法。当存在合并冲突时,如何有两个单独的提交,一个用于合并,另一个用于冲突修复?

标签: gitgit-merge

解决方案


如果你真的想这样做,你可以——嗯,主要是. Git 让它变得非常困难,我认为这不是一个好主意。您无法通过这种方式捕获某些冲突。

我将提供一个大纲,说明如何捕捉你能捕捉到的东西,而不是它的实际代码。相反,我将描述设置是什么,以及会出现什么问题。

这里的问题是:

  • Git从出现在 Git索引(又名staging area)中的文件构建新的提交。
  • 带有冲突标记的合并冲突仅出现在您的工作树中。

使所有这一切有意义的部分——因为上面没有,除非并且直到你知道另一部分——是当你不在冲突合并的中间时,每个文件都有三个活动副本。

请记住,提交充当快照:它们拥有每个文件的完整副本。但是提交中任何给定文件的快照副本都以特殊的、只读的、仅限 Git 的格式存储。它实际上无法更改,除 Git 之外的任何程序都无法使用它。因此,当您使用git checkoutgit switch选择某个特定的提交来查看和处理/使用时,Git 必须将文件从提交复制到工作区:您的工作树工作树。这些文件是普通的日常文件。提交的文件仍然存在,在当前的 commit中,所以这提供了每个文件的两个副本:

  • 当前提交中有一个冻结的:HEAD:README.md例如。跑过去看看。git show HEAD:path

  • 而且,在 中有正常的日常文件README.md:使用您喜欢的任何查看器查看它,以及您喜欢的任何编辑器来更改它。

这两者之间,Git 保留了文件的第三个副本1。此副本位于 Git 的索引中,Git 也将其称为暂存区。此副本为冻结格式,但与已提交的副本不同,您可以批发替换它为新副本。就是这样git add做的:它获取工作树副本,将其压缩为特殊的 Git 格式,并将该副本放入 Git 的索引中,准备好提交。

  • 要查看索引副本,请运行,例如。git show :pathgit show :README.md

通常,索引副本将匹配HEAD副本(因为您刚刚签出提交,或刚刚提交)或工作树副本(因为您刚刚git add-ed 一个文件),或者将匹配其他两个副本(git statusnothing to commit, working tree clean)。但有可能:

  • 检查一些提交(所有三个匹配)
  • 修改工作树文件(HEAD 和索引匹配,工作树不匹配)
  • git add修改后的文件(HEAD 不匹配,索引和工作树匹配)
  • 再修改文件

现在所有三个副本都不同。这里根本没有错:这就是 Git 的工作方式,并且git add -p旨在git reset -p让您有意识地操纵这种情况。它们的工作原理是将文件的索引副本复制到一个临时文件中,然后让您修补这个临时文件,一次一个 diff-hunk,然后将其复制回索引副本。

无论如何,这是正常设置,当您没有处于冲突合并中时:

  • HEAD代表当前的提交,当前的提交有一个你不能改变的每个文件的副本。您可以更改哪个提交是当前提交(通过签出其他提交),但您不能更改存储在这些提交中的文件。可以轻松访问HEAD已提交文件的副本,git status等等git diff会查看这些副本。

  • 索引存储每个文件的副本。您可以更改这些副本。通常,您可以通过使用git add. 或者,您可以通过将HEAD副本复制回索引来更改它,使用git reset.

  • 工作树存储每个文件的副本。这个副本是你的:只有当你告诉 Git 覆盖它时,Git 才会覆盖它。Git 不会在你使用它时使用它git commit: Git 使用 Git索引中的副本。

但是,当您进入冲突合并状态时,索引已被扩展。它现在不仅包含一个冲突文件的副本,还包含三个. 现在,事情变得棘手了。


1从技术上讲,索引包含引用,而不是实际副本,但效果是相同的,除非您开始使用git ls-files --stagegit update-index深入研究低级细节。


与冲突合并

如您所见,当您运行时:

git checkout somebranch
git merge other

有时 Git 能够自己进行合并并完成,有时它会完成一些合并,但会吐出一些CONFLICT消息并在合并中间停止。

实际上有两种不同冲突,我喜欢称之为高级低级。大多数人首先遇到的是低级冲突,因为它们是最常见的。它们是在 Git 的ll-merge.c代码中生成的,其中ll代表“低级别”,因此得名。

在 Git 中,合并使用非常标准的三向合并算法。Git 实际上使用递归变体作为默认值;您可以使用 禁用它git merge -s resolve,但很少有任何理由这样做。任何三向合并都需要三个输入文件:通用(共享)合并基础版本、左侧或本地或--ours版本,以及右侧或远程或--theirs版本。合并只是将基数与左右进行比较。这会产生一组要进行的更改。合并合并更改:如果左侧在第 42 行修复了单词的拼写,则进行更改;如果右侧删除了第 79 行,也进行该更改。

当左侧和右侧尝试对单个文件的同一区域进行不同的更改时,就会发生冲突,或者更具体地说,低级别的冲突。在这里,Git 根本不知道是接受左侧更改还是右侧更改,两者都进行,或者都不进行。因此它会通过冲突停止合并(在继续合并它可以自行合并的任何其他内容之后)。

当存在整个文件更改时会发生高级别的冲突。也就是说,左侧的更改可能包括方向:重命名README.mdREADME.rst。如果右侧没有 rename README.md,或者确实重命名了它但是 to README.rsttoo,那没关系。但是如果右边说rename README.mdto README.html: Git 应该如何结合这些变化呢?

同样,Git 只是放弃并声明了冲突。不过,这一次,这是一场高级别的冲突。

在这两种情况下,Git 在 Git 的索引中所做的事情都很简单:它只是保留所有副本。为了能够区分三个不同的README.md文件——假设没有复杂的重命名冲突——它只是对索引中的文件进行编号:

  • git show :1:README.md向您显示合并基础版本;
  • git show :2:README.md显示--ours版本;和
  • git show :3:README.md向您展示 --theirs` 版本。

Git 用冲突标记写出一个新的README.md工作树副本,但原来的三个输入仍然存在,在索引中。作为完成合并的人,您的工作不一定是修复工作树副本。Git 不需要那个副本:那个是给的。Git 需要最终版本,在 Git 的索引中。

上面的索引号是槽号,最后的副本进入槽零,这会擦除其他三个槽。你的工作是提出正确的README.md并将其放在零槽中。

一种简单的方法是编辑工作树——README.md完整的冲突标记——直到你得到正确的合并结果。然后,将此文件写回工作树并运行git add README.md. 像往常一样从工作树复制README.md到索引中:副本进入插槽零,擦除其他三个插槽。

其他三个插槽条目的存在 - :1:README.md:2:README.md和/或:3:README.md- 是将该文件标记为冲突的原因。现在它们都消失了,文件不再冲突。

您可以使用任何您喜欢的程序将正确的文件放入插槽 0。这就是 Git 真正关心的全部内容:正确的文件进入插槽 0,而其他三个插槽被删除。由 调用的一个花哨的工具git mergetool可能对您很方便,但最终,它通过将最终结果复制到插槽 0 并擦除其他插槽来工作。Git 根本不关心你的工作树文件。Git 只需要修复它的索引。

当您遇到高级别的冲突时,例如重命名/重命名冲突或修改/删除冲突,Git 也会将其记录在 Git 的索引中——但这一次,它是通过有一些插槽未被占用的事实来记录的。请记住,插槽与文件源一起使用:合并基础 = 插槽 1,我们的 = 2,他们的 = 3。因此,如果合并基础有README.md,我们有README.rst,他们有README.html,你最终得到的是:

  • :1:README.md存在,但 :2: 和 :3: 不存在
  • :2:README.rst存在,但 :1: 和 :3: 不存在
  • :3:README.html存在,但 :1: 和 :2: 不存在

你的工作是删除所有这三个并将一些东西放在某个插槽零。它不必命名README.mdREADME.rst其他:也许您可以创建一个名为README.who-knows.

您的新合并提交,当您提交时,将包含插槽 0 中的任何文件。在清除所有编号较高的暂存槽之前,您无法进行提交。所以你必须自己解决每个冲突的文件:只有这样你才能运行或做出最终的合并提交结果。git merge --continuegit commit

可以简单地git add在所有冲突的文件上运行。如果工作树中有一个带有冲突标记的低级冲突README.md,则将工作树版本复制到索引槽零并擦除其他三个槽。如果那是唯一的冲突,那么您现在可以提交了。问题是您丢失了所有三个输入文件:您必须稍后重新合并,并解决冲突。但是您可以git add在每个文件上使用,然后提交。

这不适用于高级冲突:如果存在重命名/重命名冲突,您应该使用哪个名称?如果存在修改/删除冲突,是保留修改后的文件,还是保留删除?

无论你在这里选择什么,你都已经解决了这个冲突。合并提交将存储您在插槽零索引条目中放置的任何内容,作为其新快照。

如果您已经存储了冲突的文件,并且想要恢复冲突,那么获得它的唯一方法是重新执行合并 - 或者,等效地,保存合并冲突数据(输入文件和/或索引)。目前尚不清楚其中哪一个更容易:两者都有很多潜在的问题。我认为,陷阱最少的一个是使用git merge-file,它在三个输入文件上运行低级合并。

结论

因此,对于每个低级冲突文件,您可以:

  1. 在某处提取文件的三个副本。(注意:git checkout-index有执行此操作的选项。这是git mergetool为您的合并工具提供三个副本的方式。)
  2. git add来自工作树的冲突文件,以解决冲突,将标记的版本作为正确的解决方案。
  3. 运行git merge --continue以提交合并。
  4. 使用git merge-file在步骤 1 中保存的文件来重新创建冲突。
  5. 手动解决冲突。
  6. git add生成的文件,将它们复制到 Git 的索引。
  7. 进行新的提交。

这需要做很多 Git 不做的事情,而且它不能很好地处理高层次的冲突。其他 Git 工具会假设提交包含正确的分辨率,所以你是在给其他人设陷阱,他们会假设该工具知道正确的事情。至少我不清楚你为什么要这样做——为什么有人会这样做——当你以后可以通过运行找到相同的冲突时:

git checkout <hash>
git merge <hash>

其中两个值是在您运行原始命令时确定hash的两个提交的哈希 ID 。这两个哈希值很容易从合并提交本身中找到:它们分别是它的第一个和第二个父级。因此,如果包含合并哈希 ID:somebranchotherbranchgit merge$M

git rev-parse $M^1 $M^2

显示您需要重复合并以重新获得冲突的两个哈希 ID。这里唯一缺少的是您提供给git merge命令的任何选项。Git 不会保存它们(我认为应该)——但如果没有别的,你可以手动将它们保存在你的日志消息中。


推荐阅读