简体   繁体   中英

(How) can I run git checkout from within the pre-commit hook?

There is a file that should be in our git repository so that it is in any checkout. It may be changed by users, but usually the changes should not be checked back in. Neither --assume_unchanged nor --skip_work_tree provide the required flexibility, and the file is too cumbersome to reasonably be 'modified' with smudge/clean filters.

So I've written a pre-commit hook that successfully asks the user if they're sure they want to commit the changes to this file. If they say yes, the file is checked in (hook returns 0, commit continues), if not, the commit is aborted.

Instead of aborting, I'd like to give the user the option to revert changes to the file and continue with the commit.

To revert the file to an unchanged state, I'm using git checkout -- file/in/question .

Given that the file is modified and staged for commit, I run the following pre-commit hook:

#!/bin/bash
echo "git checkout -- file/in/question"
git checkout -- file/in/question
echo "git status"
git status
exit 1 #would be 0 if the hook worked as expected

And I get the following output:

git checkout -- file/in/question
git status
On branch blah
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   file/in/question

Why does git status report that the git checkout has had no effect? (it's correct - returning 0 from the hook causes the file to be incorrectly committed)

When given a path, by default checkout updates the work tree from the index (ie from the changes staged for commit).

What you want is to update the index (presumably from HEAD , so as to leave the file unchanged by this commit). That can be done with

git reset HEAD -- file/in/question

This, by default, leaves the changes in the worktree as un-staged changes. This is probably safer than reverting both the index and the worktree, but if you really do want to revert both you can say

git checkout HEAD -- file/in/question

At all times, there are three (!) copies of every file (excluding the special cases of files being added or removed). This applies to your particular file too. For convenience let's call this file F .

  • One copy of F is in the current or HEAD commit. This copy is read-only.

  • One copy of F is in the index, and is ready to be committed. This copy is changeable, though we must be careful to see who will notice any changes and when.

  • The final copy of F is in the work-tree. This copy is changeable, though as with the index copy, we must be careful to see who will notice any changes and when. (It's also worth mentioning here, since you remarked about smudge and clean filters, that the work-tree copy of F is the only one with smudging and line-ending filtering applied; the copy in the index is the clean variant.)

  • If Git will write a commit, it does so using whatever is the index at that time.

  • If git commit is run as git commit -a or git commit <path1> <path2> ... <pathN> , Git is currently using a temporary index, with the normal or "real" index put aside. (This can come back to bite us later: Git will update the real index later if and only if the commit finishes.)

  • You are in a pre-commit hook, which means you have been run by git commit and are making a yes/no decision on whether to allow the commit to proceed.

Let's put these all together now and observe some issues.

  • If you make no changes in the pre-commit hook, there is nothing to worry about: you return a go/stop status, and Git proceeds to make a new commit from the index (after which HEAD points to the new commit), or it does not.

  • If you do make changes, you will make them in the index and/or the work-tree. Who will see these changes and when?

  • The change you actually called for is git checkout -- F . This copies from the index to the work-tree. This has no effect on what will be committed.

  • You can instead use git reset HEAD -- F or git checkout HEAD -- F . These will copy from the current commit to the index—the real index if that's the one we are using, or the temporary index if we are using a temporary index. The checkout form will also copy from the index to the work-tree.

  • If you let the commit proceed to completion, and Git is using a temporary index, Git will, as a final step, copy any entries it added to this temporary index (due to -a or <path> arguments) back to the real index; but if it's using the real index, it does not need to update the real index.

    In some (very) old versions of Git, Git fails to notice changes made to the index in the pre-commit hook (as in, it never re-reads the index). It's been too long for me to remember precisely what effect this has, or what Git versions it affects, but it's worth doing some careful testing around this: my somewhat vague memory is that Git had the commit-tree code built into the C code and by not re-reading the index it built the trees with the original index contents instead of the new contents, so that files copied into the index during a pre-commit hook did not actually make it into the commit.

In any case, if you update the file in the index, it's probably wise to update it in the work-tree as well, but consider the effect on someone who has carefully staged a different version of the file than the one in the work-tree. You will overwrite both the carefully-staged version and the work-tree version in this case.


In a different, but related, corner case, we should note when Git does copy index entries back from a temporary index to the real index (in the commit -a and commit <path> cases), this also wipes out any carefully differently-staged files. That is, if you do:

git add -p somefile

and carefully stage one version, then run git commit somefile to commit the current work-tree version first , you lose the carefully-staged version. That may (or may not) suggest how you might want to deal with this. In particular, it can be useful to have a pre-commit hook flat out refuse to work with a temporary index, if it is going to make any changes to what is staged and what is in the work-tree, just to avoid surprises.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM