(注意:这是交叉发布到git 和mercurial,所以我们需要一个涵盖两者的答案。我也很抱歉这有多长,但是有很多概念需要理解。如果这是 TL;DR,请跳到最后一部分,避免所有复制。但请注意,您必须向上阅读才能看到我在说什么。)
Git 和 Mercurial 的合并方式相同。1 特别是,分支名称无关紧要。重要的不是名称;重要的是提交图。
1有很多微小的差异,甚至有一些比较大的差异,但我这里的意思是总体想法是相同的。 Git 还支持 Git 所谓的快进合并的概念,这根本不是合并。这个概念在 Mercurial 存储库中毫无意义,根本不可能发生:Mercurial 不执行快进。运行hg merge <commit-specifier> 大致相当于运行git merge --no-ff <commit-specifier>。
在 Git 和 Mercurial 中,每个提交都以某种方式记录其父提交的 ID,或者(在合并提交的情况下)其父提交的 ID(对于 Mercurial,恰好两个,对于 Git,两个或更多)。因为每个提交都有自己的唯一 ID,并且每个提交都记录了它的父级或父级,我们可以绘制一个图——它以 tree 开始,这更简单,我将在此处说明——这些提交:
A <--B <--C
这里我们有一个简单的存储库,其中只有三个提交。提交C 是最新的。它有一个单亲B,所以C 记住B 的ID。 B 同样会记住A 的 ID。 (注意A 不知道B 和B 不知道C:箭头只会向后移动。)
任何提交都无法更改(这在 Git 中更是如此,但在 Mercurial 中也几乎如此)。所以我们不需要把箭头画成箭头;我们可以只使用连接线,只要我们记住 VCS 必须向后跟随它们。这很方便,因为我们现在可能决定添加一个新的提交 D,其父级是 B 而不是 C:
A--B--C
\
D
这是 Mercurial 和 Git 趋于分歧的地方:在 Mercurial 中,我们经常(故意)通过将提交 D 放在不同的分支上来实现这个结果。 Mercurial 记录进行任何提交的分支的实际名称,因此从那时起,Mercurial 就知道 D 位于分支 dev 或其他任何位置。 Git 使用完全不同的方案:Git 不知道也不关心提交的位置; Git 通过让名称记住 last 提交来查找提交,并按照两个系统的方式向后工作,按照连接提交的内部箭头。
因此,在 Mercurial 中,我们可能会说:
default: A--B--C
\
dev: D
每个提交都在与我们提交提交所在行对应的分支上。但在 Git 中,我们可能会切换到像这样绘制它们:
C <-- master
/
A--B
\
D <-- dev
即名称master标识提交C,名称dev标识提交D。提交A 和B 现在在两个 分支上。
此时,Mercurial 用户可能会说 Git 简直是疯了,许多人会同意他们的看法。但是 Mercurial 用户在这里并没有摆脱困境,因为我们也可以在 Mercurial 中这样做:我们可以将提交 D 放在分支 default 上,但仍然将 B 作为其父级。那就是:
A--B--C
default: \
D
在 Mercurial 中也有效!而在现代 Mercurial 中,我们可以设置两个 书签 来记住提交 C 和 D,我们的情况与 Git 中相同。
因此,在两个系统中了解提交图的工作原理很重要。由于 Mercurial 的分支系统更加死板,因此您通常可以在没有这种了解的情况下侥幸逃脱 - 但最终,您确实需要了解它。
现在,让我们看一下您的文字描述,然后绘制图表,因为重要的是 图表。
我从一个名为分支 A 的分支开始。我在分支 A 上创建了一个名为 feat1 的新分支,它正在添加一个新功能。我在 feat1 中进行了几次提交,然后将其合并回分支 A。
我想将在 feat1 分支中所做的所有更改合并到另一个分支中,我将其称为分支 B。但我不想将分支 A 中的所有内容合并到分支 B,只是在整个过程中所做的更改壮举1分支。我认为我可以使用一系列移植来完成此操作,我的 feat1 分支中的每个提交一个,但我认为应该有更好的方法。有没有标准的方法来完成这个?
这里少了一点,因为在 Mercurial 中,您几乎可以肯定真的以 default 开头,就像 Git 用户以 master 开头一样。让我们在default 上绘制至少一个提交,然后关注branchA 和feat1 和branchB。
default: A
\
branchA: B--C-...--M
\ /
feat1: D--E
提交M 是您的合并,通过运行hg checkout branchA; hg merge feat1 完成。
合并的工作方式是,它不是查看分支名称,而是查看提交图。在M 存在之前,图表显示为:
...--C--...
\
D--E
我在这里输入...,因为在C 之后可能有一些提交,例如F 或F-G-H 或其他。让我们假设有。对 Mercurial 的影响不如对 Git 重要,因为在 Git 中,这强制 Git 进行真正的合并而不是快进非合并,但它有助于说明如何合并作品。让我们只使用一个提交F:
...--C--F
\
D--E
为了实现合并,两个 VCS 在此时查看图表。他们采用当前提交(在本例中为 F)和请求的(“其他”或 --theirs)提交,在本例中为 E——并在图中向后搜索最佳共同祖先提交。从图中可以明显看出该提交:它是提交 C!
因此,此时,两个 VCS 都执行以下逻辑等效操作:
所以,既然你有了这个,让我们继续你的文本的下一部分:
我想将feat1分支中所做的所有更改合并到另一个分支中,我将调用分支B。
这个新分支branchB 并不是凭空出现的。您必须创建它。这里的方法在 Git 和 Mercurial 中有点不同,但最终结果是一样的……嗯,大部分是一样的。
在这两个系统中,如果该分支上没有提交,则该分支不能存在;但在 Git 中,任何提交都可以在 许多 分支上,因此我们可以通过将 branchB 指向提交 A 来使其存在。在 Mercurial 中,提交仅在 one 分支上,因此我们需要进行新的提交以使 branchB 存在。所以图片有点不同。
这是 Git 图片:
A <-- master, branchB
\
B--C--F---M <-- branchA
\ /
D--E <-- feat1
这是 Mercurial 的一个:
branchB: N
/
default: A
\
branchA: B--C--F---M
\ /
feat1: D--E
此时,我们可以在 Git 中进行无偿提交,只是为了使图片匹配(除了由于 Git 分支名称 move,我们将它们放在右侧,箭头指向图;Mercurial 分支名称是固定不变的,所以我们可以让它们位于左侧并永远标记它们的提交,只要我们的图无论如何都不会变得很大)。 Git 将写入新的提交 N,2 和 move 当前分支名称以指向新的提交。
但我不想将分支 A 中的所有内容合并到分支 B,只是合并在 feat1 分支过程中所做的更改。我认为我可以使用一系列移植来完成此操作,我的 feat1 分支中的每个提交一个,但我认为应该有更好的方法。有没有标准的方法来完成这个?
在 Git 和 Mercurial 中,实际上您必须复制提交。图是提交;您在feat1 上所做的提交D 和E 被卡在了它们所在的位置。 分支名称是移动(Git)还是固定固定(Mercurial)都没有关系,因为提交本身是不可变的。
在 Mercurial 中,hg graft 复制提交是正确的。您可以使用单个命令复制所有 feat1 提交。因为提交特定于一个特定的分支,所以很容易复制 all feat1 提交。
在 Git 中,git cherry-pick 命令复制提交。您也可以使用单个命令在此处复制您想要的所有提交,但是因为提交 D-E 位于 两个 分支上——它们同时位于 feat1 和上em> branchA 现在——你需要使用更复杂的说明符。 Git 方便的说明符(可能不是那么方便)使用图形操作:feat1~2..feat1 将同时指定提交 D 和 E,在这种情况下,branchA^..feat1 也是如此。 Mercurial 也可以做到这一点,但您在 Mercurial 中并不经常需要。
2请注意,我们必须git checkout branchB 并且可能使用git commit --allow-empty。 --allow-empty 标志告诉 Git 它应该进行新的提交,即使 index - 这是一个特定于 Git 的概念; Mercurial 没有索引——与当前提交完全匹配。 git checkout 步骤的副作用是将名称 HEAD 附加到分支名称,以便 Git 知道 哪个名称是当前分支。 (Mercurial 将当前分支名称记录在一个名为 dirstate 的隐藏数据结构中,与 Git 的索引不同,您不需要知道它。)
避免所有的复制
由于您的目标是避免使用hg graft 或git cherry-pick,让我们考虑如何实现这一目标。请注意,这并不总是可能的,即使它是可能的,有时也需要大量的远见和计划。但关键是:Git 和 Mercurial 都使用 graph 中的 merge base 来合并各种功能。
让我们从上面获取最终的 Mercurial 图表,其中两个 feat1 提交必须被嫁接才能影响提交 N:
branchB: N
/
default: A
\
branchA: B--C--F---M
\ /
feat1: D--E
现在,假设当我们去创建分支feat1,然后实现该功能时,我们事先知道我们希望能够只运行hg update branchA; hg merge feat1; hg update branchB; hg merge feat1。
要完成这项工作,我们必须让feat1 上的第一次提交在图中的较早提交之后。具体来说,它必须在一些提交之后,该提交是branchA 和 branchB 的提示的祖先。提交C 距离branchB 太远了:它不是branchA 上提交N 的祖先。
理想的提交是branchA 和branchB 第一次重新组合在一起的那个。也就是说,它是其哈希 ID Git 将通过运行 git merge-base branchA branchB 拼出的提交:最近(在图形方面)提交 是两个提示的祖先提交。
请注意,因为我们还没有开始feat1,所以我们现在必须实际拥有的只是:
branchB: N
/
default: A
\
branchA: B--C
Mercurial 没有特别方便的工具来查找所需的哈希 ID,3 但由于 Mercurial 提交已牢固地钉在其分支上,因此通常非常明显。在这种情况下,这是提交A,这是default 上的最后一次提交。所以我们可以:
hg update default
hg branch feat1
然后编写并提交我们的代码:
branchB: N
/
default: A
|\
feat1: | D--E
\
branchA: B--C
同时提交F 像以前一样进入branchA,所以我们hg update branchB 并发现我们现在有:
branchB: N
/
default: A
|\
feat1: | D--E
\
branchA: B--C--F
我们运行hg merge feat1 并得到:
branchB: N
/
default: A
|\
feat1: | D------E
\ \
branchA: B--C--F--M
其中M 的第一个父级是F,第二个父级是E,就像以前一样。但现在我们可以hg update branchB; hg merge feat1。 N 和 E 的合并基是 commit A; Mercurial 将A 与N 进行比较以了解我们做了什么,将A 与E 进行比较以了解他们做了什么。 Mercurial 构建新的合并提交 O 并提交它:
branchB: N----------O
/ /
default: A /
|\ /
feat1: | D------E
\ \
branchA: B--C--F--M
除了使用不同的命令和分支标签本身移动这一事实之外,Git 中的过程是相同的。
3您可以使用-r 选项和面向图形的修订说明符找到提交。从数字上讲,您想要满足ancestor(tip-of-branchA) & ancestor(tip-of-branchB) 的最后一次提交。更新到该提交,然后创建 feat1 提交。
在 Git 中,要查找哈希,只需运行 git merge-base branchA branchB。找到哈希后,将新的分支名称指向该哈希 ID,检查该分支,然后开始提交。