
# スタッシュ

Git stashは作業ディレクトリとインデックスの変更を保存するため、別のタスクに切り替えてから後で復元できます。Pitmasterは`refs/stash`とreflogを使い、gitのストレージレイアウトに沿ってstashを実装します。`Repository`にはstashメソッドがありません。以下の`Stash`クラスを使用してください。

既知の問題が2つあります。エントリーが複数ある場合、`drop()`（したがって`pop()`も）はエントリーをリストに残します（[#10](https://github.com/inline0/pitmaster/issues/10)）。また、`apply()`はstashが触れないファイルの未コミット変更を上書きします（[#11](https://github.com/inline0/pitmaster/issues/11)）。クリーンな作業ツリーにstashを適用してください。

## stashの仕組み

各stashエントリーは、2つの親を持つコミットです。
- **親0**: stash時点のHEADコミット（ベースの状態）
- **親1**: インデックスの状態（ステージ済みの変更）を記録するコミット

stashコミット自身のツリーには、作業ツリーの状態（ステージされていない変更を含む）が記録されます。

stashの「スタック」は`refs/stash`のreflogです。最新のstashは`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. stashコミットを作成する（作業ツリーのツリー、親はHEADとインデックスコミット）
6. `refs/stash`をstashコミットに向ける
7. スタックのreflogエントリーを追加する
8. 作業ツリーとインデックスをHEADにリセットする

Push後、追跡対象ファイルはHEADと一致します。`includeUntracked`が設定されていない限り、未追跡ファイルは残ります。設定されている場合は削除され、stashコミット自身のツリーに保存されます（gitでは代わりに第3の親に保存されるため、gitは追跡対象の変更として認識します）。`push()`は、変更がなくてもエントリーを作成します。その場合、gitは「No local changes to save」と報告します。

### デフォルトのメッセージを使う場合

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

スタックの先頭のstashエントリーを作業ディレクトリに適用し、スタックから削除します。`apply()`を実行してから`drop()`を実行するのと同等です。

### 特定のエントリーをPopする

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

## Apply（削除せずに復元）

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

Applyはstashを現在のHEADのファイルごとにマージします（stashのベース、現在のHEAD、stashツリー）。stashエントリーはスタックに残り、インデックスは変更されません。競合があると、ファイルに`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`: stashの位置（0が最新）
- `message`: stashのメッセージ
- `hash`: stashコミットのハッシュ

## 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)）。

## ストレージの詳細

### stashコミットの構造

```
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`、それらがなければ現在時刻から取得します。コミットとは異なり、stashは`PITMASTER_*`定数を読み取りません。

### Reflogの保存

stashエントリーは`.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は時系列順に読み込まれます。stash一覧では逆順（新しいものが先）になります。stashのreflogは`core.logallrefupdates`に関係なく書き込まれます。

## 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`がありません。

## 制限事項

- 作業ディレクトリ全体のstashのみ（`--keep-index`なし）。未追跡ファイルは`push($message, includeUntracked: true)`経由でのみ保存
- `--patch`モード（対話形式のハンク選択）なし
- Pop/Applyは作業ツリーの状態を復元しますが、インデックスは復元しません。ステージ済みの編集はステージされていない編集として戻り、新たにステージされたファイルは未追跡ファイルとして戻ります
- `stash@{n}`は`Repository::resolve()`では受け付けられません
- リンクされたworktree内のstashは、誤った設定とreflogの場所を使用します（[#14](https://github.com/inline0/pitmaster/issues/14)）
