这里的问题是git commit -a
本身。你最好的选择是不要使用-a
选项。单独添加文件,然后运行git commit
.如果您想修复挂钩,请继续阅读。
提交是如何运作的
编写 Git 钩子(嗯,至少是其中一些钩子)的人需要意识到 Git 从索引构建提交的事实。但是这个说法——Git 构建提交来自the索引——有点谎言,或者至少是不完整的。 Git 构建提交来自an index.
如果你跑git commit
without使用三个特定选项中的任何一个,都只有一个索引。该指数是the索引,所以任何假设 Git 正在工作的人the索引得到了他们期望的行为,并且完全幼稚的预提交挂钩表现良好:有时甚至不需要知道 Git 将提交索引中的内容,而不是用户工作树中的内容。但有三个提交选项可以更改此行为:
-
git commit -a
:这很像git add -u && git commit
,除了为了确保git add -u
没有效果if提交fails(被预提交钩子拒绝,或者被用户中止),Git 必须创建一个临时索引.
-
git commit --include paths
: 这类似于git commit -a
除了添加的文件不是由git add -u
而是指定的路径。
-
git commit --only paths
: 这是最糟糕的情况。注意git commit paths
,两者都没有--include
nor --only
,与使用具有相同的效果--only
。对于这种特殊情况,Git 不仅必须创建一个,而且还必须创建一个two临时索引文件。
All of this follows from the basic idea of the index. Git's index holds, at all times,1 your proposed next commit. That is, what's in the index is the set of files that should be included in the next commit. When you run git commit
with no options, you're asking Git to commit the proposed next commit. So the index has the right stuff in it.
但当你跑步时git commit -a
, git commit --include
(-i
简称),或git commit --only
(-o
简而言之),你是在说:采取当前建议的下一次提交,对其进行一些更改,然后尝试提交。如果这个动作succeeds,新索引(添加了额外更改的索引)应该是更新后的索引。但如果这个动作fails, Git 希望将索引恢复到原来的样子,使用no所做的改变。
To achieve this, Git keeps the original index file intact and creates one or two new index files.2 If you're using git commit -a
or git commit -i
, we need one extra index: Git copies the main index to the temporary index, then uses git add
, or the internal equivalent, to update the temporary index. This temporary index is named index.lock
and this file is used to prevent an additional git commit
command from running while this git commit
command runs, so even a plain git commit
with no options creates an index.lock
file: it's just that with the plain commit, the index.lock
file contents will match the index
file contents.
So, for git commit
or git commit -a
or git commit -i
,可以只使用index.lock
文件作为“the”索引,并从中获取正确的内容。当然,如果您要在编写的预提交挂钩中执行此操作,您需要弄清楚 Git 是否正在使用standard首先是索引:如脚注2所示,添加的工作树使用不同的标准索引,因此它具有不同的标准index.lock
.
1This isn't quite right, because sometimes the index gets expanded to hold entries that are not "stage zero". This is the case during conflicted merges. ("Merge" here includes cherry-pick and revert as well: anything that invokes Git's internal merge engine.) However, even during this expanded operation, the index does still hold the proposed next commit. It's just that there are parts of the index that can't be committed—that require resolving—and these get in the way of doing the commit. Resolving the conflicted entries removes the nonzero stage entries, either replacing them with a single resolved stage-zero entry, or just removing them if the file shouldn't be committed after all.
索引内容可见git ls-files --stage
:有一个包含正斜杠的路径名,例如src/somefile.ext
, a mode—one of 100644
or 100755
对于普通文件;另外两种模式是为符号链接和 gitlinks 以及哈希 ID 保留的。还有一个阶段数,它必须为零才能使索引可提交。任何阶段 1、2 或 3 条目都表明存在合并冲突,通过读取该槽即可获取冲突文件:请参阅the git checkout-index command.
2The index is, or mostly-is, just an ordinary file: the usual file is .git/index
. In secondary work-trees resulting from git worktree add
, the usual file is in a subdirectory within the .git
top level directory. You can, however, override this index with your own temporary index, using the environment variable GIT_INDEX_FILE
. Various Git shell scripts use this technique. For instance, when git stash
was a shell script, it did this. And of course, git commit
uses this same concept to create these other extra temporary index files.
git commit --only
是最难的情况
For git commit --only
,两个索引文件是不够的。我们会需要three这样的文件。原因如下。的功能git commit --only
is to:
- 阅读当前提交 (
HEAD
) 到临时索引中。
- 更新该临时索引以添加指定的文件。
- 尝试将此索引转换为新的提交。
步骤3有成功案例和失败案例。失败案例比较简单,我们先看一下:
为了为步骤 3 的成功做好准备,步骤 1 和 2 应改为这样:
- 准备两个临时索引文件:通过复制建立索引A
HEAD
到索引文件中,并通过复制现有索引文件来设置索引 B。
- 更新索引 A
git add
-ing 指定的文件,并更新索引 Bgit add
-ing 指定的文件。
步骤 3 现在已简化:
- 使用索引 A 进行提交。如果成功,则用索引 B 替换标准索引。如果失败,则删除两个临时索引文件。
Git 如何使用锁定文件
Git has a bunch of code paths that would like to make some sort of atomic change to a single file.3 That's where this index.lock
stuff comes from. On a POSIX system, there's no particularly good way to lock a file for a particular transaction, but we can approximate it in various ways.
一种方法非常简单:使用原子文件创建 (O_CREAT|O_EXCL
in open
系统调用)以确保只有一个进程可以create名称结尾为的文件.lock
。例如,如果我们想锁定名为index
,我们自动创建一个名为index.lock
。如果创建成功,我们现在就拥有了锁,并且可以复制现有的index
文件到新的index.lock
文件,对文件进行任何必要的更改,然后将其写出。
我们现在可以:
-
原子地update索引文件,释放锁定文件,带有rename
系统调用:rename("index.lock", "index")
将完全取代旧的index
文件与当前index.lock
文件并成功,删除index.lock
文件在此过程中,否则将失败并留下两者index
and index.lock
不受打扰。 (如果失败,我们将继续中止事务;见下文。)
-
或者,我们可以release文件上的锁定,通过简单地删除锁定文件来故意中止我们的事务(unlink("index.lock")
)。现有的index
文件保持不变。
请注意该技术如何无缝地完成这两个任务git commit
and git commit -a
/ git commit -i
。这两个操作之间的关键区别完全由我们放入什么内容index.lock
。对于一个普通的git commit
, both index
and index.lock
包含相同内容. For git commit -a
or git commit -i
, index
包含旧内容,并且index.lock
包含新的更新内容。
We can create the lock file, update it if appropriate, attempt the commit, and then either finish the transaction by renaming, or roll back the transaction by unlinking the lock file. This is all very straightforward and easy.4
硬壳是git commit -o
: the --only
选项需要two临时索引文件。我们离开index
独自一人,创造index.lock
使用一组内容(索引 B,因为它是我们希望通过重命名操作实现的内容)并创建我们的内容third索引,索引 A,用于提交过程的持续时间。我们读HEAD
进入索引 A,更新索引文件 A 和 B,尝试使用索引 A 提交,remove索引 A,然后要么完成索引 B 的事务,要么像以前一样回滚。这不太简单,但显然它确实有效。
3I linked here to the Wikipedia page on atomicity in databases, as that's the concept Git is attempting to achieve here: an atomic transaction. Real database software might benefit Git here; the stuff it does is kind of crude. However, real database software is (a) hard and (b) slow. Git attempts a sort of have-your-cake-and-eat-it-too here. It mostly succeeds: there are real tradeoffs here and Git manages most of them pretty well. They're breaking down now in various cases, though, and work here is ongoing.
4"Easy" here means only multiple dozens of lines of C code. If Git were written in a higher level language, it really would be relatively easy, though.
编写一个处理所有这些情况的预提交钩子
在这里,你简直有麻烦了。随着git commit --only
在这种情况下,将要提交的内容位于索引 A 中。但是您可以获取其路径的两个文件know是原始索引($GIT_INDEX_FILE
,如果已设置,或者.git/index
或适当的工作树索引)和索引 B(与之前相同的文件加上.lock
后缀)。
You can判断是否存在至少两个不同的索引文件。如果是这样的话,我们正在做git commit -a
, git commit -i
, or git commit -o
。这将告诉您无法可靠地处理此问题,并且您可以让预提交挂钩中止并告诉用户不要这样做。
由于这些都没有记录在案,因此没有official方法来做到这一点,但一些现有的预提交挂钩使用这种技术:
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 时中断。