第八章:Git 歷史修改與時光倒流 – 掌握時間的魔法

在我多年的工程生涯中,最讓初學者感到恐懼的 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:記錄所有操作,災難恢復的最後希望

重要原則:

  1. 不要修改已推送的歷史(除非你知道後果)
  2. 在修改歷史前建立備份
  3. 理解每個工具的適用場景
  4. 團隊協作時要有明確的規範
  5. 遇到問題不要驚慌,reflog 是你的朋友

安全檢查清單:

# 修改歷史前的檢查
- [ ] 確認提交尚未推送到共享倉庫
- [ ] 建立備份分支
- [ ] 確認團隊成員沒有基於這些提交工作
- [ ] 了解修改的影響範圍
- [ ] 準備好回滾計劃

記住,Git 的歷史修改功能就像手術刀,在正確的手中是救命的工具,但需要足夠的知識和謹慎的態度。多在安全的環境中練習,建立你的肌肉記憶和直覺。

在下一篇文章中,我們將探討 Git 的內部機制,了解 Git 如何儲存和管理資料,這將幫助你更深入理解 Git 的行為和效能特性。


掌握歷史修改不是為了展示技術實力,而是為了讓你在面對複雜情況時能夠優雅地處理問題。投入時間學習和實踐這些技能,它們會在關鍵時刻救你一命。

404NOTE
404NOTE
文章: 40

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *