Git commit 后怎么找回旧版本

AI 重构覆盖文件、reset --hard 选错 commit、rebase 后旧 commit 看似消失——99% 没真丢。本文给三条找回路径:文件历史、reflog 找 HEAD、fsck 捞悬挂 commit。

你让 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 popgit 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 天再延长

相关阅读

标签: #AI 编程 #排查 #排查