Merge 的結果
保留完整的分支歷史,建立一個 merge commit。
當你已經熟悉基本的 commit、branch、merge 後,這一章將帶你掌握四個進階工具:rebase、revert、reset 與 stash。理解它們的差異與適用時機,是從 Git 初學者進階到熟練使用者的關鍵。
Rebase 的核心作用是:把一個分支的 commit「搬到」另一個分支的最新 commit 之後,讓歷史看起來像一條直線。
保留完整的分支歷史,建立一個 merge commit。
將 feature 的 commit 「接」在 main 的最新 commit 之後,形成線性歷史。
C3' 和 C4' 是重新建立的 commit(內容相同但 SHA-1 不同)。
# 確保在 feature 分支上
$ git checkout feature/login
# 將 feature 的 commit rebase 到 main 的最新點
$ git rebase main
# 如果有衝突,解決後繼續
$ git add <resolved-file>
$ git rebase --continue
# 如果想放棄 rebase
$ git rebase --abort
# rebase 完成後,切回 main 合併(此時會 fast-forward)
$ git checkout main
$ git merge feature/login
Rebase 會改寫 commit 的 SHA-1(因為 parent 改變了)。如果別人已經基於舊的 commit 工作,rebase 會造成歷史不一致的問題。
安全使用 rebase 的時機:
git pull --rebase 取代預設的 merge pull。用 git rebase -i 可以在 rebase 的過程中編輯、合併或重新排列 commit。
# 對最近 3 筆 commit 進行互動式 rebase
$ git rebase -i HEAD~3
# 編輯器會顯示:
pick a1b2c3d feat: 新增登入頁面
pick e4f5g6h fix: 修正登入驗證
pick i7j8k9l style: 調整登入頁樣式
# 可以修改 pick 為其他動作:
# pick = 保留 commit
# reword = 修改 commit message
# squash = 合併到前一個 commit
# drop = 刪除 commit
git revert 是「安全撤銷」的方式——它不會刪除歷史,而是建立一個新的 commit 來反轉指定 commit 的變更。
# 撤銷最近一筆 commit
$ git revert HEAD
# 撤銷指定的 commit
$ git revert a1b2c3d
# 撤銷但不自動 commit(先看看結果)
$ git revert --no-commit a1b2c3d
git reset 會把分支指標移動到指定的 commit,效果是「讓歷史倒退」。根據模式不同,對工作目錄和暫存區的影響也不同。
| 模式 | 分支指標 | 暫存區 (Index) | 工作目錄 | 適用場景 |
|---|---|---|---|---|
--soft |
✅ 移動 | ❌ 不變 | ❌ 不變 | 想重新寫 commit message 或合併 commit |
--mixed(預設) |
✅ 移動 | ✅ 重設 | ❌ 不變 | 想重新選擇要 add 的檔案 |
--hard |
✅ 移動 | ✅ 重設 | ✅ 重設 | 想完全丟棄變更,回到乾淨狀態 |
# 回退一筆 commit,但保留所有變更在暫存區
$ git reset --soft HEAD~1
# 效果:
# - 最後一筆 commit 被移除
# - 那些變更仍在暫存區(可直接 commit)
# - 工作目錄不變
# 常見用途:重新寫 commit message
$ git commit -m "更好的提交訊息"
# 回退一筆 commit,變更從暫存區移回工作目錄
$ git reset HEAD~1
# 等同於 git reset --mixed HEAD~1
# 效果:
# - 最後一筆 commit 被移除
# - 變更存在工作目錄中(但未 staged)
# - 你可以重新選擇要 add 的檔案
$ git add file1.js # 只 add 部分檔案
$ git commit -m "只提交 file1 的變更"
# 回退一筆 commit,同時丟棄所有變更
$ git reset --hard HEAD~1
# 效果:
# - 最後一筆 commit 被移除
# - 暫存區重設
# - 工作目錄也被還原到 HEAD~1 的狀態
# - 所有未提交的變更都會遺失!
| revert | reset | |
|---|---|---|
| 是否改寫歷史 | 不改寫(新增一筆反轉 commit) | 改寫(移除 commit) |
| 已推送到遠端? | ✅ 安全使用 | ❌ 危險(需 force push) |
| 適用場景 | 撤銷已公開的 commit | 整理尚未公開的本地歷史 |
git stash 讓你把「做到一半」的變更暫時存起來,讓工作目錄回到乾淨狀態。等你處理完其他事情後,再把暫存的變更取回來。
正在開發 feature,老闆突然要你修一個 bug。先 stash,切到 hotfix 分支處理,再回來。
想 git pull 但工作目錄有未提交的變更會衝突。先 stash,pull 完再取回。
想在沒有自己修改的情況下測試某些功能。先 stash 清空,測完再恢復。
| 指令 | 用途 |
|---|---|
git stash | 暫存當前的工作目錄和暫存區變更 |
git stash push -m "描述" | 暫存並加上描述(推薦) |
git stash list | 列出所有暫存的工作 |
git stash pop | 取回最近一筆暫存並刪除它 |
git stash apply | 取回最近一筆暫存但不刪除 |
git stash drop | 刪除最近一筆暫存 |
git stash clear | 清空所有暫存 |
# 正在開發 feature...
$ echo "WIP: login form" >> login.html
$ git status
Changes not staged for commit:
modified: login.html
# 老闆說:先修 bug!
$ git stash push -m "login 頁面做到一半"
Saved working directory and index state On feature: login 頁面做到一半
# 工作目錄現在乾淨了
$ git status
nothing to commit, working tree clean
# 切去修 bug...
$ git checkout main
$ git checkout -b hotfix/fix-typo
# ...修完 bug,合併回 main...
# 回到 feature 分支
$ git checkout feature/login
# 取回暫存的工作
$ git stash pop
# login.html 的變更回來了!
# 可以暫存多次
$ git stash push -m "功能 A 的修改"
$ git stash push -m "功能 B 的修改"
# 列出所有暫存
$ git stash list
stash@{0}: On feature: 功能 B 的修改
stash@{1}: On feature: 功能 A 的修改
# 取回特定的暫存
$ git stash apply stash@{1} # 取回功能 A
# 刪除特定的暫存
$ git stash drop stash@{0} # 刪除功能 B
| 指令 | 作用 | 改寫歷史? | 安全性 | 適用場景 |
|---|---|---|---|---|
rebase |
將 commit 移到另一個基準點 | ✅ 是 | ⚠️ 注意 | 整理本地分支歷史 |
revert |
建立反轉 commit | ❌ 否 | ✅ 安全 | 撤銷已公開的 commit |
reset |
移動分支指標 | ✅ 是 | ⚠️ 注意 | 整理未公開的本地 commit |
stash |
暫時保存工作 | ❌ 否 | ✅ 安全 | 臨時切換分支或清理環境 |
# 建立 3 筆 commit
$ echo "a" > a.txt && git add . && git commit -m "add a"
$ echo "b" > b.txt && git add . && git commit -m "add b"
$ echo "c" > c.txt && git add . && git commit -m "add c"
# 測試 --soft
$ git reset --soft HEAD~1
$ git status # c.txt 在暫存區
# 恢復後測試 --mixed
$ git commit -m "add c"
$ git reset HEAD~1
$ git status # c.txt 在工作目錄但未 staged
# 恢復後測試 --hard
$ git add . && git commit -m "add c"
$ git reset --hard HEAD~1
$ ls # c.txt 消失了
# 做一些修改
$ echo "work in progress" >> README.md
# 暫存
$ git stash push -m "README 修改中"
# 確認乾淨
$ git status
# 查看 stash 清單
$ git stash list
# 取回
$ git stash pop
# 確認修改回來了
$ cat README.md
rebase 讓歷史線性化,但會改寫 commit SHA-1。revert 安全地撤銷 commit,適合已公開的變更。reset 三種模式:soft(保留暫存)、mixed(保留工作目錄)、hard(全部重設)。stash 暫存工作,臨時切換場景後再取回。git status 是你永遠的好朋友。