Pourquoi git stash pop dit-il qu'il ne peut pas restaurer les fichiers non suivis à partir de l'entrée de stash?

94

J'ai eu un tas de changements par étapes et non et je voulais passer rapidement à une autre branche, puis revenir en arrière.

J'ai donc mis en scène mes modifications en utilisant:

$ git stash push -a

(Avec le recul, j'aurais probablement pu utiliser à la --include-untrackedplace de --all)

Ensuite, quand je suis allé ouvrir la cachette, je reçois beaucoup d'erreurs du genre:

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

Il ne semble pas y avoir de modifications restaurées à partir de la réserve.

J'ai aussi essayé $ git stash branch tempmais cela montre les mêmes erreurs.

J'ai trouvé un moyen de contourner ce problème qui consistait à utiliser:

$ git stash show -p | git apply

Désastre évité pour l'instant mais cela soulève quelques questions.

Pourquoi cette erreur s'est-elle produite en premier lieu et comment l'éviter la prochaine fois?

Steinybot
la source
29
J'ai dû utiliser:git stash show -p | git apply --3
xmedeko
2
La seule chose qui a fonctionné pour moi est CI-DESSUS COMMENTAIRE !!
Mehraj Malik
4
Merci @xmedeko, quelqu'un peut-il faire la différence entre git stash show -p | git apply et git stash show -p | git apply --3?
Deepak Mohandas
3
Si vous paniquez, listez simplement vos fichiers avec planqué git stash showet que les fichiers de secours , un par un: $ git show stash@{0}:your/stashed/file.txt > your_rescued_file.txt. Cela récupérera le fichier de la réserve et l'enregistrera sous un nom différent. Vous pouvez maintenant expérimenter en toute sécurité avec les méthodes de sauvetage appropriées (voir les réponses ci-dessous). Si les choses tournent mal, vous avez toujours vos fichiers sauvés comme dernière ressource.
Danijel
Wow, merci @xmedeko! Encore une fois, votre commentaire a été la seule chose qui a fonctionné, et c'était tellement simple. +1!
pcdev

Réponses:

99

Comme explication supplémentaire, notez que cela git stashfait soit deux commits, soit trois commits. La valeur par défaut est de deux; vous en obtenez trois si vous utilisez une orthographe des options --allou --include-untracked.

Ces deux, ou trois, commits sont spéciaux d'une manière importante: ils ne sont sur aucune branche. Git les localise via le nom spécial stash. 1 Le plus important, cependant, est ce que Git vous permet - et vous oblige - à faire avec ces deux ou trois commits. Pour comprendre cela, nous devons examiner le contenu de ces commits.

Qu'y a-t-il dans une cachette

Chaque commit peut lister un ou plusieurs commits parents . Ceux-ci forment un graphique, où les commits ultérieurs renvoient aux précédents. Le stash contient normalement deux commits, que j'aime appeler ipour le contenu de l'index / de la zone de préparation et wpour le contenu de l'arbre de travail. Souvenez-vous également que chaque commit contient un instantané. Dans une validation normale, cet instantané est créé à partir du contenu de l'index / de la zone de préparation. Donc le icommit est en fait un commit parfaitement normal! Ce n'est tout simplement pas sur aucune branche:

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

Si vous créez une réserve normale, le git stashcode le crée wmaintenant en copiant tous vos fichiers d'arborescence de travail suivis (dans un index auxiliaire temporaire). Git définit le premier parent de ce wcommit pour qu'il pointe vers le HEADcommit et le second parent pour pointer vers commit i. Enfin, il se met stashà pointer vers ce wcommit:

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

Si vous ajoutez --include-untrackedou --all, Git effectue un commit supplémentaire,, uentre la création de iet w. Le contenu de l'instantané pour usont les fichiers qui ne sont pas suivis mais non ignorés ( --include-untracked), ou les fichiers qui ne sont pas suivis même s'ils sont ignorés ( --all). Ce ucommit supplémentaire n'a pas de parent, puis quand il est git stashfait w, il définit wle troisième parent de ce ucommit, de sorte que vous obtenez:

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

Git également, à ce stade, supprime tous les fichiers d'arbre de travail qui se sont terminés dans le ucommit (en utilisant git cleanpour faire cela).

Restaurer une cachette

Lorsque vous allez restaurer une réserve, vous avez la possibilité de l'utiliser --indexou de ne pas l'utiliser. Cela indique git stash apply(ou l'une des commandes qui utilisent en interne apply, telles que pop) qu'il doit utiliser la ivalidation pour tenter de modifier votre index actuel. Cette modification se fait avec:

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

(plus ou moins; il y a un tas de détails nitty qui entravent l'idée de base ici).

Si vous omettez --index, git stash applyignore complètement le icommit.

Si le stash n'a que deux validations , vous git stash applypouvez maintenant appliquer le wcommit. Il le fait en appelant git merge2 (sans lui permettre de valider ou de traiter le résultat comme une fusion normale), en utilisant le commit d'origine sur lequel le stash a été créé ( ile parent de et wle premier parent de) comme base de fusion, wcomme le --theirscommit et votre commit actuel (HEAD) comme cible de la fusion. Si la fusion réussit, tout va bien - enfin, du moins Git le pense - et le git stash applylui - même réussit. Si vous aviez l'habitude git stash popd'appliquer la réserve, le code supprime maintenant la réserve. 3 Si la fusion échoue, Git déclare que l'application a échoué. Si vous avez utiliségit stash pop, le code conserve la réserve et fournit le même état d'échec que pour git stash apply.

Mais si vous avez ce troisième commit - s'il y a un ucommit dans la réserve que vous appliquez - alors les choses changent! Il n'y a aucune option pour prétendre que le ucommit n'existe pas. 4 Git insiste pour extraire tous les fichiers de ce ucommit, dans l'arbre de travail actuel. Cela signifie que les fichiers doivent soit ne pas exister du tout, soit avoir le même contenu que dans le ucommit.

Pour que cela se produise, vous pouvez utiliser git cleanvous-même - mais rappelez-vous que les fichiers non suivis (ignorés ou non) n'ont pas d'autre existence dans un référentiel Git, alors assurez-vous que ces fichiers peuvent tous être détruits! Ou bien, vous pouvez créer un répertoire temporaire et y déplacer les fichiers pour les conserver - ou même en faire un autre git stash save -uou git stash save -a, puisque ceux-ci seront exécutés git cleanpour vous. Mais cela ne vous laisse qu'une autre uréserve de style à gérer plus tard.


1 Ceci est en fait refs/stash. Cela est important si vous créez une branche nommée stash: le nom complet de la branche est refs/heads/stash, donc ceux-ci ne sont pas en conflit. Mais ne faites pas cela: Git ne me dérangerait pas, mais vous vous confondez. :-)

2 Le git stashcode utilise en fait git merge-recursivedirectement ici. Ceci est nécessaire pour plusieurs raisons, et a également pour effet secondaire de s'assurer que Git ne le traite pas comme une fusion lorsque vous résolvez des conflits et commettez.

3 C'est pourquoi je recommande d'éviter git stash pop, en faveur de git stash apply. Vous avez la possibilité de revoir ce qui a été appliqué et de décider si cela a été effectivement appliqué correctement. Sinon, vous avez toujours votre réserve, ce qui signifie que vous pouvez git stash branchtout récupérer parfaitement. Eh bien, en supposant l'absence de cet uengagement embêtant .

4 Il devrait vraiment y avoir: git stash apply --skip-untrackedou quelque chose. Il devrait également y avoir une variante qui signifie déposer tous ces ufichiers de validation dans un nouveau répertoire , par exemple git stash apply --untracked-into <dir>, peut-être.

Torek
la source
8
Réponse étonnamment détaillée. Merci d'avoir pris le temps d'écrire ceci. J'ai beaucoup appris!
steinybot
Cette explication mérite un badge de héros. Merci d'avoir mis autant de détails!
Lucas Fowler
1
@seelts Malheureusement, Git est en quelque sorte en constante évolution, donc les livres deviennent rapidement obsolètes. Mais Git doit être comprise comme un ensemble d'outils les commandes qui font un commit-ful de fichiers, ou manipuler le commettras graphique, ou peu importe qui vous assemblez dans tout ce qui est utile pour vous , et pas trop de livres semblent l'aborder que façon non plus.
torek
3
Je ne comprends pas la solution au problème. Est - il juste d' ajouter --index: git stash apply --index?
Danijel
1
Merci @torek. Je l'ai d'abord fait git stash save --all, puis j'ai immédiatement fait git stash apply, mais certains fichiers manquaient parce que je les ai renommés puis créés à nouveau (avant de les cacher). Ce qui a aidé, c'est: git checkout stash@{0} -- .je ne vais même pas me soucier git checkout stash^3 -- .parce que tout semble OK maintenant. C'est dommage que je n'ai pas le temps de vraiment comprendre ce qui se passait. Merci.
Danijel
100

J'ai réussi à recréer votre problème. Il semble que si vous cachez des fichiers non suivis puis que vous créez ces fichiers (dans votre exemple, foo.txtet bar.txt), vous avez alors des modifications locales sur des fichiers non suivis qui seraient écrasés lorsque vous postulez git stash pop.

Pour contourner ce problème, vous pouvez utiliser la commande suivante. Cela remplacera toutes les modifications locales non enregistrées, alors soyez prudent.

git checkout stash -- .

Voici quelques informations supplémentaires que j'ai trouvées sur la commande précédente .

Daniel Smith
la source
Mon arbre de travail était définitivement propre, même s'il aurait pu y avoir des modifications sur des fichiers ignorés.
steinybot
1
Il semble que l'utilisation de --all/ -a inclura les fichiers ignorés , donc cela peut être pertinent.
Daniel Smith
1
Je vais supposer que la même logique s'applique aux fichiers ignorés et marquer cela comme la réponse (bien que je pense que l' git merge --squash --strategy-option=theirs stashapproche est meilleure dans ce cas).
steinybot
D'accord, j'aime cette seconde approche! Bonne chance avec ton travail!
Daniel Smith
Cela a été utile mais cela n'a pas restauré les fichiers non suivis - si vous rencontrez le même problème ( already exists, no checkout), consultez ma réponse ci-dessous.
Erik Koopmans
25

Pour développer la réponse de Daniel Smith : ce code ne restaure que les fichiers suivis , même si vous avez utilisé --include-untracked(ou -u) lors de la création du stash. Le code complet requis est:

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

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

Cela restaure entièrement le contenu suivi (dans stash) et le contenu non suivi (dans stash^3), puis supprime la réserve. Quelques notes:

  • Soyez prudent - cela écrasera tout avec votre contenu de cache!
  • La restauration des fichiers avec les git checkoutfait tous devenir automatiquement mis en scène, j'ai donc ajouté git resetpour tout désinstaller.
  • Certaines ressources utilisent stash@{0}et stash@{0}^3, dans mes tests, cela fonctionne de la même manière avec ou sans@{0}

Sources:

Erik Koopmans
la source
1
Pourquoi supprimeriez-vous la réserve, pourquoi ne pas la laisser là pendant un moment pour des raisons de sécurité?
Danijel
@Danijel Bien sûr, vous pouvez garder la cachette - cela dépend de votre cas d'utilisation, je suppose. Pour mes besoins, j'avais fini avec la réserve une fois qu'elle a été restaurée.
Erik Koopmans
3

à part d'autres réponses, j'ai fait un petit truc

  • Supprimé tous les nouveaux fichiers ( fichiers déjà existants, par exemple foo.txt et bar.txt dans la question)
  • git stash apply (peut utiliser n'importe quelle commande, par exemple appliquer, pop, etc.)
Sanjay Nishad
la source
Ne fonctionne pas .. les fichiers supprimés apparaissent à nouveau au stash s'appliquent .. et l'erreur aussi!
Aditya Rewari