我如何在预提交钩子中正确地存储/弹出一个干净的工作树来进行测试? [英] How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?
问题描述
我试图做一个预先提交的挂钩与单位测试的裸奔,我想确保我的工作目录是干净的。编译需要很长时间,所以我想尽可能地利用重新编译的二进制文件。我的脚本遵循我在线看到的例子:
#存储更改
git stash -q --keep-index
#运行测试
...
#恢复更改
git stash pop -q
虽然这会导致问题。这是repro:
- 将
// Step 1
添加到a.java
-
git add。
- Add
//步骤2
至a.java
-
git commit
-
git stash -q --keep-index
#Stash changes - 运行测试
-
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 ,并让工作树回到它的状态。
使用 只需使用 所以,这给出了最后一个 (我将它们分隔为: / p> 正如下面的评论所述,最终 最初的 我认为最简单的方法是如果有的话,使用 方式 当您运行 上面的脚本使用 这是出错的地方。索引从存储索引提交(正确)恢复,但工作目录从存储工作目录提交恢复。此工作目录提交的索引中包含 ( 不是我想鼓励人们修改他们的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: This causes problems though. Here's the repro: 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
$ c $我自己,只是为了清楚起见,但 pop
会做同样的事情)。 $ b
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中[还没有?]。
# Stash changes
git stash -q --keep-index
# Run tests
...
# Restore changes
git stash pop -q
// Step 1
to a.java
git add .
// Step 2
to a.java
git commit
git stash -q --keep-index
# Stash changes- Run tests
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屋!