Pro Git
professional version control
To help translate the book, please fork the book at GitHub and push your contributions.
衍合
把一个分支整合到另一个分支的办法有两种:merge(合并)
和 rebase(衍合)
。在本章我们会学习什么是衍合,如何使用衍合,为什么衍合操作如此富有魅力,以及我们应该在什么情况下使用衍合。
衍合基础
请回顾之前有关合并的一节(见图 3-27),你会看到开发进程分叉到两个不同分支,又各自提交了更新。
![](18333fig0327-tn.png)
图 3-27. 最初分叉的提交历史。
之前介绍过,最容易的整合分支的方法是 merge
命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并。如图 3-28 所示:
![](18333fig0328-tn.png)
图 3-28. 通过合并一个分支来整合分叉了的历史。
其实,还有另外一个选择:你可以把在 C3 里产生的变化补丁重新在 C4 的基础上打一遍。在 Git 里,这种操作叫做_衍合(rebase)_。有了 rebase
命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。
在这个例子里,可以运行下面的命令:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序施用每一个差异补丁文件。图 3-29 演示了这一过程:
![](18333fig0329-tn.png)
图 3-29. 把 C3 里产生的改变衍合到 C4 中。
现在,你可以回到 master 分支然后进行一次快进合并(见图 3-30):
![](18333fig0330-tn.png)
图 3-30. master 分支的快进。
现在,合并后的 C3(即现在的 C3’)所指的快照,同三方合并例子中的 C5 所指的快照内容一模一样了。最后整合得到的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来更清楚:仿佛所有修改都是先后进行的,尽管实际上它们原来是同时发生的。
你可以经常使用衍合,确保在远程分支里的提交历史更清晰。比方说,某些项目自己不是维护者,但想帮点忙,就应该尽可能使用衍合:先在一个分支里进行开发,当准备向主项目提交补丁的时候,再把它衍合到 origin/master
里面。这样,维护者就不需要做任何整合工作,只需根据你提供的仓库地址作一次快进,或者采纳你提交的补丁。
请注意,合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。
更多有趣的衍合
你还可以在衍合分支以外的地方衍合。以图 3-31 的历史为例。你创建了一个特性分支 server
来给服务器端代码添加一些功能,然后提交 C3 和 C4。然后从 C3 的地方再增加一个 client
分支来对客户端代码进行一些修改,提交 C8 和 C9。最后,又回到 server
分支提交了 C10。
![](18333fig0331-tn.png)
图 3-31. 从一个特性分支里再分出一个特性分支的历史。
假设在接下来的一次软件发布中,你决定把客户端的修改先合并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。你可以仅提取对客户端的改变(C8 和 C9),然后通过使用 git rebase
的 --onto
选项来把它们在 master 分支上重演:
$ git rebase --onto master server client
这基本上等于在说“检出 client 分支,找出 client
分支和 server
分支的共同祖先之后的变化,然后把它们在 master
上重演一遍”。是不是有点复杂?不过它的结果,如图 3-32 所示,非常酷:
![](18333fig0332-tn.png)
图 3-32. 衍合一个特性分支上的另一个特性分支。
现在可以快进 master 分支了(见图 3-33):
$ git checkout master
$ git merge client
![](18333fig0333-tn.png)
图 3-33. 快进 master 分支,使之包含 client 分支的变化。
现在你决定把 server
分支的变化也包含进来。可以直接把 server
分支衍合到 master
而不用手工转到 server
分支再衍合。git rebase [主分支] [特性分支]
命令会先检出特性分支 server
,然后在主分支 master
上重演:
$ git rebase master server
于是 server
的进度应用到 master
的基础上,如图 3-34:
![](18333fig0334-tn.png)
图 3-34. 在 master 分支上衍合 server 分支。
然后快进主分支 master
:
$ git checkout master
$ git merge server
现在 client
和 server
分支的变化都被整合了,不妨删掉它们,把你的提交历史变成图 3-35 的样子:
$ git branch -d client
$ git branch -d server
![](18333fig0335-tn.png)
图 3-35. 最终的提交历史
衍合的风险
呃,奇妙的衍合也不是完美无缺的,一句话可以总结这点:
永远不要衍合那些已经推送到公共仓库的更新。
如果你遵循这条金科玉律,就不会出差错。否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
在衍合的时候,实际上抛弃了一些现存的 commit 而创造了一些类似但不同的新 commit。如果你把commit 推送到某处然后其他人下载并在其基础上工作,然后你用 git rebase
重写了这些commit 再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。
下面我们用一个实际例子来说明为什么公开的衍合会带来问题。假设你从一个中央服务器克隆然后在它的基础上搞了一些开发,提交历史类似图 3-36:
![](18333fig0336-tn.png)
图 3-36. 克隆一个仓库,在其基础上工作一番。
现在,其他人进行了一些包含一次合并的工作(得到结果 C6),然后把它推送到了中央服务器。你获取了这些数据并把它们合并到你本地的开发进程里,让你的历史变成类似图 3-37 这样:
![](18333fig0337-tn.png)
图 3-37. 获取更多提交,并入你的开发进程。
接下来,那个推送 C6 上来的人决定用衍合取代那次合并;他们用 git push --force
覆盖了服务器上的历史,得到 C4’。然后你再从服务器上获取更新:
![](18333fig0338-tn.png)
图 3-38. 有人推送了衍合过的 C4’,丢弃了你作为开发基础的 C6。
这时候,你需要再次合并这些内容,尽管之前已经做过一次了。衍合会改变这些 commit 的 SHA-1 校验值,这样 Git 会把它们当作新的 commit,然而这时候在你的提交历史早就有了 C4 的内容(见图 3-39):
![](18333fig0339-tn.png)
图 3-39. 你把相同的内容又合并了一遍,生成一个新的提交 C8。
你迟早都是要并入其他协作者提交的内容的,这样才能保持同步。当你做完这些,你的提交历史里会同时包含 C4 和 C4’,两者有着不同的 SHA-1 校验值,但却拥有一样的作者日期与提交说明,令人费解!更糟糕的是,当你把这样的历史推送到服务器,会再次把这些衍合的提交引入到中央服务器,进一步迷惑其他人。
如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些永远不会公开的 commit,那就不会有任何问题。如果衍合那些已经公开的 commit,而与此同时其他人已经用这些 commit 进行了后续的开发工作,那你有得麻烦了。