快轉到主要內容
  1. 教學文章/

Git reflog 救援實戰:找回 reset、rebase 後消失的 Commit

·9 分鐘· loading · loading · ·
Git Reflog Recovery Reset Rebase Commit Version-Control
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 12: 本文

featured

一. 前言: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

每一列可以拆成四個重點:

  1. 3b8a10f:那個時間點指向的 commit;
  2. HEAD@{0}:reflog selector,0 是最新一筆;
  3. resetcommit:造成 reference 移動的操作;
  4. 後面的文字: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 後被刪除。 所以有三個重要結論:

  1. 發現事故要盡快處理;
  2. 找到候選後立刻建立 branch 或 tag;
  3. 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 最讓人安心的地方,是大多數歷史重寫操作不會立刻抹除物件;最容易讓人受傷的地方,則是慌張時繼續輸入更多破壞性指令。 記住四個動作:

  1. 停止操作;
  2. 用 reflog 找候選;
  3. showdiff 驗證;
  4. 先建立 rescue branch,再整合回主線。

只要救援窗口還在,reset、rebase 或 amend 後看似消失的 commit,往往都還有機會找回來。下次看到乾乾淨淨卻不對勁的 git log,先深呼吸,拍拍君陪你翻 reflog。🧰

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 12: 本文

相關文章

Git cherry-pick 實戰:精準搬運 commit、修補 hotfix 與分支同步
·11 分鐘· loading · loading
Git Cherry-Pick Hotfix Commit Version-Control
Git rebase 完全攻略:整理 commit 歷史、互動式 rebase 與衝突處理
·11 分鐘· loading · loading
Git Rebase Interactive-Rebase Commit-History Version-Control
Git worktree 實戰:同時開多個分支、平行測試與安全清理完全攻略
·11 分鐘· loading · loading
Git Worktree Branch Workflow Version-Control
Git bisect 實戰:快速定位壞 commit 與除錯流程完全攻略
·9 分鐘· loading · loading
Git Bisect Debugging Regression Version-Control
Git stash 實戰:暫存工作現場、切換任務與 patch 管理完全攻略
·11 分鐘· loading · loading
Git Git-Stash Patch Workflow Version-Control
Git Branch 策略完全攻略:feature branch、main、hotfix 與協作流程
·11 分鐘· loading · loading
Git Branch Workflow Feature-Branch Hotfix