git reset --hard 之后怎么捞回来:reflog、悬空对象,和那几个过期时限

· 约 5 分钟 🌿 Git 命令速查

敲完 git reset --hard 的下一秒,发现刚才那半天的活儿没了。

先停手。 这一刻数据多半还在 .git 里,而接下来你敲的每一条命令都可能把它推得更远。

先分清丢的是哪一种

Git 的恢复能力不是一个开关,它取决于丢掉的内容有没有进过对象库:

情况能找回吗用什么
已经 commit 过能git reflog
add 过但没 commit能(gc 之前)git fsck --lost-found
改完从没 add 过不能——

分界线就是 git add。add 会立刻把文件内容写成一个 blob 对象存进 .git/objects,哪怕你之后从没提交过。而从没 add 过的修改,Git 从头到尾就不知道它存在。

这也是那条老建议的真正理由:不确定某条命令的后果时,先 git add . 或 git stash -u。两秒钟,换一次免费快照。

一、提交过的:reflog

git reflog 记录的是引用的每一次移动——HEAD 曾经指向哪里、分支曾经指向哪里,包括被 reset 掉的那些位置。

git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
9f8e7d6 HEAD@{1}: commit: 写完了导出逻辑      ← 要找的是这个
5c4b3a2 HEAD@{2}: commit: 补测试

确认内容,再恢复:

git show 9f8e7d6              # 先看一眼是不是它
git branch rescue 9f8e7d6     # 挂到新分支上,当前分支不动

恢复时建分支而不是直接 reset,这一条值得养成习惯。git branch rescue <哈希> 完全不改变你当前的状态,确认 rescue 分支里的东西对了再合回去;直接 git reset --hard 过去,万一哈希找错了还得再捞一轮。

几个常用变体:

git reflog show feature-x          # 只看某个分支的移动(找回误删的分支)
git reflog --date=iso              # 带上时间,好认
git show HEAD@{2.hours.ago}        # 按时间回溯
git diff HEAD@{1} HEAD             # 对比上一个位置

还有一个经常被忘记的快捷方式:merge、rebase、reset 这几条命令执行前会把原位置存进 ORIG_HEAD,所以「刚 rebase 完发现搞砸了」最快的一条是:

git reset --hard ORIG_HEAD

二、add 过但没提交的:悬空对象

reflog 对这一类无能为力——没有任何引用指向过它们。但对象还在,只是成了「悬空对象」(dangling object)。

git fsck --lost-found

它会把捞到的东西写进 .git/lost-found/:

.git/lost-found/commit/   ← 悬空的提交
.git/lost-found/other/    ← 悬空的 blob,也就是文件内容

接下来是逐个认:

ls -lt .git/lost-found/other/          # 按时间排序,最近的在最上面
git cat-file -p <哈希> | head -50      # 看看是不是要找的那个文件
git cat-file -p <哈希> > src/lost.ts   # 认出来了就写回去

注意 blob 不带文件名——对象库里只存内容,路径信息在 tree 对象里。所以这一步只能靠看内容辨认,文件多的时候会有点枯燥,但东西确实在。

一个更省事的筛选方式,是先按类型和时间过滤:

git fsck --unreachable --no-reflogs | grep blob

三、stash 被 drop 或 clear 了

git stash drop 执行时会打印一行:

Dropped refs/stash@{0} (a1b2c3d4e5f6...)

那串哈希就是钥匙,终端还没关就抄下来:

git stash apply a1b2c3d4e5f6

输出已经翻不到了也不要紧,stash 条目本质上是提交对象,同样能捞:

git fsck --unreachable | grep commit
git show <哈希>                # 逐个看,stash 条目通常有 2-3 个父提交
git stash apply <哈希>

git stash clear 也一样——它只删掉了 refs/stash 这个引用,底下的提交对象要等 gc 才会被真正清掉。

四、Git 什么时候真的删

三个默认时限,各管一段:

配置项默认值管什么
gc.reflogExpire90 天仍然可达的 reflog 条目保留多久
gc.reflogExpireUnreachable30 天已经不可达的 reflog 条目保留多久
gc.pruneExpire两周悬空对象被物理删除前的宽限期

而 gc 本身不是定时跑的。多数情况下它是被 commit、merge、rebase 这类命令以 gc --auto 的形式顺带触发,且默认要等松散对象攒到六千多个才真动手。小仓库可能几个月都不 gc 一次——这也是很多「以为早就没了」的东西其实还在的原因。

反过来,有两条命令会立刻把宽限期清零:

git gc --prune=now                        # ⚠ 立刻物理删除悬空对象
git reflog expire --expire=now --all      # ⚠ 立刻清空 reflog

数据还没找回来之前,这两条绝对不要敲。

五、这几种是真的没了

诚实地列一下边界:

  • git reset --hard 丢掉的从没 add 过的工作区修改
  • git checkout -- 文件 / git restore 文件 覆盖掉的未暂存修改
  • git clean -fd 删掉的未跟踪文件——它们从来就不在 Git 里
  • 已经 git gc --prune=now 过的悬空对象

这几种只能指望 Git 之外的东西:IDE 的本地历史(JetBrains 的 Local History、VS Code 的 Timeline 都独立于 Git 记录文件快照,很多人是靠这个救回来的)、编辑器的自动备份、系统级的时间机器或文件历史。

六、慌的时候最不该做的三件事

  1. 继续敲改写历史的命令 —— 又一次 reset、又一次 rebase,只会让 reflog 更长、更难认
  2. 敲 git gc --prune=now —— 网上某些「清理仓库」的帖子会推荐它,在这个时刻它是删除键
  3. 重新 clone 覆盖当前目录 —— 远端没有的东西,clone 回来也不会有;而你本地的 .git 一删,所有悬空对象一起没

预防比恢复便宜得多

  • 不确定就先 git stash -u —— 两秒钟,只在本地,不会被推出去
  • 提交粒度小一点 —— 提交过的东西 90 天内基本捞得回来,没提交的只有两周
  • 危险命令先认一遍 —— Git 命令速查 把会丢改动的命令单独标了出来,勾上「只看会丢东西的命令」一次能看完,敲之前扫一眼比事后捞省事
  • --force-with-lease 替代 --force —— 代价为零,能挡掉一类很难解释的事故

配套的两篇:.gitignore 加了却不生效、已提交的文件怎么让 Git 忘掉,见 .gitignore 不生效怎么排查。

❓ 常见问题

reflog 里翻不到了,还有别的办法吗?

按顺序试这四个。(1) 远端还有吗 —— git fetch origin 之后看 origin/分支名,或者直接去平台的网页端看提交列表。同事的本地仓库也算一份备份,这是最省事的一条;(2) git fsck --lost-found —— 把所有不可达的对象倒出来,commit 和 blob 分开放,比 reflog 捞得更深;(3) IDE 的本地历史 —— JetBrains 系的 Local History、VS Code 的 Timeline 都独立于 Git 记录文件快照,Git 完全不知道的改动它可能有;(4) 编辑器的备份和系统快照 —— macOS 的时间机器、Windows 的文件历史记录。注意:前两条都有时限,见正文的三个默认值,所以发现丢了就立刻查,别等明天。

为什么说 --hard 丢的工作区改动 reflog 救不回,你又说 fsck 能捞?

分界线是它有没有进过暂存区。reflog 记的是引用的移动 —— 哪个分支、什么时候指向了哪个提交。从没被提交过的内容不在任何引用上,reflog 自然没有它。但 git add 会立刻把文件内容写成一个 blob 对象存进 .git/objects,即使后来从没 commit 过,那个 blob 也还躺在对象库里,只是没有任何东西指向它——这就是「悬空对象」。git fsck --lost-found 捞的正是这一类。所以三种情况:(1) commit 过 → reflog;(2) add 过没 commit → fsck;(3) 改完从没 add 过 → 真的没了,git checkout -- 文件、git clean -fd 丢掉的也一样。这就是「不确定先 add 一下」的价值——add 是一次两秒钟的免费快照。

git stash drop 或者 stash clear 之后能找回吗?

能,只要还没被 gc 清掉。最快的一条:git stash drop 执行时会打印被删条目的哈希,形如 Dropped refs/stash@{0} (a1b2c3d...),把那串哈希抄下来,之后 git stash apply a1b2c3d 就能恢复。翻不到那行输出时:git fsck --unreachable | grep commit 列出所有不可达的提交,stash 条目是一种特殊的合并提交(通常有两到三个父提交),用 git show 逐个看内容认出来,再 git stash apply <哈希>。清空过的也一样能捞,git stash clear 只是删掉了 refs/stash 这个引用,底下的提交对象还在。时限:不可达对象默认在 gc 时保留两周,所以这事要趁早。

什么时候 Git 才真的把东西删掉?

gc 跑起来、而且对象已经过了保护期的时候。三个默认时限各管一段:(1) gc.reflogExpire 90 天 —— 可达的 reflog 条目保留这么久;(2) gc.reflogExpireUnreachable 30 天 —— 已经不可达的 reflog 条目只保留这么久;(3) gc.pruneExpire 两周 —— 悬空对象在被物理删除前的宽限期。gc 什么时候跑:多数是 gc --auto 被 commit、merge、rebase 这类命令顺带触发的,默认在松散对象超过 6700 个左右时才真动手,所以小仓库可能几个月都不 gc 一次。唯一会立刻删干净的是你自己敲的 git gc --prune=now 或 git reflog expire --expire=now --all —— 这两条在数据还没找回来之前绝对不要敲。

🌿 打开 Git 命令速查 150+ 命令按 12 类整理·「我想撤销」场景直达·危险命令标红·中文释义可搜可复制 →

🔗 相关阅读

全部教程 →