AI 误删文件或清空了目录,四种情形分别怎么恢复

2026-07-28

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

多数人把这件事的因归错了:他们以为问题是「AI 失控删了东西」,于是把精力花在追责和调工具配置上。实际决定你能不能救回来的,只有一件事——文件在被删的那一刻,处在 git 的哪一层。 已经进过对象库的文件,几乎一定还在,只是你不知道用哪条命令把它捞出来;从创建到删除都没被 git 看见过的文件,再高明的命令也变不出内容来。所以真正该做的第一件事不是复盘 AI,是判断层级。

顺便说清本篇和站内两篇的分工:内容还在、只是被改错了,那是回滚问题,看 AI 改坏了代码怎么回滚;多个会话同时动一个仓库、互相覆盖互相删,那是隔离问题,看 用 worktree 做并行开发。这篇只管一件事:文件已经不见了,怎么把它捞回来,以及什么时候该承认捞不回来。

一、动手前的三分钟止血

删除已经发生了,真正会造成二次损失的是接下来这三分钟。你越着急,越容易亲手把最后的副本盖掉。

先停掉正在跑的 AI 会话。不是让它「别删了」,是直接中断执行。它如果还在循环里,下一步很可能是「重新生成一份」,而重新生成会占用同名路径,把你本来还能靠内容特征搜出来的痕迹彻底覆盖。

再停掉一切会写盘的自动化:格式化保存、监听重建、测试守护进程、提交钩子。这些东西在你排查期间会大量改写工作区和构建产物目录,而构建产物往往是未入库文件的最后一份内容拷贝。

然后把 .git 目录整体复制一份出去:

cp -a .git /tmp/git-rescue-backup

这一步是为了防你自己。后面要用的 git resetgit checkoutgit stash 全是会改索引和引用的命令,敲错一条就可能把还能救的东西弄成救不回来的。有了这份备份,最坏情况也只是回到现在。

最后一条纪律:在搞清楚情形之前,绝对不要跑 git checkout .git restore .git reset --hardgit clean -fd 这四条里的任何一条。它们是「按 git 记录的样子覆盖磁盘」,而你现在正处在「磁盘上可能有 git 不知道的东西」的状态。这四条命令造成的二次删除,比 AI 的第一次删除更难救。

二、用现象判别情形:先看 git 怎么说

不要凭记忆判断「我好像提交过」。直接看:

git status --short
git stash list
git reflog -20

git status --short 的两列很关键:第一列是索引相对 HEAD 的状态,第二列是工作区相对索引的状态。D 出现在第一列还是第二列,直接决定你该用哪条恢复命令。下面这张表按现象排:

现象大概率成因怎么验证处置动作
状态第二列是 D(形如 D path文件被直接从磁盘删掉,索引没动git ls-files -- <path> 仍能列出该路径git restore -- <path>,从索引取回
状态第一列是 D(形如 D path跑过 git rmgit add -A,删除进了索引但没提交git diff --cached --stat 里能看到该删除git restore --staged --worktree -- <path>
文件不在了,且删除已经进了某次提交AI 自作主张完成了提交git log --all --diff-filter=D --oneline -- '*<文件名片段>*' 能列出那次提交git checkout <删除提交>^ -- <path> 单独取回
文件不见,git status 干干净净,且上一行的 --diff-filter=D 查不到删除提交git stash -u 连未跟踪文件一起收走了,或分支/工作树被切走了git stash list 有条目,git stash show --include-untracked <stash> 里能看到该路径;或 git reflog 里有 checkout 记录对应地 git stash pop,或切回原分支
文件不见,git 全无记录,内容特征串全盘搜不到未跟踪文件被删,或被 git clean -fd 清掉git log --all -S'<特征串>'grep -rn '<特征串>' . 都为空转第四节,git 这条路已经断了
文件还在但内容为空或被截断覆写,不是删除ls -l 显示大小为 0,git diff -- <path> 是全量删行按回滚处理,见前面那篇回滚文章

判别这一步别省。我见过太多人在第一行现象(索引里明明还有)的情况下,直接去翻文件系统快照,绕了两小时,而一条 git restore 就够。

三、已提交和已暂存:内容一定还在对象库里

这两种情形最好办,因为 git 已经把内容算过哈希、写成 blob 了。写进对象库的东西不会因为「文件从磁盘消失」而消失。

已提交的情形,你要做的是定位删除发生在哪一次提交。如果你连路径都记不全,用文件名片段当路径通配去查:

git log --all --diff-filter=D --oneline -- '*<文件名片段>*'

这里有个新手常犯的错:写成 git log --diff-filter=D --name-only --oneline | grep '<片段>'。它确实能匹配上,但 grep 只会留下那一行文件路径,把上面带哈希的提交行滤掉了,你拿到的是「知道删过」而不是「知道哪次删的」。用通配路径当 pathspec,输出就只有提交行,哈希直接可用。加 --all 是因为 AI 提交完可能又切了分支,删除那次提交未必在当前分支的历史里。

拿到那次提交的哈希后,取它的父提交里的版本:

git checkout <sha>^ -- path/to/file

整个目录被清了也一样,把路径换成目录即可。注意用 checkout <sha>^ -- <path> 这种带路径的形式,它只动指定路径,不会把你整个工作区拖回旧状态。

两个细节容易翻车。一是 ^ 默认指第一个父提交,如果删除落在一次合并提交上,第一父里未必是你要的那份,得试 <sha>^2 或者干脆把父提交的哈希写全。二是取回之前先确认那个版本真的有内容,别取回一个空壳还以为救回来了:

git show <sha>^:path/to/file | head -20

看到内容再动手 checkout,顺序反过来的代价是你会在一堆恢复文件里分不清哪些是真捞回来的。如果被删的东西又多又散,别一个个捞,直接在旁边开一个只读的检出点比对:

git worktree add /tmp/rescue <sha>^

这样旧版本在 /tmp/rescue 里完整摊开,你当前工作区一点没动,想拷哪个拷哪个。比 git reset --hard 安全得多,也比反复 checkout 省心。

已暂存但没提交的情形分两种。索引还完好的时候,git restore --staged --worktree -- <path> 一条就回来了。麻烦的是索引已经被后续的 resetadd 覆盖过——这时提交历史里从来没有过这个文件,但 blob 其实躺在对象库里,成了没人引用的孤儿:

git fsck --lost-found

输出里 dangling blob <sha> 那几行就是候选。这里有个容易白跑一趟的细节:同一批对象,用 --lost-found 打出来的标签是 dangling blob,而用 git fsck --unreachable 打出来的标签是 unreachable blob——很多人照着「找 dangling」这句话去跑 --unreachable,看到满屏 unreachable 就以为对不上、放弃了。两者指的是同一批没人引用的对象,认标签不如认 sha。

--lost-found 还会顺手把这些对象的内容各写一份到 .git/lost-found/ 下(blob 落在 other/ 子目录里,文件名就是 sha),可以直接打开翻,不必逐个敲命令。想在命令行里看也行:

git cat-file -p <sha> | head -40

认出来是哪个文件,从 .git/lost-found/other/ 拷回去,或者把 cat-file 的输出重定向写回原路径。这个方法的前提是垃圾回收还没跑过。所以「先备份 .git、先别执行任何可能触发 gc 的操作」不是形式主义,孤儿对象是有保留窗口的,具体多久取决于仓库配置,别赌。

顺带说一句:如果被删的是分支而不是文件,git reflog 里能找到分支尖端的提交,git branch <名字> <sha> 就长回来了。分支从来不是数据,只是一个指针。

四、只在工作区和从未入库:git 这条路是断的

这里要先把两种情形分清,很多人混着说,导致找错方向。

「只在工作区」指改动只存在于磁盘上,既没 add 也没 commit。文件本身可能是被跟踪的——那你能恢复到上一次提交的样子,但这一次的改动没了。听起来是好消息,实际取决于你上次提交是十分钟前还是三天前。

「从未入库」更狠:文件从创建到被删,git 一次都没见过它。这种情况下对象库里没有它的 blob,历史里没有它的路径,git fsck 也捞不到。git 帮不上,得换层。

按命中率从高到低试这几层:

编辑器的本地历史。 主流 IDE 都会在本地留一份最近编辑的快照,和 git 完全无关,也不受工作区被清的影响。JetBrains 系有本地历史功能,VS Code 有基于时间线的本地历史。这是未入库文件恢复率最高的一层,因为你既然编辑过它,编辑器就大概率存过。

构建产物和缓存目录。 被删的源文件如果参与过一次成功构建,它的内容可能以编译后、打包后、甚至带 sourcemap 的形式留在 distbuild.nexttarget__pycache__ 里。sourcemap 尤其值钱,很多时候能还原出接近原始的源码。这也是第一节要求你立刻停掉监听重建的原因——重建会把这些产物按新状态覆盖掉。

文件系统层的快照。 Windows 上有卷影副本和文件历史,macOS 上有 Time Machine,服务器上可能有存储层快照或定时备份。能不能用完全看你此前有没有开,事后开是没用的。

回收站。 这层我要泼冷水:AI 编程工具删文件基本都是走 shell 或文件 API,不经过桌面环境的回收站。可以顺手看一眼,但不要把希望放在这。

如果这几层全空,你确实拿不回原文了。这时唯一还有价值的动作是重建,而重建不是从零重写:被删文件在别处留下的引用就是规格说明。测试文件里的断言、其他模块的 import 和调用点、接口文档、日志里的字段名,这些拼起来往往能覆盖八成的行为要求。把这些线索整理成清单,再让 AI 按清单重写,比让它「重新实现一下」靠谱得多。这一步同时是个提醒:让 AI 动手前先框定它能碰的范围,比出事后补救便宜,控制 AI 的改动范围讲的就是这件事。

五、什么时候该停手:止损点和换路的判断

排查这类问题,最大的浪费不是走错方向,是明知道走不通还继续走。给你几条硬判断:

已经确认走的是「未入库 + 无本地历史 + 无快照」这条组合,就停。 别再去搜磁盘扇区、别装数据恢复软件。原因在机制上:固态盘的删除通知(TRIM)与磨损均衡会让「被删块」很快变成上层读不到的状态,物理位置也不受你控制;开发机又一直在写日志、缓存和构建产物。扇区级工具在机械盘时代还有点用,在这种环境里成功率没有任何保障,而重建的成本是确定的。除了「这份文件里有无法再获取的一手数据」这一种例外,重建都更划算。

排查超过一小时且没有任何一条线索命中,就停。 转去重建,同时把这一小时的排查记录留下:你已经验证了对象库没有、索引没有、本地历史没有,这些结论让重建的人不再重复怀疑。

如果被删的是生成物而不是源,直接重新生成,别恢复。 锁文件、迁移文件、构建产物、类型声明这类东西,恢复回来还要担心它和当前源码不一致,重新生成反而干净。判断标准很简单:它能不能由一条命令从别的东西推出来。

如果一个仓库在同一天里出现第二次误删,停下修流程,别再修文件。 连续出事说明你的执行环境本身没有防线,继续在这个环境里干活,第三次一定会来。该做的是把危险命令拦在执行之前,用钩子拦住不该跑的命令是成本最低的一道闸。

回滚点怎么设。 排查中你会不断改工作区,随手给自己留退路:把当前状态整体提交到一个临时分支,或者 git stash -u 存一份带未跟踪文件的快照。别嫌脏,临时分支事后删掉一条命令的事,而没留退路的排查会让你在第三条命令上后悔。

六、避坑清单:为什么会踩,怎么避

坑一:把「让 AI 清理一下无用文件」当成一句安全的话。 会踩是因为「无用」在你脑子里有边界,在执行侧没有——它看到的是一批不被引用的文件,而你的临时脚本、待办草稿、本地配置正好都不被引用。避的办法是把清理动作变成两步:先让它输出待删清单,你过一遍,再执行。永远不要让判断和删除发生在同一次动作里。

坑二:日常开发全靠工作区,提交间隔按天算。 会踩是因为未提交的时间窗就是你的风险敞口,窗口开一天,一次误删就吞掉一天。避的办法不是要求自己勤提交,是把提交变成动作的一部分:让 AI 开始改之前先建一个临时提交或 stash,这个动作交给钩子做,不依赖自觉。

坑三:.gitignore 里塞了太多东西,还往里放真东西。 会踩是因为被忽略的文件在 git status 里根本不显示,你以为工作区干净,其实那批文件从来就没进过 git,删了完全无痕。避的办法是定期跑 git status --ignored 看一眼被忽略清单,把里面「其实很重要但只在本地」的东西挪出去,或者单独做备份。本地配置、密钥、一次性数据都属于这类,密钥另有一套管法,见 API 密钥安全管理

坑四:多个会话同时在一个工作目录里干活。 会踩是因为 A 会话的清理和 B 会话的新建在同一份磁盘上竞争,A 眼里「不被引用的垃圾」正是 B 刚写出来还没接线的新文件。避的办法是物理隔离,一个会话一个工作树,别指望它们互相礼让。

坑五:出事第一反应是 git checkout .git reset --hard 会踩是因为这两条命令在「文件被改坏」的场景里确实是解药,肌肉记忆就形成了,而在「文件被删」的场景里它们是毒药——会把索引和磁盘上仅存的副本一起抹平。避的办法是把判断顺序固化:先跑 git status --short 看现象,再决定命令,不看状态不敲带 . 的命令。

坑六:把「云端/后台执行」当成更安全的选项。 会踩是因为远端执行环境出事时你连磁盘都摸不到,本地历史那一层直接没了。另外提一句选型上的现实:部分海外 AI 编程工具官方并未面向中国大陆提供服务,也不支持直连,市面上存在第三方中转但可靠性和数据流向都不透明,这里不做任何背书。把重要仓库放在自己能触达文件系统的地方,出事时你的选择多得多。

坑七:恢复完就收工,不验证。 会踩是因为 git checkout <sha>^ -- <path> 取回的是那个时间点的版本,它和当前代码可能已经不兼容了。避的办法是恢复后必须跑一次完整构建和测试,确认取回的是「能用的版本」而不是「某个历史版本」。

收束

这类事故的处理质量,取决于你有多快把「AI 干了什么」这个问题换成「文件在哪一层」这个问题。前者无法收敛,后者三条命令就有答案。

给你一份自检清单,出事时照着走:

  1. 中断 AI 会话,停掉所有自动写盘的进程。
  2. cp -a .git 备份一份,再敲任何 git 命令。
  3. git status --shortgit stash listgit reflog,用第二节的表定位情形。
  4. 对象库里有的,用带路径的 checkoutrestore 取回,量大就开一个只读 worktree 比对。
  5. 索引被覆盖的,趁 gc 没跑,用 git fsck --lost-found 捞孤儿 blob,去 .git/lost-found/other/ 里认内容。
  6. git 无记录的,依次翻编辑器本地历史、构建产物与 sourcemap、文件系统快照。
  7. 三层都空就停手,改走重建,把测试和调用点整理成规格清单再动手。
  8. 恢复后跑一次完整构建和测试,确认版本可用。
  9. 事后只补一件事:把危险命令拦在执行之前,并把「开工先留快照」变成自动动作。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。