git commit -a
本身。最好不要使用-a
选项。分别添加文件,然后运行git commit
。如果您想修复钩子,请继续阅读。git commit
而没有使用三个特定选项之一,则只有一个索引。该索引是唯一的索引,因此任何假设Git正在使用该索引的人都会得到他们期望的行为,并且完全幼稚的预提交挂钩表现良好:有时甚至不必意识到Git将提交索引中的内容,而不是用户的工作树中的内容。但是,有三个提交选项会更改此行为:
git commit -a
:这个命令与git add -u && git commit
很像,但是为了确保如果提交被拒绝(被pre-commit hook拒绝或者被用户中止)时git add -u
不会生效,Git必须创建一个临时索引。
git commit --include paths
:这个命令与git commit -a
类似,但是添加的文件不是由git add -u
找到的,而是指定的路径。
git commit --only paths
:这是最糟糕的情况。需要注意的是,没有--include
或--only
参数的git commit paths
命令与使用--only
参数的命令效果相同。对于这种特殊情况,Git必须创建不只一个,而是两个临时索引文件。
git commit
命令时,您正在请求Git提交下一个建议的提交。所以,索引中有正确的东西。git commit -a
、git commit --include
(简称-i
)或git commit --only
(简称-o
)命令时,您正在说:对当前建议的下一个提交进行一些更改,然后尝试提交。如果此操作成功,那么新的索引——添加了额外更改的那个——应该是更新后的索引。但是,如果此操作失败,Git会希望将索引恢复到原来的状态,没有做任何更改。git commit -a
或 git commit -i
,我们需要一个额外的索引:Git 将主索引复制到临时索引,然后使用 git add
或内部等效项来更新临时索引。此临时索引命名为 index.lock
,该文件用于在运行此 git commit
命令时防止运行其他 git commit
命令,因此即使是没有选项的普通 git commit
命令也会创建一个 index.lock
文件:只是对于普通提交,index.lock
文件内容将与 index
文件内容匹配。git commit
或git commit -a
或git commit -i
,可以只使用index.lock
文件作为“the”索引,并从中获取正确的内容。当然,如果你想在预提交钩子中实现这个功能,你需要先确定Git是否首先使用标准索引:如脚注2所示,添加的工作树使用不同的标准索引,因此它具有不同的标准index.lock
。
1这并不完全准确,因为有时索引会被扩展以容纳不是“阶段零”的条目。这种情况发生在有冲突的合并期间。 (这里的“合并”还包括樱桃拣选和还原:任何调用Git的内部合并引擎的操作。)然而,在此扩展操作期间,索引仍然保存建议的下一个提交。只是索引中有一些部分不能被提交,需要解决这些问题才能进行提交。解决冲突的条目会删除非零阶段的条目,用单个已解决的零阶段条目替换它们,或者如果文件根本不应该被提交,则将它们删除。
索引内容可以通过git ls-files --stage
命令查看:其中包含完整的路径名,例如 src/somefile.ext
,一个模式——对于普通文件是 100644
或 100755
中的一个;另外两个模式保留用于符号链接和 gitlink——以及哈希 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
) 读入临时索引。步骤 3 有一个成功案例和一个失败案例。失败案例比较简单,所以我们先看它:
在失败时,Git 应该返回到当前提议的下一次提交作为提议的下一次提交。这意味着我们需要保留现有的索引。
然而,在 成功 的情况下,Git 应该提出一个新的建议下一次提交。这个新的建议下一次提交应该由 当前建议的下一次提交 (即当前索引) 更新为指定文件的 git add
更新 组成。
HEAD
复制到索引文件中设置Index A,并通过复制现有索引文件设置Index B。git add
命名文件更新Index A,并通过git add
命名文件更新Index B。Git有一堆代码路径,想要对单个文件进行某种原子更改。3这就是这个index.lock
东西的来源。在POSIX系统上,没有特别好的方法可以锁定一个文件以进行特定的事务,但我们可以以各种方式近似它。
使用rename
系统调用原子性地更新索引文件并释放锁文件:rename("index.lock", "index")
将完全用当前的index.lock
文件替换旧的index
文件并成功,在此过程中删除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
或git commit -i
,index
包含旧的内容,而index.lock
则包含新的更新内容。困难的情况在于git commit -o
: --only
选项需要两个临时索引文件。我们保留index
,用一组内容(因为它是我们想要在重命名操作中放置的内容)创建index.lock
,并为提交过程创建第三个索引,即索引A。我们将HEAD
读入索引A,同时更新索引文件A和B,尝试使用索引A进行提交,删除索引A,然后使用索引B完成交易,或者像以前一样回滚。这种方法不太直观,但明显它是有效的。
在这里,你会遇到麻烦。对于 git commit --only
这种情况,将被提交的内容位于 Index A 中。但是,你可以知道的两个文件的路径是 Original Index(如果设置了 $GIT_INDEX_FILE
,或者是 .git/index
或相应的工作树索引),以及 Index B(与之前相同的文件加上 .lock
后缀)。
你可以确定是否存在至少两个不同的索引文件。如果是这种情况,我们正在执行 git commit -a
、git commit -i
或 git commit -o
。那么,你就知道无法可靠地处理它,可以让你的 pre-commit 钩子中止并告诉用户不要这样做。
由于没有文档记录,因此没有官方方法来处理此问题,但一些现有的 pre-commit 钩子使用了这种技术:
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 rev-parse --git-path
来替换任何硬编码的.git/index
字符串:git rev-parse --git-path index
index.lock
文件。这就是依赖未记录行为的问题:它可能在您目前安装的Git版本中运行,但在升级到新版本的Git后会出现问题。