我怎样才能有不同的提交来进行合并和冲突解决

dev*_*v47 7 git git-merge

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

tor*_*rek 3

如果你真的想这样做,你可以\xe2\x80\x94well,大多数情况下. Git 让它变得相当困难,我认为这不是一个好主意。有些冲突无法通过这种方式捕获。

\n\n

我将提供如何捕获您可以捕获的内容的概要,但不会提供其实际代码。相反,我将描述设置是什么以及会出现什么问题。

\n\n

长的

\n\n

这里的问题是这样的:

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

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

\n\n

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

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

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

  • \n
\n\n

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

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

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

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

现在三个副本都不同了。这里没有什么根本性的错误:这就是 Git 的工作方式,git add -p并且git reset -p让您故意操纵这种情况。它们的工作原理是将文件的索引副本复制到临时文件中,然后让您修补此临时文件(一次一个差异块),然后将其复制回索引副本中。

\n\n

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

\n\n
    \n
  • HEAD代表当前提交,当前提交具有​​您无法更改的每个文件的副本。您可以更改哪个提交是当前提交(通过检查其他一些提交),但无法更改存储在这些提交中的文件。可以轻松访问HEAD已提交文件的副本,等等git statusgit diff查看这些副本。

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

  • \n
  • 工作树存储每个文件的副本。该副本是您的:只有当您告诉 Git 覆盖它时,Git 才会覆盖它。当您出现以下情况时,Git 不会使用它: Git 使用 Git索引git commit中的副本。

  • \n
\n\n

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

\n\n
\n\n

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

\n\n
\n\n

与冲突合并

\n\n

正如您所发现的,当您运行时:

\n\n
git checkout somebranch\ngit merge other\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n

当整个文件发生更改时,会发生高级冲突。也就是说,左侧的更改可能包括方向:重命名README.mdREADME.rst。如果右侧没有重命名README.md,或者也重命名了README.rst,那也没关系。但是如果右侧显示重命名README.mdREADME.html Git 应该如何组合这些更改呢?

\n\n

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

\n\n

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

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

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

\n\n

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

\n\n

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

\n\n

:1:README.md其他三个槽条目\xe2\x80\x94 (即:2:README.md、 和/或\xe2\x80\x94 )的存在:3:README.md将文件标记为冲突。现在它们都消失了,文件不再存在冲突。

\n\n

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

\n\n

当您遇到高级冲突时,例如重命名/重命名冲突或修改/删除冲突,Git 也会将其记录在 Git 的索引\xe2\x80\x94 中,但是这一次,记录的事实是:有一些插槽未被占用。请记住,插槽与文件源一致:merge base = slot 1,ours = 2,theirs = 3。因此,如果合并base有README.md,我们有README.rst,他们有README.html,你最终会得到的是:

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

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

\n\n

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

\n\n

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

\n\n

这对于高级冲突来说效果不太好:如果存在重命名/重命名冲突,您应该使用哪个名称?如果出现修改/删除冲突,是保留修改的文件,还是保留删除的文件?

\n\n

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

\n\n

如果您已存储冲突的文件,并且希望恢复冲突,则唯一的方法是重新执行合并\xe2\x80\x94,或者等效地保存合并冲突数据(输入文件和/或索引) 。目前还不清楚哪一个更容易:两者都有很多潜在的问题。我认为陷阱最少的方法是使用git merge-file,它对三个输入文件运行低级合并。

\n\n

结论

\n\n

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

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

这是一个需要做大量工作的事情,而 Git 无法做到这一点,并且它不能很好地处理高级冲突。其他 Git 工具会假设提交包含正确的解决方案,因此您正在为其他假设该工具知道正确内容的人设置陷阱。至少我不清楚为什么你想要这样做\xe2\x80\x94为什么有人会想要这样做\xe2\x80\x94当你稍后可以通过运行找到相同的冲突时:

\n\n
git checkout <hash>\ngit merge <hash>\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
git rev-parse $M^1 $M^2\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n