M7 改寫歷史:rebase 與 cherry-pick
- 解釋 rebase 是「複製 commit 再搬標籤」,並預測 rebase 之後的 commit 圖;
- 比較 merge 和 rebase 產生的歷史,按情況選擇;
- 用互動式 rebase 整理 commit(squash、fixup、reword、drop);
- 用 cherry-pick 複製單一 commit;理解 squash merge 之後分支為甚麼「看起來沒合併」;
- 遵守「不改寫已分享的歷史」,並說出違反時會發生甚麼。
1「改寫」其實是複製 Rewriting means copying
M3 說過:commit 的編號由內容計算,內容包括 parent 的編號。所以只要 parent 換了,即使檔案改動完全一樣,也必然是一個新的 commit、新的編號。M6 的 --amend 已經是這樣:它沒有修改舊 commit,而是建立一個新的,再把分支搬過去。
本單元的所有命令都依循同一個模式:
- 把某些 commit 的「改動」重新套用到另一個起點上,產生新 commit;
- 把分支標籤搬到新 commit;
- 舊 commit 沒有被刪除,只是不再有分支指向它,仍可從 reflog 找回。
2rebase:把分支搬到新的起點 Rebase
你在 feature 上做了兩個 commit,期間 main 也前進了。git rebase main(在 feature 上執行)會:
- 找出 feature 上、但不在 main 歷史裏的 commit(F1、F2);
- 暫時把 HEAD 移到 main 的頂端;
- 按次序把 F1、F2 的改動重新套用,產生新 commit F1′、F2′(保留原作者和作者時間,提交時間是現在);
- 把 feature 搬到 F2′。
如果重放某個 commit 時,它的改動和 main 上的改動重疊,就會像合併一樣停下來等你解決衝突:解決 → git add → git rebase --continue;想放棄就 git rebase --abort,一切回到 rebase 之前。
3merge 與 rebase:同一段歷史,兩種形狀 Merge vs rebase
| merge | rebase | |
|---|---|---|
| 歷史形狀 | 保留分岔,多一個合併 commit | 一直線,像是按順序寫成的 |
| 舊 commit | 原封不動 | 被複製成新 commit,舊的只在 reflog |
| 真實性 | 如實記錄「曾經並行開發」 | 經過整理的版本,較易閱讀和追查 |
| 衝突 | 一次解決全部 | 可能每個 commit 各解決一次 |
| 能否用在已分享的分支 | 可以 | 不可以(見第 7 節) |
常見的折衷做法:自己的分支,合併前先 rebase 到最新的 main 上整理好;共用的分支,一律 merge。
4互動式 rebase:整理自己的 commit Interactive rebase
開 PR 之前,你的分支上可能有「加入摘要」「修正錯字」「wip」這樣零碎的 commit。git rebase -i main 會打開一份待辦清單(todo),每行一個 commit,由舊到新;你改寫每行開頭的動作,存檔後 Git 依次執行:
| 動作 | 效果 |
|---|---|
pick | 保留這個 commit |
reword | 保留,但改寫說明 |
squash | 併入上一個 commit,兩者的說明合在一起 |
fixup | 併入上一個 commit,丟棄自己的說明(適合「修正錯字」這類) |
drop | 刪除這個 commit(它的改動也會消失) |
調換行的次序,就能調換 commit 的次序。沙盒用表單代替文字編輯器,但動作和結果相同;squash 的結果經過真實 Git 驗證,編號一致。
5cherry-pick:只複製一個 commit Cherry-pick
git cherry-pick <commit> 把某一個 commit 的改動,套用到目前分支,產生一個新 commit(說明、作者、作者時間沿用原來的)。典型用法:main 上修好了一個錯誤,正式版的維護分支也需要這個修正,但不能把 main 上其他未完成的功能一起帶過去。
git switch release-1.0
git cherry-pick 9f3c2e1 # 只把那個修正複製過來
6squash merge:分支為甚麼「看起來沒合併」 Squash merge
GitHub 的「Squash and merge」按鈕,等於 git merge --squash feature 再 git commit:把整條分支的改動壓成一個普通的新 commit放到 main 上。main 的歷史乾淨了,但這個新 commit 和 feature 上原來的 commit 沒有任何 parent 關係。
結果是:從 Git 的角度,feature 上的 commit 從未被合併。git branch -d feature 會拒絕(not fully merged),git log main..feature 也會把它們全部列出來。
branch -d、git log main..feature、git cherry)都會誤報「未合併」。可靠的做法是比較內容:git diff main feature(或只比較該分支改過的檔案)沒有輸出,代表 feature 的內容已全部在 main 裏,這時才用 git branch -D 刪除。
7黃金規則:不改寫已分享的歷史 The golden rule
rebase、amend、reset、互動式 rebase 都會用新 commit 取代舊 commit。如果舊 commit 已經 push、別人也已經 pull 下來,就會出現兩份「同樣改動、不同編號」的歷史:
- 你 push 時會被拒絕(遠端的歷史不是你的祖先);
- 如果你用
git push --force強行覆蓋,別人下一次 pull 時,他們的工作會和你的新歷史分岔,甚至被覆蓋; - 受保護的 main 根本不允許強制推送(FAQ-002、M11)。
8沙盒:改寫歷史 Sandbox: rewriting history
9容易混淆的對照 Easily confused
| 命令 | 新增甚麼 | 搬哪張標籤 | 用在已分享的歷史 |
|---|---|---|---|
merge | 一個合併 commit(或不新增:fast-forward) | 目前分支往前 | 可以 |
rebase | 每個 commit 的複本 | 目前分支搬到複本頂端 | 不可以 |
cherry-pick | 一個 commit 的複本 | 目前分支往前 | 可以(只是新增) |
merge --squash | 一個普通 commit(要再 commit) | 目前分支往前 | 可以,但原分支會「看起來未合併」 |
reset(M6) | 不新增 | 目前分支搬到任意 commit | 不可以 |
revert(M6) | 一個抵銷用的 commit | 目前分支往前 | 可以 |
- 「rebase 會移動 commit。」嚴格來說不會:它建立新的 commit,舊的仍在,只是沒有分支指向。
- 「
git rebase main會改動 main。」不會。被搬動的是你所在的分支;main 只是新的起點。 - 「rebase 比 merge 好(或相反)。」兩者各有用途:rebase 讓個人分支的歷史清楚,merge 讓共用分支的歷史真實可靠。
- 「squash merge 之後就可以放心
branch -d。」-d會拒絕,因為以 commit 判斷它從未合併;用內容比較確認後再-D。
10命令速查 Cheat sheet
| 命令 | 作用 |
|---|---|
git rebase main | 把目前分支的 commit 搬到 main 頂端重放 |
git rebase -i main | 整理目前分支上不在 main 裏的 commit |
git rebase -i HEAD~3 | 整理最近三個 commit |
git rebase --continue / --skip / --abort | 解決衝突後繼續/略過這個 commit/放棄並回到原狀 |
git cherry-pick 9f3c2e1 | 把那一個 commit 的改動複製到目前分支 |
git merge --squash feature | 把 feature 的全部改動放進暫存區,再自己 git commit |
git diff main feature | 比較兩者的內容(沒有輸出=內容相同) |
git push --force-with-lease | 改寫自己個人分支後推送;若遠端有別人的新 commit 會拒絕(M9) |
11小測 Quiz
本單元重點 Summary
- 「改寫歷史」=重新套用改動、產生新 commit、搬標籤;舊 commit 只在 reflog。
- rebase 讓分支歷史變成一直線;merge 保留真實的分岔。
- 互動式 rebase 用 pick/reword/squash/fixup/drop 整理自己的 commit。
- cherry-pick 複製單一 commit;squash merge 產生沒有 parent 關係的新 commit,刪分支前要比較內容。
- 黃金規則:只改寫只有你用過的 commit;已分享的用 merge 和 revert。
延伸閱讀
- Pro Git 3.6 Rebasing(含「The Perils of Rebasing」)
- Pro Git 7.6 Rewriting History
- git rebase 官方文件、git cherry-pick 官方文件
- Learn Git Branching 的「Moving Work Around」關卡,可以在這裏練習 rebase 與 cherry-pick
- 有問題?記下來,它很可能也是其他同學的問題 → 學生問題庫