我的存储库包含一些意外:分支 A(很差地)合并到 B,然后这些更改被还原,然后 B 被合并到主分支。
分支 A 即使包含新功能,也会合并到没有该功能的主分支中。
git checkout master
git merge A
Run Code Online (Sandbox Code Playgroud)
什么也没做。
git checkout A
git merge master
Run Code Online (Sandbox Code Playgroud)
快进至主控 HEAD。
我怎样才能将 master 和 A 与自定义合并基“合并”,基本上忽略它们已经合并了?
简短的回答是,你不能,但你不需要。只需创建一个新的“A 副本”分支并将其合并即可。最后我们将看到一种简单的方法来做到这一点,即使用git rebase.
长答案指出,实际上,您可以通过仔细使用git replace来构建合成历史记录(或通过运行git merge-recursive:这在某种意义上更容易,但不是为用户直接调用而设计的)。需要替换的内容需要进行一些分析,并且可能需要进行大量非常仔细的分析。这允许您运行git merge以实现特定结果...但该结果仅由通过 构建的精心设计的合成历史记录支持git replace,并且该合成历史记录通常不会进入新的存储库。因此,除了这一步之外,还有很多工作最终没有被真正使用,git merge这意味着:为什么要麻烦呢?
(有时这个git replace技巧很简单,然后您可能想为了美观而这样做。也就是说,进行替换和合并,而不是我将展示的解决方法。但我认为分支副本无论如何,-A 更好。)
在我们深入了解合并机制之前,让我们先看看合并的结果。这有助于我们了解“为什么要麻烦”部分。让我们从实际需要合并的典型情况开始,即当我们有一个如下所示的提交图时:
\n I--J <-- ours (HEAD)\n /\n...--G--H\n \\\n K--L <-- theirs\nRun Code Online (Sandbox Code Playgroud)\n也就是说,我们位于一个名为 的分支上ours,该分支的最后一次提交有一些又大又难看的哈希 ID,我们将其称为“提交J”。我们拿起了别人的工作,我们把它放在一个我们称之为 的分支上theirs,它的最后一次提交有一些不同的大而丑陋的哈希 ID,我们将称之为L。
\n\n侧边栏:每次提交都保存所有文件的快照以及一些元数据。元数据包括其前任或父提交的哈希 ID 。因此,我们的提交
\nJ会记住之前提交的哈希 IDI,而后者又会记住更早提交的哈希 IDH。他们的提交L会记住其提交的哈希 IDJ,而提交会记住更早提交的哈希 IDH。这一切都意味着我们和他们都是从一个共同的起点开始的,那就是 commit
\nH。提交H保存所有文件的快照,其他每个提交也是如此。Git 可以将提交中的快照H与任何其他提交中的快照进行比较。
其工作是git merge合并更改:
我们从提交H\xe2\x80\x94 开始,这个共享的公共起点git merge调用合并基\xe2\x80\x94 并进行了一些更改并提交了新快照I。然后我们进行了更多更改并提交了最后一个快照J。如果我们让 Git 将快照H与我们的快照进行比较J,这将告诉 Git\xe2\x80\x94 和 us\xe2\x80\x94 我们更改了什么:我们修改了哪些文件,对于我们修改的每个文件,我们删除了哪些行以及我们添加了什么行,上面有什么文字。
与此同时,他们从提交开始H并进行了更改,并且(要么是巧合,要么是这个答案的作者为了使图表漂亮而作弊)还进行了两次提交。如果我们对 Git 进行比较H,L我们就会发现它们改变了什么。
然后,如果幸运的话,Git 可以将这两组更改\xe2\x80\x94 结合起来,全部靠自己\xe2\x80\x94 并将组合的更改应用到 commit 中的快照H。这样,我们既保留了我们的工作,也添加了他们的工作。我喜欢将合并这一阶段称为git merge动词:这是组合更改集的操作。
正如您在问题中或多或少指出的那样,这里的一个问题\xe2\x80\x94虽然通常是一个很大的优势,但实际上\xe2\x80\x94是Git自己选择合并基础提交。Git 使用提交图和一种称为“最低共同祖先”的计算机算法来做到这一点,因为在真实的图中找到一个好的合并基础非常困难,而真实的图中很少像我画的那样整洁。
\n一旦 Git 自己完成了所有这些合并,而不会因为合并冲突而在中间停止,git merge通常会继续进行合并提交,我们可以这样绘制:
I--J\n / \\\n...--G--H M <-- ours (HEAD)\n \\ /\n K--L <-- theirs\nRun Code Online (Sandbox Code Playgroud)\n这里提交的M字母不知何故巧合地是“merge”一词的第一个字母,它不仅有一个,而且有两个父母。第一个父级是通常的父级: commit J,我们的分支刚才指向的地方。第二个父级是我们合并的另一个提交。不过,除了两个父项之外,提交M就像任何其他提交一样,因为它具有每个文件的完整快照。无论如何,就 Git 而言,这个完整快照是合并的正确结果。
我喜欢将其称为merge 作为名词或形容词,因为这里我们使用单词merge来修饰名词commit,或者作为一个独立的名词,它是两个词短语merge commit 的简写。合并提交M用于更改未来合并的未来合并基础。这既是它的优势\xe2\x80\x94,通过修复确实出现的任何合并冲突,我们通常确保它们以后不会再次出现,例如\xe2\x80\x94,也是它的弱点。无论我们输入什么都是正确的,Git 也确信这一点,即使结果证明我们做错了。M
\n\n分支 A(很糟糕)合并到 B,然后这些更改被恢复,然后 B 被合并到主分支。
\n
Git 实际上是关于提交的,而不是分支\xe2\x80\x94,但是提交包括这些父链接、提交的集合以及它形成的图表,组成了人们称为分支的东西。这种对“分支”一词的模糊使用意味着上面的句子是有意义的\xe2\x80\x94,但也意味着我们应该画出这种情况,即使我们稍微简化一下,因为人类有时使用“分支”一词来表示分支名称。那么我们来画一下它:
\n...--*--...--o---------...----------N <-- master\n \\ \\ /\n \\ B1--B2--M--B3--W--B4 <-- branch-B\n \\ /\n A1---...---An <-- branch-A\nRun Code Online (Sandbox Code Playgroud)\n提交M是合并到一系列提交A1--...--An中。B
Commit W\xe2\x80\x94W看起来像一个颠倒的M;我从 Linus Torvalds\xe2\x80\x94was git revert -m1of commit那里偷了这个M。其目的是取消“错误的合并” M。这里的问题是,虽然快照被撤消,即恢复 inW取消了 中的不正确合并M,但历史记录仍然存在:提交A1的An都是 的祖先M,因此是 的祖先B4,因此是 的祖先N。
这就是为什么Git 认为合并已经完成。A:
\ngit checkout master && git merge branch-A\nRun Code Online (Sandbox Code Playgroud)\n说没有什么可做的。A:
\ngit checkout branch-A && git merge master # or git merge --no-ff master\nRun Code Online (Sandbox Code Playgroud)\nN计算 commit和 commit的合并基础An作为 commit An:不需要实际合并,并且不会--no-ff获得快进。添加--no-ff不会产生有用的效果,因为 Git 与一组更改相比An,An因此在整个A提交系列中没有发现任何更改。
您的问题是:我们如何说服git merge使用提交*作为合并基础? 这一次就可以实现正确的合并了。在这种特殊情况下,只有一个 merge M,我们可以用来git replace拼接一个临时的假替换M提交,它只有一个父级,B2而不是两个父级。这样就可以完成工作,之后我们就可以移除git replace替换件了。但这有点棘手。git merge我们还可以按照前端的方式设置一堆环境变量,然后git merge-recursive直接调用,提供所有正确的参数,因为这允许我们直接选择合并基础提交。但还有另一种方法。
A提交怎么办?假设我们可以采用这张图:
\n...--*--...--o---------...----------N <-- master\n \\ \\ /\n \\ B1--B2--M--B3--W--B4 <-- branch-B\n \\ /\n A1---...---An <-- branch-A\nRun Code Online (Sandbox Code Playgroud)\n并生成一个新的branch-C,如下所示:
C1--...--Cn <-- branch-C\n /\n...--*--...--o---------...----------N <-- master\n \\ \\ /\n \\ B1--B2--M--B3--W--B4 <-- branch-B\n \\ /\n A1---...---An <-- branch-A\nRun Code Online (Sandbox Code Playgroud)\n我们现在可以git checkout master && git merge branch-C。这将实现您想要的,并且成本非常低:C1-...-Cn分支 C 上的 N 次提交都重新使用分支 A 上相应的 N 次提交的快照。但它们具有不同的哈希 ID,因此 Git 将它们视为不同的、未合并的提交。C这一系列提交的合并基础是commit *,这就是您一直想要的。
那么,我们如何得到呢branch-C?答案是使用git rebase:
git checkout -b branch-C branch-A\ngit rebase -f <hash-of-commit-*>\nRun Code Online (Sandbox Code Playgroud)\n该checkout命令创建新名称 ,branch-C指向现有提交An。该git rebase命令告诉 Git 复制A1到An它们现在所在的位置。通常git rebase只会说:嘿,这是一个很大的禁止操作,我不会复制任何东西。 但我们使用这个-f选项(我们可以拼写为 as--no-ff或--force-rebaseas well),它告诉 Git 闭嘴,不要再聪明了,复制那些提交。所以当我们完成后,我们就得到了我们想要的东西。现在我们可以:
git checkout master && git merge branch-C\nRun Code Online (Sandbox Code Playgroud)\n这正是我们想要的,并留下了这次重新合并的记录以及分支的额外副本。如果合适(我认为这是合适的,但无论谁进行合并都可以决定这一点),进行合并的人可以包含一条注释,指出这是一种重新合并,指出早期的合并及其恢复以解释原因分支-C 首先出现。
\n| 归档时间: |
|
| 查看次数: |
756 次 |
| 最近记录: |