缓慢的觉醒
你已经在数据科学项目上工作了一段时间。有二十个 notebook,一些图片,还有那种三个月前看起来不错的典型文件夹结构。
你执行 git status 想看看改了什么,然后… 等待。继续等待。在等待的时候你开始怀疑是不是电脑卡死了还是在沉思什么。
剧透:它不是在沉思。它在痛苦。
问题有名有姓
Git 不慢。你的仓库慢。
当你执行 git status 时,git 需要做两件看起来简单但实际很复杂的事情:
- 扫描整个文件树 看看有什么变化
- 比较每个文件 与保存的版本
在普通仓库里这是瞬间完成的。但 Jupyter notebook 是伪装成文档的 JSON。而且不是普通的 JSON:是包含代码、输出、base64 编码图片、内核元数据,以及设计这个格式的人能想到的所有东西的 JSON。
一个带几张图表的"小" notebook 可能有几兆大小。乘以二十个 notebook,你就有了一个每次看它都在呻吟的仓库。
如果你还重命名了文件夹… git 会理解为"你删除了 50 个文件然后创建了 50 个新文件"。保证有趣。
解决方案 1:给你的仓库配个门卫
第一个解决方案优雅得让人恨不得早点知道。
叫做 FSMonitor,工作原理是:不让 git 每次都扫描整个仓库,而是操作系统告诉它哪些文件变了。
用人话说:就像在门口有个门卫告诉你"只有小王进来了",而不用每次都检查整个宾客名单。
激活方法:
| |
搞定。就这样。
激活后第一次执行 git status 还是会花同样(或更多)时间,因为要初始化缓存。但从第二次开始… 魔法。
在我那个 400+ 文件的仓库里,git status 从 2-3 秒变成了瞬间完成。不是"快"。是瞬间。
在所有平台都能用吗?
- macOS: 可以,使用 FSEvents
- Linux: 可以,使用 inotify(如果内核支持的话,肯定支持)
- Windows: 可以,使用 ReadDirectoryChangesW
总之,可以。
解决方案 2:保存食谱,不保存菜品照片
FSMonitor 加速了扫描,但还有另一个问题:notebook 仍然很大。每次你执行一个单元格并保存时,即使代码相同,文件也会变化因为输出不同。
这意味着:
- 无法阅读的 diff(谁想看图片的 base64?)
- 膨胀的提交
- 地狱般的合并
解决方案叫做 nbstripout,正如其名所示:在提交前剥离 notebook 的输出。
就像保存食谱而不保存成品照片。代码在那里,需要时重新生成结果。
用 uv 安装:
| |
或用 pip,如果你是老派的话:
| |
--install 自动配置 git 使用 nbstripout 作为过滤器。从那时起,当你提交 notebook 时,会保存为无输出版本。
验证是否工作:
| |
如果返回类似 nbstripout 的内容,就对了。
如果我需要保存输出怎么办?
好问题。有时你想提交已执行的 notebook,比如用于文档或让别人无需执行就能查看。
nbstripout 让你标记特定 notebook 跳过过滤器:
| |
或者在 .gitattributes 中配置例外。但总的来说,我的建议是:不要保存输出。Notebook 是代码,不是最终文档。
荣誉提及:git-lfs
如果除了 notebook 你还有频繁变化的图片或视频,存在 git-lfs(Large File Storage)。
想法很简单:不在仓库里保存大型二进制文件,而是保存指针,真实文件存在单独的服务器上。
就像把行李存在机场寄存处,只带着取票。
| |
为什么只是荣誉提及而不是主要解决方案?因为增加了复杂性。你需要 LFS 服务器(GitHub 和 GitLab 提供,但有限制),如果有人在没安装 git-lfs 的情况下克隆仓库,得到的是指针而不是文件。
对于 notebook,FSMonitor + nbstripout 通常足够了。git-lfs 是用于真正的数据集或多媒体资源的。
制胜组合
我在任何有 notebook 的仓库中的配置:
| |
四个命令,你的仓库从关节炎老人变成奥运选手。
验证一切正常
| |
如果两个都返回预期结果,就完成了。享受你瞬间完成的 git status 和可读的 diff 吧。
总结
下次有人告诉你"git 很慢",你就知道该回答什么了:git 不慢,是你的仓库配置不当。
用 FSMonitor 让操作系统干脏活累活。用 nbstripout 让 notebook 不像背着铅块一样重。
如果做完这些还是慢… 也许该考虑是否真的需要在同一个仓库里放 200 个 notebook。但那是另一个故事了。