Git 预提交挂钩 + docker = 不同的 git 状态

Git pre-commit hook + docker = different git status

我想 运行 预提交 git 挂钩中的脚本。我希望该脚本从 docker 图像中 运行。此预提交挂钩的示例代码:

# pre-commit hook
#!/bin/bash
repo_root=$(git rev-parse --show-toplevel)
docker run -v ${repo_root}:${repo_root} -w ${repo_root}  <my_docker_image> <path_to_my_script.py>

my_script.py 在内部 运行s git status 确定要在预提交挂钩中处理哪些文件。

问题:当我 运行 git commit --all 时,git status 的输出在预提交挂钩中与在 docker 容器中不同。 示例:

# pre-commit hook
#!/bin/bash
git status
echo "------------------------------------"
repo_root=$(git rev-parse --show-toplevel)
docker run -v ${repo_root}:${repo_root} -w ${repo_root} <my_docker_image> git status

我希望通过 运行ning git commit --all、运行ning git status 在 docker 容器内,我可以看到所有已上演的更改。

但是,更改不会在 docker 容器内暂存。我之前写的代码打印如下:

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   tools/git-hooks/pre-commit
------------------------------------
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   tools/git-hooks/pre-commit

换句话说:在 docker 中,git status 没有检测到 --all 选项;更改未上演。

我错过了什么?

TL;DR:添加 -e GIT_INDEX_FILE。但是,您可能希望允许额外的 Git 变量。或者,您可以简单地禁止此类提交,或使用完全不同的机制(请参阅下面描述的最后一段)。

描述

当您使用 git commit --all 时,Git 会创建一个 临时索引 来保存暂存文件,因为它们尚未暂存到正常索引中.这个临时索引最终会成为常规索引,如果提交成功。在那之前,它不是常规索引。

现在,您的预提交脚本包含以下行:

docker run -v ${repo_root}:${repo_root} -w ${repo_root} ...

-v 选项在 运行 是 docker 映像的子进程中安装一个文件系统,并且 -w 将其设置为工作目录。但是,docker 命令会过滤子进程中的环境,去除所有未使用 --env--env-file 显式启用的可疑变量(--env 的缩写形式是 -e).

包括的top level git documentation page has a section on environment variables

GIT_INDEX_FILE

    This environment allows the specification of an alternate index file. If not specified, the default of $GIT_DIR/index is used.

git commit --all设置临时索引时,它使用这个环境变量来指示其所有子命令查看临时索引而不是$GIT_DIR/index

值得注意的是,Git也设置了$GIT_DIR,但很可能已经设置为.,所以当docker run去掉后,Git 查找从当前工作目录开始的存储库,您通过 -w 将其设置为存储库根目录——因此剥离 . 结果证明是无害的。尽管如此,您可能要考虑传递所有 Git 的环境变量。不过,这并不像盲目传递文档中列出的所有内容那么简单:例如,如果需要导出 GIT_ALTERNATE_OBJECT_DIRECTORIES 路径,则需要将该路径中的每个元素挂载到 docker 实例,使用更多 -v 选项。

幸运的是,当 GIT_INDEX_FILE 在此特定点设置时,它被设置为 .git/<em>temporary-name[ 形式的路径名=103=],因此无需挂载额外的文件系统即可将 Git 临时索引放入 运行ning 映像。这是因为这个名称将被重命名 .git/index 并且 Git 想要确保重命名可以作为原子文件系统操作完成,这要求它存在于与 .git 本身。事实上,对于 git commit --all 它只是 .git/index.lock,尽管 git commit --only 使用其他名称:--only 形式需要多个临时索引文件,而用于提交的不是一个将成为成功的正常指标。

最后——这完全独立于 Docker——请注意,可以要求 Git 提交 不匹配的暂存文件 现在工作树中有什么。例如,使用 git add -p,很容易在索引中存储一个文件版本,该文件的 HEAD 版本和 HEAD 版本之间只有 一些 的差异工作树版本。我猜您计划让 docker 环境 运行 对要提交的内容进行某种测试。没关系,但请注意 "what is to be committed" 不一定是 "what is in the work-tree"。当使用 --all 和临时索引时,它是包含要提交的内容的临时索引,临时索引刚刚从 工作树构建,因此它们将比赛;但是当 使用 --all 并使用真实索引时,或者当使用 --only 和不同的临时索引时, "what is to be committed" 不一定匹配工作树。

编写一个可以看到 "what is to be committed" 的好的预提交挂钩很棘手,但并非不可能。一种方法是将索引中的任何内容提取到与当前工作树无关的临时目录中。然后,您可以 运行 在临时目录上测试系统,不受存储库本身的干扰,也不受当前工作树的干扰。如果你在预提交脚本中这样做,你可以挂载(通过 -v-w)这个临时目录,而不用担心 运行ning any Git 命令在 docker 图像中。

旁注:#!

出于 Whosebug 发布的目的,您的示例可能会被修改,但在:

# pre-commit hook
#!/bin/bash
git status

#! 行已无用。这些行必须是脚本的 first 行。原因是内核(Linux,或派生它的 Unixes)运行s 这样的脚本的方式是检查文件的前几个字节。如果前两个字节是 #!,则第一个 行的其余部分 ——直到第一个换行符的所有内容(在某些合理的范围内)——被视为解释器的名称,也许还有那个解释器的选项。然后内核 运行 解释器,而不是脚本,从 #! 行传递选项(如果有的话),然后是脚本的名称。

如果第一行不是以 #! 开头,内核将直接拒绝 运行 文件(execve 失败并显示 ENOEXEC)。 shell 发现错误,自己检查文件,并确定文件是否是 shell 脚本...如果是,shell 本身 运行s 文件。这里的主要问题是 shell 可能会选择错误的 shell 到 运行 文件。在 #! 行中使用 正确的 解释器可以避免这种情况。