为了账号安全,请及时绑定邮箱和手机立即绑定

如何在预提交钩子中正确git stash / pop以获得一个干净的工作树进行测试?

/ 猿问

如何在预提交钩子中正确git stash / pop以获得一个干净的工作树进行测试?

Git
当年话下 2019-08-19 16:35:01

如何在预提交钩子中正确git stash / pop以获得一个干净的工作树进行测试?

我正在尝试使用一系列单元测试进行预提交钩子,我想确保我的工作目录是干净的。编译需要很长时间,所以我想尽可能利用重用编译的二进制文件。我的脚本遵循我在网上看到的例子:


# 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 #存储更改

运行测试

git stash pop -q #恢复更改

此时我遇到了问题。该git stash pop -q显然有冲突,a.java我有


// Step 1

<<<<<<< Updated upstream

=======

// Step 2

>>>>>>> Stashed changes

有没有办法让这个流畅干净?


查看完整描述

3 回答

?
精慕HU

有 - 但让我们以略微迂回的方式到达那里。(另请参阅下面的警告:隐藏代码中存在一个我认为非常罕见的错误,但显然有更多人遇到此问题。)

git stash save(默认操作git stash)进行至少有两个父项的提交。该stash承诺是工作树的状态,而第二父提交stash^2是索引状态在藏匿的时间。

在保存之后(假设没有-p选项),脚本git stash是一个shell脚本 - 用于git reset --hard清除更改。

使用时--keep-index,脚本不会以任何方式更改已保存的存储。相反,在git reset --hard操作之后,脚本使用额外git read-tree --reset -u消除工作目录更改,将其替换为存储的“索引”部分。

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

git reset --hard stash^2

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

这是您的代码重新出现的地方。您现在可以# Run tests查看索引提交的内容。

假设一切顺利,我假设您希望将索引恢复到它执行时的状态git stash,并使工作树恢复到其状态。

使用git stash apply或者git stash pop,这样做的方法是使用--index--keep-index不仅仅是为了存储创建时间,告诉存储脚本“在工作目录上打击”)。

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

所以,这是最后# Restore changes一步:

# Restore changesgit reset --hard -qgit stash pop --index -q

(我将它们分开为:

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

我自己,只是为了清晰,但pop意志做同样的事情)。


如下面的评论所述,如果初始步骤没有找到保存更改,则最终会git stash pop --index -q抱怨(或者更糟糕的是,恢复旧的存储)git stash save。因此,您应该通过测试来保护“恢复”步骤,以查看“保存”步骤是否实际隐藏了任何内容。

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

我认为最简单的方法是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-indexnew_stash=$(git rev-parse -q --verify refs/stash)# If there were no changes (e.g., `--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 0fi# Run testsstatus=...# Restore changesgit reset --hard -q && git stash apply --index -q && git stash drop -q# Exit with status from test-run: nonzero prevents commitexit $status

警告:git stash中的小bug

git stash写“藏匿袋”的方式有一个小错误。索引状态存储是正确的,但假设您执行以下操作:

cp foo.txt /tmp/save                    # save original versionsed -i '' -e '1s/^/inserted/' foo.txt   # insert a changegit add foo.txt                         # record it in the indexcp /tmp/save foo.txt                    # then undo the change

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

上面的脚本用于--keep-index将工作树设置为索引,这一切都非常好,并且正确运行测试。运行测试后,它用于git reset --hard返回HEAD提交状态(这仍然完全正常)...然后它用于git stash apply --index恢复索引(工作)和工作目录。

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

stash脚本中的错误发生是因为脚本将工作树状态与HEAD提交进行比较,以便在创建stash-bag的特殊work-dir提交部分之前计算要在特殊临时索引中记录的文件集。foo.txt对于特殊的临时索引HEAD,它没有改变git add。特殊的工作树提交是用index-commit的版本进行的foo.txt。修复非常简单,但是没有人把它放到官方git中呢? ]。

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


查看完整回答
反对 回复 2019-08-19
?
杨魅力

我能够整理一个处理未跟踪文件的脚本。(注意: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 stashold_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-indexchanges_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 0fi#now let's stash the staged changesgit stash save -qstaged_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 0fi# 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 -quntracked_stash=$(git rev-parse -q --verify refs/stash)#Re-apply the staged changesif [ "$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 testsstatus=...# Restore changes# Restore untracked if anyif [ "$staged_stash" != "$untracked_stash" ]then
    git reset --hard -q && git stash pop --index -q
    git reset HEAD -- . -qfi# Restore staged changesgit reset --hard -q && git stash pop --index -q# Restore unstaged changesgit reset --hard -q && git stash pop --index -q# Exit with status from test-run: nonzero prevents commitexit $status


查看完整回答
反对 回复 2019-08-19
?
波斯汪

根据torek的回答我提出了一种方法来确保存储更改的正确行为而不使用git rev-parse,而是使用git stash creategit stash存储(尽管使用git stash存储并不是绝对必要的)注意由于环境我在我的脚本中工作是用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/nullif ( 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/


查看完整回答
反对 回复 2019-08-19

添加回答

回复

举报

0/150
提交
取消
意见反馈 邀请有奖 帮助中心 APP下载
官方微信