我所理解的 Git 最佳实践(9):先量清仓库体积,再决定怎么迁移
Git 仓库太大时,先看是哪一步受影响。当前版本就有大文件,先处理这些文件的存储方式。当前版本不大,克隆却慢,就查旧历史和要迁移的引用。平时能正常使用,只在一次迁仓 push 时超限,则先确认平台限制的是单次传输、单个文件,还是整个仓库。
| 量出来的问题 | 优先处理 |
|---|---|
| 目标快照太大 | 找出最大的文件。可重新生成的产物移出 Git,必须交付的二进制按用途放到 LFS、制品库或发布附件 |
| 历史远大于当前快照 | 查旧大文件仍由哪些分支和标签保留。确需缩小已有仓库时,在备份后针对对应路径重写历史,并协调旧副本 |
| 只是本机目录太大 | 分开检查依赖、缓存、LFS 媒体和本机引用。先判断哪些数据需要跟项目一起迁 |
| 只卡单次 push 上限 | 确认每个文件本身合规,再沿提交祖先链分批推送 |
先决定要量的是哪一份数据
仓库目录在磁盘上有多大,只能回答本机占了多少空间。工作树里可能还有依赖、缓存和生成产物。.git 里则可能有本机独有的引用、reflog 和未回收对象。普通 Git push 会根据双方已知的历史组织要发送的对象,实际 pack 大小还与远端已有对象、协商和打包参数有关。
| 想回答的问题 | 适合量什么 |
|---|---|
| 当前文件本身有多大 | 目标提交的文件树及其 blob。评估迁移时可再看这棵树的压缩 pack |
| 历史压平大致能省多少 | 用相同方法比较所选引用的可达对象 pack 和目标 tree 的 pack |
| 本机 Git 存储用了多少 | 实际 Git 对象目录及本地引用,另计工作树、LFS 缓存等数据 |
| 这次 push 是否过得去 | 本次推送的 pack、单文件限制、权限与服务端规则 |
| 托管账号是否还有空间 | 平台的仓库计量与账号配额,Git LFS 按对应产品规则核算 |
先确认仓库顶层、推送目标和选定的提交。若关心的是迁移 main,就测 main 能到达的历史。我在 2026 年 10 月 26 日量过一个带本地检查点的工作流仓库,Git 目录约 3.30 GB,所有本地引用重打包约 2.98 GB,单量 main 的历史却只有约 329 MB。工具留下的检查点、备份引用和远端跟踪引用,都可能让前两项比主干大。看到本地 Git 目录有几个 GB,我会先拆开量,避免为本机独有的历史重写主干。
用同一套打包方法比较历史和快照
下面的命令在 Git Bash、macOS 或 Linux 的 Bash 中运行。它们需要本机具备目标对象,浅克隆或仍然缺对象的部分克隆要换完整副本再测。测量期间不要移动引用,先记录目标提交,再用这个固定提交的 tree 做对照。打开 pipefail 后,打包失败会让管道返回非零,即使 wc 已打印字节数,那次结果也应作废。
set -o pipefail
target=$(git rev-parse --verify 'HEAD^{commit}') &&
tree=$(git rev-parse --verify "${target}^{tree}") &&
printf 'commit %s\ntree %s\n' "$target" "$tree" &&
git count-objects -vH &&
git pack-objects --stdout --revs --threads=4 --all </dev/null | wc -c &&
printf '%s\n' "$tree" | git pack-objects --stdout --revs --threads=4 | wc -c
前一次测本地所有引用和 HEAD 的可达对象,后一次测目标 tree 和 blob。只想比较主干历史,先用 refs/heads/main^{commit} 固定目标,再把第一次打包也改成从标准输入读对象名,输入 "$target",第二次仍用 "$tree"。两次都按二进制流计数字节,不写回对象库。Windows 上可直接在 Git Bash 执行,尤其别把 pack 交给按文本处理管道的 PowerShell 版本再计数。这个差值只估算按相同配置重打包的收益,根提交还有少量元数据,实际传输和远端计量另算。 打包选项可以查 git pack-objects 的参数说明。
2026 年 10 月 25 日测得的两个快照相差很大。表里的体积是本机压缩 pack 的字节数,后续仓库内容更新会改变它们。
| 目标提交 | 所有本地引用的 pack | 最新 tree 的 pack | 两者差值 |
|---|---|---|---|
sheng-tool@c5fda0e |
689,617 | 82,983 | 606,634 |
sheng-blog@6795417 |
526,715,165 | 521,438,666 | 5,276,499 |
前一个仓库按比例能省很多,实际不到 1 MiB。后一个仓库的主要成本是现有快照。遇到这种结果,我会先查当前 tree 里最重的文件,再判断它们是站点必须发布的成品、后续还要编辑的图源,还是可再生成的预览。保留目录的判断要按用途做,不能用「没有发布到网站」推断它没有维护价值。
Git pack 还能复用相同对象,并在合适时对不同对象做差分压缩。磁盘上有两个相同图片,只会引用同一份 blob。图片改编码后会产生新对象,旧对象只要还在可达历史里,仍可能继续占用空间。用 Git LFS 时,Git 里存的是指针,媒体另行传输和计量,迁移还要验证实际文件可获取。
只看当前快照时,可以按 git ls-tree 的对象大小列出最大的文件。这里沿用刚才记录的 target,输出的是未压缩的 blob 字节数。
git ls-tree -r -l "$target" | sort -k4,4nr | sed -n '1,15p'
给生成产物加 .gitignore 只会防止以后误加。已经跟踪的文件要从索引移除并提交,旧提交中仍能找到它们。已经进入历史的大文件如果需要改用 LFS,还要计划迁移既有文件,并验证新远端的 LFS 媒体可下载。
图片优化也要量打包后的字节。2026 年 10 月 26 日,我把同一批 60 张资料图片的原文件和优化版分别打包,优化版里有 30 张无损结果和 30 张质量设为 90 的结果。只测图片对象,pack 从 12,130,261 字节降到 5,628,355 字节,少了约 53.6%。这组对照不包含提交和文件树,也没测原图与优化版共存时的历史差分。它能说明优化图片和 Git 打包分别能起作用,不能拿这个比例估算整个资料库。
确认平台卡在哪一层
以 2026 年 11 月 6 日核对的 GitHub 文档为例,普通 Git 文件大于 50 MiB 会警告,大于 100 MiB 会拒绝,单次 push 的排障文档则给出 2 GiB 上限。仓库大小是另一回事,平台的 Repository limits 用 .git 的磁盘占用给出 10 GB 建议,关于大文件的说明又建议仓库理想小于 1 GB、尽量小于 5 GB。按各自的用途看这些数字,容量建议不能用来判断某次 push 会不会被拒绝。
Gitee 则有两份不同口径。社区版配额页列的是 Git 单仓 500M,网站使用条款的仓库大小条目写的是 1 个 G。要迁入接近上限的仓库,我会先按实际账号、组织和产品核对容量,并向平台确认适用口径。某次 push 成功只说明那次写入被接受,推不出这个账号的长期配额。
分批提交和分批推送是两件事
目标平台若限制单次 push,先分批提交、最后一次性 push,接收方仍可能在一次传输里收到所有对象。可以沿祖先链逐批推送,且每批确认远端分支确实到了预期的提交,再继续推下一批。每批该有多大要看对象和实际传输,不能用固定提交数推算。若连第一条提交的文件树都太大,就要在隔离副本里按文件拆批,再逐次提交和推送。单文件超限则要先处理超限对象。推送前列清要迁移的分支和标签,--mirror 会同步整个引用空间,也可能推走本机的备份引用或删除远端独有的引用。
目标分支只留一个根提交时
平台的 squash merge 会保留目标分支已有的历史。要让目标分支只剩一个根提交,就得另外创建无父的提交。新根和旧 tip 没有共同祖先,即使远端已经收到那些文件,普通推送仍可能重新发送大部分 tree。强推只允许引用做非快进更新,传输大小限制仍然适用。
2026 年 10 月 25 日的本机实验用的是 Git 2.45.1 for Windows。bare 接收端设 receive.maxInputSize=700k,即 716,800 字节,然后把三份各 400 KiB 的随机文件分别提交和推送。逐批推送成功。把相同文件树做成根提交后,直接推向空接收端失败,直接强推到已经有完整文件的接收端也失败,均超过实验设定的 pack 上限。
随后用同一棵 tree 创建一个临时合并提交,父提交分别是已经上传的旧 tip 和新根。这样旧 tip 是过渡提交的祖先,Git 可以从它已有的 tree 推断哪些对象可以复用,接收方又能沿另一个父节点取得新根。原始实验里这次只发了 286 字节,第二次把目标分支从过渡提交移到新根,已经没有新对象需要发。独立克隆后只剩一个可达提交,tree 相同,git fsck --full 通过。286 字节是那次测量的结果,提交元数据变化后也会变。
两次更新都要固定自己审过的远端旧值,第一次预期是旧 tip,第二次预期是过渡提交。第二次失败时先读远端 ref,确认它还指向过渡提交,再决定完成更新还是恢复旧 tip。目标分支如果要求线性历史、禁止非快进更新,或过渡状态会触发不能接受的 CI 和发布动作,这条路线就不适用。本机实验只验证了自建接收端的行为。
在 GitHub 上还有一种基于官方接口的思路。目标 tree 已存在于同一个仓库时,可以指定该 tree 和空的 parents 创建根提交,检查提交的 tree 和父列表,再用新提交更新目标 ref。本机尚未上传的文件仍需要另行传输。REST API 的强制更新没有预期旧 SHA 参数,先读 ref 再更新也无法阻止中间插入一次并发写入。采用这种路线要暂停人员和自动化的写入,留好能恢复旧 tip 的独立备份,并确认分支规则允许更新。请求超时先读 ref 核对结果,若当前 tip 已被别的写入推进,就停下重新核对。这个方案来自官方接口文档,GitHub 大仓的实际结果仍需验证。
大仓迁移前,我会先找出到底是哪一层超限。假如当前 tree 已经占了大头,我会先想这些文件该怎样存储和交付。旧历史占得多,团队也接受丢掉它,才值得按历史重写的流程固定快照和恢复点。迁移的最后一步是从新远端重新克隆,核对文件、所需引用和实际媒体。如果还要腾出托管空间,就另外核对平台的计量结果。