跳到正文

我所理解的 Git 最佳实践(9):先量清仓库体积,再决定怎么迁移

我所理解的 Git 最佳实践

  1. 从工作区到一条可审阅的提交
  2. 什么时候用 rebase,什么时候用 merge
  3. 利用 amend 和 rebase 保持提交简洁
  4. 在 GitHub 和 GitLab 配置 PR 的合入方式
  5. 共享测试分支与按需求部署
  6. 弄清工作树、嵌套仓库和远端的归属
  7. 没有远端时怎样用 Git 维护项目
  8. 为什么切到 orphan 分支后源码不见了
  9. 先量清仓库体积,再决定怎么迁移(当前)
  10. 用 Node.js 检查 Git 路径,提前发现 Windows 检出问题

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 已经占了大头,我会先想这些文件该怎样存储和交付。旧历史占得多,团队也接受丢掉它,才值得按历史重写的流程固定快照和恢复点。迁移的最后一步是从新远端重新克隆,核对文件、所需引用和实际媒体。如果还要腾出托管空间,就另外核对平台的计量结果。