知其然,知其所以然
Git 内部原理 2026-10-11

文件权限位与 core.fileMode

在真实仓库里验证 git 只记录属主的可执行位:十种磁盘权限进去只有 100644 和 100755;检出时按 umask 重新生成权限;core.fileMode=true 时 chmod 会产生只有 old mode / new mode 的改动,false 时被忽略,新文件一律记成 100644,可用 update-index --chmod 手工指定。

Git 文件权限 core.fileMode 100755 umask 真实验证 动画演示
Git 内部原理 2026-10-11

符号链接在 git 里怎么存

在真实仓库里验证符号链接的存法:blob 内容就是 readlink 字符串,相对、绝对、悬空、指向仓库外都照存;clone 后相对链接有效、绝对链接指回原路径;git add 指向目录的链接带斜杠会被拒绝;core.symlinks=false 时链接检出成普通文件,status 全显示 T,在那里提交会把链接变成普通文件。

Git 符号链接 120000 core.symlinks 真实验证 动画演示
Git 内部原理 2026-10-11

换行符:autocrlf 与 .gitattributes 的 text 属性

在真实仓库里对比换行符设置:默认原样存;core.autocrlf=true 添加时 CRLF→LF、检出时 LF→CRLF;input 只在添加时转换;.gitattributes 的 text / eol / binary 优先于 autocrlf;已提交的 CRLF 要 git add --renormalize 才会按新属性规范;safecrlf 在混合换行时拒绝添加。

Git 换行符 CRLF autocrlf gitattributes 真实验证 动画演示
Git 内部原理 2026-10-11

大文件的 blob:压缩率与每次修改的存储代价

实测 git 存大文件的代价:1 MB 文本松散对象 430,279 字节,随机字节反而大 324 字节,全零 4,395 字节;每次改一行就新增一个完整的 430 KB 副本(6 个版本 2.58 MB),gc 之后降到 391 KB;core.compression 从 1 提到 9 只再省约 10%;随机文件改 10 字节,gc 后两个版本只比一个多一点。

Git blob 压缩 大文件 gc delta 真实验证 动画演示
Git 内部原理 2026-10-11

packfile 格式:头部、对象类型、delta 编码

用 Python 逐字节解析真实的 packfile:12 字节头部(PACK、版本 2、对象数)、每个条目的类型与大小变长整数、zlib 数据;OFS_DELTA 的向后偏移指向基础对象(最新版本完整存放,旧版本存成 delta);按 copy / insert 指令还原 delta,15 个对象的 sha 全部与 git verify-pack 一致;末尾 20 字节是整个 pack 的 SHA-1。

Git packfile delta OFS_DELTA gc 真实验证 动画演示
Git 内部原理 2026-10-11

delta 链:window 与 depth 对 pack 大小的影响

在一个文件的 40 个版本上用 git repack 调 window 与 depth:不做 delta 的 pack 是 560,627 字节;window=1 是 102,498,window≥2 就降到 28,623(单线程);depth=1 是 102,498,depth=50 是 28,623、最长链 39;读 40 个版本的耗时在 10~12 ms 之间看不出差别;gc --aggressive 的实际命令行是 -f --depth=50 --window=250。

Git delta window depth repack gc 真实验证 动画演示
Git 内部原理 2026-10-11

.idx 文件:fanout 表与二分查找

用 Python 解析真实的 .idx(v2)文件:魔数、256 项 fanout 表、升序 hash 表、crc 表与 offset 表;手工用 fanout 圈范围再二分查找一个对象,偏移与 git verify-pack 一致;对 3,032 个对象平均比较次数从整表二分的 10.65 降到 3.10;尾部校验和与 crc 逐项验证。

Git packfile idx fanout 二分查找 真实验证 动画演示
Git 内部原理 2026-10-11

multi-pack-index

实测 multi-pack-index:20 个 pack 写出一个 87,796 字节的 midx,解析头部(MIDX、版本 1)与 PNAM、OIDF、OIDL、OOFF 四个块,偏移与 git verify-pack 一致;20 个 pack 下有无 midx 查全部对象耗时都是 14 ms 左右;multi-pack-index repack --batch-size 配合 expire 把 pack 从 20 个合并到 17、11 个;默认配置下 commit 后的自动维护会悄悄合并 pack。

Git multi-pack-index midx packfile repack 真实验证 动画演示
Git 内部原理 2026-10-11

commit-graph:git log 为什么变快

在 30,300 个提交的历史上测 commit-graph:没有时 rev-list --count 99.0 ms、merge-base 67.7 ms,写完之后 15.3 ms 和 13.6 ms(快 5.0~8.5 倍);解析 1.8 MB 的 commit-graph 文件:OIDF、OIDL、CDAT(每提交 36 字节)、GDA2 四个块,parent 用下标指向 hash 表,世代数比所有 parent 的最大值大 1;git gc 默认就会写它。

Git commit-graph 世代数 git log 性能 真实验证 动画演示
Git 内部原理 2026-10-11

reachability bitmap

在 6000 个提交(30,000 个对象)的仓库上写 reachability bitmap:git repack -adb 生成 149,820 字节的 .bitmap;数全部可达对象从 32.8 ms 降到 12.5 ms;按字节解析 EWAH 位图:四张类型位图加 164 张提交位图,每 10 张抽查 17 张与 git rev-list --objects 的集合完全相同;pack-objects 打包整库 63.3 → 21.4 ms。

Git reachability bitmap bitmap repack 真实验证 动画演示
Git 内部原理 2026-10-11

repack 的参数:-a、-d、-f 各改变什么

把同一个乱七八糟的仓库(差的 pack、含不可达提交的 pack、7 个松散对象)复制七份分别 repack:无选项只新增一个 pack,-d 清掉被收进 pack 的松散对象,-a -d 合成 1 个 pack 但丢掉 pack 里的不可达提交,-A 把它们变成松散对象,-k 收进 pack,--cruft 放进带 .mtimes 的 cruft pack;-f 把 81,689 字节压到 19,756 字节。

Git repack gc cruft delta 真实验证 动画演示
Git 内部原理 2026-10-11

gc.auto 阈值:松散对象多到多少才自动 gc

在真实仓库里验证 git gc --auto 的触发条件:默认 gc.auto=6700,只数 objects/17 一个目录并与 27 比较,27 个不触发、28 个触发;随机分布的 6,700 个对象因为 objects/17 里只有 17 个而没触发,6,500 个却触发了;不可达的松散对象不会被清掉,下次仍触发;pack 数超过 50 也触发。

Git gc gc.auto 松散对象 autoPackLimit 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号