Stash
Git stash speichert Änderungen am Arbeitsverzeichnis und Index, damit du zu einer anderen Aufgabe wechseln und sie später wiederherstellen kannst. Pitmaster implementiert den Stash mit refs/stash und dem Reflog im Speicherlayout von git. Repository bietet keine Stash-Methode; verwende die folgende Klasse Stash.
Es gibt zwei bekannte Probleme: Bei mehr als einem Eintrag lässt drop() (und damit auch pop()) den Eintrag in der Liste (#10) zurück, und apply() überschreibt nicht committete Änderungen an Dateien, die der Stash nicht berührt (#11). Wende Stashes nur in einem sauberen Arbeitsverzeichnis an.
So funktioniert Stash
Jeder Stash-Eintrag ist ein Commit mit zwei Eltern:
- Parent 0: der HEAD-Commit zum Zeitpunkt des Stashens (der Ausgangszustand)
- Parent 1: ein Commit, der den Indexzustand erfasst (bereitgestellte Änderungen)
Der eigene Baum des Stash-Commits erfasst den Zustand des Arbeitsverzeichnisses (einschließlich nicht bereitgestellter Änderungen).
Der „Stack“ des Stash ist das Reflog von refs/stash. Der neueste Stash ist refs/stash selbst. Ältere Einträge sind Reflog-Einträge: stash@{0}, stash@{1} usw.
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-Objekt erstellen
use Pitmaster\Stash\Stash;
$stash = new Stash(
$repo->objectDatabase(),
$repo->refDatabase(),
$repo->gitDir(),
$repo->workDir(),
);
Push (Änderungen speichern)
// 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 führt Folgendes aus:
- Liest den aktuellen HEAD-Commit
- Erstellt einen Baum aus dem aktuellen Index (bereitgestellter Zustand)
- Erstellt einen „Index-Commit“ mit HEAD als Parent
- Erstellt einen Baum aus dem aktuellen Arbeitsverzeichnis (einschließlich nicht bereitgestellter Änderungen)
- Erstellt den Stash-Commit (Arbeitsverzeichnis-Baum, Parents = HEAD + Index-Commit)
- Aktualisiert
refs/stash, sodass es auf den Stash-Commit zeigt - Hängt einen Reflog-Eintrag für den Stack an
- Setzt Arbeitsverzeichnis und Index auf HEAD zurück
Nach dem Push stimmen getrackte Dateien mit HEAD überein. Nicht getrackte Dateien bleiben erhalten, außer includeUntracked ist gesetzt; dann werden sie entfernt und im eigenen Baum des Stash-Commits gespeichert (git speichert sie stattdessen in einem dritten Parent, sodass git sie als getrackte Änderungen erkennt). push() erstellt auch dann einen Eintrag, wenn es keine Änderungen gab; git meldet dabei „No local changes to save“.
Mit Standardnachricht
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"
Bei einem abgetrennten HEAD ist <branch> gleich HEAD.
Pop (anwenden und entfernen)
$stash->pop();
Wendet den obersten Stash-Eintrag auf das Arbeitsverzeichnis an und entfernt ihn aus dem Stack. Entspricht apply() gefolgt von drop().
Bestimmten Eintrag abrufen
$stash->pop(1); // Pop stash@{1}
Apply (wiederherstellen, ohne zu entfernen)
$stash->apply(); // Apply stash@{0}
$stash->apply(2); // Apply stash@{2}
Apply führt den Stash Datei für Datei mit dem aktuellen HEAD zusammen (Stash-Basis, aktueller HEAD, Stash-Baum). Der Stash-Eintrag bleibt im Stack, und der Index bleibt unverändert. Bei Konflikten hinterlässt es in den Dateien Markierungen mit den Bezeichnungen Updated upstream und Stashed changes und löst Pitmaster\Exceptions\MergeConflictException aus; pop() behält den Eintrag dann bei. Apply prüft nicht zuerst, ob lokale Änderungen vorhanden sind (siehe den Hinweis oben).
Einträge auflisten
$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
}
Jeder Eintrag enthält:
index: die Position im Stash (0 = neuester)message: die Stash-Nachrichthash: den Hash des Stash-Commits
Drop (entfernen, ohne anzuwenden)
$stash->drop(); // Drop stash@{0}
$stash->drop(1); // Drop stash@{1}
Wird der einzige Eintrag entfernt, werden refs/stash und sein Reflog gelöscht. Gibt es mehrere Einträge, verschiebt drop() derzeit nur refs/stash und lässt das Reflog unverändert, sodass der Eintrag in listEntries() verbleibt (#10).
Speicherdetails
Aufbau des Stash-Commits
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>
<stash message>
Aufbau des Index-Commits
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>
index on <branch>: <short hash> <HEAD subject>
Die Identität stammt der Reihe nach aus GIT_COMMITTER_NAME, dann GIT_AUTHOR_NAME, dann user.name in der Repository-Konfiguration und schließlich Pitmaster (für die E-Mail gilt dieselbe Reihenfolge; sie endet mit pitmaster@example.invalid). Das Datum stammt aus GIT_COMMITTER_DATE, dann GIT_AUTHOR_DATE und schließlich der aktuellen Uhrzeit. Anders als bei Commits liest Stash die Konstanten PITMASTER_* nicht.
Reflog-Speicherung
Stash-Einträge werden in .git/logs/refs/stash gespeichert:
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
Das Reflog wird in chronologischer Reihenfolge gelesen. Die Stash-Liste kehrt diese Reihenfolge um (neuester Eintrag zuerst). Das Stash-Reflog wird unabhängig von core.logallrefupdates geschrieben.
Verwendung von 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
Die Nachricht ist das Argument nach push (oder save); -m wird nicht unterstützt. pop, apply und drop arbeiten immer mit stash@{0}; CLI hat kein --include-untracked.
Einschränkungen
- Nur vollständiger Stash des Arbeitsverzeichnisses (kein
--keep-index); nicht getrackte Dateien nur überpush($message, includeUntracked: true) - Kein Modus
--patch(interaktive Auswahl einzelner Änderungen) - Pop/Apply stellt den Zustand des Arbeitsverzeichnisses wieder her, nicht den Index. Bereitgestellte Änderungen kommen als nicht bereitgestellte zurück, neu bereitgestellte Dateien als nicht getrackte Dateien
stash@{n}wird vonRepository::resolve()nicht akzeptiert- Stash in einem verknüpften Worktree verwendet den falschen Speicherort für Konfiguration und Reflog (#14)