變基
在 Git 中,整合來自不同分支的修改,除了 merge,還有一種方法,變基 rebase。git rebase 命令基本是是一個自動化的 cherry-pick 命令,它計算出一系列的提交,然后在其他地方以同樣的順序一個一個的 cherry-picks 出它們。
Git 中有一些修改會覆蓋提交歷史,包括:
- 使用 git commit --amend?命令修改最近一次提交的信息,會覆蓋最近一次提交的記錄。
- 使用 git rebase?命令修改提交記錄,這會修改提交的 SHA-1 校驗和,覆蓋提交歷史。
- 使用 git reset?命令回滾到之前的提交,這會刪除之后的提交歷史。
- 使用 git push --force?命令強制推送修改,這會覆蓋遠程分支的提交歷史。
在使用這些命令時,需要謹慎操作,以免不小心覆蓋提交歷史,導致代碼丟失或者出現其他問題。
Case 1:git rebase <upstream>?
假設在一個項目開發過程中,分叉到兩個不同分支,每個分支都提交了更新。
可以使用 merge 命令整合分支,它會把兩個分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)進行三方合并,合并的結果是生成一個新的快照(并提交)。
還有一種方法:使用變基 git rebase 整合分支,git rebase 的語法可以簡寫為:git rebase [--onto <newbase>] [<upstream> [<branch>]],表示將 <branch> 分支從 <upstream>?開始的提交應用到 <newbase> 分支上。具體來說,它會將 <branch> 分支自 <upstream> 之后的提交移動到 <newbase> 分支的最新提交之后,使得 <branch> 分支的提交歷史看起來像是在 <newbase>分支的基礎上進行的。
可以省略的參數是 <newbase>?和 <branch>;如果省略 --onto 參數,將以 <upstream> 參數指定的分支作為基底進行變基操作。也就是說,將當前所在分支(HEAD 指向的分支)與 <upstream> 參數指定的分支之間的差異應用到 <upstream>分支上;如果省略 <branch>,將把當前所在分支,即?HEAD 指向的分支,作為 <branch> 參數傳遞給 git rebase 命令。
?
$ git checkout experiment
$ git rebase master
接下來運行 git checkout master 回到 master 分支,然后運行 git merge experiment 進行一次快進合并。C4' 指向的快照就和使用 merge 得到的 C5 指向的快照一模一樣,這兩種整合方法的最終 結果沒有任何區別,但是變基使得提交歷史更加整潔。 你在查看一個經過變基的分支的歷史記錄時會發現,盡管實際的開發工作是并行的,但它們看上去就像是串行的一樣,提交歷史是一條直線沒有分叉。
一般這樣做的目的是為了確保在向遠程分支推送時能保持提交歷史的整潔,例如向某個其他人維護的項目貢獻代碼時。在這種情況下,你首先在自己的分支里進行開發,當開發完成時你需要先將你的代碼變基到 origin/master 上,然后再向主項目提交修改。這樣的話,該項目的維護者就不再需要進行整合工作,只需要快進合并便可。
Case2:git rebase [--onto <newbase>] [<upstream> [<branch>]]?
假設你的項目提交歷史如下:你創建了一個特性分支 server,為服務端添加了一些功能,提交了 C3 和 C4。然后從 C3 上創建了特性分支 client,為客戶端添加 了一些功能,提交了 C8 和 C9。 最后,你回到 server 分支,又提交了 C10。
你希望將 client 中的修改合并到 master 主分支并發布,但暫時并不想合并 server 中的修改,因為它們還需要經過更全面的測試。這時,你可以使用 git rebase 命令的 --onto 選項,選擇在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),將它們應用在 master 分支上。
運行命令:git rebase --onto master server client,其含義是:“取出 client 分支,找出處于 client 分支和 server 分支的共同祖先之后的修改,然后把它們在 master 分支上重放一遍”。然后將 client 合并到 master。
$ git rebase --onto master server client
$ git checkout master
$ git merge client
$ git rebase master server
$ git checkout master
$ git merge server
??
$ git branch -d client
$ git branch -d server
Case3:變基使用不當的風險
警告:不要對倉庫外有副本的分支執行變基。如果你遵循這條金科玉律,就不會出差錯。 否則,人民群眾會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。--?Scott Chacon
只要你把變基命令當作是在推送前清理提交使之整潔的工具,并且只在從未推送至共用倉庫的提交上執行變基命令,就不會有事。 假如在那些已經被推送至共用倉庫的提交上執行變基命令,并因此丟棄了一些別人的開發所基于的提交,那你就有大麻煩了,你的同事也會因此鄙視你。
變基使用不當的例子
假設你從一個中央服務器克隆然后在它的基礎上進行了一些開發,提交歷史如圖所示:
一段時間后,其他項目成員向中央服務器提交了一些修改,其中包括一次合并。?
你抓取了這些在遠程分支上的修改,并將其合并到你本地的開發分支,你的提交歷史如下:
接下來,這個成員又決定把合并操作回滾,改用變基,并用 git push --force 命令強制推送修改,這回覆蓋遠程分支的提交歷史。
此時,你從服務器抓取更新,會發現多出來一些新的提交。如果你執行 git pull 命令,你將合并來自兩條提交歷史的內容,生成一個新的合并提交 C8。
此時,如果你執行 git log 命令,你會發現有兩個提交的作者、日期、日志居然是一樣的,這會令人感到混亂。 此外,如果你將這一堆又推送到服務器上,實際上是將那些已經被變基拋棄的提交又找了回來,這會令人感到更加混亂。很明顯對方并不想在提交歷史中看到 C4 和 C6,因為之前就是他把這兩個提交通過變基丟棄的。
解決方法
如果團隊中的某人強制推送并覆蓋了一些你所基于的提交,你需要做的就是檢查你做了哪些修改,以及他們覆蓋了哪些修改。
方案1:git fetch?+?git rebase:在一個被變基然后強制推送的分支上再次執行變基
對于這種,有人推送了經過變基的提交,并丟棄了你的本地開發所基于的一些提交,不要使用 git pull,而是先 git fetch,再執行 git rebase teamone/master, Git 將會:
? 檢查哪些提交是我們的分支上獨有的(C2,C3,C4,C6,C7)
? 檢查其中哪些提交不是合并操作的結果(C2,C3,C4)
? 檢查哪些提交在對方覆蓋更新時并沒有被納入目標分支(C2 和 C3,C4 其實就是 C4')
? 把查到的這些提交應用在 teamone/master 上面
想要上述方案有效,還需要對方在變基時確保 C4' 和 C4 是幾乎一樣的。 否則變基操作將無法識別,并新建另一個類似 C4 的補丁(而這個補丁很可能無法整潔的整合入歷史,因為補丁中的修改已經存在于某個地方了)。
方案2:使用 git pull --rebase 而不是直接用 git pull
如果你或你的同事在某些情形下,不得不強制推送經過變基的提交,請一定要通知每個人執行 git pull --rebase 命令,這樣盡管不能避免麻煩,但能有所緩解。