Aller au contenu

Stash

Git stash enregistre les modifications du répertoire de travail et de l’index pour vous permettre de passer à une autre tâche, puis de les restaurer plus tard. Pitmaster implémente le stash à l’aide de refs/stash et du reflog, selon la disposition de stockage de git. Repository ne possède pas de méthode stash ; utilisez la classe Stash ci-dessous.

Deux problèmes connus : lorsqu’il y a plusieurs entrées, drop() (et donc pop()) laisse l’entrée dans la liste (#10), et apply() écrase les modifications non validées des fichiers que le stash ne touche pas (#11). Appliquez les stash sur un répertoire de travail propre.

Fonctionnement du stash

Chaque entrée de stash est un commit à deux parents :

  • Parent 0 : le commit HEAD au moment du stash (l’état de base)
  • Parent 1 : un commit qui capture l’état de l’index (modifications indexées)

L’arbre du commit stash lui-même capture l’état du répertoire de travail (y compris les modifications non indexées).

La « pile » stash est le reflog de refs/stash. Le stash le plus récent est refs/stash lui-même. Les entrées plus anciennes sont des entrées du reflog : stash@{0}, stash@{1}, etc.

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

Créer l’objet Stash

PHP
use Pitmaster\Stash\Stash;

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

Push (enregistrer les modifications)

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 effectue les opérations suivantes :

  1. Lit le commit HEAD actuel
  2. Construit un arbre à partir de l’index actuel (état indexé)
  3. Crée un « commit d’index » dont HEAD est le parent
  4. Construit un arbre à partir du répertoire de travail actuel (y compris les modifications non indexées)
  5. Crée le commit stash (arbre du worktree, parents = HEAD + commit d’index)
  6. Met à jour refs/stash pour qu’il pointe vers le commit stash
  7. Ajoute une entrée du reflog à la pile
  8. Réinitialise le répertoire de travail et l’index à HEAD

Après push, les fichiers suivis correspondent à HEAD. Les fichiers non suivis restent, sauf si includeUntracked est défini ; ils sont alors supprimés et stockés dans l’arbre du commit stash lui-même (git les stocke plutôt dans un troisième parent, afin qu’ils apparaissent comme des modifications suivies pour git). push() crée une entrée même en l’absence de modifications, alors que git indique « No local changes to save ».

Avec le message par défaut

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

Avec un HEAD détaché, <branch> vaut HEAD.

Pop (appliquer et supprimer)

PHP
$stash->pop();

Applique l’entrée stash la plus récente au répertoire de travail et la supprime de la pile. Équivalent à apply() suivi de drop().

Extraire une entrée précise

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

Apply (restaurer sans supprimer)

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

Apply fusionne le stash dans le HEAD actuel, fichier par fichier (base du stash, HEAD actuel, arbre du stash). L’entrée stash reste dans la pile et l’index n’est pas modifié. En cas de conflit, les fichiers contiennent des marqueurs libellés Updated upstream et Stashed changes, et Pitmaster\Exceptions\MergeConflictException est levée ; pop() conserve alors l’entrée. Apply ne vérifie pas au préalable la présence de modifications locales (voir la remarque en haut).

Lister les entrées

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
}

Chaque entrée contient :

  • index : la position du stash (0 = le plus récent)
  • message : le message du stash
  • hash : le hash du commit stash

Drop (supprimer sans appliquer)

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

Si l’unique entrée est supprimée, refs/stash et son reflog sont supprimés. S’il y a plusieurs entrées, drop() ne fait actuellement que déplacer refs/stash sans modifier le reflog ; l’entrée reste donc dans listEntries() (#10).

Détails du stockage

Structure du commit stash

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

<stash message>

Structure du commit d’index

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

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

L’identité provient de GIT_COMMITTER_NAME, puis de GIT_AUTHOR_NAME, puis de user.name dans la configuration du dépôt, puis de Pitmaster (même ordre pour l’adresse e-mail, en terminant par pitmaster@example.invalid). La date provient de GIT_COMMITTER_DATE, puis de GIT_AUTHOR_DATE, puis de l’heure actuelle. Contrairement aux commits, stash ne lit pas les constantes PITMASTER_*.

Stockage Reflog

Les entrées stash sont stockées dans .git/logs/refs/stash :

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

Le reflog est lu par ordre chronologique. La liste des stash inverse cet ordre (du plus récent au plus ancien). Le reflog stash est écrit quelle que soit la valeur de core.logallrefupdates.

Utilisation de 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

Le message est l’argument qui suit push (ou save) ; -m n’est pas pris en charge. pop, apply et drop fonctionnent toujours sur stash@{0} ; le CLI n’a pas de --include-untracked.

Limites

  • Uniquement le stash de tout le répertoire de travail (pas de --keep-index) ; fichiers non suivis uniquement via push($message, includeUntracked: true)
  • Pas de mode --patch (sélection interactive des portions)
  • Pop/apply restaure l’état du répertoire de travail, sans restaurer l’index. Les modifications indexées reviennent comme modifications non indexées, et les nouveaux fichiers indexés reviennent comme fichiers non suivis
  • stash@{n} n’est pas accepté par Repository::resolve()
  • Dans un worktree lié, stash utilise le mauvais emplacement de configuration et de reflog (#14)