Git 对象
git 仓库中的所有内容都以对象形式存储。对象有四种类型:blob(文件内容)、tree(目录列表)、commit(带元数据的快照)和 tag(带注释的引用)。每个对象都由其内容的 SHA-1 哈希标识。
对象类型
Blob
blob 保存原始文件内容,不包含元数据(如文件名和权限)。
use Pitmaster\Object\Blob;
// Create from content
$blob = Blob::fromContent('Hello, world!');
echo $blob->content; // 'Hello, world!'
echo $blob->size(); // 13
echo $blob->id->hex; // SHA-1 hash
echo $blob->type->value; // 'blob' (the property is ObjectType::Blob)
树
tree 是目录列表。每个条目都包含模式、名称和一个指向 blob 或子 tree 的哈希。
use Pitmaster\Object\Tree;
use Pitmaster\Object\TreeEntry;
// Read a tree
$tree = $repo->readObject($treeHash);
foreach ($tree->entries as $entry) {
echo "{$entry->mode} {$entry->name} {$entry->hash->hex}\n";
}
// Look up a specific entry
$entry = $tree->entry('README.md');
// Create a tree (entries already in git's sort order)
$entries = [
new TreeEntry('100644', 'README.md', $blobId),
new TreeEntry('100755', 'deploy.sh', $execBlobId),
new TreeEntry('40000', 'src', $subtreeId),
];
$tree = Tree::fromEntries($entries);
Tree::fromEntries() 会按给定顺序写入条目,并原样写入给定的模式字符串,因此请先排序,并将目录模式写为 40000。使用 040000 写入的 tree 或按其他顺序写入的 tree,其哈希会与 git 的不同,而 git fsck --strict 会报告补零后的模式值。
TreeEntry
TreeEntry 是一个只读值对象:
use Pitmaster\Object\TreeEntry;
$entry->mode; // '100644', '100755', '40000', '120000', '160000'
$entry->name; // Filename
$entry->hash; // ObjectId
// Type checks
$entry->isBlob(); // regular or executable file (mode 10xxxx)
$entry->isExecutable(); // mode 100755
$entry->isTree(); // mode 40000 (also accepts 040000)
$entry->isSymlink(); // mode 120000
$entry->isGitlink(); // mode 160000 (submodule)
模式值:
| 模式 | 含义 |
|---|---|
100644 |
普通文件 |
100755 |
可执行文件 |
40000 |
目录(tree);git ls-tree 将其显示为 040000 |
120000 |
符号链接 |
160000 |
Gitlink(子模块) |
Tree 条目按名称排序;比较时,目录名会被视为以 / 结尾。
提交
commit 将 tree 与其历史记录和元数据关联起来。
use Pitmaster\Object\Commit;
$commit = $repo->head();
$commit->id; // ObjectId
$commit->tree; // ObjectId of the tree
$commit->parents; // array of ObjectId (zero for root, one for normal, two+ for merge)
$commit->author; // 'Name <email> timestamp timezone'
$commit->committer; // 'Name <email> timestamp timezone'
$commit->message; // Full commit message
$commit->isMerge(); // true if two or more parents
构建 commit 内容
$content = Commit::buildContent(
tree: $treeId,
parents: [$parentId],
author: 'Jane <jane@example.com> 1700000000 +0000',
committer: 'Jane <jane@example.com> 1700000000 +0000',
message: "Add feature\n",
);
原始 commit 格式:
tree <hex hash>\n
parent <hex hash>\n (zero or more)
author <name> <<email>> <timestamp> <tz>\n
committer <name> <<email>> <timestamp> <tz>\n
\n
<message>
标签
带注释的 tag 是包含元数据的对象。
use Pitmaster\Object\Tag;
$tag = $repo->readObject($tagHash);
$tag->object; // ObjectId of the target
$tag->objectType; // ObjectType enum (ObjectType::Commit, ...)
$tag->name; // Tag name
$tag->tagger; // 'Name <email> timestamp timezone'
$tag->message; // Tag message
原始 tag 格式:
object <hex hash>\n
type commit\n
tag <name>\n
tagger <name> <<email>> <timestamp> <tz>\n
\n
<message>
ObjectId
ObjectId 是一个只读值对象,表示 SHA-1(或 SHA-256)哈希。
use Pitmaster\Object\ObjectId;
use Pitmaster\Object\ObjectType;
// From hex string
$id = ObjectId::fromHex('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2');
// From binary (20 bytes for SHA-1, 32 for SHA-256)
$id = ObjectId::fromBinary($rawBytes);
// Compute from content
$id = ObjectId::compute(ObjectType::Blob, 'Hello, world!');
$id = ObjectId::compute(ObjectType::Blob, 'Hello, world!', 'sha256');
// Zero hash
$id = ObjectId::zero();
$id = ObjectId::zero('sha256');
// Properties
$id->hex; // 40-character hex string (64 for SHA-256)
$id->binary; // 20-byte raw binary (32 for SHA-256)
$id->algo; // 'sha1' or 'sha256'
$id->isSha256();
$id->isZero();
// Comparison
$id->equals($otherId);
哈希根据未压缩数据计算:sha1("{type} {size},或对相同字节进行 SHA-256。fromHex() 和 fromBinary() 根据长度选择算法。
ObjectType 枚举
use Pitmaster\Object\ObjectType;
ObjectType::Blob; // 'blob'
ObjectType::Tree; // 'tree'
ObjectType::Commit; // 'commit'
ObjectType::Tag; // 'tag'
松散对象格式
松散对象以独立文件的形式存储在 .git/objects/ 中,并按哈希的前两个十六进制字符组织。
.git/objects/a1/b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
文件内容经过 zlib 压缩。解压后的内容为:
<type> <decimal size>\0<raw content>
例如,包含 "Hello" 的 blob 存储为:
zlib_encode("blob 5\0Hello")
读取松散对象
use Pitmaster\Storage\LooseObjectStore;
$store = new LooseObjectStore('/path/to/.git/objects');
$object = $store->read($objectId);
LooseObjectStore:
- 根据哈希计算文件路径
- 读取文件并使用 zlib 解压
- 解析标头以提取类型和大小
- 委托相应的解析器处理(
Blob、Tree、Commit、Tag)
写入松散对象
$store->write($object);
写入过程:
- 序列化:
"{type} {size}\0{content}" - 使用对象的
id(对象构建时计算;读取时会验证哈希) - 压缩:
zlib_encode(serialized) - 写入
.git/objects/{hash[0..1]}/{rest of hash} - 原子写入(先写入临时文件,再重命名)
ObjectSerializer
ObjectSerializer 负责对对象线格式进行编码和解码。
use Pitmaster\Storage\ObjectSerializer;
// Encode
$raw = ObjectSerializer::encodeRaw($object);
// "blob 5\0Hello"
$compressed = ObjectSerializer::encode($object);
// zlib-compressed form, as stored in a loose object file
// Decode
$object = ObjectSerializer::decodeRaw($raw);
$object = ObjectSerializer::decode($compressed, $expectedHex); // throws CorruptObjectException on a hash mismatch
ObjectDatabase
ObjectDatabase 是一个组合存储,先搜索松散对象,再搜索包文件。
use Pitmaster\Storage\ObjectDatabase;
$db = new ObjectDatabase('/path/to/.git/objects');
$object = $db->read($objectId); // Search loose, then packs
$db->write($object); // Always writes to loose
$exists = $db->exists($objectId); // Check loose, then packs
$all = $db->listAll(); // All hashes from loose + packs
有意将写入操作直接写入松散存储。让 git gc 将其重新打包为包文件。
SHA-256 支持
Pitmaster 通过 ObjectId 抽象哈希算法。SHA-1 是默认算法。创建仓库时会为每个仓库选择 SHA-256:
$repo = Pitmaster::init('/path/to/project', 'sha256');
./vendor/bin/pitmaster init --object-format=sha256
这会将 extensions.objectformat = sha256 写入配置。之后打开的仓库会从该配置项读取格式,随后所有对象、tree、索引和 ref 操作都会使用 64 个字符的十六进制字符串和 32 字节的二进制哈希。没有全局开关或算法常量。
自行构建对象时,对象类会将算法作为参数:Blob::fromContent($content, 'sha256')、Tree::fromEntries($entries, 'sha256')、ObjectId::compute($type, $content, 'sha256')。Pitmaster::clone() 会创建 SHA-1 仓库。
Tree 条目二进制格式
在 tree 对象的内容中,每个条目都编码为:
<octal mode as ASCII> <space> <filename> \0 <binary hash: 20 bytes SHA-1, 32 bytes SHA-256>
注意:哈希是原始二进制数据,而不是十六进制;这与 commit 和 tag 对象中的十六进制哈希不同。
条目按名称排序,比较时将目录视为末尾带有 /。要使 tree 的哈希与 git 的计算结果一致,排序顺序必须完全准确。