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