isomorphic-git 实现 sparse checkout & commit
Posted | stdout
去年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:
- 核心目的是避免 clone 整个repo!!
- 注意在 cloudflare worker 上跑。和nodejs有点差别。
- git 命令在 cloudflare worker 是不能用的!所以引入了 isomorphic-git 这个库纯js实现git。不懂就去翻它的源码
- 在本地调试可以用 git 看看问题,但是实际操作肯定是要用 isomorphic-git 通过 http 进行的
- 注意 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的时候只更新必要的部分
还尝试过一些其他方案:
- 用 git.updateIndex:想通过 index 来管理文件,但 isomorphic-git 不支持
git.readIndex,也没法清空 index。 - 用 git.resetIndex:想重置 index,但这个函数需要 filepath 参数,没法清空整个 index。
- 手动管理 index 对象:想自己构建 index 对象传给
git.writeTree,但git.writeTree不接受 index 参数。 - 用 git.add:想用
git.add来添加文件,但git.add需要文件系统里的文件,而 memfs 里没有这个文件。
一整套下来感觉人都给整神了。。。
btw 为了方便测试,找了一圈,发现国内可以免费建 repo 拉扯测试的是腾讯的 https://git.code.tencent.com/。当然这玩意只能搞私仓。毕竟没要求你实名算比较方便的了
Comments