核心觀念:git 幾乎不刪除東西

git reset --hardgit branch -D、甚至 git push --force——這些操作感覺很危險,但 git 在本機保留了幾乎所有的 commit 至少 30 天(gc.reflogExpire 設定)。

關鍵是找到 commit 的 hash,然後把它恢復出來。


reflog:所有 HEAD 移動的歷史

git reflog 顯示 HEAD 在什麼時間指向哪個 commit:

$ git reflog
abc1234 HEAD@{0}: reset: moving to HEAD~3 最近的操作
def5678 HEAD@{1}: commit: fix authentication
ghi9012 HEAD@{2}: commit: add user model
jkl3456 HEAD@{3}: checkout: moving from main to feature/auth

HEAD@{1} 就是你 reset --hard 之前的 commit。


情境 1:git reset --hard 後悔了

# 你做了
git reset --hard HEAD~3  # 刪掉了最近 3 個 commit
 
# 找回來
git reflog  # 找到你 reset 之前的 commit hash(e.g., def5678)
git reset --hard def5678  # 或 HEAD@{1}

情境 2:Branch 被 -D 刪掉了

# 你做了
git branch -D feature/payment  # 刪掉了整個 branch
 
# 找回來
git reflog | grep feature/payment  # 找到 branch 最後指向的 commit
# 或者
git reflog  # 找到那個 branch 最後一個 commit 的 hash
 
git checkout -b feature/payment <hash>  # 用那個 hash 重建 branch

情境 3:Detached HEAD 狀態

# 發生原因:直接 checkout 一個 commit hash
git checkout abc1234
# 現在 HEAD 不指向任何 branch,你的 commit 沒有 branch 記錄
 
# 如果你在 detached HEAD 做了 commit,先找到那個 commit
git reflog  # 找到那些 commit 的 hash
 
# 方案 1:建立新 branch 把那些 commit 接住
git checkout -b save-my-work
 
# 方案 2:把那些 commit cherry-pick 到目標 branch
git checkout main
git cherry-pick abc1234..def5678

情境 4:Force push 搞壞了遠端 branch

# 你做了
git push --force origin main  # 把遠端 main 的 history 搞壞了
 
# 如果有另一個人的本機還有舊的 commit:
# 讓他執行:
git reflog  # 找到遠端被覆蓋前的 commit
git push --force origin <hash>:main  # 把遠端推回去

如果沒有其他人有舊版本:

# 在你本機的 reflog 找你 force push 之前的 commit
git reflog  # e.g., abc1234
git push --force origin abc1234:main

情境 5:誤 commit 了不該有的檔案(比如 .env)

# 這個 commit 還沒 push
git reset HEAD~1         # 保留改動但 unstage
# 或
git reset --soft HEAD~1  # 保留 staged 狀態
 
# 把不該有的檔案加入 .gitignore
echo ".env" >> .gitignore
git add .gitignore
git commit -m "remove .env from tracking"

如果已經 push 了且 .env 裡有 secret:先撤銷 secret(因為任何人都可能已經看到),再清理 history。用 git filter-repo(官方推薦)或 BFG Repo Cleaner 從整個 history 移除檔案。


情境 6:stash 用完沒了

# git stash drop 之後後悔了
git stash list  # stash 還在 reflog 裡
 
# 找到 stash 的 commit hash
git fsck --no-reflogs | grep "dangling commit"
# 或
git reflog stash
 
git stash apply <hash>

預防大於救援

  • push --forcepush --force-with-lease 替代——這個指令只在遠端 branch 和你預期一樣時才允許 force push,防止覆蓋別人的 commit
  • 保護 main branch:在 GitHub / GitLab 設定 protected branch,不允許直接 force push
  • 重要的操作前先 git reflog 記住當前 commit hash