
# Тайник

Git stash сохраняет изменения рабочего каталога и индекса, чтобы переключиться на другую задачу и позже восстановить их. Pitmaster реализует stash с помощью `refs/stash` и reflog, используя структуру хранения git. У `Repository` нет метода stash; используйте класс `Stash` ниже.

Известны две проблемы: если записей больше одной, `drop()` (а значит, и `pop()`) оставляет запись в списке ([#10](https://github.com/inline0/pitmaster/issues/10)), а `apply()` перезаписывает незакоммиченные изменения в файлах, которых stash не касается ([#11](https://github.com/inline0/pitmaster/issues/11)). Применяйте stash только в чистом рабочем дереве.

## Как работает stash

Каждая запись stash — это коммит с двумя родителями:
- **Родитель 0**: коммит HEAD на момент создания stash (базовое состояние)
- **Родитель 1**: коммит, сохраняющий состояние индекса (проиндексированные изменения)

Собственное дерево коммита stash сохраняет состояние рабочего дерева, включая непроиндексированные изменения.

«Стек» stash — это reflog для `refs/stash`. Самый свежий 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 сохраняет их в третьем родителе, поэтому 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();
```

Применяет верхнюю запись stash к рабочему каталогу и удаляет ее из стека. Эквивалентно `apply()`, за которым следует `drop()`.

### Извлечение определенной записи

```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 меняет этот порядок на обратный (сначала новые записи). Reflog stash записывается независимо от `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()`
- Stash в связанном рабочем дереве использует неправильное расположение конфигурации и reflog ([#14](https://github.com/inline0/pitmaster/issues/14))
