Como faço para git apropriadamente / pop em ganchos de pré-commit para obter uma tree de trabalho limpa para testes?

Eu estou tentando fazer um gancho de pre-commit com uma corrida nua de testes de unidade e quero ter certeza de que meu diretório de trabalho está limpo. A compilation leva muito tempo, então eu quero aproveitar a reutilização de binários compilados sempre que possível. Meu script segue exemplos que eu vi online:

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

Isso causa problemas embora. Aqui está a reprodução:

  1. Adicionar // Step 1 a a.java
  2. git add .
  3. Adicione // Step 2 a a.java
  4. git commit
    1. git stash -q --keep-index # Mudanças de Stash
    2. Executar testes
    3. git stash pop -q # Restaurar alterações

Neste ponto, eu acertei o problema. O git stash pop -q aparentemente tem um conflito e em a.java eu tenho

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

Existe uma maneira de fazer isso estourar de forma limpa?

    Há, mas vamos chegar lá de uma forma ligeiramente indireta. (Além disso, veja o aviso abaixo: há um bug no código stash que eu achei muito raro, mas aparentemente mais pessoas estão encontrando).

    git stash save (a ação padrão para git stash ) faz um commit que tem pelo menos dois pais (veja essa resposta para uma questão mais básica sobre stashes). O stash commit é o estado da tree de trabalho e o segundo stash^2 confirmação pai stash^2 é o estado de índice no momento do stash.

    Depois que o stash é feito (e assumindo nenhuma opção -p ), o script – git stash é um script de shell – usa git reset --hard para limpar as alterações.

    Quando você usa --keep-index , o script não altera o stash salvo de forma alguma. Em vez disso, após a operação git reset --hard do git reset --hard , o script usa uma git read-tree --reset -u extra git read-tree --reset -u para apagar as alterações do diretório de trabalho, substituindo-as pela parte “index” do stash.

    Em outras palavras, é quase como fazer:

     git reset --hard stash^2 

    exceto que o git reset também moveria o branch – não o que você quer, daí o método read-tree .

    É aqui que o seu código volta. Agora você # Run tests no conteúdo do commit do índice.

    Supondo que tudo corra bem, eu presumo que você queira colocar o índice de volta no estado que tinha quando você fez o git stash , e colocar a tree de trabalho de volta em seu estado também.

    Com git stash apply ou git stash pop , a maneira de fazer isso é usar --index (não --keep-index , apenas para o tempo de criação de stash, para informar ao script stash “whack on the work directory”).

    O uso de --index ainda falhará, porque --keep-index o índice para o diretório de trabalho. Então você deve primeiro se livrar de todas essas mudanças … e para fazer isso, você simplesmente precisa (re) executar o git reset --hard , assim como o próprio script stash fez anteriormente. (Provavelmente você também quer -q .)

    Então, isso dá como a última etapa # Restore changes :

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

    (Eu os separaria como:

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

    eu mesmo, apenas por clareza, mas o pop vai fazer a mesma coisa).


    Como observado em um comentário abaixo, o git stash pop --index -q final git stash pop --index -q reclama um pouco (ou, pior, restaura um stash antigo ) se a etapa inicial do git stash save não encontrar nenhuma mudança para salvar. Portanto, você deve proteger a etapa de “restauração” com um teste para ver se a etapa de “salvamento” realmente escondeu algo.

    O git stash --keep-index -q inicial git stash --keep-index -q simplesmente sai silenciosamente (com status 0) quando não faz nada, então precisamos lidar com dois casos: nenhum stash existe antes ou depois do save; e, algum stash existia antes do save, e o save não fez nada, então o stash antigo existente ainda é o topo do stash stash.

    Eu acho que o método mais simples é usar o git rev-parse para descobrir quais nomes refs/stash , se houver alguma coisa. Então, devemos ter o script de ler algo mais como isto:

     #! /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 

    aviso: pequeno bug no git stash

    Há um pequeno bug na maneira como o git stash escreve seu “stash bag” . O stash do estado do índice está correto, mas suponha que você faça algo assim:

     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 

    Quando você executa git stash save depois disso, o índice-commit ( refs/stash^2 ) tem o texto inserido em foo.txt . O commit da tree de trabalho ( refs/stash ) deve ter a versão do foo.txt sem o material extra inserido. Se você olhar para ele, verá que tem a versão incorreta (modificada pelo índice).

    O script acima usa --keep-index para configurar a tree de trabalho como o índice, o que é perfeitamente correto e faz a coisa certa para executar os testes. Depois de executar os testes, ele usa git reset --hard para voltar ao estado de HEAD commit (que ainda está perfeitamente bem) … e então ele usa git stash apply --index para restaurar o índice (que funciona) eo diretório de trabalho.

    É aí que isso dá errado. O índice é (corretamente) restaurado a partir do commit do índice stash, mas o diretório de trabalho é restaurado a partir do commit do diretório de trabalho stash. Este commit do diretório de trabalho tem a versão do foo.txt que está no índice. Em outras palavras, essa última etapa – cp /tmp/save foo.txt – que desfez a alteração, foi desfeita!

    (O erro no script stash ocorre porque o script compara o estado da tree de trabalho com a consolidação HEAD para calcular o conjunto de arquivos a serem gravados no índice temporário especial antes de fazer a parte especial de confirmação do trabalho-dir do stash-bag Como o foo.txt não é alterado em relação ao HEAD , ele falha ao git add lo ao índice temporário especial.Em seguida, o commit especial da tree de trabalho é feito com a versão do foo.txt do índice- foo.txt correção é muito simples, mas ninguém colocou isso no git oficial [ainda?].

    Não que eu queira encorajar as pessoas a modificar suas versões do git, mas aqui está a correção .)

    Graças à resposta do @torek, consegui criar um script que também lida com arquivos não rastreados. (Nota: Eu não quero usar o git stash -u devido a um comportamento indesejado do git stash -u )

    O bug do git stash mencionado permanece inalterado e eu ainda não tenho certeza se este método pode ter problemas quando um .gitignore está entre os arquivos alterados. (o mesmo se aplica à resposta de @ 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 stash@{0} else git reset --hard -q && git stash apply --index -q stash@{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 

    Com base na resposta de Torek, eu criei um método para garantir o comportamento correto de stashing changes sem usar git rev-parse , em vez disso usei git stash create e git stash store (embora usar git stash store não seja estritamente necessário) Nota devido ao ambiente que estou trabalhando no meu script é escrito em php em vez de bash

     #!/php/php < ?php $files = array(); $stash = array(); exec('git stash create -q', $stash); $do_stash = !(empty($stash) || empty($stash[0])); if($do_stash) { exec('git stash store '.$stash[0]); //store the stash (does not tree state like git stash save does) exec('git stash show -p | git apply --reverse'); //remove working tree changes exec('git diff --cached | git apply'); //re-add indexed (ready to commit) changes to working tree } //exec('git stash save -q --keep-index', $stash); exec('git diff-index --cached --name-only HEAD', $files ); // dont redirect stderr to stdin, we will get the errors twice, redirect it to dev/null if ( PHP_OS == 'WINNT' ) $redirect = ' 2> 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 adaptado daqui http://blog.dotsamazing.com/2010/04/ask-git-to-check-if-your-codes-are-error-free/