一. 前言:你不是不會 commit,你只是還沒學會整理歷史 #
很多人第一次看到 git rebase 都有點戒慎恐懼:知道它很強,也知道它可能改寫歷史,於是平常只敢用 merge,一看到 rebase -i 就自動把自己歸類到「還沒那麼厲害」的那一側。
但其實 rebase 沒有想像中玄。它做的事情很單純:把你原本的 commit 暫時拿起來,換到另一個基底上重新套用。這個動作讓 branch 歷史更線性、PR 更好讀,也讓你可以在送 review 前把那些 wip、fix typo、oops again 收拾乾淨。
如果你已經看過拍拍君前一篇 Git Branch 策略完全攻略,你大概知道 branch 應該怎麼切、怎麼協作。那今天這篇,就是在 branch 之內繼續深一層:把 commit 歷史整理成「人能讀懂」的樣子。
這篇會從最實用的四個面向來講:什麼是 rebase、什麼時候該用、互動式 rebase 怎麼整理歷史、遇到衝突時怎麼處理。拍拍君不走宗教戰爭路線,merge 跟 rebase 都是工具,重點是你知不知道自己在改什麼。
如果 Git 基礎還不熟,建議先補一下 Git 入門。但如果你已經會 add、commit、push,那這篇就很適合拿來把工作流再升級一點。
二. 安裝與基本設定:先把環境整理順一點 #
rebase 是 Git 內建功能,不需要額外安裝。不過 Git 版本太舊時,某些參數與提示訊息會比較不友善,所以第一步還是先確認版本。
git --version
如果你還沒安裝 Git,可以這樣裝:
# macOS
brew install git
# Ubuntu / Debian
sudo apt update
sudo apt install git
# Windows
winget install --id Git.Git -e
基本身份設定也別忘了:
git config --global user.name "拍拍君"
git config --global user.email "pypy@example.com"
接著,拍拍君很推薦先打開兩個跟 rebase 特別有關的選項:
git config --global rerere.enabled true
git config --global rebase.autosquash true
rerere.enabled 的意思是讓 Git 記住你之前解過的衝突;下次再遇到同型態衝突時,常常可以少做一輪手工整理。rebase.autosquash 則是讓 fixup! 和 squash! 這種 commit 訊息在 interactive rebase 裡自動排到正確位置,非常適合整理 PR 前的小碎修。
如果你平常很常把功能分支跟上 main,也可以考慮:
git config --global pull.rebase true
這會讓 git pull 預設走 rebase。但拍拍君的建議是:先把 git fetch + git rebase origin/main 這個手動流程學熟,再決定要不要讓它變成預設。因為預設很方便,但看不懂流程時也最容易在奇怪的地方踩雷。
你可以順手檢查目前相關設定:
git config --global --list | rg 'rebase|rerere|pull'
三. 先搞懂概念:rebase 跟 merge 的差別到底在哪裡? #
很多人並不是不會敲指令,而是沒有搞清楚 merge 和 rebase 在歷史圖上到底做了什麼。這一點沒想通,後面遇到衝突時就很容易只剩恐懼。
先想像下面這個歷史:
A---B---C main
\
D---E feature/login
意思是:feature/login 這條分支是從 B 切出去的,之後 main 前進到 C,而功能分支上則有 D 和 E 兩個提交。
3.1 如果你用 merge #
如果你在 feature/login 上執行:
git merge main
歷史通常會變成這樣:
A---B---C---------M feature/login
\ /
D---------E
這裡的 M 是 merge commit。它的好處是保留了實際分支發展過程,不改寫既有 commit,也因此對共享分支比較安全。缺點則是歷史可能會越長越像義大利麵,尤其是多人協作、功能分支很多時,光看 log 就很想逃。
3.2 如果你用 rebase #
如果你改成在 feature/login 上執行:
git rebase main
Git 會把 D、E 暫時拿起來,重新接到 C 後面,結果看起來像這樣:
A---B---C---D'---E' feature/login
這裡的 D'、E' 不是原本那兩個 commit,而是「內容相同、但 ID 不同」的新 commit。這也是為什麼大家會說 rebase 會改寫歷史:不是改掉你的內容,而是重新生成 commit。
3.3 該怎麼選比較務實? #
拍拍君給一個很實用的懶人判斷法:
- 整理自己的功能分支歷史 → 常用
rebase - 合併共享分支、避免改寫公共歷史 → 優先
merge如果想再白話一點,merge像是在時間軸上保留分流與匯流,rebase則像是把你的支線重新剪接,讓故事更直線。兩者都不是正邪之爭,重點只在於你是否理解影響範圍。
四. 最常見的 rebase 場景:把功能分支跟上最新 main #
這是絕大多數人最常會碰到的 rebase 使用方式。你在功能分支上寫了一半,這時候 main 又進了幾個 PR,你不想等到最後一次爆一堆衝突,就可以先把最新 main 的變更搬進自己的分支。
先切到你的功能分支:
git switch feature/login-page
先抓遠端最新狀態:
git fetch origin
再把目前分支 rebase 到遠端最新 main 上:
git rebase origin/main
拍拍君很推薦用 origin/main,而不是只寫 main。理由很單純:origin/main 比較明確,它代表你剛剛 fetch 下來的遠端最新狀態,不容易誤用到本機已經落後的 main。
4.1 一個簡單小例子 #
我們快速搭一個示範 repo:
mkdir rebase-demo
cd rebase-demo
git init
echo '# Rebase Demo' > README.md
git add README.md
git commit -m 'chore: init project'
切出功能分支並做第一個提交:
git switch -c feature/login-page
echo 'const login = true;' > app.js
git add app.js
git commit -m 'feat: add login page skeleton'
假設這時候主分支又多了一個提交:
git switch main
echo 'MIT' > LICENSE
git add LICENSE
git commit -m 'docs: add license file'
現在回到功能分支並 rebase:
git switch feature/login-page
git rebase main
這樣一來,你後續開發就是站在最新的 main 上進行,而不是拖著一個過期基底一路做下去。這個習慣能讓你提早解決衝突,避免最後要開 PR 時一次爆炸。
4.2 rebase 完之後怎麼 push? #
如果這條分支之前還沒 push 過,正常推就好:
git push -u origin feature/login-page
如果你已經 push 過舊的 commit,現在 rebase 之後 commit ID 改了,通常就要這樣推:
git push --force-with-lease
關鍵是 --force-with-lease,不是裸奔的 --force。前者會先確認遠端沒有別人新的提交,比較不會一腳把隊友的東西踩爆。只要你是在整理自己的功能分支,這幾乎就是重寫歷史後的標準配備。
五. 互動式 rebase:把 commit 歷史整理成人話 #
如果說前一節是在同步底座,那 interactive rebase 就是在打掃你自己的房間。多數人開發時的 commit 都不會一次到位,這很正常;問題是如果最後歷史長成下面這樣,reviewer 真的會很想把視窗關掉:
wipfixfix againoopstypofinal final這種時候,git rebase -i就非常好用。
git rebase -i HEAD~5
這代表「把最近 5 個 commit 拿來互動式整理」。Git 會打開編輯器,讓你決定每個 commit 要保留、改名、合併,還是直接丟掉。
5.1 你會看到什麼 #
通常編輯器會長這樣:
pick a1b2c3d feat: add login form
pick b2c3d4e fix: correct placeholder
pick c3d4e5f refactor: extract validation helper
pick d4e5f6a fix: typo in button label
pick e5f6a7b test: add login form tests
最左邊那個動作可以改。最常用的幾個是:
pick:保留 commitreword:保留 commit,但改 commit messageedit:停下來修改 commit 內容squash:跟前一個 commit 合併,並整合訊息fixup:跟前一個 commit 合併,但捨棄自己的訊息drop:直接刪掉這個 commit 拍拍君實務上最常用的其實就是reword、fixup、squash、edit這四個。先把這四個練熟,日常 90% 場景就夠用了。
5.2 什麼時候用 squash?什麼時候用 fixup? #
如果某個 commit 本身有意義,只是想跟前一個合併,並保留訊息一起整理,那就用 squash。如果它只是補 typo、修 import、補一行 lint,訊息沒有保存價值,通常就用 fixup。
例如你可以把原本這樣:
pick a1b2c3d feat: add login form
pick b2c3d4e fix: correct placeholder
pick c3d4e5f refactor: extract validation helper
pick d4e5f6a fix: typo in button label
pick e5f6a7b test: add login form tests
整理成這樣:
pick a1b2c3d feat: add login form
fixup b2c3d4e fix: correct placeholder
pick c3d4e5f refactor: extract validation helper
fixup d4e5f6a fix: typo in button label
pick e5f6a7b test: add login form tests
整理完之後,你的 commit 歷史就會更像「一個功能、一個重構、一個測試」,而不是「工程師一路邊寫邊碎念的內心獨白」。
5.3 reword:內容沒問題,訊息太醜 #
如果內容是好的,只是 commit message 寫得很隨便,可以改成 reword:
reword a1b2c3d feat: add login form
pick b2c3d4e test: add validation tests
存檔後 Git 會停下來讓你改訊息。這很適合把 update、fix stuff、temp 這種沒資訊量的 commit message 改成真正看得懂的版本。
5.4 edit:內容也想一起改 #
如果不只是想改訊息,而是想回頭修改 commit 裡的實際內容,就用 edit。例如:
edit a1b2c3d feat: add login form
pick b2c3d4e test: add validation tests
當 rebase 停下來後,你可以修改檔案,再把改動補進目前這個 commit:
git add .
git commit --amend
git rebase --continue
這招特別適合在你發現某個 commit 少放了一個檔案、混進了一個不相干變更,或者切分不夠漂亮時使用。它不是拿來做大改版,而是讓歷史更準確。
六. fixup! + autosquash:超順手的日常組合
#
很多人知道 interactive rebase 很強,但覺得每次都要手動重排很麻煩。其實 Git 已經幫你準備好一套更流暢的工作法:fixup! 搭配 autosquash。
如果你在開發途中就知道「這個 commit 只是用來補前面那個 commit 的小尾巴」,你可以直接這樣做:
git commit --fixup HEAD~1
或者指定某個 commit:
git commit --fixup a1b2c3d
Git 會自動產生像這樣的訊息:
fixup! feat: add login form
之後你只要執行:
git rebase -i --autosquash HEAD~5
Git 就會把這些 fixup! commit 自動移到正確位置,還會預先標成 fixup。這在修 review comment、補一點 lint 或改小 typo 時超級方便,因為你不用等到最後才逐個手排。
七. rebase 遇到衝突怎麼處理?其實流程很固定 #
大家真正怕的不是 rebase,而是 rebase 衝突。但本質上,rebase 衝突跟 merge 衝突是同一件事:不同修改碰到同一塊內容,所以需要你決定最後保留什麼。差別只在於 rebase 是在「重新播放 commit」的過程中發生。 假設你執行:
git rebase origin/main
如果 Git 停住並告訴你有衝突,標準流程其實只有四步:
- 打開衝突檔案
- 手動整理成正確內容
git add <file>git rebase --continue例如:
vim app.js
git add app.js
git rebase --continue
如果下一個 commit 又衝突,就再做一次。rebase 的衝突不是一次解全部,而是「每 replay 一個 commit,必要時解一次」。
7.1 看懂衝突標記 #
你通常會看到這種東西:
<<<<<<< HEAD
const timeout = 30;
=======
const timeout = 60;
>>>>>>> feat/retry-logic
這不是 Git 在發怒,只是告訴你:兩邊都改了同一段。你要做的是判斷最後應該留下哪個版本,或者兩邊其實都要合併。假設最後決定用 60,那就把檔案改成:
const timeout = 60;
然後:
git add app.js
git rebase --continue
7.2 不想繼續時可以 abort #
如果你解到一半發現今天不適合碰 Git,或者衝突多到懷疑人生,不用硬撐。你可以直接:
git rebase --abort
這會把你帶回 rebase 開始前的狀態,等你腦袋清楚一點再來。拍拍君很推薦把 --abort 當成正常工具,而不是失敗象徵。知道何時退一步,通常比硬解到更亂要聰明得多。
7.3 --skip 要非常小心
#
Git 也提供:
git rebase --skip
它的意思是「這個 commit 我不要了,跳過」。這在少數情況下真的有用,例如那個 commit 的內容已經被別的提交包含了。但如果你不知道自己在跳過什麼,就先不要用。因為 skip 的代價不是重試,而是直接放棄那個 commit。
7.4 卡住時先看 status #
真的卡住時,拍拍君最常做的第一件事不是亂試,而是:
git status
git status 通常會直接告訴你現在正在 replay 哪個 commit、哪些檔案有衝突、下一步應該 continue 還是 abort。如果你願意多看一眼 status,很多 panic 其實都可以省掉。
八. 什麼時候不要 rebase?這題比「怎麼用」更重要 #
rebase 很好用,但不是每個地方都該用。最重要的一條鐵則就是:不要隨便重寫別人也在使用的公開歷史。
例如下面幾種分支就要特別小心:
- 團隊共用的
develop - 大家都會直接拉的長期分支
- 已經有人從你的 branch 再切出其他 branch 的情況
因為 rebase 會改掉 commit ID。你一改,別人的基底就跟你不一樣了,接著就可能出現重複 commit、奇怪衝突,甚至整隊一起浪費時間在「為什麼 Git 長這樣」的哲學問題上。
一個安全的自我檢查方式是問自己兩句:這些 commit 幾乎是不是只有我自己在用?改寫之後會不會讓別人的 branch 基底壞掉?如果答案分別是 yes 和 no,那通常就很適合 rebase。反過來,如果這條歷史已經是多人共享的事實來源,優先考慮
merge通常更穩。
九. push --force-with-lease 不是原罪,但要用對地方
#
很多人一看到 force push 就全身發毛,這很合理,因為它確實能造成破壞。但真正危險的不是 force 這個動作本身,而是在不理解影響範圍時隨便 force。 如果你只是整理自己的 PR branch,而團隊也接受在開 PR 前用 rebase 清理歷史,那麼:
git push --force-with-lease
其實是很合理、很常見、也很專業的做法。它表示你知道自己重寫了歷史,現在要把新的 commit 圖更新到遠端,同時也先確認遠端沒有別人偷偷加東西。 但如果你要推的是共享主分支,或是別人正在一起合作的 branch,那就最好停一下。這不是「我會不會用」的問題,而是「我有沒有權利改這段公共歷史」的問題。
十. 拍拍君自己常用的一套 rebase 工作流 #
如果你不想一開始就記住全部變化,先記住這套就很夠用。
10.1 開發途中同步主分支 #
git switch feature/something
git fetch origin
git rebase origin/main
10.2 開 PR 前整理 commit 歷史 #
git rebase -i origin/main
如果你只想整理最近幾個 commit,也可以:
git rebase -i HEAD~5
10.3 整理完後更新遠端分支 #
git push --force-with-lease
10.4 遇到衝突時的標準手勢 #
git status
# 修檔案
git add .
git rebase --continue
10.5 如果今天真的不適合碰 Git #
git rebase --abort
這套流程沒有什麼神祕招式,但非常實用。大部分日常工作都能被它涵蓋,而且越早養成這種節奏,PR 歷史就越不容易長成災難片。
十一. 常見誤區一次整理 #
11.1 誤區一:rebase 比 merge 高級 #
不是。rebase 比較像整理歷史的工具,merge 比較像保留真實分岔的工具。不同場景有不同優先級,沒有誰天生比較高級。
11.2 誤區二:rebase 會把內容吃掉 #
正常操作下不會。它主要改的是 commit 的基底與 ID。真正讓內容消失的,通常是誤用 drop、亂 skip,或是衝突解錯。
11.3 誤區三:有衝突就代表我做錯了 #
也不是。很多衝突只是因為你改的地方,別人剛好也改了;或者你正在把舊 commit replay 到新基底,所以 Git 需要你重新做一次人工判斷。
11.4 誤區四:用了 rebase 就一定要 force push #
不一定。只有在「舊 commit 已經 push 到遠端,而你後來又重寫了歷史」的情況下,才需要 --force-with-lease。如果 branch 還沒公開過,正常 push 就可以。
結語:rebase 的價值不是炫技,而是讓歷史更能被人讀懂 #
拍拍君很喜歡一句很務實的說法:好的 commit 歷史,本質上是一種溝通。它不只是給 Git 看,也是給未來的你、你的 reviewer、以及之後接手這段功能的人看。
git rebase 的價值,不在於讓你顯得像高手,而在於你可以更主動地把歷史整理成有邏輯、有層次、能快速理解的樣子。功能分支落後時,用它把基底補上;PR 太亂時,用它把 commit 收斂;遇到衝突時,按流程慢慢解。這些都不是炫技,而是很務實的工程習慣。
所以拍拍君最後給一個很簡短的練習路線:先把 git fetch + git rebase origin/main 練熟,再學 git rebase -i 整理 commit,最後養成 push --force-with-lease 和 git status 的好習慣。先把最常用的 20% 練起來,你的 Git 體感就會差很多。