AI 改代码后怎么安全回滚:选最小撤销,再用 reflog 救回来

撤销 AI 的坏改动、又不误删好改动:选范围最小的 git 撤销命令,并在 30 天期限内用 reflog 救回丢失的 commit。

让 Claude Code 跑一晚上自主任务,早上起来发现它把 12 个文件改得面目全非,新功能没做完,旧功能挂了。最危险的时刻:你想”全部撤销”——这时候打错一条 git checkout .git reset --hard 就把可能有用的部分一起扔了。

最快的安全做法:动手前先执行 git branch backup/before-rollback-$(date +%Y%m%d-%H%M)git stash push -u,给自己留一个快照,让后面所有命令都可逆。然后从下面的表里挑能解决问题的、范围最小的撤销——先用 git restore <file>,别一上来就 git reset --hard

本文按”撤销范围从小到大”列出 6 种回滚场景,配上每种命令的具体语法和”撤销了什么、保留了什么”对照表。然后教你在 reset --hard / --amend / rebase 之后怎么用 git reflog 把”以为永远丢了”的改动救回来——并讲清一个大多数教程都搞错的细节:丢失的 commit 默认只保留 30 天gc.reflogExpireUnreachable),不是 90 天,而且一条 git gc --prune=now 可能更早就把它清掉。先救回来,再慢慢收拾。

我属于哪种情况?

动手前先判断你处在哪一档——正确命令完全取决于坏改动跑了多远(working tree / 已 commit / 已 push / 已 merge)。

坏改动在哪最危险的命令安全命令对应小节
改了但没 commitgit checkout .(无声清空全部)git restore <file>git restore -p原因 1
已 commit,没 pushgit reset --hard(连好 commit 一起丢)git reset --soft HEAD~1原因 2-3
已 push,没 merge(PR 开着)git push --force(覆盖队友的 push)git push --force-with-lease原因 4
已 merge 到 main任何 reset(改写共享历史)git revert <sha>原因 5
历史被改写(--amend/rebase运行 git gc(把丢失 commit 清掉)git reflog 然后建分支原因 6

常见原因

按需要回滚的紧急程度排序。

1. AI 做得太多,需要部分撤销

最常见。Agent 自主跑了 20 分钟改了 8 个文件,3 个是你想要的,5 个是它自己脑补的。你想保留 3 个、扔掉 5 个,但不知道怎么按文件 / 按 hunk 精确撤销。

如何判断git diff --stat 显示一大堆文件,逐文件 review 后能明确分出”想留 / 想扔”两堆。

2. 本地测试过,CI 挂了

本地 npm test 全绿,push 上去 CI 红。怀疑 AI 的某次改动引入了环境相关 bug(路径大小写、CRLF、依赖版本)。需要回滚到上一个 CI 绿的 commit。

如何判断:本地 git log --oneline 有几个 AI commit;最近一次绿色 CI 的 commit 你能找到(CI 仪表盘 / GitHub Actions 历史)。

3. 想要 AI 的”计划”但不要它的”代码”

Agent 输出了一个很合理的执行计划(重命名 X、抽出 Y、补测试 Z),但实际写出来的代码质量差。你想保留 commit message 和计划,扔掉所有代码改动重新让另一个模型写。

如何判断:commit message / agent transcript 比 code diff 价值高。

4. AI 改动已经 push 但还没 merge

PR 已经 push 到远程,但 reviewer 还没 approve。你想强制 reset 自己的分支,但又怕 force push 把别人的并行 commit 弄丢。

如何判断git log @{u}..HEAD 列出还没在远程的 commit;git log HEAD..@{u} 列出远程领先你的 commit。

5. AI 改动已经 merge 到 main

最棘手。改动已经在主分支,可能有人基于它 pull 了。不能 reset,只能 git revert 生成反向 commit。

如何判断git log main --grep="<AI commit>" 显示该 commit 在 main 上。

6. AI 用 git commit --amendrebase 改了历史

Agent 自己执行了 git commit --amendgit rebase -i,把你想保留的 commit 改没了。这时候只有 reflog 能救,而且有时间限制——被丢掉的 commit 现在是”不可达”(unreachable)状态,由 gc.reflogExpireUnreachable 决定保留多久(截至 2026 年 6 月默认 30 天),而不是普通条目的 90 天。今天就把它救回来。

如何判断git log 看不到你记得的 commit,但你确定昨天还在。

最短修复路径

Step 1:先 snapshot 当前状态——任何回滚之前

# 创建安全分支,即使所有操作都搞砸也能回到这里
git branch backup/before-rollback-$(date +%Y%m%d-%H%M)

# 把 working tree 的脏改动也存一份
git stash push -u -m "before rollback $(date)"

git stash-u 包含 untracked 文件;-m 加 message 方便之后从 git stash list 找回来。

Step 2:按”撤销范围”选对命令

下表覆盖 99% 场景。先确认当前状态,再选命令

想撤销什么命令撤销了什么保留了什么
单个文件的未 stage 改动git restore <file>该文件回到 HEAD 状态其他文件、staged 区
全部未 stage 改动git restore .所有 unstaged 改动staged 区、untracked
已 stage 但未 commitgit restore --staged <file>取消 stage(保留改动)文件内容
最近一次 commit(保留改动)git reset --soft HEAD~1commit 记录所有改动在 staged 区
最近一次 commit(改动放 working tree)git reset HEAD~1commit + stage改动在 unstaged
最近一次 commit(彻底丢)git reset --hard HEAD~1commit + 所有改动
部分 hunkgit restore -p <file>你选中的 hunk其他 hunk
已 push 已 merge 的 commitgit revert <sha>生成反向 commit历史完整

关键区别

  • --soft = 改动到 staged;--mixed(默认)= 改动到 unstaged;--hard = 改动消失(但能从 reflog 救回)

Step 3:用 reflog 救回”消失的”改动

reset --hard / --amend / rebase 之后的”消失”通常还没真的没——Git 在每个仓库里记着 HEAD 指过的每个位置,那个 commit 对象会一直留在 .git 里,直到垃圾回收把它清掉。

git reflog              # 列出所有 HEAD 移动历史,最新在上
# c9d3e5a HEAD@{0}: reset: moving to HEAD~1
# 8b4f2a1 HEAD@{1}: commit: AI: refactor user service
# 2e1c8d3 HEAD@{2}: commit: WIP

git branch rescue 8b4f2a1   # 第一步:先建分支让它重新"可达"(最稳)
git checkout rescue         # 再切过去检查

git reflog --since="2 hours ago" 限定时间窗减少噪音。要在建分支前先看看某个丢失 commit 的内容,用 git show 8b4f2a1;stash 则用 git stash show

你还有多长时间? 这正是大多数教程搞错的地方。截至 2026 年 6 月,Git 的默认值是:

条目状态配置项默认保留时长
可从分支 / HEAD 到达gc.reflogExpire90 天
不可达(被 reset / amend / rebase 丢掉)gc.reflogExpireUnreachable30 天

reset --hard 掉的改动属于 30 天那一档。更糟的是,手动 git gc --prune=now(或某些 IDE / agent 的”清理”动作)会立刻删掉不可达对象。所以规则是:先救回来,没救回来之前别跑 git gc 如果想给以后留更长的保险,可以设 git config --global gc.reflogExpireUnreachable 90.days

Step 4a:已 push 但还没 merge——用 lease 强推,绝不裸 force

PR 分支是你自己的、没别人在上面,所以可以改写——但要用 --force-with-lease,不要用 --force。lease 会在远程被别人(队友或你的 CI bot)动过时中止 push,这样你不会无声覆盖掉别人的提交。

git reset --hard <good-sha>           # 把本地分支回退到好的位置
git push --force-with-lease           # 远程意外移动过就拒绝

先用 git log @{u}..HEAD(你还没 push 的 commit)和 git log HEAD..@{u}(远程领先你的 commit)确认谁领先。

Step 4b:已 push 已 merge 到 main——用 revert

如果 commit 已经在 main 这种共享分支上,不要 force push。改成生成反向 commit:

git revert <bad-sha>           # 单个 commit
git revert <bad-sha>^..<bad-sha2>   # 一段范围,从旧到新
git revert -m 1 <merge-sha>    # revert 一个 merge commit

对 merge commit,-m 1 是告诉 Git 哪个父提交是要保留的”主线”——父 1 是你 merge 进去的分支(通常是 main),父 2 是被 merge 进来的分支。选 -m 1 还是 -m 2 之前,先用 git show <merge-sha>Merge: 那一行(按顺序列出两个父)。revert 会生成一个新 commit,可以安全 push 到受保护的分支。

Step 5:从 stash 恢复你之前保险的状态

如果 rollback 做错了想回到 Step 1 的 snapshot:

git stash list
# stash@{0}: On main: before rollback 2026-05-22

git stash apply stash@{0}    # 应用但保留 stash
git stash pop stash@{0}      # 应用并删除 stash

# 或者直接 checkout 安全分支
git checkout backup/before-rollback-20260522-1430

怎么确认回滚到位了

别凭”看着对”就收工——验证一下树确实是你想要的状态:

git status                 # 树干净、分支正确、没有遗留的 staged 文件
git diff HEAD             # 这里没东西,说明 working tree == 最近一次 commit
git log --oneline -5       # 坏 commit 已经消失(reset)或被反向掉(revert)
git diff <good-sha>..HEAD  # 输出为空 == 你已经精确回到已知正确的状态

然后跑项目里最便宜的真实检查——npm testnpm run build,或者干脆把应用启起来。回滚后留下一个脏 lockfile 或半 stage 的文件,是这事第二常见的翻车方式(仅次于 reset --hard 本身)。一切变绿之后,把不再需要的安全分支删掉:git branch -D backup/before-rollback-...

预防建议

  • 让 AI 工作前 working tree 必须是干净的:git status 必须 nothing to commit,脏的先 stash
  • 在 CLAUDE.md / AGENTS.md 写:“每完成一个原子改动就 git commit,绝不一次 commit 多个无关改动”——commit 越小,回滚损失越小
  • 让 agent commit 一律加前缀 AI:git config commit.template),方便之后 git log --grep="^AI:" --since="1 day" 一次找出所有 AI commit
  • 高风险任务(删大量文件、改 lockfile、改 migration)让 agent 在执行前先 git tag wip/before-<task>,回滚一行命令
  • 禁止 agent 直接执行 git reset --hard / git push --force / git rebase -i——这些命令必须人来按
  • 关键分支开启 GitHub branch protection,禁止 force push,确保 reflog 之外还有远端历史可恢复

常见问题

我跑了 git reset --hard,丢了一个 AI 写的、还没 commit 的文件。reflog 能救回来吗? 只有它曾经被 commit 或 stash 过才行。reflog 跟踪的是 commit 和 stash 条目,不包括散落在 working tree 里的改动。一个写出来但从没 git add + commit 过的文件,在 .git 里没有对象,靠 Git 救不回来——去看编辑器的本地历史(VS Code 的 Timeline 面板;JetBrains 的 Local History)。

reset --hard 之后我到底有多久时间救回 commit? 默认 30 天,不是 90 天。90 天(gc.reflogExpire)是给仍可从分支到达的条目用的。被 reset / --amend / rebase 丢掉的 commit 是不可达的,由 gc.reflogExpireUnreachable 管,截至 2026 年 6 月默认 30 天——而且手动 git gc --prune=now 会立刻删掉它。当天就救回来。

git reset--soft / --mixed / --hard 有什么区别? 三者都把分支指针往回挪。--soft 把改动留在 staged 区;--mixed(默认)把改动留在 working tree 的 unstaged;--hard 直接把改动从 working tree 抹掉(只能趁对象还在时靠 reflog 救)。想重做最近一次 commit 用 --soft,确实要彻底丢掉改动才用 --hard

撤销一个 commit 该用 git revert 还是 git reset 只在本地的 commit,reset 更干净。已经 push 到别人也在用的分支(尤其 main),用 revert——它加一个反向 commit、不改写共享历史,别人就不会拿到分叉的副本。

git push --force-with-lease 被拒了,为什么? lease 发现你上次 fetch 之后远程分支动过了——队友或 CI push 了东西。这正是保护机制在起作用。先 git fetch,用 git log HEAD..@{u} 看看新来的 commit,处理好再 push。永远别为了”推过去”改用裸 --force

能只回滚一个文件里 AI 的部分改动吗? 可以——git restore -p <file> 会逐个 hunk 问你留还是扔。已 stage 的 hunk 用 git restore --staged -p <file>。这是最常见场景的精确工具:同一个文件里 agent 改对了三处、改错了五处。

相关阅读

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