Por que o git stash pop diz que não pode restaurar arquivos não rastreados da entrada do stash?

103

Eu tinha um monte de mudanças em estágios e não em estágios e queria mudar rapidamente para outro branch e depois voltar.

Então, preparei minhas alterações usando:

$ git stash push -a

(Olhando para trás, provavelmente poderia ter usado em --include-untrackedvez de --all)

Então, quando fui abrir o estoque, recebo uma série de erros do tipo:

$ git stash pop
foo.txt already exists, no checkout
bar.txt already exists, no checkout
...
Could not restore untracked files from stash entry

Não parece haver nenhuma alteração restaurada do stash.

Eu também tentei, $ git stash branch tempmas mostra os mesmos erros.

Eu descobri uma maneira de contornar isso que era usar:

$ git stash show -p | git apply

Desastre evitado por enquanto, mas isso levanta algumas questões.

Por que esse erro aconteceu em primeiro lugar e como posso evitá-lo na próxima vez?

steinybot
fonte
31
Tive de usar:git stash show -p | git apply --3
xmedeko
2
A única coisa que funcionou para mim é o COMENTÁRIO ACIMA !!
Mehraj Malik
4
Obrigado @xmedeko, alguém pode dizer a diferença entre git stash show -p | git apply e git stash show -p | git apply --3?
Deepak Mohandas
5
Se você entrar em pânico, apenas listar seus arquivos escondidos com git stash showe que os arquivos de resgate, um por um: $ git show stash@{0}:your/stashed/file.txt > your_rescued_file.txt. Isso irá obter o arquivo do stash e salvá-lo com um nome diferente. Agora você está seguro para experimentar os métodos de resgate adequados (veja as respostas abaixo). Se as coisas derem errado, você sempre terá seus arquivos resgatados como último recurso.
Danijel
Nossa, obrigado @xmedeko! Mais uma vez, seu comentário foi a única coisa que funcionou, e foi tão simples. +1!
pcdev

Respostas:

104

Como um pouco de explicação adicional, note que git stashfaz dois commits ou três commits. O padrão é dois; você ganha três se usar qualquer grafia das opções --allou --include-untracked.

Esses dois ou três commits são especiais de uma maneira importante: eles não estão em nenhum branch. Git os localiza por meio do nome especial stash. 1 A coisa mais importante, entretanto, é o que o Git permite - e o faz - fazer com esses dois ou três commits. Para entender isso, precisamos olhar o que está nesses commits.

O que há dentro de um esconderijo

Cada commit pode listar um ou mais commits pais . Eles formam um gráfico, onde os commits posteriores apontam para os anteriores. O stash normalmente contém dois commits, que eu gosto de chamar ipara o conteúdo da área de teste / índice e wpara o conteúdo da árvore de trabalho. Lembre-se também de que cada commit contém um instantâneo. Em uma confirmação normal, este instantâneo é feito do índice / conteúdo da área de teste. Portanto, o icommit é de fato um commit perfeitamente normal! Simplesmente não está em qualquer ramo:

...--o--o--o   <-- branch (HEAD)
           |
           i

Se você está fazendo um estoque normal, o git stashcódigo o faz wcopiando todos os seus arquivos da árvore de trabalho rastreados (em um índice auxiliar temporário). Git configura o primeiro pai deste wcommit para apontar para o HEADcommit e o segundo pai para apontar para o commit i. Por último, ele define stashpara apontar para este wcommit:

...--o--o--o   <-- branch (HEAD)
           |\
           i-w   <-- stash

Se você adicionar --include-untrackedou --all, o Git fará um commit extra,, uentre fazer ie w. O conteúdo do instantâneo usão aqueles arquivos que não são rastreados, mas não são ignorados ( --include-untracked), ou arquivos que não são rastreados, mesmo que sejam ignorados ( --all). Este ucommit extra não tem pai, e então quando git stashé feito w, ele configura wo terceiro pai para este ucommit, de forma que você obtém:

...--o--o--o   <-- branch (HEAD)
           |\
           i-w   <-- stash
            /
           u

Git também, neste ponto, remove todos os arquivos da árvore de trabalho que terminaram no ucommit (usando git cleanpara fazer isso).

Restaurando um estoque

Ao restaurar um estoque, você tem a opção de usá-lo --indexou não. Isso diz git stash apply(ou qualquer um dos comandos que usa internamente apply, como pop) que deve usar o icommit para tentar modificar seu índice atual. Essa modificação é feita com:

git diff <hash-of-i> <hash-of-i's-parent> | git apply --index

(mais ou menos; há um monte de detalhes essenciais que atrapalham a ideia básica aqui).

Se você omitir --index, git stash applyignora completamente o icommit.

Se o stash tiver apenas dois commits, git stash applyagora pode aplicar o wcommit. Ele faz isso chamando git merge2 (sem permitir que ele comprometa ou trate o resultado como uma mesclagem normal), usando o commit original no qual o stash foi feito ( io pai de, e wo primeiro pai de) como a base de mesclagem, wcomo o --theirscommit, e seu commit atual (HEAD) como o alvo da fusão. Se a fusão for bem-sucedida, tudo estará bem - bem, pelo menos Git pensa assim - e o git stash applypróprio sucesso. Se você costumava git stash popaplicar o stash, o código agora descarta o stash. 3 Se a fusão falhar, o Git declara que a aplicação falhou. Se você usougit stash pop, o código retém o stash e entrega o mesmo status de falha de git stash apply.

Mas se você tiver aquele terceiro commit - se houver um ucommit no stash que você está aplicando - então as coisas mudam! Não há opção de fingir que o ucommit não existe. 4 Git insiste em extrair todos os arquivos de que ucometeu, na obra-árvore atual. Isso significa que os arquivos não devem existir ou ter o mesmo conteúdo do ucommit.

Para fazer isso acontecer, você pode usar git cleanvocê mesmo - mas lembre-se de que arquivos não rastreados (ignorados ou não) não têm outra existência dentro de um repositório Git, portanto, certifique-se de que todos esses arquivos possam ser destruídos! Ou você pode criar um diretório temporário e mover os arquivos para segurança - ou até mesmo fazer outro git stash save -uou git stash save -a, já que esses serão executados git cleanpara você. Mas isso só deixa você com outro uestoque de estilo para lidar mais tarde.


1 Isso é de fato refs/stash. Isso é importante se você criar um branch chamado stash: o nome completo do branch é refs/heads/stash, portanto, eles não estão em conflito. Mas não faça isso: o Git não se importará, mas você se confundirá. :-)

2 Na git stashverdade, o código usa git merge-recursivediretamente aqui. Isso é necessário por vários motivos e também tem o efeito colateral de garantir que o Git não o trate como uma mesclagem quando você resolver conflitos e enviar commit.

3 É por isso que recomendo evitar git stash pop, em favor de git stash apply. Você tem a chance de revisar o que foi aplicado e decidir se foi realmente aplicado corretamente. Se não, você ainda tem seu estoque, o que significa que pode usar git stash branchpara recuperar tudo perfeitamente. Bem, supondo a falta daquele ucommit irritante .

4 Realmente deveria haver: git stash apply --skip-untrackedou algo assim. Também deve haver uma variante que significa colocar todos os uarquivos de commit em um novo diretório , por exemplo git stash apply --untracked-into <dir>, talvez.

Torek
fonte
Uau! Você merece uma medalha! Existe algum livro que você recomendaria para tais explicações detalhadas do git? Impressionante! 🙇‍♂️
setembro de
1
@seelts Infelizmente, o Git está em constante evolução, então os livros ficam desatualizados rapidamente. Mas Git deve ser entendido como um conjunto de ferramentas - comandos que manipulam um commit-ful de arquivos, ou manipulam o gráfico de commit, ou qualquer outra coisa - que você reúne em tudo o que é útil para você , e não muitos livros parecem abordar isso maneira também.
Torek de
3
Não consigo entender a solução para o problema. É apenas acrescentando --index: git stash apply --index?
Danijel
1
Obrigado @torek. Eu fiz primeiro git stash save --all, depois fiz imediatamente git stash apply, mas alguns arquivos estavam faltando porque eu os renomei e criei novamente (antes de armazenar). O que ajudou foi: git checkout stash@{0} -- .não vou nem me preocupar com isso git checkout stash^3 -- .porque agora tudo parece bem. É uma pena não ter tempo de realmente entender o que estava acontecendo. Obrigado.
Danijel
1
@ user1169587: significa o que diz. Talvez um exemplo ajude. Suponha que o terceiro ucommit contenha um arquivo chamado path/to/file(com algum conteúdo). Suponha ainda que sua árvore de trabalho já contenha um arquivo nomeadopath/to/file . Extrair o conteúdo do ucommit substituiria o conteúdo do arquivo existente por um novo conteúdo, destruindo qualquer trabalho que você tivesse no arquivo nomeado path/to/file. Então Git não fará isso. Você terá que destruir esses conteúdos primeiro (removendo o arquivo inteiramente) ou removê-los do caminho (movendo o arquivo).
Torek em
110

Consegui recriar seu problema. Parece que se você armazena arquivos não rastreados e os cria (em seu exemplo foo.txte bar.txt), então você tem alterações locais em arquivos não rastreados que seriam substituídos quando você aplicasse git stash pop.

Para contornar esse problema, você pode usar o seguinte comando. Isso substituirá todas as alterações locais não salvas, portanto, tome cuidado.

git checkout stash -- .

Aqui estão mais algumas informações que encontrei no comando anterior .

Daniel Smith
fonte
Minha árvore de trabalho estava definitivamente limpa, embora pudesse ter havido alterações nos arquivos ignorados.
steinybot
1
Parece que usar --all/ -a incluirá arquivos ignorados , então isso pode ser relevante.
Daniel Smith
1
Vou assumir que a mesma lógica se aplica aos arquivos ignorados e marcar isso como a resposta (embora eu ache que a git merge --squash --strategy-option=theirs stashabordagem seja melhor neste caso).
steinybot
Concordo, eu gosto dessa segunda abordagem! Boa sorte com seu trabalho!
Daniel Smith
Isso foi útil, mas não restaurou arquivos não rastreados - se você tiver o mesmo problema ( already exists, no checkout), verifique minha resposta abaixo.
Erik Koopmans
26

Para expandir a resposta de Daniel Smith : esse código apenas restaura os arquivos rastreados , mesmo se você usou --include-untracked(ou -u) ao criar o stash. O código completo necessário é:

git checkout stash -- .
git checkout stash^3 -- .
git stash drop

# Optional to unstage the changes (auto-staged by default).
git reset

Isso restaura totalmente o conteúdo rastreado (entrada stash) e o conteúdo não rastreado (entrada ) e stash^3, em seguida, exclui o estoque. Algumas notas:

  • Tenha cuidado - isso sobrescreverá tudo com o conteúdo do seu estoque!
  • Restaurar os arquivos com git checkoutfaz com que todos sejam git resettestados automaticamente, então adicionei tudo para remover o estágio.
  • Alguns recursos usam stash@{0}e stash@{0}^3, em meus testes, funciona da mesma forma com ou sem@{0}

Fontes:

Erik Koopmans
fonte
1
Por que você excluiria o estoque, por que não deixá-lo lá por um tempo por razões de segurança?
Danijel
@Danijel Claro que você pode manter o estoque - depende do seu caso de uso, eu acho. Para meus propósitos, terminei com o esconderijo assim que fosse restaurado.
Erik Koopmans
5

além de outras respostas, fiz um pequeno truque

  • Excluiu todos os novos arquivos ( arquivos já existentes, por exemplo, foo.txt e bar.txt na questão)
  • git stash apply (pode usar qualquer comando, por exemplo, aplicar, pop etc.)
Sanjay Nishad
fonte
Não está funcionando .. os arquivos excluídos apenas aparecem novamente no estoque se aplicam .. e o erro também!
Aditya Rewari de