把一個(gè)分支中的修改整合到另一個(gè)分支的辦法有兩種:merge
和 rebase
(譯注:rebase
的翻譯暫定為“衍合”,大家知道就可以了。)。在本章我們會(huì)學(xué)習(xí)什么是衍合,如何使用衍合,為什么衍合操作如此富有魅力,以及我們應(yīng)該在什么情況下使用衍合。
請(qǐng)回顧之前有關(guān)合并的一節(jié)(見圖 3-27),你會(huì)看到開發(fā)進(jìn)程分叉到兩個(gè)不同分支,又各自提交了更新。
圖 3-27. 最初分叉的提交歷史。
之前介紹過,最容易的整合分支的方法是 merge
命令,它會(huì)把兩個(gè)分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)進(jìn)行三方合并,合并的結(jié)果是產(chǎn)生一個(gè)新的提交對(duì)象(C5)。如圖 3-28 所示:
圖 3-28. 通過合并一個(gè)分支來整合分叉了的歷史。
其實(shí),還有另外一個(gè)選擇:你可以把在 C3 里產(chǎn)生的變化補(bǔ)丁在 C4 的基礎(chǔ)上重新打一遍。在 Git 里,這種操作叫做衍合(rebase)。有了 rebase
命令,就可以把在一個(gè)分支里提交的改變移到另一個(gè)分支里重放一遍。
在上面這個(gè)例子中,運(yùn)行:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是回到兩個(gè)分支最近的共同祖先,根據(jù)當(dāng)前分支(也就是要進(jìn)行衍合的分支 experiment
)后續(xù)的歷次提交對(duì)象(這里只有一個(gè) C3),生成一系列文件補(bǔ)丁,然后以基底分支(也就是主干分支 master
)最后一個(gè)提交對(duì)象(C4)為新的出發(fā)點(diǎn),逐個(gè)應(yīng)用之前準(zhǔn)備好的補(bǔ)丁文件,最后會(huì)生成一個(gè)新的合并提交對(duì)象(C3'),從而改寫 experiment
的提交歷史,使它成為 master
分支的直接下游,如圖 3-29 所示:
圖 3-29. 把 C3 里產(chǎn)生的改變到 C4 上重演一遍。
現(xiàn)在回到 master
分支,進(jìn)行一次快進(jìn)合并(見圖 3-30):
圖 3-30. master 分支的快進(jìn)。
現(xiàn)在的 C3' 對(duì)應(yīng)的快照,其實(shí)和普通的三方合并,即上個(gè)例子中的 C5 對(duì)應(yīng)的快照內(nèi)容一模一樣了。雖然最后整合得到的結(jié)果沒有任何區(qū)別,但衍合能產(chǎn)生一個(gè)更為整潔的提交歷史。如果視察一個(gè)衍合過的分支的歷史記錄,看起來會(huì)更清楚:仿佛所有修改都是在一根線上先后進(jìn)行的,盡管實(shí)際上它們?cè)臼峭瑫r(shí)并行發(fā)生的。
一般我們使用衍合的目的,是想要得到一個(gè)能在遠(yuǎn)程分支上干凈應(yīng)用的補(bǔ)丁 — 比如某些項(xiàng)目你不是維護(hù)者,但想幫點(diǎn)忙的話,最好用衍合:先在自己的一個(gè)分支里進(jìn)行開發(fā),當(dāng)準(zhǔn)備向主項(xiàng)目提交補(bǔ)丁的時(shí)候,根據(jù)最新的 origin/master
進(jìn)行一次衍合操作然后再提交,這樣維護(hù)者就不需要做任何整合工作(譯注:實(shí)際上是把解決分支補(bǔ)丁同最新主干代碼之間沖突的責(zé)任,化轉(zhuǎn)為由提交補(bǔ)丁的人來解決。),只需根據(jù)你提供的倉(cāng)庫(kù)地址作一次快進(jìn)合并,或者直接采納你提交的補(bǔ)丁。
請(qǐng)注意,合并結(jié)果中最后一次提交所指向的快照,無(wú)論是通過衍合,還是三方合并,都會(huì)得到相同的快照內(nèi)容,只不過提交歷史不同罷了。衍合是按照每行的修改次序重演一遍修改,而合并是把最終結(jié)果合在一起。
衍合也可以放到其他分支進(jìn)行,并不一定非得根據(jù)分化之前的分支。以圖 3-31 的歷史為例,我們?yōu)榱私o服務(wù)器端代碼添加一些功能而創(chuàng)建了特性分支 server
,然后提交 C3 和 C4。然后又從 C3 的地方再增加一個(gè) client
分支來對(duì)客戶端代碼進(jìn)行一些相應(yīng)修改,所以提交了 C8 和 C9。最后,又回到 server
分支提交了 C10。
圖 3-31. 從一個(gè)特性分支里再分出一個(gè)特性分支的歷史。
假設(shè)在接下來的一次軟件發(fā)布中,我們決定先把客戶端的修改并到主線中,而暫緩并入服務(wù)端軟件的修改(因?yàn)檫€需要進(jìn)一步測(cè)試)。這個(gè)時(shí)候,我們就可以把基于 server
分支而非 master
分支的改變(即 C8 和 C9),跳過 server
直接放到 master
分支中重演一遍,但這需要用 git rebase
的 --onto
選項(xiàng)指定新的基底分支 master
:
$ git rebase --onto master server client
這好比在說:“取出 client
分支,找出 client
分支和 server
分支的共同祖先之后的變化,然后把它們?cè)?nbsp;master
上重演一遍”。是不是有點(diǎn)復(fù)雜?不過它的結(jié)果如圖 3-32 所示,非??幔ㄗg注:雖然 client
里的 C8, C9 在 C3 之后,但這僅表明時(shí)間上的先后,而非在 C3 修改的基礎(chǔ)上進(jìn)一步改動(dòng),因?yàn)?nbsp;server
和 client
這兩個(gè)分支對(duì)應(yīng)的代碼應(yīng)該是兩套文件,雖然這么說不是很嚴(yán)格,但應(yīng)理解為在 C3 時(shí)間點(diǎn)之后,對(duì)另外的文件所做的 C8,C9 修改,放到主干重演。):
圖 3-32. 將特性分支上的另一個(gè)特性分支衍合到其他分支。
現(xiàn)在可以快進(jìn) master
分支了(見圖 3-33):
$ git checkout master
$ git merge client
圖 3-33. 快進(jìn) master 分支,使之包含 client 分支的變化。
現(xiàn)在我們決定把 server
分支的變化也包含進(jìn)來。我們可以直接把 server
分支衍合到 master
,而不用手工切換到 server
分支后再執(zhí)行衍合操作 — git rebase [主分支] [特性分支]
命令會(huì)先取出特性分支 server
,然后在主分支 master
上重演:
$ git rebase master server
于是,server
的進(jìn)度應(yīng)用到 master
的基礎(chǔ)上,如圖 3-34 所示:
圖 3-34. 在 master 分支上衍合 server 分支。
然后就可以快進(jìn)主干分支 master
了:
$ git checkout master
$ git merge server
現(xiàn)在 client
和 server
分支的變化都已經(jīng)集成到主干分支來了,可以刪掉它們了。最終我們的提交歷史會(huì)變成圖 3-35 的樣子:
$ git branch -d client
$ git branch -d server
圖 3-35. 最終的提交歷史
呃,奇妙的衍合也并非完美無(wú)缺,要用它得遵守一條準(zhǔn)則:
一旦分支中的提交對(duì)象發(fā)布到公共倉(cāng)庫(kù),就千萬(wàn)不要對(duì)該分支進(jìn)行衍合操作。
如果你遵循這條金科玉律,就不會(huì)出差錯(cuò)。否則,人民群眾會(huì)仇恨你,你的朋友和家人也會(huì)嘲笑你,唾棄你。
在進(jìn)行衍合的時(shí)候,實(shí)際上拋棄了一些現(xiàn)存的提交對(duì)象而創(chuàng)造了一些類似但不同的新的提交對(duì)象。如果你把原來分支中的提交對(duì)象發(fā)布出去,并且其他人更新下載后在其基礎(chǔ)上開展工作,而稍后你又用 git rebase
拋棄這些提交對(duì)象,把新的重演后的提交對(duì)象發(fā)布出去的話,你的合作者就不得不重新合并他們的工作,這樣當(dāng)你再次從他們那里獲取內(nèi)容時(shí),提交歷史就會(huì)變得一團(tuán)糟。
下面我們用一個(gè)實(shí)際例子來說明為什么公開的衍合會(huì)帶來問題。假設(shè)你從一個(gè)中央服務(wù)器克隆然后在它的基礎(chǔ)上搞了一些開發(fā),提交歷史類似圖 3-36 所示:
圖 3-36. 克隆一個(gè)倉(cāng)庫(kù),在其基礎(chǔ)上工作一番。
現(xiàn)在,某人在 C1 的基礎(chǔ)上做了些改變,并合并他自己的分支得到結(jié)果 C6,推送到中央服務(wù)器。當(dāng)你抓取并合并這些數(shù)據(jù)到你本地的開發(fā)分支中后,會(huì)得到合并結(jié)果 C7,歷史提交會(huì)變成圖 3-37 這樣:
圖 3-37. 抓取他人提交,并入自己主干。
接下來,那個(gè)推送 C6 上來的人決定用衍合取代之前的合并操作;繼而又用 git push --force
覆蓋了服務(wù)器上的歷史,得到 C4'。而之后當(dāng)你再?gòu)姆?wù)器上下載最新提交后,會(huì)得到:
圖 3-38. 有人推送了衍合后得到的 C4',丟棄了你作為開發(fā)基礎(chǔ)的 C4 和 C6。
下載更新后需要合并,但此時(shí)衍合產(chǎn)生的提交對(duì)象 C4' 的 SHA-1 校驗(yàn)值和之前 C4 完全不同,所以 Git 會(huì)把它們當(dāng)作新的提交對(duì)象處理,而實(shí)際上此刻你的提交歷史 C7 中早已經(jīng)包含了 C4 的修改內(nèi)容,于是合并操作會(huì)把 C7 和 C4' 合并為 C8(見圖 3-39):
圖 3-39. 你把相同的內(nèi)容又合并了一遍,生成一個(gè)新的提交 C8。
C8 這一步的合并是遲早會(huì)發(fā)生的,因?yàn)橹挥羞@樣你才能和其他協(xié)作者提交的內(nèi)容保持同步。而在 C8 之后,你的提交歷史里就會(huì)同時(shí)包含 C4 和 C4',兩者有著不同的 SHA-1 校驗(yàn)值,如果用 git log
查看歷史,會(huì)看到兩個(gè)提交擁有相同的作者日期與說明,令人費(fèi)解。而更糟的是,當(dāng)你把這樣的歷史推送到服務(wù)器后,會(huì)再次把這些衍合后的提交引入到中央服務(wù)器,進(jìn)一步困擾其他人(譯注:這個(gè)例子中,出問題的責(zé)任方是那個(gè)發(fā)布了 C6 后又用衍合發(fā)布 C4' 的人,其他人會(huì)因此反饋雙重歷史到共享主干,從而混淆大家的視聽。)。
如果把衍合當(dāng)成一種在推送之前清理提交歷史的手段,而且僅僅衍合那些尚未公開的提交對(duì)象,就沒問題。如果衍合那些已經(jīng)公開的提交對(duì)象,并且已經(jīng)有人基于這些提交對(duì)象開展了后續(xù)開發(fā)工作的話,就會(huì)出現(xiàn)叫人沮喪的麻煩。
更多建議: