Zum Inhalt springen

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.

Code
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

PHP
use Pitmaster\Stash\Stash;

$stash = new Stash(
    $repo->objectDatabase(),
    $repo->refDatabase(),
    $repo->gitDir(),
    $repo->workDir(),
);

Push (Änderungen speichern)

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 führt Folgendes aus:

  1. Liest den aktuellen HEAD-Commit
  2. Erstellt einen Baum aus dem aktuellen Index (bereitgestellter Zustand)
  3. Erstellt einen „Index-Commit“ mit HEAD als Parent
  4. Erstellt einen Baum aus dem aktuellen Arbeitsverzeichnis (einschließlich nicht bereitgestellter Änderungen)
  5. Erstellt den Stash-Commit (Arbeitsverzeichnis-Baum, Parents = HEAD + Index-Commit)
  6. Aktualisiert refs/stash, sodass es auf den Stash-Commit zeigt
  7. Hängt einen Reflog-Eintrag für den Stack an
  8. 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

PHP
$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)

PHP
$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

PHP
$stash->pop(1);  // Pop stash@{1}

Apply (wiederherstellen, ohne zu entfernen)

PHP
$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

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
}

Jeder Eintrag enthält:

  • index: die Position im Stash (0 = neuester)
  • message: die Stash-Nachricht
  • hash: den Hash des Stash-Commits

Drop (entfernen, ohne anzuwenden)

PHP
$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

Code
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

Code
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:

Code
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

Shell
# 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 über push($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 von Repository::resolve() nicht akzeptiert
  • Stash in einem verknüpften Worktree verwendet den falschen Speicherort für Konfiguration und Reflog (#14)