一. 前言:Commit 看似消失,通常只是沒人指著它 #
你剛做完 git reset --hard HEAD~3,才發現那三個 commit 還沒推到遠端。
或者 interactive rebase 整理得很開心,存檔後才發現其中一顆 commit 被自己 drop 掉了。
先別急著翻垃圾桶,也別立刻在慌亂中執行十條來路不明的 Git 指令。很多時候,內容沒有真的消失,只是 branch、tag 或 HEAD 不再指向它。
這時候該找的是 reflog:Git 在本機記錄 reference 如何移動的救援日誌。
這篇會用一個可重做的示範 repo,依序處理:
reset --hard後找回 commit;- rebase 後回到整理前;
commit --amend後找回舊版本;- branch 被刪除後重建;
- reflog 找不到時,再用
git fsck做最後搜索。
拍拍君先講結論:先停止製造新操作、讀 reflog、建立救援 branch,確認內容後再決定怎麼合回去。
二. Reflog 是什麼:它記錄「指標怎麼移動」 #
Git 的 commit 物件用雜湊值識別,例如:
8f16d2a Add export validation
branch 則只是指向某個 commit 的可移動名稱。當你 commit、reset、checkout、merge 或 rebase,Git 會更新 HEAD 或 branch reference。
reflog 記錄的正是這些更新:
git reflog
常見輸出長這樣:
3b8a10f (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
8f16d2a HEAD@{1}: commit: Add export validation
16acb87 HEAD@{2}: commit: Add CSV parser
3b8a10f HEAD@{3}: commit (initial): Start project
每一列可以拆成四個重點:
3b8a10f:那個時間點指向的 commit;HEAD@{0}:reflog selector,0是最新一筆;reset、commit:造成 reference 移動的操作;- 後面的文字:Git 留下的動作說明或 commit message。
HEAD@{1} 不是「上一顆 commit」的同義詞,而是「HEAD 上一次記錄的位置」。兩者有時相同,有時完全不同。
三. 先建立一個可以放心弄壞的 Repo #
不要第一次練救援就拿正式專案當教材。先建一個沙盒:
mkdir reflog-lab
cd reflog-lab
git init
git config user.name "拍拍君"
git config user.email "pypy@example.invalid"
接著做三顆 commit:
printf "base\n" > notes.txt
git add notes.txt
git commit -m "Start project"
printf "parser\n" >> notes.txt
git commit -am "Add CSV parser"
printf "validation\n" >> notes.txt
git commit -am "Add export validation"
先確認歷史:
git log --oneline --decorate --graph --all
你會看到類似:
* 8f16d2a (HEAD -> main) Add export validation
* 16acb87 Add CSV parser
* 3b8a10f Start project
短雜湊每台電腦都會不同,後面的範例請以自己的輸出為準。
四. 實戰一:救回 reset --hard 丟掉的 Commit
#
現在故意把 main 往回移兩顆:
git reset --hard HEAD~2
git log --oneline --decorate
一般的 git log 只從目前 reference 可到達的 commit 往回走,因此剛才兩顆 commit 看起來消失了。
先查 reflog:
git reflog --date=local
找到 reset 前的位置,通常會是 HEAD@{1}:
3b8a10f HEAD@{0}: reset: moving to HEAD~2
8f16d2a HEAD@{1}: commit: Add export validation
4.1 最安全的第一步:建立救援 Branch #
不要一看到雜湊就再做一次 reset --hard。先建立不會改動目前 branch 的救援錨點:
git switch -c rescue/reset-loss HEAD@{1}
確認內容:
git log --oneline --decorate -3
cat notes.txt
現在 rescue/reset-loss 已經指回原本的頂端 commit。就算後面又操作失誤,也多了一個清楚名稱可以保護它。
4.2 確認後,怎麼放回 main?
#
如果 main 只是錯誤地退後,而且還沒和別人共享,可以 fast-forward:
git switch main
git merge --ff-only rescue/reset-loss
如果 main 在事故後已經出現新的 commit,可以選擇一般 merge:
git merge rescue/reset-loss
或只挑需要的 commit:
git cherry-pick <commit-id>
是否重寫共享歷史是另一個決策。reflog 幫你找到資料,不會替你判斷團隊協作規則。
五. 實戰二:Rebase 後想回到整理前 #
rebase 會重寫 commit,產生新的雜湊。舊 commit 常常不在一般 log 裡,但 rebase 開始前的 branch 位置通常仍留在 reflog。 先看最近紀錄:
git reflog --date=iso -20
可能看到:
5c9012e HEAD@{0}: rebase (finish): returning to refs/heads/feature/report
5c9012e HEAD@{1}: rebase (pick): Add report command
7de831a HEAD@{2}: rebase (start): checkout main
20a54b9 HEAD@{3}: checkout: moving from main to feature/report
你要找的通常是 rebase (start) 之前,feature branch 原本指向的位置。不要只背 HEAD@{3},因為每次 rebase 的步驟數不同。
先檢查候選:
git show --stat HEAD@{3}
git log --oneline --graph HEAD@{3} -5
建立保護 branch:
git branch rescue/pre-rebase HEAD@{3}
接著比較整理前後:
git range-diff main...rescue/pre-rebase main...feature/report
range-diff 很適合確認 rebase 前後的 patch 是否等價;若某顆真的漏掉,再從 rescue branch 合併或 cherry-pick。
延伸閱讀可以先看拍拍君的 Git rebase 完全攻略,那篇講的是如何整理歷史;本篇專注於整理出錯後怎麼復原。
六. 實戰三:commit --amend 覆蓋了不該改的版本
#
git commit --amend 不是修改原 commit,而是建立一顆新的 commit,再讓 branch 指向它。
因此 amend 前的版本通常會出現在 reflog:
git reflog -5
91a642c HEAD@{0}: commit (amend): Fix release notes
607bf89 HEAD@{1}: commit: Add release notes
先比較兩個版本:
git diff HEAD@{1} HEAD
git show --stat HEAD@{1}
如果想完整回到 amend 前:
git branch rescue/pre-amend HEAD@{1}
如果只想拿回某個檔案,不必搬整顆 commit:
git restore --source=HEAD@{1} -- docs/release-notes.md
git diff
git add docs/release-notes.md
git commit -m "Restore omitted release note"
這比直接 reset 更精準,也比較不會一起改掉其他已確認的內容。
七. 實戰四:Branch 刪掉了,怎麼重建? #
假設你誤刪一條還沒 merge 的 branch:
git branch -D feature/report
git reflog 預設主要顯示 HEAD 的紀錄。若你曾 checkout 過該 branch,通常可以從 HEAD reflog 找到:
git reflog --all --date=local
用訊息縮小範圍:
git reflog --all --grep-reflog='feature/report'
找到 branch 最後的 commit 後重建:
git branch feature/report <commit-id>
git switch feature/report
git log --oneline --decorate -5
注意:刪除 branch 時,那條 branch 自己的 reflog 也可能被移除。這也是為什麼要查 HEAD、--all,並且在事故後盡快處理。
八. 不只 HEAD:查指定 Branch 的 Reflog #
下面三條指令的範圍不同:
git reflog
git reflog show main
git reflog --all
git reflog:通常等同查看HEAD的 reflog;git reflog show main:只看refs/heads/main如何移動;git reflog --all:查看所有有 reflog 的 reference。
你也可以直接使用時間 selector:
git show 'main@{yesterday}'
git show 'HEAD@{2026-08-20 15:00}'
shell 可能會特殊處理解譯大括號,所以拍拍君習慣替時間 selector 加引號。 時間查詢依 reflog 的本機紀錄解析,不適合當成跨機器一致的 release identifier。正式版本仍應使用 tag 或完整 commit ID。
九. 讓輸出更好讀:日期、Diff 與 Graph #
reflog 很長時,不要靠肉眼硬掃全部:
git reflog --date=iso -30
git reflog --date=relative -15
git reflog --oneline --all
找到幾個候選後,用 show 驗證:
git show --stat <candidate>
git show --name-status <candidate>
git diff <candidate>^ <candidate>
若想理解候選和目前 branch 的關係:
git log --graph --oneline --decorate --all
git merge-base HEAD <candidate>
判斷標準不是「這顆看起來最舊」或「HEAD@{2} 好像差不多」,而是 commit message、作者時間、檔案差異與 parent 關係都符合預期。
十. ORIG_HEAD:方便,但不是萬靈丹
#
部分高風險操作會把操作前的位置寫到 ORIG_HEAD。例如某些 merge、reset 或 rebase 情境後,可以先檢查:
git show --stat ORIG_HEAD
如果正好是你要的位置,可以建立 branch:
git branch rescue/orig-head ORIG_HEAD
但 ORIG_HEAD 只有一個位置,後續操作可能覆蓋它。reflog 是一串有順序的紀錄,通常更適合完整調查。
拍拍君的習慣是:先看 ORIG_HEAD 能不能快速命中,再用 reflog 驗證,不把希望全部押在單一特殊 reference 上。
十一. Reflog 為什麼不是永久備份? #
reflog 是本機、會過期、可清理的操作紀錄。它不是雲端備份,也不會自動跟著 git push 上傳。
先查看目前過期設定:
git config --get gc.reflogExpire
git config --get gc.reflogExpireUnreachable
若沒有輸出,代表使用 Git 預設或更高層級設定。一般概念是:可到達與不可到達的 reflog entry 有不同保存期限,之後可能被 expire;沒有 reference 保護的物件,也可能在 garbage collection 後被刪除。 所以有三個重要結論:
- 發現事故要盡快處理;
- 找到候選後立刻建立 branch 或 tag;
- reflog 不能取代 remote、backup 或團隊共享流程。
不要為了「測試救援」隨便執行 aggressive GC。那是在主動縮短自己的救援窗口。
十二. Reflog 找不到:用 git fsck 搜尋 Dangling Commit
#
若 branch reflog 已刪、HEAD 從沒走過那顆 commit,或紀錄已過期,可以再試物件層級的檢查:
git fsck --full --no-reflogs --unreachable
輸出可能包含:
unreachable commit a18c940f0b8...
unreachable blob 83fe41c2f4d...
逐一檢查 commit:
git show --stat a18c940f0b8
git log -1 --format=fuller a18c940f0b8
確認命中後,立即建立 reference:
git branch rescue/from-fsck a18c940f0b8
git fsck 的結果通常缺少 reflog 那種「是哪次操作造成」的脈絡,因此它是第二線工具,不是日常第一選擇。
如果物件已被 garbage collection 真正移除,Git 本機就無法憑空重建內容。此時只能找 remote、同事 clone、CI artifact、備份或編輯器 local history。
十三. 遠端有沒有 Reflog?別搞混本機與 Server #
你在自己的 clone 裡看到的 reflog,只記錄這個 repository 本機發生的 reference 更新。
git reflog show origin/main
這顯示的是本機 remote-tracking branch origin/main 的移動,不是 GitHub 或 GitLab 伺服器替你公開保存的完整操作歷史。
如果遠端 branch 被 force push,可能的救援來源包括:
- 仍保有舊
origin/main紀錄的同事 clone; - Pull Request 頁面的 commit 或 event;
- CI checkout 過的 commit 與 artifact;
- 平台支援人員或 repository backup。
因此重要 branch 仍應設定保護規則,避免把 reflog 當成 force push 的安全網。
十四. 一套安全、可重複的救援 SOP #
真正出事時,照下面順序做:
14.1 停止增加變數 #
先別再 rebase、reset、expire 或 GC。若工作目錄還有未提交修改,先用 git status 盤點,不要急著執行會覆寫檔案的指令。
14.2 保存現況 #
git branch rescue/current-state HEAD
若目前 detached HEAD,一樣可以建立 branch 把它命名。
14.3 查 Reflog #
git reflog --all --date=iso
依事故時間、動作類型與 commit message 找出候選。
14.4 驗證,不要猜 #
git show --stat <candidate>
git diff HEAD...<candidate>
三點範圍 diff 的語意要先理解;若只是想直接比較兩個 snapshot,也可以使用:
git diff HEAD <candidate>
14.5 建立救援錨點 #
git branch rescue/2026-08-21 <candidate>
branch 建好後,那顆 commit 就重新變成可到達物件。
14.6 選擇整合方式 #
- 整條 branch 都要:merge 或在確認安全時 reset;
- 只要幾顆 commit:cherry-pick;
- 只要一個檔案:
git restore --source; - 只想先閱讀:保持 rescue branch,暫時不要動主線。
14.7 驗證與備份 #
執行測試、檢查 log,再把必要的 rescue branch 推到私人遠端或建立備份。不要把充滿敏感或半成品內容的 branch 不加判斷地推到公開 repository。
十五. 常見錯誤:救援時最怕二次傷害 #
15.1 直接 reset --hard 到第一個看見的候選
#
這會再次改動 branch 和工作目錄。先 git show,再建立 rescue branch,風險小很多。
15.2 把 HEAD@{1} 當固定答案
#
只要你在事故後多做 checkout、commit 或 reset,reflog 序號就會改變。永遠閱讀動作文字與 commit 內容。
15.3 忘記 Reflog 只在本機 #
換一台電腦 clone,不會自動帶回原機 reflog。事故發生在哪個 clone,優先去哪個 clone 查。
15.4 用 git log --all 就認為搜尋完整
#
--all 只從現有 reference 出發。沒有 branch 或 tag 指向的 commit 仍可能不顯示,這時 reflog 或 fsck 才有用。
15.5 太早執行清理 #
git reflog expire 和 aggressive git gc 都可能減少可救援資訊。除非你明確在做維護,而且確認不需要復原,否則事故期間別碰。
15.6 救回內容就立刻 force push #
本機復原成功不代表遠端歷史應該被覆蓋。先確認 branch protection、PR 狀態與團隊共識,再處理共享 reference。
十六. Reflog、Stash、Bisect 到底各自救什麼? #
| 工具 | 主要用途 | 典型問題 |
|---|---|---|
reflog |
追蹤 reference 移動 | reset、rebase、amend、刪 branch 後找舊 commit |
stash |
暫存未提交的工作目錄變更 | 臨時切任務,不想先 commit |
bisect |
二分搜尋 regression 起點 | 不知道哪顆 commit 開始壞掉 |
fsck |
檢查物件並找 unreachable 內容 | reflog 已無線索時做最後搜索 |
如果丟掉的是尚未 commit 的檔案修改,reflog 通常救不了,因為 Git 根本還沒有建立對應 commit 物件。 想深入未提交工作現場,可以看 Git stash 實戰;想定位哪顆 commit 引入 bug,則看 Git bisect 實戰。
十七. 指令速查:先讀、再保護、最後整合 #
# 看 HEAD 最近移動
git reflog --date=iso -20
# 看所有 reference 的 reflog
git reflog --all --date=local
# 看指定 branch
git reflog show main
# 驗證候選
git show --stat HEAD@{3}
# 建立救援 branch
git branch rescue/pre-reset HEAD@{3}
# 比較目前內容與候選 snapshot
git diff HEAD rescue/pre-reset
# 恢復單一檔案
git restore --source=rescue/pre-reset -- path/to/file
# reflog 無結果時搜索 unreachable objects
git fsck --full --no-reflogs --unreachable
把這段留在團隊 runbook 裡,真的出事時會比搜尋「git undo everything」可靠得多。
結語:Reflog 不是時光機,但很接近 #
Git 最讓人安心的地方,是大多數歷史重寫操作不會立刻抹除物件;最容易讓人受傷的地方,則是慌張時繼續輸入更多破壞性指令。 記住四個動作:
- 停止操作;
- 用 reflog 找候選;
- 用
show、diff驗證; - 先建立 rescue branch,再整合回主線。
只要救援窗口還在,reset、rebase 或 amend 後看似消失的 commit,往往都還有機會找回來。下次看到乾乾淨淨卻不對勁的 git log,先深呼吸,拍拍君陪你翻 reflog。🧰