在预提交钩子中调用git [英] Calling git in pre-commit hook

查看:93
本文介绍了在预提交钩子中调用git的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

例如,当我执行git pre-commit钩子时,我得到奇怪的结果 git diff --name-only在终端中似乎给出了不同的结果 在.git/hooks/pre-commit

中执行时

所以我的问题是:

  1. 我可以在git hooks中调用git吗?
  2. 如果1.正常:如果我执行git commit,则何时确切调用预提交钩子? -am"bla"?特别是git是否先进行登台,然后再调用预提交钩子?

我问这个是因为我尝试了2到3次: 我修改文件,我手动运行脚本,它会打印出来

#! /bin/sh -xv
files=$(git diff --name-only)
+ git diff --name-only
+ files=path/to/file.h
echo $files
+ echo path/to/file.h
path/to/file.h
...

当我执行git commit -am"eh"时,输出将不同

#! /bin/sh -xv
files=$(git diff --name-only)
+ git diff --name-only
+ files=
echo $files
+ echo

解决方案

  1. 我可以在git hooks中调用git吗?

是的,但是您必须谨慎行事,因为环境中设置了许多东西,并且您正在做的事情正在完成中:

  • GIT_DIR设置为Git目录的路径.
  • 可以将
  • GIT_WORKTREE设置为工作树的路径(来自git --work-tree).
  • 其他Git变量,例如GIT_NO_REPLACE_OBJECTS,也可以从命令行设置.

(如果您继续使用当前存储库,则应保留这些设置,但如果使用不同存储库,则应清除它们.)

  1. 如果1.可以:如果我执行git commit -am"bla",则何时调用预提交钩子?特别是git是否先进行登台,然后再调用预提交钩子?

这很复杂.

git commit内部使用三种模式". (对此没有任何保证,但是事情已经实现了很多年,所以这三种模式似乎很稳定.)这些模式是:

  • git commit,不带-a--include--only和/或任何命令行指定的文件名.这是默认或正常模式.基本的实现细节不会显示出来.

  • git commit -a或命令行指定的文件名.这分为两个子模式:

    • 使用--include这样的提交,或
    • 使用--only这样的提交.


    这时,基础实现将显示出来.

此处的基础实现细节涉及Git调用的内容,分别是 index 登台区域和(现在很少)缓存,通常实现为名为$GIT_DIR/index的文件(其中$GIT_DIR是有关第1点的注释中的环境变量).通常,只有以下一项: the 索引.它保存了您打算提交的内容. 1 当您运行git commit时,Git会将索引中的任何内容打包为下一次提交.

但是,在 git commit操作期间,可能最多有三个索引文件.对于普通的git commit,只有一个索引,您的预提交钩子可以使用它,甚至可以更新它. (出于某些原因,我们建议您不要对其进行更新.)

但是,如果您执行git commit -agit commit --include file.ext,则现在有两个索引文件.已经准备好要提交的内容-常规索引-一个 extra 索引,这是原始索引加上在file.ext上执行git add的结果或在所有文件上(等效于git add -u).因此,现在有两个索引文件.

在这种模式下,Git将常规索引文件保留为.该文件照常位于$GIT_DIR/index中.带有附加内容的 second 索引文件位于$GIT_DIR/index.lock中,并且环境变量GIT_INDEX_FILE设置为保留该路径.如果提交失败,Git将删除index.lock文件,一切将好像您根本没有运行git commit.如果提交成功 ,Git会将index.lock重命名为index,释放锁并一次更新 (标准,常规)索引.

最后,有一个 third 模式,例如,当您运行git commit --only file.ext时会得到.这里,现在有三个索引文件:

  • $GIT_DIR/index:标准索引,保留其通常的功能.
  • $GIT_DIR/index.lock:file.ext-已编辑到的file.ext标准索引的副本.
  • $GIT_DIR/indexsuffix:file.ext -ed已被编辑的HEAD commit 2 的副本.

环境变量GIT_INDEX_PATH指向该第三个索引.如果提交成功,Git会将index.lock文件重命名为index,以使其成为 the 索引.如果提交失败,Git将删除index.lock文件,以便索引返回到开始之前的状态. (无论哪种情况,Git都会删除已达到其目的的第三个索引.)

请注意,从预提交钩子中,您可以检测到git commit是标准提交(GIT_INDEX_FILE未设置或设置为$GIT_DIR/index)还是两种特殊模式之一.在标准模式下,如果要更新 索引,则可以照常进行.在两种特殊模式下,您可以使用git add修改GIT_INDEX_FILE命名的文件,这将修改提交内容.如果您使用的是--include样式的提交,那么这也会修改成功时将成为标准索引的内容.但是,如果您处于--only模式,则修改建议的提交不会会影响标准indexindex.lock成为标准索引.

考虑一个具体的例子,假设用户这样做了:

git add file1 file2

,以使标准索引与HEAD匹配,但file1file2除外.然后用户运行:

git commit --only file3

,因此建议的提交是HEAD的副本,其中添加了file3,如果该提交成功,则Git将用其中file1file2file3都被添加了(但由于file3将匹配新的HEAD提交,因此在新索引中仅文件1和2将被修改).

现在假设您的提交挂钩运行git add file4并且整个过程成功(新提交成功完成). git add步骤会将file4的工作树版本复制到临时索引中,以便该提交将同时更新file3 file4.然后,Git将重命名index.lock文件,以便file3与新的HEAD提交匹配.但是index.lock中的file4从未更新过,因此它不会HEAD提交匹配.用户似乎会以某种方式将file4还原了! git status将显示对其的暂挂更改,已暂存以提交,而git diff --cached将显示HEAD和索引之间的区别是file4已更改回以匹配HEAD~1中的file4 >.

可以对此模式进行预提交的钩子测试,并在此模式下拒绝git add文件,以避免出现此问题. (或者,您甚至可以使用第二个git add命令将file4添加到index.lock!)通常最好让钩子拒绝提交,并建议用户执行任何git add自己,这样您就不必一开始就知道所有关于 git commit的实现秘密.


1 该索引还包含一些额外的信息:缓存有关工作树的数据.这就是为什么有时将其称为缓存.我在这里描述的这些额外副本是通过复制原始索引而制成的,因此这些额外副本还具有相同的缓存数据,除非它们是通过git add更新的.

2 未指定Git是否通过以下等效的内部副本制作此副本:

TMP=$GIT_DIR/index<digits>
cp $GIT_DIR/index $TMP
GIT_INDEX_FILE=$TMP git reset
GIT_INDEX_FILE=$TMP git add file3

或其他某种方式(例如,内部等效项git read-tree),但是由于总是在过程结束时才删除此特定副本,因此这无关紧要:工作树的任何缓存信息都将变为不相关的.

I am getting weird results of running my git pre-commit hook, for example when I do git diff --name-only in terminal it seems to give different result than when it is executed in .git/hooks/pre-commit

So my questions are:

  1. Am I allowed to call git inside git hooks?
  2. If 1. is ok: when exactly is pre-commit hook called if I do git commit -am"bla"? In particular does git do staging first and then it calls the pre-commit hook or not?

I ask this because I tried 2 or 3 times this: I modify a file, I run the script manually, it prints out

#! /bin/sh -xv
files=$(git diff --name-only)
+ git diff --name-only
+ files=path/to/file.h
echo $files
+ echo path/to/file.h
path/to/file.h
...

When I do git commit -am"eh" then the output is different

#! /bin/sh -xv
files=$(git diff --name-only)
+ git diff --name-only
+ files=
echo $files
+ echo

解决方案

  1. Am I allowed to call git inside git hooks?

Yes, but you must exercise caution, as there are a number of things set in the environment and you're working with something that is in the middle of being done:

  • GIT_DIR is set to the path to the Git directory.
  • GIT_WORKTREE may be set to the path to the work-tree (from git --work-tree).
  • Other Git variables, such as GIT_NO_REPLACE_OBJECTS, may be set from the command line as well.

(You should leave these set if you're continuing to work with the current repository, but clear them out if you're working with a different repository.)

  1. If 1. is ok: when exactly is pre-commit hook called if I do git commit -am"bla"? In particular does git do staging first and then it calls the pre-commit hook or not?

This is complicated.

There are three "modes" that git commit uses internally. (There are no promises about this, but that's how things have been implemented for many years now so this three-modes thing seems pretty stable.) The modes are:

  • git commit without -a, --include, --only, and/or any command-line-specified file names. This is the default or normal mode. The underlying implementation details do not show through.

  • git commit with -a or with command-line-specified file names. This divides into two sub-modes:

    • such a commit with --include, or
    • such a commit with --only.


    At this point, the underlying implementation shows through.

The underlying implementation details here involve the thing that Git calls, variously, the index, the staging area, and (rarely now) the cache, which is normally implemented as a file named $GIT_DIR/index (where $GIT_DIR is the environment variable from the note about point 1). Normally, there is only one of these: the index. It holds the content that you intend to commit.1 When you run git commit, Git will package up whatever is in the index as the next commit.

But, during the operation of git commit, there may be up to three index files. For the normal git commit there's just the one index, and your pre-commit hook can use it and can even update it. (I advise against updating it, for reasons we'll see in a moment.)

But, if you do a git commit -a, or git commit --include file.ext, now there are two index files. There's the content that's ready to be committed—the regular index—and one extra index, which is the original index plus the result of doing a git add on file.ext or on all files (the equivalent of git add -u). So now there are two index files.

In this mode, Git leaves the regular index file as the regular index file. This file is in $GIT_DIR/index as usual. The second index file, with the extra added stuff, is in $GIT_DIR/index.lock and the environment variable GIT_INDEX_FILE is set to hold that path. If the commit fails, Git will remove the index.lock file and everything will be as if you had not run git commit at all. If the commit succeeds, Git will rename index.lock to index, releasing the lock and updating the (standard, regular) index all in one motion.

Finally, there's the third mode, which you get when you run git commit --only file.ext for instance. Here, there are now three index files:

  • $GIT_DIR/index: The standard index, which holds what it usually does.
  • $GIT_DIR/index.lock: A copy of the standard index to which file.ext has been git add-ed.
  • $GIT_DIR/indexsuffix: A copy of the HEAD commit2 to which file.ext has been git add-ed.

The environment variable GIT_INDEX_PATH points to this third index. If the commit succeeds, Git will rename the index.lock file to index, so that it become the index. If the commit fails, Git will remove the index.lock file, so that the index goes back to the state it had before you started. (And in either case, Git removes the third index, which has now served its purpose.)

Note that from a pre-commit hook, you can detect whether git commit is a standard commit (GIT_INDEX_FILE is unset or set to $GIT_DIR/index) or one of the two special modes. In standard mode, if you want to update the index, you can do so as usual. In the two special modes, you can use git add to modify the file that GIT_INDEX_FILE names, which will modify what goes into the commit; and if you're in the --include style commit, this also modifies what will become the standard index on success. But if you're in the --only mode, modifying the proposed commit doesn't affect the standard index, nor the index.lock that will become the standard index.

To consider a concrete example, suppose the user did:

git add file1 file2

so that the standard index matches HEAD except for file1 and file2. Then the user runs:

git commit --only file3

so that the proposed commit is a copy of HEAD with file3 added, and, if this commit succeeds, Git will replace the standard index with one in which file1, file2, and file3 are all added (but since file3 will match the new HEAD commit, only files 1 and 2 will be modified in the new index).

Now suppose your commit hook runs git add file4 and the process as a whole succeeds (the new commit is made successfully). The git add step will copy the work-tree version of file4 into the temporary index, so that the commit will have both file3 and file4 updated. Then Git will rename the index.lock file, so that file3 will match the new HEAD commit. But file4 in the index.lock was never updated, so it won't match the HEAD commit. It will appear to the user that somehow, file4 got reverted! A git status will show a pending change to it, staged for commit, and git diff --cached will show that the difference between HEAD and the index is that file4 has been changed back to match the file4 in HEAD~1.

You could have your pre-commit hook test for this mode and refuse to git add files when in this mode, to avoid the problem. (Or, you could even sneakily add file4 to index.lock, with a second git add command!) But it's generally better to have your hook just reject the commit, with advice to the user to do any git adds themselves, so that you don't have to know all of these implementation secrets about git commit in the first place.


1The index holds some extra information as well: cache data about the work-tree. That's why it's sometimes called the cache. These extra copies that I describe here are made by copying the original index, so the extra copies also have the same cache data, except if and when they get updated via git add.

2It's not specified whether Git makes this copy via the internal equivalent of:

TMP=$GIT_DIR/index<digits>
cp $GIT_DIR/index $TMP
GIT_INDEX_FILE=$TMP git reset
GIT_INDEX_FILE=$TMP git add file3

or some other means (e.g., the internal equivalent of git read-tree), but since this particular copy is always just removed at the end of the process, it doesn't matter: any cache information for the work-tree becomes irrelevant.

这篇关于在预提交钩子中调用git的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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