bigywater.tech
Git · 71

bundle:离线传输仓库

没有网络怎么传一个 git 仓库?git bundle 把若干个 ref 和对应的对象打成一个文件。这篇拆开这个文件看它到底由什么组成,再看增量 bundle 的前置条件、clone 与 fetch 的用法。

30 个对象
完整 bundle 里 pack 的对象数,等于 git rev-list --objects --all 的可达对象数
855 vs 2,457 字节
增量 bundle(9 个对象)与完整 bundle(30 个对象)的文件大小
- 开头的行
bundle 文本头里的前置条件,接收方必须已经有这些提交

文件结构

实测 · 本机执行输出
  $ git bundle create /work/l67full.bundle --all 2>&1 | tail -1; ls -l /work/l67full.bundle | awk '{print $5, "字节"}'
  2457 字节
  文件开头的文本部分(PACK 之前的字节):
    # v2 git bundle
    <sha40> refs/heads/dev
    <sha40> refs/heads/main
    <sha40> refs/tags/v1
    <sha40> HEAD
    (最后有一个空行:文本头与 pack 的分隔)
  PACK 头:版本 2,对象数 30;源仓库 git rev-list --objects --all 数出 30 个对象——pack 里装的就是全部可达对象

bundle 文件开头是文本:# v2 git bundle,然后每行 hash ref名,增量 bundle 还有以 - 开头的前置条件行;一个空行之后就是标准的 packfile。用 Python 逐字节读出来,对象数与源仓库的可达对象数一致。

verify 与 clone

实测 · 本机执行输出
  $ git bundle verify /work/l67full.bundle 2>&1 | tail -2 | sed -E 's/[0-9a-f]{40}/<sha40>/g'
  The bundle records a complete history.
  The bundle uses this hash algorithm: sha1
  $ git bundle list-heads /work/l67full.bundle | sed -E 's/[0-9a-f]{40}/<sha40>/g'
  <sha40> refs/heads/dev
  <sha40> refs/heads/main
  <sha40> refs/tags/v1
  <sha40> HEAD
  $ git log --oneline | wc -l | sed "s/^ */clone 出来的提交数: /"; git branch -a | tr "\n" " "; echo; git tag | tr "\n" " "; echo; git config --get remote.origin.url | sed "s#.*/#…/#"
  clone 出来的提交数: 10
  * main   remotes/origin/HEAD -> origin/main   remotes/origin/dev   remotes/origin/main
  v1
  …/l67full.bundle

git bundle verify 检查完整性与前置条件,list-heads 列出其中的 ref;git 可以把 bundle 当只读的远程直接 git clone x.bundle。

增量

实测 · 本机执行输出
  $ git bundle create /work/l67inc.bundle main ^92579846b3c90c197a3c05ee36e59ad88cc8fbce 2>&1 | tail -1; ls -l /work/l67inc.bundle | awk '{print $5, "字节"}'
  855 字节
  增量 bundle 的文本部分:
    # v2 git bundle
    -<sha40> 提交 10
    <sha40> refs/heads/main
  pack 里的对象数 9(完整 bundle 是 30),文件大小 855 字节(完整 bundle 2,457 字节)
  在缺少前置提交的仓库里验证:
  $ git bundle verify /work/l67inc.bundle 2>&1 | head -3 | sed -E 's/[0-9a-f]{40}/<sha40>/g'
  error: Repository lacks these prerequisite commits:
  error: <sha40>
  在已经有这些提交的仓库(上面 clone 出来的 l67c)里验证:
  $ git bundle verify /work/l67inc.bundle 2>&1 | head -4 | sed -E "s/[0-9a-f]{40}/<sha40>/g"
  /work/l67inc.bundle is okay
  The bundle contains this ref:
  <sha40> refs/heads/main
  The bundle requires this ref:

git bundle create inc.bundle main ^<已有的提交> 只打包新对象,在文本头里记录前置条件。接收方缺少前置提交时 verify 报 Repository lacks these prerequisite commits。

fetch

实测 · 本机执行输出
  $ git fetch /work/l67inc.bundle main:refs/remotes/bundle/main 2>&1 | tail -2 | sed -E "s/[0-9a-f]{7}\.\.[0-9a-f]{7}/<旧>..<新>/; s#From .*#From <bundle>#"; git log --oneline refs/remotes/bundle/main | wc -l | sed "s#^ *#bundle/main 的提交数: #"
  From <bundle>
   * [new branch]      main       -> bundle/main
  bundle/main 的提交数: 13

git fetch x.bundle main:refs/remotes/… 和从普通远程 fetch 一样更新引用,适合用 U 盘、邮件同步仓库。

开销

实测 · 本机执行输出
  git pack-objects 直接打包全部对象: 3,099 字节;bundle 里 PACK 部分: 2,894 字节;文本头 230 字节

bundle 没有额外的格式负担:pack 部分的大小和 git pack-objects 同级,再加几百字节的文本头。

分步演示

bundle
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。bundle 文件由我用 Python 按字节读取(文本头加 PACK),pack 内容复用上一系列的 parse_pack;“与 pack-objects 同级”只比较了一个小仓库(30 个对象)
    • 没有验证:bundle v3(object-format、capability 行,如 filter);--since / 日期范围打包;git bundle unbundle;包含标签注解对象的情形;很大仓库上 bundle 的耗时与大小;通过邮件传输时的编码
    京ICP备2026067502号-1京公网安备11010502063487号