
# Сховище змін

Git зберігає зміни робочого каталогу та індексу, щоб ви могли перейти до іншого завдання, а згодом відновити їх. 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))
