章节 ▾ 第二版

3.6 Git 分支 - 变基

变基

在 Git 中,整合来自不同分支的修改主要有两种方法:`merge`(合并)和 `rebase`(变基)。 在本节中,我们将学习什么是变基,如何使用变基,为什么变基是一个非常棒的工具,以及在什么情况下你不想使用它。

变基的基础

如果你回到之前[基础合并](/book#-cn/v2/ch00/_basic_merging)的例子,可以看到你分叉了工作,并在两个不同的分支上进行了提交。

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

最容易的整合分支的方法,正如我们已经介绍的,是 `merge` 命令。 它会在两个最新的分支快照(`C3` 和 `C4`)以及它们最近的共同祖先(`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

它的原理是首先找到这两个分支(即当前分支 `experiment`、变基操作的目标基底分支 `master`)的共同祖先 `C2`,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 `master`,最后以此将之前另存为临时文件的修改依序应用。

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 分支。 你可以将 server 分支变基到 master 分支上,而无需先检出它,方法是运行 git rebase <basebranch> <topicbranch> ——这将为你检出主题分支(在本例中为 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 还计算一个仅基于提交引入的补丁的校验和。 这称为“patch-id”。

如果你拉取了重写的工作,并将其变基在你合作伙伴的新提交之上,Git 通常可以成功地找出什么是你独有的,并将它们重新应用到新分支之上。

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

  • 确定哪些工作对我们的分支是唯一的 (C2, C3, C4, C6, C7)

  • 确定哪些不是合并提交 (C2, C3, C4)

  • 确定哪些尚未重写到目标分支中(仅 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,以尽量减少发生后的痛苦。

变基 vs. 合并

现在你已经看到了变基和合并的实际操作,你可能想知道哪个更好。 在我们可以回答这个问题之前,让我们稍微退一步,谈谈历史意味着什么。

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

相反的观点是,提交历史是你的项目是如何制作的故事。 你不会发布一本书的初稿,那么为什么要展示你混乱的工作呢? 当你正在处理一个项目时,你可能需要记录你的所有错误步骤和死胡同路径,但是当该向世界展示你的工作时,你可能想讲述一个更连贯的故事,说明如何从 A 到 B。 这个阵营中的人使用诸如 rebasefilter-branch 之类的工具来重写他们的提交,然后再将它们合并到主线分支中。 他们使用诸如 rebasefilter-branch 之类的工具,以最适合未来读者的方式讲述故事。

现在,对于合并或变基哪个更好的问题:希望你能明白它并不是那么简单。 Git 是一个强大的工具,它允许你对你的历史做很多事情,但每个团队和每个项目都是不同的。 既然你已经知道了这两种工作方式,那么就由你来决定哪一种最适合你的特定情况。

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

scroll-to-top