看到國外論壇上這篇文章講的很好,翻譯過來學習學習
關於git rebase的指令一直有個說法,這是一個神奇的git指令,但是新手要遠離它。實際上當小心使用它的時候,可以讓一個開發團隊的工作簡單得多。在這篇文章中,我們將比較git rebase與git merge指令並尋找一切可以將rebasing整合到典型的git工作流的機會。
概念性回顧
關於git rebase需要理解的第一點是它解決的是和git merge一樣的問題。這兩個指令都是設計來將代碼的變化從一個分支整合到另一個分支,但是兩者用了非常不同的方法來做這件事。
回想一下當你在一個獨立的feature分支上時,另一個團隊成員更新了master分支,這時會發生什么。對於任何將git作為一個協作工作的人來說,很顯然這件事會導致分岔的history出現。
現在,假設master分支中的新commits與你正在做的需求息息相關。想把新的commits整合到你的feature分支上,你有兩種選擇:merging或者rebasing。
Merge 選項
最簡單的選擇就是把master分支merge到feature分支,指令如下:
git checkout feature
git merge master
或者可以將上面兩條整合到一條指令:
這會在feature分支創建一個新的merge commit,將所有分支的歷史都連結到一起,會給你一個如下所示的分支結構:

Merging很優秀,因為它是一種非破壞性的操作。現存的分支不會以任何形式被改變,這一點避免了rebasing所有的潛在陷阱。
在另一方面,這同樣意味着,每次當你需要整合上游的改變時,feature分支會有一個額外的merge commit。如果master分支很活躍,這會將你feature分支的歷史污染的很嚴重。當需要用到git log的時候,這些歷史會使其他開發者很難理解項目的歷史。
Rebase 選項
作為merging的替代品,你也可以以下指令在master分支上rebase feature分支:
git checkout feature
git rebase master
這段指令將整個feature分支挪到了master分支的后面,有效地將所有新commits整合到master分支中。但是,和使用merge commit不同的是,rebasing通過為每個原始分支中的commit創建全新的commit,重寫了項目的歷史。

rebasing的主要好處是你可以得到一個更干凈的項目歷史。首先,它消除了git merge引發的不必要的merge commit。其次,你可以從上面的圖像中看出,rebasing還造就了完美的線性項目歷史。你可以避開任何的分岔順着feature的流程走到項目的開端。這使你用git log,git bisect,gitk來導航項目變的極其簡單。
但是對於這個嶄新的commit歷史來說有兩件需要權衡的事:安全性和可追溯性。如果你不遵循rebasing的黃金定律,重寫項目歷史可能會是你協作工作流程的潛在災難。另外不那么重要的是,rebasing失去了merge commit提供的context,你不能看到上游改變時什么時候被合並到feature分支中。
交互性 Rebasing
當commits被移到新的分支時,交互性Rebasing給了你機會去改變它們。這一點甚至比自動化的rebase更加給力,因為它提供了對於分支commit歷史的完全控制。很典型的情景是,這一點被用於在將一個feature分支合並到master分支前,將混亂的歷史清理干凈。
開始一個交互性的rebasing,加入一個i的字段
git checkout feature
git rebase -i master
這會打開一個文本編輯器,將所有需要移動的commit都列出來:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
這個列表就決定了之后合並后的branch的log會長什么樣。改變pick指令或者重新排列這些條目,你就可以將分支的歷史做成你想要的樣子。舉個例子,如果第二個commit修改了第一個commit中的一個小問題,你可以通過fixup指令將他們合並到一個單獨的commit:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
當你保存並關掉這個文件時,git會根據你的指令來執行rebase。然后項目歷史就會看起來如下:

像上面這樣忽略掉一些非必要的commit可以使你的分支歷史看起來好理解的多。這是git merge不太能做到的。
Rebasing的黃金法則
當你了解rebasing是什么之后,最重要的事情就是去了解什么時候不能用它。git rebase的黃金法則就是永遠不要在公共分支上用這個指令。
舉個例子,想一下當你將master分支rebase到你的feature分支上,會發生什么:
rebase指令會將所有master分支的commit接到feature分支的末端。問題是這件事僅僅出現在你的倉庫中。其他的所有開發者仍然在原來的master分支上工作。因為rebasing創造了新的commit,git會認為你的master分支的歷史與其他人的會發生分岔。
唯一使兩個master分支同步的辦法是merge,這會導致一個額外的merge commit和兩堆含有相同改變的commit(一堆是原始的,一堆是從rebase分支合並過來的)。這真的是個很令人費解的情景。
所以,在你執行git rebase前,多考慮考慮團隊其他人。盡量使用非破壞性的方式去改變,比如git revert。
強制push
如果你試圖將rebase過的master分支push到遠程倉庫,git將會阻止你這么做。因為這會和遠程的master分支發生沖突。但是,你可以通過傳遞一個--force指令強制push:
# Be very careful with this command!
git push --force
這會重寫遠程的master分支來匹配你倉庫中rebase過的分支,這會讓團隊的其他人非常的迷惑。所以,只有當你真的知道自己在做什么的時候,再小心使用這條指令。
為數不多你必須使用這條指令的情景是當你在push了一個私有的feature分支到遠程倉庫上,你需要做一個本地的清理並將還原提交到遠程倉庫。同樣要注意的是,沒有人在feature分支的原始版本上改動是很重要的。
工作流預排
rebasing可以以盡可能小影響的方式合成到你已有的git工作流中,這樣會讓你的團隊很舒服。這這一部分,我們會看看rebasing在多種情景下提供的優勢。
任何工作流中利用git rebase的第一步是去為每個feature創建一個獨立的分支。這會給你必要的分支結構來安全地利用rebasing:
本地清理
將rebasing合成到工作流中最好的方式之一是清除本地正在運行中的feature。定期執行交互性的rebase,你可以確信你的feature中每個commit都是專一且有意義的。這會讓你不用擔心在寫代碼的時候會破壞掉commit的結構。
當使用git rebase的時候,你有兩個新的base可以選擇:一個是feature的父分支,或者你的feature中一個早前的commit。我們在交互性rebasing章節有展示過一個第一種選擇的例子。后面的一種選擇當你只需要修改最后的幾個commit時是很不錯的。舉個例子,下面的指令開啟了一個只有三個commit的可交互性rebase:
git checkout feature
git rebase -i HEAD~3
通過指定HEAD~3作為新的base,你實際上並沒有移動分支,而是交互性的重寫了3個隨后的commit。注意這不會將上游的改變融入到feature分支中。
如果你想用這個方法重寫整個feature,git merge指令在找到feature分支的原始base上是有效的。接下來會返回原始base的commit ID,你可以利用這些ID來git rebase:
git merge-base feature master
這種交互性的使用方式是一種將git rebase融入到你的工作流很好的方式,因為它只會改變本地的分支。其他的開發者只會看到你完成了一個有着清爽,簡單易懂的feature分支歷史的項目。
再一次說明的是,這只會在私有的feature分支上起作用,如果你通過相同的feature分支與其他人協作開發。公共的feature分支歷史改寫是不被允許的。對於使用交互性rebase清理本地commit,git merge的選項是行不通的。
將上游的改變融入一個feature
在概念回顧部分,我們講述了如果利用git merge或git rebase將上游master分支中的變化融入到feature分支中。merging是一個安全的選擇,它保留了你倉庫完整的歷史,而rebasing通過將feature分支移動到master的末端來創造一個線性的歷史。
git rebase的這種用法類似於本地的清理,但是在這個過程中,它將這些上游的commit融合了。要記得在遠程分支上rebase而非是在master分支上rebase,當你與其他的開發者在相同的feature分支上開發時,你需要將他們的變化融入到你的倉庫中。
你可以解決這個分岔正如同你可以將master上游的變化融入進來,或者將john/feature merge到你的本地feature分支,或者將你的本地feature分支rebase到john/feature的末端。
注意這個rebase沒有違反rebasing的黃金法則,因為只有你本地的feature的commit被移動了,前面任何事都沒有被改變。說起來就是把你的改變加在John已經做好的部分上。在大多數情況下,這比用merge commit來同步遠程分支要直觀的多。
git pull默認使用了一個merge指令,但是你可以強行將遠程分支利用--rebase選項進行強行融合。
利用Pull Request復審一個feature
如果你將pull request作為你代碼復審流程的一部分,你需要避免在創建PR之后使用git rebase指令。當你一開始用PR,其他的開發者就會看到你的commit,意味着這是一個公共的分支。重寫它的歷史對於git來說是不可能的,而且你的團隊成員可以追蹤到任何加在feature后的commit。
將其他開發者的代碼改變必須利用git merge而非git rebase融入。因此,在提交PR之前經常使用交互性rebase清理你的代碼是一個好主意。
與審批過的feature交互
當一個feature被你的團隊審批過后,你可以在使用git merge將feature融入到主代碼base前,選擇將feature rebase到master分支的末端。
這和將上游改變融入到feature分支有些相似,但是你不被允許重寫master分支中的commit,你必須使用git merge去融入feature。然后通過在merge前使用rebase,你可以確信merge是快速推進的,創建一個完美的線性歷史。這也給了你在PR的時候將后續的commit去除的機會。
如果你對git rebase感覺不是很舒服,你也可以在暫時的分支中使用git rebase。在這種方式下,如果你意外弄亂了你的feature的歷史,你可以切換到原始分支再試一次,如下:
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch
總結
以上就是在開始使用rebase分支之前你需要了解的東西。如果你更傾向於一個干凈,線性的歷史,不被非必要的merge commit拖累,你必須在從另一個分支融入變化時采用git rebase而非git merge。
在另一方面,如果你想保存你項目的完整歷史並且避免重寫公共commit的風險,你可以依舊堅持使用git merge。兩種選擇都是完美有效的,但是至少你現在可以選擇去衡量git rebase的好處。