Pro Git - Pro Git 3.6 Git 分支 衍合
From Pro Git
Pro Git
professional version control
To help translate the book, please fork the book at GitHub and push your contributions.
衍合
把一个分支整合到另一个分支的办法有两种:merge(合并) 和 rebase(衍合)。在本章我们会学习什么是衍合,如何使用衍合,为什么衍合操作如此富有魅力,以及我们应该在什么情况下使用衍合。
衍合基础
请回顾之前有关合并的一节(见图 3-27),你会看到开发进程分叉到两个不同分支,又各自提交了更新。

图 3-27. 最初分叉的提交历史。
之前介绍过,最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并。如图 3-28 所示:

图 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 演示了这一过程:

图 3-29. 把 C3 里产生的改变衍合到 C4 中。
现在,你可以回到 master 分支然后进行一次快进合并(见图 3-30):

图 3-30. master 分支的快进。
现在,合并后的 C3(即现在的 C3’)所指的快照,同三方合并例子中的 C5 所指的快照内容一模一样了。最后整合得到的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来更清楚:仿佛所有修改都是先后进行的,尽管实际上它们原来是同时发生的。
你可以经常使用衍合,确保在远程分支里的提交历史更清晰。比方说,某些项目自己不是维护者,但想帮点忙,就应该尽可能使用衍合:先在一个分支里进行开发,当准备向主项目提交补丁的时候,再把它衍合到 origin/master 里面。这样,维护者就不需要做任何整合工作,只需根据你提供的仓库地址作一次快进,或者采纳你提交的补丁。
请注意,合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。
更多有趣的衍合
你还可以在衍合分支以外的地方衍合。以图 3-31 的历史为例。你创建了一个特性分支 server 来给服务器端代码添加一些功能,然后提交 C3 和 C4。然后从 C3 的地方再增加一个 client 分支来对客户端代码进行一些修改,提交 C8 和 C9。最后,又回到 server 分支提交了 C10。

图 3-31. 从一个特性分支里再分出一个特性分支的历史。
假设在接下来的一次软件发布中,你决定把客户端的修改先合并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。你可以仅提取对客户端的改变(C8 和 C9),然后通过使用 git rebase 的 --onto 选项来把它们在 master 分支上重演:
$ git rebase --onto master server client
这基本上等于在说“检出 client 分支,找出 client 分支和 server 分支的共同祖先之后的变化,然后把它们在 master 上重演一遍”。是不是有点复杂?不过它的结果,如图 3-32 所示,非常酷:

图 3-32. 衍合一个特性分支上的另一个特性分支。
现在可以快进 master 分支了(见图 3-33):
$ git checkout master
$ git merge client

图 3-33. 快进 master 分支,使之包含 client 分支的变化。
现在你决定把 server 分支的变化也包含进来。可以直接把 server 分支衍合到 master 而不用手工转到 server 分支再衍合。git rebase [主分支] [特性分支] 命令会先检出特性分支 server,然后在主分支 master 上重演:
$ git rebase master server
于是 server 的进度应用到 master 的基础上,如图 3-34:

图 3-34. 在 master 分支上衍合 server 分支。
然后快进主分支 master:
$ git checkout master
$ git merge server
现在 client 和 server 分支的变化都被整合了,不妨删掉它们,把你的提交历史变成图 3-35 的样子:
$ git branch -d client
$ git branch -d server

图 3-35. 最终的提交历史
衍合的风险
呃,奇妙的衍合也不是完美无缺的,一句话可以总结这点:
永远不要衍合那些已经推送到公共仓库的更新。
如果你遵循这条金科玉律,就不会出差错。否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
在衍合的时候,实际上抛弃了一些现存的 commit 而创造了一些类似但不同的新 commit。如果你把commit 推送到某处然后其他人下载并在其基础上工作,然后你用 git rebase 重写了这些commit 再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。
下面我们用一个实际例子来说明为什么公开的衍合会带来问题。假设你从一个中央服务器克隆然后在它的基础上搞了一些开发,提交历史类似图 3-36:

图 3-36. 克隆一个仓库,在其基础上工作一番。
现在,其他人进行了一些包含一次合并的工作(得到结果 C6),然后把它推送到了中央服务器。你获取了这些数据并把它们合并到你本地的开发进程里,让你的历史变成类似图 3-37 这样:

图 3-37. 获取更多提交,并入你的开发进程。
接下来,那个推送 C6 上来的人决定用衍合取代那次合并;他们用 git push --force 覆盖了服务器上的历史,得到 C4’。然后你再从服务器上获取更新:

图 3-38. 有人推送了衍合过的 C4’,丢弃了你作为开发基础的 C6。
这时候,你需要再次合并这些内容,尽管之前已经做过一次了。衍合会改变这些 commit 的 SHA-1 校验值,这样 Git 会把它们当作新的 commit,然而这时候在你的提交历史早就有了 C4 的内容(见图 3-39):

图 3-39. 你把相同的内容又合并了一遍,生成一个新的提交 C8。
你迟早都是要并入其他协作者提交的内容的,这样才能保持同步。当你做完这些,你的提交历史里会同时包含 C4 和 C4’,两者有着不同的 SHA-1 校验值,但却拥有一样的作者日期与提交说明,令人费解!更糟糕的是,当你把这样的历史推送到服务器,会再次把这些衍合的提交引入到中央服务器,进一步迷惑其他人。
如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些永远不会公开的 commit,那就不会有任何问题。如果衍合那些已经公开的 commit,而与此同时其他人已经用这些 commit 进行了后续的开发工作,那你有得麻烦了。