Git 版本控制 › 第 5 章

分支與合併

2026-06-17  ·  branch · switch · fast-forward · 三方合併 · merge commit

到第 4 章為止,我們的歷史都是一條直線。但真實開發要能同時進行多件事:修 bug 的同時開發新功能、實驗一個想法又不弄亂主線。這就是分支。第 3 章已埋好伏筆——分支只是一個存著 40 字元 hash 的輕量指標;本章把它用起來:怎麼開分支、切換,以及兩條分岔的線最後怎麼合併回來。你會看到合併其實只有兩種劇情:指標直接滑過去(fast-forward),或是生一顆有兩個 parent 的新 commit(三方合併)。看懂這兩種,合併就不再神祕。

5.1建立與切換分支:branch / switch

第 3 章說過:一個分支不是一份程式碼拷貝,而是 .git/refs/heads/<名字> 底下一個小檔,裡面只有一行——某顆 commit 的 40 字元 hash。所以「開一條分支」這件事,Git 做的幾乎不用花力氣:

指令做了什麼
git branch feature建立一個叫 feature 的新指標,指向目前 HEAD 那顆 commit不切換過去,你還在原分支。
git switch feature切換:把 HEAD 改指向 feature,並把工作目錄檢出成該分支的內容。
git switch -c feature建立 切換(-c = create),等於上面兩步合一,最常用。
git checkout feature舊指令,同時管「切分支」和「還原檔案」兩件事,易混淆;Git 2.23 後拆成 switch(切分支)與 restore(動檔案),新代碼建議用後者。

關鍵在於分清楚兩件事:建立指標(branch)和移動 HEAD(switch)。下圖中,在 commit Cgit switch -c feature:憑空多出一個 feature 指標、HEAD 從黏著 main 改成黏著 feature——三顆 commit 一個都沒動,工作目錄內容也一樣(因為它們都指向同一顆 C):

switch -c feature 只是新增指標並移動 HEAD 在 commit C 上建立 feature 分支,main 與 feature 都指向 C,HEAD 由指向 main 改為指向 feature,三顆 commit 不變 A B C main feature HEAD HEAD → feature → C commit 一顆都沒動
圖 5-1git switch -c feature:新增 feature 指標(也指向 C)、HEAD 改黏 feature。開分支只是「多一個指標、HEAD 換邊站」
因為開分支這麼廉價(一個小檔 + 改一行 HEAD),Git 鼓勵你頻繁開分支——每個功能、每個實驗都開一條,不要了用 git branch -d 刪掉指標即可,commit 物件本身不受影響。這和「複製整個資料夾來做實驗」的舊世界天差地遠。但 -d安全刪除:Git 會檢查這條分支的 commit 是否已合併進其他分支,沒合併就拒絕(error: ... is not fully merged)——這恰好是「搞砸的實驗」最常見的狀態。真的要丟掉那些沒合併的 commit,得用大寫的 git branch -D 強制刪除;之後那些 commit 雖然還躺在物件庫裡,但沒有指標指著,終究會被垃圾回收清掉。

真正讓兩條分支「分岔」的,是在不同分支上各自 commit。一旦 feature 上提交了新 commit,feature 指標往前走、main 留在原地,歷史就長出了 Y 形的岔路——接下來的問題就是:怎麼把它們合回來

5.2Fast-forward 合併:指標直接滑過去

最簡單的合併情境:你開了 feature、提交了幾顆 commit,而這段期間 main 完全沒動。此時 mainfeature 的「直系祖先」——從 feature 順著 parent 往回走,一定會經過 main 指的那顆。兩條線其實是同一條直線,只是 main 落後了。

這時站在 main 上執行 git merge feature 不會大費周章,Git 發現「沒有任何分岔需要調和」,於是直接把 main 指標往前滑到 feature 的位置。這叫 fast-forward(快轉):

fast-forward 合併:main 指標直接前移 main 在 B、feature 在 D 且 B 是 D 的祖先,merge 後 main 指標直接滑到 D,不產生新 commit A B C D main main merge → 指標快轉 feature
圖 5-2合併前 main 在 B(虛線);因 B 是 D 的祖先,merge 直接把 main 滑到 D。沒有新 commit、歷史仍是直線
fast-forward 能成立的唯一條件是:你目前所在的分支(main)是被合併分支(feature)的祖先——換句話說,自從分岔後,main 一步都沒往前。只要 main 在這期間也提交過哪怕一顆 commit,兩條就真的分岔了,快轉不成立——必須走下一節的三方合併。

5.3三方合併(3-way merge):真正的調和

更常見的情況:你在 feature 上開發時,別人(或你自己)也在 main 上提交了東西。現在兩條線各自往前、真正分岔了——main 不再是 feature 的祖先,沒辦法快轉。Git 必須把兩邊的改動實際合在一起

它靠「三方」來做這件事。哪三方?關鍵角色是共同祖先(merge base):從兩個分支各自往回走,第一個相遇的 commit。有了它,Git 就能比較三顆快照:

三方合併:用共同祖先調和兩邊改動,產生雙 parent 的 merge commit 共同祖先 B 分岔出 main 的 M 與 feature 的 F,三方合併產生 merge commit G,G 同時以 M 與 F 為 parent A B 共同祖先 merge base M F G merge commit (雙 parent) main feature
圖 5-3三方 = 共同祖先 B + main 端 M + feature 端 F。比對三者後產生 merge commit G

「三方」就是拿這三顆 commit 的快照來比對,逐檔(其實逐區塊)決定結果:

對共同祖先 B 而言…合併結果
只有 main 端改了某段、feature 沒動採用 main 的版本
只有 feature 端改了某段、main 沒動採用 feature 的版本
兩邊都沒動原樣保留
兩邊改了同一段、改得不一樣⚠️ 衝突(conflict)——Git 不敢替你決定,交給你手動解(第 6 章)
為什麼非要共同祖先不可?因為光看兩個最終版本,Git 無法分辨「某行是被某方改成這樣」還是「本來就是這樣、另一方刪掉了」。有了祖先當基準點,「相對基準誰動了什麼」就一清二楚——這正是「三方」比「兩方」高明的地方。只要兩邊動的不是同一段,Git 就能自動合好,你甚至不會察覺它在工作。

合併成功(沒衝突,或衝突已解)後,Git 把調和出來的結果寫成一顆新的 commit——它的特別之處在 parent,正是下一節主題。

5.4merge commit:雙 parent 把兩條歷史縫起來

回想第 3 章:一顆普通 commit 有一個 parent(它前一顆)。merge commit 的唯一特別之處是——它有兩個 parent:一個指向你所在分支的最後一顆(M),一個指向被合併分支的最後一顆(F)。這兩個指標就是把兩條分岔歷史重新縫合的那一針。

在 main 上執行 git merge feature 後的結構 在 main 上 merge feature:merge commit G 有兩個 parent,parent 1 指向主動方 main 的 M、parent 2 指向被併方 feature 的 F;main 與 HEAD 前移到 G,feature 仍留在 F M main 合併前 F feature ← 留在原地,不前進 commit G(merge) tree:    合併後的根目錄快照 parent 1: M  ← 你所在分支 parent 2: F  ← 被合併分支 main HEAD 主動方,前進到 G →
圖 5-4main 上執行 git merge feature 的結果:G 有兩個 parent(parent 1 = 主動方 main 的 M、parent 2 = 被併方 feature 的 F);mainHEAD 前移到 G,而 feature 仍留在 F 不動

5.5互動:兩種合併並排對照

左邊是 fast-forward(main 沒動過)、右邊是 三方合併(兩邊都動過)。各按「執行合併」看結果:左邊只是指標滑過去、不生 commit;右邊長出一顆雙 parent 的 merge commit G

① Fast-forward
合併前 main 完全沒動 → 可快轉
fast-forward 互動圖 main 在 B、feature 在 D,執行合併後 main 指標滑到 D A B C D feature main
main 停在 B、feature 在 D,且 B 是 D 的祖先。按合併看 main 怎麼動。
② 三方合併
兩邊都提交過 → 分岔,需 merge commit
三方合併互動圖 共同祖先 B 分出 main 的 M 與 feature 的 F,執行合併後出現雙 parent 的 merge commit G A B M F feature G main
main 在 M、feature 在 F,從共同祖先 B 各自分岔。按合併看會發生什麼。
一句話分辨:看「我這條分支從分岔後有沒有再 commit」。沒有 → 可 fast-forward(指標滑過去,歷史是直線);有 → 必須三方合併(生 merge commit,歷史出現菱形)。下一章接著處理三方合併時最棘手的那一格:兩邊改了同一段造成的衝突。

5.6本章小結

概念一句話記住
branch建一個 40 字元 hash 的輕量指標(指向目前 HEAD),不切換
switch -c建立並切換:移動 HEAD + 檢出工作目錄;舊式 checkout 已拆成 switch / restore
fast-forward目標是我的直系後代 → main 指標直接滑過去,不生 commit、歷史維持直線
三方合併兩邊都動過 → 用共同祖先(merge base)當基準調和,逐區塊決定取捨
共同祖先有了基準才分得清「誰改了什麼」;兩邊改同一段 → 衝突(第 6 章)
merge commit唯一特別處是兩個 parent(parent 1 = 你所在分支),把兩條歷史縫合,沒改寫任何舊 commit
現在你知道分岔的歷史怎麼合回來了——但只講了「順利合好」的情況。當兩邊改到同一段程式碼,Git 會停下來把選擇權交給你。下一章專講合併衝突:衝突標記 <<<<<<< ======= >>>>>>> 怎麼讀、解衝突的完整流程與工具。