3.6 Git 分支 - 变基
變基
在 Git 中整合來(lái)自不同分支的修改主要有兩種方法:merge?以及?rebase。 在本節(jié)中我們將學(xué)習(xí)什么是“變基”,怎樣使用“變基”,并將展示該操作的驚艷之處,以及指出在何種情況下你應(yīng)避免使用它。
變基的基本操作
請(qǐng)回顧之前在?分支的合并?中的一個(gè)例子,你會(huì)看到開發(fā)任務(wù)分叉到兩個(gè)不同分支,又各自提交了更新。
Figure 35. 分叉的提交歷史之前介紹過,整合分支最容易的方法是?merge?命令。 它會(huì)把兩個(gè)分支的最新快照(C3?和?C4)以及二者最近的共同祖先(C2)進(jìn)行三方合并,合并的結(jié)果是生成一個(gè)新的快照(并提交)。
Figure 36. 通過合并操作來(lái)整合分叉了的歷史其實(shí),還有一種方法:你可以提取在?C4?中引入的補(bǔ)丁和修改,然后在?C3?的基礎(chǔ)上應(yīng)用一次。 在 Git 中,這種操作就叫做?變基。 你可以使用?rebase?命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。
在上面這個(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è)分支(即當(dāng)前分支?experiment、變基操作的目標(biāo)基底分支?master)的最近共同祖先?C2,然后對(duì)比當(dāng)前分支相對(duì)于該祖先的歷次提交,提取相應(yīng)的修改并存為臨時(shí)文件,然后將當(dāng)前分支指向目標(biāo)基底?C3, 最后以此將之前另存為臨時(shí)文件的修改依序應(yīng)用。(譯注:寫明了 commit id,以便理解,下同)
Figure 37. 將?C4?中的修改變基到?C3?上現(xiàn)在回到?master?分支,進(jìn)行一次快進(jìn)合并。
$ git checkout master $ git merge experiment Figure 38. master 分支的快進(jìn)合并此時(shí),C4'?指向的快照就和上面使用?merge?命令的例子中?C5?指向的快照一模一樣了。 這兩種整合方法的最終結(jié)果沒有任何區(qū)別,但是變基使得提交歷史更加整潔。 你在查看一個(gè)經(jīng)過變基的分支的歷史記錄時(shí)會(huì)發(fā)現(xiàn),盡管實(shí)際的開發(fā)工作是并行的,但它們看上去就像是串行的一樣,提交歷史是一條直線沒有分叉。
一般我們這樣做的目的是為了確保在向遠(yuǎn)程分支推送時(shí)能保持提交歷史的整潔——例如向某個(gè)其他人維護(hù)的項(xiàng)目貢獻(xiàn)代碼時(shí)。 在這種情況下,你首先在自己的分支里進(jìn)行開發(fā),當(dāng)開發(fā)完成時(shí)你需要先將你的代碼變基到?origin/master?上,然后再向主項(xiàng)目提交修改。 這樣的話,該項(xiàng)目的維護(hù)者就不再需要進(jìn)行整合工作,只需要快進(jìn)合并便可。
請(qǐng)注意,無(wú)論是通過變基,還是通過三方合并,整合的最終結(jié)果所指向的快照始終是一樣的,只不過提交歷史不同罷了。 變基是將一系列提交按照原有次序依次應(yīng)用到另一分支上,而合并是把最終結(jié)果合在一起。
更有趣的變基例子
在對(duì)兩個(gè)分支進(jìn)行變基時(shí),所生成的“重放”并不一定要在目標(biāo)分支上應(yīng)用,你也可以指定另外的一個(gè)分支進(jìn)行應(yīng)用。 就像?從一個(gè)特性分支里再分出一個(gè)特性分支的提交歷史?中的例子那樣。 你創(chuàng)建了一個(gè)特性分支?server,為服務(wù)端添加了一些功能,提交了?C3?和?C4。 然后從?C3?上創(chuàng)建了特性分支?client,為客戶端添加了一些功能,提交了?C8?和?C9。 最后,你回到?server?分支,又提交了?C10。
Figure 39. 從一個(gè)特性分支里再分出一個(gè)特性分支的提交歷史假設(shè)你希望將?client?中的修改合并到主分支并發(fā)布,但暫時(shí)并不想合并?server?中的修改,因?yàn)樗鼈冞€需要經(jīng)過更全面的測(cè)試。 這時(shí),你就可以使用?git rebase?命令的?--onto?選項(xiàng),選中在?client?分支里但不在?server?分支里的修改(即?C8?和?C9),將它們?cè)?master?分支上重放:
$ git rebase --onto master server client以上命令的意思是:“取出?client?分支,找出處于?client?分支和?server?分支的共同祖先之后的修改,然后把它們?cè)?master?分支上重放一遍”。 這理解起來(lái)有一點(diǎn)復(fù)雜,不過效果非常酷。
Figure 40. 截取特性分支上的另一個(gè)特性分支,然后變基到其他分支現(xiàn)在可以快進(jìn)合并?master?分支了。(如圖?快進(jìn)合并 master 分支,使之包含來(lái)自 client 分支的修改):
$ git checkout master $ git merge client Figure 41. 快進(jìn)合并 master 分支,使之包含來(lái)自 client 分支的修改接下來(lái)你決定將?server?分支中的修改也整合進(jìn)來(lái)。 使用?git rebase [basebranch] [topicbranch]?命令可以直接將特性分支(即本例中的?server)變基到目標(biāo)分支(即?master)上。這樣做能省去你先切換到?server?分支,再對(duì)其執(zhí)行變基命令的多個(gè)步驟。
$ git rebase master server如圖?將 server 中的修改變基到 master 上?所示,server?中的代碼被“續(xù)”到了?master?后面。
Figure 42. 將 server 中的修改變基到 master 上然后就可以快進(jìn)合并主分支 master 了:
$ git checkout master $ git merge server至此,client?和?server?分支中的修改都已經(jīng)整合到主分支里了,你可以刪除這兩個(gè)分支,最終提交歷史會(huì)變成圖?最終的提交歷史?中的樣子:
$ git branch -d client $ git branch -d server Figure 43. 最終的提交歷史變基的風(fēng)險(xiǎn)
呃,奇妙的變基也并非完美無(wú)缺,要用它得遵守一條準(zhǔn)則:
不要對(duì)在你的倉(cāng)庫(kù)外有副本的分支執(zhí)行變基。
如果你遵循這條金科玉律,就不會(huì)出差錯(cuò)。 否則,人民群眾會(huì)仇恨你,你的朋友和家人也會(huì)嘲笑你,唾棄你。
變基操作的實(shí)質(zhì)是丟棄一些現(xiàn)有的提交,然后相應(yīng)地新建一些內(nèi)容一樣但實(shí)際上不同的提交。 如果你已經(jīng)將提交推送至某個(gè)倉(cāng)庫(kù),而其他人也已經(jīng)從該倉(cāng)庫(kù)拉取提交并進(jìn)行了后續(xù)工作,此時(shí),如果你用?git rebase?命令重新整理了提交并再次推送,你的同伴因此將不得不再次將他們手頭的工作與你的提交進(jìn)行整合,如果接下來(lái)你還要拉取并整合他們修改過的提交,事情就會(huì)變得一團(tuán)糟。
讓我們來(lái)看一個(gè)在公開的倉(cāng)庫(kù)上執(zhí)行變基操作所帶來(lái)的問題。 假設(shè)你從一個(gè)中央服務(wù)器克隆然后在它的基礎(chǔ)上進(jìn)行了一些開發(fā)。 你的提交歷史如圖所示:
Figure 44. 克隆一個(gè)倉(cāng)庫(kù),然后在它的基礎(chǔ)上進(jìn)行了一些開發(fā)然后,某人又向中央服務(wù)器提交了一些修改,其中還包括一次合并。 你抓取了這些在遠(yuǎn)程分支上的修改,并將其合并到你本地的開發(fā)分支,然后你的提交歷史就會(huì)變成這樣:
Figure 45. 抓取別人的提交,合并到自己的開發(fā)分支接下來(lái),這個(gè)人又決定把合并操作回滾,改用變基;繼而又用?git push --force?命令覆蓋了服務(wù)器上的提交歷史。 之后你從服務(wù)器抓取更新,會(huì)發(fā)現(xiàn)多出來(lái)一些新的提交。
Figure 46. 有人推送了經(jīng)過變基的提交,并丟棄了你的本地開發(fā)所基于的一些提交結(jié)果就是你們兩人的處境都十分尷尬。 如果你執(zhí)行?git pull?命令,你將合并來(lái)自兩條提交歷史的內(nèi)容,生成一個(gè)新的合并提交,最終倉(cāng)庫(kù)會(huì)如圖所示:
Figure 47. 你將相同的內(nèi)容又合并了一次,生成了一個(gè)新的提交此時(shí)如果你執(zhí)行?git log?命令,你會(huì)發(fā)現(xiàn)有兩個(gè)提交的作者、日期、日志居然是一樣的,這會(huì)令人感到混亂。 此外,如果你將這一堆又推送到服務(wù)器上,你實(shí)際上是將那些已經(jīng)被變基拋棄的提交又找了回來(lái),這會(huì)令人感到更加混亂。 很明顯對(duì)方并不想在提交歷史中看到?C4?和?C6,因?yàn)橹熬褪撬堰@兩個(gè)提交通過變基丟棄的。
用變基解決變基
如果你?真的?遭遇了類似的處境,Git 還有一些高級(jí)魔法可以幫到你。 如果團(tuán)隊(duì)中的某人強(qiáng)制推送并覆蓋了一些你所基于的提交,你需要做的就是檢查你做了哪些修改,以及他們覆蓋了哪些修改。
實(shí)際上,Git 除了對(duì)整個(gè)提交計(jì)算 SHA-1 校驗(yàn)和以外,也對(duì)本次提交所引入的修改計(jì)算了校驗(yàn)和—— 即 “patch-id”。
如果你拉取被覆蓋過的更新并將你手頭的工作基于此進(jìn)行變基的話,一般情況下 Git 都能成功分辨出哪些是你的修改,并把它們應(yīng)用到新分支上。
舉個(gè)例子,如果遇到前面提到的?有人推送了經(jīng)過變基的提交,并丟棄了你的本地開發(fā)所基于的一些提交?那種情境,如果我們不是執(zhí)行合并,而是執(zhí)行?git rebase teamone/master, Git 將會(huì):
-
檢查哪些提交是我們的分支上獨(dú)有的(C2,C3,C4,C6,C7)
-
檢查其中哪些提交不是合并操作的結(jié)果(C2,C3,C4)
-
檢查哪些提交在對(duì)方覆蓋更新時(shí)并沒有被納入目標(biāo)分支(只有 C2 和 C3,因?yàn)?C4 其實(shí)就是 C4')
-
把查到的這些提交應(yīng)用在?teamone/master?上面
從而我們將得到與?你將相同的內(nèi)容又合并了一次,生成了一個(gè)新的提交?中不同的結(jié)果,如圖?在一個(gè)被變基然后強(qiáng)制推送的分支上再次執(zhí)行變基?所示。
Figure 48. 在一個(gè)被變基然后強(qiáng)制推送的分支上再次執(zhí)行變基要想上述方案有效,還需要對(duì)方在變基時(shí)確保 C4' 和 C4 是幾乎一樣的。 否則變基操作將無(wú)法識(shí)別,并新建另一個(gè)類似 C4 的補(bǔ)丁(而這個(gè)補(bǔ)丁很可能無(wú)法整潔的整合入歷史,因?yàn)檠a(bǔ)丁中的修改已經(jīng)存在于某個(gè)地方了)。
在本例中另一種簡(jiǎn)單的方法是使用?git pull --rebase?命令而不是直接?git pull。 又或者你可以自己手動(dòng)完成這個(gè)過程,先?git fetch,再?git rebase teamone/master。
如果你習(xí)慣使用?git pull?,同時(shí)又希望默認(rèn)使用選項(xiàng)?--rebase,你可以執(zhí)行這條語(yǔ)句?git config --global pull.rebase true?來(lái)更改?pull.rebase?的默認(rèn)配置。
只要你把變基命令當(dāng)作是在推送前清理提交使之整潔的工具,并且只在從未推送至共用倉(cāng)庫(kù)的提交上執(zhí)行變基命令,就不會(huì)有事。 假如在那些已經(jīng)被推送至共用倉(cāng)庫(kù)的提交上執(zhí)行變基命令,并因此丟棄了一些別人的開發(fā)所基于的提交,那你就有大麻煩了,你的同事也會(huì)因此鄙視你。
如果你或你的同事在某些情形下決意要這么做,請(qǐng)一定要通知每個(gè)人執(zhí)行?git pull --rebase?命令,這樣盡管不能避免傷痛,但能有所緩解。
變基 vs. 合并
至此,你已在實(shí)戰(zhàn)中學(xué)習(xí)了變基和合并的用法,你一定會(huì)想問,到底哪種方式更好。 在回答這個(gè)問題之前,讓我們退后一步,想討論一下提交歷史到底意味著什么。
有一種觀點(diǎn)認(rèn)為,倉(cāng)庫(kù)的提交歷史即是?記錄實(shí)際發(fā)生過什么。 它是針對(duì)歷史的文檔,本身就有價(jià)值,不能亂改。 從這個(gè)角度看來(lái),改變提交歷史是一種褻瀆,你使用_謊言_掩蓋了實(shí)際發(fā)生過的事情。 如果由合并產(chǎn)生的提交歷史是一團(tuán)糟怎么辦? 既然事實(shí)就是如此,那么這些痕跡就應(yīng)該被保留下來(lái),讓后人能夠查閱。
另一種觀點(diǎn)則正好相反,他們認(rèn)為提交歷史是?項(xiàng)目過程中發(fā)生的事。 沒人會(huì)出版一本書的第一版草稿,軟件維護(hù)手冊(cè)也是需要反復(fù)修訂才能方便使用。 持這一觀點(diǎn)的人會(huì)使用 rebase 及 filter-branch 等工具來(lái)編寫故事,怎么方便后來(lái)的讀者就怎么寫。
現(xiàn)在,讓我們回到之前的問題上來(lái),到底合并還是變基好?希望你能明白,這并沒有一個(gè)簡(jiǎn)單的答案。 Git 是一個(gè)非常強(qiáng)大的工具,它允許你對(duì)提交歷史做許多事情,但每個(gè)團(tuán)隊(duì)、每個(gè)項(xiàng)目對(duì)此的需求并不相同。 既然你已經(jīng)分別學(xué)習(xí)了兩者的用法,相信你能夠根據(jù)實(shí)際情況作出明智的選擇。
總的原則是,只對(duì)尚未推送或分享給別人的本地修改執(zhí)行變基操作清理歷史,從不對(duì)已推送至別處的提交執(zhí)行變基操作,這樣,你才能享受到兩種方式帶來(lái)的便利。
from:?https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA
總結(jié)
以上是生活随笔為你收集整理的3.6 Git 分支 - 变基的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JSP简明教程
- 下一篇: Git Rebase教程: 用Git R