索引格式
索引文件(.git/index)是暂存区。它记录将纳入下次提交的文件,以及用于高效计算状态的元数据。Pitmaster 可读取索引版本 2、3 和 4,并写入版本 2 和 3。将以版本 4 读取的索引写入后,会生成 git 无法读取的文件(#15),因此请先用 git update-index --index-version 2 转换此类仓库,再通过 Pitmaster 写入。
读取索引
use Pitmaster\Index\Index;
$index = Index::open('/path/to/.git/index'); // pass 32 as the second argument for SHA-256
// Or through the repository
$index = $repo->index();
// Entry count (all stages)
echo $index->count();
// Iterate stage 0 entries, keyed by path
foreach ($index->entries() as $path => $entry) {
echo "{$entry->path} {$entry->hash->hex}\n";
}
// Look up a specific entry
$entry = $index->entry('src/File.php');
文件不存在时,Index::open() 会返回空索引。entries() 仅返回阶段 0 条目;allEntries() 包含冲突阶段,paths() 则列出已跟踪路径。
IndexEntry
索引中的每个条目都是只读值对象:
use Pitmaster\Index\IndexEntry;
$entry->path; // File path relative to repo root
$entry->hash; // ObjectId of the blob
$entry->mode; // File mode as an int (0100644, 0100755, 0120000, 0160000)
$entry->fileSize; // File size in bytes
$entry->ctimeSec; // Metadata change time (seconds)
$entry->ctimeNsec; // Metadata change time nanoseconds
$entry->mtimeSec; // Modification time (seconds)
$entry->mtimeNsec; // Modification time nanoseconds
$entry->dev; // Device number
$entry->ino; // Inode number
$entry->uid; // User ID
$entry->gid; // Group ID
$entry->flags; // Raw flags field
$entry->extendedFlags; // v3 extended flags (skip-worktree, intent-to-add)
$entry->stage(); // Conflict stage (0 = normal, 1 = base, 2 = ours, 3 = theirs)
$entry->assumeValid(); // Assume-valid bit
$entry->skipWorktree(); // Skip-worktree bit
创建条目
// From file stat
$entry = IndexEntry::fromStat('src/File.php', $blobId, '/full/path/to/src/File.php');
// Minimal (no stat info)
$entry = IndexEntry::create('src/File.php', $blobId);
// With explicit mode
$entry = IndexEntry::create('deploy.sh', $blobId, 0100755);
修改索引
// Add or replace an entry
$index->addEntry($entry);
// Remove an entry
$index->removeEntry('src/OldFile.php');
// Replace all stages of a conflicted path with one resolved entry
$index->resolveConflict('src/File.php', $entry);
addEntries() 和 removeEntries() 可在一次操作中处理多个条目或路径。对条目的任何更改都会丢弃已存储的扩展(见下文)。
写入索引
use Pitmaster\Index\IndexWriter;
IndexWriter::write($index, '/path/to/.git/index');
也可以通过仓库写入(会自动处理):
$repo->add('src/File.php'); // Reads file, creates blob, updates index, writes
$repo->remove('--cached', 'old-file.txt'); // Removes from index only, writes
二进制格式
头部
Signature: DIRC (4 bytes, ASCII)
Version: 4 bytes, big-endian (2, 3, or 4)
Entry count: 4 bytes, big-endian
条目格式(v2)
每个条目包含:
ctime seconds: 4 bytes, big-endian
ctime nanoseconds: 4 bytes, big-endian
mtime seconds: 4 bytes, big-endian
mtime nanoseconds: 4 bytes, big-endian
dev: 4 bytes, big-endian
ino: 4 bytes, big-endian
mode: 4 bytes, big-endian
uid: 4 bytes, big-endian
gid: 4 bytes, big-endian
file size: 4 bytes, big-endian
object hash: 20 bytes SHA-1 or 32 bytes SHA-256 (raw binary)
flags: 2 bytes, big-endian
path: variable length, NUL-terminated
padding: 1-8 NUL bytes to align to 8-byte boundary
标志字段:
- 位 15:假定有效标志
- 位 13-12:合并阶段(0-3)
- 位 11-0:路径名称长度;如果路径长度达到或超过 4095 字节,则为 0xFFF(读取器随后会扫描 NUL)
版本差异
版本 2:上述基本格式。最常见。
版本 3:增加扩展标志(常规标志后额外增加 2 个字节)。支持 skip-worktree 和 intent-to-add 位。在任何条目需要扩展标志时使用。
版本 4:增加路径前缀压缩。不存储完整路径,而是为每个条目存储从前一个条目的路径中移除的字节数,后接新的后缀。对于嵌套层级很深的树,此格式可显著缩小索引大小。
冲突阶段
合并冲突期间,同一路径可以在不同阶段拥有多个索引条目:
| 阶段 | 含义 |
|---|---|
| 0 | 正常(无冲突) |
| 1 | 基础版本(公共祖先) |
| 2 | 我方(当前分支) |
| 3 | 对方(正在合并的分支) |
// During a conflict, entries have non-zero stages
foreach ($index->allEntries() as $entry) {
if ($entry->stage() > 0) {
echo "Conflict: {$entry->path} (stage {$entry->stage()})\n";
}
}
$index->hasUnmerged(); // true while conflicts remain
$index->unmergedPaths(); // conflicted paths
$index->stageEntries('a.txt'); // [1 => base, 2 => ours, 3 => theirs]
索引扩展
扩展追加在条目之后、校验和之前。每个扩展包含 4 字节签名、4 字节大小和可变长度数据。
Pitmaster 会将扩展块作为原始字节保留($index->extensions() 返回 signature 和 data 对),并在条目未更改时原样写回。它不会解析或生成任何扩展;条目一旦更改,所有扩展都会被丢弃,git 会根据需要接受并重建这些扩展。以下列出 git 使用的扩展,供参考。
TREE 扩展
为目录缓存预先计算的树对象。当只有索引发生更改时,可避免重新计算树,从而加快 git status 和 git commit。
签名:TREE(0x54524545)
REUC 扩展
记录解决冲突时用于撤销恢复的信息。解决冲突后,原始阶段 1/2/3 条目会移到这里,以便通过 git checkout -m 恢复。
签名:REUC(0x52455543)
其他扩展
git 还会写入:
EOIE(索引条目结束):标记条目结束和扩展开始的位置IEOT(索引条目偏移表):支持并行加载索引link(拆分索引):供使用拆分索引的超大型仓库使用
在 git 的格式中,签名以大写字母开头的扩展是可选的,以小写字母开头的扩展是必需的。Pitmaster 不作检查,接受这两类扩展。拆分索引(link)不会加载,因此不支持使用拆分索引的仓库。
校验和
索引文件以仓库哈希算法对之前所有字节计算出的校验和结尾(SHA-1 为 20 字节,SHA-256 为 32 字节)。Pitmaster 会在保存时写入校验和,但读取时不会验证。
缺少 DIRC 签名或遇到不支持的版本时,Index::open() 和 Index::parse() 会抛出 IndexParseException。
索引与状态
索引是计算状态的核心。WorkingTreeStatus 会比较:
- HEAD 树与索引:检测已暂存的更改
- 索引与工作树:检测未暂存的更改
stat 字段(ctime、mtime、dev、ino、size、mode、uid、gid)可在不读取文件内容的情况下快速检查“文件是否已更改”。如果 stat 字段匹配,且文件的最后修改时间早于状态扫描的开始时间,就会假定文件未更改。大小不同则会立即标记为已修改。否则,Pitmaster 会读取文件并计算其哈希,以检查实际内容是否发生更改。
假定有效标志
if ($entry->assumeValid()) {
// Entry has assume-valid set (flags & 0x8000)
}
此标志告知 git 在计算状态时跳过对文件的检查,即使 stat 字段已更改也是如此(git update-index --assume-unchanged)。Pitmaster 重写条目时会保留此位,但其状态检查不会遵循此标志,仍会比较文件。