Git 仓库 8.7GB,clone 一次 42 分钟——我花了两个周末把它砍到 1.4GB

🔑 关键词:git仓库瘦身,git clone太慢,git filter-repo,git浅克隆,trunk based development

📖 摘要:一个真实的小团队仓库抢救记录:8.7GB 的 Unity 项目仓库怎么从 42 分钟 clone 降到 90 秒,附完整命令。顺带聊聊为什么我一直觉得「该用 Git Flow 还是 Trunk Based」是个伪问题——小团队真正被拖垮的不是分支模型,是仓库体积和提交粒度。

起因:42 分 17 秒

图片

去年三月,我一个做工业检测设备的朋友半夜发微信找我。他们新招的应届生第一天上班,clone 代码仓库,去楼下吃了碗面回来还没好。我本来想调侃一句「你们是不是把 node_modules 提交上去了」,第二天他把 git count-objects -vH 的输出截图甩过来,size-pack 那一栏写着 8.11 GiB。我笑不出来了。

仓库 2013 年建的,24000 多个 commit,满打满算 5 个开发加 3 个美术。我让他在一台干净机器上掐表完整克隆一次,42 分 17 秒,办公网 100Mbps。也就是说这 8 个人每天都在忍这件事,忍了至少五年,都以为「Git 就是这样,慢是正常的」。

先说结论,省得你往下翻:瘦身之后仓库 1.4GB,完整 clone 90 秒。但我踩的坑比省下的时间更值得说,尤其是有一个同事丢了本地改动,到现在还在群里阴阳我。

先搞清楚大文件躺在哪儿

我没有一上来就 filter-repo,先摸底。这一步别省,不然你根本不知道砍什么。

图片

git count-objects -vH
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | sort -k3 -nr | head -20

前十名加起来 5.4GB。三个 2017 年的 .psd 源文件,每个 400MB 以上,对应的功能 2019 年就下线了;一堆 .fbx 模型的历史版本;还有 Unity 的 Library 文件夹被误提交过好几个版本,光那个就 2.4GB。这些字节全都老老实实躺在 .git 目录里,每一次 clone、每一次 CI 拉代码,都要陪着一起搬。

顺手记一下:git-sizer 这个工具也好用,它会把「最大的 blob」「历史里最深的路径」直接列出来,省得自己写 awk。我当时是后来才知道的。

动手:filter-repo 大概 8 分钟,filter-branch 我跑到一半放弃了

工具上我没纠结。git filter-branch 是老牌方案,我先拿它试了一次,跑了 2 小时 50 分还没结束,磁盘 IO 一直满,我直接 Ctrl-C 了。git filter-repo 是 Python 写的,得先 pip install git-filter-repo,同样的仓库干完活大概 8 分钟——差了两个数量级,没什么可比的。BFG 也行,但要 Java 环境,我们那台打包机没装。

流程大概是:

图片

  1. 提前两天通知所有人,约定一个时间点全员停手,谁也别 push,有未提交的改动先自己 commit 出来
  2. 在 GitLab 上开一个只读镜像仓库做备份,别嫌麻烦,这一步是保命的
  3. git clone --mirror 拉一份裸仓库到本地
  4. 执行过滤:git filter-repo --path-glob '*.psd' --invert-paths,把 .psd、.fbx、Library/、Temp/ 分开跑了几轮
  5. 重新设置 remote,force push 回服务端
  6. 所有人重新 clone,注意是重新 clone,不是 pull

第 6 条我强调了三遍,还是有人直接 rm -rf 了老目录才去 clone,结果他本地一个 300 多行的设备配置文件改动没了,那东西没 commit 过。第二天他被我从工位上薅起来听我念了十分钟。所以:清理历史前,务必让每个人 git stash listgit status 各自检查一遍,或者干脆拷一份目录出来。

清理完 1.4GB。顺手在每台机器上开了两个开关,小仓库也能有感知:

git config --global core.fsmonitor true
git config --global core.untrackedCache true

Windows 那几位额外加了 core.autocrlf input,之前 .png 因为换行符转换被反复标成已修改的事也一起解决了。

图片

关于浅克隆和 partial clone:止痛药,不是药

瘦身之前我也试过止疼。同样 100Mbps、同样那台机器,几种方案的实测数字放在一起挺有意思:

方式 首次 clone 耗时 代价
完整 clone(清理前) 42 分 17 秒
--depth=1 --single-branch 3 分 20 秒 blame、bisect、log --follow 基本残废
--filter=blob:none(partial clone) 4 分左右 按需拉 blob,CI 上偶发卡顿
完整 clone(清理后) 1 分 30 秒

我想说的是第二行和第三行。很多人一遇到慢就上浅克隆,觉得问题解决了,其实你丢掉的是这个工具最有价值的部分。我们团队去年靠 git bisect 定位过一个偶发三个月前的时序 bug,20 个 commit 二分,跑四轮就锁定了。浅克隆的仓库做这件事就是废的,blame 出来的作者也全是错的。partial clone 好一些,但 GitLab 要 14.0 以上,而且美术同学在 500KB 的网络下点开一个模型文件要转圈半分钟。

所以我的态度是:浅克隆是应急,不是方案。真正的病根是仓库里躺着没人再看的字节,你要把它割掉,而不是每次路过都绕开。

图片

顺带说说 LFS,我们试了,放弃了

美术的大文件总得有地方放。我们试过 Git LFS,用了一个月就拆了——不是因为不会用,是因为我们那台自建 GitLab 的带宽被打爆了。美术同事每天重新 clone 工作区是常态,LFS 每次都要把 3-4GB 的模型全拉一遍,路由器晚上直接冒烟。后来改成网盘按版本号分发美术源文件,仓库里只留 .meta 和占位说明,反而消停了。

如果你上 GitHub 的 LFS,也得留意配额:免费额度是 1GB 存储 + 每月 1GB 流量,超了要买数据包,美术资源这个量级很容易一个月就烧掉几百块。这不是说 LFS 不行,是说它适合「大文件数量少、改动频率低」的场景,天天改的模型源文件不适合。

我的观点:Git Flow 还是 Trunk Based,在小团队里是个伪问题

清理那阵子,朋友反复问我一个问题是「我们是不是该上 Git Flow 才显得规范一点」。我当时的回答有点冲:你们 5 个人,一天合不到 2 次代码,上 Git Flow 就是给自己造 KPI。

我自己在 6 人团队待过 Git Flow 的年月,develop 分支常年落后 master 三百多个 commit,每次 release 合并要处理 17 个冲突文件,光理顺就得半天。后来换 trunk-based:主干 + 活不过 24 小时的短命分支 + feature flag,合并冲突掉到每次两三个文件,部署从两周一次变成一天两三次。这个变化不是分支图画得漂亮带来的,是「每个提交只干一件事」带来的。

图片

我的独立观点是:小团队 90% 的版本控制痛苦,来自仓库体积和提交粒度,跟分支模型几乎无关。你去参加十场关于分支策略的讨论,回来还是会在一个 400MB 的 .psd 上卡住。提交信息我们只定了一条规矩——标题写「做了什么」,正文写「为什么」,绝对不写「怎么做的」,因为怎么做代码里有。半年下来,git log 变得能读了,这比任何流程图都值。

另外提一个我最近在试的东西:Jujutsu(命令行是 jj),Git 兼容的后端,工作方式跟 Git 差别挺大,撤销和自动 rebase 用起来是真的舒服,我装的那个 0.2x 版本已经很稳了。但我不敢在团队里推,因为除了你没人会用,等于给所有人加一道学习成本。工具选型这件事,团队里最慢的那个人才是你的真实速度。

最后,一个可以照抄的检查清单

  • 先跑 git count-objects -vHgit-sizer,搞清楚大文件在哪,别盲砍
  • 清理前开只读镜像备份,全员停手,检查 git status
  • git filter-repo,别碰 filter-branch
  • 加 .gitignore:Unity 的 Library/、Temp/、Obj/,Python 的 pycache,前端的 node_modules 和 dist
  • 加 .gitattributes,图片标 binary,文本统一换行符
  • 清理后所有人重新 clone,不能 pull
  • 大文件走独立分发通道,别硬塞进版本库
  • 主干开发,分支活不过一天,提交粒度小到能一句话说清

那台机器现在 clone 一次 90 秒。朋友后来请我吃了顿饭,但他那位丢了配置的同事到现在见我还点头不说话。

🏷️ 标签: