2026-06-19 · rebase · merge · patch · SHA · 黃金法則 · force-push
先記住一句話:rebase 不是搬移 commit,而是用舊 commit 的「改動」當藍圖,在新地點重新蓋一批 commit。「base」就是一個 commit 的起點——它的 parent。Rebase 做的事,就是把起點換掉。
假設 main 在你開分支後又前進了一顆 C,而你在 feature 上提交了 D、E。執行 git switch feature; git rebase main 時,Git 內部走三步:
D、E;若你的分支裡面本身含有 merge commit,預設的 rebase 會把它們拉平——因為重放的是「改動」,而合併結構不是改動。要保留分支拓樸得用 git rebase --rebase-merges,細節留到第 17 章。
rebase 把起點換掉之後,為什麼還會產生衝突?照理說接過去就好了啊?
因為「換掉起點」不是只把 parent 欄位改個指向,而是要把你的改動重新套用到一份可能已經不一樣的程式碼上。衝突就出在「可能不一樣」這五個字。
回想上面第二步:rebase 把 D、E 抽成 patch,而 patch 記的不是「檔案最終長怎樣」,是「在某幾行附近,把這幾行換成那幾行」(還帶著上下幾行當定位用的 context)。第三步「在 C 之後重放」,實際做的是把 patch 貼到 C 的內容上:
patch D 想做的: 第 42 行 "你好" → "Hi" (這是站在 B 寫的)
但 C 已經把 : 第 42 行 "你好" → "Hello" (main 上也動了同一行)
▲ 同一塊被兩邊各改成不一樣 → 撞上 → 衝突
所以衝突從來不是「換 parent」這個動作造成的,而是「你的改動」和「新 base 上的改動」碰到了同一段。把 patch 想成一塊磁磚:起點換掉等於把它重貼到新地基上——地基沒動到那一處,Git 默默貼好、毫無衝突;地基在同一處也被改過,就貼不平、停下來請你裁決,標記跟第 6 章一模一樣的 <<< === >>>。
順帶一提,這也解釋了一個常見的痛點——rebase 的衝突可能要解很多次:
| 衝突在何時發生 | |
|---|---|
| merge | 兩邊改同一段 → 在生 merge commit 那一刻,一次算總帳 |
| rebase | D、E 逐顆重貼到 C 上,每一顆都可能撞到 C 改過的同段 → 解完一顆 git rebase --continue 再貼下一顆 |
兩者判定衝突的規則完全相同(同一塊 hunk 雙方都改,見第 6 章),差別只在 merge 一次結清、rebase 一顆一顆貼所以可能反覆遇到。還有個容易忽略的邊角:就算 C 沒改到你那幾行、卻改到了 patch 用來定位的上下文 context,也可能讓 patch「對不準」而衝突——這就是第 6 章「相鄰行保守判定」在 rebase 裡的版本。
下面是完全相同的起點:main 走到 C,feature 從共同祖先 B 分出 D、E。按按鈕,看兩種整合方式產出的歷史長得多不一樣。
程式碼最終內容兩邊相同,但歷史的「形狀」與「誠實度」不同。怎麼選?看你重視整潔還是真實:
| merge(縫合) | rebase(重寫) | |
|---|---|---|
| 歷史形狀 | 保留分叉,出現菱形/merge commit | 拉成一條直線,像沒分岔過 |
| commit 身分 | 原 commit 全部不動,SHA 不變 | 被搬的 commit 重建、SHA 全換 |
| 忠於事實 | ✅ 記錄「真的有兩條線並行過」 | ❌ 抹掉並行的痕跡(換來乾淨) |
| 適合 | 合併 PR、整合長期分支、要保留協作脈絡 | 推送前整理自己本地的零碎 commit、讓 PR 線性好讀 |
| 常見搭配 | git merge --no-ff 明確留下合併點 | git rebase -i 順手 squash/reword/reorder |
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 併掉):
幾個關鍵點: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」?
--continue | --skip | --abort | |
|---|---|---|---|
| 丟掉什麼 | 什麼都不丟 | 眼前這一顆 | 整場 rebase |
| 這顆的改動 | 以你解完的樣子留下 | ❌ 永久不進新歷史 | 原封不動留在舊 commit 裡 |
| rebase 之後 | 繼續貼下一顆 | 繼續貼下一顆 | 結束,回到起點 |
| 會不會弄丟工作 | 不會 | ⚠️ 會,而且靜悄悄 | 不會 |
--skip 是三者裡唯一會弄丟工作的。它常被誤用成「這顆的衝突我解不動,跳過去再說」——但跳過的代價是那顆的改動整個消失,Git 不會再問第二次。卡在衝突而想脫身,要按的是 --abort(全身而退、什麼都不會少),不是 --skip。
那 --skip 什麼時候才是正解?答案是:當這顆 patch 已經沒有意義的時候——它想做的改動,新 base 上早就有人做過了。這時貼上去等於什麼都沒改,留著只會多一顆空 commit。最常見的兩個場景:
這種情況 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 了。
rebase 的威力來自「重建 commit」,它的危險也來自同一件事。只要那些 commit 還只在你自己電腦上,重寫它們沒人會受傷。但一旦你 push 出去、別人已經基於那些 SHA 接著工作,你再 rebase 就是在改寫「大家共享的事實」。
麻煩在於:rebase 後本地的 commit 跟遠端對不上,普通 push 會被拒絕,你只能 --force。而 force push 會把遠端的舊 commit 換掉——
幾個實務推論:
main / develop:永遠不要 rebase,整合一律走 merge。--force-with-lease(比 --force 安全:遠端在你上次 fetch 之後被動過就會擋下——但 fetch 本身會解除這道保護,陷阱與完整解法見 8.5)是常見做法;但要團隊有共識才這麼搞。git reflog 找得回 rebase 前的舊 commit(第 9 章專講救援)。等等——我最常做的 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:被重建的是你未推送的 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
pull --rebase 一樣可能撞衝突(原因見 7.2 的問答)——解完 git add 再 git rebase --continue,想整個放棄用 git rebase --abort,不會有任何損失。(2) 如果你的 commit 已經推到自己的 PR 分支,再 rebase 到最新 main,那就確實是在重寫已推送的 commit 了,得用 --force-with-lease。只有你自己用的 PR 分支這樣做很常見,但那已經是另一個情境——別和這裡的「本地未推送」混為一談。
| 概念 | 一句話記住 |
|---|---|
| rebase 本質 | 不是搬移,是用舊改動當藍圖、在新 base 上重建一批全新 commit |
| 三步驟 | 找共同祖先 → 把你的 commit 抽成 patch → 在新 base 之後逐一重放 |
| SHA 為何變 | parent 換了 → commit 物件內容變 → 雜湊出的 SHA 必然不同(回扣第 2、3 章) |
| 停下來的三條路 | --continue 解好了繼續;--skip 丟掉這一顆(會弄丟改動,只在 Git 說 now empty 時用);--abort 丟掉整場(什麼都不會少) |
| merge vs rebase | merge 保留真實分叉(留 merge commit);rebase 換來乾淨直線(抹掉並行痕跡) |
| 怎麼選 | rebase 整理私有的、merge 記錄公開的合入 |
| 黃金法則 | 不要 rebase 已推送/別人可能在用的分支——force push 會撞出重複 commit 與衝突 |
| 法則怎麼判 | 看被重寫的那幾顆推出去過沒有,不是看你 rebase 到哪;git pull --rebase 追上 origin/main 只重寫自己未推送的 commit,安全 |
fetch / pull / push 各自搬了哪些物件、追蹤分支(upstream)怎麼把本地和遠端綁在一起,以及 feature branch + PR 的典型協作流程。把第 8 章看完,你就能把前七章的單機心智模型,延伸到多人協作的世界。