Vai al contenuto

Stash

Git stash salva le modifiche alla directory di lavoro e all’indice, così potete passare a un’altra attività e ripristinarle in seguito. Pitmaster implementa lo stash usando refs/stash e il reflog, secondo il layout di archiviazione di git. Repository non dispone di un metodo stash; usate la classe Stash qui sotto.

Due problemi noti: con più di una voce, drop() (e quindi pop()) lascia la voce nell’elenco (#10), mentre apply() sovrascrive le modifiche non registrate nei file che lo stash non tocca (#11). Applicate gli stash su un albero di lavoro pulito.

Come funziona lo stash

Ogni voce stash è un commit con due genitori:

  • Genitore 0: il commit HEAD al momento dello stash (lo stato di base)
  • Genitore 1: un commit che acquisisce lo stato dell’indice (modifiche in staging)

L’albero del commit stash stesso acquisisce lo stato dell’albero di lavoro (incluse le modifiche non in staging).

Lo stack di stash è il reflog di refs/stash. Lo stash più recente è refs/stash stesso. Le voci meno recenti sono voci del reflog: stash@{0}, stash@{1} e così via.

Codice
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

Creazione dell'oggetto Stash

PHP
use Pitmaster\Stash\Stash;

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

Push (salvare le modifiche)

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 esegue le seguenti operazioni:

  1. Legge il commit HEAD corrente
  2. Crea un albero dall'indice corrente (stato di staging)
  3. Crea un «commit dell'indice» con HEAD come genitore
  4. Crea un albero dalla directory di lavoro corrente (incluse le modifiche non in staging)
  5. Crea il commit stash (albero della directory di lavoro, genitori = HEAD + commit dell'indice)
  6. Aggiorna refs/stash affinché punti al commit stash
  7. Aggiunge una voce reflog per lo stack
  8. Reimposta la directory di lavoro e l'indice su HEAD

Dopo il push, i file tracciati corrispondono a HEAD. I file non tracciati rimangono, a meno che non sia impostato includeUntracked; in tal caso vengono rimossi e salvati nell'albero del commit stash (git li memorizza invece in un terzo genitore, quindi git li considera modifiche tracciate). push() crea una voce anche quando non è cambiato nulla; git indica «No local changes to save».

Con il messaggio predefinito

PHP
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"

Con HEAD scollegato, <branch> è HEAD.

Pop (applicare e rimuovere)

PHP
$stash->pop();

Applica la voce stash in cima alla directory di lavoro e la rimuove dallo stack. Equivale a apply() seguito da drop().

Rimuovere una voce specifica

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

Apply (ripristinare senza rimuovere)

PHP
$stash->apply();      // Apply stash@{0}
$stash->apply(2);     // Apply stash@{2}

Apply unisce lo stash al file HEAD corrente, file per file (base dello stash, HEAD corrente, albero dello stash). La voce stash rimane nello stack e l'indice non viene toccato. In caso di conflitti, nei file vengono lasciati marcatori etichettati Updated upstream e Stashed changes e viene generato Pitmaster\Exceptions\MergeConflictException; pop() mantiene quindi la voce. Apply non verifica prima la presenza di modifiche locali (vedere la nota in alto).

Elencare le voci

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
}

Ogni voce contiene:

  • index: la posizione dello stash (0 = la più recente)
  • message: il messaggio dello stash
  • hash: l'hash del commit stash

Drop (rimuovere senza applicare)

PHP
$stash->drop();       // Drop stash@{0}
$stash->drop(1);      // Drop stash@{1}

Se si rimuove l'unica voce, refs/stash e il relativo reflog vengono rimossi. Se ci sono altre voci, al momento drop() sposta soltanto refs/stash e lascia invariato il reflog, quindi la voce resta in listEntries() (#10).

Dettagli di archiviazione

Struttura del commit stash

Codice
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>

<stash message>

Struttura del commit dell'indice

Codice
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>

index on <branch>: <short hash> <HEAD subject>

L'identità proviene da GIT_COMMITTER_NAME, poi da GIT_AUTHOR_NAME e infine da user.name nella configurazione del repository, poi da Pitmaster (lo stesso ordine vale per l'email, che termina con pitmaster@example.invalid). La data proviene da GIT_COMMITTER_DATE, poi da GIT_AUTHOR_DATE e infine dall'ora corrente. A differenza dei commit, stash non legge le costanti PITMASTER_*.

Archiviazione di Reflog

Le voci stash sono memorizzate in .git/logs/refs/stash:

Codice
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

Il reflog viene letto in ordine cronologico. L'elenco degli stash lo inverte (dal più recente al meno recente). Il reflog dello stash viene scritto indipendentemente da core.logallrefupdates.

Utilizzo di 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

Il messaggio è l'argomento dopo push (o save); -m non è supportato. pop, apply e drop operano sempre su stash@{0}; CLI non ha --include-untracked.

Limitazioni

  • Solo stash dell'intera directory di lavoro (senza --keep-index); i file non tracciati si includono solo tramite push($message, includeUntracked: true)
  • Nessuna modalità --patch (selezione interattiva degli hunk)
  • Pop/apply ripristina lo stato della directory di lavoro, ma non quello dell'indice. Le modifiche in staging tornano come modifiche non in staging e i nuovi file messi in staging tornano come file non tracciati
  • stash@{n} non è accettato da Repository::resolve()
  • Lo stash in un worktree collegato usa la configurazione e la posizione del reflog errate (#14)