我如何正确地git存储/popup预先提交挂钩得到一个干净的工作树进行testing?
我试图做一个预先提交钩与裸奔的unit testing,我想确保我的工作目录是干净的。 编译需要很长时间,所以我想尽可能地利用重新编译的二进制文件。 我的脚本遵循我在网上看到的例子:
# Stash changes git stash -q --keep-index # Run tests ... # Restore changes git stash pop -q
这虽然导致问题。 这是repro:
- 将
// Step 1
添加到a.java
-
git add .
- 将
// Step 2
添加到a.java
-
git commit
-
git stash -q --keep-index
#保存更改 - 运行testing
-
git stash pop -q
#恢复更改
-
在这一点上,我遇到了问题。 git stash pop -q
显然有一个冲突,并在a.java
我有
// Step 1 <<<<<<< Updated upstream ======= // Step 2 >>>>>>> Stashed changes
有没有办法让这个popup干净?
有 – 但我们稍微迂回地到达那里。 (另外,请参阅下面的警告:存储代码中存在一个我认为非常罕见的错误,但显然有更多的人遇到。
git stash save
( git stash save
的默认操作)使一个至less有两个父母的提交(请参阅这个回答关于一个更基本的问题)。 stash
提交是工作树状态,第二个父提交stash^2
是stash^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 apply
或git 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.txt
与HEAD
没有任何关系,因此无法git add
它git 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 create和git 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/