快轉到主要內容
  1. 教學文章/

Git rebase 完全攻略:整理 commit 歷史、互動式 rebase 與衝突處理

·11 分鐘· loading · loading · ·
Git Rebase Interactive-Rebase Commit-History Version-Control
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 7: 本文

featured

一. 前言:你不是不會 commit,你只是還沒學會整理歷史
#

很多人第一次看到 git rebase 都有點戒慎恐懼:知道它很強,也知道它可能改寫歷史,於是平常只敢用 merge,一看到 rebase -i 就自動把自己歸類到「還沒那麼厲害」的那一側。 但其實 rebase 沒有想像中玄。它做的事情很單純:把你原本的 commit 暫時拿起來,換到另一個基底上重新套用。這個動作讓 branch 歷史更線性、PR 更好讀,也讓你可以在送 review 前把那些 wipfix typooops again 收拾乾淨。 如果你已經看過拍拍君前一篇 Git Branch 策略完全攻略,你大概知道 branch 應該怎麼切、怎麼協作。那今天這篇,就是在 branch 之內繼續深一層:把 commit 歷史整理成「人能讀懂」的樣子。 這篇會從最實用的四個面向來講:什麼是 rebase、什麼時候該用、互動式 rebase 怎麼整理歷史、遇到衝突時怎麼處理。拍拍君不走宗教戰爭路線,mergerebase 都是工具,重點是你知不知道自己在改什麼。 如果 Git 基礎還不熟,建議先補一下 Git 入門。但如果你已經會 addcommitpush,那這篇就很適合拿來把工作流再升級一點。

二. 安裝與基本設定:先把環境整理順一點
#

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 的差別到底在哪裡?
#

很多人並不是不會敲指令,而是沒有搞清楚 mergerebase 在歷史圖上到底做了什麼。這一點沒想通,後面遇到衝突時就很容易只剩恐懼。 先想像下面這個歷史:

A---B---C  main
     \
      D---E  feature/login

意思是:feature/login 這條分支是從 B 切出去的,之後 main 前進到 C,而功能分支上則有 DE 兩個提交。

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 會把 DE 暫時拿起來,重新接到 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 真的會很想把視窗關掉:

  • wip
  • fix
  • fix again
  • oops
  • typo
  • final 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:保留 commit
  • reword:保留 commit,但改 commit message
  • edit:停下來修改 commit 內容
  • squash:跟前一個 commit 合併,並整合訊息
  • fixup:跟前一個 commit 合併,但捨棄自己的訊息
  • drop:直接刪掉這個 commit 拍拍君實務上最常用的其實就是 rewordfixupsquashedit 這四個。先把這四個練熟,日常 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 會停下來讓你改訊息。這很適合把 updatefix stufftemp 這種沒資訊量的 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 停住並告訴你有衝突,標準流程其實只有四步:

  1. 打開衝突檔案
  2. 手動整理成正確內容
  3. git add <file>
  4. 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-leasegit status 的好習慣。先把最常用的 20% 練起來,你的 Git 體感就會差很多。

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 7: 本文

相關文章

Git Branch 策略完全攻略:feature branch、main、hotfix 與協作流程
·11 分鐘· loading · loading
Git Branch Workflow Feature-Branch Hotfix
GitHub Actions 進階:Matrix Build + 自動發佈 PyPI
·5 分鐘· loading · loading
Github-Actions Cicd Pypi Matrix-Build Python
Github Actions: 自動化你的工作流
·5 分鐘· loading · loading
版本控制 Github Git
GitHub 指令工具
·2 分鐘· loading · loading
版本控制 Git 指令模式 Github
版本控制軟體 Git - 安裝篇
·3 分鐘· loading · loading
版本控制 Git
Python difflib 實戰:文字差異比對、相似度比較與 patch 輸出完全攻略
·10 分鐘· loading · loading
Python Difflib Text-Processing Developer-Tools Cli