2026-07-01 · reset · revert · restore · checkout · reflog · dangling commit
git add 到不該加的檔案、在錯的分支改了半天,甚至下錯 reset --hard 把工作毀了一半。
本章要拆解 Git 的「悔改機制」:reset 如何移動指標並牽動三大區域、
revert/restore/checkout 這幾個容易搞混的指令各自管什麼範圍,
以及最重要的——reflog 為什麼能讓你幾乎永遠不會真的「弄丟」一個 commit。
搞懂這章,你才能真正放心大膽地用 Git,而不是每次下指令前都心驚膽跳。
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 是最後一道防線。
git reset <mode> B
.git/objects/ 裡。這正是 9.3 節 reflog 能救回它的原因。
revert / restore / checkout 的差異與時機
reset 只是「悔改工具箱」的其中一件。Git 依「操作範圍」把其他工具分成兩層:
commit 層級(整個歷史怎麼變)與檔案層級(單一檔案的內容怎麼變)。
git reset | git revert | |
|---|---|---|
| 動作 | 移動分支指標到另一個 commit | 建立一個新 commit,內容是目標 commit 變更的「反向 diff」 |
| 對歷史的影響 | 改寫——原 commit 從分支的歷史裡消失(除非用 reflog 找回) | 保留——原 commit 還在,只是多了一個抵銷它的新 commit |
| 適合情境 | 只存在本地、尚未推送的 commit | 已推送、別人可能已經拉走的 commit |
| 與第 7 章的關係 | 呼應 7.3「黃金法則」:公開歷史別用 reset/rebase 硬改,用 revert 補一筆才安全 |
Git 2.23(2019)之前,checkout 身兼二職——切分支和還原檔案共用同一個指令,語意常讓人搞混。
後來拆成兩個專職指令:switch(切分支,見第 5 章)與restore(還原檔案)。
checkout 舊語法仍然能用(相容性保留),但新手建議直接學 restore/switch。
| 指令 | 作用範圍 | 效果 | 對應舊語法 |
|---|---|---|---|
git restore <file> | 單一檔案 | 用暫存區(或指定 commit)的內容覆蓋工作目錄,丟棄未暫存的修改 | git checkout -- <file> |
git restore --staged <file> | 單一檔案 | 把檔案從暫存區移回「已修改但未暫存」,不動 HEAD | git reset HEAD -- <file> |
git restore --source=<commit> <file> | 單一檔案 | 從任意歷史 commit 取出該檔案內容,覆蓋工作目錄 | git checkout <commit> -- <file> |
checkout 猜語意。
reflog 救回「以為弄丟」的 commit
git log 顯示的是「從目前 HEAD 出發可以走到的 commit」——一旦分支指標移開,
舊的 commit 就從 log 裡消失。但 Git 在本地還悄悄維護另一本帳:reflog,
它記錄的不是 commit 之間的關係,而是「HEAD 曾經指到過哪裡」的時間軸——
每次 commit、checkout、switch、merge、rebase、reset,都會在 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 的輸出(由新到舊)長這樣——點一列看說明:
HEAD@{2}:救回它不需要放棄目前 main 上的 E,只要另開一個分支指向它——
git branch rescued 5e6f。這示範了 reflog 救援的黃金原則:先用新分支「釘住」找回的 commit,
再決定要不要合併回主線,不要急著用 reset --hard 蓋掉目前的工作。
| 概念 | 一句話記住 |
|---|---|
| reset --soft | 只搬指標,暫存區與工作目錄都不動——變更變成已暫存 |
| reset --mixed | 搬指標 + 重置暫存區,工作目錄不動——變更變成未暫存(預設模式) |
| reset --hard | 搬指標 + 暫存區 + 工作目錄全部歸位——會真的丟掉未 commit 的修改 |
| revert | 不改寫歷史,新增一個反向 commit——已推送的公開分支請用這個 |
| restore | 檔案層級的還原,取代 checkout 的一半語意(另一半是 switch) |
| reflog | 本地的 HEAD 移動時間軸;dangling commit 靠它找回,但只存在本機、有過期時間 |
stash 暫存半成品、cherry-pick 挑 commit、tag 標版本、
commit --amend 修上一筆,以及 Git Flow / GitHub Flow / Trunk-based 這些團隊工作流怎麼選。