將一個(gè)分支合并到另一個(gè)分支有兩種胖替,一種是大多都很熟悉的 merge(合并),另一種就是本篇要介紹的 rebase(衍合)。
看下本文綱要
已經(jīng)有 merge 了,為什么需要 rebase 怠肋?我們先跟著官方文檔學(xué)習(xí)下 rebase 的基本概念
rebase 是做什么的?
(如果你大概知道 rebase 是做什么的淹朋,可以直接跳到第二趴笙各,實(shí)戰(zhàn) rebase)
在了解 rebase 之前钉答,先溫習(xí)下 merge 的過(guò)程
假設(shè)現(xiàn)在基于 <master> 分支,檢出一個(gè) <hotfix> 分支
然后在這個(gè)分支 <hotfix> 做一些修改酪惭,生成兩個(gè)提交(C3 和 C4),同時(shí)也有其他人在 <master> 分支做了提交(C5 和 C6 )者甲,那么在同一位置(C2) <master> 和 hotfix 兩個(gè)分支分別前進(jìn)了
使用 merge 合并分支
此時(shí)拉取 <master> 分支內(nèi)容并合并到 <hotfix> 分支中春感,Git 會(huì)把兩個(gè)分支最新的快照(C3~C6)以及他們共同的祖先(C2)進(jìn)行合并,然后就形成了一次新的合并提交(C7)
如果想讓 <hotfix> 分支看起來(lái)沒(méi)有經(jīng)過(guò)任何合并一樣虏缸,就可以使用 git rebase
使用 rebase 合并
rebase 有的翻譯成衍合鲫懒,有的直接翻譯成變基,變基就很好理解了刽辙,就是重新設(shè)定基底窥岩。
$ git checkout hotfix
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: ***
git rebase
這個(gè)命令會(huì)將 <hotfix> 分支里的提交(C3、C4)取消宰缤,保存成臨時(shí)文件颂翼,然后把 <hotfix> 更新到最新的 <master> 分支,最后把保存的這些內(nèi)容應(yīng)用到 <master> 分支上慨灭,這個(gè)過(guò)程就是衍合朦乏。
rebase 原理 衍合前回到兩個(gè)分支(所在的分支和想要衍合的分支)的共同祖先,提取你所在分支每次提交時(shí)產(chǎn)生的差異(diff)氧骤,把這些差異分別保存到臨時(shí)文件里呻疹,然后從當(dāng)前分支轉(zhuǎn)換到你需要衍合的分支,依照順序使用每一個(gè)差異補(bǔ)丁文件筹陵。
當(dāng) <hotfix> 更新后刽锤,它會(huì)指向新創(chuàng)建的提交,而老的提交會(huì)被丟棄
使用 git rebase
之后產(chǎn)生的歷史會(huì)是如下
還是有點(diǎn)亂嗎朦佩?再通俗點(diǎn)呢并思,原來(lái)的「基點(diǎn)」是 C2,現(xiàn)在把「基點(diǎn)」改變到要衍合的分支處语稠,就是把「基點(diǎn)」搞到 C6 哪兒去纺荧,然后把改變的內(nèi)容,應(yīng)用上去颅筋。然后分支歷史就被改寫啦宙暇。
實(shí)戰(zhàn)體驗(yàn) rebase
概念和效果清楚了,實(shí)戰(zhàn)一下议泵,推薦一個(gè)學(xué)習(xí) Git 實(shí)踐的網(wǎng)站占贫,這個(gè)工具非常有意思,可以用來(lái)學(xué)習(xí)模擬基本的 Git 操作先口,戳這里學(xué)習(xí)
模擬以下操作過(guò)程
- 在 <master>:C1 處創(chuàng)建 <b1> 和 <b2> 分支型奥,在三個(gè)分支分別產(chǎn)生提交 C2~C4
<b1>:C2
<b2>:C3
<master>:C4
那么當(dāng)前的歷史節(jié)點(diǎn)如下圖:
2:衍合 b1 分支到 master 分支
<master>: git rebase b1
衍合過(guò)程分析:先回到 C1瞳收,提取當(dāng)前分支<master> 和 C1 的差異,形成 C4' (C4 和 C1 的差異) 保存起來(lái)厢汹,當(dāng)前分支<master> 轉(zhuǎn)換到要衍合的分支<b1>(C2處)螟深,再把 C4' 應(yīng)用進(jìn)去。
這里要注意一下:在命令里面可以看到 Git 提示衍合過(guò)程
$ git rebase b1
First, rewinding head to replay your work on top of it...
Applying: C4
這里其實(shí)應(yīng)用的并不是 C4 那次 commit烫葬,而是 C4 和 C1 比較后的 diff界弧,C4'。所以 C4 和 C4' 是兩個(gè)不同的提交(會(huì)產(chǎn)生不同的歷史搭综,但是內(nèi)容是一樣的)垢箕。
- 再做一次衍合,將 b2 也合并過(guò)來(lái)
<master>:git rebase b2
$ git rebase b2
First, rewinding head to replay your work on top of it...
Applying: C2
Applying: C4
還是會(huì)提取 <master> 分支 和 C1 的 差異兑巾,產(chǎn)生了 C2' 和 C4''条获,轉(zhuǎn)到 <b2>分支再把 diff 應(yīng)用進(jìn)去。
- 此時(shí)如果 b2 merge master 的話 實(shí)際就是快速跟進(jìn)了蒋歌。
<b2>:git merge master
同樣的過(guò)程帅掘,把上面的 rebase
都換成 merge
小結(jié)
可以看到不論用 rebase 還是 merge 得到的結(jié)果是沒(méi)有區(qū)別的,但是衍合能產(chǎn)生一個(gè)更為整潔的提交歷史堂油。如果視察一個(gè)衍合過(guò)的分支的歷史記錄锄开,看起來(lái)更清楚:仿佛所有修改都是先后進(jìn)行的,盡管實(shí)際上它們?cè)瓉?lái)是同時(shí)發(fā)生的称诗。
你可以經(jīng)常使用衍合萍悴,確保在遠(yuǎn)程分支里的提交歷史更清晰。比方說(shuō)寓免,某些項(xiàng)目自己不是維護(hù)者癣诱,但想幫點(diǎn)忙,就應(yīng)該盡可能使用衍合:先在一個(gè)分支里進(jìn)行開(kāi)發(fā)袜香,當(dāng)準(zhǔn)備向主項(xiàng)目提交補(bǔ)丁的時(shí)候撕予,再把它衍合到 origin/master 里面。這樣蜈首,維護(hù)者就不需要做任何整合工作实抡,只需根據(jù)你提供的倉(cāng)庫(kù)地址作一次快進(jìn),或者采納你提交的補(bǔ)丁欢策。
請(qǐng)注意吆寨,合并結(jié)果中最后一次提交所指向的快照,無(wú)論是通過(guò)一次衍合還是一次三方合并踩寇,都是同樣的快照內(nèi)容啄清,只是提交的歷史不同罷了。衍合按照每行改變發(fā)生的次序重演發(fā)生的改變俺孙,而合并是把最終結(jié)果合在一起辣卒。
rebase 的其他操作
前面用了大篇幅來(lái)說(shuō)明 rebase 的概念及實(shí)踐掷贾,看下 rebase 的其他操作
onto 選項(xiàng)
--onto
剪切指定范圍內(nèi)提交節(jié)點(diǎn),并在指向的分支上對(duì)這些節(jié)點(diǎn)執(zhí)行變基操作
git rebase --onto base from to
將 (from,to] 范圍內(nèi)所有提交的節(jié)點(diǎn)在 base 指向的節(jié)點(diǎn)之后重建
看官方的例子了解下這個(gè) onto 選項(xiàng)
你創(chuàng)建了一個(gè)特性分支 <server> 來(lái)給服務(wù)器端代碼添加一些功能荣茫,然后提交 C3 和 C4想帅。然后從 C3 的地方再增加一個(gè) <client> 分支來(lái)對(duì)客戶端代碼進(jìn)行一些修改,提交 C8 和 C9啡莉。最后港准,又回到 <server> 分支提交了 C10。
假設(shè)在接下來(lái)的一次軟件發(fā)布中票罐,你決定把客戶端的修改先合并到主線中叉趣,而暫緩并入服務(wù)端軟件的修改(因?yàn)檫€需要進(jìn)一步測(cè)試)泞边。你可以僅提取對(duì)客戶端的改變(C8 和C9)该押,然后通過(guò)使用 git rebase
的 --onto
選項(xiàng)來(lái)把它們?cè)?<master> 分支上重演:
$ git rebase --onto master server client
這基本上等于在說(shuō)“檢出 <client> 分支,找出 <client> 分支和 <server> 分支的共同祖先之后的變化阵谚,然后把它們?cè)?<master> 上重演一遍”蚕礼。是不是有點(diǎn)復(fù)雜?不過(guò)它的結(jié)果梢什,非车斓牛酷:
現(xiàn)在你決定把 <server> 分支的變化也包含進(jìn)來(lái)∥宋纾可以直接把 <server> 分支衍合到 <master> 而不用手工轉(zhuǎn)到 <server> 分支再衍合囤躁。git rebase [主分支] [特性分支]
命令會(huì)先檢出特性分支 <server>,然后在主分支 <master> 上重演
$ git rebase master server
rebase 沖突處理
在 rebase 的過(guò)程中荔睹,也許也會(huì)出現(xiàn)沖突狸演,這時(shí)候 Git 會(huì)停止 rebase 讓你解決沖突(這個(gè)過(guò)程和 merge 是一樣的)
手動(dòng)處理沖突之后,通過(guò) add
命令暫存沖突文件
可以使用 --continue
選項(xiàng)僻他,繼續(xù)本次操作
git rebase --continue
或者使用 --abort
選項(xiàng) 放棄本次衍合操作
git rebase --abort
在進(jìn)行衍合或合并操作時(shí)宵距,Git 類似新建了一個(gè)匿名分支,當(dāng)使用 --abort
選項(xiàng)時(shí)吨拗, Git 會(huì)切回原分支满哪,丟棄匿名分支,放棄本次操作劝篷。
如果使用 Git 管理工具哨鸭,當(dāng) merge 或者 rebase 操作有沖突需要處理時(shí),都會(huì)有相關(guān)提示娇妓,比如有哪些文件有沖突兔跌,也有 continue 和 abort 操作供選擇。
rebase 還有其他的一些選項(xiàng)峡蟋,比如 -i
坟桅,后面學(xué)習(xí)重寫歷史的時(shí)候再做補(bǔ)充
rebase 的風(fēng)險(xiǎn)和使用場(chǎng)景
永遠(yuǎn)不要衍合哪些已經(jīng)推送到公共倉(cāng)庫(kù)的更新
衍合的時(shí)候华望,實(shí)際上拋棄了一些已經(jīng)存在的 commit 而創(chuàng)建了一些類似的但是不同的新 commit。如果把這個(gè) commit(假設(shè)是 C6)推送到遠(yuǎn)程端仅乓,其他人在其基礎(chǔ)上工作赖舟,然后你使用 git rebase
重寫了C6 推送了 C6',那么別人不得不重新合并夸楣,而這次合并的內(nèi)容和之前已經(jīng)獲取到的 C6 是一樣宾抓,而再獲取的時(shí)候就可能是 C6-C7-C6'-C8 ( C6 和 C6' 有著相同的內(nèi)容(包括作者、提交說(shuō)明等)豫喧,C7 是其他人的提交石洗,C8是其他人合并 C6' 產(chǎn)生的提交),這個(gè)歷史記錄會(huì)變的非常令人費(fèi)解紧显。
如果把衍合當(dāng)成一種在推送之前清理提交歷史的手段讲衫,而且僅僅衍合那些永遠(yuǎn)不會(huì)公開(kāi)的 commit,那就不會(huì)有任何問(wèn)題孵班。如果衍合那些已經(jīng)公開(kāi)的commit涉兽,而與此同時(shí)其他人已經(jīng)用這些 commit 進(jìn)行了后續(xù)的開(kāi)發(fā)工作,那就很麻煩了篙程。
參考:Git 分支 - 變基枷畏;rebase