章節 ▾ 第二版

3.6 Git 分支 - 變基

變基

在 Git 中,將一個分支的更改整合到另一個分支有兩種主要方式:merge(合併)和 rebase(變基)。本節將介紹什麼是變基、如何執行變基、為什麼它是一個相當棒的工具,以及在哪些情況下你不希望使用它。

基本變基

回顧基本合併中的一個早期例子,你可以看到你的工作產生了分歧,並在兩個不同的分支上進行了提交。

Simple divergent history
圖 35. 簡單的分叉歷史

正如我們已經介紹過的,整合分支最簡單的方法是使用 merge 命令。它會在兩個最新的分支快照(C3C4)以及兩者最近的共同祖先(C2)之間執行三方合併,從而建立一個新的快照(和提交)。

Merging to integrate diverged work history
圖 36. 合併以整合分叉的工作歷史

然而,還有另一種方式:你可以獲取在 C4 中引入的更改補丁,並將其重新應用到 C3 之上。在 Git 中,這被稱為 變基。使用 rebase 命令,你可以將一個分支上提交的所有更改在另一個分支上重放。

對於這個例子,你需要檢出 experiment 分支,然後將其變基到 master 分支,如下所示:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

此操作的原理是:首先找到兩個分支(當前分支和要變基到的分支)的共同祖先,然後獲取當前分支的每個提交所引入的差異,將這些差異儲存到臨時檔案中,接著將當前分支重置到與目標變基分支相同的提交,最後依次應用每個更改。

Rebasing the change introduced in `C4` onto `C3`
圖 37. 將 C4 中引入的更改變基到 C3 之上

此時,你可以回到 master 分支並進行一次快進合併。

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
圖 38. 快進 master 分支

現在,C4' 指向的快照與合併示例C5 指向的快照完全相同。整合的最終結果沒有區別,但變基使得歷史記錄更清晰。如果你檢查一個變基後的分支日誌,它看起來像一條線性歷史:所有工作似乎都是按順序發生的,即使它們最初是並行進行的。

通常,你這樣做是為了確保你的提交能夠乾淨地應用到遠端分支上——也許是你在嘗試貢獻但並非維護的專案。在這種情況下,你會在一個分支中完成工作,然後在準備好將補丁提交給主專案時,將你的工作變基到 origin/master。這樣,維護者就不必做任何整合工作——只需進行一次快進或乾淨的應用。

請注意,無論是變基操作後最後一次變基提交,還是合併操作後最終的合併提交,它們指向的最終快照是相同的——不同的只是歷史記錄。變基按照更改引入的順序,將一條工作線的更改在另一條工作線上重放,而合併則將終點合併在一起。

更有趣的變基

你也可以將變基重放到除了目標變基分支以外的其他地方。例如,一個像從另一個主題分支分出的主題分支的歷史。你分出一個主題分支 (server) 來為你的專案新增一些服務端功能,並進行了提交。然後,你從這個分支再分出一個分支來做客戶端更改 (client),並提交了幾次。最後,你回到了你的 server 分支並又提交了幾次。

A history with a topic branch off another topic branch
圖 39. 從另一個主題分支分出的主題分支的歷史

假設你決定將客戶端更改合併到主線中進行釋出,但希望暫緩伺服器端更改,直到它們得到進一步測試。你可以透過使用 git rebase 命令的 --onto 選項,將 client 分支上不在 server 分支上的更改(C8C9)重放到你的 master 分支上:

$ git rebase --onto master server client

這基本上是說:“取出 client 分支,找出它從 server 分支分化以來的補丁,並將這些補丁在 client 分支中重放,就好像它直接基於 master 分支一樣。” 這有點複雜,但結果相當棒。

Rebasing a topic branch off another topic branch
圖 40. 變基一個從另一個主題分支分出的主題分支

現在你可以快進你的 master 分支了(參見快進你的 master 分支以包含 client 分支的更改):

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
圖 41. 快進你的 master 分支以包含 client 分支的更改

假設你決定也將 server 分支拉入。你可以透過執行 git rebase <basebranch> <topicbranch>,將 server 分支變基到 master 分支上,而無需先檢出它——這會為你檢出主題分支(在本例中是 server),並將其重放到基礎分支(master)上:

$ git rebase master server

這會將你的 server 工作重放到你的 master 工作之上,如將你的 server 分支變基到你的 master 分支之上所示。

Rebasing your `server` branch on top of your `master` branch
圖 42. 將你的 server 分支變基到你的 master 分支之上

然後,你可以快進基礎分支 (master)。

$ git checkout master
$ git merge server

你可以刪除 clientserver 分支,因為所有工作都已整合,你不再需要它們,整個過程的歷史記錄看起來就像最終提交歷史

$ git branch -d client
$ git branch -d server
Final commit history
圖 43. 最終提交歷史

變基的危險

啊,但是變基的幸福並非沒有缺點,這可以用一句話概括:

不要對你倉庫外部的,或其他人可能基於其進行過工作的提交進行變基。

如果你遵循這個準則,你會沒事。如果你不遵循,人們會討厭你,你也會受到朋友和家人的鄙視。

當你變基時,你是在放棄現有的提交併建立相似但不同的新提交。如果你將提交推送到某個地方,並且其他人拉取這些提交併基於它們進行工作,然後你又用 git rebase 重寫了這些提交併再次推送,你的協作者將不得不重新合併他們的工作,當你嘗試將他們的工作拉回你的倉庫時,事情會變得一團糟。

讓我們看一個例子,說明對已公開的工作進行變基會如何導致問題。假設你從中心伺服器克隆,然後在此基礎上進行了一些工作。你的提交歷史看起來像這樣:

Clone a repository, and base some work on it
圖 44. 克隆一個倉庫,並在此基礎上做一些工作

現在,其他人做了更多工作,其中包括一次合併,並將這些工作推送到中心伺服器。你獲取它並將新的遠端分支合併到你的工作中,使你的歷史看起來像這樣:

Fetch more commits, and merge them into your work
圖 45. 獲取更多提交,並將它們合併到你的工作中

接下來,推送了合併工作的人決定回去變基他們的工作;他們執行 git push --force 來覆蓋伺服器上的歷史。然後你從該伺服器獲取,拉下新的提交。

Someone pushes rebased commits, abandoning commits you’ve based your work on
圖 46. 有人推送了變基後的提交,放棄了你基於其進行工作的提交

現在你們都陷入了困境。如果你執行 git pull,你將建立一個包含兩條歷史線的合併提交,你的倉庫將看起來像這樣:

You merge in the same work again into a new merge commit
圖 47. 你再次將相同的工作合併到一個新的合併提交中

如果你的歷史記錄看起來像這樣時,你執行 git log,你會看到兩個具有相同作者、日期和訊息的提交,這會讓人感到困惑。此外,如果你將這段歷史推送回伺服器,你將把所有那些變基過的提交重新引入到中心伺服器,這會進一步混淆人們。可以很安全地假設,另一個開發者不希望 C4C6 出現在歷史中;這就是他們最初進行變基的原因。

遇到被變基的提交,如何變基

如果你**確實**發現自己處於這種情況,Git 還有一些更高階的技巧可以幫助你。如果你的團隊成員強制推送了覆蓋你已基於其進行工作的更改,你的挑戰是弄清楚哪些是你的工作,哪些是他們重寫的。

事實證明,除了提交的 SHA-1 校驗和之外,Git 還會計算一個僅基於提交引入的補丁的校驗和。這被稱為“補丁 ID”(patch-id)。

如果你拉取了被重寫的工作,並將其變基到你的夥伴的新提交之上,Git 通常可以成功地找出哪些是屬於你獨有的,並將其重新應用到新分支的頂部。

例如,在前面的場景中,如果我們在有人推送了變基後的提交,放棄了你基於其進行工作的提交時,不進行合併,而是執行 git rebase teamone/master,Git 將會:

  • 確定我們分支獨有的工作(C2C3C4C6C7

  • 確定哪些不是合併提交(C2C3C4

  • 確定哪些尚未被重寫到目標分支(只有 C2C3,因為 C4C4' 是相同的補丁)

  • 將這些提交應用到 teamone/master 的頂部

Rebase on top of force-pushed rebase work
圖 48. 在強制推送的變基工作之上進行變基

這僅在你的夥伴建立的 C4C4' 幾乎完全是相同的補丁時才有效。否則,變基將無法判斷它是重複的,並將新增另一個類似於 C4 的補丁(這很可能無法乾淨地應用,因為更改至少已經部分存在)。

你也可以透過執行 git pull --rebase 而不是普通的 git pull 來簡化此操作。或者,在這種情況下,你可以手動執行 git fetch,然後執行 git rebase teamone/master

如果你正在使用 git pull 並希望將 --rebase 設定為預設行為,你可以透過 git config --global pull.rebase true 等命令設定 pull.rebase 配置值。

如果你只對從未離開過你本地電腦的提交進行變基,那就沒問題。如果你變基了已經推送但其他人尚未基於其進行提交的提交,你也會沒事。如果你變基了已經公開推送的提交,並且其他人可能已經基於這些提交進行了工作,那麼你可能會遇到一些令人沮喪的麻煩,並受到隊友的鄙視。

如果某個時候你或你的夥伴確實覺得有必要這樣做,請確保每個人都知道執行 git pull --rebase,以儘量減輕事後帶來的痛苦。

變基 vs. 合併

現在你已經看到了變基和合並的實際操作,你可能想知道哪一個更好。在我們回答這個問題之前,讓我們退一步,談談歷史的意義。

其中一種觀點是,你的倉庫的提交歷史是**實際發生過的記錄**。它是一份有其自身價值的歷史文獻,不應該被篡改。從這個角度來看,改變提交歷史幾乎是褻瀆神明的行為;你是在撒謊關於實際發生的事情。所以,即使有一系列混亂的合併提交又如何?事情就是這樣發生的,倉庫應該為後代保留它。

相反的觀點是,提交歷史是**你的專案如何建立的故事**。你不會出版一本書的初稿,那為什麼還要展示你的凌亂工作呢?當你在一個專案上工作時,你可能需要記錄你所有的失誤和死衚衕,但當你需要向世界展示你的工作時,你可能希望講述一個更連貫的從A到B的故事。持這種觀點的人會使用像 rebasefilter-branch 這樣的工具在提交合併到主線分支之前重寫它們。他們使用像 rebasefilter-branch 這樣的工具,以最適合未來讀者的方式講述故事。

現在,回到合併和變基哪個更好的問題:希望你現在能明白,這並非那麼簡單。Git 是一個強大的工具,允許你對歷史記錄進行許多操作,但每個團隊和每個專案都不同。既然你已經瞭解了這兩種方式的工作原理,就由你來決定哪種最適合你的具體情況。

你可以兩全其美:在推送到遠端之前對本地更改進行變基以清理你的工作,但永遠不要變基任何你已經推送到遠端的提交。

scroll-to-top