在预提交挂钩中调用 git

Calling git in pre-commit hook

我得到 运行 宁我的 git 预提交挂钩的奇怪结果,例如当我这样做时 git diff --name-only 在终端中它似乎给出不同的结果 当它在 .git/hooks/pre-commit

中执行时

所以我的问题是:

  1. 我可以在 git 钩子中调用 git 吗?
  2. 如果 1. 没问题:如果我做 git 提交,那么什么时候调用预提交挂钩 -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 diff --cached --name-only 列出即将提交的文件。

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

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

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

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

  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?

这很复杂。

git commit内部使用了三个"modes"。 (对此没有任何承诺,但这就是多年来实施的方式,所以这三种模式的东西看起来相当稳定。)模式是:

  • git commit 没有 -a--include--only、and/or 任何命令行指定的文件名。这是默认或正常模式。底层实现细节就不展示了。

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

    • 这样的提交 --include,或
    • --only 这样的提交。


    至此,底层实现就透彻了。

这里的底层实现细节涉及 Git 调用的东西,不同的是 indexstaging area,和(现在很少)缓存,它通常作为名为 $GIT_DIR/index 的文件实现(其中 $GIT_DIR 是关于第 1 点的注释中的环境变量)。通常,只有其中之一:the 索引。它包含您打算提交的内容。1 当您 运行 git commit, Git 将打包 中的任何内容 作为下一次提交的索引。

但是,在ofgit commit的操作过程中,最多可能有三个个索引文件。对于正常的 git commit 只有一个索引,您的预提交挂钩可以使用它甚至可以更新它。 (我建议不要更新它,原因我们稍后会看到。)

但是,如果您执行 git commit -agit commit --include file.ext,现在有 两个 索引文件。有准备提交的内容——常规索引——一个extra索引,它是原始索引加上执行git addfile.ext 或所有文件上(相当于 git add -u)。所以现在有两个个索引文件。

在此模式下,Git 将常规索引文件 保留为 常规索引文件。这个文件像往常一样在 $GIT_DIR/index 中。 second 索引文件,以及额外添加的内容,位于 $GIT_DIR/index.lock 中,环境变量 GIT_INDEX_FILE 设置为保存该路径。如果提交 失败 ,Git 将删除 index.lock 文件,一切都会像你根本没有 运行 git commit 一样.如果提交 成功 ,Git 会将 index.lock 重命名为 index,释放锁并更新 the (标准,常规)索引全部在一个动作中。

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

  • $GIT_DIR/index: 标准索引,保持它通常做的事情。
  • $GIT_DIR/index.lockfile.ext 已被 git add 编辑的标准索引的副本。
  • $GIT_DIR/index<em>后缀</em>HEAD提交的副本2 file.ext 已被 git add 编辑。

环境变量GIT_INDEX_PATH指向这第三个索引。如果提交成功,Git会将index.lock文件重命名为index,使其成为索引。如果提交失败,Git 将删除 index.lock 文件,以便索引恢复到开始之前的状态。 (在任何一种情况下,Git 都会删除第三个索引,它现在已经达到了它的目的。)

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

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

git add file1 file2

以便标准索引匹配 HEAD 除了 file1file2。那么用户 运行s:

git commit --only file3

因此建议的提交是 HEAD 的副本,添加了 file3,如果此提交成功,Git 将将标准索引替换为 file1file2file3 都已添加的索引(但由于 file3 将匹配新的 HEAD 提交,因此只有文件1 和 2 将在新索引中 修改

现在假设您的提交挂钩 运行s 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 文件,以避免出现问题。 (或者,你甚至可以偷偷地添加 file4index.lock,使用第二个 git add 命令!)但是通常最好让你的钩子拒绝提交,并建议用户自己做任何 git adds,这样你就不必首先知道所有这些实施秘密 about 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 的内部等价物),但是由于这个特定的副本总是在进程结束时被删除,所以这无关紧要:任何缓存信息工作树变得无关紧要。