在我多年的工程生涯中,最讓初學者感到恐懼的 Git 功能莫過於歷史修改。但實際上,這些功能就像是程式碼的「時光機」,讓你能夠優雅地修正錯誤、整理提交歷史,甚至從看似無法挽回的災難中恢復。今天我們要深入探討 Git 最強大但也最需要謹慎使用的功能。
理解 Git 的時間概念
在開始修改歷史之前,我們需要理解 Git 如何看待時間和歷史:
HEAD~0 (HEAD) ← 現在
HEAD~1 ← 1 個提交前
HEAD~2 ← 2 個提交前
HEAD~3 ← 3 個提交前
想像 Git 的提交歷史就像一條時間軸,每個提交都是一個時間點。我們可以:
- 查看過去:檢視歷史提交
- 回到過去:重置到某個時間點
- 修改過去:改變歷史記錄
- 恢復未來:找回看似消失的提交
Git Reset:三種重置模式詳解
git reset
是修改歷史的核心指令,它有三種模式,理解它們的差異至關重要。
Soft Reset:溫和的時光倒流
git reset --soft HEAD~1
Soft reset 只移動 HEAD 指標,保留所有檔案變更在暫存區中。
實際範例:
# 目前狀態
git log --oneline -3
# a1b2c3d (HEAD -> main) 修復登入 bug
# e4f5g6h 新增使用者驗證
# h7i8j9k 實作登入功能
# 執行 soft reset
git reset --soft HEAD~1
# 檢查狀態
git status
# On branch main
# Changes to be committed:
# (use "git restore --staged <file>..." to unstage)
# modified: login.js
# 檢查歷史
git log --oneline -2
# e4f5g6h (HEAD -> main) 新增使用者驗證
# h7i8j9k 實作登入功能
使用場景:
- 合併多個小提交成一個
- 修改最後一次提交訊息
- 重新組織暫存區內容
Mixed Reset:平衡的選擇(預設模式)
git reset HEAD~1
# 等同於
git reset --mixed HEAD~1
Mixed reset 移動 HEAD 指標,並清空暫存區,但保留工作區的變更。
實際範例:
# 執行前的狀態
git status
# On branch main
# nothing to commit, working tree clean
# 執行 mixed reset
git reset HEAD~1
# 檢查狀態
git status
# On branch main
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# modified: login.js
使用場景:
- 撤銷提交但保留變更,重新整理暫存區
- 將大型提交拆分成多個小提交
- 修改提交內容
Hard Reset:徹底的重置
git reset --hard HEAD~1
⚠️ 警告:Hard reset 會完全丟棄所有變更,包括工作區和暫存區的內容。
實際範例:
# 執行前檢查未儲存的變更
git status
git diff
# 執行 hard reset(不可逆!)
git reset --hard HEAD~1
# 結果:回到 HEAD~1 的狀態,所有後續變更消失
git status
# On branch main
# nothing to commit, working tree clean
使用場景:
- 完全放棄某些提交和變更
- 回到乾淨的歷史狀態
- 緊急回滾到穩定版本
Reset 模式比較表
模式 | HEAD | 暫存區 | 工作區 | 使用場景 |
---|---|---|---|---|
--soft | ✓移動 | ✗保留 | ✗保留 | 重新提交、合併提交 |
--mixed | ✓移動 | ✓清空 | ✗保留 | 重新暫存、拆分提交 |
--hard | ✓移動 | ✓清空 | ✓清空 | 完全重置、緊急回滾 |
Git Revert:安全的撤銷
與 reset
不同,revert
不會修改歷史,而是建立新的提交來撤銷之前的變更。
基本 Revert 操作
# 撤銷最後一次提交
git revert HEAD
# 撤銷特定提交
git revert a1b2c3d
# 撤銷多個提交
git revert HEAD~3..HEAD
實際範例:
# 目前歷史
git log --oneline -3
# a1b2c3d (HEAD -> main) 錯誤的功能實作
# e4f5g6h 新增使用者驗證
# h7i8j9k 基礎功能
# 撤銷錯誤的提交
git revert a1b2c3d
# Git 會開啟編輯器讓你輸入提交訊息
# 預設訊息:Revert "錯誤的功能實作"
#
# This reverts commit a1b2c3d4e5f6789...
# 檢查結果
git log --oneline -4
# f9e8d7c (HEAD -> main) Revert "錯誤的功能實作"
# a1b2c3d 錯誤的功能實作
# e4f5g6h 新增使用者驗證
# h7i8j9k 基礎功能
撤銷合併提交
合併提交有多個父提交,需要指定要撤銷到哪個父提交:
# 查看合併提交的父提交
git show --format=fuller HEAD
# 撤銷合併提交(-m 1 表示撤銷到第一個父提交,通常是主分支)
git revert -m 1 HEAD
批次撤銷
# 撤銷一個範圍的提交,但不自動提交
git revert --no-commit HEAD~3..HEAD
# 這會將撤銷的變更放在暫存區,你可以:
# 1. 檢查變更內容
git diff --cached
# 2. 一次性提交所有撤銷
git commit -m "撤銷最近 3 個提交的變更"
互動式 Rebase:精細的歷史編輯
互動式 rebase 是最強大的歷史編輯工具,讓你可以重新組織、修改、合併或刪除提交。
啟動互動式 Rebase
# 編輯最近 5 個提交
git rebase -i HEAD~5
# 編輯從特定提交開始的歷史
git rebase -i e4f5g6h
# 編輯從分支開始的所有提交
git rebase -i main
Rebase 編輯器介面
當你執行互動式 rebase 時,Git 會開啟編輯器顯示:
pick a1b2c3d 實作使用者登入功能
pick e4f5g6h 修復登入驗證 bug
pick h7i8j9k 新增記住我功能
pick l1m2n3o 優化登入介面
pick p4q5r6s 新增登入錯誤處理
# Rebase instructions:
# p, pick <commit> = 保留提交
# r, reword <commit> = 保留提交但修改訊息
# e, edit <commit> = 保留提交但暫停以便修改
# s, squash <commit> = 合併到前一個提交
# f, fixup <commit> = 合併到前一個提交但丟棄訊息
# x, exec <command> = 執行指令
# b, break = 在此處暫停
# d, drop <commit> = 刪除提交
# l, label <label> = 標記目前 HEAD
# t, reset <label> = 重置 HEAD 到標記
實際的 Rebase 操作範例
1. 合併提交 (Squash)
# 將功能相關的小提交合併成一個大提交
pick a1b2c3d 實作使用者登入功能
squash e4f5g6h 修復登入驗證 bug
squash h7i8j9k 新增記住我功能
pick l1m2n3o 優化登入介面
pick p4q5r6s 新增登入錯誤處理
儲存後,Git 會讓你編輯合併後的提交訊息:
# 將此提交訊息設為:
實作完整的使用者登入功能
包含以下改善:
- 基本登入功能實作
- 修復驗證邏輯 bug
- 新增記住我選項
2. 修改提交訊息 (Reword)
pick a1b2c3d 實作使用者登入功能
reword e4f5g6h 修復登入驗證 bug
pick h7i8j9k 新增記住我功能
3. 編輯提交內容 (Edit)
pick a1b2c3d 實作使用者登入功能
edit e4f5g6h 修復登入驗證 bug
pick h7i8j9k 新增記住我功能
當 rebase 暫停在 edit 提交時:
# 進行修改
echo "額外的修改" >> auth.js
git add auth.js
# 修改目前提交
git commit --amend
# 或加入新的提交
git commit -m "額外的安全檢查"
# 繼續 rebase
git rebase --continue
4. 刪除提交 (Drop)
pick a1b2c3d 實作使用者登入功能
drop e4f5g6h 修復登入驗證 bug # 這個提交會被刪除
pick h7i8j9k 新增記住我功能
5. 重新排序提交
# 原始順序
pick a1b2c3d 實作基礎功能
pick e4f5g6h 新增樣式
pick h7i8j9k 新增測試
# 重新排序後
pick a1b2c3d 實作基礎功能
pick h7i8j9k 新增測試
pick e4f5g6h 新增樣式
Git Reflog:找回遺失的提交
Reflog 是 Git 的「黑盒子」,記錄了所有 HEAD 的移動歷史,包括已經被刪除的提交。
查看 Reflog
# 查看 reflog
git reflog
# 輸出範例:
# a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
# e4f5g6h HEAD@{1}: commit: 新增使用者驗證
# h7i8j9k HEAD@{2}: commit: 實作登入功能
# l1m2n3o HEAD@{3}: clone: from https://github.com/user/repo.git
# 查看特定分支的 reflog
git reflog show main
# 查看詳細的 reflog 資訊
git log -g --pretty=format:'%h %gd %gs %s'
使用 Reflog 恢復提交
假設你意外執行了 git reset --hard HEAD~3
並想要恢復:
# 1. 查看 reflog 找到要恢復的提交
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# e4f5g6h HEAD@{1}: commit: 重要的功能實作 ← 想要恢復這個
# h7i8j9k HEAD@{2}: commit: 新增測試
# l1m2n3o HEAD@{3}: commit: 修復 bug
# 2. 恢復到指定的 reflog 項目
git reset --hard HEAD@{1}
# 或者使用提交 hash
git reset --hard e4f5g6h
# 3. 確認恢復成功
git log --oneline -3
Reflog 的清理
Reflog 項目預設會保留 90 天:
# 手動清理 reflog(小心使用)
git reflog expire --expire=now --all
git gc --prune=now
# 設定 reflog 過期時間
git config gc.reflogExpire "30 days"
git config gc.reflogExpireUnreachable "7 days"
實際災難恢復案例
案例 1:意外的 Hard Reset
# 災難:意外重置丟失重要工作
git reset --hard HEAD~10 # 糟糕!10 個提交不見了
# 恢復步驟:
# 1. 不要驚慌,不要執行更多 Git 指令
# 2. 查看 reflog
git reflog
# 3. 找到重置前的位置
git reflog show | grep "before reset"
# 4. 恢復到重置前的狀態
git reset --hard HEAD@{1} # 或對應的 hash
# 5. 確認恢復
git log --oneline -5
案例 2:錯誤的 Rebase
# 災難:rebase 過程中搞砸了歷史
git rebase -i HEAD~5
# ... 在 rebase 過程中出錯 ...
git rebase --abort # 取消 rebase
# 或者如果已經完成但結果不對:
git reflog
git reset --hard HEAD@{1} # 回到 rebase 前的狀態
案例 3:刪除了重要分支
# 災難:意外刪除分支
git branch -D important-feature # 意外刪除
# 恢復步驟:
# 1. 查看 reflog 找到分支最後的提交
git reflog show important-feature # 如果還在 reflog 中
# 2. 或查看全域 reflog
git reflog --all | grep important-feature
# 3. 重新建立分支
git branch important-feature a1b2c3d # 使用找到的 hash
# 4. 確認恢復
git switch important-feature
git log --oneline -5
歷史修改的最佳實踐
1. 黃金法則:不要修改已推送的歷史
# ❌ 危險:修改已推送的提交
git reset --hard HEAD~3 # 如果這些提交已經推送到共享倉庫
git push --force # 會影響其他開發者
# ✅ 安全:使用 revert 撤銷已推送的變更
git revert HEAD~2..HEAD
git push # 其他開發者可以正常拉取
2. 在修改歷史前建立備份
# 建立備份分支
git branch backup-before-rebase
# 進行歷史修改
git rebase -i HEAD~5
# 如果出問題,可以恢復
git reset --hard backup-before-rebase
3. 使用適當的工具處理不同情況
# 情況 1:修改最後一次提交
git commit --amend
# 情況 2:撤銷已推送的提交
git revert HEAD
# 情況 3:整理私人分支的歷史
git rebase -i HEAD~5
# 情況 4:完全重置本地變更
git reset --hard origin/main
4. 理解何時使用哪種工具
需求 | 工具 | 原因 |
---|---|---|
修改最後提交訊息 | git commit --amend | 簡單直接 |
撤銷已推送的提交 | git revert | 不改變歷史 |
整理本地提交歷史 | git rebase -i | 可精細控制 |
回到某個版本 | git reset | 快速但要小心 |
找回遺失的提交 | git reflog | 最後的救命稻草 |
進階歷史操作技巧
1. 條件式歷史修改
# 只對包含特定文字的提交進行操作
git rebase -i --exec "if git show --format=%s -s HEAD | grep -q 'WIP'; then git commit --amend -m 'Work in progress'; fi" HEAD~10
2. 自動化提交訊息修正
# 使用 filter-branch 批量修改提交訊息
git filter-branch --msg-filter '
if [ "$GIT_COMMIT" = "a1b2c3d..." ]; then
echo "修正後的提交訊息"
else
cat
fi
' HEAD~10..HEAD
3. 基於時間的歷史操作
# 查看特定時間範圍的提交
git log --since="2024-01-01" --until="2024-01-31"
# 重置到特定時間點
git log --since="1 week ago" --oneline
git reset --hard <commit-hash>
4. 互動式修復衝突
# 在 rebase 過程中遇到衝突時
git status # 查看衝突檔案
# 編輯衝突檔案
git add . # 標記衝突已解決
git rebase --continue # 繼續 rebase
# 或者跳過有問題的提交
git rebase --skip
# 或者完全放棄 rebase
git rebase --abort
團隊環境中的歷史管理
1. 建立歷史修改政策
# 團隊 Git 歷史修改規範
## 允許的操作
- 修改尚未推送的本地提交
- 使用 revert 撤銷已推送的提交
- 在功能分支上進行 squash merge
## 禁止的操作
- 修改 main/develop 分支的歷史
- force push 到共享分支
- 刪除其他人正在使用的提交
## 緊急情況處理
- 立即通知團隊
- 建立事後檢討
- 更新流程防止再次發生
2. 使用保護機制
# 設定 pre-push hook 防止危險操作
# .git/hooks/pre-push
#!/bin/sh
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ]; then
echo "直接推送到 main 分支被禁止"
exit 1
fi
3. 建立恢復流程
# 團隊恢復流程腳本
#!/bin/bash
# recover-branch.sh
echo "恢復分支工具"
echo "1. 查看 reflog"
git reflog --all --oneline | head -20
echo "2. 請輸入要恢復的 commit hash:"
read commit_hash
echo "3. 請輸入新分支名稱:"
read branch_name
git branch "$branch_name" "$commit_hash"
echo "分支 $branch_name 已恢復到 $commit_hash"
歷史分析和診斷
1. 找出問題提交
# 使用 bisect 找出引入 bug 的提交
git bisect start
git bisect bad # 標記目前版本有 bug
git bisect good HEAD~10 # 標記 10 個提交前是好的
# Git 會自動切換到中間的提交,測試後標記
git bisect good # 或 git bisect bad
# 重複直到找到問題提交
git bisect reset # 結束 bisect
2. 分析歷史變更
# 查看檔案的修改歷史
git log --follow -- path/to/file.js
# 查看特定行的修改歷史
git blame -L 10,20 path/to/file.js
# 查看兩個版本之間的差異
git diff HEAD~5..HEAD~2 -- path/to/file.js
# 統計提交活動
git shortlog -sn --since="1 month ago"
3. 歷史品質評估
# 檢查提交訊息品質
git log --oneline | grep -E "^.{1,10} " | wc -l # 過短的訊息
git log --oneline | grep -E "WIP|TODO|temp" | wc -l # 臨時提交
# 檢查提交大小
git log --pretty=format:"%h %s" --shortstat | \
awk '/files? changed/ {files+=$1; ins+=$4; del+=$6}
END {print "avg files:", files/NR, "avg +/-:", (ins+del)/NR}'
常見錯誤和修復
錯誤 1:Reset 後推送失敗
# 問題:本地 reset 後無法推送
git push
# ! [rejected] main -> main (non-fast-forward)
# 解決方案(如果確定安全):
git push --force-with-lease
# 更安全的方案:
git pull --rebase origin main
git push
錯誤 2:Rebase 過程中迷失
# 問題:rebase 過程中不知道在哪裡
# 查看目前狀態
git status
git log --oneline -5
# 如果想要放棄
git rebase --abort
# 如果想要繼續但不知道下一步
git rebase --skip # 跳過當前提交
git rebase --continue # 繼續下一步
錯誤 3:找不回重要的變更
# 查看所有引用的歷史
git fsck --lost-found
# 查看未引用的物件
git fsck --unreachable
# 查看懸空的提交
git fsck --dangling
# 恢復懸空的提交
git show <dangling-commit-hash>
git branch recovery-branch <dangling-commit-hash>
小結
Git 的歷史修改功能強大但需要謹慎使用:
核心工具回顧:
- git reset:移動 HEAD 指標,三種模式適用不同場景
- git revert:安全地撤銷變更,不修改歷史
- git rebase -i:精細的歷史編輯,重組提交
- git reflog:記錄所有操作,災難恢復的最後希望
重要原則:
- 不要修改已推送的歷史(除非你知道後果)
- 在修改歷史前建立備份
- 理解每個工具的適用場景
- 團隊協作時要有明確的規範
- 遇到問題不要驚慌,reflog 是你的朋友
安全檢查清單:
# 修改歷史前的檢查
- [ ] 確認提交尚未推送到共享倉庫
- [ ] 建立備份分支
- [ ] 確認團隊成員沒有基於這些提交工作
- [ ] 了解修改的影響範圍
- [ ] 準備好回滾計劃
記住,Git 的歷史修改功能就像手術刀,在正確的手中是救命的工具,但需要足夠的知識和謹慎的態度。多在安全的環境中練習,建立你的肌肉記憶和直覺。
在下一篇文章中,我們將探討 Git 的內部機制,了解 Git 如何儲存和管理資料,這將幫助你更深入理解 Git 的行為和效能特性。
掌握歷史修改不是為了展示技術實力,而是為了讓你在面對複雜情況時能夠優雅地處理問題。投入時間學習和實踐這些技能,它們會在關鍵時刻救你一命。