-
A1. 附录 A:其他环境下的 Git
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 总结
-
A2. 附录 B:在你的应用中嵌入 Git
-
A3. 附录 C:Git 命令
3.6 Git 分支 - 变基
变基
在 Git 中,整合来自不同分支的修改主要有两种方法:`merge`(合并)和 `rebase`(变基)。 在本节中,我们将学习什么是变基,如何使用变基,为什么变基是一个非常棒的工具,以及在什么情况下你不想使用它。
变基的基础
如果你回到之前[基础合并](/book#-cn/v2/ch00/_basic_merging)的例子,可以看到你分叉了工作,并在两个不同的分支上进行了提交。

最容易的整合分支的方法,正如我们已经介绍的,是 `merge` 命令。 它会在两个最新的分支快照(`C3` 和 `C4`)以及它们最近的共同祖先(`C2`)之上进行三方合并,创建一个新的快照(和提交)。

然而,还有另外一种方法:你可以把在 `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`,最后以此将之前另存为临时文件的修改依序应用。

现在,你可以回到 `master` 分支,进行一次快进合并。
$ git checkout master
$ git merge experiment

现在,C4'
指向的快照与 合并示例中 C5
指向的快照完全相同。集成的最终产品没有任何区别,但变基可以使历史记录更清晰。 如果你检查变基分支的日志,它看起来就像一个线性历史:即使它最初是并行发生的,所有的工作也都是按顺序发生的。
通常,你会这样做以确保你的提交能够干净地应用到远程分支上——也许是在一个你试图贡献但并不维护的项目中。 在这种情况下,你会在一个分支中完成你的工作,然后在你准备好将你的补丁提交到主项目时,将你的工作变基到 origin/master
上。 这样,维护者就不必做任何集成工作——只需一个快进或一个干净的应用即可。
请注意,最终提交指向的快照(无论是变基的最后一个提交,还是合并后的最后一个合并提交)是相同的快照——只有历史记录不同。 变基以引入的顺序将一个工作线上的更改重放到另一个工作线上,而合并则获取端点并将它们合并在一起。
更有趣的变基
你也可以让你的变基在变基目标分支以外的其他东西上重放。 以 一个主题分支基于另一个主题分支的历史为例。 你创建了一个主题分支 (server
),以便向你的项目添加一些服务器端功能,并提交了一个提交。 然后,你基于该分支进行客户端更改 (client
) 并提交了几次。 最后,你回到你的 server
分支并做了更多的提交。

假设你决定将你的客户端更改合并到主线中以进行发布,但你想推迟服务器端更改,直到它经过进一步测试。 你可以使用 git rebase
的 --onto
选项,将 client
上不在 server
上的更改(C8
和 C9
)重放到你的 master
分支上。
$ git rebase --onto master server client
这基本上是说,“获取 client
分支,找出自它与 server
分支分叉以来的补丁,并将这些补丁在 client
分支中重放,就好像它是直接基于 master
分支一样。” 这有点复杂,但结果非常酷。

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

master
分支以包含 client
分支的更改假设你决定也拉取你的 server
分支。 你可以将 server
分支变基到 master
分支上,而无需先检出它,方法是运行 git rebase <basebranch> <topicbranch>
——这将为你检出主题分支(在本例中为 server
)并将其重放到基础分支 (master
) 上
$ git rebase master server
这将你的 server
工作重放到你的 master
工作之上,如 将你的 server
分支变基到你的 master
分支之上所示。

server
分支变基到你的 master
分支之上然后,你可以快进基础分支 (master
)
$ git checkout master
$ git merge server
你可以删除 client
和 server
分支,因为所有的工作都已集成,你不再需要它们,使你整个过程的历史记录看起来像 最终提交历史
$ git branch -d client
$ git branch -d server

变基的风险
啊哈,但是变基的幸福并非没有其缺点,可以用一句话概括
不要变基存在于你的仓库之外且其他人可能基于其进行工作的提交。
如果你遵循该准则,你就会没事的。 如果你不这样做,人们会讨厌你,你会被朋友和家人鄙视。
当你变基内容时,你正在放弃现有的提交并创建新的提交,这些提交相似但不同。 如果你将提交推送到某个地方,其他人将其拉取下来并基于其进行工作,然后你使用 git rebase
重写这些提交并再次将它们推送上去,你的协作者将不得不重新合并他们的工作,并且当你尝试将他们的工作拉回到你的工作中时,事情会变得混乱。
让我们看一个例子,说明变基你已公开的工作如何导致问题。 假设你从中央服务器克隆,然后基于它进行一些工作。 你的提交历史记录如下所示

现在,其他人做了更多的工作,其中包括一个合并,并将该工作推送到中央服务器。 你获取它并将新的远程分支合并到你的工作中,使你的历史记录看起来像这样

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

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

如果你的历史记录看起来像这样时运行 git log
,你将看到两个具有相同作者、日期和消息的提交,这将令人困惑。 此外,如果你将此历史记录推送回服务器,你将重新将所有这些变基的提交引入到中央服务器中,这会进一步使人们感到困惑。 可以很安全地假设其他开发人员不希望 C4
和 C6
出现在历史记录中; 这就是他们首先进行变基的原因。
何时变基
如果你确实发现自己处于这种情况,Git 还有一些其他的魔力可以帮助你。 如果你团队中的某个人强制推送更改,从而覆盖了你基于其进行的工作,那么你的挑战是弄清楚什么是你的,以及他们重写了什么。
事实证明,除了提交 SHA-1 校验和之外,Git 还计算一个仅基于提交引入的补丁的校验和。 这称为“patch-id”。
如果你拉取了重写的工作,并将其变基在你合作伙伴的新提交之上,Git 通常可以成功地找出什么是你独有的,并将它们重新应用到新分支之上。
例如,在前面的场景中,如果在我们位于 有人推送了变基的提交,放弃了你基于其进行的工作时,没有执行合并而是运行 git rebase teamone/master
,Git 将
-
确定哪些工作对我们的分支是唯一的 (
C2
,C3
,C4
,C6
,C7
) -
确定哪些不是合并提交 (
C2
,C3
,C4
) -
确定哪些尚未重写到目标分支中(仅
C2
和C3
,因为C4
与C4'
是相同的补丁) -
将这些提交应用到
teamone/master
的顶部
因此,我们看到的不是 你将相同的工作再次合并到新的合并提交中中的结果,而是最终得到更像 在强制推送的变基工作之上变基的东西。

这只有在你的合作伙伴所做的 C4
和 C4'
几乎完全相同的情况下才有效。 否则,变基将无法判断它是重复的,并且会添加另一个类似于 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。 这个阵营中的人使用诸如 rebase
和 filter-branch
之类的工具来重写他们的提交,然后再将它们合并到主线分支中。 他们使用诸如 rebase
和 filter-branch
之类的工具,以最适合未来读者的方式讲述故事。
现在,对于合并或变基哪个更好的问题:希望你能明白它并不是那么简单。 Git 是一个强大的工具,它允许你对你的历史做很多事情,但每个团队和每个项目都是不同的。 既然你已经知道了这两种工作方式,那么就由你来决定哪一种最适合你的特定情况。
你可以获得两全其美的效果:在推送到清理你的工作之前变基本地更改,但永远不要变基你已推送到任何地方的内容。