Перейти к содержимому

Тайник

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

Известны две проблемы: если записей больше одной, drop() (а значит, и pop()) оставляет запись в списке (#10), а apply() перезаписывает незакоммиченные изменения в файлах, которых stash не касается (#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).

Сведения о хранении

Структура коммита 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

Командная оболочка
# 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)