# 變基
在 Git 中整合來自不同分支的修改主要有兩種方法:`merge` 以及 `rebase`。在本節中我們將學習什么是“變基”,怎樣使用“變基”,并將展示該操作的驚艷之處,以及指出在何種情況下你應避免使用它。
## 變基的基本操作
請回顧之前在 [“分支的合并”](#) 中的一個例子,你會看到開發任務分叉到兩個不同分支,又各自提交了更新。

Figure 3-27. 分叉的提交歷史
之前介紹過,整合分支最容易的方法是 `merge` 命令。它會把兩個分支的最新快照(`C3` 和 `C4`)以及二者最近的共同祖先(`C2`)進行三方合并,合并的結果是生成一個新的快照(并提交)。

Figure 3-28. 通過合并操作來整合分叉了的歷史
其實,還有一種方法:你可以提取在 `C4` 中引入的補丁和修改,然后在 `C3` 的基礎上再應用一次。在 Git 中,這種操作就叫做 *變基*。你可以使用 `rebase` 命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。
在上面這個例子中,運行:
~~~
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
~~~
它的原理是首先找到這兩個分支(即當前分支 `experiment`、變基操作的目標基底分支 `master`)的最近共同祖先 `C2`,然后對比當前分支相對于該祖先的歷次提交,提取相應的修改并存為臨時文件,然后將當前分支指向目標基底 `C3`, 最后以此將之前另存為臨時文件的修改依序應用。(譯注:寫明了 commit id,以便理解,下同)

Figure 3-29. 將 `C4` 中的修改變基到 `C3` 上
現在回到 `master` 分支,進行一次快進合并。
~~~
$ git checkout master
$ git merge experiment
~~~

Figure 3-30. master 分支的快進合并
此時,`C4'` 指向的快照就和上面使用 `merge` 命令的例子中 `C5` 指向的快照一模一樣了。這兩種整合方法的最終結果沒有任何區別,但是變基使得提交歷史更加整潔。你在查看一個經過變基的分支的歷史記錄時會發現,盡管實際的開發工作是并行的,但它們看上去就像是先后串行的一樣,提交歷史是一條直線沒有分叉。
一般我們這樣做的目的是為了確保在向遠程分支推送時能保持提交歷史的整潔——例如向某個別人維護的項目貢獻代碼時。在這種情況下,你首先在自己的分支里進行開發,當開發完成時你需要先將你的代碼變基到 `origin/master` 上,然后再向主項目提交修改。這樣的話,該項目的維護者就不再需要進行整合工作,只需要快進合并便可。
請注意,無論是通過變基,還是通過三方合并,整合的最終結果所指向的快照始終是一樣的,只不過提交歷史不同罷了。變基是將一系列提交按照原有次序依次應用到另一分支上,而合并是把最終結果合在一起。
## 更有趣的變基例子
在對兩個分支進行變基時,所生成的“重演”并不一定要在目標分支上應用,你也可以指定另外的一個分支進行應用。就像 [Figure?3-31](#) 中的例子這樣。你創建了一個特性分支 `server`,為服務端添加了一些功能,提交了 `C3` 和 `C4`。然后從 `C3` 上創建了特性分支 `client`,為客戶端添加了一些功能,提交了 `C8` 和 `C9`。最后,你回到 `server` 分支,又提交了 `C10`。

Figure 3-31. 從一個特性分支里再分出一個特性分支的提交歷史
假設你希望將 `client` 中的修改合并到主分支并發布,但暫時并不想合并 `server` 中的修改,因為它們還需要經過更全面的測試。這時,你就可以使用 `git rebase` 命令的 `--onto` 選項,選中在 `client` 分支里但不在 `server` 分支里的修改(即 `C8` 和 `C9`),將它們在 `master` 分支上重演:
~~~
$ git rebase --onto master server client
~~~
以上命令的意思是:“取出 `client` 分支,找出處于 `client` 分支和 `server` 分支的共同祖先之后的修改,然后把它們在 `master` 分支上重演一遍”。這理解起來有一點復雜,不過效果非常酷。

Figure 3-32. 截取特性分支上的另一個特性分支,然后變基到其他分支
現在可以快進合并 `master` 分支了。(如圖 [Figure?3-33](#)):
~~~
$ git checkout master
$ git merge client
~~~

Figure 3-33. 快進合并 master 分支,使之包含來自 client 分支的修改
接下來你決定將 `server` 分支中的修改也整合進來。使用 `git rebase [basebranch] [topicbranch]` 命令可以直接將特性分支(即本例中的 `server`)變基到目標分支(即 `master`)上。這樣做能省去你先切換到 `server` 分支,再對其執行變基命令的多個步驟。
~~~
$ git rebase master server
~~~
如圖 [Figure?3-34](#) 所示,`server` 中的代碼被“續”到了 `master` 后面。

Figure 3-34. 將 server 中的修改變基到 master 上
然后就可以快進合并主分支 master 了:
~~~
$ git checkout master
$ git merge server
~~~
至此,`client` 和 `server` 分支中的修改都已經整合到主分支里去了,你可以刪除這兩個分支,最終提交歷史會變成圖 [Figure?3-35](#) 中的樣子:
~~~
$ git branch -d client
$ git branch -d server
~~~

Figure 3-35. 最終的提交歷史
## 變基的風險
呃,奇妙的變基也并非完美無缺,要用它得遵守一條準則:
**不要對在你的倉庫外有副本的分支執行變基。**
如果你遵循這條金科玉律,就不會出差錯。否則,人民群眾會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。
變基操作的實質是丟棄一些現有的提交,然后相應地新建一些內容一樣但實際上不同的提交。如果你已經將提交推送至某個倉庫,而其他人也已經從該倉庫拉取提交并進行了后續工作,此時,如果你用 `git rebase` 命令重新整理了提交并再次推送,你的同伴因此將不得不再次將他們手頭的工作與你的提交進行整合,如果接下來你還要拉取并整合他們修改過的提交,事情就會變得一團糟。
讓我們來看一個在公開的倉庫上執行變基操作所帶來的問題。假設你從一個中央服務器克隆然后在它的基礎上進行了一些開發。你的提交歷史如圖所示:

Figure 3-36. 克隆一個倉庫,然后在它的基礎上進行了一些開發
然后,某人又向中央服務器提交了一些修改,其中還包括一次合并。你抓取了這些在遠程分支上的修改,并將其合并到你本地的開發分支,然后你的提交歷史就會變成這樣:

Figure 3-37. 抓取別人的提交,合并到自己的開發分支
接下來,這個人又決定把合并操作回滾,改用變基;繼而又用 `git push --force` 命令覆蓋了服務器上的提交歷史。之后你從服務器抓取更新,會發現多出來一些新的提交。

Figure 3-38. 有人推送了經過變基的提交,并丟棄了你的本地開發所基于的一些提交
結果就是你們兩人的處境都十分尷尬。如果你執行 `git pull` 命令,你將合并來自兩條提交歷史的內容,生成一個新的合并提交,最終倉庫會如圖所示:

Figure 3-39. 你將相同的內容又合并了一次,生成了一個新的提交
此時如果你執行 `git log` 命令,你會發現有兩個提交的作者、日期、日志居然是一樣的,這會令人感到混亂。此外,如果你將這一堆又推送到服務器上,你實際上是將那些已經被變基拋棄的提交又找了回來,這會令人感到更加混亂。很明顯對方并不想在提交歷史中看到 `C4` 和 `C6`,因為之前就是他們把這兩個提交通過變基丟棄的。
## 用變基解決變基
如果你 **真的** 遭遇了類似的處境,Git 還有一些高級魔法可以幫到你。如果團隊中的某人強制推送并覆蓋了一些你所基于的提交,你需要做的就是檢查你做了哪些修改,以及他們覆蓋了哪些修改。
實際上,Git 除了對整個提交計算 SHA-1 校驗和以外,也對本次提交所引入的修改計算了校驗和——即 “patch-id”。
如果你拉取被覆蓋過的更新并將你手頭的工作基于此進行變基的話,一般情況下 Git 都能成功分辨出哪些是你的修改,并把它們應用到新分支上。
舉個例子,如果遇到前面提到的 [Figure?3-38](#) 那種情境,如果我們不是執行合并,而是執行 `git rebase teamone/master`, Git 將會:
- 檢查哪些提交是我們的分支上獨有的(C2,C3,C4,C6,C7)
- 檢查其中哪些提交不是合并操作的結果(C2,C3,C4)
- 檢查哪些提交在對方覆蓋更新時并沒有被納入目標分支(只有 C2 和 C3,因為 C4 其實就是 C4')
- 把查到的這些提交應用在 `teamone/master` 上面
從而我們將得到與 [Figure?3-39](#) 中不同的結果,如圖 [Figure?3-40](#) 所示。

Figure 3-40. 在一個被變基然后強制推送的分支上再次執行變基
要想上述方案有效,還需要對方在變基時確保 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. 合并
至此,你已在實戰中學習了變基和合并的用法,你一定會想問,到底哪種方式更好。在回答這個問題之前,讓我們退后一步,想討論一下提交歷史到底意味著什么。
有一種觀點認為,倉庫的提交歷史即是 **記錄實際發生過什么**。它是針對歷史的文檔,本身就有價值,不能亂改。從這個角度看來,改變提交歷史是一種褻瀆,你使用*謊言*掩蓋了實際發生過的事情。如果由合并產生的提交歷史是一團糟怎么辦?既然事實就是如此,那么這些痕跡就應該被保留下來,讓后人能夠查閱。
另一種觀點則正好相反,他們認為提交歷史是 **項目過程中發生的故事**。沒人會出版一本書的第一批草稿,軟件維護手冊也是需要反復修訂才能方便使用。持這一觀點的人會使用 rebase 及 filter-branch 等工具來編寫故事,怎么方便后來的讀者就怎么寫。
現在,讓我們回到之前的問題上來,到底合并還是變基好?希望你能明白,并沒有一個簡單的答案。Git 是一個非常強大的工具,它允許你對提交歷史做許多事情,但每個團隊、每個項目對此的需求并不相同。既然你已經分別學習了兩者的用法,相信你能夠根據實際情況作出明智的選擇。
總的原則是,只對尚未推送或分享給別人的本地修改執行變基操作清理歷史,從不對已推送至別處的提交執行變基操作,這樣,你才能享受到兩種方式帶來的便利。
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令