bigywater.tech
Git · 26

fsck 与悬空对象:误删的 commit 怎么找回

误删了分支、reset --hard 冲掉了暂存的文件、stash drop 了,内容还能找回吗?这篇逐个造出这些误操作,用 git fsck 找出悬空对象并恢复,说明 dangling 与 unreachable 的区别,最后看 fsck 怎么报告损坏的对象。

1 vs 10
删掉分支后 dangling 只列出 1 个末端 commit,而 unreachable 共列出 10 个对象
lost-found
git add 过的内容被 reset --hard 冲掉后,fsck --lost-found 可以把它写回成文件
退出码 3
fsck 发现损坏或缺失对象时的退出码

删分支不删提交

实测 · 本机执行输出
  $ git branch -D feature
  Deleted branch feature (was cf1a7af).
  $ git branch -a; git log --oneline --all
  * main
  a9b2bc9 主线 1
  fsck 找悬空对象(--no-reflogs 忽略 reflog 的保护,等于“假设 reflog 已经过期”):
  $ git fsck --no-reflogs --lost-found 2>&1
  dangling commit cf1a7af242a9d48843721356ad1435445eb40a93
  悬空的 commit 就是被删分支的最后一个提交 f3: True

git branch -D 只删除引用。三个只被它引用的提交变成不可达的对象,仍留在对象库里,git fsck --no-reflogs 把没有任何东西指向的末端列为 dangling commit。

找回整条历史

实测 · 本机执行输出
  $ git show -s --format="%h %s" cf1a7af242a9d48843721356ad1435445eb40a93
  cf1a7af 功能 3(还没合并)
  $ git branch rescued cf1a7af242a9d48843721356ad1435445eb40a93 && git log --oneline rescued
  cf1a7af 功能 3(还没合并)
  c9f466c 功能 2
  99e3446 功能 1
  a9b2bc9 主线 1

用 git branch 新名字 <hash> 重新挂上引用,parent 链把整段历史都带回来:只需要找到那个末端。

dangling 与 unreachable

实测 · 本机执行输出
  $ git fsck --no-reflogs 2>&1 | sort | uniq -c | sed -E 's/[0-9a-f]{40}/<sha>/g'
     1 dangling commit <sha>
  $ git fsck --no-reflogs --unreachable 2>&1 | sed "s/ [0-9a-f]\{40\}//" | sort | uniq -c | sed -E 's/[0-9a-f]{40}/<sha>/g'
     4 unreachable blob
     3 unreachable commit
     3 unreachable tree
  (3 个提交只有最后一个是 dangling,前两个被它的 parent 链引用着;它们的 tree 和 blob 同理)

dangling 是不可达对象的“顶端”:没有任何引用或对象指向它。unreachable 则包括所有走不到的对象——被丢掉的 3 个提交里只有最后一个是 dangling,但 3 个 commit、3 个 tree、4 个 blob 都是 unreachable。

暂存过的文件

实测 · 本机执行输出
  $ git reset --hard -q; ls; git status --short
  a.txt
  $ git fsck --lost-found --no-reflogs 2>&1 | sed -E "s/[0-9a-f]{40}/<sha>/"
  dangling blob <sha>
  $ ls .git/lost-found/other/ | sed -E "s/[0-9a-f]{40}/<sha>/"; cat .git/lost-found/other/*
  <sha>
  写了一整天的重要内容
  没提交,只做了 git add
  fsck --lost-found 把悬空 blob 写成文件(文件名就是 hash),没有路径信息:
  文件名里的 hash 就是原来暂存的 blob: True

只要 git add 过,文件内容就已经作为 blob 写进了对象库。之后 git reset --hard 把暂存区和工作区冲掉,这个 blob 变成悬空对象;git fsck --lost-found 把它们写到 .git/lost-found/other/,文件名是 hash,没有路径。从来没有 add 过的修改没有进对象库,无法找回。

stash 也是 commit

实测 · 本机执行输出
  $ git stash -q; git stash list; git stash drop
  stash@{0}: WIP on main: 2ef4622 起点
  Dropped refs/stash@{0} (8b6cfd0bd100f83582de78ed93bef1529415951e)
  $ git fsck --no-reflogs --unreachable 2>&1 | grep commit | sed -E 's/[0-9a-f]{40}/<sha>/'
  unreachable commit <sha>
  unreachable commit <sha>
  $ git show -s --format="%s | parents=%p" 8b6cfd0bd100f83582de78ed93bef1529415951e | sed -E "s/[0-9a-f]{7}/<sha>/g"
  WIP on main: <sha> 起点 | parents=<sha> <sha>
  $ git stash apply -q 8b6cfd0bd100f83582de78ed93bef1529415951e && cat a.txt
  改了没提交

git stash drop 会打印被丢弃的 stash 的 hash。stash 是带多个 parent 的 commit,变成不可达之后同样可以用 fsck 找到,git stash apply <hash> 恢复。

损坏检测

实测 · 本机执行输出
  $ git fsck 2>&1; echo "退出码 $?"
  退出码 0
  $ { git fsck 2>&1; echo "退出码 $?"; } | sed -E 's/[0-9a-f]{40}/<sha>/g'
  error: inflate: data stream error (incorrect data check)
  error: unable to unpack header of .git/objects/1b/b77240495ec07c912bf8696093f20726108add
  error: <sha>: object corrupt or missing: .git/objects/1b/b77240495ec07c912bf8696093f20726108add
  missing blob <sha>
  退出码 3
  $ { git fsck 2>&1; echo "退出码 $?"; } | sed -E 's/[0-9a-f]{40}/<sha>/g'
  error: inflate: data stream error (incorrect data check)
  error: unable to unpack header of .git/objects/1b/b77240495ec07c912bf8696093f20726108add
  error: <sha>: object corrupt or missing: .git/objects/1b/b77240495ec07c912bf8696093f20726108add
  missing blob <sha>
  missing blob <sha>
  退出码 3

git fsck 遍历对象、校验 zlib 与 hash。改坏一个字节会报告 inflate 错误和对象损坏,删掉文件会报告 missing,退出码都是 3。它只负责发现,不负责修复。

分步演示

fsck 与悬空对象
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。分支删除、reset --hard、stash drop 的恢复都是本机真实操作;fsck 加 --no-reflogs 是为了不让 reflog 保护这些对象(实验里提交时间固定在 2026-01-01,reflog 条目对 gc 而言已经过期);损坏用 Python 直接改文件字节 / 删除文件制造
    • 没有验证:gc 之后这些对象何时被真正删除(见上一篇 prune 与宽限期);pack 里的对象损坏时 fsck 的输出;git fsck 的 --connectivity-only、--strict 等选项;从未 git add 的改动(无法通过 git 找回,没有尝试其它手段);fsck 修复损坏的办法(如从远程重新取回对象)
    京ICP备2026067502号-1京公网安备11010502063487号