Git 版本控制 › 第 7 章

rebase vs. merge

2026-06-19  ·  rebase · merge · patch · SHA · 黃金法則 · force-push

第 5、6 章把合併講完了:merge 把兩條分岔的線起來,留下一顆有兩個 parent 的 merge commit,真實歷史原封不動。本章換一個完全不同的思路——rebase。它不縫合,而是把你的 commit 逐一重寫、接到另一條分支的尾端,讓歷史看起來像「從來沒分岔過」的一條直線。兩者產出的程式碼可以完全一樣,差別在歷史長什麼樣、以及commit 的身分(SHA)有沒有被改掉。看懂這點,就懂了為什麼有條鐵律叫「不要 rebase 已經推送出去的分支」。

7.1rebase 怎麼「重寫」commit、換掉 parent

先記住一句話:rebase 不是搬移 commit,而是用舊 commit 的「改動」當藍圖,在新地點重新蓋一批 commit。「base」就是一個 commit 的起點——它的 parent。Rebase 做的事,就是把起點換掉

假設 main 在你開分支後又前進了一顆 C,而你在 feature 上提交了 DE。執行 git switch feature; git rebase main 時,Git 內部走三步:

rebase 的三個內部步驟 找共同祖先、把 D E 抽成 patch、在 C 之後重放成 D' E' 點擊各步驟看 Git 內部到底做了什麼 ① 找共同祖先 B C D E ▸ B 是兩線分岔點 ② 抽成 patch patch D = B→D 的差異 「改了什麼」,不是快照 patch E = D→E 的差異 暫存到 .git/rebase-merge/ ③ 在 C 之後重放 C D' E' ▸ 全新 commit、全新 SHA 內容同 D E,身分卻不同
圖 7-1rebase 三步:找共同祖先 B → 把 D、E 抽成 patch → 在 C 之後重放成 D'、E'。點節點展開細節
關鍵心智模型:rebase 會製造新的 commit、丟棄舊的。程式碼可以一模一樣,但 commit 的「身分證號碼」全換了。這正是它強大(歷史變乾淨)也危險(改寫歷史)的根源——7.3 的黃金法則就建立在這件事上。
先記著一個進階地雷:本章的例子都是線性的 DE;若你的分支裡面本身含有 merge commit,預設的 rebase 會把它們拉平——因為重放的是「改動」,而合併結構不是改動。要保留分支拓樸得用 git rebase --rebase-merges,細節留到第 17 章。
延伸問答

rebase 把起點換掉之後,為什麼還會產生衝突?照理說接過去就好了啊?

因為「換掉起點」不是只把 parent 欄位改個指向,而是要把你的改動重新套用到一份可能已經不一樣的程式碼上。衝突就出在「可能不一樣」這五個字。

回想上面第二步:rebase 把 DE 抽成 patch,而 patch 記的不是「檔案最終長怎樣」,是「在某幾行附近,把這幾行換成那幾行」(還帶著上下幾行當定位用的 context)。第三步「在 C 之後重放」,實際做的是把 patch 貼到 C 的內容上:

patch D 想做的: 第 42 行  "你好"  →  "Hi"     (這是站在 B 寫的)
但 C 已經把  : 第 42 行  "你好"  →  "Hello"  (main 上也動了同一行)
                          ▲ 同一塊被兩邊各改成不一樣 → 撞上 → 衝突

所以衝突從來不是「換 parent」這個動作造成的,而是「你的改動」和「新 base 上的改動」碰到了同一段。把 patch 想成一塊磁磚:起點換掉等於把它重貼到新地基上——地基動到那一處,Git 默默貼好、毫無衝突;地基在同一處也被改過,就貼不平、停下來請你裁決,標記跟第 6 章一模一樣的 <<< === >>>

順帶一提,這也解釋了一個常見的痛點——rebase 的衝突可能要解很多次:

衝突在何時發生
merge兩邊改同一段 → 在生 merge commit 那一刻,一次算總帳
rebaseD、E 逐顆重貼到 C 上,每一顆都可能撞到 C 改過的同段 → 解完一顆 git rebase --continue 再貼下一顆

兩者判定衝突的規則完全相同(同一塊 hunk 雙方都改,見第 6 章),差別只在 merge 一次結清、rebase 一顆一顆貼所以可能反覆遇到。還有個容易忽略的邊角:就算 C 沒改到你那幾行、卻改到了 patch 用來定位的上下文 context,也可能讓 patch「對不準」而衝突——這就是第 6 章「相鄰行保守判定」在 rebase 裡的版本。

7.2同一組 commit,merge 與 rebase 的兩種結果

下面是完全相同的起點:main 走到 C,feature 從共同祖先 B 分出 DE。按按鈕,看兩種整合方式產出的歷史長得多不一樣。

merge 與 rebase 結果對照 同一組分岔的 commit,merge 產生雙 parent 的 merge commit、保留分叉;rebase 重寫成 D'E' 的線性歷史 A B C D E 共同祖先 main feature M main 分叉被保留 多一顆 merge commit M 2 個 parent D' E' feature 一條直線 沒有 merge commit D E 已被改寫
兩條線從共同祖先 B 分岔:main 上有 C、feature 上有 D、E。選一種整合方式,看歷史變成什麼樣。
圖 7-2同一組起點,按鈕切換:merge 生雙 parent 的 M、保留分叉;rebase 重寫成 D'、E' 的線性歷史

程式碼最終內容兩邊相同,但歷史的「形狀」與「誠實度」不同。怎麼選?看你重視整潔還是真實:

merge(縫合)rebase(重寫)
歷史形狀保留分叉,出現菱形/merge commit拉成一條直線,像沒分岔過
commit 身分原 commit 全部不動,SHA 不變被搬的 commit 重建、SHA 全換
忠於事實✅ 記錄「真的有兩條線並行過」❌ 抹掉並行的痕跡(換來乾淨)
適合合併 PR、整合長期分支、要保留協作脈絡推送前整理自己本地的零碎 commit、讓 PR 線性好讀
常見搭配git merge --no-ff 明確留下合併點git rebase -i 順手 squash/reword/reorder
實務上常兩者並用:在 feature 分支用 rebase 把自己的歷史整理乾淨、跟上 main 最新進度;真正合進 main 時用 merge 留下「這個功能在此合入」的記錄。一句話口訣:rebase 整理私有的、merge 紀錄公開的
延伸問答

表格提到的 git rebase -i(interactive rebase)是什麼?它能做什麼?

-i 是 rebase 的「手動駕駛模式」。普通 git rebase main 把你的 commit 原封不動逐一重放到新 base;加上 -i,Git 會在重放前先打開一份「待辦清單」,讓你逐顆決定每個 commit 怎麼處理——改順序、合併、改訊息、丟掉都行。本質還是 7.1 那套(抽 patch → 重放 → 全新 SHA),所以一樣別對已推送的分支做(7.3)。

最常見的用途不是接到別條分支,而是整理自己最近幾顆 commit:

git rebase -i HEAD~4     # 整理最近 4 顆;清單順序是「舊在上、新在下」
指令作用
pick照原樣保留這顆
reword保留改動,停下來讓你改 commit 訊息
edit重放到這顆就暫停,讓你修改內容或拆分
squash把這顆併進前一顆,兩者訊息合併
fixup同 squash,但丟棄這顆的訊息(「修正上一顆」的小 commit 最常用)
drop刪掉這顆(直接刪行也等效)
調整行序沒有關鍵字,直接搬動行的上下位置即可 reorder
exec在該點跑一條 shell 指令(如 x npm test 每顆都驗)

下面這個模擬器就是一份 git rebase -i HEAD~4 的待辦清單。改每列左邊的動作、用 ▲▼ 調順序,再按套用 rebase,看 Git 會把歷史重建成什麼樣(預設已示範:兩個 typo 用 fixup 併掉):

$ git rebase -i HEAD~4  —  待辦清單(舊 → 新,由上而下)

幾個關鍵點:squash vs fixup 都合併,差別只在要不要留被併者的訊息;edit 能拆 commit(暫停後 git reset HEAD^ 退回改動,再分多次 commit);中途撞衝突照樣可能發生(原因見上一則問答),解完 git rebase --continue、想整個放棄用 git rebase --abort;真的整理壞了,git reflog 救得回(第 9 章)。

延伸問答

上面提到 git rebase --continue--abort,那還有個 --skip 是什麼?跟前面兩個差在哪?

rebase 停下來的時候,Git 其實是在問你一個問題:「這一顆貼不上去,你要怎麼辦?」三個指令就是三種回答。分辨它們只要抓一個維度——你要丟掉的是「這一顆」,還是「整場 rebase」?

rebase 停下來時的三條路:continue、skip、abort rebase 停在 D 這顆時,--continue 把解好的 D 重建成 D' 並繼續;--skip 把 D 整顆丟掉、改動不進新歷史;--abort 放棄整場 rebase、回到開始前 ⏸ rebase 停在 D D 的 patch 貼不上新 base Git 等你決定 git rebase --continue  ——「我解好了」 用你 git add 進暫存區的內容把 D 重建成 D',接著貼下一顆 E。 改動保留,rebase 繼續走。 git rebase --skip  ——「不要 D 這一顆了」 把 D 整顆丟掉:它的改動不會進入新歷史,直接跳去貼 E。 丟的是「一顆」,rebase 仍繼續走。 git rebase --abort  ——「整場都不要了」 回到 rebase 開始前:D、E 原封不動、SHA 都沒變,當沒發生過。 丟的是「整場」,一顆都不會少。
圖 7-3rebase 停下來時的三條路。關鍵分野:--skip 丟掉的是眼前這一顆(rebase 照樣走完),--abort 丟掉的是整場 rebase(一顆都沒少)
--continue--skip--abort
丟掉什麼什麼都不丟眼前這一顆整場 rebase
這顆的改動以你解完的樣子留下永久不進新歷史原封不動留在舊 commit 裡
rebase 之後繼續貼下一顆繼續貼下一顆結束,回到起點
會不會弄丟工作不會⚠️ ,而且靜悄悄不會
--skip 是三者裡唯一會弄丟工作的。它常被誤用成「這顆的衝突我解不動,跳過去再說」——但跳過的代價是那顆的改動整個消失,Git 不會再問第二次。卡在衝突而想脫身,要按的是 --abort(全身而退、什麼都不會少),不是 --skip

--skip 什麼時候才是正解?答案是:當這顆 patch 已經沒有意義的時候——它想做的改動,新 base 上早就有人做過了。這時貼上去等於什麼都沒改,留著只會多一顆空 commit。最常見的兩個場景:

  • 你的 commit 被 cherry-pick 或 squash merge 進 main 了:內容已經在新 base 裡,再貼一次是重複。
  • 你解衝突時,決定「整段都採用新 base 的版本」:等於這顆什麼都不改了。

這種情況 Git 通常會自己提示你,訊息長這樣:

$ git rebase --continue
The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to skip this commit, use:

    git rebase --skip

看到 Git 主動說 now empty / 建議你 skip,那才是按 --skip 的時機。其他時候看到衝突就想 skip,幾乎都是誤按。

萬一真的誤按了 --skip?還有救,但要快:那顆被丟掉的 commit 只是沒進新歷史,物件本身還在。用 git reflog 找到 rebase 開始前的位置,把丟掉的 SHA git cherry-pick 回來,或直接 git reset --hard 退回去重做一次。完整救援手法見第 9 章——順帶一提,這正是「rebase 前先確認自己在哪」的價值:--abort 只在 rebase 還沒結束時有效,rebase 一旦跑完,後悔藥就只剩 reflog 了。

7.3黃金法則:不要 rebase 已經推送的公開分支

rebase 的威力來自「重建 commit」,它的危險也來自同一件事。只要那些 commit 還只在你自己電腦上,重寫它們沒人會受傷。但一旦你 push 出去、別人已經基於那些 SHA 接著工作,你再 rebase 就是在改寫「大家共享的事實」。

麻煩在於:rebase 後本地的 commit 跟遠端對不上,普通 push 會被拒絕,你只能 --force。而 force push 會把遠端的舊 commit 換掉——

rebase 已推送分支造成的歷史分裂 你 force push 改寫後的 D' E',同事本地仍基於舊 D E,pull 後出現重複 commit 與衝突 你(rebase 後 force push) C D' E' 新 SHA ↓ git push --force(把遠端的 D E 換成 D' E') 同事(本地還停在舊 D、E) C D E 舊 SHA 同事一 pull → 災難 • Git 把 D E 與 D' E' 當成 四顆「不同」commit • 內容一樣 → 重複的改動 擠成一團、處處衝突 • 有人的工作可能被 force push 直接蓋掉
圖 7-4你把已推送的 D、E rebase 成 D'、E' 並 force push,同事本地仍基於舊 D、E——兩套 SHA 一 pull 就撞成重複 commit 與大量衝突
黃金法則:只 rebase 還沒推送、或確定只有你自己用的分支。判準很簡單——問自己「有沒有別人可能已經基於這些 commit 工作了?」。會的話,改用 merge。

幾個實務推論:

延伸問答

等等——我最常做的 rebase,是「本地有幾顆 commit,結果遠端 main 已經往前跑了,push 被拒絕」時跑 git pull --rebase。這不就是在 rebase 一條公開分支嗎?違反黃金法則了嗎?

完全沒有違反,而且這正是 rebase 最常見、最安全的用法。很多人第一次碰 rebase 就是這個情境,又剛好讀到黃金法則,結果嚇得不敢按下去——這裡把界線畫清楚。

關鍵在於黃金法則管的是「被重寫的是哪幾顆 commit」,不是「你 rebase 到哪裡去」git rebase origin/main 這行指令裡,origin/main新的 base(目的地),它一根寒毛都不會被動到;被抽成 patch、重建成新 SHA 的,是你自己那幾顆還沒推送的 commit。沒推送 → 沒人基於它們工作 → 重寫它們不會傷到任何人。

落後遠端時用 git pull --rebase 追上 origin/main 已前進到 C3,你本地未推送的 D、E 仍接在 C1 之後;pull --rebase 把 D、E 重建成接在 C3 之後的 D'、E',遠端的 C1 到 C3 完全沒被改寫 ① push 被拒:origin/main 已經跑到 C3,你的 D、E 還接在 C1 後面 C1 C2 C3 origin/main(同事推的) D E 你的 D、E:只在本機,從沒推送過 ↓ git pull --rebase(= fetch + rebase origin/main) ② D、E 被重建到 C3 之後,歷史變直線,push 就過了 C1 C2 C3 D' E' 新 SHA ✅ 沒違反黃金法則 • 被重寫的只有 D、E • C1–C3 原封不動 • 不必 force push
圖 7-5落後遠端時的 git pull --rebase:被重建的是你未推送的 D、E,新 base C1–C3 完全沒被改寫——所以安全,而且普通 push 就能推上去

把兩個情境並排看,界線就很清楚了:

圖 7-5:追上 origin/main圖 7-4:rebase 已推送的分支
被重寫的 commit你本機的 D、E,從沒推送過D、E 已經在遠端,別人看得到
遠端歷史只是被當成新 base,沒動到被 force push 換掉
推送方式普通 git push 就過只能 --force
結論✅ 安全,天天做都行⚠️ 危險,除非分支只有你在用

所以真正的判準只有一句:rebase 前先問「我要被重寫的那幾顆,推出去過嗎?」沒有 → 放心做;有 → 回頭看上面那串實務推論。

那為什麼這情境要用 --rebase 而不是預設的 git pull(= fetch + merge)?因為預設的 merge 會替你生一顆 Merge branch 'main' of github.com:… 的 merge commit——它不代表任何有意義的功能合入,純粹是「我 pull 的時候剛好落後了」的雜訊。每個人每天 pull 幾次,歷史就被這種菱形塞爆。--rebase 則是把你的 commit 挪到最新進度後面,歷史保持一條直線:

git pull --rebase                       # 這次 pull 用 rebase 追上
git config --global pull.rebase true    # 一勞永逸:以後 pull 預設都用 rebase
git config --global rebase.autoStash true   # 順手:工作區有未 commit 的改動也不用先手動 stash
兩個實務提醒:(1) pull --rebase 一樣可能撞衝突(原因見 7.2 的問答)——解完 git addgit rebase --continue,想整個放棄用 git rebase --abort,不會有任何損失。(2) 如果你的 commit 已經推到自己的 PR 分支,再 rebase 到最新 main,那就確實是在重寫已推送的 commit 了,得用 --force-with-lease。只有你自己用的 PR 分支這樣做很常見,但那已經是另一個情境——別和這裡的「本地未推送」混為一談。

7.4本章小結

概念一句話記住
rebase 本質不是搬移,是用舊改動當藍圖、在新 base 上重建一批全新 commit
三步驟找共同祖先 → 把你的 commit 抽成 patch → 在新 base 之後逐一重放
SHA 為何變parent 換了 → commit 物件內容變 → 雜湊出的 SHA 必然不同(回扣第 2、3 章)
停下來的三條路--continue 解好了繼續;--skip 丟掉這一顆(會弄丟改動,只在 Git 說 now empty 時用);--abort 丟掉整場(什麼都不會少)
merge vs rebasemerge 保留真實分叉(留 merge commit);rebase 換來乾淨直線(抹掉並行痕跡)
怎麼選rebase 整理私有的、merge 記錄公開的合入
黃金法則不要 rebase 已推送/別人可能在用的分支——force push 會撞出重複 commit 與衝突
法則怎麼判被重寫的那幾顆推出去過沒有,不是看你 rebase 到哪;git pull --rebase 追上 origin/main 只重寫自己未推送的 commit,安全
本章一直提到「push」「force push」「同事 pull」——但還沒正式講遠端怎麼運作。下一章補上這塊:remote / origin 是什麼、fetch / pull / push 各自搬了哪些物件、追蹤分支(upstream)怎麼把本地和遠端綁在一起,以及 feature branch + PR 的典型協作流程。把第 8 章看完,你就能把前七章的單機心智模型,延伸到多人協作的世界。