reflog 与数据找回:几乎什么都能救回来
"我 reset --hard 把一天的工作弄丢了!"——别慌。只要那些提交曾经存在于你的仓库里,且还没被 Git 当垃圾回收,就几乎总能找回来。秘密武器是 reflog(引用日志)和 git fsck。
1. reflog:HEAD 的"行车记录仪"
reflog 默默记录着 HEAD(以及各分支)每一次指向了哪个提交,包括 commit、reset、checkout、merge、rebase 等所有移动。它只在本地存在,不会被 push,默认保留 90 天。
echo '===== reflog:HEAD 的每一步移动都被记录 ====='
git init -q -b main rl && cd rl
echo 'v1' > a.txt && git add . && git commit -q -m "feat: v1"
echo 'v2' > a.txt && git add . && git commit -q -m "feat: v2"
echo 'v3' > a.txt && git add . && git commit -q -m "feat: v3"
echo 'git reflog 记录 HEAD 何时指向了哪个提交:'
git reflog
echo
echo '语法 HEAD@{n} 表示“第 n 步之前的 HEAD”:'
echo "当前 HEAD = $(git rev-parse --short HEAD)"
echo "HEAD@{1} = $(git rev-parse --short HEAD@{1})"
echo "HEAD@{2} = $(git rev-parse --short HEAD@{2})"
echo
echo 'reset 回退也会写进 reflog,所以回退本身可回退:'
git reset -q --hard HEAD@{2}
echo "回退后 HEAD = $(git rev-parse --short HEAD)"
echo '回退动作本身出现在 reflog 里:'
git reflog | head -3
cd ..HEAD@{0} 是当前,HEAD@{1} 是上一步,HEAD@{2} 是再上一步……时间写法 HEAD@{10.minutes.ago}、master@{yesterday} 也支持。每条记录的最左边就是那个历史位置的 commit 哈希——这就是找回数据的钥匙。
reflog 有保质期:默认 gc.reflogExpire 是 90 天(已合并分支 30 天)。超过期限或被 git gc --prune=now 清理后,悬空对象会被真正删除。所以丢了东西越早救越好。另外 reflog 是本地的:克隆别人的仓库时 reflog 是空的,救不了别人机器上丢的东西。
2. 找回被 reset 掉的提交
git reset --hard 只是把分支指针挪走了,原来的提交还在库里、只是"不可达"。从 reflog 找到它的哈希,再把分支指回去即可。
echo '===== 用 reflog 找回被 reset 掉的提交 ====='
git init -q -b main lost && cd lost
echo 'v1' > a.txt && git add . && git commit -q -m "feat: v1"
echo 'v2' > a.txt && git add . && git commit -q -m "feat: v2 (重要工作)"
echo "丢失前 HEAD = $(git rev-parse --short HEAD)"
echo '误操作:git reset --hard HEAD~1,把 v2 弄丢了:'
git reset -q --hard HEAD~1
echo "reset 后 HEAD = $(git rev-parse --short HEAD)"
echo '但 reflog 还记着 v2 的哈希:'
git reflog | head -4
echo
echo '找回:把分支指回 reflog 里的那个提交(取 v2 那行开头的哈希):'
LOST=$(git reflog --format='%H %gs' | grep 'feat: v2' | head -1 | awk '{print $1}')
echo "要恢复的提交 = $LOST"
git reset -q --hard $LOST
echo "恢复后 HEAD = $(git rev-parse --short HEAD)"
echo 'a.txt 内容是否回来了:'
cat a.txt
cd ..
echo
echo '===== 找回被删除的分支 ====='
git init -q -b main del && cd del
echo 'base' > a.txt && git add . && git commit -q -m "init"
git checkout -q -b feature/x
echo 'work' > feature.txt && git add . && git commit -q -m "feat: x 的工作"
git checkout -q main
echo '误删分支 feature/x:'
git branch -D feature/x
echo 'git branch -a 已看不到它:'
git branch -a
echo '但 HEAD reflog 仍记着 feature/x 上的那次提交:'
git reflog --oneline | grep 'feat: x'
echo '从 reflog 找到分支 tip 哈希并重建分支:'
TIP=$(git reflog --format='%H %gs' | grep 'feat: x' | head -1 | awk '{print $1}')
git branch feature/x $TIP
echo '分支重建成功,feature.txt 还在:'
git checkout -q feature/x && cat feature.txt && git checkout -q main && cd ..要点:
- 救 reset:
git reflog找到目标哈希,再git reset --hard <哈希>(或git branch rescue <哈希>新建一个救援分支,更安全)。 - 救误删分支:分支本身被删后它的专属 reflog 也没了,但那个提交曾出现在 HEAD 的 reflog 里(因为当时 HEAD 就在这个分支上),从
git reflog里 grep 提交信息拿到 tip 哈希,然后git branch 原名 <哈希>重建。
3. git fsck:reflog 之外的兜底
如果连 reflog 都过期了(或者对象是从别处来的、从没出现在 reflog 里),还有最后一招:直接扫描对象数据库,找出**悬空(dangling)/不可达(unreachable)**的对象。
echo '===== git fsck 找出悬空对象(reflog 之外的兜底)====='
git init -q -b main fs && cd fs
echo 'v1' > a.txt && git add . && git commit -q -m "feat: v1"
echo 'v2' > a.txt && git add . && git commit -q -m "feat: v2 悬空提交"
echo "悬空提交哈希 = $(git rev-parse --short HEAD)"
git reset -q --hard HEAD~1
echo '现在普通 log 已经看不到 v2:'
git log --oneline
echo
echo '情况 A:reflog 还在,直接查 reflog 找回来:'
git reflog --all --oneline | grep 'v2' | head -2
echo
echo '情况 B:reflog 过期/不存在,用 git fsck 扫出所有悬空提交:'
DAN=$(git fsck --no-reflogs --lost-found --unreachable 2>/dev/null | grep 'unreachable commit' | head -1 | awk '{print $3}')
echo "fsck 找到悬空 commit = $DAN"
echo '查看它的提交信息确认是不是我们要的:'
git log --oneline -1 $DAN
echo
echo '把它恢复成分支:'
git branch recovered $DAN
echo "recovered 分支指向:$(git rev-parse --short recovered)"
cd ..git fsck --lost-found 会把找到的悬空 commit / blob 的哈希分别写进 .git/lost-found/commit/ 和 .git/lost-found/other/,方便你逐个检查。确认后 git branch <name> <哈希> 即可复活。
4. 什么时候真的救不回来
- 对象从未被提交过(比如只
git add了、还没commit就reset/clean了某部分)——git add会生成 blob,通常还在,用git fsck碰碰运气。 - 已经超过 reflog 过期时间,且
git gc已真正回收(git gc --prune=now后基本没戏)。 - 从未提交的纯工作区改动(没
add也没commit)被git checkout/reset --hard覆盖——Git 从不曾见过它,无法恢复,只能靠编辑器本地历史。
所有"找回"技巧都是事后补救。最稳的还是:频繁做小提交,重要改动立刻 commit(哪怕只是 WIP)。提交进了对象库,reflog 就能兜住你。哪怕推到远程(或自己的备份分支)一秒,那也是最可靠的保险。
小结
reflog是 HEAD/分支的"行车记录仪",记录每次移动与对应哈希,本地保存约 90 天HEAD@{n}表示第 n 步之前的 HEAD;记录最左就是当时的 commit 哈希- 救 reset:
git reflog找哈希 ->git reset --hard <哈希>或git branch rescue <哈希> - 救误删分支:从 HEAD reflog 里 grep 到分支 tip 哈希,再
git branch 原名 <哈希> - reflog 过期或没有时,用
git fsck --lost-found扫悬空对象兜底 - 真救不回:超过 reflog 过期且已 gc、或从未提交过的纯工作区改动
- 下一章:团队协作工作流 →
- 初始化仓库,连续做 3 个提交。执行
git reset --hard HEAD~2,再用git reflog找到被丢掉的提交哈希,把它恢复成一个名为rescue的分支(git branch rescue <哈希>),并git log rescue确认内容还在。 - 新建分支
feature/temp,在上面提交一次,切回 main 后git branch -D feature/temp。用git reflog找出那次提交的哈希,重建feature/temp,验证文件存在。 - 做两次提交后
git reset --hard HEAD~1,然后用git fsck --lost-found找出悬空 commit,把它恢复成分支。对比git reflog和git fsck两种找法。 - 为什么
git reflog在刚克隆(clone)下来的仓库里是空的?这说明了 reflog 的哪一特性?(提示:它是否随仓库传输?)