
在.git/objects下面看到许多两位名字的目录很容易把“一个对象对应一个小文件”当成Git永远不变的规则。打包后小文件没了旧提交却依然能打开。真正不变的是对象身份不是文件路径。这次在临时仓库做两个对照先把完整对象装进pack再允许Git为相似对象选择delta。分别观察ID、存储位置和压缩表示不用一个“压缩”把三件事糊在一起。实验只需要标准Git和PowerShell使用Git for Windows 2.45.2.windows.1、SHA-1仓库。以下代码在同一个窗口按顺序运行不在已有项目里打包也不把一次文件大小当性能基准。1. 为什么不用两个三字节文件测delta很小的对象可能没有差量收益。这里造1000行内容两次提交只改一行给delta搜索一个明确的相似性条件。显式写无BOM的UTF-8和LF避免PowerShell默认重定向编码影响字节。$ErrorActionPreference Stop $PSNativeCommandUseErrorActionPreference $false function G { git args; if ($LASTEXITCODE -ne 0) { throw git failed: $args } } $lab Join-Path $env:TEMP (git-pack-lab- [guid]::NewGuid().ToString(N)) New-Item -ItemType Directory -Path $lab | Out-Null Set-Location $lab G init --object-formatsha1 -b master G config user.name Blog experiment G config user.email blogexample.invalid G config core.autocrlf false G config gc.auto 0 $utf8 New-Object System.Text.UTF8Encoding($false) $lines (1..1000 | ForEach-Object { row-{0:D4}: stable payload for delta experiment -f $_ }) [IO.File]::WriteAllText((Join-Path $lab f.txt), ($lines -join n) n, $utf8) G add f.txt G commit -m c1 $blob1 G rev-parse HEAD:f.txt $lines[499] row-0500: changed payload for delta experiment [IO.File]::WriteAllText((Join-Path $lab f.txt), ($lines -join n) n, $utf8) G add f.txt G commit -m c2 $tip G rev-parse HEAD $blob2 G rev-parse HEAD:f.txt $beforeIds (G cat-file --batch-all-objects --batch-check%(objectname) | Sort-Object) G count-objects -v本例有两个不同blob、两个tree、两个commit共六个对象。不是说“两次提交必定六个”文件内容复用、目录结构或额外引用都会改变这个数。2. 没有delta也可以有packG repack -a -d --window0 G prune-packed $indexes (Get-ChildItem .git/objects/pack/*.idx) if ($indexes.Count -ne 1) { throw expected one pack in this fresh lab } $fullReport (G verify-pack -v $indexes[0].FullName) $fullReport $fullRows ($fullReport | Where-Object { $_ -match ^[0-9a-f]{40}\s }) $fullDeltas ($fullRows | Where-Object { ($_ -split \s).Count -eq 7 }) full_pack_objects $fullRows.Count full_pack_delta_objects $fullDeltas.Count G count-objects -v tip_unchanged ((G rev-parse HEAD) -eq $tip) object_ids_unchanged (-not [bool](Compare-Object $beforeIds (G cat-file --batch-all-objects --batch-check%(objectname) | Sort-Object)))window0禁用这次打包的delta搜索prune-packed只清除已在包中存在的松散副本不是删除唯一对象。新仓库此时只有一组pack/idx才可以明确选择那个idx不能把“取第一个文件”的写法照搬到已有多包仓库。本轮得到六个对象、零个delta对象集合和HEAD都不变。即使每个对象仍采用完整内容表示pack已经成立。Git repack手册3. idx提供的是位置不是另一份文件内容verify-pack里普通对象行包含对象ID、类型、对象大小、包内大小和offset。delta行还给出深度及base对象ID其中size不能直接当作重建后的完整对象长度本轮旧blob的delta行显示18cat-file读取其完整内容长度却是46000。offset是包内位置不是对象的新名字也不是文件在工作区里的位置。层稳定或变化的东西本例检查手段身份请求哪个对象ID打包前后比较完整ID集合定位loose路径或idx中的pack偏移列出pack/idx查看verify-pack表示完整压缩对象或delta检查delta行与base信息内容调用者最终读到的字节从包读blob重新计算其IDv2 idx用fan-out和排序ID表帮助定位条目再取得offsetpack里才存对象的表示。这里是查询路径示意不把图当字节格式说明。SHA-1长度也不能套到所有对象格式。Git pack格式4. 证明能读不只证明还有一个ID从包里读取第二版blob按实验规定的编码和换行重建一个文件再重新hash。它比“log还能显示提交说明”更接近我们的目标确认文件内容也正确。$readBack (G cat-file blob $blob2) [IO.File]::WriteAllText((Join-Path $lab read-back.txt), ($readBack -join n) n, $utf8) packed_blob_round_trip ((G hash-object read-back.txt) -eq $blob2) old_blob_readable ((G cat-file -t $blob1) -eq blob) G fsck --full两项为True。读回文件没有add不会给后续对象集合增加一次提交。这个文本拼接只适用于我们特意构造的ASCII/LF样例不是通用二进制文件恢复方法任意字节不能先当PowerShell文本读再重写。5. 重新打包为什么先没有得到deltaG repack -a -d --window50 --depth50 $indexes (Get-ChildItem .git/objects/pack/*.idx) if ($indexes.Count -ne 1) { throw expected one reused pack } $reuseReport (G verify-pack -v $indexes[0].FullName) $reuseDeltaRows ($reuseReport | Where-Object { $_ -match ^[0-9a-f]{40}\s -and ($_ -split \s).Count -eq 7 }) delta_objects_without_recompute $reuseDeltaRows.Count G repack -a -d -f --window50 --depth50 G prune-packed $indexes (Get-ChildItem .git/objects/pack/*.idx) if ($indexes.Count -ne 1) { throw expected one replacement pack } $deltaReport (G verify-pack -v $indexes[0].FullName) $deltaReport $deltaRows ($deltaReport | Where-Object { $_ -match ^[0-9a-f]{40}\s -and ($_ -split \s).Count -eq 7 }) delta_detected ($deltaRows.Count -gt 0) delta_pack_objects (($deltaReport | Where-Object { $_ -match ^[0-9a-f]{40}\s }).Count) ids_after_delta_unchanged (-not [bool](Compare-Object $beforeIds (G cat-file --batch-all-objects --batch-check%(objectname) | Sort-Object))) delta_blob_content_equal (((G cat-file blob $blob2) -join n) -eq ($lines -join n)) $oldBlobRow ($deltaRows | Where-Object { ($_ -split \s)[0] -eq $blob1 }) if ($oldBlobRow.Count -ne 1) { throw expected one old blob delta row in this sample } old_blob_delta_base_is_new ($oldBlobRow.Count -eq 1 -and ($oldBlobRow[0] -split \s)[6] -eq $blob2) old_blob_delta_payload_size ($oldBlobRow[0] -split \s)[2] old_blob_full_size (G cat-file -s $blob1) G fsck --full本轮中间检查仍是零个delta加入-f后才出现delta。-f要求底层不复用既有delta选择而重新计算仅扩大搜索窗口不能直接推断现有包已经按新条件重新选择所有表示。这个失败过的中间状态保留在正文里读者才知道要排查什么。Git pack-objects手册这不是说-f及这些参数保证任意仓库都产生delta。对象大小、相似性、搜索限制和实现版本都会影响选择本例是给定数据下的观察。本轮还检查到旧blob的delta以新blob为base。delta需要base才能重建内容Git完成重建后仍向上层返回原来那个对象。一个blob有了delta表示不等于它变成另一个版本也不意味着它的base就是历史上的“上一次提交”。pack选择的是存储依赖commit表达的是历史依赖两套关系不能等同。size字段更应结合cat-file确认不应把18字节说成原文件变成18字节了。同一个旧blobverify-pack的sizecat-file -s的完整大小delta base完整对象表示4600046000无本轮delta表示1846000新blob这组数仅对应本篇固定样例大小字段的含义不同就不能直接拿来比较“文件缩小了多少”。6. 把对照条件写成断言if ($fullRows.Count -ne 6 -or $fullDeltas.Count -ne 0) { throw full-object control failed } if ($reuseDeltaRows.Count -ne 0) { throw reuse control changed on this environment } if ($deltaRows.Count -eq 0) { throw this sample did not produce a delta } if ((G rev-parse HEAD) -ne $tip) { throw tip changed } if (Compare-Object $beforeIds (G cat-file --batch-all-objects --batch-check%(objectname) | Sort-Object)) { throw object IDs changed } if ((G hash-object read-back.txt) -ne $blob2) { throw read-back mismatch } PACK_IDENTITY_CHECKS_PASSED图是实际stdout节选排版后截图不是桌面终端窗口。验证先固定内容与ID集合再控制delta搜索最后检查读取、重新hash与fsck而不是只看压缩包是否变小。小对象可能因为格式开销占用更多空间运行一次也不能推出性能结论。这个实验真正要澄清的是对象身份、定位索引和压缩表示可以分开变化。objects下面的小文件不再存在不应该先被解释为“Git把提交删掉了”。