一台云端 Mac 连续承担数周构建后,git fetch 仍可能很快,但分支切换、合并基线计算和对象遍历会逐渐变慢。常见原因不是网络,而是长驻仓库累积了大量松散对象、零散 pack 文件、失效远端引用,以及多次中断留下的维护锁。直接删除 .git 重新克隆虽然简单,却会丢掉已有对象缓存,并把问题推迟到下一轮。
更稳妥的做法,是把仓库维护当成独立作业:先量化,再排他执行增量任务,最后验证对象连通性。下面的流程适合固定工作区,也适合使用多个 worktree 的构建节点。
先建立可比较的仓库基线
不要先看目录总大小。Git 对象的数量、组织方式和可达性更能说明问题。进入仓库后记录以下结果:
git count-objects -vH
git rev-parse --git-dir
git rev-parse --git-common-dir
git for-each-ref --format='%(refname)' refs/remotes | wc -l
find "$(git rev-parse --git-common-dir)/objects/pack" -name '*.pack' | wc -l
count 较高表示松散对象较多,packs 持续增加则说明历史任务不断产生小 pack。使用 worktree 时,维护目标应以 git rev-parse --git-common-dir 返回的公共目录为准,不能只检查当前 worktree 下的 .git 指针文件。
建议把每次维护前后的 count、in-pack、packs、size-pack 写入普通文本日志。不要仅以磁盘占用下降判断成功:提交图更新可能几乎不减少空间,却能改善历史遍历和合并基线查询。
选择增量任务而不是激进清理
Git 的维护任务可以拆开运行。长驻构建节点通常先启用提交图、松散对象整理和增量重打包:
git config maintenance.strategy incremental
git maintenance run \
--task=commit-graph \
--task=loose-objects \
--task=incremental-repack
提交图用于加速历史可达性查询;多包索引让 Git 不必逐个扫描所有 pack;增量重打包则逐步合并对象,避免一次完整回收占用过多时间和临时空间。
不要把
git gc --prune=now当作日常定时任务。立即清除会移除恢复缓冲期,也可能与仍持有旧对象引用的并发进程冲突。
若仓库刚经历大规模分支删除,仍应先保留默认宽限期。只有确认没有构建、检出、变基、子模块更新或归档任务运行时,才考虑更深入的回收。
远端引用要单独治理
长期不清理的远端跟踪分支会扩大遍历范围。先预览,再执行清理:
git remote prune origin --dry-run
git remote prune origin
这只处理远端跟踪引用,不会代替对象回收,也不会删除本地开发分支。构建脚本若依赖已经从远端删除的引用,应先修正脚本,而不是永久保留失效引用。
用原子锁隔离构建与维护
维护任务与构建任务必须共享同一把仓库级锁。mkdir 在同一文件系统内具有原子性,适合作为简单锁原语:
common_dir="$(git rev-parse --git-common-dir)"
lock_dir="$common_dir/maintenance.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "maintenance skipped: repository is busy"
exit 0
fi
trap 'rmdir "$lock_dir"' EXIT INT TERM
git maintenance run \
--task=commit-graph \
--task=loose-objects \
--task=incremental-repack
构建入口也应检查或持有同一锁,否则维护脚本单方面加锁没有意义。多个 worktree 不能各自在工作目录下创建锁,因为它们共享对象库。
锁目录里可以写入进程号和启动时间,供人工排查,但不要看见旧时间就直接删除。先用 ps 确认进程是否存在,再检查是否有 git pack-objects、git index-pack 或 git maintenance 正在运行。
验证索引与对象连通性
维护命令退出为零,只代表命令完成,不代表仓库一定适合继续投入构建。至少执行三类验证:
git commit-graph verify
git multi-pack-index verify
git fsck --connectivity-only
检查项可以按下表落入流水线:
若验证失败,不要先执行更激进的回收。应保留现场,包括命令输出、Git 版本、公共对象目录路径和最近一次成功构建的提交号。对象缺失时,可从可信远端重新抓取;若仍无法恢复,再隔离旧目录并创建新克隆,而不是在原目录上反复试删。
把维护接入节点生命周期
维护频率应由仓库变化量决定,而不是所有项目共用固定分钟数。高频提交的单仓库可以每天在最后一个任务结束后运行增量维护;低频项目则按松散对象和 pack 数量趋势触发。执行前还要确认可用磁盘空间,因为重打包期间新旧 pack 会短暂并存。
在 VMOak 的云端 Mac 上,建议将维护脚本放在仓库外的只读运维目录,由调度任务传入目标路径。脚本不要读取项目分支中的同名文件,避免普通代码变更改写维护逻辑。若节点承载多个仓库,应逐库加锁、逐库验证,单个仓库失败不应触发其他仓库的清理。
最终验收不只看“空间变小”,而要确认日常操作没有退化:执行一次远端更新、目标提交检出、合并基线查询和只读构建准备,再比较维护前后的日志。这样,Git 维护才是一项可审计的节点操作,而不是遇到磁盘压力时临时运行的删除命令。
常见问题
可以在构建任务运行时执行 Git 维护吗?
不建议让回收任务与检出、变基或打包并发执行。至少使用仓库级原子锁,并把增量维护安排在工作区空闲阶段。
为什么不直接定期运行 git gc --prune=now?
立即清理会缩短误删恢复窗口,并放大并发进程仍引用旧对象时的风险。长驻节点更适合增量重打包,并保留默认宽限期。
维护完成后应检查哪些内容?
至少验证提交图、多包索引和对象连通性,同时记录包文件数量、松散对象数量与磁盘占用,确认维护结果可解释。
为构建、自动化或远程创作选择云端 Mac
比较 M4 16GB 与 M4 Pro 64GB 两档配置,确认租用周期和可订节点后再提交订单。