你让 Claude Code 重构一个 helper,它顺手把整个文件重写、你 commit 了,然后才发现旧版本里有一段处理特殊情况的 5 行代码被一并删了。或者刚跑完 git reset --hard,发现 reset 错了 commit。或者远程被 rebase,本地 git pull 之后旧 commit 看起来”不见了”。99% 的情况这些代码并没真的丢——只是 ref 指针不再指向它们,但 git object 还在仓库里至少 30 天。这篇给出最常用的 3 条找回路径:从文件历史还原、用 reflog 找回 HEAD、以及 fsck 捞悬挂 commit。
常见原因
按出现频率从高到低排序。
1. AI / 手动 commit 覆盖了局部内容
你 git add . && git commit -am "..." 时,AI agent 重写过的文件覆盖了你想保留的几行。这是和 AI 编程时最常见的失误——尤其是 prompt 没指定”只改某函数”时。
如何判断:git log --oneline -5 -- path/to/file.ts 看该文件最近的 commit;git diff HEAD~1 -- path/to/file.ts 检查上一个 commit 究竟改了什么。
2. git reset --hard 把 HEAD 移走
想撤销最后一个 commit,结果输成 git reset --hard HEAD~3,三个 commit 一起没了。
如何判断:git reflog 第一行如果是 HEAD@{0}: reset: moving to ...,就是。
3. 分支被 rebase / force-push
队友(或你自己)git rebase main 然后 git push --force,旧 commit hash 全变了,你本地的 git pull 之后老 commit 在 ref 树里找不到。
如何判断:git fetch && git log origin/branchname --oneline 看到的 hash 和你 git reflog 里的对不上。
4. 误删分支
git branch -D feature-x 删掉了还没合并的分支,分支 tip 的 commit 立刻成了悬挂状态。
如何判断:git reflog show feature-x 已经报错,但 git reflog | grep feature-x 还能找到最后一次 checkout 的 hash。
5. Stash 被覆盖或 drop
git stash pop 时遇到冲突,没解决就 git stash drop;或者多次 stash 后 git stash clear。
如何判断:git fsck --unreachable | grep commit 列出来的所有悬挂 commit 里,找时间戳接近 stash 时间的。
最短修复路径
按”丢失类型”分支选择。
Step 1:先备份当前状态
无论接下来走哪条路,第一步都是:
git stash push -u -m "before-recovery-$(date +%s)"
# 或更稳妥
git branch backup/before-recovery-$(date +%s)
这样万一找回操作出错,你能 git stash pop 或 git checkout backup/... 回到现在。
Step 2:路径 A — 知道哪个文件丢了内容
最常见情况。用 log -p 看文件的完整历史:
git log -p --follow -- path/to/file.ts
--follow 让 git 追踪文件改名。翻到你想要的版本,记下那个 commit 的 SHA。然后:
# 把那一版恢复到工作区(不影响其他文件)
git checkout <sha> -- path/to/file.ts
# 或者只看不改
git show <sha>:path/to/file.ts > /tmp/oldversion.ts
diff /tmp/oldversion.ts path/to/file.ts
如果只想要旧版本里的某几行,复制粘贴比 cherry-pick 干净。
Step 3:路径 B — 整个 HEAD 被 reset / rebase 移走
git reflog 是 git 的”本地操作日志”,记录 HEAD 每次的移动:
git reflog
# 输出例:
# a3f7c1d HEAD@{0}: reset: moving to HEAD~3
# 9b2e8f4 HEAD@{1}: commit: fix auth bug
# 7c4a1d2 HEAD@{2}: commit: add login form
# ...
找到 reset 之前的那个 hash(这里 9b2e8f4),然后:
# 看看那个状态长什么样
git show 9b2e8f4
# 完全回到那个状态
git reset --hard 9b2e8f4
# 或者基于它建一个新分支,不影响当前
git branch recovered-state 9b2e8f4
Step 4:路径 C — reflog 也找不到(删除分支、悬挂 commit)
如果连 reflog 都没记录(比如另一个 clone、或者 reflog 太久被 gc),用 fsck 扫描悬挂对象:
git fsck --lost-found
# 输出例:
# dangling commit a1b2c3d...
# dangling commit e4f5a6b...
# 逐个查看
git show a1b2c3d
git show e4f5a6b
# 找到目标后,用 checkout 或建分支
git branch recovered a1b2c3d
按时间排序更快:
git fsck --unreachable --no-reflogs | grep commit | \
awk '{print $3}' | \
xargs -I{} git log -1 --format='%ci %H %s' {} | sort -r
Step 5:路径 D — 用 git bisect 二分查找正确版本
如果你只知道”两周前能跑,现在不行”但不知道哪 commit 引入了问题:
git bisect start
git bisect bad HEAD # 现在是坏的
git bisect good v1.4.0 # 知道哪个 tag 是好的
# git 自动 checkout 中间的 commit,你测试后回答:
git bisect good # 或 git bisect bad
# 完成后:
git bisect reset
定位到问题 commit 后,用 Step 2 的方法把那个文件回退到 commit 之前。
Step 6:还没 gc?立刻 push 备份
找回的 commit 是悬挂状态,默认 30 天后会被 git gc 清理。立刻 push 到远程,把它变成 ref:
git branch recovery/found-it <sha>
git push origin recovery/found-it
预防建议
- AI 重构前先
git commit -am "checkpoint before AI refactor",让 agent 有”已知好”的回退点 - 启用 IDE 的 auto-save + Local History(VS Code / Cursor 默认就有),独立于 git 多一层保险
- 每天结束前
git push到远程或个人备份分支,reflog 只在本地、清盘就没 - 关键功能改完先 push 到
wip/分支再继续,别 squash 掉中间 commit 后才发现要找回 - 团队规范里禁止
force push到共享分支(main / develop),改用--force-with-lease - 设置
git config gc.reflogExpire 90.days.ago把 reflog 保留期从 90 天再延长