Тайник
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
use Pitmaster\Stash\Stash;
$stash = new Stash(
$repo->objectDatabase(),
$repo->refDatabase(),
$repo->gitDir(),
$repo->workDir(),
);
Push (сохранение изменений)
// 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 выполняет следующие действия:
- Читает текущий коммит HEAD
- Создает дерево на основе текущего индекса (подготовленного состояния)
- Создает «коммит индекса» с HEAD в качестве родителя
- Создает дерево на основе текущего рабочего каталога (включая неподготовленные изменения)
- Создает коммит stash (дерево рабочего каталога, родители = HEAD + коммит индекса)
- Перемещает
refs/stashна коммит stash - Добавляет запись reflog для стека
- Сбрасывает рабочее дерево и индекс до состояния HEAD
После push отслеживаемые файлы соответствуют HEAD. Неотслеживаемые файлы остаются, если не задан includeUntracked; в таком случае они удаляются и сохраняются в собственном дереве коммита stash (git сохраняет их в третьем родителе, поэтому git видит их как отслеживаемые изменения). push() создает запись, даже если ничего не изменилось; в этом случае git сообщает «Нет локальных изменений для сохранения».
С сообщением по умолчанию
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"
При отсоединенном HEAD <branch> — это HEAD.
Pop (применение и удаление)
$stash->pop();
Применяет верхнюю запись stash к рабочему каталогу и удаляет ее из стека. Эквивалентно apply(), за которым следует drop().
Извлечение определенной записи
$stash->pop(1); // Pop stash@{1}
Apply (восстановление без удаления)
$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 не проверяет наличие локальных изменений заранее (см. примечание в начале).
Список записей
$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: сообщение stashhash: хеш коммита stash
Drop (удаление без применения)
$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)