isomorphic-git 实现 sparse checkout & commit

去年9月手搓了套blog评论系统 - req4cmt,可能是全世界很少见通过 git repo 文件本身存储评论内容,而不是 github issue。

git repo 文件 append 内容涉及到一个性能问题:repo作为整体,也就是历史所有全体评论,被 fetch, commit , push 的成本太高。如果只能修改其中的一个文件就好了。这就是 sparse checkout。git底层早就支持了,git 命令在2020年之后2.25.0+支持,但是 Cloudflare Worker 没法执行命令,也没文件系统,于是召唤AI跟我一起折腾。

大概用的这个 prompt:

  1. 核心目的是避免 clone 整个repo!!
  2. 注意在 cloudflare worker 上跑。和nodejs有点差别。
  3. git 命令在 cloudflare worker 是不能用的!所以引入了 isomorphic-git 这个库纯js实现git。不懂就去翻它的源码
  4. 在本地调试可以用 git 看看问题,但是实际操作肯定是要用 isomorphic-git 通过 http 进行的
  5. 注意 cloudflare worker 是没有文件系统的,所以引入 memfs

第一版 直接用 await git.clone({fs, dir, url: GIT_URL, depth: 1, singleBranch: true});, 改成 init + fetch。只拿tree和需要的blob

写完一跑,报错:"git.hashObject is not a function"。AI一看扭头就去撸了个 SHA-1 准备造个git轮子。。。赶紧停下来调教,让它仔细读isomorphic-git源码,让用 git.writeBlob

最折磨人的坑:文件覆盖问题。去年用 gemini-2.5,trae 搞不定,这次也是反复改了好几个方案才解决。现象是修改的那个文件提交,仓库里就只剩下修改的那一个文件,其他文件全没了。git.writeTree给我整不会了。这次让AI反复尝试了很多方案,最后有希望是先删除旧的,再添加新的

const oldTree = await git.readTree(...);
const filteredEntries = oldTree.tree.filter(e => e.path !== "test.txt");
const newTreeSha = await git.writeTree(...);

这个方案对根目录文件有效,但子目录文件还是不行!因为 git.readTree 只能读根目录,subdir/test.txt 的 entry 在根目录的 tree 里是 subdir 这个 tree entry,不是文件本身。

没办法,搞了个巨蛋痛的10多行递归读取整个 tree,但是git.writeTree 不接受带斜杠的路径,报错:"The filepath 'subdir/test.txt' contains unsafe character sequences"。

然后又得笨办法构建嵌套的 tree。这个方案终于成功了!但有个问题:每次都要递归读取整个 tree,然后重建整个 tree,效率太低了。特别是大仓库,会很慢。所以又不得不在解析path的时候只更新必要的部分

还尝试过一些其他方案:

  1. 用 git.updateIndex:想通过 index 来管理文件,但 isomorphic-git 不支持 git.readIndex,也没法清空 index。
  2. 用 git.resetIndex:想重置 index,但这个函数需要 filepath 参数,没法清空整个 index。
  3. 手动管理 index 对象:想自己构建 index 对象传给 git.writeTree,但 git.writeTree 不接受 index 参数。
  4. 用 git.add:想用 git.add 来添加文件,但 git.add 需要文件系统里的文件,而 memfs 里没有这个文件。

一整套下来感觉人都给整神了。。。

btw 为了方便测试,找了一圈,发现国内可以免费建 repo 拉扯测试的是腾讯的 https://git.code.tencent.com/。当然这玩意只能搞私仓。毕竟没要求你实名算比较方便的了

Comments