“ git commit”的意外行为。预提交挂钩修改暂存文件时 [英] Unexpected behavior with "git commit ." when pre-commit hook modifies staged files

查看:80
本文介绍了“ git commit”的意外行为。预提交挂钩修改暂存文件时的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据我的经验, git commit -a 具有与 git commit相同的行为。但是,最近我创建了一个预先提交的挂钩会自动格式化我的源代码,现在 git commit。有一些意外的副作用:提交的文件最终在工作目录中被修改,并且commit命令完成后在索引中输入。 git commit -a 不会发生这种情况。我试图了解运行 git commit时发生的情况。导致这种情况的发生,并查看是否有一种方法可以在我的内部正确处理它

In my experience git commit -a has had equivalent behavior to git commit . However, recently I have created a pre-commit hook that automatically formats my source code and now git commit . has some unexpected side-effects: the file that is committed ends up as modified in the working directory and in the index after the commit command finishes. This doesn't happen with git commit -a. I'm trying to understand what is going on behind the scenes when running git commit . that is causing this to happen and see if there is a way to handle it properly within my pre-commit hook script.

预提交钩子:

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

Currently using git version 1.8.3.1 but am seeing the same behavior in more recent versions.

以下是在行首添加一个简单空格的命令序列:

Here are the sequence of commands for a simple space added at the beginning of a line:

[]$ 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]-< files> 。这是我的预提交脚本中的检查内容:

UPDATE: Using the very thorough answer from @torek (thanks!), I decided to give an error in the pre-commit hook if the user tries to use git commit . or git commit [--only] -- <files>. Here is the check in my pre-commit script:

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并不是从工作树中进行提交,而是从索引,这就是为什么您首先需要 git add 个文件的原因,但是 the 索引是一种谎言,因为在那里可能会比一个标准索引文件多。 (该索引也称为临时区域缓存,具体取决于Git的哪个部分正在执行调用。)

The fundamental problem here is that Git makes commits not from the work-tree but from the index, which is why you need to git add files in the first place—but the index is a sort of white lie, because there can be more index fles than just the one standard one. (The index is also called the staging area or the cache, depending on which part of Git is doing the calling.)

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

The index, by which I mean the one standard one, is a file in .git named index. If you inspect your .git directory you will find such a file. In the past, there really was only this one file. In modern Git (2.5 on up), the picture is considerably cloudier due to added work-trees: there's actually one index file per work-tree, so that .git/index is only the index for the main work-tree. There's an auxiliary the index per work-tree—but that's not quite what I mean to get at, here, it's just a case of showing how the assumption that there is one single index is already fraying at the edges. Admittedly, you're using Git 1.8.3.1 (which is really quite old) but it, too, is more complex than the nice simple white-lie "one index" setup.

当您使用 git commit -a 时,Git会创建一个新的额外索引。当您使用 git commit。时,您正在调用 git commit --only。有关详细信息,请参阅文档),然后Git制作两个新的额外索引(索引?)。

When you use git commit -a, Git makes a new, extra index. When you use git commit ., you're invoking git commit --only . (see the documentation for details), and Git makes two new extra indexes (indices?).

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

All parts of Git have the ability to redirect the rest of Git to use a different, non-standard index, and these various options to git commit make use of this feature. Note that git commit -a is equivalent to git commit --include followed by the names of any files that need adding. The really tricky case is the one you're using, git commit --only.

一旦开始增加索引文件,

Once you start multiplying index files, things get confusing!

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

Remember that the index is, in essence, the proposed next commit. If there's only one index (for this work-tree, if we're talking Git 2.5 or later), there's only one proposed next commit. That's not too difficult, we just have to consider that there are three copies of every file. Let's pick a file such as README.md:


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

  • HEAD:README.md is the currently committed version of README.md. You can't change it. (You can move HEAD itself, but the committed copy of README.md is inside the commit, as found by the commit's hash ID, and won't change.)

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

The name HEAD:README.md only works inside Git. That name accesses this frozen, Git-ified, freeze-dried copy of the file; this copy will never change. You can see it wth git show HEAD:README.md, for instance.

:README.md 是索引中 README.md 的副本。它最初与 HEAD:README.md 相同,但是如果您运行 git添加README.md ,则可能是

:README.md is the copy of README.md in the index. It was originally the same as HEAD:README.md but if you ran git add README.md, it might be different now.

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

The name :README.md also only works inside Git. That name accesses this replaceable, but Git-ified (freeze-dried format) copy of the file, as stored in the index. You can replace this any time with git add.

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

Finally, README.md is an ordinary (non-Git-ified) file. It's not in Git! It's not in the index! It's in your work-tree, where you can see it and work on it, using all your normal computer tools. Git really doesn't use this file for anything, it just overwrites it or removes it when you check out some other commit. The only thing Git does with it, other than check it with git status and such, is let you use git add to copy it back into the index, overwriting what was there before (and freeze-drying it in the process).

运行 git状态运行两个 git diff s:


  • 第一个将 HEAD 提交与索引进行比较,即当前提交中的内容与提议的下一个内容中的内容承诺。此处所有不同都被列为暂存提交。一切都一样,Git安静地什么也没说。

  • The first compares the HEAD commit to the index, i.e., what's in the current commit vs what's in the proposed next commit. Anything different here is listed as staged for commit. Anything that's the same, Git just quietly says nothing.

第二个 git diff 将索引与工作树进行比较,即提议的提交中包含什么,以及可以复制到索引中的内容。这里所有不同都被列为未上演提交的。同样,再次,Git安静地什么也没说。

The second git diff compares the index to the work-tree, i.e., what's in the proposed commit, vs what you could copy into the index. Anything different here is listed as not staged for commit. Anything that's the same, again, Git quietly says nothing.

(然后,最后通过了检查工作树中的文件根本不在索引中。Git会抱怨这些,说它们是未跟踪的,除非您在中列出它们。 c> .gitignore 不会更改索引中是否存在该文件的副本,而只会更改Git是否发牢骚。)

(Then there's a final pass to check for files in the work-tree that aren't in the index at all. Git will whine about these, saying that they are untracked, unless you list them in a .gitignore. Being listed in .gitignore doesn't change whether there is a copy of the file in the index, it just changes whether Git whines.)

当您运行 git commit 时,Git打包索引中的所有内容,并使用该内容进行新的提交.. 。除非,否则您使用-仅 -include -a

When you run git commit, Git packages up whatever is in the index, and uses that to make the new commit ... unless you use --only, --include, or -a.

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

With git commit --only, Git makes three index files:


  • 一个是标准文件。一开始没有被改动。那是正常的 .git / index

  • 一个是该副本的副本,其中-仅个文件 git添加了。有时它位于 .git / index.lock 中。 也许它总是在这里!如果是这样,那将提供一种处理我在下面概述的情况的方法。

  • 第三个是新的,首先提取 HEAD ,然后提取 git添加-仅个文件。

  • One is the standard one. It's untouched at the start. That's the normal .git/index.
  • One is a copy of that one, with the --only files git added to it. It's in .git/index.lock at some point. Maybe it's always here! If so, that would offer a way to handle the case I outline below. But there's no documentation that promises this.
  • The third is a fresh one made by first extracting HEAD, then git adding the --only files to it.

如果您未在之前添加 git ,则您运行 git commit -a ,第一个索引文件和第三个索引文件匹配,因为将-仅文件添加到常规索引与从 HEAD <创建新的临时索引具有相同的效果/ code>并向其中添加-仅文件。但是否则所有这三个文件可能都不同!

If you did not git add anything before you ran git commit -a, the first and third index files match, because adding the --only files to the regular index has the same effect as making a new temporary index from HEAD and adding the --only files to it. But otherwise all three files might be different!

Git然后根据 third 索引进行新提交。如果新提交成功,则Git用 second 索引替换常规索引(此替换通过重命名系统调用发生)。否则,Git返回正常索引。 (请注意,工作树什么都没有发生。)

Git then makes the new commit from the third index. If the new commit succeeds, Git replaces the regular index with the second index (this replacement happens via a rename system call). Otherwise Git goes back to the normal index. (Note that nothing happens to the work-tree at all.)

如果使用 git commit --include git commit -a ,Git仅使一个额外索引,因此您具有:

If you use git commit --include or git commit -a, Git makes only one extra index, so that you have:


  • .git / index 中的标准索引,以及到目前为止添加的所有内容;

  • 临时文件中的额外索引:这是标准索引的副本,但是Git然后将列出的文件或其他修改的文件添加到该索引中。

  • the standard index in .git/index, with whatever you had added so far; and
  • an extra index in a temporary file: this starts as a copy of the standard index, but then Git adds the listed files, or other modified files, to that index.

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

Then Git starts the commit process. If it all goes well, when Git is done, Git renames the temporary index so that it becomes the standard index. If things go badly, Git removes the temporary index and the standard index remains unchanged. Again, nothing happens to the work-tree.

Git在准备任何额外的索引文件后运行您的预提交钩子。特殊环境变量 $ GIT_INDEX_FILE 命名Git用于进行新提交的索引。因此,有3种情况,其中2种还算不错,另一种却很糟糕:

Git runs your pre-commit hook after preparing any extra index files. The special environment variable $GIT_INDEX_FILE names the index that Git will use to make the new commit. So there are three cases, two of which are not too bad and one of which is terrible:


  • 您正在执行常规提交。 GIT_INDEX_FILE 命名普通索引,一切正常。

  • 您正在执行 git commit- include git commit -a GIT_INDEX_FILE 命名第二个索引;没有第三索引;如果提交完成,Git将重命名第二个索引。

  • 您正在执行 git commit --only GIT_INDEX_FILE 命名第三个索引。没有简单的方法查找第二个索引,如果提交成功,则该第二个索引将在提交后存在!

  • You're doing a normal commit. GIT_INDEX_FILE names the normal index, and everything is normal.
  • You're doing a git commit --include or git commit -a and GIT_INDEX_FILE names the second index; there's no third index; if the commit completes, Git will rename the second index.
  • You're doing a git commit --only and GIT_INDEX_FILE names the third index. There's no easy way to find the second index, the one that will be in place after the commit, if the commit succeeds!

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

Your job, should you choose to make changes to the files stored in the index, is to make them to the index that Git will use to commit. To do that, you can use git add if you like, as this will copy files from the work-tree to the index named in $GIT_INDEX_FILE.

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

The first problem, though, is that you must not look at the files in the work-tree. They are irrelevant! They may contain something entirely different from what's in the index. This is particularly true during git commit --only.

第二个更大的问题是,如果您更新了 git commit --only 正在使用的>第三索引,还应该更新 second 索引git commit --only 正在使用。这部分很棘手,因为除了假定它位于 .git / index.lock 中之外,没有其他简单的方法可以找到它。

The second and bigger problem is that if you've updated the third index that git commit --only is using, you should also update the second index that git commit --only is using. This part is tricky, because there is no easy way to find it, other than to assume it is in .git/index.lock. While that might work I won't advise it here.

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

I really have no suggestions for this—any sneaky method you find may break, as the code to deal with this third index (which the current 2.21-ish Git calls the "false index") has changed a lot between 1.8 and modern Git. The usual best-practice recommendation is not to do any special formatting in a Git hook at all. Instead, have the Git hook merely check whether the index copy of the file is correctly formatted: if so, proceed with the commit, and if not, abort the commit. Leave the rest to the user.

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

An alternative that I have seen and used is to check the actual setting of $GIT_INDEX_FILE. If it's set to .git/index, the user is using git commit without any special settings. Another trick in this same pre-commit hook (which invokes clang-format and autopep8) is to compare the index and work-tree for files that would be formatted, and refuse to run if they don't match.

这篇关于“ git commit”的意外行为。预提交挂钩修改暂存文件时的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆