Перейти до вмісту

Сховище змін

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

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)