-
A1. 附錄 A:其他環境中的 Git
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 摘要
-
A2. 附錄 B:在您的應用程式中嵌入 Git
-
A3. 附錄 C:Git 命令
3.6 Git 分支 - 變基 (Rebasing)
變基 (Rebasing)
在 Git 中,有兩種主要的方式可以將一個分支的變更整合到另一個分支:merge
(合併)和 rebase
(變基)。在本節中,您將學習什麼是變基、如何進行變基、為什麼它是一個非常棒的工具,以及在哪些情況下您不應該使用它。
基本的變基
如果您回到基本合併的早期範例,您可以看到您分散了工作,並在兩個不同的分支上進行了提交。

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

然而,還有另一種方式:您可以取得 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
這個操作的運作方式是,先前往兩個分支(您所在的分支和您要變基到的分支)的共同祖先,取得您所在分支的每個提交所引入的差異,將這些差異儲存到暫存檔案中,將目前分支重設為與您要變基到的分支相同的提交,最後依序應用每個變更。

C4
中引入的變更變基到 C3
上此時,您可以回到 master
分支並執行快轉合併。
$ git checkout master
$ git merge experiment

master
分支現在,C4'
所指向的快照與合併範例中 C5
所指向的快照完全相同。整合的最終產品沒有差異,但變基會產生更乾淨的歷史。如果您檢查變基分支的日誌,它看起來會像一個線性歷史:它似乎所有工作都是依序發生的,即使它最初是平行發生的。
通常,你會這樣做以確保你的提交能夠乾淨地套用在遠端分支上 — 也許是在你試圖貢獻,但並非你維護的專案中。在這種情況下,你會在一個分支中進行你的工作,然後當你準備好將你的修補程式提交到主專案時,將你的工作變基到 origin/master
上。這樣一來,維護者就不必做任何整合工作 — 只需快速前進或乾淨地套用即可。
請注意,最終提交所指向的快照,無論是變基的最後一個提交還是合併後的最終合併提交,都是相同的快照 — 只有歷史記錄不同。變基會按照引入的順序,將一個工作線上的變更重播到另一個工作線上,而合併則是將端點合併在一起。
更有趣的變基
你也可以讓變基在目標分支以外的其他東西上重播。例如,以從另一個主題分支分岔出主題分支的歷史為例。你分岔了一個主題分支 (server
) 以為你的專案新增一些伺服器端功能,並進行了一次提交。然後,你從該分支分岔出另一個分支來進行客戶端變更 (client
) 並提交了幾次。最後,你回到你的 server
分支並進行了更多提交。

假設你決定要將你的客戶端變更合併到你的主線中以進行發佈,但你想暫緩伺服器端的變更,直到經過進一步測試。你可以使用 git rebase
的 --onto
選項,將 client
上不屬於 server
的變更 (C8
和 C9
) 重播到你的 master
分支上。
$ git rebase --onto master server client
這基本上是說:「取得 client
分支,找出它與 server
分支分歧後的修補程式,然後在 client
分支中重播這些修補程式,就像它是直接基於 master
分支一樣。」這有點複雜,但結果非常酷。

現在你可以快速前進你的 master
分支 (請參閱快速前進你的 master
分支以包含 client
分支的變更)
$ git checkout master
$ git merge client

master
分支以包含 client
分支的變更假設你決定也要納入你的 server
分支。你可以將 server
分支變基到 master
分支上,而無需先簽出它,方法是執行 git rebase <basebranch> <topicbranch>
— 這會為你簽出主題分支 (在此例中為 server
),並將其重播到基礎分支 (master
) 上。
$ git rebase master server
這會將你的 server
工作重播到你的 master
工作之上,如將你的 server
分支變基到你的 master
分支之上所示。

server
分支變基到你的 master
分支之上然後,你可以快速前進基礎分支 (master
)
$ git checkout master
$ git merge server
你可以移除 client
和 server
分支,因為所有工作都已整合,你不再需要它們,讓整個過程的歷史記錄看起來像最終提交歷史
$ git branch -d client
$ git branch -d server

變基的風險
啊,但變基的樂趣並非沒有缺點,可以用一句話總結:
不要變基存在於你的儲存庫之外,且人們可能已基於其進行工作的提交。
如果你遵循該準則,你就會沒事。如果你不這樣做,人們會恨你,你的朋友和家人也會鄙視你。
當你變基東西時,你是在放棄現有的提交並建立類似但不同的新提交。如果你將提交推送到某處,其他人將它們拉下來並基於它們進行工作,然後你使用 git rebase
重寫這些提交並再次推送它們,你的協作者將必須重新合併他們的工作,並且當你嘗試將他們的工作拉回到你的工作時,事情會變得一團糟。
讓我們來看一個範例,說明變基你已公開的工作如何導致問題。假設你從中央伺服器複製,然後在該基礎上進行一些工作。你的提交歷史看起來像這樣:

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

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

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

如果你的歷史記錄看起來像這樣時,你執行 git log
,你將看到兩個具有相同作者、日期和訊息的提交,這會讓人感到困惑。此外,如果你將此歷史記錄推回伺服器,你將重新將所有這些變基的提交引入中央伺服器,這可能會進一步使人困惑。可以相當肯定地假設其他開發人員不希望歷史記錄中有 C4
和 C6
;這就是他們首先變基的原因。
當你變基時變基
如果你確實發現自己處於這種情況,Git還有一些額外的魔法可以幫助你。如果你的團隊中的某人強制推送變更,覆蓋了你基於其工作所做的變更,那麼你的挑戰是找出哪些是你的,哪些是他們重寫的。
事實證明,除了提交 SHA-1 檢查碼之外,Git 還會計算一個僅基於提交引入的修補程式的檢查碼。這稱為「修補程式 ID」。
如果你提取了被重寫的工作,並將其變基在合作夥伴的新提交之上,Git 通常可以成功地找出哪些是你的獨特之處,並將它們重新套用在新分支之上。
例如,在先前的場景中,如果我們在有人推送了變基的提交,放棄了你基於其工作所做的提交時,沒有執行合併,而是執行 git rebase teamone/master
,Git 將會:
-
判斷哪些工作對我們的分支是獨特的 (
C2
、C3
、C4
、C6
、C7
) -
判斷哪些不是合併提交 (
C2
、C3
、C4
) -
判斷哪些尚未被重寫到目標分支中 (只有
C2
和C3
,因為C4
與C4'
是相同的修補程式) -
將這些提交套用到
teamone/master
的頂端
因此,我們最終不會得到在你將相同的工作再次合併到新的合併提交中看到的結果,而是會得到更像在強制推送的變基工作之上變基的結果。

這只有在你的合作夥伴製作的 C4
和 C4'
幾乎是相同的修補程式時才有效。否則,變基將無法判斷它是重複的,並且會新增另一個類似 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
,以嘗試讓發生後的痛苦變得簡單一點。
變基與合併
現在你已經看到了變基和合併的實際操作,你可能會想知道哪一個比較好。在回答這個問題之前,讓我們先退一步,談談歷史記錄的意義。
對此的一個觀點是,你的儲存庫的提交歷史是實際發生情況的記錄。它本身就是一份有價值的歷史文檔,不應被篡改。從這個角度來看,更改提交歷史幾乎是褻瀆神明的行為;你是在謊報實際發生的事情。那麼,如果有一系列混亂的合併提交怎麼辦?事情就是這樣發生的,儲存庫應該為後代保存下來。
相反的觀點是,提交歷史是你的專案是如何建立的故事。你不會發表一本書的初稿,所以為什麼要展示你混亂的工作呢?當你在一個專案上工作時,你可能需要記錄你的所有失誤和死胡同路徑,但是當你準備好向世界展示你的工作時,你可能想講述一個如何從 A 到 B 的更連貫的故事。這個陣營中的人使用 rebase
和 filter-branch
之類的工具來重寫他們的提交,然後再將它們合併到主線分支中。他們使用 rebase
和 filter-branch
之類的工具,以最適合未來讀者的方式來講述故事。
現在,回到合併還是變基更好的問題:希望你看到這並非那麼簡單。Git 是一個強大的工具,可以讓你對你的歷史記錄執行許多操作,但是每個團隊和每個專案都不相同。既然你已經了解了這兩種方法的工作原理,那麼就由你來決定哪一種最適合你的特定情況。
你可以兩全其美:在推送之前變基本地變更以清理你的工作,但永遠不要變基你已推送到某處的任何內容。