git - Git pre-commit hook:使用-a标志提交时如何获取添加/修改的文件
问题描述
当我使用 git commit -a 提交我的工作时,pre-commit 钩子中的“git diff --diff-filter=ACM --name-only --cached”无法获取文件将由 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 -a
,git commit --include
(-i
简称)或git commit --only
(-o
简称),你是在说:接受当前提议的下一次提交,对其进行一些更改,然后尝试提交。如果此操作成功,则新索引(添加了额外更改的索引)应该是更新后的索引。但是如果这个动作失败了,Git 想把索引放回原来的样子,不做任何改变。
为此,Git保持原始索引文件不变,并创建一两个新的索引文件。2 如果您使用git commit -a
or git commit -i
,我们需要一个额外的索引:Git 将主索引复制到临时索引,然后使用git add
或内部等效项来更新临时索引。这个临时索引被命名并且这个文件用于防止在这个index.lock
命令运行时运行额外的命令,所以即使是没有选项的普通文件也会创建一个文件:只是使用普通提交,文件内容将与文件内容匹配。git commit
git commit
git commit
index.lock
index.lock
index
因此,对于git commit
or git commit -a
or 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
是:
- 将当前提交( )读
HEAD
入临时索引。 - 更新该临时索引以添加指定的文件。
- 尝试将此索引转换为新的提交。
第三步有一个成功案例和一个失败案例。失败案例比较简单,我们先来看一下:
如果失败,Git 应该返回当前提议的下一个提交作为提议的下一个提交。所以这意味着我们需要保留现有的索引。
然而,在成功时,Git 应该提出一个新的建议下一个提交。这个新提议的下一个提交应该包括当前提议的下一个提交(即当前索引),就像由
git add
命名文件更新一样。
为了为步骤 3 的成功做好准备,步骤 1 和 2 应改为:
- 准备两个临时索引文件:通过复制
HEAD
到索引文件中设置索引A,通过复制现有索引文件设置索引B。 - 通过 -ing 命名文件更新索引 A,并通过-ing
git add
命名文件更新索引 B。git add
现在简化了第 3 步:
- 使用索引 A 进行提交。如果成功,将标准索引替换为索引 B。如果失败,删除两个临时索引文件。
Git 如何使用锁文件
Git 有一堆代码路径想要对单个文件进行某种原子更改。3 这就是这些index.lock
东西的来源。在 POSIX 系统上,没有特别好的方法可以为特定事务锁定文件,但我们可以通过各种方式对其进行近似。
一种方法非常简单:使用原子文件创建(O_CREAT|O_EXCL
在open
系统调用中)确保只有一个进程可以创建名称以.lock
. 例如,如果我们想锁定名为 的文件index
,我们会原子地创建一个名为 的文件index.lock
。如果创建成功,我们现在有了锁,可以将现有index
文件复制到新index.lock
文件,对文件进行任何必要的更改,然后将它们写出来。
我们现在可以:
使用系统调用以原子方式更新索引文件,释放锁定文件:要么用当前文件完全替换旧文件并成功,在此过程中删除文件,否则将失败并保持两者不受干扰。(如果失败,我们将继续中止交易;见下文。)
rename
rename("index.lock", "index")
index
index.lock
index.lock
index
index.lock
或者,我们可以通过简单地删除锁定文件 ( )来释放文件上的锁定,故意中止我们的事务。
unlink("index.lock")
现有index
文件保持不受干扰。
请注意此技术如何无缝地完成git commit
和git commit -a
/ git commit -i
。这两个操作之间的主要区别完全由我们输入的内容index.lock
控制。对于一个普通的git commit
,两者都index
包含index.lock
相同的内容。对于git commit -a
or git commit -i
,index
包含旧内容,并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 时中断。
推荐阅读
- python - 如何提高 matplotlib 的精度?
- jquery - 用jquery ajax替换html不会改变隐藏字段值
- android - ViewModel 单元测试
- powershell - 使用 powershell 拆分列表
- testing - Spring Batch - JUnit 测试时回滚
- python - 在 Python 中使用 zstandard 压缩文件
- python - 按特定列表分组(?)数据帧
- java - 删除 Elasticsearch 中小于某个值的所有元素
- javascript - 试图返回具有最高值对象的父数组的索引
- reactjs - 将两个 ag-grids 数据导出到单个 excel 文件的任何可能方式?