Git 分支 - 分支的衍合

分支的衍合

把一個分支中的修改整合到另外一個分支的辦法有兩種:merge 和 rebase(譯註:rebase 的翻譯暫定爲「衍合」,你們知道就能夠了。)。在本章咱們會學習什麼是衍合,如何使用衍合,爲何衍合操做如此富有魅力,以及咱們應該在什麼狀況下使用衍合。git

基本的衍合操做

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


圖 3-27. 最初分叉的提交歷史。學習

以前介紹過,最容易的整合分支的方法是 merge 命令,它會把兩個分支最新的快照(C3 和 C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)。如圖 3-28 所示:測試


圖 3-28. 經過合併一個分支來整合分叉了的歷史。spa

其實,還有另一個選擇:你能夠把在 C3 裏產生的變化補丁在 C4 的基礎上從新打一遍。在 Git 裏,這種操做叫作衍合(rebase)。有了 rebase 命令,就能夠把在一個分支裏提交的改變移到另外一個分支裏重放一遍。翻譯

在上面這個例子中,運行:code

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是回到兩個分支最近的共同祖先,根據當前分支(也就是要進行衍合的分支 experiment)後續的歷次提交對象(這裏只有一個 C3),生成一系列文件補丁,而後以基底分支(也就是主幹分支master)最後一個提交對象(C4)爲新的出發點,逐個應用以前準備好的補丁文件,最後會生成一個新的合併提交對象(C3'),從而改寫 experiment 的提交歷史,使它成爲 master 分支的直接下游,如圖 3-29 所示:server


圖 3-29. 把 C3 裏產生的改變到 C4 上重演一遍。對象

如今回到 master 分支,進行一次快進合併(見圖 3-30):進程


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

如今的 C3' 對應的快照,其實和普通的三方合併,即上個例子中的 C5 對應的快照內容如出一轍了。雖然最後整合獲得的結果沒有任何區別,但衍合能產生一個更爲整潔的提交歷史。若是視察一個衍合過的分支的歷史記錄,看起來會更清楚:彷彿全部修改都是在一根線上前後進行的,儘管實際上它們本來是同時並行發生的。

通常咱們使用衍合的目的,是想要獲得一個能在遠程分支上乾淨應用的補丁 — 好比某些項目你不是維護者,但想幫點忙的話,最好用衍合:先在本身的一個分支裏進行開發,當準備向主項目提交補丁的時候,根據最新的 origin/master 進行一次衍合操做而後再提交,這樣維護者就不須要作任何整合工做(譯註:其實是把解決分支補丁同最新主幹代碼之間衝突的責任,化轉爲由提交補丁的人來解決。),只需根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的補丁。

請注意,合併結果中最後一次提交所指向的快照,不管是經過衍合,仍是三方合併,都會獲得相同的快照內容,只不過提交歷史不一樣罷了。衍合是按照每行的修改次序重演一遍修改,而合併是把最終結果合在一塊兒。

有趣的衍合

衍合也能夠放到其餘分支進行,並不必定非得根據分化以前的分支。以圖 3-31 的歷史爲例,咱們爲了給服務器端代碼添加一些功能而建立了特性分支 server,而後提交 C3 和 C4。而後又從 C3 的地方再增長一個client 分支來對客戶端代碼進行一些相應修改,因此提交了 C8 和 C9。最後,又回到 server 分支提交了 C10。


圖 3-31. 從一個特性分支裏再分出一個特性分支的歷史。

假設在接下來的一次軟件發佈中,咱們決定先把客戶端的修改併到主線中,而暫緩併入服務端軟件的修改(由於還須要進一步測試)。這個時候,咱們就能夠把基於 server 分支而非 master 分支的改變(即 C8 和 C9),跳過 server 直接放到 master 分支中重演一遍,但這須要用 git rebase 的 --onto 選項指定新的基底分支 master

$ git rebase --onto master server client

這比如在說:「取出 client 分支,找出 client 分支和 server 分支的共同祖先以後的變化,而後把它們在 master 上重演一遍」。是否是有點複雜?不過它的結果如圖 3-32 所示,很是酷(譯註:雖然 client裏的 C8, C9 在 C3 以後,但這僅代表時間上的前後,而非在 C3 修改的基礎上進一步改動,由於 server 和client 這兩個分支對應的代碼應該是兩套文件,雖然這麼說不是很嚴格,但應理解爲在 C3 時間點以後,對另外的文件所作的 C8,C9 修改,放到主幹重演。):


圖 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. 最終的提交歷史

衍合的風險

呃,奇妙的衍合也並不是天衣無縫,要用它得遵照一條準則:

一旦分支中的提交對象發佈到公共倉庫,就千萬不要對該分支進行衍合操做。

若是你遵循這條金科玉律,就不會出差錯。不然,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。

在進行衍合的時候,實際上拋棄了一些現存的提交對象而創造了一些相似但不一樣的新的提交對象。若是你把原來分支中的提交對象發佈出去,而且其餘人更新下載後在其基礎上開展工做,而稍後你又用 git rebase 拋棄這些提交對象,把新的重演後的提交對象發佈出去的話,你的合做者就不得不從新合併他們的工做,這樣當你再次從他們那裏獲取內容時,提交歷史就會變得一團糟。

下面咱們用一個實際例子來講明爲何公開的衍合會帶來問題。假設你從一箇中央服務器克隆而後在它的基礎上搞了一些開發,提交歷史相似圖 3-36 所示:


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

如今,某人在 C1 的基礎上作了些改變,併合並他本身的分支獲得結果 C6,推送到中央服務器。當你抓取併合並這些數據到你本地的開發分支中後,會獲得合併結果 C7,歷史提交會變成圖 3-37 這樣:


圖 3-37. 抓取他人提交,併入本身主幹。

接下來,那個推送 C6 上來的人決定用衍合取代以前的合併操做;繼而又用 git push --force 覆蓋了服務器上的歷史,獲得 C4'。而以後當你再從服務器上下載最新提交後,會獲得:


圖 3-38. 有人推送了衍合後獲得的 C4',丟棄了你做爲開發基礎的 C4 和 C6。

下載更新後須要合併,但此時衍合產生的提交對象 C4' 的 SHA-1 校驗值和以前 C4 徹底不一樣,因此 Git 會把它們看成新的提交對象處理,而實際上此刻你的提交歷史 C7 中早已經包含了 C4 的修改內容,因而合併操做會把 C7 和 C4' 合併爲 C8(見圖 3-39):


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

C8 這一步的合併是早晚會發生的,由於只有這樣你才能和其餘協做者提交的內容保持同步。而在 C8 以後,你的提交歷史裏就會同時包含 C4 和 C4',二者有着不一樣的 SHA-1 校驗值,若是用 git log 查看歷史,會看到兩個提交擁有相同的做者日期與說明,使人費解。而更糟的是,當你把這樣的歷史推送到服務器後,會再次把這些衍合後的提交引入到中央服務器,進一步困擾其餘人(譯註:這個例子中,出問題的責任方是那個發佈了 C6 後又用衍合發布 C4' 的人,其餘人會所以反饋雙重歷史到共享主幹,從而混淆你們的視聽。)。

若是把衍合當成一種在推送以前清理提交歷史的手段,並且僅僅衍合那些還沒有公開的提交對象,就沒問題。若是衍合那些已經公開的提交對象,而且已經有人基於這些提交對象開展了後續開發工做的話,就會出現叫人沮喪的麻煩。

相關文章
相關標籤/搜索