首頁/版本管理

Git 實戰系列:Rebase

2025年04月09日 版本管理 Git Rebase SourceTree 版本控制 AI 協作

merge 出來的歷史和 rebase 出來的歷史

我們團隊有個習慣:功能分支開發到一半,每天早上第一件事是把 main merge 回自己的分支,避免落後太多、之後衝突太痛苦。立意是好的。

但這個習慣用了幾個月之後,佈版前看歷史紀錄變成一場災難。git log --graph 拉出來的圖,混了一堆「Merge branch ‘main’ into feature/xxx」的節點,跟真正的功能改動纏在一起。等到這個分支真的要併回 main,佈版的人打開 log,根本分不出哪些 commit 是這次真正要上線的東西,哪些只是每天同步進度留下的痕跡。

問題不在「該不該常跟 main 同步」,這件事沒有錯。問題出在同步的手法:用 git merge 同步,每次都會留一個 merge commit,天天做就天天多一個節點,把分支的歷史攪成一團毛線球。換成 git rebase,同樣天天同步,但歷史線不會分岔。

merge 同步留下的痕跡

假設 feature/order-refund 從 main 拉出來,開發兩週,中間工程師每天做一次 git merge main

*   f3a91c2 Merge branch 'main' into feature/order-refund
|\
| * 8b2e441 main: 修正結帳頁的稅額計算
* | 7d1a003 退款流程加上審核步驟
* |   c92f118 Merge branch 'main' into feature/order-refund
|\ \
| |/
| * 4e6a9c0 main: 更新金流 SDK 到 3.2
* | 1f0b552 退款金額上限檢查
* |   a44de77 Merge branch 'main' into feature/order-refund
|\ \
| |/
| * 9c1d2f3 main: 修 Log 格式
* | 6b3a1e0 退款單建立 API

畫成圖會更清楚看出這三次同步做了什麼:

merge 同步留下的分支歷史:main 的三個 commit 跟三個 merge commit 混在功能 commit 之間

這是兩週、三次同步的結果。真正跟「退款功能」有關的只有三行:7d1a0031f0b5526b3a1e0。但 log 裡有一半是 merge commit,跟從 main 帶進來、跟這個分支毫無關係的東西。等這條分支併回 main,這些 merge commit 也會一起被帶進 main 的歷史——main 的紀錄變成「這個分支哪天跟自己同步過」的流水帳,而不是「main 到底改了什麼」。

佈版的人要寫 release note、要抓某個版本區間到底動了哪些檔案,看到這種圖基本上放棄用 log,只能改用 diff 硬比對,或是回頭問工程師「這次到底改了什麼」。

rebase 做的是同一件事,但不留痕跡

git merge main 是「把 main 的進度,跟我分支的進度,做一次合併,產生一個新的合併節點」。

git rebase main 做的事情完全不同:把我在這個分支上的 commit 一個一個摘下來,改成「假裝我是從 main 現在的位置才開始寫的」,重新接上去。分支的起點往前挪到 main 最新的位置,但我自己的 commit 內容和數量不變,只是接的位置變了。因為不是「合併兩條線」,就不會產生 merge commit。

同樣兩週三次同步,改用 rebase,最後的歷史線長這樣:

* 6b3a1e0 退款單建立 API
* 1f0b552 退款金額上限檢查
* 7d1a003 退款流程加上審核步驟
(此時分支已經接在 main 最新的 commit 之後)

三次同步動作完全沒有在歷史上留下任何節點,因為 rebase 不是「多一個 commit 記錄這次同步」,而是「把分支的起點搬過去」。等這個分支開發完、併回 main,main 上新增的就只有這三筆退款相關的 commit,一眼看得出這次上線動了什麼。

實際做法:多久 rebase 一次、怎麼處理衝突

把每天的 git merge main 換成:

git fetch origin
git rebase origin/main

fetch 只是把遠端最新進度抓下來,不動你目前的分支;rebase 才是真的把你的 commit 接到 origin/main 最新的位置。

遇到衝突的處理跟 merge 很像,只是要重複好幾次,因為是一個 commit 一個 commit 重放:

# rebase 卡住了,先看是哪個檔案衝突
git status

# 打開衝突檔案手動改好,通常是解決 <<<<<<< / ======= / >>>>>>> 那幾段
git add <解決完的檔案>

# 繼續處理下一個 commit
git rebase --continue

# 發現這次 rebase 整個接錯方向,隨時可以整包退回原狀
git rebase --abort

因為 rebase 會重寫 commit 的雜湊值,如果這個分支已經推上遠端、之前有人拉過,直接 git push 會被拒絕。這時候要用:

git push --force-with-lease

不要用 --force--force-with-lease 會先檢查遠端有沒有被別人動過,如果遠端在你 rebase 之後多了新的 commit(代表有人也在這條分支上推過東西),它會拒絕推送,逼你先確認狀況,不會盲目蓋掉別人的進度。

有一條紀律要守住:只 rebase 你自己在用、還沒有被別人依賴的分支。main、release 這種大家都在拉的共用分支,絕對不要 rebase——一旦歷史被重寫,所有已經拉過舊版本的人,本地紀錄會跟遠端對不起來,接下來的合併會亂成一團。判斷標準很簡單:分支只要還「只有你自己在改」,rebase 都安全;一旦變成多人共用同一條分支,就只能用 merge,或約定好大家同時 rebase。

在 SourceTree 裡怎麼操作

不想背指令的話,SourceTree 整套流程可以完全用滑鼠做完:

SourceTree 示意:在 main 上按右鍵選 Rebase current changes onto main,衝突時用 Continue Rebase 或 Abort Rebase

SourceTree 介面是英文的,操作步驟對照如下:

  1. 先切到 main,按上方的 Pull 把本地 main 更新到最新。

  2. 切回自己的功能分支,例如 feature/order-refund

  3. 在左側分支清單裡找到 main,在它上面按右鍵,選 Rebase current changes onto main。SourceTree 會逐一重放你的 commit。

  4. 中途出現衝突時,該檔案會標紅色驚嘆號。點開手動解決,解決完把檔案 Stage 起來,再按上方的 Continue Rebase。想放棄就按 Abort Rebase,會整個退回 rebase 前的狀態,不會留下半殘的紀錄。

  5. rebase 做完,因為分支的 commit 雜湊值變了,推送時 SourceTree 會提示遠端有分歧。這時候不要直接按預設的強制推送——先看 Push 對話框的進階選項裡有沒有 force-with-lease 的選項,有就優先勾那個;沒有的話,改用 SourceTree 內建的 Terminal 按鈕,手動打 git push --force-with-lease 比較保險,對應的道理跟前面 CLI 那段一樣:會先檢查遠端有沒有被別人動過,不會盲目蓋掉別人的進度。

整個操作跟 merge 唯一差在第 3、4 步按的按鈕不一樣,其他流程一致,不需要額外學新的東西。

請 AI 幫忙做 rebase 時,指令要講清楚

現在不少人會直接請 AI 助理(像 Claude Code)跑 rebase,這沒問題,但指令下得太模糊,AI 遇到衝突時可能會自己「猜」該留哪一邊,猜錯了不會馬上發現——因為 rebase 本來就會重寫歷史,錯誤不容易從 diff 一眼看出來。

比較穩妥的指令,會明確要求 AI 在關鍵決策點停下來讓你確認,而不是一路做到底:

請把目前分支 rebase 到最新的 origin/main。

規則:
1. 先執行 git fetch origin,再用 git status 和 git log 跟我確認
   目前分支落後 main 幾個 commit。
2. 開始 rebase 之後,如果遇到衝突,先列出所有衝突檔案和衝突內容,
   不要自動選邊或用其中一方蓋掉,等我看過並回覆怎麼處理再繼續。
3. 全部 commit 重放完後,先執行 git log --oneline 讓我看過最終的
   歷史,我確認沒問題才可以 push。
4. push 一律用 --force-with-lease,不要用 --force。如果
   --force-with-lease 被拒絕,代表遠端有其他人推送過,停下來跟我
   說,不要改用 --force 硬推。

重點是把「衝突怎麼解」「push 用哪個指令」這兩個最容易出事的地方寫死在指令裡,不要讓 AI 自由發揮。AI 遇到衝突通常會傾向選一個看起來合理的版本自動合掉,但「看起來合理」跟「業務邏輯正確」是兩回事,這種判斷該留給人。

main 的紀錄乾不乾淨,是為了下一個要看的人

Rebase 本身不會讓你少做事——該同步的還是要同步,該解的衝突一個都不會少。它改變的只是「這件事有沒有在歷史上留下痕跡」。

天天 merge,佈版的人打開 log 要先過濾一堆跟 main 同步的雜訊,才找得到真正的改動;天天 rebase,main 上新增的每一筆 commit 都是這個分支真正做的事。差別不大的操作習慣,換來的是幾個月後有人要抓某次上線到底動了什麼、要回溯某個 bug 是哪個 commit 引入的時候,log 能不能直接拿來用。