土法炼钢兴趣小组的算法知识备份

【Git 内部】pack 与 idx 文件格式

文章导航

分类入口
opensource
标签入口
#git#git-internals#packfile#idx#verify-pack#OFS_DELTA#REF_DELTA

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

松散对象适合少量写入;历史一长,.git/objects/ 下成千上万个小文件会带来 inode 与目录压力(参见 小文件问题)。packfile 把多个对象串进单个 .pack,用 .idx 按 SHA 查偏移;git fetch / git push 传输的也是 pack(第 14 篇)。

读 pack 的常见误区是把「文件名里的 hash」当成 idx 的校验和,或把 verify-pack 里带 SHA 的 delta 行直接等同于 on-wire 的 REF_DELTA——本地 repack 生成的 pack 里,基对象在同一文件内时更常见 OFS_DELTA(负偏移引用),verify-pack 会把基对象解析成 SHA 便于人读。本文按 gitformat-pack 逐字段说明 .pack / .idx v2 布局,并用本机 hexdumpgit verify-pack -v 对照实测输出。


一、文件对

文件 作用
objects/pack/pack-<hash>.pack 连续存放的对象数据 + pack 尾部 SHA-1/SHA-256 校验
objects/pack/pack-<hash>.idx 按对象 ID 排序的索引,指向 pack 内字节偏移
objects/info/packs 列出仓库启用的 pack 文件(git repack / git gc 维护)
pack-<hash>.rev(可选) reverse index:按 pack 内偏移排序,加速按偏移反查对象(Git 2.31+)
multi-pack-index(可选) MIDX:跨多个 pack 的统一索引(大仓库维护场景)

pack 文件名中的 <hash>pack 文件内容(含尾部校验和之前全部字节)的对象哈希,不含 idx。idx 末尾另有一份 pack 校验和副本,以及 idx 自身的校验和。

pack 与 idx 文件布局:PACK 头、对象序列、尾部校验与 idx 偏移表

二、pack 头部与尾部(hexdump)

gitformat-pack, pack header 规定 pack 以固定 12 字节头 开始,以 20 字节(SHA-1 仓库)或 32 字节(SHA-256 仓库) 尾部校验结束。

构造两个约 10 KB、仅改一行的相似 blob,提交后 git repack -ad(Git 2.43.0,临时仓库;提交时间固定为 2026-01-01 以便复现 SHA;完整命令见 复现脚本)。对生成的 .pack 执行:

hexdump -C -n 12 .git/objects/pack/pack-*.pack
tail -c 20 .git/objects/pack/pack-*.pack | hexdump -C

输出(以下经删减,仅保留头尾):

00000000  50 41 43 4b 00 00 00 02  00 00 00 04              |PACK........|
0000000c

00000000  0f 0a ae ae 56 17 46 75  b1 68 92 37 1b 6f f2 30  |....V.Fu.h.7.o.0|
00000010  47 56 d7 7e                                       |GV.~|
00000014
偏移 字节 含义
0–3 50 41 43 4b 签名 PACK
4–7 00 00 00 02 版本号 2(网络字节序;Git 当前生成 v2)
8–11 00 00 00 04 对象个数 4
尾部 20 字节 0f0a…d77e 整个 pack(头 + 所有对象)的 SHA-1

文件名 pack-0f0aaeae56174675b16892371b6ff2304756d77e.pack 与尾部校验一致——这是 pack 自描述 命名,不是 idx 哈希。


三、对象头:type 与 size 变长编码

pack 内每个对象以 n 字节 type+length 头 开始,后跟 zlib 压缩载荷(或 delta 指令)。与松散对象不同,压缩数据里不再重复 type size\0 前缀;对象 ID 仍按「逻辑头 + 载荷」计算(gitformat-pack, Object encoding)。

3.1 类型(3 bit)

名称 含义
1 OBJ_COMMIT commit
2 OBJ_TREE tree
3 OBJ_BLOB blob
4 OBJ_TAG annotated tag
6 OBJ_OFS_DELTA 基对象在同一 pack,以负偏移定位
7 OBJ_REF_DELTA 基对象以 20/32 字节 ID 给出

类型 5 保留;类型 0 非法。

3.2 变长 size 编码

规范中的 size encoding(与 offset encoding 不同):每个字节的低 7 位拼成整数,MSB 为 1 则继续读下一字节;MSB 为 0 的字节提供最后 7 位。后读的字节贡献更高位。

首字节布局(gitformat-pack, packed object header):

若 size 超出 4 bit,后续每个字节的低 7 位依次为更高位段。对未 delta 对象,size 指 zlib 解压后的原始对象大小;对 delta 对象,size 指 delta 指令流解压后的大小(不含基对象名/偏移字段)。

示例:本仓库 pack 内第一个对象(commit)起始于偏移 12,首两字节为 9c 08

第二个 blob 基对象头三字节 ba f7 04,type=blob,解压 size = 10106(约 10 KB 文本)。


四、delta 对象:OFS_DELTA 与 verify-pack

相似 blob 经 git repack 可链成 delta。本实验 pack 内 4 个对象,其中 1 个为 delta:

git verify-pack -v .git/objects/pack/pack-*.pack

输出(以下经删减):

412b5a5172433ce86ff5c3f07488a848746a9996 commit 140 113 12
a6b2e782ea475dfc18665c5b69c0ad7fdc2f2f20 blob   10106 297 125
bc8fc393455ba3ef27451f1db74e3aa26f5f84b4 blob   24 36 422 1 a6b2e782ea475dfc18665c5b69c0ad7fdc2f2f20
7881410e23ffcddf4b7f91321b0446534fbd5be4 tree   73 74 458
non delta: 3 objects
chain length = 1: 1 object
pack-0f0aaeae56174675b16892371b6ff2304756d77e.pack: ok

列含义:对象ID 类型 解压大小 压缩大小 pack内偏移;delta 行末尾还有 depth base——此处 depth=1,基对象 SHA 为 a6b2e782…(10 KB 的 data.txt)。

在偏移 422 处 hexdump 可见 on-disk 类型为 OFS_DELTA(type=6),而非 REF_DELTA:

000001a6  e8 01 81 29 78 9c fb e5  d7 e4 bf 61 9f 28 bb b3  |...)x......a.(..|

REF_DELTA 在 thin pack 传输中更常见(基对象可能在接收方已有,见 第 08 篇);磁盘上的 pack 要求自包含,同一 pack 内优先 OFS_DELTA 以省 20 字节基 ID。verify-pack 统一把基对象展示为 SHA,读字节时需看 type 字段区分。


五、idx v2 布局与 fanout

idx 使 git cat-file 能在 pack 内 \(O(\log N)\) 定位对象:SHA → idx 查 offset → pack 解压。v2 索引以魔数 \377tOc 开头(v1 直接把 fanout[0] 当作首字段,不可能出现该值)。

hexdump -C -n 64 .git/objects/pack/pack-*.idx
00000000  ff 74 4f 63 00 00 00 02  00 00 00 00 00 00 00 00  |.tOc............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
...
00000230  00 00 00 01 00 00 00 02  00 00 00 02  00 00 00 02  |................|
00000240  00 00 00 02 00 00 00 02  00 00 00 02  00 00 00 02  |................|
区段 内容
0–3 魔数 \377tOc
4–7 版本 2
8–1031 fanout[0..255]:按对象 ID 首字节(256 桶)的累计对象数
其后 排序的 object names(20 或 32 字节 × N,与 v1 不同:不再与 offset 交错)
再后 CRC32 表(每对象 4 字节):对 pack 内该对象完整条目(含 type/size 头、基引用、压缩体)的 CRC32
再后 offset 表(每对象 4 字节,大端):通常 31-bit pack 偏移;MSB=1 时表示索引指向 large offset 表 中的 8 字节项
可选 large offset 表(8 字节 × M):pack ≥ 2 GiB 时需要
尾部 pack 校验和副本 + idx 自身校验和

本实验 4 个对象的 fanout 仅在高位桶非零:fanout[252]=fanout[253]=fanout[254]=fanout[255]=4,表示 ID 首字节 ≤ 252/253/254/255 的对象数均为 4(4 个 SHA 首字节均落在 0x78 附近)。

查对象时:用 SHA 首字节定位 fanout 区间 → 在 object name 表二分 → 取同下标 CRC32/offset → 读 pack。

idx 查对象:SHA 经 fanout 与二分定位 pack 内偏移

六、idx v1 → v2 的动机

gitformat-pack, Original (version 1)Version 2 的差异不是 cosmetic,而是三类生产痛点的回应:

问题 v1 行为 v2 改动
4 GiB pack 上限 offset 固定 4 字节,无扩展 31-bit 偏移 + large offset 表(8 字节);MSB 标记间接项
repack 数据完整性 无 per-object 校验 CRC32 表:pack-to-pack 拷贝压缩体时可发现损坏
索引体积与缓存 offset 与 SHA 交错 24/28 字节条目 SHA 集中存放,offset/CRC 分表,二分查找 cache 更友好

v1 仍可能出现在极老仓库;现代 Git 生成的 idx 均为 v2。offset 表的 MSB 机制要点:当 pack 内偏移 \(\lt 2^{31}\) 时直接存;更大偏移在 4 字节项里置 MSB,下标指向 large offset 表中的 8 字节真实偏移——这与 size encoding 的「7-bit 延续」是两套规则,读规范时不要混用。


七、工程谱系与开放问题

Git pack 格式主要是工程规范而非单篇论文产物,但存储层演进仍有一条可核对的脉络:

松散 zlib 对象(per-SHA 文件)
  → pack + idx v1(单文件聚合、按 SHA 索引)
  → idx v2(大 pack、CRC32、分表布局)
  → .rev reverse index(按 offset 反查,服务 reachability)
  → MIDX(多 pack 统一索引)
  → SHA-256 object format / reftable(对象 ID 宽度与引用存储,见第 16 篇)

松散 → pack 的动机在系统层是经典的小文件 / inode 成本(与 存储工程:小文件问题 同一脉络);delta 思路与 rsync 式滚动校验 diff 同源,Git 在 pack 内用 OFS_DELTA / REF_DELTA 两种引用基对象。idx v2 的 CRC32 与 large offset 来自 Git 自身维护超大 monorepo pack 的需求,而非外部标准。

仍开放、且与本系列后续篇相关的问题

  1. 哈希宽度迁移:SHA-1 pack/idx 尾部与 object name 表均为 20 字节;SHA-256 仓库扩展到 32 字节后,idx、MIDX、位图、协议层如何互操作——第 16 篇 专述,pack 逻辑类型不变但 on-disk 宽度全链路要改。
  2. 多 pack vs 单 pack:MIDX 减少打开多个 idx 的开销,但是否默认启用、与 git gc --auto 的交互仍在版本间演进;不宜写死「已全面替代单 idx」。
  3. reverse index 与 bitmap 的耦合.rev 加速按 pack 顺序遍历;commit-graph / pack bitmap(第 09 篇)进一步加速 reachability——三者是叠加优化,删除后语义仍正确,仅可能更慢。

八、与松散对象的语义等价

pack 是物理编码;对象 ID 仍是对「type size\0 + 载荷」的哈希,与 松散对象 一致。git cat-file -p <sha> 无需关心对象来自松散文件还是 pack;git count-objects -v 在本实验 repack 后:

count: 0
size: 0
in-pack: 4
packs: 1
size-pack: 1

count: 0 表示松散对象已清空(-d 删除冗余松散副本);对象仍可通过 idx 读取。


九、本文边界

复现:

bash post/git-internals/reproduce/07-pack-idx-format.sh

参考资料

规范

源码

实验

系列索引Git 内部结构 · 上篇index 暂存区 · 下篇pack-objects 与 delta

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-05 · opensource

【Git 内部】Git 内部结构:对象库与磁盘文件格式

从 .git 目录布局、松散对象与 packfile 格式,到 refs、index、reflog、gc/fsck 与 fetch/push 落地——用官方 format 文档与本地实测 dump 系统讲清 Git 把版本历史存在哪、每个命令改写了哪些文件。全 16 篇。


By .