-
1. 起步
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協議
- 4.2 在伺服器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設伺服器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管服務
- 4.10 小結
-
5. 分散式 Git
-
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 分支 - 變基
變基
在 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
分支拉入。你可以透過執行 git rebase <basebranch> <topicbranch>
,將 server
分支變基到 master
分支上,而無需先檢出它——這會為你檢出主題分支(在本例中是 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”(patch-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
,以儘量減輕事後帶來的痛苦。
變基 vs. 合併
現在你已經看到了變基和合並的實際操作,你可能想知道哪一個更好。在我們回答這個問題之前,讓我們退一步,談談歷史的意義。
其中一種觀點是,你的倉庫的提交歷史是**實際發生過的記錄**。它是一份有其自身價值的歷史文獻,不應該被篡改。從這個角度來看,改變提交歷史幾乎是褻瀆神明的行為;你是在撒謊關於實際發生的事情。所以,即使有一系列混亂的合併提交又如何?事情就是這樣發生的,倉庫應該為後代保留它。
相反的觀點是,提交歷史是**你的專案如何建立的故事**。你不會出版一本書的初稿,那為什麼還要展示你的凌亂工作呢?當你在一個專案上工作時,你可能需要記錄你所有的失誤和死衚衕,但當你需要向世界展示你的工作時,你可能希望講述一個更連貫的從A到B的故事。持這種觀點的人會使用像 rebase
和 filter-branch
這樣的工具在提交合併到主線分支之前重寫它們。他們使用像 rebase
和 filter-branch
這樣的工具,以最適合未來讀者的方式講述故事。
現在,回到合併和變基哪個更好的問題:希望你現在能明白,這並非那麼簡單。Git 是一個強大的工具,允許你對歷史記錄進行許多操作,但每個團隊和每個專案都不同。既然你已經瞭解了這兩種方式的工作原理,就由你來決定哪種最適合你的具體情況。
你可以兩全其美:在推送到遠端之前對本地更改進行變基以清理你的工作,但永遠不要變基任何你已經推送到遠端的提交。