將一個分支合并到另一個分支有兩種,一種是大多都很熟悉的 merge(合并),另一種就是本篇要介紹的 rebase(衍合)。
看下本文綱要

已經(jīng)有 merge 了,為什么需要 rebase ?我們先跟著官方文檔學(xué)習(xí)下 rebase 的基本概念
rebase 是做什么的?
(如果你大概知道 rebase 是做什么的,可以直接跳到第二趴,實戰(zhàn) rebase)
在了解 rebase 之前,先溫習(xí)下 merge 的過程
假設(shè)現(xiàn)在基于 <master> 分支,檢出一個 <hotfix> 分支

然后在這個分支 <hotfix> 做一些修改,生成兩個提交(C3 和 C4),同時也有其他人在 <master> 分支做了提交(C5 和 C6 ),那么在同一位置(C2) <master> 和 hotfix 兩個分支分別前進了

使用 merge 合并分支
此時拉取 <master> 分支內(nèi)容并合并到 <hotfix> 分支中,Git 會把兩個分支最新的快照(C3~C6)以及他們共同的祖先(C2)進行合并,然后就形成了一次新的合并提交(C7)

如果想讓 <hotfix> 分支看起來沒有經(jīng)過任何合并一樣,就可以使用 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 這個命令會將 <hotfix> 分支里的提交(C3、C4)取消,保存成臨時文件,然后把 <hotfix> 更新到最新的 <master> 分支,最后把保存的這些內(nèi)容應(yīng)用到 <master> 分支上,這個過程就是衍合。
rebase 原理 衍合前回到兩個分支(所在的分支和想要衍合的分支)的共同祖先,提取你所在分支每次提交時產(chǎn)生的差異(diff),把這些差異分別保存到臨時文件里,然后從當(dāng)前分支轉(zhuǎn)換到你需要衍合的分支,依照順序使用每一個差異補丁文件。

當(dāng) <hotfix> 更新后,它會指向新創(chuàng)建的提交,而老的提交會被丟棄

使用 git rebase 之后產(chǎn)生的歷史會是如下

還是有點亂嗎?再通俗點呢,原來的「基點」是 C2,現(xiàn)在把「基點」改變到要衍合的分支處,就是把「基點」搞到 C6 哪兒去,然后把改變的內(nèi)容,應(yīng)用上去。然后分支歷史就被改寫啦。
實戰(zhàn)體驗 rebase
概念和效果清楚了,實戰(zhàn)一下,推薦一個學(xué)習(xí) Git 實踐的網(wǎng)站,這個工具非常有意思,可以用來學(xué)習(xí)模擬基本的 Git 操作,戳這里學(xué)習(xí)
模擬以下操作過程
- 在 <master>:C1 處創(chuàng)建 <b1> 和 <b2> 分支,在三個分支分別產(chǎn)生提交 C2~C4
<b1>:C2
<b2>:C3
<master>:C4
那么當(dāng)前的歷史節(jié)點如下圖:

2:衍合 b1 分支到 master 分支
<master>: git rebase b1

衍合過程分析:先回到 C1,提取當(dāng)前分支<master> 和 C1 的差異,形成 C4' (C4 和 C1 的差異) 保存起來,當(dāng)前分支<master> 轉(zhuǎn)換到要衍合的分支<b1>(C2處),再把 C4' 應(yīng)用進去。
這里要注意一下:在命令里面可以看到 Git 提示衍合過程
$ git rebase b1
First, rewinding head to replay your work on top of it...
Applying: C4
這里其實應(yīng)用的并不是 C4 那次 commit,而是 C4 和 C1 比較后的 diff,C4'。所以 C4 和 C4' 是兩個不同的提交(會產(chǎn)生不同的歷史,但是內(nèi)容是一樣的)。
- 再做一次衍合,將 b2 也合并過來
<master>:git rebase b2
$ git rebase b2
First, rewinding head to replay your work on top of it...
Applying: C2
Applying: C4
還是會提取 <master> 分支 和 C1 的 差異,產(chǎn)生了 C2' 和 C4'',轉(zhuǎn)到 <b2>分支再把 diff 應(yīng)用進去。

- 此時如果 b2 merge master 的話 實際就是快速跟進了。
<b2>:git merge master

同樣的過程,把上面的 rebase 都換成 merge

小結(jié)
可以看到不論用 rebase 還是 merge 得到的結(jié)果是沒有區(qū)別的,但是衍合能產(chǎn)生一個更為整潔的提交歷史。如果視察一個衍合過的分支的歷史記錄,看起來更清楚:仿佛所有修改都是先后進行的,盡管實際上它們原來是同時發(fā)生的。
你可以經(jīng)常使用衍合,確保在遠程分支里的提交歷史更清晰。比方說,某些項目自己不是維護者,但想幫點忙,就應(yīng)該盡可能使用衍合:先在一個分支里進行開發(fā),當(dāng)準(zhǔn)備向主項目提交補丁的時候,再把它衍合到 origin/master 里面。這樣,維護者就不需要做任何整合工作,只需根據(jù)你提供的倉庫地址作一次快進,或者采納你提交的補丁。
請注意,合并結(jié)果中最后一次提交所指向的快照,無論是通過一次衍合還是一次三方合并,都是同樣的快照內(nèi)容,只是提交的歷史不同罷了。衍合按照每行改變發(fā)生的次序重演發(fā)生的改變,而合并是把最終結(jié)果合在一起。
rebase 的其他操作
前面用了大篇幅來說明 rebase 的概念及實踐,看下 rebase 的其他操作
onto 選項
--onto 剪切指定范圍內(nèi)提交節(jié)點,并在指向的分支上對這些節(jié)點執(zhí)行變基操作
git rebase --onto base from to
將 (from,to] 范圍內(nèi)所有提交的節(jié)點在 base 指向的節(jié)點之后重建
看官方的例子了解下這個 onto 選項
你創(chuàng)建了一個特性分支 <server> 來給服務(wù)器端代碼添加一些功能,然后提交 C3 和 C4。然后從 C3 的地方再增加一個 <client> 分支來對客戶端代碼進行一些修改,提交 C8 和 C9。最后,又回到 <server> 分支提交了 C10。

假設(shè)在接下來的一次軟件發(fā)布中,你決定把客戶端的修改先合并到主線中,而暫緩并入服務(wù)端軟件的修改(因為還需要進一步測試)。你可以僅提取對客戶端的改變(C8 和C9),然后通過使用 git rebase 的 --onto 選項來把它們在 <master> 分支上重演:
$ git rebase --onto master server client
這基本上等于在說“檢出 <client> 分支,找出 <client> 分支和 <server> 分支的共同祖先之后的變化,然后把它們在 <master> 上重演一遍”。是不是有點復(fù)雜?不過它的結(jié)果,非??幔?/p>

現(xiàn)在你決定把 <server> 分支的變化也包含進來??梢灾苯影?<server> 分支衍合到 <master> 而不用手工轉(zhuǎn)到 <server> 分支再衍合。git rebase [主分支] [特性分支] 命令會先檢出特性分支 <server>,然后在主分支 <master> 上重演
$ git rebase master server

rebase 沖突處理
在 rebase 的過程中,也許也會出現(xiàn)沖突,這時候 Git 會停止 rebase 讓你解決沖突(這個過程和 merge 是一樣的)
手動處理沖突之后,通過 add 命令暫存沖突文件
可以使用 --continue 選項,繼續(xù)本次操作
git rebase --continue
或者使用 --abort 選項 放棄本次衍合操作
git rebase --abort
在進行衍合或合并操作時,Git 類似新建了一個匿名分支,當(dāng)使用 --abort 選項時, Git 會切回原分支,丟棄匿名分支,放棄本次操作。
如果使用 Git 管理工具,當(dāng) merge 或者 rebase 操作有沖突需要處理時,都會有相關(guān)提示,比如有哪些文件有沖突,也有 continue 和 abort 操作供選擇。
rebase 還有其他的一些選項,比如 -i,后面學(xué)習(xí)重寫歷史的時候再做補充
rebase 的風(fēng)險和使用場景
永遠不要衍合哪些已經(jīng)推送到公共倉庫的更新
衍合的時候,實際上拋棄了一些已經(jīng)存在的 commit 而創(chuàng)建了一些類似的但是不同的新 commit。如果把這個 commit(假設(shè)是 C6)推送到遠程端,其他人在其基礎(chǔ)上工作,然后你使用 git rebase 重寫了C6 推送了 C6',那么別人不得不重新合并,而這次合并的內(nèi)容和之前已經(jīng)獲取到的 C6 是一樣,而再獲取的時候就可能是 C6-C7-C6'-C8 ( C6 和 C6' 有著相同的內(nèi)容(包括作者、提交說明等),C7 是其他人的提交,C8是其他人合并 C6' 產(chǎn)生的提交),這個歷史記錄會變的非常令人費解。
如果把衍合當(dāng)成一種在推送之前清理提交歷史的手段,而且僅僅衍合那些永遠不會公開的 commit,那就不會有任何問題。如果衍合那些已經(jīng)公開的commit,而與此同時其他人已經(jīng)用這些 commit 進行了后續(xù)的開發(fā)工作,那就很麻煩了。