写入仓库
Pitmaster 使用与标准 git 二进制程序相同的二进制格式写入 git 仓库。对象会写入松散对象存储(使用 zlib 压缩),索引会以 v2 格式写入(条目需要扩展标志时使用 v3;现有索引保留其版本),引用则写为松散文件。写入的数据可通过 git fsck --strict 验证。
初始化仓库
use Pitmaster\Pitmaster;
$repo = Pitmaster::init('/path/to/new-project');
这会创建:
.git/objects/(空对象存储).git/refs/heads/和.git/refs/tags/(空引用目录).git/HEAD,其中包含ref: refs/heads/main.git/hooks/、.git/info/exclude和.git/description.git/config,包含默认设置
将 'sha256' 作为第二个参数传入,以创建 SHA-256 仓库:Pitmaster::init($path, 'sha256')。
默认配置未设置 core.logallrefupdates,因此 Pitmaster 创建的仓库在你将该键设为 true 之前不会记录 reflog 条目(#8)。由 git init 创建的仓库已设置该项。
暂存文件
添加
$repo->add('src/Feature.php');
$repo->add('src/Feature.php', 'tests/FeatureTest.php', 'README.md');
每个参数都是相对于工作树根目录的文件路径。目录和 glob 不会展开,文件不存在时会抛出 RuntimeException。对于每个文件,Pitmaster 会:
- 从工作树读取文件内容
- 创建
Blob对象并计算其哈希值 - 将 blob 写入
.git/objects/(使用 zlib 压缩) - 使用新的哈希值和文件元数据更新该路径的索引条目
移除
$repo->remove('old-file.txt');
$repo->remove('-r', 'src');
$repo->remove('--cached', 'old-file.txt');
从索引和工作树中移除已跟踪路径。使用 --cached 仅从索引中移除,或使用 -r / --recursive 移除已跟踪目录。
移动
$repo->mv('old-name.php', 'new-name.php');
在工作树中重命名文件,移除旧索引条目,并添加新条目。
创建提交
$commitId = $repo->commit('Fix null pointer bug');
echo $commitId->hex;
提交过程:
- 从索引构建树:将索引条目的扁平列表组织为树层级结构(目录变为树对象,文件变为 blob 引用)。每棵树都会写入对象存储。
- 确定父项:当前 HEAD 提交将成为父项。根提交(仓库中的第一次提交)没有父项。
- 确定作者:依次从环境变量
PITMASTER_AUTHOR_*和GIT_AUTHOR_*、常量PITMASTER_AUTHOR_NAME/PITMASTER_AUTHOR_EMAIL、.git/config(user.name/user.email),最后从Pitmaster <pitmaster@localhost>获取。提交者也按相同方式确定。 - 写入提交对象:提交内容(
tree、parent、author、committer、消息)会被序列化、计算哈希值并写入。 - 更新 HEAD:HEAD 指向的分支引用会更新为新的提交哈希值。
除非句柄是使用 ['hooks' => false] 打开的,否则这些步骤前后都会运行钩子(pre-commit、prepare-commit-msg、commit-msg、post-commit)。缺少消息、树与 HEAD 相比没有变化,或索引中存在未合并路径时,commit() 会抛出异常。
自定义作者
$author = 'Jane Smith <jane@example.com> 1700000000 +0000';
$commitId = $repo->commit('Add feature', $author);
使用 git fsck 验证
Pitmaster 的集成测试会将写入的对象与 git fsck --strict 进行核验,你也可以手动执行相同验证:
cd /path/to/repo
git fsck --strict
如果 Pitmaster 写入了无效对象,git fsck 会立即报告错误。
管理分支
创建分支
// Branch from HEAD
$repo->createBranch('feature/new-thing');
// Branch from a specific commit
$repo->createBranch('hotfix', $repo->resolve('v1.0.0'));
在 .git/refs/heads/<name> 创建一个包含目标提交哈希值的引用文件。
删除分支
$repo->deleteBranch('feature/old');
切换分支
$repo->checkout('feature/login');
将 HEAD 更新为指向该分支的符号引用,将索引重置为与分支顶端的树一致,并更新工作树。
如果两个树之间存在差异的路径包含本地更改,检出操作会因 RuntimeException 而停止。其他所有已跟踪文件中的未提交更改都会被分支内容替换;git 会保留这些更改(#2)。切换之前请先提交或暂存这些更改。
检查是否已合并
if ($repo->isBranchMerged('feature/done')) {
$repo->deleteBranch('feature/done');
}
管理标签
轻量标签
$repo->createLightweightTag('v1.0.0'); // At HEAD
$repo->createLightweightTag('v0.9.0', $repo->resolve('HEAD~5'));
轻量标签只是直接指向某个提交的引用。
带注释的标签
$tagId = $repo->createTag('v2.0.0', 'Release version 2.0.0');
带注释的标签会创建一个标签对象(包含标记者、消息和目标),以及一个指向它的引用。标记者是当前提交者身份,其解析方式与提交作者相同。
自定义标记者
$tagId = $repo->createTag(
'v2.0.0',
'Release version 2.0.0',
$repo->resolve('HEAD'),
'Jane Smith <jane@example.com> 1700000000 +0000',
);
直接更新引用
$repo->updateRef('refs/heads/main', $commitId);
$repo->updateRef('refs/tags/latest', $commitId);
直接写入对象
对于高级用例,你可以不通过高级别的 API,直接写入对象。
写入 blob
use Pitmaster\Object\Blob;
$blob = Blob::fromContent('Hello, world!');
$id = $repo->writeObject($blob);
echo $id->hex; // SHA-1 of "blob 13\0Hello, world!"
在 SHA-256 仓库中,传入算法:Blob::fromContent('Hello, world!', 'sha256')。
写入树
use Pitmaster\Object\Tree;
use Pitmaster\Object\TreeEntry;
use Pitmaster\Object\ObjectId;
$entries = [
new TreeEntry('100644', 'README.md', $blobId),
new TreeEntry('40000', 'src', $subtreeId),
];
$tree = Tree::fromEntries($entries);
$id = $repo->writeObject($tree);
Tree::fromEntries() 会按给定顺序写入条目,并按给定方式写入模式字符串。请按 git 的树顺序传入(按名称排序,比较目录时视为其名称以 / 结尾),并将目录模式写作 40000:git 存储树模式时会去掉前导零,而 git fsck --strict 会拒绝 040000。
写入提交
use Pitmaster\Object\Commit;
use Pitmaster\Object\ObjectId;
use Pitmaster\Object\ObjectType;
$content = Commit::buildContent(
tree: $treeId,
parents: [$parentId],
author: 'Name <email> 1700000000 +0000',
committer: 'Name <email> 1700000000 +0000',
message: "Commit message\n",
);
$commitId = ObjectId::compute(ObjectType::Commit, $content);
$commit = Commit::parse($content, $commitId);
$repo->writeObject($commit);
重置
// Soft: move HEAD only
$repo->reset('HEAD~1', 'soft');
// Mixed: move HEAD + reset index (default)
$repo->reset('HEAD~1');
// Hard: move HEAD + reset index + reset working tree
$repo->reset('HEAD~3', 'hard');
还原
// Restore a file from the index into the worktree
$repo->restore('src/File.php');
// Restore the index version from HEAD (the worktree is left alone)
$repo->restore('src/File.php', staged: true);
// Restore from a specific commit
$repo->restore('src/File.php', 'HEAD~1');
// Restore both index and worktree from a specific commit
$repo->restore('src/File.php', 'HEAD~1', staged: true, worktree: true);
Cherry-pick 和 revert
// Apply a commit on the current branch
$newId = $repo->cherryPick('feature~1');
// Undo a commit
$revertId = $repo->revert('HEAD~2');
两者都接受 resolve() 支持的任何修订版本。完整哈希值有效;缩写哈希值无效。