4.6 变更基线(Rebasing)
在Git中,有两种方法来从一个分支集成到另外一个分支:合并以及变更基线。在本节中,你将会学习什么是变更基线,怎么变更基线,为什么它是一个相当迷人的工具,以及在什么情况下,你会不想用它。
4.6.1 基本的变更基线(The Basic Rebase)
如果你返回到一个合并章节中的较早期的例子(如图3-27),你可以看到你偏离了你的工作并在两个不同的分支上进行提交。
就像我们已经讲解的那样,最简单的集成分支的方法是merge命令。它在两个分支的最新的快照(C3和C4)和它们两者的最近的共同祖先(C2)之间执行一个三方合并,创建一个新的快照(并提交),如图3-28所示:
然而,有另外一种方式:你可以拿C3上引进的更改的补丁并把它应用到C4上。在Git中,这称之为变更基线。使用rebase命令,你可以取出所有的在一个分支上的已提交变更并把它应用到另外一个分支上。
在本例中,你可以运行以下命令:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它是这么工作的: 首先进入到两个分支(你工作的分支以及你正在变更基线到的那个分支)的共同祖先,取出你所在分支每个提交引入的不同,存储这些不同到一个临时文件中,重新设置你当前的分支到你变更基线的那个分支的同一个提交上,最后,顺序地应用每个更改。图3-29演示了这个过程:
现在,你可以回到你的master分支上做一个快速向前的合并(如图3-30):
现在,C3’所指向的快照完全与在合并示例中C5指向的相同。在最终的集成产品中,没有什么不同,但变更基线创建了一个更为清晰的历史。如果你检查一下基线变更的分支,它看起来象一个线性历史:它显示了所有的工作是以线性发生的,即使当它最终发生时是并行的。
通常,你这么做以确保你的提交干净地应用到了一个远端分支上――或许在项目中,你是一个贡献者但你确并不维护它。在这种情况下,你会在一个分支上做你自己的工作,然后当你准备好提交你的补丁到主项目时,你会变更你的工作分支的基线到origin/master分支上。用这种方法,项目维护者不需要做任何集成工作――只需要一个快速向前或者一个干净的应用(Apply)。
注意,最后那个提交所指向的快照,不管是最后的变更基线后的变更基线提交或者是合并后的最后的合并提交,它们是相同的快照――只是它们的历史不同。基线变更把一条工作线上的变更按照顺序应用到另外一条工作线上,而合并则是拿两个端点并把它们合并在一起。
4.6.2 更有趣的基线变更(More Interesting Rebases)
你也可以让你的基线变更到变更基线分支之外的其它分支上。例如,拿如图3-31的历史来说:你分离了一个主题分支(server)来增加一些服务器端的功能到你的项目中,并做了一些提交。然后,你分离了一个分支来实现一些客户端的变更(client)并提交了几次。最后,你返回到你的服务器分支并做了一些提交。
假设你决定你想合并你的客户端变更到主线中做一个发布,但你又想暂停服务器端的变更以便进一步测试。你可以取出不在服务器上的客户端的变更(C8和C9) ,通过使用git rebase的onto选项来把它应用到你的master分支上:
$ git rebase --onto master server client
这基本上是说,“检出客户端分支(client),从client和server分支的共同祖先处计算出补丁,然后把它应用到master分支上。”它有点复杂,但结果如图3-32所示,相当的酷:
现在,你可以快速前进你的master分支(如图3-33):
$ git checkout master
$ git merge client
现在你决定也把server分支拉进来。你可以通过运行git rebase [basebranch] [topicbranch] 来把server分支变更基线到master分支上而不用先把它检出――它检出主题分支(本例为server),并把它应用到基础分支(master):
$ git rebase master server
这把你的server上的工作放在master工作的上方,如图3-34所示:
然后,你可以快速前进基础分支(master):
$ git checkout master
$ git merge server
你可以移除client和server分支了,因为所有的工作都已经被集成,你不再需要它们了,那么,你整个过程的历史看起来如图3-35所示:
$ git branch -d client
$ git branch -d server
4.6.3 变更基线的风险(The Perils of Rebasing)
啊哈,变更基线并非没有缺点,它可以被总结成一句话:
不要变更基线到你已经上传的一个公共库 (Do not rebase commits that you have pushed to a public repository.)
如果你遵守这个原则,那就没有问题;如果你不遵守,别人会恨你,你也会被朋友和家人责备。
当你变更基线时,你正在丢弃现存的提交并创建一个新的相似的但不同的提交。如果你上传了提交到什么地方而其它人取下来并作为工作的基础,然后你用git rebase重写了这些提交并把它们再次上传,你的协作者将不得不重新合并他们得工作,当你试图把它们的工作拿到你那儿时事情会变得糟糕。
让我们通过一个示例来看一下变更你创建的公共工作基线怎么会导致问题。假设你从一个中关新服务器上clone了一个项目然后在其上做了一些工作。你的提交历史看起来如图3-36所示:
现在,其它人做了更多的工作包括一个合并,并把他的工作上传到了服务器。你获取它们并合并到你的新远端分支到你的工作中,使得你的历史看起来如图3-37所示:
下一步,上传合并工作的那个人决定返回并变更他们的工作基线;他们使用git push –force来重写了服务器上的历史,然后,你从那个服务器上获取,带来一个新的提交:
此时,你不得不再次合并这个工作,即使你已经做过了。变更基线改变了这些提交的SHA-1哈希这使得Git把他们看成了一个新提交,而实际上你已经在你的历史上有了C4的工作(如图3-39)。
你不得不在某个时间点把这个工作合并进来以使你在未来可以与其它开发者同步。在你这么做以后,你的提交历史将同时包含C4和C4’提交,他们有不同的SHA-1哈希但引入了相同的工作并有相同的提交消息。如果此时你运行一个git log,你会看到两个提交有着相同的作者日期以及消息,这会使人混淆。另外,如果你把这个历史上传回服务器上,你将会在服务器上重复引入所有这些变更了基线的提交,这会进一步使其它人混淆。
如果你把基线变更看作是你上传他们之前的清扫工作的一种方法,而且如果你仅仅变更到哪些从未公共可用的基线提交,那么你将不会有什么问题。如果你变更到哪些已经被上传到公共区域的基线提交,其它人以及基于这些提交做了工作,那么你可能会面临一些令人沮丧的问题。
4.7 总结(Summary)
我们已经讲解了Git中的基本分支和合并。你应该可以轻松地创建和切换一个新的分支,在分支间切换以及合并本地分支到一起。你也应该会通过把你的分支上传到一个共享服务器上来共享你的分支,与其它人在共享分支上一起工作以及在他们被共享前重建分支的基线。