git - 如何为合并和冲突解决提供不同的提交
问题描述
我将开发分支合并到我的功能分支中,这导致在解决我提交和推送的内容后出现合并冲突,现在问题是合并和冲突解决更改都在一次提交中,很难找到解决冲突的方法。当存在合并冲突时,如何有两个单独的提交,一个用于合并,另一个用于冲突修复?
解决方案
如果你真的想这样做,你可以——嗯,主要是. Git 让它变得非常困难,我认为这不是一个好主意。您无法通过这种方式捕获某些冲突。
我将提供一个大纲,说明如何捕捉你能捕捉到的东西,而不是它的实际代码。相反,我将描述设置是什么,以及会出现什么问题。
长
这里的问题是:
- Git从出现在 Git索引(又名staging area)中的文件构建新的提交。
- 带有冲突标记的合并冲突仅出现在您的工作树中。
使所有这一切有意义的部分——因为上面没有,除非并且直到你知道另一部分——是当你不在冲突合并的中间时,每个文件都有三个活动副本。
请记住,提交充当快照:它们拥有每个文件的完整副本。但是提交中任何给定文件的快照副本都以特殊的、只读的、仅限 Git 的格式存储。它实际上无法更改,除 Git 之外的任何程序都无法使用它。因此,当您使用git checkout
或git switch
选择某个特定的提交来查看和处理/使用时,Git 必须将文件从提交复制到工作区:您的工作树或工作树。这些文件是普通的日常文件。提交的文件仍然存在,在当前的 commit中,所以这提供了每个文件的两个副本:
当前提交中有一个冻结的:
HEAD:README.md
例如。跑过去看看。git show HEAD:path
而且,在 中有正常的日常文件
README.md
:使用您喜欢的任何查看器查看它,以及您喜欢的任何编辑器来更改它。
但在这两者之间,Git 保留了文件的第三个副本1。此副本位于 Git 的索引中,Git 也将其称为暂存区。此副本为冻结格式,但与已提交的副本不同,您可以批发替换它为新副本。就是这样git add
做的:它获取工作树副本,将其压缩为特殊的 Git 格式,并将该副本放入 Git 的索引中,准备好提交。
- 要查看索引副本,请运行,例如。
git show :path
git show :README.md
通常,索引副本将匹配HEAD
副本(因为您刚刚签出提交,或刚刚提交)或工作树副本(因为您刚刚git add
-ed 一个文件),或者将匹配其他两个副本(git status
说nothing 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 --stage
并git 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.md
为README.rst
。如果右侧没有 rename README.md
,或者确实重命名了它但是 to README.rst
too,那没关系。但是如果右边说rename README.md
to 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.md
或README.rst
其他:也许您可以创建一个名为README.who-knows
.
您的新合并提交,当您提交时,将包含插槽 0 中的任何文件。在清除所有编号较高的暂存槽之前,您无法进行提交。所以你必须自己解决每个冲突的文件:只有这样你才能运行或做出最终的合并提交结果。git merge --continue
git commit
您可以简单地git add
在所有冲突的文件上运行。如果工作树中有一个带有冲突标记的低级冲突README.md
,则将工作树版本复制到索引槽零并擦除其他三个槽。如果那是唯一的冲突,那么您现在可以提交了。问题是您丢失了所有三个输入文件:您必须稍后重新合并,并解决冲突。但是您可以只git add
在每个文件上使用,然后提交。
这不适用于高级冲突:如果存在重命名/重命名冲突,您应该使用哪个名称?如果存在修改/删除冲突,是保留修改后的文件,还是保留删除?
无论你在这里选择什么,你都已经解决了这个冲突。合并提交将存储您在插槽零索引条目中放置的任何内容,作为其新快照。
如果您已经存储了冲突的文件,并且想要恢复冲突,那么获得它的唯一方法是重新执行合并 - 或者,等效地,保存合并冲突数据(输入文件和/或索引)。目前尚不清楚其中哪一个更容易:两者都有很多潜在的问题。我认为,陷阱最少的一个是使用git merge-file
,它在三个输入文件上运行低级合并。
结论
因此,对于每个低级冲突文件,您可以:
- 在某处提取文件的三个副本。(注意:
git checkout-index
有执行此操作的选项。这是git mergetool
为您的合并工具提供三个副本的方式。) git add
来自工作树的冲突文件,以解决冲突,将标记的版本作为正确的解决方案。- 运行
git merge --continue
以提交合并。 - 使用
git merge-file
在步骤 1 中保存的文件来重新创建冲突。 - 手动解决冲突。
git add
生成的文件,将它们复制到 Git 的索引。- 进行新的提交。
这需要做很多 Git 不做的事情,而且它不能很好地处理高层次的冲突。其他 Git 工具会假设提交包含正确的分辨率,所以你是在给其他人设陷阱,他们会假设该工具知道正确的事情。至少我不清楚你为什么要这样做——为什么有人会这样做——当你以后可以通过运行找到相同的冲突时:
git checkout <hash>
git merge <hash>
其中两个值是在您运行原始命令时确定hash
的两个提交的哈希 ID 。这两个哈希值很容易从合并提交本身中找到:它们分别是它的第一个和第二个父级。因此,如果包含合并哈希 ID:somebranch
otherbranch
git merge
$M
git rev-parse $M^1 $M^2
显示您需要重复合并以重新获得冲突的两个哈希 ID。这里唯一缺少的是您提供给git merge
命令的任何选项。Git 不会保存它们(我认为应该)——但如果没有别的,你可以手动将它们保存在你的日志消息中。
推荐阅读
- r - 如何将一个列表中的 data.tables 合并为一个 data.table/data.frame
- python - Python 在与 ElementTree 的同一循环中获取 XML 的父值和子值
- c# - c# winform 如何替换代码 If-else not working
- javascript - 如何避免节点/快递中的全局变量
- datetime - 在 hive 中将 090216(字符串)转换为 16-FEB-09(日期)
- reactjs - 在 Apollo / GraphQL 中发生突变后如何更新缓存?
- pine-script - 有没有办法绘制超过 53 个条的标签?
- python - train_in_memory 的输入函数要求
- amazon-web-services - aws dynamodb 定价 - 我的账单是否已计入免费套餐?
- java - 将树打印为输出的Java程序中的间距问题