我如何在预提交钩子中正确地存储/弹出一个干净的工作树来进行测试? [英] How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?

查看:95
本文介绍了我如何在预提交钩子中正确地存储/弹出一个干净的工作树来进行测试?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

 #存储更改
git stash -q --keep-index

#运行测试
...

#恢复更改
git stash pop -q

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


  1. // Step 1 添加到 a.java

  2. git add。

  3. Add //步骤2 a.java

  4. git commit


    1. git stash -q --keep-index #Stash changes

    2. 运行测试

    3. git存储pop -q #还原更改


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

  //步骤1 
<< {<更新上游
=======
//步骤2
>>>>>>>隐藏更改

有没有办法让这个弹出干净?

解决方案

现在有了 - 但我们可以稍微环游一下。 (另外,请参阅下面的警告:存储代码中存在一个我认为非常罕见的错误,但显然有更多人遇到。)

git stash save ( git stash 的默认操作)提交至少有两个父项(请参阅这个答案关于存储的一个更基本的问题)。 stash commit是工作树状态,而第二个父提交 stash ^ 2 是索引状态存款的时间。



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



当您使用 - keep-index 时,脚本不会以任何方式更改保存的存储。相反,在 git reset --hard 操作之后,脚本使用额外的 git读取树--reset -u 消除工作目录的变化,用隐藏的索引部分替换它们。换句话说,就像做:



  git reset --hard stash ^ 2 

除了 git reset 也会移动分支 - 根本就不是你想要的,因此读取树 code>方法。



这是您的代码返回的位置。您现在#Run tests 对索引提交的内容。



假设一切顺利,我认为您希望将索引恢复到您执行<$ c $时的状态c> git stash ,并让工作树回到它的状态。



使用 git stash apply 或 git stash pop ,要做到这一点的方法是使用 - index (不是 - keep-index ,那就是ju st创建存储时间,告诉存储脚本重击工作目录)。

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



所以,这给出了最后一个 #恢复更改步骤:

 #恢复更改
git reset --hard -q
git stash pop --index -q

(我将它们分隔为:

  git stash apply --index -q&&&& git stash drop -q 
pop
会做同样的事情)。 $ b

/ p>




正如下面的评论所述,最终 git stash pop --index -q <如果最初的 git存储保存步骤找不到要保存的更改,code>会抱怨一点(或者更糟糕的是会恢复旧的存储) 。因此,您应该通过测试来保护恢复步骤,以查看保存步骤是否实际存储了任何内容。



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



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

 #! / bin / sh 
#脚本对要提交的内容运行测试

#首先,隐藏索引和工作目录,仅保留
#将被提交的更改在工作目录中。
old_stash = $(git rev-parse -q --verify refs / stash)
git stash save -q --keep-index
new_stash = $(git rev-parse -q - -verify refs / stash)

#如果没有变化(例如`--amend`或`--allow-empty`)
#则没有任何东西被隐藏,我们应该跳过一切,
#包括测试本身。 (可能测试在前一次提交时通过
#,所以不需要重新运行它们。)
if [$ old_stash=$ new_stash];然后
echopre-commit script:no changes to test
sleep 1#XXX hack,editor may delete message
exit 0
fi

#运行测试
status = ...

#恢复更改
git reset --hard -q&& git stash apply --index -q&& git stash drop -q

#从测试运行状态退出:非零防止提交
退出$ status






警告:git存储中的小bug



方式 git stash 写入它的藏匿袋。索引状态隐藏是正确的,但是假设你做了这样的事情:

  cp foo.txt / tmp / save#save原始版本
sed -i''-e'1s / ^ / inserted /'foo.txt#插入一个变化
git add foo.txt#记录它在索引
cp / tmp / save foo.txt#然后撤消更改

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



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



这是出错的地方。索引从存储索引提交(正确)恢复,但工作目录从存储工作目录提交恢复。此工作目录提交的索引中包含 foo.txt 版本。换句话说,最后一步 - cp / tmp / save foo.txt - 没有改变!



stash 脚本中的错误发生是因为脚本比较了工作树状态与 HEAD commit以便计算要在特殊临时索引中记录的文件集,然后再将特殊工作目录提交到隐藏包的一部分。 c>对于 HEAD 是不变的,它没有将 git add 它添加到特殊临时索引。然后使用索引提交的版本 foo.txt 进行-tree提交,修复非常简单,但没有人将它放到官方的git中[还没有?]。



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


I'm trying to do a pre-commit hook with a bare run of unit tests and I want to make sure my working directory is clean. Compiling takes a long time so I want to take advantage of reusing compiled binaries whenever possible. My script follows examples I've seen online:

# Stash changes
git stash -q --keep-index

# Run tests
...

# Restore changes
git stash pop -q

This causes problems though. Here's the repro:

  1. Add // Step 1 to a.java
  2. git add .
  3. Add // Step 2 to a.java
  4. git commit

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

At this point I hit the problem. The git stash pop -q apparently has a conflict and in a.java I have

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

Is there a way to get this to pop cleanly?

解决方案

There is—but let's get there in a slightly roundabout fashion. (Also, see warning below: there's a bug in the stash code which I thought was very rare, but apparently more people are running into.)

git stash save (the default action for git stash) makes a commit that has at least two parents (see this answer to a more basic question about stashes). The stash commit is the work-tree state, and the second parent commit stash^2 is the index-state at the time of the stash.

After the stash is made (and assuming no -p option), the script—git stash is a shell script—uses git reset --hard to clean out the changes.

When you use --keep-index, the script does not change the saved stash in any way. Instead, after the git reset --hard operation, the script uses an extra git read-tree --reset -u to wipe out the work-directory changes, replacing them with the "index" part of the stash.

In other words, it's almost like doing:

git reset --hard stash^2

except that git reset would also move the branch—not at all what you want, hence the read-tree method instead.

This is where your code comes back in. You now # Run tests on the contents of the index commit.

Assuming all goes well, I presume you want to get the index back into the state it had when you did the git stash, and get the work-tree back into its state as well.

With git stash apply or git stash pop, the way to do that is to use --index (not --keep-index, that's just for stash-creation time, to tell the stash script "whack on the work directory").

Just using --index will still fail though, because --keep-index re-applied the index changes to the work directory. So you must first get rid of all of those changes ... and to do that, you simply need to (re)run git reset --hard, just like the stash script itself did earlier. (Probably you also want -q.)

So, this gives as the last # Restore changes step:

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

(I'd separate them out as:

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

myself, just for clarity, but the pop will do the same thing).


As noted in a comment below, the final git stash pop --index -q complains a bit (or, worse, restores an old stash) if the initial git stash save step finds no changes to save. You should therefore protect the "restore" step with a test to see if the "save" step actually stashed anything.

The initial git stash --keep-index -q simply exits quietly (with status 0) when it does nothing, so we need to handle two cases: no stash exists either before or after the save; and, some stash existed before the save, and the save did nothing so the old existing stash is still the top of the stash stack.

I think the simplest method is to use git rev-parse to find out what refs/stash names, if anything. So we should have the script read something more like this:

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


warning: small bug in git stash

There's a minor bug in the way git stash writes its "stash bag". The index-state stash is correct, but suppose you do something like this:

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

When you run git stash save after this, the index-commit (refs/stash^2) has the inserted text in foo.txt. The work-tree commit (refs/stash) should have the version of foo.txt without the extra inserted stuff. If you look at it, though, you'll see it has the wrong (index-modified) version.

The script above uses --keep-index to get the working tree set up as the index was, which is all perfectly fine and does the right thing for running the tests. After running the tests, it uses git reset --hard to go back to the HEAD commit state (which is still perfectly fine) ... and then it uses git stash apply --index to restore the index (which works) and the work directory.

This is where it goes wrong. The index is (correctly) restored from the stash index commit, but the work-directory is restored from the stash work-directory commit. This work-directory commit has the version of foo.txt that's in the index. In other words, that last step—cp /tmp/save foo.txt—that undid the change, has been un-un-done!

(The bug in the stash script occurs because the script compares the work-tree state against the HEAD commit in order to compute the set of files to record in the special temporary index before making the special work-dir commit part of the stash-bag. Since foo.txt is unchanged with respect to HEAD, it fails to git add it to the special temporary index. The special work-tree commit is then made with the index-commit's version of foo.txt. The fix is very simple but no one has put it into official git [yet?].

Not that I want to encourage people to modify their versions of git, but here's the fix.)

这篇关于我如何在预提交钩子中正确地存储/弹出一个干净的工作树来进行测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆