How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?

Hounshell

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?

torek

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.)

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

git how to do diff outside of working tree

来自分类Dev

Git钩子:“。git / hooks / pre-commit”:不允许操作

来自分类Dev

Pre Commit hook git error

来自分类Dev

pre-commit-config.yaml中的args是否覆盖pre-commit-hooks.yaml中的args?

来自分类Dev

How do I create a git post-commit hook for trac repository integration

来自分类Dev

GIT: How do I add a file to the first commit (and rewrite history in the process)?

来自分类Dev

不要创建 git pre-commit 钩子

来自分类Dev

How do I update the working tree when checking out a branch with libgit2?

来自分类Dev

How can I stop myself from using 'git commit -a'?

来自分类Dev

如何进行git pre-commit代码检查?

来自分类Dev

如何使用这个git pre-commit钩子

来自分类Dev

git pre-commit 钩子中的整洁文件

来自分类常见问题

How do I edit an incorrect commit message with TortoiseGit?

来自分类Dev

How do I get Time Machine working again after changing my hard drive?

来自分类Dev

How do you properly split up casperjs tests with a shared browser instance

来自分类Dev

使用python库“ pre-commit”,如何添加运行单个python文件的git pre-commit钩子?

来自分类Dev

git commit和git commit-tree有什么区别

来自分类Dev

.Net Database how do I properly close my database connections?

来自分类Dev

什么是在输出中具有“ tree”的git commit?

来自分类Dev

什么是在输出中具有“ tree”的git commit?

来自分类Dev

How to deploy client-side git hooks?

来自分类Dev

如何使用flake8在git pre-commit挂钩中排除文件?

来自分类Dev

在GitHub客户端上下文中使用git pre-commit挂钩

来自分类Dev

在git pre-commit钩子中临时修改工作目录和暂存区

来自分类Dev

git pre-commit中的autopep8-如何自动重新提交?

来自分类Dev

git pre-commit挂钩可同时格式化和重新添加文件

来自分类Dev

尝试为checkstyle运行pre-commit git挂钩会返回多个“找不到文件!” 错误

来自分类Dev

仅当某些目录中的文件被修改时,才执行git pre-commit hook

来自分类Dev

如何使用flake8在git pre-commit挂钩中排除文件?

Related 相关文章

  1. 1

    git how to do diff outside of working tree

  2. 2

    Git钩子:“。git / hooks / pre-commit”:不允许操作

  3. 3

    Pre Commit hook git error

  4. 4

    pre-commit-config.yaml中的args是否覆盖pre-commit-hooks.yaml中的args?

  5. 5

    How do I create a git post-commit hook for trac repository integration

  6. 6

    GIT: How do I add a file to the first commit (and rewrite history in the process)?

  7. 7

    不要创建 git pre-commit 钩子

  8. 8

    How do I update the working tree when checking out a branch with libgit2?

  9. 9

    How can I stop myself from using 'git commit -a'?

  10. 10

    如何进行git pre-commit代码检查?

  11. 11

    如何使用这个git pre-commit钩子

  12. 12

    git pre-commit 钩子中的整洁文件

  13. 13

    How do I edit an incorrect commit message with TortoiseGit?

  14. 14

    How do I get Time Machine working again after changing my hard drive?

  15. 15

    How do you properly split up casperjs tests with a shared browser instance

  16. 16

    使用python库“ pre-commit”,如何添加运行单个python文件的git pre-commit钩子?

  17. 17

    git commit和git commit-tree有什么区别

  18. 18

    .Net Database how do I properly close my database connections?

  19. 19

    什么是在输出中具有“ tree”的git commit?

  20. 20

    什么是在输出中具有“ tree”的git commit?

  21. 21

    How to deploy client-side git hooks?

  22. 22

    如何使用flake8在git pre-commit挂钩中排除文件?

  23. 23

    在GitHub客户端上下文中使用git pre-commit挂钩

  24. 24

    在git pre-commit钩子中临时修改工作目录和暂存区

  25. 25

    git pre-commit中的autopep8-如何自动重新提交?

  26. 26

    git pre-commit挂钩可同时格式化和重新添加文件

  27. 27

    尝试为checkstyle运行pre-commit git挂钩会返回多个“找不到文件!” 错误

  28. 28

    仅当某些目录中的文件被修改时,才执行git pre-commit hook

  29. 29

    如何使用flake8在git pre-commit挂钩中排除文件?

热门标签

归档