git merge和rebase的區別與選擇
轉自:https://github.com/geeeeeeeeek/git-recipes/wiki/5.1-%E4%BB%A3%E7%A0%81%E5%90%88%E5%B9%B6%EF%BC%9AMerge%E3%80%81Rebase-%E7%9A%84%E9%80%89%E6%8B%A9#merge
BY 童仲毅(geeeeeeeeek@github)
這是一篇在原文(BY atlassian)基礎上演繹的譯文。除非另行注明,頁面上所有內容采用知識共享-署名(CC BY 2.5 AU)協議共享。
git rebase
這個命令經常被人認為是一種 Git 巫術,初學者應該避而遠之。但如果使用得當的話,它能給你的團隊開發省去太多煩惱。在這篇文章中,我們會比較 git rebase
和類似的 git merge
命令,找到 Git 工作流中 rebase 的所有用法。
概述
你要知道的第一件事是,git rebase
和git merge
做的事其實是一樣的。它們都被設計來將一個分支的更改并入另一個分支,只不過方式有些不同。
想象一下,你剛創建了一個專門的分支開發新功能,然后團隊中另一個成員在 master 分支上添加了新的提交。這就會造成提交歷史被 fork 一份,用 Git 來協作的開發者應該都很清楚
現在,如果 master 中新的提交和你的工作是相關的。為了將新的提交并入你的分支,你有兩個選擇:merge 或 rebase。
merge
將 master 分支合并到 feature 分支最簡單的辦法就是用下面這些命令:
git checkout feature
git merge master
或者,你也可以把它們壓縮在一行里。
git merge master feature
feature 分支中新的合并提交(merge commit)將兩個分支的歷史連在了一起。你會得到下面這樣的分支結構:
Merge 好在它是一個安全的操作。現有的分支不會被更改,避免了 rebase 潛在的缺點(后面會說)。
另一方面,這同樣意味著每次合并上游更改時 feature 分支都會引入一個外來的合并提交。如果 master 非常活躍的話,這或多或少會污染你的分支歷史。雖然高級的 git log
選項可以減輕這個問題,但對于開發者來說,還是會增加理解項目歷史的難度。
rebase
作為 merge 的替代選擇,你可以像下面這樣將 feature 分支并入 master 分支:
git checkout feature
git rebase master
它會把整個 feature 分支移動到 master 分支的后面,有效地把所有 master 分支上新的提交并入過來。但是,rebase 為原分支上每一個提交創建一個新的提交,重寫了項目歷史,并且不會帶來合并提交。
rebase最大的好處是你的項目歷史會非常整潔。首先,它不像 git merge
那樣引入不必要的合并提交。其次,如上圖所示,rebase 導致最后的項目歷史呈現出完美的線性——你可以從項目終點到起點瀏覽而不需要任何的 fork。這讓你更容易使用 git log
、git bisect
和 gitk
來查看項目歷史。
不過,這種簡單的提交歷史會帶來兩個后果:安全性和可跟蹤性。如果你違反了 rebase 黃金法則,重寫項目歷史可能會給你的協作工作流帶來災難性的影響。此外,rebase 不會有合并提交中附帶的信息——你看不到 feature 分支中并入了上游的哪些更改。
交互式rebase
交互式的 rebase 允許你更改并入新分支的提交。這比自動的 rebase 更加強大,因為它提供了對分支上提交歷史完整的控制。一般來說,這被用于將 feature 分支并入 master 分支之前,清理混亂的歷史。
把 -i
傳入 git rebase
選項來開始一個交互式的rebase過程:
git checkout feature
git rebase -i master
它會打開一個文本編輯器,顯示所有將被移動的提交:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
這個列表定義了 rebase 將被執行后分支會是什么樣的。更改 pick
命令或者重新排序,這個分支的歷史就能如你所愿了。比如說,如果第二個提交修復了第一個提交中的小問題,你可以用 fixup
命令把它們合到一個提交中:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
保存后關閉文件,Git 會根據你的指令來執行 rebase,項目歷史看上去會是這樣:
忽略不重要的提交會讓你的 feature 分支的歷史更清晰易讀。這是 git merge
做不到的。
Rebase 的黃金法則
當你理解 rebase 是什么的時候,最重要的就是什么時候 不能 用 rebase。git rebase
的黃金法則便是,絕不要在公共的分支上使用它。
比如說,如果你把 master 分支 rebase 到你的 feature 分支上會發生什么:
這次 rebase 將 master 分支上的所有提交都移到了 feature 分支后面。問題是它只發生在你的代碼倉庫中,其他所有的開發者還在原來的 master 上工作。因為 rebase 引起了新的提交,Git 會認為你的 master 分支和其他人的 master 已經分叉了。
同步兩個 master 分支的唯一辦法是把它們 merge 到一起,導致一個額外的合并提交和兩堆包含同樣更改的提交。不用說,這會讓人非常困惑。
所以,在你運行 git rebase
之前,一定要問問你自己「有沒有別人正在這個分支上工作?」。如果答案是肯定的,那么把你的爪子放回去,重新找到一個無害的方式(如 git revert
)來提交你的更改。不然的話,你可以隨心所欲地重寫歷史。
強制推送
如果你想把 rebase 之后的 master 分支推送到遠程倉庫,Git 會阻止你這么做,因為兩個分支包含沖突。但你可以傳入 --force
標記來強行推送。就像下面一樣:
# 小心使用這個命令!
git push --force
它會重寫遠程的 master 分支來匹配你倉庫中 rebase 之后的 master 分支,對于團隊中其他成員來說這看上去很詭異。所以,務必小心這個命令,只有當你知道你在做什么的時候再使用。
僅有的幾個強制推送的使用場景之一是,當你在想向遠程倉庫推送了一個私有分支之后,執行了一個本地的清理(比如說為了回滾)。這就像是在說「哦,其實我并不想推送之前那個 feature 分支的。用我現在的版本替換掉吧。」同樣,你要注意沒有別人正在這個 feature 分支上工作。
工作流
rebase 可以或多或少應用在你們團隊的 Git 工作流中。在這一節中,我們來看看在 feature 分支開發的各個階段中,rebase 有哪些好處。
第一步是在任何和 git rebase
有關的工作流中為每一個 feature 專門創建一個分支。它會給你帶來安全使用 rebase 的分支結構:
本地清理
在你工作流中使用 rebase 最好的用法之一就是清理本地正在開發的分支。隔一段時間執行一次交互式 rebase,你可以保證你 feature 分支中的每一個提交都是專注和有意義的。你在寫代碼時不用擔心造成孤立的提交——因為你后面一定能修復。
調用 git rebase
的時候,你有兩個基(base)可以選擇:上游分支(比如 master)或者你 feature 分支中早先的一個提交。我們在「交互式 rebase」一節看到了第一種的例子。后一種在當你只需要修改最新幾次提交時也很有用。比如說,下面的命令對最新的 3 次提交進行了交互式 rebase:
git checkout feature
git rebase -i HEAD~3
通過指定 HEAD~3
作為新的基提交,你實際上沒有移動分支——你只是將之后的 3 次提交重寫了。注意它不會把上游分支的更改并入到 feature 分支中。
如果你想用這個方法重寫整個 feature 分支,git merge-base
命令非常方便地找出 feature 分支開始分叉的基。下面這段命令返回基提交的 ID,你可以接下來將它傳給 git rebase
:
git merge-base feature master
交互式 rebase 是在你工作流中引入 git rebase
的的好辦法,因為它只影響本地分支。其他開發者只能看到你已經完成的結果,那就是一個非常整潔、易于追蹤的分支歷史。
但同樣的,這只能用在私有分支上。如果你在同一個 feature 分支和其他開發者合作的話,這個分支是公開的,你不能重寫這個歷史。
用帶有交互式的 rebase 清理本地提交,這是無法用 git merge
命令代替的。
將上游分支更改并入feature分支
在概覽一節,我們看到了 feature 分支如何通過 git merge
或 git rebase
來并入上游分支。merge 是保留你完整歷史的安全選擇,rebase 將你的 feature 分支移動到 master 分支后面,創建一個線性的歷史。
git rebase
的用法和本地清理非常類似(而且可以同時使用),但之間并入了 master 上的上游更改。
記住,rebase 到遠程分支而不是 master 也是完全合法的。當你和另一個開發者在同一個 feature 分之上協作的時候,你會用到這個用法,將他們的更改并入你的項目。
比如說,如果你和另一個開發者 John 往 feature 分支上添加了幾個提交,在從 John 的倉庫中 fetch 之后,你的倉庫可能會像下面這樣:
就和并入 master 上的上游更改一樣,你可以這樣解決這個 fork:要么 merge 你的本地分支和 John 的分支,要么把你的本地分支 rebase 到 John 的分支后面。
注意,這里的 rebase 沒有違反 rebase 黃金法則,因為只有你的本地分支上的 commit 被移動了,之前的所有東西都沒有變。這就像是在說「把我的改動加到 John 的后面去」。在大多數情況下,這比通過合并提交來同步遠程分支更符合直覺。
默認情況下,git pull
命令會執行一次merge,但你可以傳入--rebase
來強制它通過rebase來整合遠程分支。
用 Pull Request 進行審查
如果你將 Pull Request 作為你代碼審查過程中的一環,你需要避免在創建 Pull Request 之后使用 git rebase
。只要你發起了 Pull Request,其他開發者能看到你的代碼,也就是說這個分支變成了公共分支。重寫歷史會造成 Git 和你的同事難以找到這個分支接下來的任何提交。
來自其他開發者的任何更改都應該用 git merge
而不是 git rebase
來并入。
因此,在提交 Pull Request前用交互式的 rebase 進行代碼清理通常是一個好的做法。
并入通過的功能分支
如果某個功能被你們團隊通過了,你可以選擇將這個分支 rebase 到 master 分支之后,或是使用 git merge
來將這個功能并入主代碼庫中。
這和將上游改動并入 feature 分支很相似,但是你不可以在 master 分支重寫提交,你最后需要用 git merge
來并入這個 feature。但是,在 merge 之前執行一次 rebase,你可以確保 merge 是一直向前的,最后生成的是一個完全線性的提交歷史。這樣你還可以加入 Pull Request 之后的提交。
如果你還沒有完全熟悉 git rebase
,你還可以在一個臨時分支中執行 rebase。這樣的話,如果你意外地弄亂了你 feature 分支的歷史,你還可以查看原來的分支然后重試。
比如說:
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [清理目錄]
git checkout master
git merge temporary-branch
總結
你使用 rebase 之前需要知道的知識點都在這了。如果你想要一個干凈的、線性的提交歷史,沒有不必要的合并提交,你應該使用 git rebase
而不是 git merge
來并入其他分支上的更改。
另一方面,如果你想要保存項目完整的歷史,并且避免重寫公共分支上的 commit, 你可以使用 git merge
。兩種選項都很好用,但至少你現在多了 git rebase
這個選擇。