本文是我们 “高级 Git” 系列的一部分。请务必关注我们的 Twitter 或 订阅我们的新闻通讯 以了解下一篇文章!
大多数开发人员都了解在 Git 中使用分支的重要性。事实上,我已经写了一篇关于Git 中的分支策略的文章,解释了 Git 强大的分支模型、不同类型的分支以及两种最常见的分支工作流程。概括地说:为您的工作创建独立的容器,即分支,非常有帮助,也是使用版本控制系统的主要原因之一。
在这篇文章中,我们将研究集成分支。如何将新代码重新整合到现有的开发线上?有不同的方法可以实现这一点。我们“高级 Git”系列的第五集讨论了在 Git 中集成更改,即合并和变基。
在我们深入了解细节之前,务必理解这两个命令——git merge 和 git rebase——都解决了同一个问题。它们将一个 Git 分支中的更改集成到另一个分支中;只是实现方式不同。让我们从合并开始,以及它们是如何工作的。
高级 Git 系列
- 第 1 部分: 在 Git 中创建完美的提交
- 第 2 部分: Git 中的分支策略
- 第 3 部分: 通过拉取请求实现更好的协作
- 第 4 部分: 合并冲突
- 第 5 部分: Rebase 与 Merge(您现在所处位置!)
- 第 6 部分: 交互式变基
- 第 7 部分: 在 Git 中挑选提交
- 第 8 部分: 使用 Reflog 恢复丢失的提交
理解合并
要将一个分支合并到另一个分支中,可以使用 git merge 命令。假设您在一个分支 branch-B 上有一些新的提交,现在您想将此分支合并到另一个分支 branch-A 中。为此,您可以键入以下内容
$ git checkout branch-A
$ git merge branch-B
结果,Git 在您当前的工作分支(此示例中的branch-A
)中创建一个新的合并提交,连接两个分支的历史记录。为了实现这一点,Git 会查找三个提交
- 第一个是“共同祖先提交”。如果您跟踪项目中两个分支的历史记录,它们始终至少有一个共同的提交。此时,两个分支具有相同的内容。之后,它们以不同的方式发展。
- 另外两个有趣的提交是每个分支的端点,即它们当前的状态。请记住,集成的目标是组合两个分支的当前状态。因此,它们的最新修订当然很重要。
组合这三个提交可以执行我们想要进行的集成。

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

但是,在大多数情况下,这两个分支都会使用不同的提交向前移动。所以让我们来看一个更现实的例子

为了进行集成,Git 必须创建一个包含所有更改的新提交,并处理分支之间的差异——这就是我们所说的合并提交。
人工提交和合并提交
通常,提交是由人类仔细创建的。它是一个有意义的单元,仅包含相关的更改,以及一个有意义的提交消息,提供上下文和注释。
现在,合并提交有点不同:它不是由开发人员创建的,而是由 Git自动创建的。此外,合并提交不一定包含“语义相关的更改集合”。相反,它的目的仅仅是连接两个(或多个)分支并系上结。

如果您想了解这种自动合并操作,则必须查看所有分支及其各自提交历史记录的历史记录。
使用变基进行集成
在我们讨论变基之前,请允许我明确一点:变基并不比合并好或坏,它只是不同。您只需合并分支就可以过上幸福的(Git)生活,甚至不必考虑变基。但是,了解变基的作用以及随之而来的优缺点确实很有帮助。也许您会在项目的某个阶段发现变基可能会有所帮助……
好的,让我们开始吧!还记得我们刚才讨论的自动合并提交吗?有些人不太喜欢这些提交,更愿意不使用它们。此外,有些开发人员希望他们的项目历史看起来像一条直线——即使在分支已集成之后,也不显示它在某些时候被拆分为多个分支的任何迹象。这基本上是在 Git 变基期间发生的事情。

变基:分步操作
让我们分步了解变基操作。场景与前面的示例相同,这是起点的示例

我们希望将branch-B
中的更改集成到branch-A
中——但通过变基而不是合并。为此使用的 Git 命令非常简单
$ git checkout branch-A
$ git rebase branch-B
与git merge
命令类似,您告诉 Git 您要集成哪个分支。让我们看看幕后发生了什么……

在第一步中,Git 将“删除”branch-A
上在共同祖先提交之后发生的所有提交。别担心,它不会将它们丢弃:您可以将这些提交视为“已停放”或临时保存在安全的地方。

在第二步中,Git 应用来自branch-B
的新提交。此时,暂时,这两个分支实际上看起来完全相同。

最后,那些“已停放”的提交(来自branch-A
的新提交)被包含进来。由于它们位于来自branch-B
的已集成提交之上,因此它们被变基了。
结果,项目历史看起来像是开发以直线进行的。没有包含所有组合更改的合并提交,并且保留了原始提交结构。
变基的潜在陷阱
还有一件事——这一点对于理解 Git 变基非常重要——那就是它会重写提交历史记录。再看一下我们最后的图表。提交 C3* 带有一个星号。虽然 C3* 与 C3 具有相同的内容,但它实际上是一个不同的提交。为什么?因为在变基之后,它有一个新的父提交。在变基之前,C1 是父提交。变基之后,父提交是 C4——它被变基到其中。
提交只有少数几个重要的属性,如作者、日期、更改集及其父提交。更改其中的任何信息都会创建一个全新的提交,并具有新的 SHA-1 哈希 ID。
像这样重写历史记录对于尚未发布的提交来说不是问题。但是,如果您正在重写已经推送到远程存储库的提交,那么您可能会遇到麻烦。也许其他人已基于原始的 C3 提交开展工作,而现在它突然消失了……
为了避免出现问题,这里有一条使用变基的简单规则:永远不要在公共分支上使用变基,即在已推送到远程存储库的提交上使用!相反,在将其集成到共享团队分支之前,使用git rebase
清理您的本地提交历史记录。
整合至关重要!
归根结底,合并和变基都是有用的 Git 策略,具体取决于您想要实现的目标。合并某种程度上是非破坏性的,因为它不会更改现有历史记录。另一方面,变基可以通过避免不必要的合并提交来帮助清理您的项目历史记录。请记住,不要在公共分支中执行此操作,以免干扰其他开发人员。
如果您想深入了解高级 Git 工具,请随时查看我的(免费!)“高级 Git 工具包”:它包含一系列关于分支策略、交互式变基、Reflog、子模块等等主题的短视频。
祝您合并和变基愉快——并在我们“高级 Git”系列的下一部分中与您相见!
高级 Git 系列
- 第 1 部分: 在 Git 中创建完美的提交
- 第 2 部分: Git 中的分支策略
- 第 3 部分: 通过拉取请求实现更好的协作
- 第 4 部分: 合并冲突
- 第 5 部分: Rebase 与 Merge(您现在所处位置!)
- 第 6 部分: 交互式变基
- 第 7 部分: 在 Git 中挑选提交
- 第 8 部分: 使用 Reflog 恢复丢失的提交
如果您想提升水平并完全掌握如何使用 Git 变基,我建议您阅读我关于此主题的文章这里。对我们来说,变基是唯一的工作方式,因为在团队沟通方面,其好处是无与伦比的,因为仅使用 Git 合并时,Git 历史记录会变得混乱且难以调试。
无论如何,希望这有帮助。享受!
“另一方面,变基可以通过避免不必要的合并提交来帮助清理您的项目历史记录。”
为什么不使用 squash 呢?
是的,压缩绝对是一个选择。
但是,变基的美妙之处在于它保留了原始提交。(当然,如果您希望这样做。)
使用“压缩”选项合并会将(可能很有价值的)提交历史记录减少到一个修订版。
就像生活中经常发生的那样:这取决于您的目标 :-)
在第一个示例中,您将分支 A 合并到分支 B,但随后您说“Git 可以将分支 B 中的所有新提交添加到公共祖先提交的顶部”。我理解错了吗,或者第一个示例应该将 B 合并到 A 中?
谢谢,Sandro!如果将分支 B 集成到分支 A 中,该示例确实更有意义。我根据您的反馈更新了帖子!谢谢!
如果在变基期间发生冲突会发生什么?这可能发生吗,或者“顶部”的提交始终覆盖它们?
别担心,Rych:Git 永远不会“覆盖”任何东西!如果发生冲突,变基操作将暂停,Git 会向您展示问题,您可以解决它们——然后继续变基。