
# 储藏

Git 储藏会保存工作目录和索引中的更改，让你可以切换到其他任务，之后再恢复这些更改。Pitmaster 使用 `refs/stash` 和 reflog，在 git 的存储布局中实现储藏。`Repository` 没有储藏方法；请使用下面的 `Stash` 类。

有两个已知问题：当条目多于一个时，`drop()`（因此 `pop()` 也是）会将条目留在列表中（[#10](https://github.com/inline0/pitmaster/issues/10)）；`apply()` 会覆盖储藏未涉及文件中的未提交更改（[#11](https://github.com/inline0/pitmaster/issues/11)）。请在干净的工作树上应用储藏。

## 储藏的工作原理

每个储藏条目都是一个有两个父提交的提交：
- **父提交 0**：储藏时的 HEAD 提交（基准状态）
- **父提交 1**：用于记录索引状态（已暂存更改）的提交

储藏提交自身的树记录了工作树状态（包括未暂存的更改）。

储藏“栈”是 `refs/stash` 的 reflog。最新的储藏就是 `refs/stash` 本身。较早的条目是 reflog 条目：`stash@{0}`、`stash@{1}` 等。

```
refs/stash -> stash commit (worktree state)
                parent[0] -> HEAD at time of stash
                parent[1] -> index state commit
                               parent[0] -> HEAD at time of stash
```

## 创建 Stash 对象

```php
use Pitmaster\Stash\Stash;

$stash = new Stash(
    $repo->objectDatabase(),
    $repo->refDatabase(),
    $repo->gitDir(),
    $repo->workDir(),
);
```

## Push（保存更改）

```php
// public function push(string $message = '', bool $includeUntracked = false): ObjectId
$stashId = $stash->push('Work in progress on login');
echo "Saved: {$stashId->hex}\n";

// Also stash untracked files
$stashId = $stash->push('With new files', includeUntracked: true);
```

Push 会执行以下操作：
1. 读取当前 HEAD 提交
2. 根据当前索引构建树（已暂存状态）
3. 创建以 HEAD 为父提交的“索引提交”
4. 根据当前工作目录构建树（包括未暂存的更改）
5. 创建储藏提交（工作树树对象，父提交 = HEAD + 索引提交）
6. 更新 `refs/stash`，使其指向储藏提交
7. 为栈追加一条 reflog 记录
8. 将工作树和索引重置为 HEAD

Push 后，已跟踪文件与 HEAD 一致。未跟踪文件会保留，除非设置了 `includeUntracked`；设置后，这些文件会被删除并存储在储藏提交自身的树中（git 则会将它们存储在第三个父提交中，因此 git 会将它们视为已跟踪的更改）。即使没有任何更改，`push()` 也会创建条目，此时 git 会报告“没有本地更改可保存”。

### 使用默认消息

```php
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"
```

在分离的 HEAD 状态下，`<branch>` 是 `HEAD`。

## Pop（应用并移除）

```php
$stash->pop();
```

将栈顶的储藏条目应用到工作目录，并从栈中移除。等同于先执行 `apply()`，再执行 `drop()`。

### 弹出指定条目

```php
$stash->pop(1);  // Pop stash@{1}
```

## Apply（恢复但不移除）

```php
$stash->apply();      // Apply stash@{0}
$stash->apply(2);     // Apply stash@{2}
```

Apply 会逐个文件将储藏合并到当前 HEAD（储藏基准、当前 HEAD、储藏树）。储藏条目仍保留在栈中，索引也不会被修改。发生冲突时，文件中会留下标记 `Updated upstream` 和 `Stashed changes`，并抛出 `Pitmaster\Exceptions\MergeConflictException`；随后 `pop()` 会保留该条目。Apply 不会先检查本地更改（见顶部说明）。

## 列出条目

```php
$entries = $stash->listEntries();

foreach ($entries as $entry) {
    echo "stash@{{$entry['index']}}: {$entry['message']}\n";
    // stash@{0}: On main: Work in progress on login
    // stash@{1}: WIP on main: f707601 Add login form
}
```

每个条目包含：
- `index`：储藏位置（0 = 最新）
- `message`：储藏消息
- `hash`：储藏提交哈希

## Drop（移除但不应用）

```php
$stash->drop();       // Drop stash@{0}
$stash->drop(1);      // Drop stash@{1}
```

如果删除唯一条目，`refs/stash` 及其 reflog 会被移除。如果还有其他条目，`drop()` 目前只会移动 `refs/stash`，而不会更改 reflog，因此该条目仍会留在 `listEntries()` 中（[#10](https://github.com/inline0/pitmaster/issues/10)）。

## 存储详情

### 储藏提交结构

```
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>

<stash message>
```

### 索引提交结构

```
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>

index on <branch>: <short hash> <HEAD subject>
```

身份信息依次取自仓库配置中的 `GIT_COMMITTER_NAME`、`GIT_AUTHOR_NAME`、`user.name`，然后是 `Pitmaster`（电子邮件也遵循相同顺序，以 `pitmaster@example.invalid` 结尾）。日期依次取自 `GIT_COMMITTER_DATE`、`GIT_AUTHOR_DATE`，然后是当前时间。与提交不同，储藏不会读取 `PITMASTER_*` 常量。

### Reflog 存储

储藏条目存储在 `.git/logs/refs/stash` 中：

```
0000000000000000000000000000000000000000 abc123... Name <email> 1700000000 +0000	WIP on main: f707601 Add login form
abc123... def456... Name <email> 1700000001 +0000	On main: Work in progress on login
```

reflog 按时间顺序读取。储藏列表会将其倒序排列（最新的在前）。无论 `core.logallrefupdates` 如何，都会写入储藏 reflog。

## CLI 用法

```bash
# Save current changes
./vendor/bin/pitmaster stash
./vendor/bin/pitmaster stash push
./vendor/bin/pitmaster stash push "Work in progress on login"

# Apply and remove top entry
./vendor/bin/pitmaster stash pop

# Apply without removing
./vendor/bin/pitmaster stash apply

# List all entries
./vendor/bin/pitmaster stash list

# Remove top entry without applying
./vendor/bin/pitmaster stash drop
```

消息是 `push` 后面的参数（或 `save`）；不支持 `-m`。`pop`、`apply` 和 `drop` 始终作用于 `stash@{0}`；CLI 没有 `--include-untracked`。

## 限制

- 仅支持完整工作目录储藏（不支持 `--keep-index`）；未跟踪文件只能通过 `push($message, includeUntracked: true)` 储藏
- 不支持 `--patch` 模式（交互式选择代码块）
- Pop/apply 会恢复工作树状态，但不会恢复索引。已暂存的编辑会以未暂存编辑的形式恢复，新暂存的文件会以未跟踪文件的形式恢复
- `stash@{n}` 不能由 `Repository::resolve()` 接受
- 在关联工作树中储藏时会使用错误的配置和 reflog 位置（[#14](https://github.com/inline0/pitmaster/issues/14)）
