工作文件在添加到登台后会被修改。然后将文件提交给 git 并添加。应该发生啥?

Posted

技术标签:

【中文标题】工作文件在添加到登台后会被修改。然后将文件提交给 git 并添加。应该发生啥?【英文标题】:A work file is modified after it was added to staging. Then the file is committed to git without and add. What should happen?工作文件在添加到登台后会被修改。然后将文件提交给 git 并添加。应该发生什么? 【发布时间】:2021-05-15 23:03:46 【问题描述】:

我注意到,在将修改后的文件添加到 git 中的 staging 并再次更改文件然后随后提交没有添加的情况下,没有错误也没有警告。工作文件中的最新更改被提交。最初添加到分期中的内容是否被丢弃?

$ git init
Initialized empty Git repository in /tmp/test/.git/

/tmp/test (master)
$ git config --global user.name "Your Name"

/tmp/test (master)
$ git config --global user.email "you@example.com"

/tmp/test (master)
$ echo A > my.txt

/tmp/test (master)
$ git add my.txt

/tmp/test (master)
$ git commit -m '1st' my.txt

[master (root-commit) c804a96] 1st
 1 file changed, 1 insertion(+)
 create mode 100644 my.txt
此时 my.txt 已提交 'A'
/tmp/test (master)
$ echo B >> my.txt

/tmp/test (master)
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   my.txt

no changes added to commit (use "git add" and/or "git commit -a")


/tmp/test (master)
$ git diff

The file will have its original line endings in your working directory
diff --git a/my.txt b/my.txt
index f70f10e..35d242b 100644
--- a/my.txt
+++ b/my.txt
@@ -1 +1,2 @@
 A
+B

/tmp/test (master)
$ git add my.txt

此时工作文件有额外的“B”并被添加到暂存

/tmp/test (master)
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   my.txt

/tmp/test (master)
$ git diff

/tmp/test (master)
$ git diff --cached
diff --git a/my.txt b/my.txt
index f70f10e..35d242b 100644
--- a/my.txt
+++ b/my.txt
@@ -1 +1,2 @@
 A
+B

/tmp/test (master)
$ git diff HEAD
diff --git a/my.txt b/my.txt
index f70f10e..35d242b 100644
--- a/my.txt
+++ b/my.txt
@@ -1 +1,2 @@
 A
+B

/tmp/test (master)
$ echo C >> my.txt

此时“C”已添加到工作文件中,但未添加到暂存中

/tmp/test (master)
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   my.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   my.txt


/tmp/test (master)
$ git diff --cached
diff --git a/my.txt b/my.txt
index f70f10e..35d242b 100644
--- a/my.txt
+++ b/my.txt
@@ -1 +1,2 @@
 A
+B

/tmp/test (master)
$ git diff
diff --git a/my.txt b/my.txt
index 35d242b..b1e6722 100644
--- a/my.txt
+++ b/my.txt
@@ -1,2 +1,3 @@
 A
 B
+C


/tmp/test (master)
$ git commit -m '2nd' my.txt
[master 4f574dc] 2nd
 1 file changed, 2 insertions(+)

此时提交是在没有“添加”的情况下完成的

/tmp/test (master)
$ git status
On branch master
nothing to commit, working tree clean

/tmp/test (master)
$ git diff

/tmp/test (master)
$ git diff --staged

/tmp/test (master)
$ git diff HEAD

/tmp/test (master)
$ cat my.txt
A
B
C

【问题讨论】:

【参考方案1】:

来自https://git-scm.com/docs/git-commit 的文档:

    通过将文件列为commit 命令的参数(没有--interactive 或--patch 开关),在这种情况下,commit 将忽略索引中暂存的更改,而是记录当前的内容列出的文件(Git 必须已经知道);

(强调我的——phd

【讨论】:

【参考方案2】:

phd's answer 是正确的,但值得扩展。

如果您查看the current documentation for git commit(随着时间的推移而演变),它应该包括--include--only 选项:

-i--include

      到目前为止,在使用暂存内容进行提交之前,还要暂存命令行中给出的路径的内容。这通常不是您想要的,除非您要结束有冲突的合并。

-o--only

      通过获取命令行中指定路径的更新后的工作树内容进行提交,忽略已为其他路径暂存的任何内容。如果在命令行中给出了任何路径,这是 git commit 的默认操作模式,在这种情况下可以省略此选项。 [剪辑]

正如最后引用的那句话所说,default 操作,在将路径名添加到您的 git commit 命令时,将表现为 git commit --only。这个特定的操作是以一种非常复杂的方式实现的,这可能会混淆一些预提交钩子。

--include 的行为更容易描述,尽管这个简单/简单的描述有一些缺陷(请参阅下面的完整和正确的描述)。将git commit --include 与:

$ git add file1.txt
$ git commit --include file2.txt

例如本质上等同于做:

$ git add file1.txt
$ git add file2.txt
$ git commit

也就是说,--include 只是运行git add 你,虽然复杂的是如果提交失败,这些文件神奇地“未添加”。

但是,--only 选项要复杂得多。没有简单的方法来描述它也是完全正确的。为了正确地描述这两个,我们需要详细了解 Git 的索引。

技术细节:索引

当 Git 进行新的提交时,它总是1indexstaging areacache。这是同一件事的三个词。索引/暂存区/缓存是 Git 跟踪您想要提交的内容的方式。除了冲突合并的特殊情况外,2索引包含您的建议的下一次提交。当你第一次 git checkoutgit switch 提交时,Git 填写它的索引 from 提交。所以你提议的 next 提交与你的 current 提交匹配。

你可能注意到这里我有时会说索引,或Git的索引,好像只有一个索引,但有时我也会说an索引,好像可以有多个。这里的棘手部分是 两者都是正确的:有一个特定的特殊索引 - 索引 - 但您可以有多个。

从技术上讲,区分索引是每个工作树的:如果您使用git worktree add,您不仅会添加另一个工作树,而且还会为该特定工作树添加另一个区分索引。您可以通过以下方式找到可分辨索引的文件名:

git rev-parse --git-path index

通常打印.git/index,但在添加的工作树中,打印其他内容。如果在环境中设置了$GIT_INDEX_FILE,它会打印这个变量的值。这就是 Git 交换到某个备用索引文件的方式——或者更准确地说,它是 可以使用的外部可用机制,将 Git 指向某个备用索引文件,以及一种预提交挂钩的方式例如,检测 git commit --only 调用。

当您运行 git add 时,Git 会在索引中为您的 git add-ing 文件找到现有条目:

如果没有现有条目——如果这是一个 new 文件——Git 将文件复制到 Git 的索引中,现在有一个现有条目:你提议的新提交有一个新添加的文件,如与当前提交相比。

否则,Git 的索引中有一些现有文件。 Git 从它的索引中引导这个文件,并将文件的工作树版本复制到它的索引中。如果此文件副本与当前提交中的副本不同git status 现在会说该文件暂存以进行提交

所以,git add 只是更新您提议的 next 提交,它始终(但请参见脚注 2)保存 Git 将快照的每个文件的副本index 中的副本是 git commit 将使用的副本。

现在我们知道索引是如何工作的,并且 Git 可以使用我们可以创建的一些额外的临时索引文件,现在我们可以真正看到 git commit --includegit commit --only 是如何工作的。


1这对于git commit 是正确的,但如果您使用git commit-tree,您可以绕过对索引的需求。您必须向git commit-tree 提供树的哈希ID。你会从哪里得到那棵树?如果您使用git write-tree,则使用索引。但是你可以从其他地方得到一棵树,例如,只使用一些现有的树,或者使用git mktree。但是请注意,使用git mktree 您可以构建不正确的树;生成的提交将无法检出。

2在冲突合并期间,Git 会扩展索引。此扩展索引无法写出:git write-tree 抱怨并中止。使用git addgit rm,您可以将扩展的索引条目替换为普通条目,或者完全删除一些条目。一旦没有扩展的、非零阶段的条目,所有冲突都解决了,因为git write-tree 现在可以写出索引:再次可以提交。


技术细节:--include--only

为了实现git commit --include,Git 或多或少地这样做了:

    索引复制到临时索引(“an”索引); 在临时索引上运行git add,包含您要包含的文件; 尝试提交。

尝试的提交可能会成功(创建新提交并更新当前分支名称),也可能会失败。提交失败,例如,如果git commit 运行您的编辑器,然后您选择删除整个提交消息。也许您正在查看某些内容并意识到您不应该提交,所以您这样做了。或者,如果预提交钩子确定此提交尚未准备好,则提交失败。请注意,预提交钩子应该查看 临时索引 !它不应该查看工作树中的文件。这不一定是提交中的内容。您提议的下一次提交现在是 临时索引中的任何内容。

如果提交失败,Git 只会删除临时索引。原来的索引—— 索引——没有被改动,所以现在一切都恢复了原状。第 2 步中的 git adds 被神奇地撤消了。

如果提交成功,Git 只需将 索引替换为临时 索引。现在索引和当前提交——也就是我们刚刚创建的那个——匹配,所以没有任何东西是“为提交准备的”。这就是我们喜欢它的方式。

实现git commit --only 更难。还有两种情况:提交可以失败,或者提交可以成功。对于“失败”情况,我们希望发生与git commit --include 相同的事情: 索引,主要的区别索引,不受干扰,就好像我们甚至没有尝试运行 @ 987654371@。但是,对于 success 的情况,git commit --only 很棘手(我认为文档有点不足)。

假设我们这样做:

$ git checkout somebranch         # extract a commit that has files
$ echo new file > newfile.txt     # create an all-new file
$ git add newfile.txt             # stage the all-new file (copy into index)
$ echo mod 1 >> file.txt          # append a line to an existing file
$ git add file.txt                # stage the updated file (copy into index)
$ echo mod 2 >> file.txt          # append *another* line to the file
$ git commit --only file.txt -m "test"

如果成功,我们希望得到什么结果?我们告诉 Git 提交两行加法。我们的文件工作树副本是添加了两行的版本。 staged 文件是否应该在我们的测试提交之后建议下一次提交,是否只添加了 one 行?还是应该都添加行?

Git 对这个问题的回答是它应该同时添加两行。也就是说,如果git commit 有效,git status 现在应该对file.txt 什么都不说;它应该只说newfile.txt 是一个新文件。因此,该文件的 两行 版本必须是建议的下一次提交中的版本,此时。 (你可能同意 Git,也可能不同意,但这就是 Git 作者选择的结果。)

这意味着我们在git commit --only 尝试提交时需要三个 版本的索引:3

一个(原始索引)将包含新文件和添加的行。 一个——git commit 用于进行新提交的索引——将包含新文件,但会将两行添加到file.txt。李> 最后一个将包含新文件,以及添加到file.txt 的两行。

这三个中的中间一个是git commit 在尝试进行新提交时将使用的那个。这有两个添加的行,但没有新文件:它是 git commit --only 操作,正在执行中。

如果提交失败git commit 会简单地删除两个临时索引文件,而使原始索引(索引)不受干扰。我们现在在提议的下一次提交版本file.txt 中添加了一行,并且我们在提议的下一次提交中也有新添加的file,就好像我们根本没有运行过git commit --only file.txt

如果提交成功git commit 将创建 last 索引——其中既有新添加的文件,也有两行版本的file.txt ——成为(主要/杰出)索引。原始索引用于提交的临时索引都被删除了。

这就是git commit --only 如此复杂的原因。假设您正在自己编写一个预提交挂钩,并且在这个预提交挂钩中,您打算做两件事:

    使用 linter 确保任何要提交的代码(pylintpep8go vet 等)中没有明显的错误。 使用格式化程序确保代码符合项目标准(blackgo fmt 等)。

(在我看来,第 2 步是一个错误:不要这样做。但其他人喜欢这个想法。)

我们现在有三种情况:

你正在做一个普通的git commit$GIT_INDEX_FILE 未设置。只有一个指标需要担心。您将文件从(普通、日常、标准)索引中读取到一个临时目录中,并在那里对它们进行 lint。如果 linting 失败,您将停止并拒绝提交。如果 linting 成功,您将文件格式化并 git add 将它们返回到(单个)索引,然后让提交发生。

这里仍然存在一个大问题,因为刚刚提交的文件是暂存的文件,而不是用户工作树中的文件。在git adding 任何格式更新之前,您也许可以检查 工作树 文件与索引中预先更新、尚未格式化的文件。如果工作树文件与索引副本匹配,那么在这里也可以重新格式化工作树副本。

你正在做一个git commit --include。有两个索引文件需要担心,但出于 linting 的目的,您只需从 Git 正在使用的索引文件中读取那些索引文件 now 用于此提交,该文件位于 $GIT_INDEX_FILE(通常命名为.git/index.lock 此时)。4

您可以像以前一样对待它,因为您所做的任何格式化都会进入建议的提交,并且破坏用户的工作树文件与上次一样安全。如果您要拒绝提交,您已经拒绝了提交(大概没有进行任何格式化);如果提交成功,正如您认为的那样,用户的--include 文件也应该被格式化,毕竟。成功后,您对临时索引所做的任何更新都将在实际索引中,因为临时索引成为真正的索引。

你正在做一个git commit --only。现在有 三个 索引文件需要担心。其中之一——git commit 将要使用的——在$GIT_INDEX_FILE 中。其中之一——git commit 计划用来替换主要/可区分索引的一个文件位于您不知道其名称的文件中。第三个,Git 在失败时会退回的那个,是标准的主索引。

您可以像往常一样进行检查:lint / vet $GIT_INDEX_FILE 中的文件。毕竟,这就是用户提议的提交。

但是现在,如果您格式化这些文件并将它们添加到 $GIT_INDEX_FILE ... 那么,格式化的文件就是那些将被提交的文件。这一切都很好。但是你还需要git add那些格式化的文件到你不知道名字的临时索引文件!而且,在针对某些索引副本检查工作树文件时,您可能应该使用您不知道名称的临时索引文件中的副本。

如果您更改任何文件,而只是简单地检查/检查所有文件并检查所需的格式,这些问题就会出现离开。所以最好只是检查一下东西。如果用户希望根据项目规则格式化工作树文件,请为它们提供工作树文件格式化程序。让 them 运行它,然后让 them 在更新的文件上运行 git add(或者,如果你真的必须,提供在格式化脚本中添加回格式化文件)。

我参与过一个项目,其中 pre-commit 挂钩进行了检查,然后,如果格式错误,检查 $GIT_INDEX_FILE 并且会停止并且对于棘手的情况不做任何事情,或者提供给 git add 重新格式化文件。这也是一种选择,但它有点冒险,因为 Git 可能会改变某些行为,导致 $GIT_INDEX_FILE 测试以某种方式失败。


3毫无疑问还有其他方法可以达到预期的效果,但考虑到索引文件实际上是一个文件(至少最初是这样),加上一个一堆现有的 Git 代码,这个三个索引文件的技巧是可以使用的。

4这是我最后一次测试这一切时的情况,但那是很久以前的事了——在git worktree add 存在之前,这显然会影响这一点,至少对于增加的工作来说-树。

【讨论】:

【参考方案3】:

感谢 torek 提供了足够多的上下文来消除混乱。 what"s the difference between git commit <file> and git commit --only? 中还有更多内容

这个故事的寓意是—— 除非你知道自己在做什么,否则在提交时不要指定文件。

典型动作:

 git commit -m "commit everything that is currently staged"

非典型行为:

git commit -m "commit only my.txt working file, ignore what is staged" my.txt 

在上述提交中指定文件意味着 --only 选项

git commit --only -m "commit only my.txt working file, ignore what is staged" my.txt

此 --only 选项表示仅提交在提交命令中指定的文件并忽略当前正在暂存的文件。提交后,阶段文件将保持原样,等待提交。我想这种“插队”行为是有用例的。但是允许隐式使用 --only 选项是鲁莽的。这是一场等待发生的意外。

这是一个可以帮助记住这一点的类比。 您和您的朋友到达一家餐厅,正在等待(上演)坐在一张大桌子旁。领班意识到您 12 人小组中的 2 人实际上不属于您的小组,并特别优先考虑(仅)让他们先坐在 2 人餐桌上。而不是说“下一个要就座的人跟我来” ,领班说“你们两个跟我来”。

正如已经指出的那样,--include 提交选项始终是显式的,并且表示还将命令中指定的工作文件添加到当前已暂存的任何其他文件中。

git commit -m "add almostforgot.txt to what is staged " --include almostforgot.txt

相当于:

git add almostforgot.txt
git commit -m "now have everything"

【讨论】:

以上是关于工作文件在添加到登台后会被修改。然后将文件提交给 git 并添加。应该发生啥?的主要内容,如果未能解决你的问题,请参考以下文章

git 忽略本地修改,不将修改提交到远程

Git 使用

docker-数据管理

类加载的过程

git命令

GIT教程笔记