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.
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
use Pitmaster\Stash\Stash;
$stash = new Stash(
$repo->objectDatabase(),
$repo->refDatabase(),
$repo->gitDir(),
$repo->workDir(),
);
Push (salvare le modifiche)
// 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:
- Legge il commit HEAD corrente
- Crea un albero dall'indice corrente (stato di staging)
- Crea un «commit dell'indice» con HEAD come genitore
- Crea un albero dalla directory di lavoro corrente (incluse le modifiche non in staging)
- Crea il commit stash (albero della directory di lavoro, genitori = HEAD + commit dell'indice)
- Aggiorna
refs/stashaffinché punti al commit stash - Aggiunge una voce reflog per lo stack
- 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
$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)
$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
$stash->pop(1); // Pop stash@{1}
Apply (ripristinare senza rimuovere)
$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
$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 stashhash: l'hash del commit stash
Drop (rimuovere senza applicare)
$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
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
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:
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
# 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 tramitepush($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 daRepository::resolve()- Lo stash in un worktree collegato usa la configurazione e la posizione del reflog errate (#14)