让 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)。
| 坏改动在哪 | 最危险的命令 | 安全命令 | 对应小节 |
|---|---|---|---|
| 改了但没 commit | git checkout .(无声清空全部) | git restore <file> 或 git restore -p | 原因 1 |
| 已 commit,没 push | git 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 --amend 或 rebase 改了历史
Agent 自己执行了 git commit --amend 或 git 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 但未 commit | git restore --staged <file> | 取消 stage(保留改动) | 文件内容 |
| 最近一次 commit(保留改动) | git reset --soft HEAD~1 | commit 记录 | 所有改动在 staged 区 |
| 最近一次 commit(改动放 working tree) | git reset HEAD~1 | commit + stage | 改动在 unstaged |
| 最近一次 commit(彻底丢) | git reset --hard HEAD~1 | commit + 所有改动 | 无 |
| 部分 hunk | git restore -p <file> | 你选中的 hunk | 其他 hunk |
| 已 push 已 merge 的 commit | git 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.reflogExpire | 90 天 |
| 不可达(被 reset / amend / rebase 丢掉) | gc.reflogExpireUnreachable | 30 天 |
你 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 test、npm 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 改对了三处、改错了五处。