Come recuperare da “git stash save –all”?

Volevo hide i file non tracciati, ma continuo a passare l’opzione sbagliata. A me sembra giusto:

git stash save [-a|--all] 

ma questo in effetti archivia anche file ignorati. Quello corretto è:

 git stash save [-u|--include-untracked] 

Quando git stash save -a e provo a git stash pop , ottengo innumerevoli errori per tutti i file ignorati:

 path/to/file1.ext already exists, no checkout path/to/file1.ext already exists, no checkout path/to/file1.ext already exists, no checkout ... Could not restore untracked files from stash 

quindi il comando fallisce.

Come posso recuperare le modifiche memorizzate tracciate e non tracciate? git reflog non memorizza i comandi di stash.

TL; Versione DR:

È necessario che la directory sia pulita (in termini git clean ) affinché lo stash si applichi correttamente. Ciò significa eseguire git clean -f , o anche git clean -fdx , che è una specie di brutta cosa da fare, dato che alcuni dei file / directory non tracciati o ignorati e ignorati potrebbero essere elementi che si desidera mantenere, piuttosto di cancellare completamente. (Se è così, dovresti spostarli fuori dall’albero di lavoro invece di git clean via. Ricorda, i file che git clean rimuove sono proprio quelli che non puoi recuperare da Git!)

Per capire perché, guarda il punto 3 nella descrizione “applica”. Nota che non esiste alcuna opzione per saltare i file non tracciati e / o ignorati in una scorta.

Informazioni di base sullo stesso stash

Quando usi git stash save con -u o -a , lo stash script scrive il suo “stash bag” come un commit a tre -parenti piuttosto che il solito commit a due genitore.

Diagrammaticamente, lo “stash bag” normalmente appare così, in termini di grafico di commit:

 o--o--C <-- HEAD (typically, a branch) |\ iw <-- stash 

Gli o sono vecchi nodes di commit ordinari, come C Il Nodo C (per Commit) ha una lettera in modo che possiamo chiamarlo: è da qui che pesa la "borsa".

La sacca stessa è la piccola borsa triangular che pende da C e contiene due commit: w è il commit dell'albero di lavoro e i è il commit dell'indice. (Non mostrato, perché è solo difficile da diagrammare, è il fatto che il primo genitore di w è C e il suo secondo genitore è i ).

Con --untracked o --all c'è un terzo genitore per w , quindi il diagramma è più simile a questo:

 o--o--C <-- HEAD |\ iw <-- stash / u 

(questi diagrammi hanno davvero bisogno di essere immagini in modo che possano avere frecce, piuttosto che arte ASCII dove le frecce sono difficili da includere). In questo caso, stash è commit w , stash^ è commit C (ancora anche HEAD ), stash^2 è commit i , e stash^3 è commit u , che contiene i file "untracked" o anche "non tracciati e ignorati". (In realtà non è importante, per quanto posso dire, ma aggiungerò qui che i C come commit genitore, mentre u sei un commit senza parenting o root, non sembra esserci alcun motivo particolare per questo, è come lo script fa le cose, ma spiega perché le "frecce" (linee) sono come sono nel diagramma).

Le varie opzioni al momento del save

Al momento del salvataggio, è ansible specificare una o tutte le seguenti opzioni:

  • -p , --patch
  • -k , --keep-index , --no-keep-index
  • -q , --quiet
  • -u , --include-untracked
  • -a , --all

Alcuni di questi implicano, sovrascrivono o distriggersno altri. L'uso di -p , ad esempio, modifica completamente l'algoritmo utilizzato dallo script per creare lo stash e triggers anche --keep-index , forzandoti a usare --no-keep-index per distriggersrlo se non vuoi quella. È incompatibile con -a e -u e causerà errori se qualcuno di questi è indicato.

Altrimenti, tra -a e -u , viene mantenuto qualsiasi valore impostato per ultimo .

A questo punto lo script crea uno o due commit:

  • uno per l'indice corrente (anche se non contiene modifiche), con il commit padre C
  • con -u o -a , un commit parentless contenente (solo) file non tracciati o tutti i file (non tracciati e ignorati).

Lo script di stash salva quindi l'albero di lavoro corrente. Lo fa con un file indice temporaneo (in pratica, una nuova area di staging). Con -p , lo script legge il commit HEAD nella nuova area di staging, quindi in pratica 1 esegue git add -i --patch , in modo che questo indice si git add -i --patch con le patch selezionate. Senza -p , differisce semplicemente la directory di lavoro rispetto all'indice nascosto per trovare i file modificati. 2 In entrambi i casi, scrive un object albero dall'indice temporaneo. Questo albero sarà l'albero per il commit w .

Come suo ultimo passo di creazione di uno stash , lo script usa l'albero appena salvato, il genitore commit C , il commit dell'indice e il commit di root per i file non tracciati, se esiste, per creare il commit stash finale w . Tuttavia, lo script richiede molti più passaggi che riguardano la directory di lavoro , a seconda che tu stia usando -a , -u , -p , e / o --keep-index (e ricorda che -p implica --keep-index ):

  • Con -p :

    1. "Reverse-patch" la cartella di lavoro per rimuovere la differenza tra HEAD e lo stash. In sostanza, questo lascia la directory di lavoro con solo quelle modifiche non nascoste (in particolare, quelle non in commit w ; tutto in commit i viene ignorato qui).

    2. Solo se hai specificato --no-keep-index : avvia git reset (senza opzioni, es. git reset --mixed ). Questo cancella lo stato di "essere impegnato" per tutto, senza cambiare altro. (Naturalmente, eventuali modifiche parziali effettuate prima dell'esecuzione di git stash save -p , con git add o git add -p , vengono salvate in commit i .)

  • Senza -p :

    1. Esegui git reset --hard (con -q se lo hai specificato anche tu). In questo modo l'albero di lavoro torna allo stato nel commit HEAD .

    2. Solo se hai specificato -a o -u : esegui git clean --force --quiet -d (con -x se -a , o senza esso if -u ). Questo rimuove tutti i file non tracciati, comprese le directory non tracciate; con -x (cioè, sotto la modalità -a ), rimuove anche tutti i file ignorati.

    3. Solo se hai specificato -k / --keep-index : usa git read-tree --reset -u $i_tree per "riportare" l'indice nascosto come "modifiche da eseguire" che appaiono anche nell'albero di lavoro. (Il --reset dovrebbe avere alcun effetto dal momento in cui il passaggio 1 ha eliminato l'albero di lavoro.)

Le varie opzioni al momento della apply

I due sottocomandi principali che ripristinano uno stash sono apply e pop . Il codice pop appena eseguito si apply e quindi, se l' apply esito positivo, viene eseguito il drop , quindi, in effetti, è davvero solo apply . (Bene, c'è anche un branch , che è un po 'più complicato, ma alla fine usa anche apply ).

Quando si applica uno stash, qualsiasi "object stash-like", in realtà, ad esempio, tutto ciò che lo script stash può trattare come una borsa-stash, ci sono solo due opzioni specifiche per la stash:

  • -q , --quiet
  • --index (non - --keep-index !)

Altre bandiere vengono accumulate, ma vengono comunque ignorate in ogni caso. (Lo stesso codice di parsing è usato per show , e qui gli altri flag vengono passati a git diff .)

Tutto il resto è controllato dal contenuto dello stash-bag e dallo stato dell'albero di lavoro e dell'indice. Come sopra, userò le etichette w , i e u per indicare i vari commit nella memoria, e C per indicare il commit da cui si appende la borsa.

La sequenza di apply va in questo modo, assumendo che tutto vada bene (se qualcosa fallisce presto, ad esempio, siamo nel mezzo di un'unione, o git apply --cached fallisce, lo script ha errori in quel punto):

  1. scrivi l'indice corrente in un albero, assicurandoti di non trovarci nel bel mezzo di un'unione
  2. solo se --index : diff commit i contro commit C , pipe to git apply --cached , salva la struttura risultante, e usa git reset per renderlo stemperato
  3. solo se esiste: usa git read-tree e git checkout-index --all con un indice temporaneo, per recuperare l'albero u
  4. usa git merge-recursive per unire l'albero per C (la "base") con quello scritto nel passo 1 ("aggiornato upstream") e l'albero in w ("modifiche nascoste")

Dopo questo punto diventa un po 'complicato 🙂 in quanto dipende dal fatto che l'unione nel passaggio 4 sia andata bene. Ma prima espandiamoci un po 'sopra.

Il passaggio 1 è piuttosto semplice: lo script esegue semplicemente git write-tree , che fallisce se ci sono voci non sommerse nell'indice. Se l'albero di scrittura funziona, il risultato è un ID albero ( $c_tree nello script).

Il passaggio 2 è più complicato in quanto controlla non solo l'opzione --index ma anche quella $b_tree != $i_tree (cioè, c'è una differenza tra l'albero per C e l'albero per i ), e quello $c_tree ! = $i_tree (ovvero, esiste una differenza tra l'albero scritto nel passaggio 1 e l'albero per i ). Il test per $b_tree != $i_tree ha senso: sta controllando se c'è qualche modifica da applicare. Se non c'è alcun cambiamento - se l'albero per i corrisponde a quello per C -there's nessun indice da ripristinare, e --index non è necessario dopo tutto. Tuttavia, se $i_tree corrisponde a $c_tree , ciò significa semplicemente che l'indice corrente contiene già le modifiche da ripristinare tramite --index . È vero che, in questo caso, non vogliamo git apply queste modifiche; ma vogliamo che restino "ripristinati". (Forse è questo il punto del codice che non capisco più in basso, sembra più probabile che ci sia un piccolo bug qui, però.)

In ogni caso, se il passaggio 2 ha bisogno di eseguire git apply --cached , esegue anche git write-tree per scrivere l'albero, salvandolo nella variabile $unstashed_index_tree dello script. Altrimenti $unstashed_index_tree è vuoto.

Il passaggio 3 è dove le cose vanno male in una directory "sporca". Se il commit di u esiste nella memoria, lo script richiede di git checkout-index --all , ma git checkout-index --all fallirà se qualcuno di questi file verrà sovrascritto. (Si noti che questo viene fatto con un file indice temporaneo, che viene rimosso in seguito: il passaggio 3 non usa affatto la normale area di staging).

(Il passo 4 utilizza tre variabili di ambiente "magiche" che non ho visto documentate: $GITHEAD_ t fornisce il "nome" degli alberi che vengono uniti. Per eseguire git merge-recursive , lo script fornisce quattro argomenti: $b_tree -- $c_tree $w_tree Come già notato, questi sono gli alberi per il commit di base C , l'index-at-start-of- apply e il commit del lavoro stoppato w . Per ottenere nomi di stringhe per ciascuno di questi alberi, git merge-recursive aspetto git merge-recursive nell'ambiente per i nomi formati dalla GITHEAD_ di GITHEAD_ al raw SHA-1 per ogni albero Lo script non passa alcun argomento di strategia a git merge-recursive , né consente di scegliere una strategia diversa da recursive . Probabilmente dovrebbe.)

Se l'unione ha un conflitto, lo script di stash esegue git rerere (qv) e, se --index , ti dice che l'indice non è stato ripristinato ed esce con lo stato di conflitto di unione. (Come con altre uscite anticipate, questo impedisce a un pop di rilasciare lo stash.)

Se l'unione ha esito positivo, tuttavia:

  • Se abbiamo un $unstashed_index_tree -ie, stiamo facendo --index , e anche tutti gli altri test nel passaggio 2 - quindi dobbiamo ripristinare lo stato dell'indice creato nel passaggio 2. In questo caso un semplice git read-tree $unstashed_index_tree (senza opzioni) fa il trucco.

  • Se non abbiamo qualcosa in $unstashed_index_tree , lo script usa git diff-index --cached --name-only --diff-filter=A $c_tree per trovare i file da aggiungere, git read-tree --reset $c_tree per fare una fusione ad albero singolo contro l'indice salvato originale, e quindi git update-index --add con i nomi dei file dal precedente diff-index . Non sono proprio sicuro del motivo per cui va a queste lunghezze (c'è un suggerimento nella pagina man di git-read-tree , su come evitare i falsi colpi per i file modificati, che potrebbero spiegarlo), ma è quello che fa.

Infine, lo script esegue lo git status (con l'output inviato a /dev/null per la modalità -q , non è sicuro il motivo per cui viene eseguito tutto sotto -q ).

Qualche parola su git stash branch

Se hai problemi nell'applicare una scorta, puoi trasformarla in un "ramo reale", che lo rende garantito-per-ripristinare (eccetto, come al solito, per il problema di una scorta contenente un commit che non si applica a meno che tu non pulisca prima i file non programmati e forse anche ignorati prima).

Il trucco qui è iniziare verificando commit C (es. git checkout stash^ ). Questo ovviamente si traduce in un "HEAD distaccato", quindi è necessario creare un nuovo ramo, che è ansible combinare con il passaggio che controlla commit C :

 git checkout -b new_branch stash^ 

Ora puoi applicare lo stash, anche con --index , e dovrebbe funzionare dato che si applicherà allo stesso commit che il sacchetto stash pende da:

 git stash apply --index 

A questo punto, qualsiasi modifica di prima messa in scena dovrebbe essere riproposta, e qualsiasi file nonstaged (ma tracciato) precedente avrà le modifiche non tracciate ma non tracciate nella directory di lavoro. È sicuro di lasciare la scorta ora:

 git stash drop 

usando:

 git stash branch new_branch 

fa semplicemente la sequenza sopra per te. Esegue letteralmente git checkout -b , e se questo succede, applica lo stash (con --index ) e poi lo rilascia.

Dopo averlo fatto, puoi commettere l'indice (se lo desideri), quindi aggiungere e salvare i file rimanenti, per fare due (o uno se si omette il primo, indice, commit) commit "regolari" su un "normale" " ramo:

 ooCo-... <-- some_branch \ IW <-- new_branch 

e tu hai convertito la sacca di scorta che i e w impegna in ordinarie, commesse in filiale I e W


1 Più correttamente, git add-interactive --patch=stash -- , che richiama direttamente lo script perl per l'aggiunta intertriggers, con un set magico speciale per la memorizzazione. Ci sono alcuni altri modi magici - di --patch ; guarda la sceneggiatura

2 C'è un bug molto piccolo qui: git legge $i_tree , l'albero dell'indice impegnato, nell'indice temporaneo, ma poi diff la directory di lavoro contro HEAD . Ciò significa che se hai modificato un file f nell'indice, poi cambiato di nuovo in modo che corrisponda alla revisione HEAD , l'albero di lavoro memorizzato sotto w nella cartella contiene la versione di indice di f invece della versione di albero di lavoro di f .

Senza comprendere appieno perché il problema si verifica, ho trovato una soluzione rapida:

 git show -p --no-color [] | git apply 

L’opzione --no-color rimuove tutti i colors dall’output diff, perché git apply comando git apply .

Tuttavia, sarebbe bello se qualcuno potesse modificare questa risposta, fornendo la spiegazione del perché git stash pop fallisce.