Pro Git 3.6 衍合

出自DILA Wiki

把一個分支整合到另一個分支的辦法有兩種:merge(合併) 和 rebase(衍合)。在本章我們會學習什麼是衍合,如何使用衍合,為什麼衍合操作如此富有魅力,以及我們應該在什麼情況下使用衍合。

衍合基礎

請回顧之前有關合併的一節(見圖 3-27),你會看到開發進程分叉到兩個不同分支,又各自提交了更新。

Pro-git-3-27.png
圖 3-27. 最初分叉的提交歷史。

之前介紹過,最容易的整合分支的方法是 merge 命令,它會把兩個分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)進行三方合併。如圖 3-28 所示:

Pro-git-3-28.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 演示了這一過程:

Pro-git-3-29.png
圖 3-29. 把 C3 裡產生的改變衍合到 C4 中。

現在,你可以回到 master 分支然後進行一次快進合併(fast-forward merge)(見圖 3-30):

Pro-git-3-30.png
圖 3-30. master 分支的快進。

現在,合併後的 C3(即現在的 C3’)所指的快照,同三方合併例子中的 C5 所指的快照內容一模一樣了。最後整合得到的結果沒有任何區別,但衍合能產生一個更為整潔的提交歷史。如果視察一個衍合過的分支的歷史記錄,看起來更清楚:仿佛所有修改都是先後進行的,儘管實際上它們原來是同時發生的。

你可以經常使用衍合,確保在遠端分支裡的提交歷史更清晰。比方說,某些項目自己不是維護者,但想幫點忙,就應該盡可能使用衍合:先在一個分支裡進行開發,當準備向主專案提交補丁的時候,再把它衍合到 origin/master 裡面。這樣,維護者就不需要做任何整合工作,只需根據你提供的倉庫位址作一次快進,或者採納你提交的補丁。

請注意,合併結果中最後一次提交所指向的快照,無論是通過一次衍合還是一次三方合併,都是同樣的快照內容,只是提交的歷史不同罷了。衍合按照每行改變發生的次序重演發生的改變,而合併是把最終結果合在一起。

更多有趣的衍合

你還可以在衍合分支以外的地方衍合。以圖 3-31 的歷史為例。你創建了一個特性分支 server 來給伺服器端代碼添加一些功能,然後提交 C3 和 C4。然後從 C3 的地方再增加一個 client 分支來對用戶端代碼進行一些修改,提交 C8 和 C9。最後,又回到 server 分支提交了 C10。

Pro-git-3-31.png
圖 3-31. 從一個特性分支裡再分出一個特性分支的歷史。

假設在接下來的一次軟體發佈中,你決定把用戶端的修改先合併到主線中,而暫緩併入服務端軟體的修改(因為還需要進一步測試)。你可以僅提取對用戶端的改變(C8 和 C9),然後通過使用 git rebase 的 --onto 選項來把它們在 master 分支上重演:

$ git rebase --onto master server client

這基本上等於在說「檢出 client 分支,找出 client 分支和 server 分支的共同祖先之後的變化,然後把它們在 master 上重演一遍」。是不是有點複雜?不過它的結果,如圖 3-32 所示,非常酷:

Pro-git-3-32.png
圖 3-32. 衍合一個特性分支上的另一個特性分支。

現在可以快進 master 分支了(見圖 3-33):

$ git checkout master
$ git merge client

Pro-git-3-33.png
圖 3-33. 快進 master 分支,使之包含 client 分支的變化。

現在你決定把 server 分支的變化也包含進來。可以直接把 server 分支衍合到 master 而不用手工轉到 server 分支再衍合。git rebase [主分支] [特性分支] 命令會先檢出特性分支 server,然後在主分支 master 上重演:

$ git rebase master server

於是 server 的進度應用到 master 的基礎上,如圖 3-34:

Pro-git-3-34.png
圖 3-34. 在 master 分支上衍合 server 分支。

然後快進主分支 master:

$ git checkout master
$ git merge server

現在 client 和 server 分支的變化都被整合了,不妨刪掉它們,把你的提交歷史變成圖 3-35 的樣子:

$ git branch -d client
$ git branch -d server

Pro-git-3-35.png
圖 3-35. 最終的提交歷史

衍合的風險

呃,奇妙的衍合也不是完美無缺的,一句話可以總結這點:

永遠不要衍合那些已經推送到公共倉庫的更新。

如果你遵循這條金科玉律,就不會出差錯。否則,群眾會仇恨你,你的朋友和家人也會嘲笑你、唾棄你。

在衍合的時候,實際上拋棄了一些現存的 commit 而創造了一些類似但不同的新 commit。如果你把commit 推送到某處然後其他人下載並在其基礎上工作,然後你用 git rebase 重寫了這些commit 再推送一次,你的合作者就不得不重新合併他們的工作,這樣當你再次從他們那裡獲取內容的時候事情就會變得一團糟。

下面我們用一個實際例子來說明為什麼公開的衍合會帶來問題。假設你從一個中央伺服器克隆然後在它的基礎上做了一些開發,提交歷史類似圖 3-36:

Pro-git-3-36.png
圖 3-36. 克隆一個倉庫,在其基礎上工作一番。

現在,其他人進行了一些包含一次合併的工作(得到結果 C6),然後把它推送到了中央伺服器。你獲取了這些資料並把它們合併到你本地的開發進程裡,讓你的歷史變成類似圖 3-37 這樣:

Pro-git-3-37.png
圖 3-37. 獲取更多提交,併入你的開發進程。

接下來,那個推送 C6 上來的人決定用衍合取代那次合併;他們用 git push --force 覆蓋了伺服器上的歷史,得到 C4’。然後你再從伺服器上獲取更新:

Pro-git-3-38.png
圖 3-38. 有人推送了衍合過的 C4’,丟棄了你作為開發基礎的 C6。

這時候,你需要再次合併這些內容,儘管之前已經做過一次了。衍合會改變這些 commit 的 SHA-1 校驗值,這樣 Git 會把它們當作新的 commit,然而這時候在你的提交歷史早就有了 C4 的內容(見圖 3-39):

Pro-git-3-39.png
圖 3-39. 你把相同的內容又合併了一遍,生成一個新的提交 C8。

你遲早都是要併入其他協作者提交的內容的,這樣才能保持同步。當你做完這些,你的提交歷史裡會同時包含 C4 和 C4’,兩者有著不同的 SHA-1 校驗值,但卻擁有一樣的作者日期與提交說明,令人費解!更糟糕的是,當你把這樣的歷史推送到伺服器,會再次把這些衍合的提交引入到中央伺服器,進一步迷惑其他人。

如果把衍合當成一種在推送之前清理提交歷史的手段,而且僅僅衍合那些永遠不會公開的 commit,那就不會有任何問題。如果衍合那些已經公開的 commit,而與此同時其他人已經用這些 commit 進行了後續的開發工作,那你有得麻煩了。