这篇文章是我们"高级Git "系列的一部分。请务必在Twitter上关注我们,或者注册我们的新闻通讯,以了解下一篇文章的内容。
大多数开发者都明白,在 Git 中使用分支是很重要的。事实上,我已经写了一整篇关于 Git 分支策略的文章,解释了 Git 强大的分支模型,不同类型的分支,以及两种最常见的分支工作流程。总结一下:为你的工作拥有独立的容器,即分支,是非常有用的,也是使用版本控制系统的主要原因之一。
在这篇文章中,我们要看一下整合分支。如何将新的代码回到现有的开发线中?有不同的方法来实现这一点。我们的 "高级Git "系列的第五集讨论了在Git中整合变化,即合并和重定位。
在我们深入讨论之前,重要的是要理解这两个命令--git merge和git rebase--解决的是同一个问题。它们都是将一个 Git 分支的改动整合到另一个分支,只是方式不同。让我们从合并开始,看看它们究竟是如何工作的。
了解合并
要将一个分支合并到另一个分支,可以使用 git merge 命令。假设你想把主分支*(branch-A*)合并到特性分支*(branch-B*),你可以输入这样的内容:
$ git checkout branch-B
$ git merge branch-A
这样,Git 就会在你当前的工作分支(本例中为branch-B )创建一个新的合并提交,将两个分支的历史连接起来。为了完成这个任务,Git 寻找了三个提交:
- 第一个是 "共同祖先提交"。如果你追踪一个项目中两个分支的历史,它们总是至少有一个共同提交。在这一点上,两个分支的内容是一样的。在这之后,它们的演化就不同了。
- 另外两个有趣的提交是每个分支的端点,即它们的当前状态。记住,整合的目的是将两个分支的当前状态结合起来。所以它们的最新修订版当然很重要。
把这三个提交合并起来,就能实现我们的目标:

诚然,这是一个简化的场景--两个分支中的一个(branch-A )自创建以来没有任何新的提交,这在大多数软件项目中是很不可能的。因此,在这个例子中,它的最后一次提交也是共同祖先。
在这种情况下,整合是非常简单的。Git 只需将所有来自branch-B 的新提交添加到共同祖先的提交之上。在 Git 中,这种最简单的整合方式被称为 "快进式 "合并。这样两个分支就共享完全相同的历史(不需要额外的 "合并提交"):

但在大多数情况下,两个分支会以不同的提交来推进。所以让我们举一个更现实的例子:

为了进行整合,Git 必须创建一个新的提交,其中包含所有的修改,并照顾到各分支之间的差异--这就是我们所说的合并提交。
人类提交和合并提交
通常情况下,一个提交是由人精心创建的。它是一个有意义的单元,只包括相关的修改,再加上一个 有意义的提交信息,提供背景和注释。
现在,合并提交有点不同:它不是由开发人员创建的,而是由 Git自动创建的。而且,合并提交不一定包含 "相关修改的语义集合"。相反,它的目的只是连接两个(或更多)分支并打上结。

如果你想理解这样的自动合并操作,你必须看一下所有分支的历史和它们各自的提交历史。
与 rebases 集成
在我们谈论rebasing之前,让我明确一点:rebase不比merge好,也不比merge坏,它只是不同而已。你可以通过合并分支过上幸福的(Git)生活,甚至不用考虑回溯的问题。不过,了解rebase的作用,以及了解它的利弊,确实有帮助。也许你会在项目中遇到一个问题,那就是回溯可能会有帮助......
好了,我们开始吧!还记得我们刚刚谈到的自动合并提交吗?有些人并不热衷于这些,他们更喜欢不做这些。此外,有些开发者喜欢他们的项目历史看起来像一条直线--没有任何迹象表明它曾在某个时候被分割成多个分支,即使这些分支已经被整合。这基本上就是 Git 重置时的情况。

重置、一步一步来
让我们一步步走过一次重命名操作。场景与前面的例子相同,起点是这样的。

我们想把branch-B 上的改动整合到branch-A 上 - 但要通过重命名,而不是合并。实际的 Git 命令非常简单。
$ git checkout branch-A
$ git rebase branch-B
类似于git merge 命令,你告诉 Git 你想整合哪个分支。让我们来看看幕后的情况...

在这第一步,Git 将 "移除 "分支 A 上发生在共同祖先提交之后的所有提交。别担心,它不会把它们扔掉:你可以把这些提交看作是 "停放 "或暂时保存在一个安全的地方。

在第二步,Git 会应用来自branch-B 的新提交。此时,暂时来说,两个分支看起来是完全一样的。

最后,这些 "停放 "的提交(来自branch-A 的新提交)被包括在内。由于它们被放置在来自branch-B 的集成提交之上,所以它们被重新定位。
这样一来,项目历史看起来就像是在一条直线上进行的开发。没有一个合并提交包含了所有的合并修改,而原始的提交结构被保留了下来。
重置的潜在隐患
还有一件事--这一点对了解Git重命名很重要--就是它重写了提交历史。再看一下我们的上一张图。提交C3*有一个星号。虽然C3*的内容与C3相同,但它实际上是一个不同的提交。为什么呢?因为它在重构后有一个新的父提交。在重写之前,C1 是父提交。重置之后,它的父级是C4--它被重置到C4中。
一个提交只有少数几个重要的属性,比如作者、日期、变更集和它的父提交。改变任何这些信息都会产生一个全新的提交,并有一个新的SHA-1哈希ID。
对于尚未发布的提交,重写历史是没有问题的。但如果你改写已经推送到远程仓库的提交,就会有麻烦了。也许别人的工作是基于C3的原始提交,而现在它突然不存在了......
为了避免麻烦,这里有一个使用 rebase 的简单规则。永远不要在公共分支上使用 rebase,也就是在已经被推送到远程仓库的提交上使用。相反,在整合到团队共享分支之前,使用git rebase 来清理本地提交历史。
集成就是一切
最后,合并和重命名都是有用的 Git 策略,这取决于你想达到什么目的。合并是一种非破坏性的,因为合并不会改变现有的历史。另一方面,重放可以避免不必要的合并提交,从而帮助清理项目历史。只要记住不要在公共分支中这样做,以免给其他开发者带来麻烦。
祝你合并和重命名愉快--我们的 "高级Git "系列的下一部分很快就会见到你。