“ git commit”的异常行为。预提交挂钩修改暂存文件时

Eri*_*ler 2 git code-formatting pre-commit-hook git-commit

以我的经验,git commit -a它的行为等同于git commit .但是,最近,我创建了一个pre-commit钩子,该钩子自动格式化我的源代码,现在git commit .有一些意想不到的副作用:提交的文件最终在工作目录和目录中被修改。提交命令完成后的索引。不会发生这种情况git commit -a。我试图了解运行时在幕后git commit .发生的情况,这导致这种情况发生,并查看是否有办法在我的预提交钩子脚本中正确处理它。

预提交挂钩:

git_toplevel=$(git rev-parse --show-toplevel)

git --no-pager diff -z --cached --name-only --diff-filter=ACMRT | $git_toplevel/meta/reformat.bash -s files
git --no-pager diff -z --name-only --diff-filter=ACMRT | xargs -0 --no-run-if-empty git add
Run Code Online (Sandbox Code Playgroud)

当前使用的是git版本1.8.3.1,但在最新版本中却看到了相同的行为。

这是在行的开头添加一个简单空格的命令序列:

[]$ git status
# On branch eroller/format-clean-filter
# Your branch is ahead of 'origin/eroller/format-clean-filter' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#
no changes added to commit (use "git add" and/or "git commit -a")
Run Code Online (Sandbox Code Playgroud)

--

[]$ git diff
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 7cfb8dc..14058e3 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

-SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+ SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);
Run Code Online (Sandbox Code Playgroud)

--

[]$ git commit -m 'test' .
Run Code Online (Sandbox Code Playgroud)

--

[]$ git status
# On branch eroller/format-clean-filter
# Your branch is ahead of 'origin/eroller/format-clean-filter' by 2 commits.
#   (use "git push" to publish your local commits)
#
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#
Run Code Online (Sandbox Code Playgroud)

--

[]$ git diff
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 14058e3..7cfb8dc 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

- SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);
Run Code Online (Sandbox Code Playgroud)

--

[]$ git diff --cached
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 7cfb8dc..14058e3 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

-SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+ SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);
Run Code Online (Sandbox Code Playgroud)

更新:使用@torek的非常详尽的答案(谢谢!),如果用户尝试使用git commit .或,我决定在预提交挂钩中给出错误git commit [--only] -- <files>。这是我的预提交脚本中的检查内容:

if [[ $GIT_INDEX_FILE != *"/index" ]] && [[ $GIT_INDEX_FILE != *"/index.lock" ]] ; then
  echo "Error: pre-commit reformatting using unsupported index file ($GIT_INDEX_FILE)." >&2
  echo "       Are you using 'git commit [--only] -- <files>' to bypass staging?" >&2
  echo "       Use git commit -a or stage your files before committing using git add -- <files>" >&2
  echo "       Use '--no-verify' to bypass reformatting (not recommended)" >&2
  exit 1
fi
Run Code Online (Sandbox Code Playgroud)

tor*_*rek 5

这里的根本问题是混帐使承诺不会从工作树但从指数,这就是为什么你需要git add在第一个文件的地方,但指数是一种善意的谎言,因为有可能是更多的索引FLES比只是一种标准。(该索引也称为暂存区域缓存,具体取决于Git的哪一部分正在执行调用。)

指数,我指的是在一个标准之一,是在文件.git命名index。如果检查.git目录,则会找到这样的文件。过去,实际上只有一个文件。在现代的Git(2.5或更高版本)中,由于添加了工作树,因此画面变得更加模糊:实际上,每个工作树.git/index只有一个索引文件,因此这仅仅是工作树索引。这里有一个辅助每个工作树的索引-但这并不是我要表达的意思,这里只是一个例子,它显示了一个单一索引的假设已经在边缘发生了变化。诚然,您使用的是Git 1.8.3.1(确实很老),但它也比简单的白色谎言“一个索引”设置更为复杂。

使用时git commit -a,Git会创建一个新的额外索引。当您使用时git commit .,您要进行调用git commit --only .有关详细信息请参见文档),Git会创建两个新的额外索引(索引?)。

Git的所有部分都能够重定向Git 的其余部分,以使用不同的非标准索引,以及git commit使用此功能的各种选项。请注意,这git commit -a等于git commit --include后跟需要添加的任何文件的名称。真正棘手的情况是您正在使用的情况git commit --only

一旦开始增加索引文件,事情就会变得混乱!

请记住,索引本质上是建议的下一个commit。如果只有一个索引(对于这个工作树,如果我们正在谈论Git 2.5或更高版本),那么只有一个提议的下一次提交。不太困难,我们只需要考虑每个文件有三个副本。让我们选择一个文件,例如README.md

  • HEAD:README.md是的当前提交版本README.md。您无法更改。(您可以移动HEAD自己,但是提交的副本README.md位于提交内部,如提交的哈希ID所示,并且不会更改。)

    该名称HEAD:README.md仅在Git内部有效。该名称访问该文件的冻结,经过Git验证的冻结干燥副本;此副本将永远不会改变。例如,您可以看到它git show HEAD:README.md

  • :README.mdREADME.md索引中的副本。最初是相同的,HEAD:README.md但是如果您运行git add README.md,现在可能会有所不同。

    该名称:README.md也仅在Git内部有效。该名称将访问此可替换但已Git格式(冻结干燥格式)的文件副本,该副本存储在索引中。您可以随时用替换git add

  • 最后,README.md是一个普通的(非Git化的)文件。它不在Git中!它不在索引中!它在您的工作树中,您可以使用所有普通的计算机工具在其中查看和使用它。Git确实不使用该文件,它只是覆盖它或在您签出其他提交时将其删除。除了使用git status诸如此类的方式进行检查之外,Git唯一要做的就是让您git add用来复制回索引,覆盖之前的内容(并在过程中将其冻干)。

运行git status运行git diff两秒:

  • 第一个将HEAD提交与索引进行比较,即当前提交中的内容与提议的下一个提交中的内容。任何不同在这里被列为上演提交。一切都一样,Git只是悄无声息。

  • 第二个git diff将索引与工作树进行比较,即提议的提交中包含什么,以及可以复制到索引中的内容。任何不同在这里被列为未上演承诺。同样,再次,Git悄无声息。

  • (然后有一个最后的传球来检查工作树是不是在所有的索引文件。Git会抱怨这些,说他们是未经跟踪,除非你在列出它们.gitignore。被列在.gitignore不更改索引中是否有文件的副本,只是更改Git是否发牢骚。)

当您运行git commit,Git的封装了无论是在指数和用途,为了使新的承诺...... 除非你使用--only--include-a

指示出无足轻重

使用git commit --only,Git可以创建三个索引文件:

  • 一是标准。一开始没有被改动。那是正常的.git/index
  • 一个是该文件的副本,并附有--only文件git add。这是在.git/index.lock一些点。 也许总是在这里!如果是这样,那将提供一种处理我在下面概述的情况的方法。但是没有文档可以保证这一点。
  • 第三是新的,首先提取HEAD,然后git add--only文件放入其中。

如果运行之前没有git add任何操作,则第一个索引文件和第三个索引文件会匹配,因为将文件添加到常规索引的效果与从中创建新的临时索引并将文件添加到其中的效果相同。但是,否则所有三个文件可能都不同!git commit -a--onlyHEAD--only

然后,Git从第三个索引进行新的提交。如果新的提交成功,则Git用第二个索引替换常规索引(此替换通过rename系统调用发生)。否则,Git返回正常索引。(请注意,工作树什么都没有发生。)

如果使用git commit --includegit commit -a,则Git仅增加一个索引,因此您具有:

  • 中的标准索引.git/index,以及到目前为止添加的内容;和
  • 临时文件中的一个额外索引:该索引从标准索引的副本开始,但是随后Git将列出的文件或其他修改的文件添加到该索引中。

然后,Git启动提交过程。如果一切顺利,完成Git后,Git重命名临时索引,使其成为标准索引。如果情况不佳,Git会删除临时索引,而标准索引保持不变。同样,工作树没有任何反应。

介绍预提交挂钩

在准备任何额外的索引文件之后,Git会运行您的预提交钩子。特殊的环境变量$GIT_INDEX_FILE为Git用来进行新提交的索引命名。因此,存在三种情况,其中两种情况还不错,而其中一种则很糟糕:

  • 您正在执行常规提交。GIT_INDEX_FILE命名普通索引,一切正常。
  • 您正在执行git commit --includegit commit -aGIT_INDEX_FILE命名第二个索引;没有第三索引;如果提交完成,Git将重命名第二个索引。
  • 您正在执行git commit --onlyGIT_INDEX_FILE命名第三个索引。有没有简单的方法来找到第二个指标,一个将到位后提交,如果提交成功!

如果您选择对存储在索引中的文件进行更改,您的工作就是将它们更改为Git将用于提交的索引。为此,您可以根据需要使用git add,因为这会将文件从工作树复制到中的索引$GIT_INDEX_FILE

但是,第一个问题是您不能查看工作树中的文件。他们无关紧要!它们可能包含与索引中完全不同的内容。期间尤其如此git commit --only

第二个更大的问题是,如果您更新了正在使用的第三个索引,git commit --only则还应该更新正在使用的第二个索引git commit --only。这部分很棘手,因为除了假定位于in之外,没有其他简单的方法可以找到它.git/index.lock。尽管这可能会起作用,但在这里我不建议这样做。

我真的对此没有任何建议-您发现的任何偷偷摸摸的方法都可能会破坏,因为处理该第三个索引的代码(当前的2.21 ish Git称为“假索引”)在1.8和现代Git之间已发生了很大变化。通常的最佳实践建议是根本不对 Git挂钩进行任何特殊格式化。取而代之的是,已经有了一份Git挂钩只是检查是否该文件的索引拷贝格式正确:如果是这样,请继续提交,如果没有,取消提交。其余的留给用户。

另一种选择

我看到并使用过的另一种方法是检查的实际设置$GIT_INDEX_FILE。如果设置为.git/index,则用户正在使用git commit而没有任何特殊设置。相同的预提交钩子(调用clang-format和autopep8)的另一个技巧是比较索引和工作树中要格式化的文件,如果不匹配则拒绝运行。