Git 版本控制 › 第 9 章

回溯與救援

2026-07-01  ·  reset · revert · restore · checkout · reflog · dangling commit

前八章假設一切順利——commit、分支、合併、推送都照計畫走。但真實世界裡你會 commit 錯訊息、 git add 到不該加的檔案、在錯的分支改了半天,甚至下錯 reset --hard 把工作毀了一半。 本章要拆解 Git 的「悔改機制」:reset 如何移動指標並牽動三大區域、 revert/restore/checkout 這幾個容易搞混的指令各自管什麼範圍, 以及最重要的——reflog 為什麼能讓你幾乎永遠不會真的「弄丟」一個 commit。 搞懂這章,你才能真正放心大膽地用 Git,而不是每次下指令前都心驚膽跳。

9.1reset 三模式(soft / mixed / hard)對三區域的不同影響

git reset 做的第一件事永遠一樣:把目前分支指標(連帶 HEAD)搬到指定的 commit。 差別只在於它「順便」對暫存區(index)工作目錄做多少事—— 這就是 --soft/--mixed/--hard 三個模式的全部差異。

模式分支指標 / HEAD暫存區(index)工作目錄典型情境
--soft移動不動(仍是舊 HEAD 的內容)不動「這幾個 commit 揉成一個重新 commit」,變更全部變成已暫存
--mixed(預設)移動回到新 HEAD(git reset 不加旗標時的行為)不動「先把這些 commit 收回來,但檔案內容我留著自己重新整理」
--hard移動回到新 HEAD回到新 HEAD(覆蓋工作目錄)「這些改動我完全不要了」——最危險的模式
--hard 會直接覆蓋工作目錄裡尚未 commit 的內容,而且不會問你。更麻煩的是,做完之後 git status 會顯示完全乾淨——不像 soft/mixed 還留著 M 提醒你「這裡有異動」, hard 之後現場看起來一切正常,新手很容易誤以為什麼事都沒發生(見下方工作目錄卡片的徽章)。 下手前養成 git status/git diff 看一眼的習慣;真的手滑了,9.3 節的 reflog 是最後一道防線。
情境:main 上有 A → B → C 三個 commit,檔案內容依序是 v1 / v2 / v3。點選下方按鈕模擬 git reset <mode> B
reset 前的 commit 鏈與指標位置 A 到 B 到 C 三個 commit,main 與 HEAD 目前指向 C A 1a2b B 3c4d C 5e6f main HEAD
🖥 工作目錄
v3
尚未執行 reset
📋 暫存區(index)
v3
尚未執行 reset
🗄 Repository(HEAD)
C · 5e6f
尚未執行 reset
點選上方任一按鈕,觀察三個區域各自如何反應。
無論哪個模式,C 都不再被任何分支指到——它變成所謂的 dangling commit(懸空提交)。 但物件本身沒被刪除,還完整躺在 .git/objects/ 裡。這正是 9.3 節 reflog 能救回它的原因。

9.2revert / restore / checkout 的差異與時機

reset 只是「悔改工具箱」的其中一件。Git 依「操作範圍」把其他工具分成兩層: commit 層級(整個歷史怎麼變)與檔案層級(單一檔案的內容怎麼變)。

commit 層級:reset vs. revert

git resetgit revert
動作移動分支指標到另一個 commit建立一個新 commit,內容是目標 commit 變更的「反向 diff」
對歷史的影響改寫——原 commit 從分支的歷史裡消失(除非用 reflog 找回)保留——原 commit 還在,只是多了一個抵銷它的新 commit
適合情境只存在本地、尚未推送的 commit已推送、別人可能已經拉走的 commit
與第 7 章的關係呼應 7.3「黃金法則」:公開歷史別用 reset/rebase 硬改,用 revert 補一筆才安全
reset 與 revert 對照圖 左側 reset --hard B 讓 C 變成 dangling;右側 revert C 保留 C 並新增反向的 commit D git reset --hard B A B main C dangling main 直接改指向 B C 不在歷史裡了(但物件還在) git revert C A B C D main D = 反向套用 C 的變更 A、B、C、D 全部都在歷史裡
圖 9-1reset 讓分支指標離開 C(C 變成 dangling);revert 保留完整歷史,另外新增一個抵銷 C 的 commit D

檔案層級:restore vs. checkout

Git 2.23(2019)之前,checkout 身兼二職——切分支和還原檔案共用同一個指令,語意常讓人搞混。 後來拆成兩個專職指令:switch(切分支,見第 5 章)與restore(還原檔案)。 checkout 舊語法仍然能用(相容性保留),但新手建議直接學 restore/switch

指令作用範圍效果對應舊語法
git restore <file>單一檔案用暫存區(或指定 commit)的內容覆蓋工作目錄,丟棄未暫存的修改git checkout -- <file>
git restore --staged <file>單一檔案把檔案從暫存區移回「已修改但未暫存」,不動 HEADgit reset HEAD -- <file>
git restore --source=<commit> <file>單一檔案從任意歷史 commit 取出該檔案內容,覆蓋工作目錄git checkout <commit> -- <file>
一句話分辨:reset 動指標(可能整批 commit)、restore 動單一檔案、revert 是用新 commit 表達「撤銷」、 switch 專心切分支。四個各司其職,不用再靠萬用的 checkout 猜語意。

9.3reflog 救回「以為弄丟」的 commit

git log 顯示的是「從目前 HEAD 出發可以走到的 commit」——一旦分支指標移開, 舊的 commit 就從 log 裡消失。但 Git 在本地還悄悄維護另一本帳:reflog, 它記錄的不是 commit 之間的關係,而是「HEAD 曾經指到過哪裡」的時間軸—— 每次 commit、checkout、switch、merge、rebase、reset,都會在 reflog 多留一行。

reflog 只存在你自己的本地版本庫,不會被 push/clone 帶走,而且會過期—— 兩個設定值管的是不同東西:還能從目前分支走到的項目預設留 90 天(gc.reflogExpire), 已經沒有任何分支指到的項目只留 30 天(gc.reflogExpireUnreachable)。 本節要救的 dangling commit 正是後者,所以搶救窗口是 30 天,不是 90 天;過期之後,真正無主的物件才會被 git gc 清掉。 它是「自救」工具,救不了別人電腦上的失誤。

接續 9.1 的情境:git reset --hard B 之後,你在 B 上繼續開發、多 commit 了一筆 E, 這時才發現 C 的內容其實不該丟。git reflog 的輸出(由新到舊)長這樣——點一列看說明:

dangling commit 與 reflog 救援示意圖 main 分支經過 reset 後從 A-B-C 變成 A-B-E,C 懸空但仍可由 reflog 找回 A 1a2b B 3c4d E 7f8g C ⚠ dangling · 5e6f main HEAD reset --hard B 之後 又在 B 上 commit 了 E
HEAD@{0}7f8gcommit: 新功能 E
HEAD@{1}3c4dreset: moving to B
HEAD@{2}5e6fcommit: C(不小心弄丟的那筆)
HEAD@{3}3c4dcommit: B
HEAD@{4}1a2bcommit: A (initial)
HEAD@{0}:目前位置——main 上最新的 commit E(新功能)。點其他列查看每一行 reflog 代表的意義。
重點在 HEAD@{2}:救回它不需要放棄目前 main 上的 E,只要另開一個分支指向它—— git branch rescued 5e6f。這示範了 reflog 救援的黃金原則:先用新分支「釘住」找回的 commit, 再決定要不要合併回主線,不要急著用 reset --hard 蓋掉目前的工作。

9.4本章小結

概念一句話記住
reset --soft只搬指標,暫存區與工作目錄都不動——變更變成已暫存
reset --mixed搬指標 + 重置暫存區,工作目錄不動——變更變成未暫存(預設模式)
reset --hard搬指標 + 暫存區 + 工作目錄全部歸位——會真的丟掉未 commit 的修改
revert不改寫歷史,新增一個反向 commit——已推送的公開分支請用這個
restore檔案層級的還原,取代 checkout 的一半語意(另一半是 switch)
reflog本地的 HEAD 移動時間軸;dangling commit 靠它找回,但只存在本機、有過期時間
這一章補上了「安全網」——知道怎麼悔改,才能放心大膽地實驗。下一章(第 10 章)實用工具與工作流 轉向日常效率:stash 暫存半成品、cherry-pick 挑 commit、tag 標版本、 commit --amend 修上一筆,以及 Git Flow / GitHub Flow / Trunk-based 這些團隊工作流怎麼選。