操纵提交历史,同时避免分支之间的合并冲突
manipulating commit history while avoiding merge conflicts between branches
我们的团队第一次开始使用 git。我们有 3 个分支:master、a 和 b。 Master 有五个提交(commit1 到 commit5),而分支 a 和 b 也包含所有五个提交加上它们自己的一些新提交。 a和b以后会合并为master
最近,我发现提交 2 和 3 不属于 master。它应该属于一个。 Master 应该只有提交 1、4 和 5。提交 2 和 3 稍后将合并。这是我们还没有创建单独的分支的时候。这是一个问题,因为 a 和 b 是从 master 分支出来的,因此前 5 次提交是相同的。
有没有办法从 master 和 b 中删除提交 2 和 3,同时避免稍后当我们将 a 合并到 master 时发生合并冲突?
简答一般是"no"。然而,由于提交相对较少,而且您的团队可能足够小,所以有一种方法可以公平、干净地处理这个问题,还有几种方法有点混乱但可能就足够了。 (另外,一个简短的旁注:有更快的方法来完成我在下面描述的几乎所有事情,使用个人的强大工具版本,one-commit-at-a-time 方法。我将把它们留给 space原因。)
TL;DR 摘要
我可能会重写历史记录,因为存储库很小而且您的团队可能有能力处理它。 (如果每个人都可以一起工作几个小时左右,则尤其如此。)在一个更大的项目中,我可能会选择我在下面展示的最后一个(混合)替代方案——或者,在许多情况下,忽略这个问题,因为我标记为 B
和 C
的提交可能不会造成太多困扰。但所有这些都是价值 judgments,不一定有 "correct" 答案。
长的部分
主要技巧是从绘制提交开始。请记住,在 Git 中,提交由其丑陋的大哈希 ID 标识,并且每个提交记录其 parent 提交的哈希 ID。这意味着总是 "point backwards" 提交,实际上 Git 总是向后工作。每个提交还包含整个源的完整快照:每当您将提交视为 更改 时,您就是通过 Git 从该提交向后查看它的parent,然后比较两个快照。
每个提交的哈希 ID 非常依赖于(通过加密哈希)关于提交的一切:作者和提交者姓名和电子邮件地址和时间戳、源快照、提交消息和 parent 也提交哈希 ID。因此,如果您尝试更改 关于提交的任何内容,您将获得一个新的、不同的哈希 ID。
不过,我不会写出丑陋的大哈希 ID,而是每次提交都使用一个大写字母(并且 运行 在 26 次提交后输出...)。你或多或少有这个(我只是猜测两个分支中的每一个都有两个提交):
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
(请注意,像 master
这样的分支名称充当可移动指针,指向分支上的 last 提交。通过使新提交点回到当前提示,然后移动分支名称以指向新提交。Git 现在找到所有曾经在分支上的提交,因为它从新提交开始并向后工作,并且后退的第一步将它带到旧提示。)
方法一:大量历史重写(不是那么大量)
您喜欢拥有但不能完全拥有的是:
H--I <-- branch-b
/
A--D--E <-- master
\
B--C--F--G <-- branch-a
无法实现此图,因为 D
的哈希 ID 表示 parent 是 A
的提交,其源快照是 [=38= 中的快照] ] 现在,类似于 C
.
中的快照
我们可以做的是提交,我们称它为D'
,A
作为它的parent,并且采用 change-from-C
-to-D
并将更改应用于 A
中的快照。我们通过使用 git checkout
直接检出提交 A
(作为 "detached HEAD"),然后使分支名称 new-master
指向它,使用:
$ git checkout <hash of A>
$ git checkout -b new-master
或一步使用:
$ git checkout -b new-master <hash of A>
给予:
A <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
现在我们有了这个新命名的分支,我们可以使用 git cherry-pick
来 复制 提交 D
:
$ git cherry-pick <hash of D>
Cherry-pick 将比较 D
与其 parent C
,并使用结果更改将更改应用到我们现在的位置(HEAD = new-master
) 并进行新的提交 D'
:
A--D' <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
现在我们有 D'
我们也可以 cherry-pick E
:
A--D'-E' <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
请注意这与您希望拥有的东西有多少相似之处。图的下半部分我们先停止绘制,但记住它还在,现在创建new-branch-a
并上去:
$ git checkout -b new-branch-a
A--D'-E' <-- new-master, new-branch-a (HEAD)
然后是 cherry-pick B
、C
、F
和 G
:
$ git cherry-pick <hash of B>
A--D'-E' <-- new-master
\
B' <-- new-branch-a (HEAD)
(对剩余的三个提交重复):
A--D'-E' <-- new-master
\
B'-C'-F'-G' <-- new-branch-a (HEAD)
现在我们回到 new-master
并创建 new-branch-b
和 cherry-pick 所有旧 branch-b
的提交:
H'-I' <-- new-branch-b (HEAD)
/
A--D'-E' <-- new-master
\
B'-C'-F'-G' <-- new-branch-a
我们现在要做的就是删除原来的三个分支名称,将这三个分支重命名为master
,branch-a
, 和 branch-b
, 任何不记得原始 哈希 ID 的人都会认为历史已经以某种方式神奇地被改写了。 历史真的没有' 实际上根本没有改变,每个拥有原始提交和原始名称的人仍然拥有原始历史——这就是它变得有点毛茸茸的地方,因为 Git 是分布式的。
每个拥有存储库克隆的人都拥有所有原始提交及其原始哈希 ID。 这意味着您团队的每个成员在他们自己的 Git 存储库中都有 origin/master
、origin/branch-a
和 origin/branch-b
形式的名称,这些名称会记住提交的哈希 ID E
、G
和 I
,它们具有您 不 想要的形式的所有原始提交——原始历史,而不是重写的历史。
他们必须谨慎对待 他们 的任何工作,这些工作建立在 E
、G
或 I
和 [=318= 的基础上]transplant 有效(使用 git cherry-pick
或等价物),因此它现在从 E'
、G'
或 I'
增长。如果你的团队足够小,并且每个人都通过会议讨论如何处理这个历史重写,那么这对他们来说可能就像重写对你一样容易。
棘手的部分是,如果他们 不 小心,他们可以 re-introduce 所有原始提交回到他们自己的历史记录中(通过让他们的分支名称指向到原件,或提交 parent 指向原件)。然后,当他们将他们的历史与您重写的历史合并时,他们 re-introduce 所有原始历史。这种事情就是为什么 "history rewriting" 通常是一个坏主意:只需要一个人就可以把它弄错 re-introduce "wrong" 历史。
(历史重写的另一个问题是合并提交。你不能 cherry-pick 合并提交来创建新的合并。你可以做一些事情,但所有的选择都有问题。在你的不过,在特殊情况下,您还没有合并提交,所以您在这里很安全。)
方法二:还原
您无需重写历史,只需向历史中添加内容即可。从某种意义上说,这更安全:Git 是为了在旧提交之上添加新提交而构建的,因此所有这些都是自然发生的,无需任何其他人做任何额外的工作。缺点是丑陋,而且当你去git merge
其他分支时,每次合并都需要额外的工作。
再次强调,理解这一点的关键是从绘制图形开始(或至少绘制其中的一部分,而您的图形足够小,可以绘制所有图形):
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
我们现在想要 "undo" 提交 C
和 B
,按照 branch-a
和 branch-b
的顺序。执行此操作的 Git 命令是 git revert
。还原,可能应该被称为 "backout",其工作方式与 git cherry-pick
.
大致相同
这个撤销可能有冲突(但是如果是的话,注意方法一中的历史重写也会有冲突)。我们将从还原 C
开始,它比较 B
与 C
以查看发生了什么变化。然后它与我们现在所处的位置相反。让我们开始 branch-a
,即提交 G
,并执行 backing-out 从 C:
撤消这些更改
$ git checkout branch-a
$ git revert <hash of C>
与 cherry-pick 一样,如果一切顺利,还原将进行新的提交,提交消息显示 "this reverts ":
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G--J <-- branch-a (HEAD)
(我没有聪明的方法来写一个颠倒的 C 或其他东西......好吧,我知道,例如 Ↄ
,但我担心它可能不会在某些 fonts/browsers.) 现在我们还必须退出 B
,使用第二个 git revert
命令:
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G--J--K <-- branch-a (HEAD)
现在我们通过检查并恢复两次来在 branch-b
上重复这项工作。 (作为一个小 short-cut,我们可能会注意到 git revert
可以还原多个提交:我们可以 select 在命令行上同时提交。但重要的是我们将两者都还原,在在 C
的情况下 back-to-front 的正确顺序取决于 B
。)现在的结果是:
H--I--L--M <-- branch-b (HEAD)
/
A--B--C--D--E <-- master
\
F--G--J--K <-- branch-a
其中所有四个新提交都是 backing-out C
的回退,然后是 B
.
我们也可以在 master
上取消 C
和 B
,但这很愚蠢,因为我们可能想立即将它们放回去。这将向 E
添加四次提交,最终源快照将与 E
中的快照匹配。它对未来的合并也没有帮助,因为两个分支重新加入 master
分支的点仍然是提交 E
.
方法2设置的陷阱
进行所有这些恢复的缺点会在稍后出现,当您将分支合并回 master
时。要了解缺点,请回顾一下我们已有的绘图(或者如果您已删除它,请重新绘制它:-))。从 branch-a
和 master
的提示开始,向后(向左)工作到您可以在 both 分支上的第一个提交。那是提交 E
,所以提交 E
是合并基础。
当我们稍后 运行 git checkout master && git merge branch-a
时,Git 将查看 branch-a
的尖端和 master
的尖端并向后工作以找到 E
。无论我们向 branch-a
和 master
添加了多少提交,只要我们还没有合并,我们最终会回到 E
这里。
Git 将在这一点上 运行 两个 git diff
s(无论如何都有效):
git diff --find-renames E <tip of master> # what we changed
git diff --find-renames E <tip of branch-a> # what they changed
无论我们在 master
中做了什么,Git 都会记录哪些文件受到影响(以及这些文件的哪些行)。无论他们在 branch-a
做了什么,Git 记录哪些文件受到影响(以及这些文件的哪些行)。请注意 "we",在 master
中,还没有对 任何 受 B
和 C
呢。即使我们还原并重新 cherry-pick 它们,我们仍然没有对它们做任何事情,因为 Git 不查看每个单独的提交,它只查看最终产品。
另一方面,他们在 branch-a
中......好吧,他们 恢复了 东西,所以他们有 backing-out 里面的东西B
和 C
在 他们的 提示中,与提交 E
相比。所以 Git 认为撤销 B
和 C
中的内容很重要。因此,合并 branch-a
将退出 B
和 C
,正如我们非常清楚地告诉 Git 很重要一样。
那么陷阱就在于,在合并 branch-a
之后,我们必须记住将这两个提交放回去,例如,通过 cherry-picking 它们。或者,等效地,就在合并 branch-a
之前,我们可以通过 cherry-picking 将两个提交放回 branch-a
中。现在是第二个差异,看看他们在 branch-a
中做了什么, 不会 包括退出 B
和 C
。
此陷阱与 branch-b
重复,其中包含 其 内容(与提交 E
相比)退出 B
和 C
。 请注意,我们很可能不会在这里遇到任何冲突;我们必须记住re-add每次合并时的提交。没有冲突就是陷阱!
混合体:退出、合并和 cherry-pick
现在,正如我们在上面看到的,避免重写历史的问题是我们为后面的 git merge
命令设置了陷阱。但是,如果相反,我们违反了一些通常的 good-practice 规则并提前合并怎么办?
让我们再回到最初的图表:
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
假设此时我们git checkout master && git revert <C> && git revert <B>
得到这个:
H--I <-- branch-b
/
A--B--C--D--E--J--K <-- master (HEAD)
\
F--G <-- branch-a
master
分支提示提交 K
现在看起来像——也就是说,它的源快照——我们希望 E
在我们制作两个时看起来像侧枝。所以现在我们可以 merge master 进入 两个分支。有时这不是个好主意,但我们会谨慎行事:
$ git checkout branch-a && git merge master
# write a commit message explaining that this is a special backout merge
(and repeat for branch-b)
现在我们的图表如下所示:
H--I--M <-- branch-b (HEAD)
/ /
A--B--C--D--E--J--K <-- master
\ \
F--G--L <-- branch-a
现在我们立即返回 master
并通过 cherry-pick 将 B
和 C
带回来(或还原它们的还原,这做同样的事情) .我们称 revert-of-revert 或 re-picks B'
和 C'
因为它们很像 B
和 C
:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C' <-- master (HEAD)
\ \
F--G--L <-- branch-a
提交 L
,branch-a
顶端的合并,现在指向 G
和 K
(按此顺序)。我们不会担心 branch-b
因为它基本上是一样的。提交 L
具有 撤消 B
和 C
.
的效果
让我们想象一下,现在,我们及时前进,人们在 branch-a
上做出一些承诺:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C' <-- master
\ \
F--G--L--N--O--P <-- branch-a
现在是 git checkout master && git merge branch-a
的时候了。 git merge
将选择哪个提交作为合并基础?
向后访问链接以查看哪些提交在哪些分支上。 C'
在 master
上(仅); B'
在 master 上(仅); K
在两个分支上。提交 P
、O
、N
和 L
仅在 branch-a
上,但 L
返回到 K
。 合并基础现已提交 K
。
Git 现在会将 K
的内容(没有 B
和 C
的变化)与 C'
的内容进行比较: master 上的更改是恢复两个提交。 我们就是这么做的。然后 Git 会将 K
的内容与 P
的内容进行比较,看看他们做了什么。这将 not 包括退出 B
和 C
(因为那是 G
与 L
所做的,所以 K
vs L
没有)。如果我们遇到任何冲突,它们将不会包含 backing-out 部分。我们可以做一个正常的合并,就像我们做任何其他合并一样。最后的结果是:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C'-----Q <-- master (HEAD)
\ \ /
F--G--L--N--O--P <-- branch-a
其中 Q
是从 K
加上我们的更改(re-introduce B
和 C
)加上他们的更改(引入 F
+G
+N
+O
+P
).
我们的团队第一次开始使用 git。我们有 3 个分支:master、a 和 b。 Master 有五个提交(commit1 到 commit5),而分支 a 和 b 也包含所有五个提交加上它们自己的一些新提交。 a和b以后会合并为master
最近,我发现提交 2 和 3 不属于 master。它应该属于一个。 Master 应该只有提交 1、4 和 5。提交 2 和 3 稍后将合并。这是我们还没有创建单独的分支的时候。这是一个问题,因为 a 和 b 是从 master 分支出来的,因此前 5 次提交是相同的。
有没有办法从 master 和 b 中删除提交 2 和 3,同时避免稍后当我们将 a 合并到 master 时发生合并冲突?
简答一般是"no"。然而,由于提交相对较少,而且您的团队可能足够小,所以有一种方法可以公平、干净地处理这个问题,还有几种方法有点混乱但可能就足够了。 (另外,一个简短的旁注:有更快的方法来完成我在下面描述的几乎所有事情,使用个人的强大工具版本,one-commit-at-a-time 方法。我将把它们留给 space原因。)
TL;DR 摘要
我可能会重写历史记录,因为存储库很小而且您的团队可能有能力处理它。 (如果每个人都可以一起工作几个小时左右,则尤其如此。)在一个更大的项目中,我可能会选择我在下面展示的最后一个(混合)替代方案——或者,在许多情况下,忽略这个问题,因为我标记为 B
和 C
的提交可能不会造成太多困扰。但所有这些都是价值 judgments,不一定有 "correct" 答案。
长的部分
主要技巧是从绘制提交开始。请记住,在 Git 中,提交由其丑陋的大哈希 ID 标识,并且每个提交记录其 parent 提交的哈希 ID。这意味着总是 "point backwards" 提交,实际上 Git 总是向后工作。每个提交还包含整个源的完整快照:每当您将提交视为 更改 时,您就是通过 Git 从该提交向后查看它的parent,然后比较两个快照。
每个提交的哈希 ID 非常依赖于(通过加密哈希)关于提交的一切:作者和提交者姓名和电子邮件地址和时间戳、源快照、提交消息和 parent 也提交哈希 ID。因此,如果您尝试更改 关于提交的任何内容,您将获得一个新的、不同的哈希 ID。
不过,我不会写出丑陋的大哈希 ID,而是每次提交都使用一个大写字母(并且 运行 在 26 次提交后输出...)。你或多或少有这个(我只是猜测两个分支中的每一个都有两个提交):
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
(请注意,像 master
这样的分支名称充当可移动指针,指向分支上的 last 提交。通过使新提交点回到当前提示,然后移动分支名称以指向新提交。Git 现在找到所有曾经在分支上的提交,因为它从新提交开始并向后工作,并且后退的第一步将它带到旧提示。)
方法一:大量历史重写(不是那么大量)
您喜欢拥有但不能完全拥有的是:
H--I <-- branch-b
/
A--D--E <-- master
\
B--C--F--G <-- branch-a
无法实现此图,因为 D
的哈希 ID 表示 parent 是 A
的提交,其源快照是 [=38= 中的快照] ] 现在,类似于 C
.
我们可以做的是提交,我们称它为D'
,A
作为它的parent,并且采用 change-from-C
-to-D
并将更改应用于 A
中的快照。我们通过使用 git checkout
直接检出提交 A
(作为 "detached HEAD"),然后使分支名称 new-master
指向它,使用:
$ git checkout <hash of A>
$ git checkout -b new-master
或一步使用:
$ git checkout -b new-master <hash of A>
给予:
A <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
现在我们有了这个新命名的分支,我们可以使用 git cherry-pick
来 复制 提交 D
:
$ git cherry-pick <hash of D>
Cherry-pick 将比较 D
与其 parent C
,并使用结果更改将更改应用到我们现在的位置(HEAD = new-master
) 并进行新的提交 D'
:
A--D' <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
现在我们有 D'
我们也可以 cherry-pick E
:
A--D'-E' <-- new-master (HEAD)
\
B H--I <-- branch-b
\ /
C--D--E <-- master
\
F--G <-- branch-a
请注意这与您希望拥有的东西有多少相似之处。图的下半部分我们先停止绘制,但记住它还在,现在创建new-branch-a
并上去:
$ git checkout -b new-branch-a
A--D'-E' <-- new-master, new-branch-a (HEAD)
然后是 cherry-pick B
、C
、F
和 G
:
$ git cherry-pick <hash of B>
A--D'-E' <-- new-master
\
B' <-- new-branch-a (HEAD)
(对剩余的三个提交重复):
A--D'-E' <-- new-master
\
B'-C'-F'-G' <-- new-branch-a (HEAD)
现在我们回到 new-master
并创建 new-branch-b
和 cherry-pick 所有旧 branch-b
的提交:
H'-I' <-- new-branch-b (HEAD)
/
A--D'-E' <-- new-master
\
B'-C'-F'-G' <-- new-branch-a
我们现在要做的就是删除原来的三个分支名称,将这三个分支重命名为master
,branch-a
, 和 branch-b
, 任何不记得原始 哈希 ID 的人都会认为历史已经以某种方式神奇地被改写了。 历史真的没有' 实际上根本没有改变,每个拥有原始提交和原始名称的人仍然拥有原始历史——这就是它变得有点毛茸茸的地方,因为 Git 是分布式的。
每个拥有存储库克隆的人都拥有所有原始提交及其原始哈希 ID。 这意味着您团队的每个成员在他们自己的 Git 存储库中都有 origin/master
、origin/branch-a
和 origin/branch-b
形式的名称,这些名称会记住提交的哈希 ID E
、G
和 I
,它们具有您 不 想要的形式的所有原始提交——原始历史,而不是重写的历史。
他们必须谨慎对待 他们 的任何工作,这些工作建立在 E
、G
或 I
和 [=318= 的基础上]transplant 有效(使用 git cherry-pick
或等价物),因此它现在从 E'
、G'
或 I'
增长。如果你的团队足够小,并且每个人都通过会议讨论如何处理这个历史重写,那么这对他们来说可能就像重写对你一样容易。
棘手的部分是,如果他们 不 小心,他们可以 re-introduce 所有原始提交回到他们自己的历史记录中(通过让他们的分支名称指向到原件,或提交 parent 指向原件)。然后,当他们将他们的历史与您重写的历史合并时,他们 re-introduce 所有原始历史。这种事情就是为什么 "history rewriting" 通常是一个坏主意:只需要一个人就可以把它弄错 re-introduce "wrong" 历史。
(历史重写的另一个问题是合并提交。你不能 cherry-pick 合并提交来创建新的合并。你可以做一些事情,但所有的选择都有问题。在你的不过,在特殊情况下,您还没有合并提交,所以您在这里很安全。)
方法二:还原
您无需重写历史,只需向历史中添加内容即可。从某种意义上说,这更安全:Git 是为了在旧提交之上添加新提交而构建的,因此所有这些都是自然发生的,无需任何其他人做任何额外的工作。缺点是丑陋,而且当你去git merge
其他分支时,每次合并都需要额外的工作。
再次强调,理解这一点的关键是从绘制图形开始(或至少绘制其中的一部分,而您的图形足够小,可以绘制所有图形):
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
我们现在想要 "undo" 提交 C
和 B
,按照 branch-a
和 branch-b
的顺序。执行此操作的 Git 命令是 git revert
。还原,可能应该被称为 "backout",其工作方式与 git cherry-pick
.
这个撤销可能有冲突(但是如果是的话,注意方法一中的历史重写也会有冲突)。我们将从还原 C
开始,它比较 B
与 C
以查看发生了什么变化。然后它与我们现在所处的位置相反。让我们开始 branch-a
,即提交 G
,并执行 backing-out 从 C:
$ git checkout branch-a
$ git revert <hash of C>
与 cherry-pick 一样,如果一切顺利,还原将进行新的提交,提交消息显示 "this reverts ":
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G--J <-- branch-a (HEAD)
(我没有聪明的方法来写一个颠倒的 C 或其他东西......好吧,我知道,例如 Ↄ
,但我担心它可能不会在某些 fonts/browsers.) 现在我们还必须退出 B
,使用第二个 git revert
命令:
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G--J--K <-- branch-a (HEAD)
现在我们通过检查并恢复两次来在 branch-b
上重复这项工作。 (作为一个小 short-cut,我们可能会注意到 git revert
可以还原多个提交:我们可以 select 在命令行上同时提交。但重要的是我们将两者都还原,在在 C
的情况下 back-to-front 的正确顺序取决于 B
。)现在的结果是:
H--I--L--M <-- branch-b (HEAD)
/
A--B--C--D--E <-- master
\
F--G--J--K <-- branch-a
其中所有四个新提交都是 backing-out C
的回退,然后是 B
.
我们也可以在 master
上取消 C
和 B
,但这很愚蠢,因为我们可能想立即将它们放回去。这将向 E
添加四次提交,最终源快照将与 E
中的快照匹配。它对未来的合并也没有帮助,因为两个分支重新加入 master
分支的点仍然是提交 E
.
方法2设置的陷阱
进行所有这些恢复的缺点会在稍后出现,当您将分支合并回 master
时。要了解缺点,请回顾一下我们已有的绘图(或者如果您已删除它,请重新绘制它:-))。从 branch-a
和 master
的提示开始,向后(向左)工作到您可以在 both 分支上的第一个提交。那是提交 E
,所以提交 E
是合并基础。
当我们稍后 运行 git checkout master && git merge branch-a
时,Git 将查看 branch-a
的尖端和 master
的尖端并向后工作以找到 E
。无论我们向 branch-a
和 master
添加了多少提交,只要我们还没有合并,我们最终会回到 E
这里。
Git 将在这一点上 运行 两个 git diff
s(无论如何都有效):
git diff --find-renames E <tip of master> # what we changed
git diff --find-renames E <tip of branch-a> # what they changed
无论我们在 master
中做了什么,Git 都会记录哪些文件受到影响(以及这些文件的哪些行)。无论他们在 branch-a
做了什么,Git 记录哪些文件受到影响(以及这些文件的哪些行)。请注意 "we",在 master
中,还没有对 任何 受 B
和 C
呢。即使我们还原并重新 cherry-pick 它们,我们仍然没有对它们做任何事情,因为 Git 不查看每个单独的提交,它只查看最终产品。
另一方面,他们在 branch-a
中......好吧,他们 恢复了 东西,所以他们有 backing-out 里面的东西B
和 C
在 他们的 提示中,与提交 E
相比。所以 Git 认为撤销 B
和 C
中的内容很重要。因此,合并 branch-a
将退出 B
和 C
,正如我们非常清楚地告诉 Git 很重要一样。
那么陷阱就在于,在合并 branch-a
之后,我们必须记住将这两个提交放回去,例如,通过 cherry-picking 它们。或者,等效地,就在合并 branch-a
之前,我们可以通过 cherry-picking 将两个提交放回 branch-a
中。现在是第二个差异,看看他们在 branch-a
中做了什么, 不会 包括退出 B
和 C
。
此陷阱与 branch-b
重复,其中包含 其 内容(与提交 E
相比)退出 B
和 C
。 请注意,我们很可能不会在这里遇到任何冲突;我们必须记住re-add每次合并时的提交。没有冲突就是陷阱!
混合体:退出、合并和 cherry-pick
现在,正如我们在上面看到的,避免重写历史的问题是我们为后面的 git merge
命令设置了陷阱。但是,如果相反,我们违反了一些通常的 good-practice 规则并提前合并怎么办?
让我们再回到最初的图表:
H--I <-- branch-b
/
A--B--C--D--E <-- master
\
F--G <-- branch-a
假设此时我们git checkout master && git revert <C> && git revert <B>
得到这个:
H--I <-- branch-b
/
A--B--C--D--E--J--K <-- master (HEAD)
\
F--G <-- branch-a
master
分支提示提交 K
现在看起来像——也就是说,它的源快照——我们希望 E
在我们制作两个时看起来像侧枝。所以现在我们可以 merge master 进入 两个分支。有时这不是个好主意,但我们会谨慎行事:
$ git checkout branch-a && git merge master
# write a commit message explaining that this is a special backout merge
(and repeat for branch-b)
现在我们的图表如下所示:
H--I--M <-- branch-b (HEAD)
/ /
A--B--C--D--E--J--K <-- master
\ \
F--G--L <-- branch-a
现在我们立即返回 master
并通过 cherry-pick 将 B
和 C
带回来(或还原它们的还原,这做同样的事情) .我们称 revert-of-revert 或 re-picks B'
和 C'
因为它们很像 B
和 C
:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C' <-- master (HEAD)
\ \
F--G--L <-- branch-a
提交 L
,branch-a
顶端的合并,现在指向 G
和 K
(按此顺序)。我们不会担心 branch-b
因为它基本上是一样的。提交 L
具有 撤消 B
和 C
.
让我们想象一下,现在,我们及时前进,人们在 branch-a
上做出一些承诺:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C' <-- master
\ \
F--G--L--N--O--P <-- branch-a
现在是 git checkout master && git merge branch-a
的时候了。 git merge
将选择哪个提交作为合并基础?
向后访问链接以查看哪些提交在哪些分支上。 C'
在 master
上(仅); B'
在 master 上(仅); K
在两个分支上。提交 P
、O
、N
和 L
仅在 branch-a
上,但 L
返回到 K
。 合并基础现已提交 K
。
Git 现在会将 K
的内容(没有 B
和 C
的变化)与 C'
的内容进行比较: master 上的更改是恢复两个提交。 我们就是这么做的。然后 Git 会将 K
的内容与 P
的内容进行比较,看看他们做了什么。这将 not 包括退出 B
和 C
(因为那是 G
与 L
所做的,所以 K
vs L
没有)。如果我们遇到任何冲突,它们将不会包含 backing-out 部分。我们可以做一个正常的合并,就像我们做任何其他合并一样。最后的结果是:
H--I--M <-- branch-b
/ /
A--B--C--D--E--J--K--B'-C'-----Q <-- master (HEAD)
\ \ /
F--G--L--N--O--P <-- branch-a
其中 Q
是从 K
加上我们的更改(re-introduce B
和 C
)加上他们的更改(引入 F
+G
+N
+O
+P
).