松散对象适合少量写入;历史一长,.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
布局,并用本机 hexdump 与
git 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 头部与尾部(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):
- bit 7:size 延续标志(1 = 还有后续 size 字节)
- bit 6–4:3-bit type
- bit 3–0:
size0(size 的最低 4 位)
若 size 超出 4 bit,后续每个字节的低 7 位依次为更高位段。对未 delta 对象,size 指 zlib 解压后的原始对象大小;对 delta 对象,size 指 delta 指令流解压后的大小(不含基对象名/偏移字段)。
示例:本仓库 pack
内第一个对象(commit)起始于偏移 12,首两字节为
9c 08:
0x9c:MSB=1 继续;type=001=commit;size0=120x08:MSB=0 结束;下一 7 位 = 8- 解压后 size = \(12 + 8 \times
16 = 140\)(与
verify-pack第三列一致)
第二个 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.(..|
e8 01:type=OFS_DELTA,解压 size=24(data2.txt相对data.txt的 delta)81 29:offset encoding(与 size encoding 规则不同):解码得相对偏移 297,基对象头在 \(422 - 297 = 125\)——与a6b2e782…的偏移一致
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-*.idx00000000 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 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 的需求,而非外部标准。
仍开放、且与本系列后续篇相关的问题:
- 哈希宽度迁移:SHA-1 pack/idx 尾部与 object name 表均为 20 字节;SHA-256 仓库扩展到 32 字节后,idx、MIDX、位图、协议层如何互操作——第 16 篇 专述,pack 逻辑类型不变但 on-disk 宽度全链路要改。
- 多 pack vs 单 pack:MIDX 减少打开多个
idx 的开销,但是否默认启用、与
git gc --auto的交互仍在版本间演进;不宜写死「已全面替代单 idx」。 - 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 读取。
九、本文边界
- delta 指令集(copy/insert 位级格式)与
pack-objects窗口启发式见 第 08 篇。 - reachability 加速(commit-graph、pack bitmap)见 第 09 篇。
git gc/ repack / prune 触发的文件改写见 第 10 篇。- 不写 pack v3 生成路径(规范提及 v3,当前 Git 仍只生成 v2 pack)。
- 不对 bitmap / MIDX 给出未实测的性能倍率。
复现:
bash post/git-internals/reproduce/07-pack-idx-format.sh参考资料
规范
- Git documentation, gitformat-pack
— pack header、size/offset encoding、idx
v1/v2、
.rev、MIDX - Git documentation, gitrepository-layout
—
objects/pack/路径约定
源码
- Git source,
Documentation/technical/pack-format.txt— 与 gitformat-pack 同步的格式说明 - Git source,
unpack-objects.c/pack-revindex.c— OFS_DELTA offset 解码与 reverse index 生成
实验
- 环境:Git 2.43.0,Linux;临时仓库两个
~10 KB 相似 blob +
git repack -ad - 复现脚本:post/git-internals/reproduce/07-pack-idx-format.sh
系列索引:Git 内部结构 · 上篇:index 暂存区 · 下篇:pack-objects 与 delta
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Git 内部】pack-objects 与 delta 压缩
相似 blob 如何变成 REF_DELTA 链?pack-objects 窗口、thin pack 与 git verify-pack 里的 chain length,用 20 个近邻文件实测。
【Git 内部】Git 内部结构:对象库与磁盘文件格式
从 .git 目录布局、松散对象与 packfile 格式,到 refs、index、reflog、gc/fsck 与 fetch/push 落地——用官方 format 文档与本地实测 dump 系统讲清 Git 把版本历史存在哪、每个命令改写了哪些文件。全 16 篇。
【Git 内部】.git 目录全景:三棵树与仓库布局
git init 之后 .git 里每个路径干什么?对照 gitrepository-layout 与本地 find 清单,建立 bare/non-bare、三棵树、objects/refs/index 的磁盘级地图。
【Git 内部】松散对象:zlib 载荷与 SHA-1 路径
blob/tree/commit 在磁盘上如何编码?对象头 type size、zlib 压缩、objects/ab/cdef 路径命名,对照 git hash-object 与手工 SHA-1 验证。