news 2026/10/6 8:06:54

objects的小文件不见了,Git为什么还能读?用两个pack实验拆开身份、索引与delta

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
objects的小文件不见了,Git为什么还能读?用两个pack实验拆开身份、索引与delta

在.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-format=sha1 -b master G config user.name 'Blog experiment' G config user.email 'blog@example.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,也可以有pack

G repack -a -d --window=0 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)))

window=0禁用这次打包的delta搜索;prune-packed只清除已在包中存在的松散副本,不是删除唯一对象。新仓库此时只有一组pack/idx,才可以明确选择那个idx;不能把“取第一个文件”的写法照搬到已有多包仓库。

本轮得到六个对象、零个delta;对象集合和HEAD都不变。即使每个对象仍采用完整内容表示,pack已经成立。Git repack手册

3. idx提供的是位置,不是另一份文件内容

verify-pack里普通对象行包含对象ID、类型、对象大小、包内大小和offset。delta行还给出深度及base对象ID;其中size不能直接当作重建后的完整对象长度,本轮旧blob的delta行显示18,cat-file读取其完整内容长度却是46000。offset是包内位置,不是对象的新名字,也不是文件在工作区里的位置。

层稳定或变化的东西本例检查手段
身份请求哪个对象ID打包前后比较完整ID集合
定位loose路径或idx中的pack偏移列出pack/idx,查看verify-pack
表示完整压缩对象或delta检查delta行与base信息
内容调用者最终读到的字节从包读blob,重新计算其ID

v2 idx用fan-out和排序ID表帮助定位条目,再取得offset;pack里才存对象的表示。这里是查询路径示意,不把图当字节格式说明。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. 重新打包,为什么先没有得到delta

G repack -a -d --window=50 --depth=50 $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 --window=50 --depth=50 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把提交删掉了”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 8:06:45

视频会议系统设计技术指南:从H.323协议到H.264编解码与部署实践

简介:面向智能视频会议系统规划与实施人员,这是一份可直接参考的技术解决方案文档,适用于政企、教育、医疗等行业的远程会议、应急指挥、远程培训场景,可辅助需求梳理、方案设计与项目汇报。文档以 doc 格式提供,压缩包…

作者头像 李华
网站建设 2026/10/6 8:06:21

一篇文章,吃透数据在内存中的存储

文章目录 整数在内存中的存储◦ 大小端字节序和字节序判断◦ 为什么会有大小端之分◦ 经典练习题 浮点数在内存中的存储浮点数存的过程浮点数取的过程 整数在内存中的存储 前面我们了解到整数的二进制表示方法有三种:原码,反码,补码。 对于有…

作者头像 李华
网站建设 2026/10/6 7:59:41

第二篇:keepalived VRRP 原理篇:协议与 VIP 漂移机制深入解析

Keepalived 原理篇:VRRP 协议与 VIP 漂移机制深入解析开篇很多运维朋友都用过 Keepalived,知道它能做高可用、能漂移 VIP,但对它"为什么能漂移"、VRRP 报文里到底传了什么、主备是怎么选举的,往往一知半解。本文作为 Ke…

作者头像 李华