【问题标题】:Unexpected behavior with "git commit ." when pre-commit hook modifies staged files“git commit”的意外行为。当预提交挂钩修改暂存文件时
【发布时间】:2019-04-08 22:58:02
【问题描述】:

根据我的经验,git commit -a 的行为与 git commit . 相同。但是,最近我创建了一个预提交挂钩,可以自动格式化我的源代码,现在 git commit . 有一些意想不到的副作用:在 commit 命令完成后,commited 最终会在工作目录和索引中修改。 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

当前使用 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")

-

[]$ 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);

-

[]$ git commit -m 'test' .

-

[]$ 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
#

-

[]$ 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);

-

[]$ 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);

更新:使用来自@torek 的非常彻底的回答(谢谢!),如果用户尝试使用git commit .git commit [--only] -- &lt;files&gt;,我决定在预提交挂钩中给出错误。这是我的预提交脚本中的检查:

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

【问题讨论】:

    标签: git code-formatting git-commit pre-commit-hook


    【解决方案1】:

    这里的根本问题是 Git 不是从工作树提交,而是从索引提交,这就是为什么你首先需要 git add 文件的原因——但是 索引是一个有点善意的谎言,因为可以比一个标准的索引文件更多。 (索引也称为暂存区缓存,具体取决于 Git 的哪个部分进行调用。)

    索引,我的意思是一个标准索引,是.git 中名为index 的文件。如果你检查你的.git 目录,你会发现这样一个文件。过去,真的只有这一个文件。在现代 Git(2.5 以上)中,由于添加了工作树,情况变得更加模糊:实际上每个工作树都有一个索引文件,因此 .git/index 只是 的索引em>main 工作树。每个工作树都有一个辅助 the 索引——但这并不是我要表达的意思,在这里,这只是一个例子,说明只有一个索引的假设已经在边缘。诚然,您使用的是 Git 1.8.3.1(它确实很旧),但它也比漂亮的简单的白谎“一个索引”设置更复杂。

    当您使用 git commit -a 时,Git 会创建一个新的额外索引。当您使用git commit . 时,您正在调用git commit --only . (see the documentation for details),Git 会生成两个 新的额外索引(索引?)。

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

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

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

    • HEAD:README.mdREADME.md 的当前提交版本。你不能改变它。 (您可以移动HEAD 本身,但README.md 的提交副本位于提交内部,由提交的哈希ID 找到,并且不会更改。)

      名称HEAD:README.md 仅在 Git 中有效。该名称访问该文件的冻结、Git 化、冻干副本;这个副本永远不会改变。例如,您可以通过git show HEAD:README.md 看到它。

    • :README.md 是索引中README.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 diffs:

    • 第一个将HEAD 提交与索引进行比较,即当前提交中的内容与建议的下一次提交中的内容。此处不同的任何内容都被列为staged for commit。任何相同的东西,Git 只是静静地什么也不说。

    • 第二个git diff 将索引与工作树进行比较,即建议提交中的内容与您可以复制到索引中的内容。此处不同的任何内容都被列为未暂存以进行提交。再一次,任何相同的东西,Git 都会悄悄地一言不发。

    • (然后最后一次检查工作树中根本不在索引中的文件。Git 会抱怨这些文件,说它们没有被跟踪,除非你在 .gitignore 中列出它们。列在.gitignore 中并不会改变索引中是否存在文件的副本,它只会改变 Git 是否抱怨。)

    当您运行 git commit 时,Git 会打包索引中的所有内容,并使用它来进行新提交...除非您使用 --only--include 或 @ 987654362@.

    指数out the wazoo

    使用git commit --only,Git 生成 三个 索引文件:

    • 一个是标准的。一开始是没动过的。这是正常的.git/index
    • 一个是那个的副本,带有--only 文件git added。它在某个时候位于.git/index.lock也许它一直都在这里!如果是这样,那将提供一种处理我在下面概述的情况的方法。但是没有任何文件可以保证这一点。
    • 第三个是一个新鲜的,首先提取HEAD,然后git add--only文件提取到它。

    如果你在运行git commit -a 之前没有git add 任何东西之前,则第一个和第三个索引文件匹配,因为将--only 文件添加到常规索引与制作相同的效果来自HEAD 的新临时索引并将--only 文件添加到其中。但否则所有三个文件都可能不同!

    Git 然后从 third 索引进行新的提交。如果新提交成功,Git 用 second 索引替换常规索引(这种替换通过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 正在使用的third 索引,您还应该更新git commit --only 使用的second 索引正在使用。这部分很棘手,因为没有简单的方法可以找到它,只能假设它在.git/index.lock 中。虽然这可能有效,但我不会在这里建议。

    我对此真的没有任何建议——任何你发现的偷偷摸摸的方法都可能会破坏,因为处理第三个索引的代码(当前 2.21-ish Git 称之为“错误索引”)在 1.8 和现代 Git。通常的最佳实践建议是不要在 Git 挂钩中进行任何特殊格式化。相反,让 Git 挂钩仅检查 是否 文件的索引副本格式正确:如果是,则继续提交,如果不是,则中止提交。剩下的交给用户。

    另一种选择

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

    【讨论】:

    • If it's set to .git/index.lock, the user is using git commit without any special settings - 使用标准git commit 我看到它设置为.git/indexgit commit -a 我看到它设置为.git/index.lockgit commit --only 是一些临时文件(例如 .git/next-index-100213.lock)
    • @EricRoller:有趣的是,我使用的钩子(在公共回购中)只是检查.git/index.lock,过去曾为我工作过简单的git commit。这可能是 1.8 和 2.x 之间的另一个区别。
    • 刚刚用 2.13.5 测试过,$GIT_INDEX_FILE 在预提交钩子的开头是.git/index。这与您所说的答案不一致:You're doing a normal commit. GIT_INDEX_FILE names the normal index, and everything is normal.
    • 是的,这是有道理的。不过,我只是在报告这个公共回购 Git 挂钩中的内容。也许我错过了一些东西:这是实际的钩子:github.com/NetSys/bess/blob/master/.hooks/pre-commit ...啊哈,它正在检查.git/index而不是.git/index.lock(那我从哪里得到index.lock?!)。将修复上述答案。
    • 由于格式化预提交挂钩也可以按预期与git commit -a 一起工作,我想我将允许索引为.git/index.git/index.lock,否则就会失败。
    猜你喜欢
    • 1970-01-01
    • 2015-01-09
    • 2019-05-06
    • 1970-01-01
    • 2020-08-23
    • 2014-07-26
    • 2012-08-23
    • 1970-01-01
    相关资源
    最近更新 更多