2026-06-17 · branch · switch · fast-forward · 三方合併 · merge commit
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 C 上 git switch -c feature:憑空多出一個 feature 指標、HEAD 從黏著 main 改成黏著 feature——三顆 commit 一個都沒動,工作目錄內容也一樣(因為它們都指向同一顆 C):
git switch -c feature:新增 feature 指標(也指向 C)、HEAD 改黏 feature。開分支只是「多一個指標、HEAD 換邊站」git branch -d 刪掉指標即可,commit 物件本身不受影響。這和「複製整個資料夾來做實驗」的舊世界天差地遠。但 -d 是安全刪除:Git 會檢查這條分支的 commit 是否已合併進其他分支,沒合併就拒絕(error: ... is not fully merged)——這恰好是「搞砸的實驗」最常見的狀態。真的要丟掉那些沒合併的 commit,得用大寫的 git branch -D 強制刪除;之後那些 commit 雖然還躺在物件庫裡,但沒有指標指著,終究會被垃圾回收清掉。
真正讓兩條分支「分岔」的,是在不同分支上各自 commit。一旦 feature 上提交了新 commit,feature 指標往前走、main 留在原地,歷史就長出了 Y 形的岔路——接下來的問題就是:怎麼把它們合回來。
最簡單的合併情境:你開了 feature、提交了幾顆 commit,而這段期間 main 完全沒動。此時 main 是 feature 的「直系祖先」——從 feature 順著 parent 往回走,一定會經過 main 指的那顆。兩條線其實是同一條直線,只是 main 落後了。
這時站在 main 上執行 git merge feature 不會大費周章,Git 發現「沒有任何分岔需要調和」,於是直接把 main 指標往前滑到 feature 的位置。這叫 fast-forward(快轉):
merge 直接把 main 滑到 D。沒有新 commit、歷史仍是直線feature 指標還留在 D,通常接著 git branch -d feature 收掉它。git merge --no-ff feature,強迫產生一顆 merge commit(下一節的結構),即使本來可以快轉。main)是被合併分支(feature)的祖先——換句話說,自從分岔後,main 一步都沒往前。只要 main 在這期間也提交過哪怕一顆 commit,兩條就真的分岔了,快轉不成立——必須走下一節的三方合併。
更常見的情況:你在 feature 上開發時,別人(或你自己)也在 main 上提交了東西。現在兩條線各自往前、真正分岔了——main 不再是 feature 的祖先,沒辦法快轉。Git 必須把兩邊的改動實際合在一起。
它靠「三方」來做這件事。哪三方?關鍵角色是共同祖先(merge base):從兩個分支各自往回走,第一個相遇的 commit。有了它,Git 就能比較三顆快照:
「三方」就是拿這三顆 commit 的快照來比對,逐檔(其實逐區塊)決定結果:
| 對共同祖先 B 而言… | 合併結果 |
|---|---|
| 只有 main 端改了某段、feature 沒動 | 採用 main 的版本 |
| 只有 feature 端改了某段、main 沒動 | 採用 feature 的版本 |
| 兩邊都沒動 | 原樣保留 |
| 兩邊改了同一段、改得不一樣 | ⚠️ 衝突(conflict)——Git 不敢替你決定,交給你手動解(第 6 章) |
合併成功(沒衝突,或衝突已解)後,Git 把調和出來的結果寫成一顆新的 commit——它的特別之處在 parent,正是下一節主題。
回想第 3 章:一顆普通 commit 有一個 parent(它前一顆)。merge commit 的唯一特別之處是——它有兩個 parent:一個指向你所在分支的最後一顆(M),一個指向被合併分支的最後一顆(F)。這兩個指標就是把兩條分岔歷史重新縫合的那一針。
main 上執行 git merge feature 的結果:G 有兩個 parent(parent 1 = 主動方 main 的 M、parent 2 = 被併方 feature 的 F);main 與 HEAD 前移到 G,而 feature 仍留在 F 不動git log --oneline --graph 會畫出那個「分岔再會合」的菱形——看到雙線收合,就是一顆 merge commit。merge 時所在的那條分支(本例的 main)是 parent 1,被合併進來的分支(feature)是 parent 2。順序不是隨便排的——它記錄了「站在誰身上、把誰併進來」。main 與 HEAD 移到新的 merge commit G;被併的 feature 留在原本的 F 不動。道理很單純——你只是把 feature 的內容「抓過來併進 main」,並沒有對 feature 做任何事,它沒有理由移動。想讓 feature 也追上,得另外切到 feature 再合併(或之後 feature 自己往前 commit)。HEAD^1(走 parent 1 = main 側)、HEAD^2(走 parent 2 = feature 側)與 git show <merge> 區分兩邊的依據。左邊是 fast-forward(main 沒動過)、右邊是 三方合併(兩邊都動過)。各按「執行合併」看結果:左邊只是指標滑過去、不生 commit;右邊長出一顆雙 parent 的 merge commit G。
| 概念 | 一句話記住 |
|---|---|
branch | 建一個 40 字元 hash 的輕量指標(指向目前 HEAD),不切換 |
switch -c | 建立並切換:移動 HEAD + 檢出工作目錄;舊式 checkout 已拆成 switch / restore |
| fast-forward | 目標是我的直系後代 → main 指標直接滑過去,不生 commit、歷史維持直線 |
| 三方合併 | 兩邊都動過 → 用共同祖先(merge base)當基準調和,逐區塊決定取捨 |
| 共同祖先 | 有了基準才分得清「誰改了什麼」;兩邊改同一段 → 衝突(第 6 章) |
| merge commit | 唯一特別處是兩個 parent(parent 1 = 你所在分支),把兩條歷史縫合,沒改寫任何舊 commit |
<<<<<<< ======= >>>>>>> 怎麼讀、解衝突的完整流程與工具。