Git
章节 ▾ 第二版

3.6 Git 分支 - 变基

变基

在 Git 中,将一个分支的更改集成到另一个分支中主要有两种方法:merge(合并)和 rebase(变基)。在本节中,您将学习什么是变基,如何进行变基,为什么它是一个非常棒的工具,以及在哪些情况下您不希望使用它。

基本变基

如果您回顾一下 基本合并 中的早期示例,您会发现您已经将工作分叉并在两个不同的分支上进行了提交。

Simple divergent history
图 35. 简单的分叉历史

正如我们已经介绍过的,集成分支的最简单方法是使用 merge 命令。它在两个最新分支快照(C3C4)以及这两个分支最新的共同祖先(C2)之间执行三次合并,创建一个新的快照(和提交)。

Merging to integrate diverged work history
图 36. 合并以集成分叉的工作历史

但是,还有另一种方法:您可以获取在 C4 中引入的更改的补丁,并将其重新应用到 C3 的顶部。在 Git 中,这称为变基。使用 rebase 命令,您可以获取在一个分支上提交的所有更改,并将其重新应用到另一个分支上。

对于此示例,您将检出 experiment 分支,然后将其变基到 master 分支,如下所示:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

此操作的工作原理是:转到两个分支的共同祖先(您所在的和您要变基到的分支),获取您所在分支的每个提交引入的差异,将这些差异保存到临时文件中,将当前分支重置到与您要变基到的分支相同的提交,最后依次应用每个更改。

Rebasing the change introduced in `C4` onto `C3`
图 37. 将 C4 中引入的更改变基到 C3

此时,您可以返回到 master 分支并执行快速向前合并。

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
图 38. 快速向前合并 master 分支

现在,C4' 指向的快照与 合并示例C5 指向的快照完全相同。集成最终产品没有区别,但变基使历史记录更清晰。如果您检查变基分支的日志,它看起来像一个线性历史记录:即使最初是并行发生的,它看起来所有工作都是按顺序发生的。

通常,您会这样做以确保您的提交在远程分支上干净地应用——也许是在您尝试为其贡献代码但您不维护的项目中。在这种情况下,您将在一个分支中完成您的工作,然后在准备好将您的补丁提交到主项目时,将您的工作变基到 origin/master。这样,维护人员就不需要执行任何集成工作——只需快速向前或干净应用即可。

请注意,最终生成的最终提交指向的快照,无论是变基的最后一个提交还是合并后的最终合并提交,都是相同的快照——只有历史记录不同。变基会按引入的顺序将一条工作线上的更改重新应用到另一条工作线上,而合并则将端点合并在一起。

更有趣的变基

你也可以让你的变基操作作用于除变基目标分支之外的其他分支。例如,考虑一下一个主题分支从另一个主题分支分出的历史记录。你创建了一个主题分支(server)来向你的项目添加一些服务器端功能,并提交了一个更改。然后,你从该分支上分出一个分支来进行客户端更改(client),并提交了几次更改。最后,你回到你的server分支并进行了一些额外的提交。

A history with a topic branch off another topic branch
图 39. 一个主题分支从另一个主题分支分出的历史记录

假设你决定将你的客户端更改合并到你的主线分支中以进行发布,但你想推迟服务器端更改,直到它经过进一步测试。你可以使用git rebase--onto选项,将client分支上不存在于server分支上的更改(C8C9)重新应用到你的master分支上。

$ git rebase --onto master server client

这基本上表示:“获取client分支,找出它与server分支分叉以来的补丁,并将这些补丁重新应用到client分支上,就好像它直接基于master分支一样。”这有点复杂,但结果非常棒。

Rebasing a topic branch off another topic branch
图 40. 将一个主题分支变基到另一个主题分支上

现在你可以快进你的master分支(参见快进你的master分支以包含client分支的更改)。

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
图 41. 快进你的master分支以包含client分支的更改

假设你决定也引入你的server分支。你可以通过运行git rebase <basebranch> <topicbranch>server分支变基到master分支上,而无需先检出它——这会为你检出主题分支(在本例中为server),并将其重新应用到基础分支(master)上。

$ git rebase master server

这将把你的server工作重新应用到你的master工作的顶部,如将你的server分支变基到你的master分支的顶部所示。

Rebasing your `server` branch on top of your `master` branch
图 42. 将你的server分支变基到你的master分支的顶部

然后,你可以快进基础分支(master)。

$ git checkout master
$ git merge server

你可以删除clientserver分支,因为所有工作都已集成,你不再需要它们了,这使得整个过程的历史记录看起来像最终提交历史

$ git branch -d client
$ git branch -d server
Final commit history
图 43. 最终提交历史

变基的风险

啊,但是变基的幸福并非没有其缺点,可以用一句话概括:

不要变基存在于你的仓库之外且其他人可能已经基于其进行工作提交。

如果你遵循这条准则,你就会没事。如果你不遵循,人们会讨厌你,你的朋友和家人会鄙视你。

当你变基时,你放弃了现有的提交,并创建了新的、相似但不同的提交。如果你将提交推送到某个地方,其他人拉取它们并基于它们进行工作,然后你使用git rebase重写这些提交并再次推送,你的协作者将不得不重新合并他们的工作,当你尝试将他们的工作拉回你的工作时,事情会变得混乱。

让我们来看一个如何变基你已公开的工作会导致问题的例子。假设你从一个中央服务器克隆,然后基于此进行一些工作。你的提交历史如下所示:

Clone a repository, and base some work on it
图 44. 克隆一个仓库,并基于它进行一些工作

现在,其他人进行更多工作,包括合并,并将这些工作推送到中央服务器。你获取它并将新的远程分支合并到你的工作中,使你的历史看起来像这样:

Fetch more commits, and merge them into your work
图 45. 获取更多提交,并将它们合并到你的工作中

接下来,推送合并工作的人决定返回并重新变基他们的工作;他们执行git push --force以覆盖服务器上的历史记录。然后你从该服务器获取,下载新的提交。

Someone pushes rebased commits, abandoning commits you’ve based your work on
图 46. 有人推送了变基的提交,放弃了你已基于其工作的提交

现在你们都陷入困境了。如果你执行git pull,你将创建一个包含两条历史记录的合并提交,你的仓库将如下所示:

You merge in the same work again into a new merge commit
图 47. 你再次将相同的工作合并到一个新的合并提交中

当你的历史看起来像这样时,如果你运行git log,你将看到两个具有相同作者、日期和消息的提交,这将令人困惑。此外,如果你将此历史记录推回服务器,你将把所有这些重新变基的提交重新引入中央服务器,这可能会进一步让人困惑。可以肯定地说,其他开发者不希望C4C6出现在历史记录中;这就是他们最初进行变基的原因。

变基时应注意什么

如果你确实发现自己处于这种情况,Git 有一些额外的魔法可能可以帮助你。如果你的团队中的某个人强制推送覆盖了你已基于其工作的更改,你的挑战是弄清楚哪些是你的,哪些是被他们重写的。

事实证明,除了提交 SHA-1 校验和之外,Git 还计算一个仅基于提交中引入的补丁的校验和。这被称为“补丁 ID”。

如果你拉取了被重写的更改并在你的合作伙伴的新提交之上重新变基,Git 通常可以成功地找出哪些是独一无二的,并将它们重新应用到新分支的顶部。

例如,在前面的场景中,如果我们没有在有人推送了变基的提交,放弃了你已基于其工作的提交时执行合并,而是运行git rebase teamone/master,Git 将:

  • 确定哪些工作对我们的分支是唯一的(C2C3C4C6C7)。

  • 确定哪些不是合并提交(C2C3C4)。

  • 确定哪些没有被重写到目标分支中(只有C2C3,因为C4C4'是相同的补丁)。

  • 将这些提交应用到teamone/master的顶部。

因此,我们不会得到你再次将相同的工作合并到一个新的合并提交中中显示的结果,而是会得到类似于在强制推送的变基工作之上进行变基的结果。

Rebase on top of force-pushed rebase work
图 48. 在强制推送的变基工作之上进行变基

这只在你的合作伙伴所做的C4C4'几乎完全是相同的补丁时才有效。否则,变基将无法识别它是一个重复,并将添加另一个类似于C4的补丁(这很可能会导致应用失败,因为更改至少已经部分存在)。

你也可以通过运行git pull --rebase而不是普通的git pull来简化此操作。或者你可以在这种情况下手动执行git fetch,然后执行git rebase teamone/master

如果你正在使用git pull并希望将--rebase设置为默认值,你可以使用类似于git config --global pull.rebase true的内容设置pull.rebase配置值。

如果你只变基从未离开过你自己的电脑的提交,你就会没事。如果你变基了已经推送但其他人没有基于其进行提交的提交,你也会没事。如果你变基了已经公开推送的提交,并且其他人可能已经基于这些提交进行了工作,那么你可能会遇到一些令人沮丧的麻烦,以及队友的鄙视。

如果在某些时候你或你的合作伙伴发现有必要这样做,请确保每个人都知道运行git pull --rebase以尝试使发生此情况后的痛苦稍微简单一些。

变基与合并

既然你已经看到了变基和合并的实际操作,你可能想知道哪一个更好。在我们能够回答这个问题之前,让我们退一步,谈谈历史的意义。

关于这一点的一个观点是,你的仓库的提交历史是实际发生的事情的记录。它是一份历史文献,本身就很有价值,不应该被篡改。从这个角度来看,更改提交历史几乎是亵渎神灵;你是在撒谎关于实际发生的事情。那么,如果有一系列混乱的合并提交又如何呢?这就是它发生的方式,仓库应该将其保存以供后代参考。

相反的观点是,提交历史是你的项目是如何创建的故事。你不会出版一本书的第一稿,那么为什么要展示你杂乱无章的工作呢?当你处理一个项目时,你可能需要记录下你所有失误和死胡同的路径,但是当是时候向世界展示你的工作时,你可能希望讲述一个更连贯的故事,说明如何从 A 到 B。这个阵营中的人们使用rebasefilter-branch等工具在将他们的提交合并到主线分支之前重写它们。他们使用rebasefilter-branch等工具,以最适合未来读者的方式讲述故事。

现在,关于合并还是变基更好的问题:希望你能够看到,事情并没有那么简单。Git 是一款强大的工具,允许你对你的历史进行许多操作,但每个团队和每个项目都不同。现在你已经了解了这两者的工作原理,现在由你来决定哪一个最适合你的特定情况。

你可以获得两全其美:在推送之前变基本地更改以清理你的工作,但永远不要变基你已经推送到某个地方的任何内容。

scroll-to-top