在预提交钩子中调用git [英] Calling git in pre-commit hook
问题描述
例如,当我执行git pre-commit钩子时,我得到奇怪的结果 git diff --name-only在终端中似乎给出了不同的结果 在.git/hooks/pre-commit
中执行时所以我的问题是:
- 我可以在git hooks中调用git吗?
- 如果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
- 我可以在git hooks中调用git吗?
是的,但是您必须谨慎行事,因为环境中设置了许多东西,并且您正在做的事情正在完成中:
-
GIT_DIR
设置为Git目录的路径.
可以将 -
GIT_WORKTREE
设置为工作树的路径(来自git --work-tree
). - 其他Git变量,例如
GIT_NO_REPLACE_OBJECTS
,也可以从命令行设置.
(如果您继续使用当前存储库,则应保留这些设置,但如果使用不同存储库,则应清除它们.)
- 如果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 -a
或git 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
模式,则修改建议的提交不会会影响标准index
,也会的index.lock
成为标准索引.
考虑一个具体的例子,假设用户这样做了:
git add file1 file2
,以使标准索引与HEAD
匹配,但file1
和file2
除外.然后用户运行:
git commit --only file3
,因此建议的提交是HEAD
的副本,其中添加了file3
,和,如果该提交成功,则Git将用其中file1
,file2
和file3
都被添加了(但由于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:
- Am I allowed to call git inside git hooks?
- 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
- 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 (fromgit --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.)
- 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.- such a commit with
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 whichfile.ext
has beengit add
-ed.$GIT_DIR/indexsuffix
: A copy of theHEAD
commit2 to whichfile.ext
has beengit 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 add
s 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屋!