我如何正确地git存储/popup预先提交挂钩得到一个干净的工作树进行testing?

我试图做一个预先提交钩与裸奔的unit testing,我想确保我的工作目录是干净的。 编译需要很长时间,所以我想尽可能地利用重新编译的二进制文件。 我的脚本遵循我在网上看到的例子:

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

这虽然导致问题。 这是repro:

  1. // Step 1添加到a.java
  2. git add .
  3. // Step 2添加到a.java
  4. git commit
    1. git stash -q --keep-index #保存更改
    2. 运行testing
    3. git stash pop -q #恢复更改

在这一点上,我遇到了问题。 git stash pop -q显然有一个冲突,并在a.java我有

 // Step 1 <<<<<<< Updated upstream ======= // Step 2 >>>>>>> Stashed changes 

有没有办法让这个popup干净?

有 – 但我们稍微迂回地到达那里。 (另外,请参阅下面的警告:存储代码中存在一个我认为非常罕见的错误,但显然有更多的人遇到。

git stash savegit stash save的默认操作)使一个至less有两个父母的提交(请参阅这个回答关于一个更基本的问题)。 stash提交是工作树状态,第二个父提交stash^2stash^2的索引状态。

隐藏之后(并且假设没有-p选项),脚本git stash是一个shell脚本 – 使用git reset --hard清除更改。

当您使用--keep-index ,脚本不会以任何方式更改保存的存储。 相反,在git reset --hard操作之后,脚本会使用额外的git read-tree --reset -u来清除工作目录的变化,并将其replace为隐藏的“索引”部分。

换句话说,这几乎就像是在做:

 git reset --hard stash^2 

除了git reset也会移动分支 – 根本不是你想要的,因此是read-tree方法。

这是你的代码返回的地方。你现在# Run tests对索引提交的内容# Run tests

假设一切顺利,我认为你想让索引恢复到你做git stash时的状态,并且让工作树回到它的状态。

使用git stash applygit stash pop ,要做到这一点的方法是使用--index (不是--keep-index ,这只是为了创build存储时间,告诉存储脚本“在工作目录上重击)”。

只是使用--index仍然会失败,因为--keep-index将索引更改重新应用于工作目录。 所以你必须首先摆脱所有这些变化……而且要做到这一点,你只需要(重新)运行git reset --hard ,就像之前的隐藏脚本一样。 (可能你也想要-q 。)

所以,这给最后一个# Restore changes步骤:

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

(我将它们分开为:

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

我自己,只是为了清楚,但pop将做同样的事情)。


正如在下面的注释中指出的那样,如果最初的git stash save步骤找不到保存的更改,最终的git stash pop --index -q抱怨一点(或者更糟糕的是还原一个旧的存储器)。 因此,您应该通过testing来保护“恢复”步骤,以确定“保存”步骤是否实际存储了任何内容。

初始的git stash --keep-index -q在没有任何操作的时候会静静地退出(状态为0),所以我们需要处理两种情况:在保存之前或之后都不存在stash, 并且在保存之前存在一些存储,并且存储没有做任何事情,所以旧的存储仍然是存储堆栈的顶部。

我认为最简单的方法是使用git rev-parse来找出什么是refs/stash名字,如果有的话。 所以我们应该让脚本读更多的东西:

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

警告:在git存储中的小错误

git stash方式有一个小问题。 索引状态隐藏是正确的,但是假设你做了这样的事情:

 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 

当你在这之后运行git stash save ,index-commit( refs/stash^2 )在foo.txt插入了文本。 工作树提交( refs/stash应该有没有额外插入的东西foo.txt的版本。 但是,如果你看看它,你会发现它有错误的(索引修改)版本。

上面的脚本使用了--keep-index来设置工作树作为索引,这一切都很好,并且正确运行testing。 运行testing后,它使用git reset --hard返回到HEAD commit状态(这仍然非常好)…然后它使用git stash apply --index来恢复索引(工作)和工作目录。

这是错误的地方。 索引从存储索引提交(正确)恢复,但工作目录从存储工作目录提交恢复。 此工作目录提交具有索引中的foo.txt版本。 换句话说,最后一步 – cp /tmp/save foo.txt – 解开了这个变化,一直没有完成!

stash脚本中的错误是因为脚本将工作树状态与HEAD提交进行比较,以便计算要在特殊临时索引中logging的文件集,然后才能创build隐藏包的特殊工作目录提交部分。由于foo.txtHEAD没有任何关系,因此无法git addgit add到特殊的临时索引中,然后使用索引提交的版本foo.txt进行特殊的工作树提交,修复非常简单,没有人把它放到官方的git中去了吗?

不是我想鼓励人们修改他们的版本的git,但这是修复 。)

感谢@torek的回答,我能够把一个脚本也处理未跟踪的文件。 (注意:由于git stash -u的不良行为,我不想使用git stash -u )

提到的git stash漏洞保持不变,我还不确定,当一个.gitignore位于已更改的文件中时,这个方法是否会遇到问题。 (同样适用于@ 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 

基于托雷克的答案我想出了一个方法,以确保正确的行为存储更改而不使用git rev-parse ,而是使用了git stash creategit stash store (尽pipe使用git stash store并不是绝对必要的)。环境我在我的脚本工作是用PHP而不是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 ); ?> 

从这里改编的PHP脚本http://blog.dotsamazing.com/2010/04/ask-git-to-check-if-your-codes-are-error-free/