首页 > 解决方案 > Git pre-commit hook:使用-a标志提交时如何获取添加/修改的文件

问题描述

当我使用 git commit -a 提交我的工作时,pre-commit 钩子中的“git diff --diff-filter=ACM --name-only --cached”无法获取文件将由 git 添加。那么这种情况的正确解决方案是什么。

标签: git

解决方案


这里的问题是它git commit -a本身。 您最好的选择是不使用该-a选项。单独添加文件,然后运行git commit​​. 如果你想修复钩子,请继续阅读。

提交如何真正起作用

编写 Git-hooks(好吧,至少其中一些钩子)的人需要了解 Git 从索引构建提交的事实。但是这种说法——Git 从索引构建提交——有点谎言,或者至少是不完整的。Git 从索引构建提交

如果您在git commit 使用任何三个特定选项的情况下运行,则只有一个索引。该索引就是索引,因此任何假设 Git 正在使用索引工作的人都会得到他们期望的行为,并且完全幼稚的预提交钩子表现良好:有时甚至不需要意识到 Git 将提交索引中的内容,而不是用户工作树中的内容。但是有三个提交选项可以改变这种行为

  • git commit -a:这很像git add -u && git commit,除了为了确保如果提交失败(被预提交钩子拒绝,或被用户中止)git add -u没有效果,Git 必须创建一个临时索引

  • git commit --include paths:这类似于git commit -a除了添加的文件不是由指定路径找到的文件git add -u而是指定路径之外。

  • git commit --only paths: 这个是最坏的情况。请注意,使用 noor和using 具有相同的效果。对于这种特殊情况,Git 必须创建的不仅仅是一个而是两个临时索引文件。git commit paths--include--only--only

这一切都源于索引的基本思想。Git 的索引始终保持1提议的下一次提交。也就是说,索引中的内容是应该包含在下一次提交中的文件。当您git commit不带任何选项运行时,您是在要求 Git 提交提议的下一次提交。所以索引中有正确的东西。

但是当你运行时git commit -agit commit --include-i简称)或git commit --only-o简称),你是在说:接受当前提议的下一次提交,对其进行一些更改,然后尝试提交。如果此操作成功,则新索引(添加了额外更改的索引)应该是更新后的索引。但是如果这个动作失败了,Git 想把索引放回原来的样子,不做任何改变。

为此,Git保持原始索引文件不变,并创建一两个新的索引文件。2 如果您使用git commit -aor git commit -i,我们需要一个额外的索引:Git 将主索引复制到临时索引,然后使用git add或内部等效项来更新临时索引。这个临时索引被命名并且这个文件用于防止在这个index.lock命令运行时运行额外的命令,所以即使是没有选项的普通文件也会创建一个文件:只是使用普通提交,文件内容将与文件内容匹配。git commit git commitgit commitindex.lockindex.lockindex

因此,对于git commitor git commit -aor git commit -i,可以仅将index.lock文件用作“the”索引,并从中获取正确的内容。当然,如果您要在编写的预提交挂钩中执行此操作,则首先需要弄清楚 Git 是否使用标准索引:如脚注 2 所示,添加的工作树使用不同的标准索引,所以它有不同的标准index.lock


1这不太正确,因为有时索引会扩展以保存非“阶段零”的条目。在冲突合并期间就是这种情况。(这里的“Merge”也包括cherry-pick 和revert:任何调用Git 内部合并引擎的东西。)然而,即使在这个扩​​展操作期间,索引仍然保留提议的下一次提交。只是索引的某些部分无法提交(需要解析),而这些部分妨碍了提交。解决冲突的条目会删除非零阶段条目,或者将它们替换为单个已解决的阶段零条目,或者如果文件根本不应该提交,则仅删除它们。

索引内容通过 : 可见git ls-files --stage:有一个完整的带有正斜杠的路径名,例如src/somefile.ext,一个模式——普通文件的一个100644或一个100755;另外两种模式是为符号链接和 gitlinks 保留的——以及一个哈希 ID。还有一个阶段号,它必须为零才能使索引可提交。任何阶段 1、2 或 3 条目都表明存在合并冲突,通过读取该插槽可以使用冲突文件:请参阅git checkout-index命令

2索引是,或者大部分是,只是一个普通文件:普通文件是.git/index. 在由 产生的二级工作树中git worktree add,通常的文件位于.git顶级目录的子目录中。但是,您可以使用环境变量 用您自己的临时索引覆盖此索引GIT_INDEX_FILE。各种 Git shell 脚本都使用这种技术。例如,当git stash是一个 shell 脚本时,它会这样做。当然,git commit使用相同的概念来创建这些其他额外的临时索引文件。


git commit --only是最难的情况

对于git commit --only,两个索引文件是不够的。我们将需要三个这样的文件。这就是为什么。的功能git commit --only是:

  1. 将当前提交( )读HEAD入临时索引。
  2. 更新该临时索引以添加指定的文件。
  3. 尝试将此索引转换为新的提交。

第三步有一个成功案例和一个失败案例。失败案例比较简单,我们先来看一下:

  • 如果失败,Git 应该返回当前提议的下一个提交作为提议的下一个提交。所以这意味着我们需要保留现有的索引。

  • 然而,在成功时,Git 应该提出一个新的建议下一个提交。这个新提议的下一个提交应该包括当前提议的下一个提交(即当前索引),就像由git add命名文件更新一样。

为了为步骤 3 的成功做好准备,步骤 1 和 2 应改为:

  1. 准备两个临时索引文件:通过复制HEAD到索引文件中设置索引A,通过复制现有索引文件设置索引B。
  2. 通过 -ing 命名文件更新索引 A,并通过-inggit add命名文件更新索引 B。git add

现在简化了第 3 步:

  1. 使用索引 A 进行提交。如果成功,将标准索引替换为索引 B。如果失败,删除两个临时索引文件。

Git 如何使用锁文件

Git 有一堆代码路径想要对单个文件进行某种原子更改3 这就是这些index.lock东西的来源。在 POSIX 系统上,没有特别好的方法可以为特定事务锁定文件,但我们可以通过各种方式对其进行近似。

一种方法非常简单:使用原子文件创建O_CREAT|O_EXCLopen系统调用中)确保只有一个进程可以创建名称以.lock. 例如,如果我们想锁定名为 的文件index,我们会原子地创建一个名为 的文件index.lock。如果创建成功,我们现在有了锁,可以将现有index文件复制到新index.lock文件,对文件进行任何必要的更改,然后将它们写出来。

我们现在可以:

  • 使用系统调用以原子方式更新索引文件,释放锁定文件:要么用当前文件完全替换旧文件并成功,在此过程中删除文件,否则将失败并保持两者不受干扰。(如果失败,我们将继续中止交易;见下文。)renamerename("index.lock", "index")indexindex.lockindex.lockindexindex.lock

  • 或者,我们可以通过简单地删除锁定文件 ( )来释放文件上的锁定,故意中止我们的事务。unlink("index.lock")现有index文件保持不受干扰。

请注意此技术如何无缝地完成git commitgit commit -a/ git commit -i。这两个操作之间的主要区别完全由我们输入的内容index.lock控制。对于一个普通的git commit,两者都index包含index.lock相同的内容。对于git commit -aor git commit -iindex包含旧内容,并index.lock包含新更新的内容。

我们可以创建锁定文件,在适当的情况下更新它,尝试提交,然后通过重命名完成事务,或者通过取消链接锁定文件来回滚事务。这一切都非常简单明了。4

困难的情况是git commit -o:该--only选项需要两个临时索引文件。我们不理会index,使用一组内容创建index.lock——索引 B,因为它是我们希望通过重命名操作获得的内容——并在提交过程中创建我们的第三个索引,索引 A。我们读HEAD入索引 A,更新索引文件 A 和 B,尝试使用索引 A 提交,删除索引 A,然后使用索引 B 完成事务,或者像以前一样回滚。这不那么简单,但很明显它是有效的。


3我在此处链接到有关数据库原子性的 Wikipedia 页面,因为这是 Git 试图在这里实现的概念:原子事务。真正的数据库软件可能会让 Git 受益;它做的东西有点粗糙。然而,真正的数据库软件是(a)硬(b)慢。Git 在这里尝试了一种“吃蛋糕吃”的方式。它基本上是成功的:这里有真正的权衡,Git 很好地管理了其中的大部分。不过,他们现在在各种情况下都崩溃了,这里的工作正在进行中。

4这里的“Easy”意味着只有几十行C代码。不过,如果 Git 是用更高级的语言编写的,那确实会相对容易。


编写一个处理所有这些情况的预提交钩子

在这里,你只是有麻烦了。对于这种git commit --only情况,将要提交的内容在索引 A 中。但是您可以知道其路径的两个文件是原始索引($GIT_INDEX_FILE如果已设置,或者.git/index或适当的工作树索引)和索引 B(与以前相同的文件加上.lock后缀)。

可以确定是否至少有两个不同的索引文件。如果是这种情况,我们正在做git commit -a, git commit -i, 或git commit -o。这将告诉您您无法可靠地处理此问题,您可以让您的预提交挂钩中止并告诉用户不要这样做。

由于这些都没有记录,因此没有官方的方法可以做到这一点,但是一些现有的预提交钩子使用了这种技术:

if [ $GIT_INDEX_FILE != ".git/index" ]; then
    echo "Error: non-default index file is being used (GIT_INDEX_FILE is set)." >&2
    ...
    exit 1
fi

不过,这会产生令人讨厌的副作用,即拒绝来自添加的工作树的提交。要修复它,如果您的 Git 足够新,可以git rev-parse --git-path将任何硬编码.git/index字符串替换为:

git rev-parse --git-path index

正如您所观察到的,某些版本的 Git 在不需要index.lock时不会创建。这是依赖未记录行为的问题:它可能在您现在安装的 Git 版本中工作,然后在您升级到较新版本的 Git 时中断。


推荐阅读