警告:这个答案有点长,但那是因为它实际上是关于这种预提交钩子的所有陷阱。有好几种,在复杂的情况下会变得复杂。
您没有直接显示钩子,但您确实有一个指向包含该钩子的the link to the GitHub repository 的链接;这是more direct link to the hook itself)。我将引用钩子中的几行。
钩子做了一些相当粗鲁的假设,因为当你运行git commit时,每个文件至少有三个我喜欢称之为“活动副本”,而这个钩子不是复杂到足以注意到它们之间的差异。
三份文件,有时内容不同
三个副本是:
当前或HEAD 提交中的已提交副本。这个文件实际上是无法更改的——它一直被冻结——但它很重要,因为它是我们用于比较的基础。
-
索引副本。此文件可以更改。这就是您要提交的内容:如果您的 pre-commit 和 commit-message 挂钩允许提交并且一切正常,则索引中的文件副本就是将要提交的副本。因此,您可以将索引(Git 也称为 暂存区)视为提议的下一次提交。
前两个文件(冻结的HEAD 副本和索引副本)采用特殊的、仅限 Git 的压缩格式。虽然可以更改索引副本,但始终通过替换它来完成,通常使用git add 覆盖它。 git add 命令将文件压缩为仅 Git 格式,并将压缩副本(从技术上讲,是对压缩副本的引用)放入索引中。
工作树副本。此文件是您可以查看和操作的普通文件。
现在,您正在使用 Git 的 LF/CRLF 翻译,如下所示:
warning: LF will be replaced by CRLF in src/hello.c
当 Git 将文件从工作树复制到索引时(即在git add 期间)或将文件从索引复制到工作树时(例如在git checkout 期间)时,会发生实际的转换。 extract-to-work-tree 步骤将仅 LF 行结尾更改为 CRLF 行结尾; add-to-index 步骤将 CRLF 行尾更改为 LF-only 行尾。 (你可以控制它并稍微改变它,但这是通常的方案。)
git status、git add,以及现有的钩子
我们现在进入脚本,看几行:
for line in $(git status -s)
(从技术上讲这应该是git status --porcelain,但目前他们几乎都在做同样的事情:主要危险是--short 输出可能被着色,这会破坏下一个位)
if [[ $line == A* || $line == M* ]]
现在是时候考虑git status 打印的内容了。 The documentation 说,关于短格式:
...每条路径的状态显示为这些形式之一
XY PATH
XY ORIG_PATH -> PATH
ORIG_PATH 是重命名/复制的内容的来源。
ORIG_PATH 仅在条目被重命名或复制时显示。 XY 是
两个字母的状态码。 [片段] X
显示索引的状态,Y显示工作树的状态。
(除此之外:已复制目前不是git status 的可能状态。git status 调用的内部差异引擎可以设置它,但要这样做,调用者必须启用它,而git status 则没有。如果git status 获得了启用复制检测的新命令行标志或配置条目,您可以获得C status-es,但目前还不能。)
这里的关键是第一个字母,也就是脚本在这里测试的,是基于索引的状态。也就是说,它是比较HEAD 提交与索引的结果的摘要——与建议的提交。如果一个文件在索引中是新的(不会出现在HEAD 提交中),则该文件将是Added,或者如果它同时在索引和HEAD 提交中,则为Modified ,但索引副本不同于 HEAD 提交。
这里要实现的是,无论 index 副本是否匹配 head 副本,work-tree 副本都是第三个完全归档。它可能与其他两个副本中的一个或两个完全不同! 没关系,事实上,如果您使用git add -p 选择性地仅暂存工作树文件的部分,这是故意的。在我们继续耕耘时请牢记这一点。
现在让我们回到预提交挂钩脚本:
if [[ $line == *.c || $line == *.cc || $line == *.h || $line == *.cpp ]]
then
# format the file
clang-format -i -style=file $(pwd)/${line:3}
# and then add the file (so that any formatting changes get committed)
git add $(pwd)/${line:3}
fi
如果行尾的文件名——A 和M 状态文件只是一个文件名;只有R 状态文件有两个名称;但是该脚本在不检查R 状态文件时出错,因为文件可以重命名和修改—以.c、.cc 等结尾,运行clang-format。
clang-format 的 输入 是 work-tree 文件。输入几乎可以肯定应该是文件的索引副本,但它不是。因此脚本假定索引和工作树副本匹配。
运行clang-format 后,脚本然后运行git add 将(更新的)工作树文件复制回索引中。如果我们想正确地做到这一点,我们需要格式化索引副本,然后添加格式化的索引副本,这非常棘手。这可能是为什么脚本有点懒惰,但这绝对值得一提。
clang-format 编写的工作树文件可能只有 LF 行结尾(请参阅https://reviews.llvm.org/D19031)。这符合警告的文字:
警告:src/hello.c 中的 LF 将被替换为 CRLF。
这告诉您当前的工作树副本src/hello.c 仅具有 LF 行尾。 Git 被告知,当 Git 从索引复制回工作树时,Git 应该将 LF-only 结尾更改为 CRLF 结尾。
三份以上
现在事情变得复杂了。我上面提到过每个文件至少三个副本,然后描述了这三个副本所在的位置。有HEAD 提交、索引和工作树。这个描述的一个缺陷是短语索引,因为Git有时会使用一个临时索引。 一些 git commit 命令就是这种情况,但不是所有的。
git commit 的完整故事是它总是从 an 索引构建您的新提交,但不一定从 索引构建。有一个“the”索引——工作树附带的一个特殊的、可区分的索引。1 然后还有一些额外的索引文件,一些 Git 命令为各种目的而创建——例如,git stash创建一个临时索引来保存工作树,git filter-branch 在运行时会创建许多临时索引文件。不过,我们对git commit 感兴趣,git commit 有时会创建一两个自己的临时索引文件。
如果你运行git commit——根本没有额外的参数——git commit 只使用 索引文件。那是你提议的提交,它已经包含了所有文件。如果您的预提交挂钩运行 git add,它会将新文件复制到 索引中,替换索引中的旧文件,并最终 git commit 使用新文件写出新提交.如果新文件来自工作树,则大部分情况都匹配,除了 CRLF 行结尾。
但是,如果您运行 git commit --only 或 git commit --include,甚至只是运行 git commit -a,Git 就会发生变化。例如,如果您运行git commit file1.cc,则意味着 git commit --only file1.cc,除非您添加--include,在这种情况下,它意味着git commit --include file1.cc。
要执行这些操作(实际上包括纯 git commit),Git 至少会创建一个临时索引文件,尽管对于纯 git commit,这会尽可能晚地发生。一个临时索引文件被命名为index.lock(嗯,.git/index.lock,取决于您的.git 目录在哪里)。这个临时索引将是新提交的文件的真正来源。提交完成后,如果全部成功,Git 通过将 .git/index.lock 重命名为 .git/index 来释放锁。
我们可以通过一个虚拟的.git/hooks/pre-commit 看到这些操作,它只打印环境变量的名称$GIT_INDEX_FILE,然后退出并阻止提交失败:
$ cat .git/hooks/pre-commit
$ git commit
$GIT_INDEX_FILE is .git/index
$ git commit -a
$GIT_INDEX_FILE is [path]/git/.git/index.lock
$ git commit --only cache.h
$GIT_INDEX_FILE is [path]/.git/next-index-53061.lock
$ git commit --include cache.h
$GIT_INDEX_FILE is [path]/.git/index.lock
所以:
普通的git commit 使用常规索引文件。如果您的钩子运行git add,您将替换索引中的文件。当 Git 开始创建锁定文件 index.lock 时,它从 index 创建它,当 git commit 完成时(假设成功),你对索引的更改,由你的钩子,将生效。
git commit -a 或 git commit --include 的工作方式类似。锁定较早,但git add 应该更新到位的index.lock,当git commit 完成时,您的主索引应该有更新。 (我没有对此进行测试,但似乎很明显。)
但是git commit --only 会创建一个临时索引 (next-index-53061.lock) 以及锁定主索引和 git add 将 --only 文件锁定到主锁定索引。提交完成后,新提交的文件将是临时索引中的文件,包括您更新的任何内容;但是 main 索引将来自index.lock,这是更新了特定文件的旧索引。 当他们得到更新将控制该索引中的实际内容。
1当您使用git worktree add 创建额外的工作树时,额外的工作树会获得自己的单数索引,因此 索引是与 the 工作树配对:添加的工作树是具有单独索引的单独工作树。添加的工作树也有自己的HEAD,这使得事情在 Windows 上特别复杂,但我们不需要去那里。
结论
这些是提交钩子中需要注意的陷阱。所有这一切的含义是,除非你想深入了解 Git 本身的内部结构——例如检查 $GIT_INDEX_FILE 的名称,和/或将内容添加到多个索引文件中——否则通常是一个坏主意钩住修改正在进行的提交。相反,检查正在进行的提交通常更明智。如果提交是好的,让它继续。如果没有,请提醒用户运行所需的任何内容,并让提交失败。
您可以修改正在进行的提交;你只需要注意这些奇怪的情况。