Come faccio a gettare correttamente il pop / pop nei ganci pre-commit per ottenere un albero di lavoro pulito per i test?

Sto provando a fare un hook pre-commit con una serie di test unitari e voglio assicurarmi che la mia directory di lavoro sia pulita. La compilazione richiede molto tempo quindi voglio sfruttare il riutilizzo dei binari compilati ogni volta che è ansible. La mia sceneggiatura segue esempi che ho visto online:

# Stash changes git stash -q --keep-index # Run tests ... # Restore changes git stash pop -q 

Questo causa problemi però. Ecco la replica:

  1. Aggiungi // Step 1 a a.java
  2. git add .
  3. Aggiungi // Step 2 a a.java
  4. git commit
    1. git stash -q --keep-index # Stash cambia
    2. Esegui test
    3. git stash pop -q # Ripristina le modifiche

A questo punto ho colpito il problema. Il git stash pop -q apparentemente ha un conflitto e in a.java che ho

 // Step 1 <<<<<<>>>>>> Stashed changes 

C’è un modo per farlo sembrare pulito?

C’è, ma andiamo in un modo un po ‘approssimativo. (Inoltre, vedi l’avvertimento di seguito: c’è un bug nel codice di scorta che ho pensato fosse molto raro, ma apparentemente più persone stanno incontrando.)

git stash save (l’azione predefinita per git stash ) esegue un commit che ha almeno due genitori (vedere questa risposta ad una domanda più basilare sugli stash). Il commit stash è lo stato dell’albero di lavoro e il secondo parent stash^2 commit stash^2 è lo stato dell’indice al momento della stash.

Dopo aver fatto lo stash (e supponendo che non ci sia l’opzione -p ), lo git stash script è uno script di shell, usa git reset --hard per cancellare le modifiche.

Quando usi --keep-index , lo script non cambia in alcun modo la scorta salvata. Invece, dopo l’operazione git reset --hard , lo script usa un extra git read-tree --reset -u per cancellare le modifiche della directory di lavoro, sostituendole con la parte “index” della stash.

In altre parole, è quasi come fare:

 git reset --hard stash^2 

ad eccezione del fatto che git reset sposterebbe anche il ramo, non del tutto ciò che si desidera, da cui il metodo read-tree .

È qui che rientra il tuo codice. Ora # Run tests sui contenuti del commit dell’indice.

Supponendo che tutto vada per il verso giusto, presumo che tu voglia riportare l’indice nello stato in cui si trovava quando hai fatto il git stash e riportare l’albero di lavoro nel suo stato.

Con git stash apply o git stash pop , il modo per farlo è usare --index (non --keep-index , che è solo per il tempo di creazione di una stash, per dire allo script di stash “whack on the work directory”).

L’uso di --index fallirà comunque, perché --keep-index ha applicato nuovamente le modifiche dell’indice alla directory di lavoro. Quindi devi prima sbarazzarti di tutti quei cambiamenti … e per farlo devi semplicemente (ri) eseguire git reset --hard , proprio come lo stesso script di stash in precedenza. (Probabilmente vuoi anche -q .)

Quindi, questo dà come l’ultimo passo # Restore changes :

 # Restore changes git reset --hard -q git stash pop --index -q 

(Li separerei come:

 git stash apply --index -q && git stash drop -q 

me stesso, solo per chiarezza, ma il pop farà la stessa cosa).


Come notato in un commento qui sotto, l’ultimo git stash pop --index -q lamenta un po ‘(o, peggio, ripristina una vecchia memoria) se la fase iniziale di git stash save non trova modifiche da salvare. Dovresti quindi proteggere il passaggio “ripristina” con un test per vedere se il passaggio “salva” ha effettivamente nascosto qualcosa.

L’iniziale git stash --keep-index -q esce semplicemente tranquillamente (con stato 0) quando non fa nulla, quindi dobbiamo gestire due casi: nessuna stash esiste prima o dopo il salvataggio; e, alcuni stash esistevano prima del salvataggio, e il salvataggio non ha fatto nulla in modo che il vecchio stash esistente sia ancora in cima alla pila di scorta.

Penso che il metodo più semplice sia usare git rev-parse per scoprire quali sono i nomi refs/stash , se non altro. Quindi dovremmo fare in modo che la sceneggiatura legga qualcosa di più simile a questo:

 #! /bin/sh # script to run tests on what is to be committed # First, stash index and work dir, keeping only the # to-be-committed changes in the working directory. old_stash=$(git rev-parse -q --verify refs/stash) git stash save -q --keep-index new_stash=$(git rev-parse -q --verify refs/stash) # If there were no changes (eg, `--amend` or `--allow-empty`) # then nothing was stashed, and we should skip everything, # including the tests themselves. (Presumably the tests passed # on the previous commit, so there is no need to re-run them.) if [ "$old_stash" = "$new_stash" ]; then echo "pre-commit script: no changes to test" sleep 1 # XXX hack, editor may erase message exit 0 fi # Run tests status=... # Restore changes git reset --hard -q && git stash apply --index -q && git stash drop -q # Exit with status from test-run: nonzero prevents commit exit $status 

avviso: piccolo bug in git stash

C’è un piccolo bug nel modo in cui git stash scrive il suo “sacchetto di scorta” . Lo stash index-state è corretto, ma supponiamo di fare qualcosa del genere:

 cp foo.txt /tmp/save # save original version sed -i '' -e '1s/^/inserted/' foo.txt # insert a change git add foo.txt # record it in the index cp /tmp/save foo.txt # then undo the change 

Dopo aver eseguito git stash save dopo questo, index-commit ( refs/stash^2 ) ha il testo inserito in foo.txt . Il commit dell’albero di lavoro ( refs/stash ) dovrebbe avere la versione di foo.txt senza le cose in più inserite. Se lo guardi, però, vedrai che ha la versione sbagliata (indicizzata).

Lo script precedente utilizza --keep-index per ottenere la struttura dell’albero di lavoro come era l’indice, che è tutto perfettamente a posto e fa la cosa giusta per eseguire i test. Dopo aver eseguito i test, usa git reset --hard per tornare allo stato di commit HEAD (che è ancora perfettamente a posto) … e poi usa git stash apply --index per ripristinare l’indice (che funziona) e il directory di lavoro.

Questo è dove va male. L’indice è (correttamente) ripristinato dal commit dell’indice di stash, ma la directory di lavoro viene ripristinata dal commit della directory di lavoro stash. Questo commit della directory di lavoro ha la versione di foo.txt presente nell’indice. In altre parole, l’ultimo steppp cp /tmp/save foo.txt – che ha annullato la modifica, non è stato completato!

(Il bug nello script di stash verifica perché lo script confronta lo stato dell’albero di lavoro con il commit HEAD per calcolare il set di file da registrare nell’indice temporaneo speciale prima di rendere la cartella di lavoro speciale parte di commit dello stash-bag Dato che foo.txt è invariato rispetto a HEAD , non riesce ad git add all’indice temporaneo speciale, quindi il commit speciale dell’albero di lavoro viene eseguito con la versione index-commit di foo.txt . La correzione è molto semplice ma nessuno l’ha messo in git ufficiale [ancora?].

Non che io voglia incoraggiare le persone a modificare le loro versioni di git, ma ecco la soluzione .)

Grazie alla risposta di @torek sono riuscito a mettere insieme uno script che si occupa anche di file non tracciati. (Nota: non voglio usare git stash -u causa di un comportamento indesiderato di git stash -u )

Il citato bug git stash rimane invariato e non sono ancora sicuro se questo metodo potrebbe incorrere in problemi quando un .gitignore è tra i file modificati. (lo stesso vale per la risposta di @ torek)

 #! /bin/sh # script to run tests on what is to be committed # Based on http://stackoverflow.com/a/20480591/1606867 # Remember old stash old_stash=$(git rev-parse -q --verify refs/stash) # First, stash index and work dir, keeping only the # to-be-committed changes in the working directory. git stash save -q --keep-index changes_stash=$(git rev-parse -q --verify refs/stash) if [ "$old_stash" = "$changes_stash" ] then echo "pre-commit script: no changes to test" sleep 1 # XXX hack, editor may erase message exit 0 fi #now let's stash the staged changes git stash save -q staged_stash=$(git rev-parse -q --verify refs/stash) if [ "$changes_stash" = "$staged_stash" ] then echo "pre-commit script: no staged changes to test" # re-apply changes_stash git reset --hard -q && git stash pop --index -q sleep 1 # XXX hack, editor may erase message exit 0 fi # Add all untracked files and stash those as well # We don't want to use -u due to # http://blog.icefusion.co.uk/git-stash-can-delete-ignored-files-git-stash-u/ git add . git stash save -q untracked_stash=$(git rev-parse -q --verify refs/stash) #Re-apply the staged changes if [ "$staged_stash" = "$untracked_stash" ] then git reset --hard -q && git stash apply --index -q [email protected]{0} else git reset --hard -q && git stash apply --index -q [email protected]{1} fi # Run tests status=... # Restore changes # Restore untracked if any if [ "$staged_stash" != "$untracked_stash" ] then git reset --hard -q && git stash pop --index -q git reset HEAD -- . -q fi # Restore staged changes git reset --hard -q && git stash pop --index -q # Restore unstaged changes git reset --hard -q && git stash pop --index -q # Exit with status from test-run: nonzero prevents commit exit $status 

basandomi sulla risposta di torek ho trovato un metodo per garantire il comportamento corretto delle modifiche di stash senza usare git rev-parse , invece ho usato git stash create e git stash store (sebbene l’uso di git stash store non sia strettamente necessario) Nota dovuta al ambiente che sto lavorando nel mio script è scritto in php invece di bash

 #!/php/php  NUL'; else $redirect = ' 2> /dev/null'; $exitcode = 0; foreach( $files as $file ) { if ( !preg_match('/\.php$/i', $file ) ) continue; exec('php -l ' . escapeshellarg( $file ) . $redirect, $output, $return ); if ( !$return ) // php -l gives a 0 error code if everything went well continue; $exitcode = 1; // abort the commit array_shift( $output ); // first line is always blank array_pop( $output ); // the last line is always "Errors parsing httpdocs/test.php" echo implode("\n", $output ), "\n"; // an extra newline to make it look good } if($do_stash) { exec('git reset --hard -q'); exec('git stash apply --index -q'); exec('git stash drop -q'); } exit( $exitcode ); ?> 

script php adattato da qui http://blog.dotsamazing.com/2010/04/ask-git-to-check-if-your-codes-are-error-free/