Pular para o conteúdo

Stash

Git stash salva alterações no diretório de trabalho e no índice para que você possa mudar para outra tarefa e restaurá-las depois. Pitmaster implementa o stash usando refs/stash e o reflog, no layout de armazenamento de git. Repository não tem um método stash; use a classe Stash abaixo.

Dois problemas conhecidos: com mais de uma entrada, drop() (e, portanto, pop()) mantém a entrada na lista (#10), e apply() sobrescreve alterações não confirmadas em arquivos que o stash não afeta (#11). Aplique os stashes em uma árvore de trabalho limpa.

Como o stash funciona

Cada entrada do stash é um commit com dois pais:

  • Pai 0: o commit HEAD no momento do stash (o estado base)
  • Pai 1: um commit que captura o estado do índice (alterações preparadas)

A própria árvore do commit do stash captura o estado da árvore de trabalho (incluindo alterações não preparadas).

A "pilha" de stash é o reflog de refs/stash. O stash mais recente é o próprio refs/stash. As entradas mais antigas são entradas do reflog: stash@{0}, stash@{1} etc.

Código
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

Criando o objeto Stash

PHP
use Pitmaster\Stash\Stash;

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

Push (salvar alterações)

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 faz o seguinte:

  1. Lê o commit HEAD atual
  2. Cria uma árvore a partir do índice atual (estado preparado)
  3. Cria um "commit de índice" com HEAD como pai
  4. Cria uma árvore a partir do diretório de trabalho atual (incluindo alterações não preparadas)
  5. Cria o commit stash (árvore do diretório de trabalho, pais = HEAD + commit de índice)
  6. Atualiza refs/stash para apontar para o commit stash
  7. Acrescenta uma entrada ao reflog da pilha
  8. Redefine a árvore de trabalho e o índice para HEAD

Após o push, os arquivos rastreados correspondem a HEAD. Os arquivos não rastreados permanecem, a menos que includeUntracked esteja definido; nesse caso, são removidos e armazenados na própria árvore do commit stash (git os armazena em um terceiro pai, então git os vê como alterações rastreadas). push() cria uma entrada mesmo quando nada mudou; nesse caso, git informa "No local changes to save".

Com mensagem padrão

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

Em um HEAD desanexado, <branch> é HEAD.

Pop (aplicar e remover)

PHP
$stash->pop();

Aplica a entrada stash do topo ao diretório de trabalho e a remove da pilha. Equivale a apply() seguido de drop().

Remover uma entrada específica

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

Apply (restaurar sem remover)

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

Apply mescla o stash no arquivo do HEAD atual por arquivo (base do stash, HEAD atual, árvore do stash). A entrada stash permanece na pilha, e o índice não é alterado. Conflitos deixam marcadores rotulados como Updated upstream e Stashed changes nos arquivos e lançam Pitmaster\Exceptions\MergeConflictException; pop() então mantém a entrada. Apply não verifica primeiro se há alterações locais (consulte a observação no início).

Listar entradas

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
}

Cada entrada contém:

  • index: a posição no stash (0 = mais recente)
  • message: a mensagem do stash
  • hash: o hash do commit stash

Drop (remover sem aplicar)

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

Se a única entrada for removida, refs/stash e seu reflog são removidos. Com mais entradas, drop() atualmente apenas move refs/stash e deixa o reflog inalterado, então a entrada permanece em listEntries() (#10).

Detalhes de armazenamento

Estrutura do commit stash

Código
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>

<stash message>

Estrutura do commit de índice

Código
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>

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

A identidade vem de GIT_COMMITTER_NAME, depois GIT_AUTHOR_NAME, depois user.name na configuração do repositório e, por fim, Pitmaster (a mesma ordem vale para o e-mail, terminando em pitmaster@example.invalid). A data vem de GIT_COMMITTER_DATE, depois GIT_AUTHOR_DATE e, em seguida, da hora atual. Ao contrário dos commits, stash não lê as constantes PITMASTER_*.

Armazenamento de Reflog

As entradas stash são armazenadas em .git/logs/refs/stash:

Código
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

O reflog é lido em ordem cronológica. A lista stash inverte essa ordem (mais recentes primeiro). O reflog do stash é gravado independentemente de core.logallrefupdates.

Uso 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

A mensagem é o argumento após push (ou save); -m não é compatível. pop, apply e drop sempre funcionam em stash@{0}; CLI não tem --include-untracked.

Limitações

  • Apenas stash de todo o diretório de trabalho (sem --keep-index); arquivos não rastreados apenas por meio de push($message, includeUntracked: true)
  • Não há modo --patch (seleção interativa de trechos)
  • Pop/apply restaura o estado da árvore de trabalho; não restaura o índice. Edições preparadas voltam como edições não preparadas, e arquivos recém-preparados voltam como arquivos não rastreados
  • stash@{n} não é aceito por Repository::resolve()
  • Stash em uma árvore de trabalho vinculada usa a configuração e o local do reflog incorretos (#14)