
# 写入仓库

Pitmaster 使用与标准 `git` 二进制程序相同的二进制格式写入 git 仓库。对象会写入松散对象存储（使用 zlib 压缩），索引会以 v2 格式写入（条目需要扩展标志时使用 v3；现有索引保留其版本），引用则写为松散文件。写入的数据可通过 `git fsck --strict` 验证。

## 初始化仓库

```php
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](https://github.com/inline0/pitmaster/issues/8)）。由 `git init` 创建的仓库已设置该项。

## 暂存文件

### 添加

```php
$repo->add('src/Feature.php');
$repo->add('src/Feature.php', 'tests/FeatureTest.php', 'README.md');
```

每个参数都是相对于工作树根目录的文件路径。目录和 glob 不会展开，文件不存在时会抛出 `RuntimeException`。对于每个文件，Pitmaster 会：
1. 从工作树读取文件内容
2. 创建 `Blob` 对象并计算其哈希值
3. 将 blob 写入 `.git/objects/`（使用 zlib 压缩）
4. 使用新的哈希值和文件元数据更新该路径的索引条目

### 移除

```php
$repo->remove('old-file.txt');
$repo->remove('-r', 'src');
$repo->remove('--cached', 'old-file.txt');
```

从索引和工作树中移除已跟踪路径。使用 `--cached` 仅从索引中移除，或使用 `-r` / `--recursive` 移除已跟踪目录。

### 移动

```php
$repo->mv('old-name.php', 'new-name.php');
```

在工作树中重命名文件，移除旧索引条目，并添加新条目。

## 创建提交

```php
$commitId = $repo->commit('Fix null pointer bug');
echo $commitId->hex;
```

提交过程：

1. **从索引构建树**：将索引条目的扁平列表组织为树层级结构（目录变为树对象，文件变为 blob 引用）。每棵树都会写入对象存储。
2. **确定父项**：当前 HEAD 提交将成为父项。根提交（仓库中的第一次提交）没有父项。
3. **确定作者**：依次从环境变量 `PITMASTER_AUTHOR_*` 和 `GIT_AUTHOR_*`、常量 `PITMASTER_AUTHOR_NAME`/`PITMASTER_AUTHOR_EMAIL`、`.git/config`（`user.name`/`user.email`），最后从 `Pitmaster <pitmaster@localhost>` 获取。提交者也按相同方式确定。
4. **写入提交对象**：提交内容（`tree`、`parent`、`author`、`committer`、消息）会被序列化、计算哈希值并写入。
5. **更新 HEAD**：HEAD 指向的分支引用会更新为新的提交哈希值。

除非句柄是使用 `['hooks' => false]` 打开的，否则这些步骤前后都会运行钩子（`pre-commit`、`prepare-commit-msg`、`commit-msg`、`post-commit`）。缺少消息、树与 HEAD 相比没有变化，或索引中存在未合并路径时，`commit()` 会抛出异常。

### 自定义作者

```php
$author = 'Jane Smith <jane@example.com> 1700000000 +0000';
$commitId = $repo->commit('Add feature', $author);
```

## 使用 git fsck 验证

Pitmaster 的集成测试会将写入的对象与 `git fsck --strict` 进行核验，你也可以手动执行相同验证：

```bash
cd /path/to/repo
git fsck --strict
```

如果 Pitmaster 写入了无效对象，`git fsck` 会立即报告错误。

## 管理分支

### 创建分支

```php
// 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>` 创建一个包含目标提交哈希值的引用文件。

### 删除分支

```php
$repo->deleteBranch('feature/old');
```

### 切换分支

```php
$repo->checkout('feature/login');
```

将 HEAD 更新为指向该分支的符号引用，将索引重置为与分支顶端的树一致，并更新工作树。

如果两个树之间存在差异的路径包含本地更改，检出操作会因 `RuntimeException` 而停止。其他所有已跟踪文件中的未提交更改都会被分支内容替换；git 会保留这些更改（[#2](https://github.com/inline0/pitmaster/issues/2)）。切换之前请先提交或暂存这些更改。

### 检查是否已合并

```php
if ($repo->isBranchMerged('feature/done')) {
    $repo->deleteBranch('feature/done');
}
```

## 管理标签

### 轻量标签

```php
$repo->createLightweightTag('v1.0.0');                          // At HEAD
$repo->createLightweightTag('v0.9.0', $repo->resolve('HEAD~5'));
```

轻量标签只是直接指向某个提交的引用。

### 带注释的标签

```php
$tagId = $repo->createTag('v2.0.0', 'Release version 2.0.0');
```

带注释的标签会创建一个标签对象（包含标记者、消息和目标），以及一个指向它的引用。标记者是当前提交者身份，其解析方式与提交作者相同。

### 自定义标记者

```php
$tagId = $repo->createTag(
    'v2.0.0',
    'Release version 2.0.0',
    $repo->resolve('HEAD'),
    'Jane Smith <jane@example.com> 1700000000 +0000',
);
```

## 直接更新引用

```php
$repo->updateRef('refs/heads/main', $commitId);
$repo->updateRef('refs/tags/latest', $commitId);
```

## 直接写入对象

对于高级用例，你可以不通过高级别的 API，直接写入对象。

### 写入 blob

```php
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')`。

### 写入树

```php
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`。

### 写入提交

```php
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);
```

## 重置

```php
// 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');
```

## 还原

```php
// 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

```php
// Apply a commit on the current branch
$newId = $repo->cherryPick('feature~1');

// Undo a commit
$revertId = $repo->revert('HEAD~2');
```

两者都接受 `resolve()` 支持的任何修订版本。完整哈希值有效；缩写哈希值无效。
