一次因误用 git filter-repo 导致的代码覆盖事件复盘

2025年11月15日 | Ruichen Zhou
GitGit-Filter-RepoVSCode代码恢复Git-Workflow科研

我决定开始写博客, 是为了对抗遗忘。

在科研和开发中, 会遇到大量的工具和命令。许多不常用的工具(比如 git filter-repo)平时很少接触,在错误的时间、错误的场景下使用,就可能付出代价。

这篇博客记录一次因误用命令导致本地所有未提交代码被覆盖的事故, 把”学费”变成”资产”。它不仅是写给未来的自己看的备忘录, 也希望能帮助每一个读到它的人, 在使用会”改历史”、“动全局”的工具前, 多一份审慎。


一次因误用 git filter-repo 导致的代码覆盖事件复盘

这是一篇关于错误使用 Git 工具的记录。

起点很简单:我想把整理过的代码 push 到 GitHub 做一次备份; 结果因为在大量未提交改动的情况下使用了 git filter-repo --force,导致本地代码被旧版本覆盖。 最终,我依靠编辑器的本地历史记录才把代码一点点恢复回来。

这篇文章冷静地回顾整个过程,重点关注两个问题:

  • 在什么前提下,像 git filter-repo 这样的工具才是”安全”的?
  • 过度依赖 AI 给出的命令,会带来什么隐性风险?

一、开发环境与操作背景

设备与环境:

  • 代码实际存放在中国的 Linux 工作站上(与其他文章中的设备 A 一致)
  • 我使用两台本地电脑通过 VSCode Remote SSH 连接到工作站:
    • Windows 电脑:位于德国学生宿舍,长期在线
    • MacBook Air:便携设备,在宿舍和工位之间通勤使用
  • 所有 Git 仓库都在工作站上,Git 命令也都在工作站上执行

也就是说:

工作站是唯一的代码真源,Windows 电脑和 MacBook 只是不同的远程终端。

1.1 导火索:历史中的大文件导致 push 失败

在一次重构完成后,我准备把代码推送到 GitHub。 执行 git push 后,GitHub 返回错误,大意是:

File model_visualizations/transformer_unet.onnx is 296.16 MB
this exceeds GitHub's file size limit of 100MB

这个 .onnx 文件是历史遗留的模型文件,虽然在当前工作区已经删除,但它仍存在于之前的某个 commit 中。 为了让仓库可以顺利推送到 GitHub,我需要从 Git 历史中彻底移除这个文件


二、出事前的工作区状态:大量重构但全部未提交

在尝试解决大文件问题之前,工作区的状态大致是这样(抽象化描述):

  • 大量 modified 文件
    • train.py:训练流程重构、日志与监控调整
    • data_loader.py:数据管线整理、数据格式兼容修改
    • 其他核心模块:loss 计算、模型接口、配置加载等
  • 大量 deleted 文件
    • 各类老的 archive/ 目录
    • 旧的 backup_xxx/ 目录
    • 某些已经弃用的可视化与脚本
  • 大量 untracked 文件
    • 新写的数据预处理脚本(如 scripts/1_prepare_data.py
    • 新增的区域评估脚本、绘图脚本
    • 用于论文整理的 CSV 结果与笔记文件
    • 若干新的 Notebook

简单来说:

我已经对项目进行了一个比较彻底的重构和清理,但全部停留在工作区,没有进入任何一次 commit。

这点是后续问题的根源。


三、解决大文件时走错了路:git filter-repo --force

为了解决 GitHub 拒绝大文件的问题,我选择了使用 git filter-repo 这个工具。

3.1 依赖 AI 帮忙给方案

当时我没有先阅读 git filter-repo 的文档,而是直接向一个 AI 助手提问:“如何从 Git 历史中移除一个大文件,使仓库能推送到 GitHub?”

AI 给出的大致思路是:

  • 使用 git filter-repo --path <文件路径> --invert-paths,从历史中移除该路径
  • 如果工具提示需要 --force,可以添加这个参数继续执行

这个方案本身并不是完全错误的。 问题在于:

我几乎完全照抄了这些命令,没有结合当前仓库状态(尤其是未提交改动)的实际情况进行判断。

3.2 第一次运行被阻止

在工作站上执行了类似命令:

git filter-repo --path model_visualizations/transformer_unet.onnx --invert-paths

工具返回了一个警告,大意是:

这是破坏性操作,它检测到当前仓库并不是一个”全新克隆”的仓库,因此拒绝执行,并提示如果确认要执行,需要加入 --force

这是一个明确的安全提示。 但当时注意力几乎全部放在”尽快让 push 通过”这件事上,加上对 AI 建议的依赖,继续执行了:

git filter-repo --force --path model_visualizations/transformer_unet.onnx --invert-paths

这一刻,问题正式发生。


四、命令执行后的现象:所有未提交工作被覆盖

命令执行后,我做的第一件事是:

git status

结果显示异常:

  • 之前被我删除的 archive/ 等目录又重新出现在工作区
  • 许多文件回到了之前的状态
  • train.pydata_loader.py 等文件的重构内容不见了
  • 新增的脚本、实验结果文件也不再出现在未追踪列表中

这说明:

工作区已经被“强制替换”为某个旧 commit 对应的文件内容。 换句话说,所有未提交的修改被覆盖了

从 Git 的角度看,这是合理的行为:

  1. git filter-repo 的核心作用是:

    在历史层面重写 commit,并生成一套新的历史

  2. 为了让工作区与新历史保持一致,它会在操作结束后:

    • 将 HEAD 重定位到重写后的最新 commit
    • 再将工作区同步到该 commit 的文件内容
  3. 由于这些改动之前从未进入 commit,自然不会出现在重写后的历史里,也不会被保留。

问题不在工具,而在于:

  • 在一个有大量未提交变更的工作区里直接对历史进行破坏性操作;
  • 对命令的实际行为没有足够理解;
  • 过于依赖 AI 提供的“可运行命令”,而缺乏独立的安全检查。

五、恢复过程:借助编辑器的本地历史记录

从 Git 的角度,这些未提交的变更已经不在版本历史中,因此无法通过常规 Git 操作恢复。 不过,开发环境中还存在另一个“历史来源”:编辑器的 Local History。

5.1 多终端开发带来的冗余机制

因为我使用了两台本地电脑连接工作站编码:

  • Windows 电脑和 MacBook 上的 VSCode 都会对本地编辑过的文件做时间线记录(Local History)
  • 这些记录存储在各自本地环境中,而不是工作站上
  • git filter-repo 只作用于工作站上的 Git 仓库,不会影响本地编辑器的缓存

也就是说:

虽然工作站上的文件已被覆盖,但 Windows 电脑和 MacBook 上 VSCode 对文件的历史快照依然存在。

5.2 通过 VSCode Timeline 恢复文件

主要恢复过程(在 Windows 电脑上):

  1. 通过 Remote SSH 连接到工作站;
  2. 打开受影响的项目目录;
  3. 依次打开关键文件(例如 train.py, data_loader.py 等);
  4. 在 VSCode 侧边栏中打开 Timeline / Local History 面板;
  5. 查看最近几次保存记录,逐条比对内容;
  6. 找到符合”事故发生前”状态的版本,选择恢复该版本内容;
  7. 保存文件,使恢复内容再次写回工作站。

对每个关键文件,都重复这一步骤。

补充恢复(在 MacBook 上):

对于一些只在 MacBook 上修改过的文件(例如项目的 README.md),在 MacBook 上重复类似的操作。

这个过程是逐文件、手动完成的,但能有效避免误恢复过旧的版本。

5.2 在电脑 B 上通过 VSCode Timeline 恢复文件

在电脑 B 上的 VSCode 中:

  1. 通过 Remote SSH 连接到同一台服务器;
  2. 打开受影响的项目目录;
  3. 依次打开关键文件(例如 train.py, data_loader.py 等);
  4. 在 VSCode 侧边栏中打开 Timeline / Local History 面板;
  5. 查看最近几次保存记录,逐条比对内容;
  6. 找到符合“事故发生前”状态的版本,选择恢复该版本内容;
  7. 保存文件,使恢复内容再次写回服务器。

对每个关键文件,都重复这一步骤。 对于一些只在电脑 A 上修改过的文件,则在电脑 A 上重复类似的操作。

这个过程是逐文件、手动完成的,能有效避免误恢复过旧的版本。

5.3 恢复后的 Git 状态与完整提交

当主要文件都从 Local History 恢复完毕后,在工作站上检查:

git status

此时可以看到:

  • 被恢复的文件显示为 modified
  • 已删除的目录在文件系统层面被手动删除
  • 新增脚本和实验文件重新处于 untracked 状态

确认状态符合预期后,做了一个完整的提交:

git add -A
git commit -m "Restore project state after mistaken filter-repo via editor local history"

这个 commit 将恢复后的状态正式写入 Git 历史,使仓库重新回到可控状态。


六、事后反思:工具不是问题,使用方式才是

这次事件带来的主要教训有几点。

6.1 过度依赖 AI 建议

在处理 Git 这种会影响历史和工作区安全的工具时,仅仅”复制 AI 的命令”是不够的。 关键问题不在于 AI 的回答本身,而在于:

  • 没有主动确认命令对当前仓库状态的影响
  • 没有在命令前先判断:“现在的工作区是否干净?”
  • 把对工具行为的理解”外包”给了一个不掌握完整上下文的助手

6.2 未提交改动的风险

所有停留在工作区的内容,本质上都处于”没有备份”的状态。 对这些内容来说:

  • 编辑器崩溃、误删、误操作 Git 命令,都可能导致直接丢失
  • 一次 git add -A && git commit 就可以显著降低这个风险

七、后续改进的工作流

针对这次教训,我对自己的工作流做了几项约束。

7.1 任何破坏性操作前,先做一次快照式提交

包括但不限于:

  • git filter-repo
  • git rebase
  • git reset --hard
  • 大规模目录重构 / 重命名

习惯性执行:

git add -A
git commit -m "WIP: snapshot before history rewrite"

即便这个 commit 不够“干净”,也比完全丢失要安全得多。


7.2 确保在“工作区干净”时才执行历史重写类命令

今后对类似命令加上一条硬性规则:

  • 在运行之前,先明确检查:
git status
# 要看到:nothing to commit, working tree clean

如果不是这行输出,就先整理、提交、或明确地放弃当前工作区改动。


7.3 为科研开发建立分支 + PR 的习惯

将主分支(例如 main 或稳定训练分支)视为“相对稳定的基线”:

  • 新实验、新重构、新论文相关工作,都从主分支开新分支:

    git switch main
    git pull
    git switch -c feat/new-experiment
  • 在独立分支上完成工作、提交、推送;

  • 通过 Pull Request 合并回主分支,顺便把这次实验/修改的背景与目的写在 PR 描述里。

这种方式,比在主分支上直接多次叠加修改,要更可控,也更适合作为科研日志。


八、总结

这次事件本质上是使用习惯与思维方式的问题,而不是某个工具的问题。

关键教训:

  • 在未提交状态下执行历史重写命令,是主要错误
  • 过度依赖 AI 建议,而没有检查命令语义,是另一个错误
  • 编辑器的 Local History 在关键时刻提供了补救机会,这是运气,也是一种冗余机制的体现

这篇记录主要是写给未来的自己: 在使用任何会”改历史""动全局”的工具时,多花几分钟理解,多做一次提交,远比事后恢复要轻松得多。

如果这篇复盘能让别人在用 --force 之前多看一眼 git status,那这次教训也算有一点价值。