Stash
Git stash zapisuje zmiany w katalogu roboczym i indeksie, aby można było przejść do innego zadania, a później je przywrócić. Pitmaster implementuje stash za pomocą refs/stash i refloga, zgodnie z układem przechowywania git. Klasa Repository nie ma metody stash; użyj poniższej klasy Stash.
Znane są dwa problemy: gdy istnieje więcej niż jeden wpis, drop() (a zatem i pop()) pozostawia wpis na liście (#10), a apply() nadpisuje niezatwierdzone zmiany w plikach, których stash nie obejmuje (#11). Stosuj stash na czystym drzewie roboczym.
Jak działa stash
Każdy wpis stash to commit z dwoma rodzicami:
- Rodzic 0: commit HEAD w chwili utworzenia stash (stan bazowy)
- Rodzic 1: commit zawierający stan indeksu (zmiany przygotowane)
Własne drzewo commitu stash zapisuje stan drzewa roboczego (w tym zmiany nieprzygotowane).
Stos stash to reflog refs/stash. Najnowszy stash to samo refs/stash. Starsze wpisy znajdują się w reflogu: stash@{0}, stash@{1} itd.
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
Tworzenie obiektu Stash
use Pitmaster\Stash\Stash;
$stash = new Stash(
$repo->objectDatabase(),
$repo->refDatabase(),
$repo->gitDir(),
$repo->workDir(),
);
Push (zapis zmian)
// 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 wykonuje następujące czynności:
- Odczytuje bieżący commit HEAD
- Buduje drzewo z bieżącego indeksu (stan przygotowanych zmian)
- Tworzy „commit indeksu”, którego rodzicem jest HEAD
- Buduje drzewo z bieżącego katalogu roboczego (w tym zmian nieprzygotowanych)
- Tworzy commit stash (drzewo katalogu roboczego, rodzice = HEAD + commit indeksu)
- Ustawia
refs/stash, aby wskazywał na commit stash - Dodaje wpis refloga dla stosu
- Resetuje katalog roboczy i indeks do HEAD
Po pushu śledzone pliki odpowiadają HEAD. Pliki nieśledzone pozostają, chyba że ustawiono includeUntracked; wtedy są usuwane i zapisywane we własnym drzewie commitu stash (git przechowuje je zamiast tego w trzecim rodzicu, więc git widzi je jako śledzone zmiany). push() tworzy wpis nawet wtedy, gdy nic się nie zmieniło; git zgłasza wówczas „No local changes to save”.
Z domyślnym komunikatem
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"
Przy odłączonym HEAD wartość <branch> to HEAD.
Pop (zastosuj i usuń)
$stash->pop();
Stosuje najnowszy wpis stash w katalogu roboczym i usuwa go ze stosu. Odpowiada kolejno apply() i drop().
Pop określonego wpisu
$stash->pop(1); // Pop stash@{1}
Apply (przywróć bez usuwania)
$stash->apply(); // Apply stash@{0}
$stash->apply(2); // Apply stash@{2}
Apply scala stash z plikami bieżącego HEAD, plik po pliku (baza stash, bieżący HEAD, drzewo stash). Wpis stash pozostaje na stosie, a indeks nie jest zmieniany. W przypadku konfliktów w plikach pozostają znaczniki Updated upstream i Stashed changes, a operacja zgłasza Pitmaster\Exceptions\MergeConflictException; pop() zachowuje wtedy wpis. Apply nie sprawdza wcześniej, czy istnieją zmiany lokalne (zobacz uwagę na początku).
Lista wpisów
$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
}
Każdy wpis zawiera:
index: pozycję stash (0 = najnowszy)message: komunikat stashhash: hash commitu stash
Drop (usuń bez stosowania)
$stash->drop(); // Drop stash@{0}
$stash->drop(1); // Drop stash@{1}
Jeśli usuwany jest jedyny wpis, refs/stash i jego reflog są usuwane. Jeśli wpisów jest więcej, drop() obecnie tylko przenosi refs/stash i pozostawia reflog bez zmian, więc wpis nadal znajduje się w listEntries() (#10).
Szczegóły przechowywania
Struktura commitu stash
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>
<stash message>
Struktura commitu indeksu
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>
index on <branch>: <short hash> <HEAD subject>
Tożsamość pochodzi kolejno z GIT_COMMITTER_NAME, GIT_AUTHOR_NAME, a następnie user.name w konfiguracji repozytorium, a na końcu z Pitmaster (w tej samej kolejności ustalany jest adres e-mail, kończący się na pitmaster@example.invalid). Data pochodzi kolejno z GIT_COMMITTER_DATE, GIT_AUTHOR_DATE, a następnie z bieżącego czasu. W przeciwieństwie do commitów stash nie odczytuje stałych PITMASTER_*.
Przechowywanie Reflog
Wpisy stash są przechowywane w .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 jest odczytywany chronologicznie. Lista stash odwraca tę kolejność (najnowsze wpisy są na początku). Reflog stash jest zapisywany niezależnie od core.logallrefupdates.
Użycie 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
Komunikat jest argumentem po push (lub save); -m nie jest obsługiwane. pop, apply i drop zawsze działają na stash@{0}; CLI nie ma --include-untracked.
Ograniczenia
- Obsługiwany jest tylko pełny stash katalogu roboczego (bez
--keep-index); pliki nieśledzone można uwzględnić tylko przezpush($message, includeUntracked: true) - Brak trybu
--patch(interaktywnego wyboru fragmentów zmian) - Pop/apply przywracają stan drzewa roboczego, ale nie przywracają indeksu. Przygotowane edycje wracają jako nieprzygotowane, a nowo przygotowane pliki jako nieśledzone
stash@{n}nie jest akceptowane przezRepository::resolve()- Stash w połączonym worktree używa niewłaściwej konfiguracji i lokalizacji refloga (#14)