
# 包文件

包文件是 git 高效存储和传输数据的机制。包文件不会将每个对象分别存储为 zlib 压缩文件，而是将多个对象合并到一个文件中，并可选择使用增量压缩。几乎所有克隆的仓库都会使用包文件。

## 包文件结构

包文件（`.git/objects/pack/pack-{hash}.pack`）包含：

```
Header:   PACK (4 bytes) + version (4 bytes, big-endian) + count (4 bytes, big-endian)
Entries:  [type+size varint] [delta-base] [zlib-compressed data] ...
Trailer:  checksum of the pack (20 bytes SHA-1, 32 bytes SHA-256)
```

### 对象类型编码

每个条目都以一个变长整数开头，其中类型和大小交错编码：

```
First byte: 1TTTSSSS
  TTT  = object type (1-4 for base objects, 6 for OFS_DELTA, 7 for REF_DELTA)
  SSSS = first 4 bits of uncompressed size
  MSB  = 1 means more size bytes follow
```

类型值：
| 值 | 类型 |
|-------|------|
| 1 | Commit |
| 2 | Tree |
| 3 | Blob |
| 4 | Tag |
| 6 | OFS_DELTA（基于偏移量的增量）|
| 7 | REF_DELTA（基于哈希的增量）|

## 读取包文件

```php
use Pitmaster\Pack\PackFile;

$pack = PackFile::open(
    '/path/to/.git/objects/pack/pack-abc123.pack',
    '/path/to/.git/objects/pack/pack-abc123.idx',
);

$pack->has($hex);               // Is the object in this pack?
$object = $pack->read($hex);    // Resolved GitObject, or null
$object = $pack->readAtOffset($offset);
$hashes = $pack->allHashes();
```

`PackFile` 类：
1. 验证 `PACK` 标头和版本号
2. 通过 `.idx` 文件查找偏移量，并解析条目的类型/大小变长整数
3. 解压 zlib 压缩的数据
4. 对于基对象：直接返回内容
5. 对于增量对象：解析基对象（沿着链逐层追溯）并应用增量，同时缓存已解析的对象

## 包索引文件

每个 `.pack` 文件都有对应的 `.idx` 文件以便快速查找。没有索引时，查找对象需要扫描整个包。

### v2 索引格式

```
Magic:    FF 74 4F 63 (identifies v2)
Version:  00 00 00 02
Fanout:   256 x 4-byte big-endian counts (cumulative)
Names:    N x 20-byte SHA-1 hashes, sorted (32 bytes for SHA-256)
CRC32s:   N x 4-byte CRC32 checksums
Offsets:  N x 4-byte pack offsets (MSB=1 means large offset table)
Large:    M x 8-byte offsets (for packs > 2GB)
Checksums: pack checksum + index checksum (20 bytes each for SHA-1)
```

```php
use Pitmaster\Pack\PackIndex;

$index = PackIndex::open('/path/to/pack-abc123.idx');

// Look up an object
$offset = $index->findOffset($hex);  // Pack file offset, or null

// List all objects in the pack
$hashes = $index->allHashes();
$count = $index->objectCount();
```

扇出表支持 O(log n) 二分查找：256 条目表示以每种可能的首字节开头的哈希数量。

### v1 索引格式

不包含 CRC32 校验和的旧格式。Pitmaster 同时支持两种格式：若缺少 v2 魔数，`PackIndex::open()` 会读取 v1 文件，因此同一段代码可用于任一种格式。

## 增量解析

增量对象存储的是从基对象重建内容的指令，而非完整内容。对于相似对象（e.g，同一文件的连续版本），这样可以大幅提高压缩率。

### 增量类型

**OFS_DELTA**（类型 6）：基对象由同一包文件中的负偏移量标识。这种方式更常见，因为不需要知道基对象的哈希。

**REF_DELTA**（类型 7）：基对象由其哈希标识。在 git 中，基对象可以位于包外部（精简包）。Pitmaster 只会在同一包内查找 REF_DELTA 基对象，因此不支持精简包（[#16](https://github.com/inline0/pitmaster/issues/16)）。

### 增量指令

增量内容由一系列复制和插入指令组成：

```
Copy:   1XXXXXXX [offset bytes] [size bytes]   (copy from base)
Insert: 0LLLLLLL <L bytes of literal data>       (insert new data)
```

复制指令：7 个 X 位指示哪些偏移量和大小字节存在。偏移量最多为 4 字节，大小最多为 3 字节。如果大小为零，则表示 0x10000（65536）字节。

插入指令：7 个 L 位给出字面数据长度（1-127 字节）。L=0 表示保留/无效。

### DeltaApplier

```php
use Pitmaster\Pack\DeltaApplier;

$result = DeltaApplier::apply($baseContent, $deltaInstructions);
```

`DeltaApplier`：
1. 从增量标头（以 LEB128 编码）读取预期基对象大小和结果大小
2. 遍历各条指令：
   - 复制：提取偏移量和大小，并从基内容复制字节
   - 插入：提取长度，并从增量数据复制字面字节
3. 验证结果大小是否与预期大小一致

### 增量链

增量对象的基对象本身也可能是增量对象。`PackFile::read()` 和 `readAtOffset()` 会沿链逐层追溯：

1. 读取条目。如果它是基类型（commit、tree、blob、tag），则返回。
2. 如果是 OFS_DELTA：计算基偏移量（当前偏移量减去编码的负偏移量），读取基条目并递归处理。
3. 如果是 REF_DELTA：在同一包的索引中按哈希查找基对象，并递归处理。
4. 对已解析的基对象应用增量指令。
5. 执行 `PITMASTER_MAX_DELTA_CHAIN`（默认值为 50）限制，防止链条过长。

```php
// Configure max chain depth (integer)
define('PITMASTER_MAX_DELTA_CHAIN', 50);

// Read the effective limit
$max = \Pitmaster\Pack\DeltaResolver::maxChainDepth();
```

## PackWriter

Pitmaster 可以为网络操作（推送）写入包文件。

```php
use Pitmaster\Pack\PackWriter;
use Pitmaster\Pack\PackIndexer;

$packData = PackWriter::encode($objects);           // GitObject[] to pack bytes
$paths = PackWriter::write($packDir, $objects);     // ['packPath' => ..., 'idxPath' => ..., 'hash' => ...]

$idxPath = PackIndexer::writeIndex($packPath);      // Build the .idx for a received pack
```

`PackWriter`：
1. 收集要包含在包中的对象
2. 使用类型/大小变长整数标头序列化每个对象
3. 使用 zlib 压缩内容
4. 写入 PACK 标头、条目和 SHA-1 尾部

目前只写入基对象（不进行增量压缩）。之后可以添加增量压缩优化；让 `git gc` 重新打包以提高存储效率。

## PackEnumerator

按索引顺序遍历包中的所有对象：

```php
use Pitmaster\Pack\PackEnumerator;

$enumerator = new PackEnumerator($packFile); // a PackFile opened with its .idx

foreach ($enumerator->enumerate() as $object) {
    echo "{$object->type->value} {$object->id->hex} {$object->size()}\n";
}
```

每一项都是已解析的 `GitObject`。

## MultiPackIndex

对于包含大量包文件的仓库，多包索引（MIDX）可以跨所有包进行统一查找。

```php
use Pitmaster\Pack\MultiPackIndex;
```

MIDX 文件（`.git/objects/pack/multi-pack-index`）包含：
- 扇出表（类似包索引 v2）
- 按顺序排列的所有包中的对象 ID
- 包文件名和每个对象所属的包
- 各个包文件中的偏移量

`MultiPackIndex::open($path)` 会解析 MIDX 文件，并提供 `findObject($hex)` 和 `packNames()`。仓库中的对象查找不会使用它：对象数据库始终读取各个 `.idx` 文件。

## CommitGraph

commit-graph 文件可加快 commit 历史遍历和合并基点计算。

```php
use Pitmaster\Pack\CommitGraph;
```

它位于 `.git/objects/info/commit-graph`，包含：
- 按顺序排列的 commit OID
- 父级信息（已索引，无需读取对象）
- generation number（用于更快地查询可达性）
- commit 时间戳

存在该文件时，`Repository::merge()`、`mergeBase()`、`isBranchMerged()` 背后的合并基点计算以及 push 快进检查会从 commit-graph 读取父级链接，而不是读取各个 commit 对象。`CommitWalker`（log）不会使用它。

## 变长整数

包文件使用多种变长整数编码：

### Git 变长整数（类型/大小标头）

MSB-continue 编码中，最高有效位表示后面还有更多字节：

```php
use Pitmaster\Encoding\VarInt;

$size = VarInt::decodePackSize($reader, $initialBits);  // continues the size from the first header byte
$offset = VarInt::decodeOfsOffset($reader);             // OFS_DELTA base distance
$header = VarInt::encodePackSize($type, $size);
```

### LEB128（增量标头）

用于增量指令标头中的基大小和结果大小：

```php
use Pitmaster\Encoding\Leb128;

$value = Leb128::decodeUnsigned($reader);   // a BinaryReader
$bytes = Leb128::encodeUnsigned($value);
```

### BinaryReader

带位置跟踪的底层字节流：

```php
use Pitmaster\Encoding\BinaryReader;

$reader = new BinaryReader($data);
$byte = $reader->readByte();
$int = $reader->readUint32();   // big-endian
$bytes = $reader->read(20);
$hash = $reader->readHash(20);
$reader->position();  // Current offset
$reader->seek(0);
```
