在git中,“重置后未暂存的更改”是什么意思?

3

我无意中将一堆文本文件添加到了我的git repo中,并试图将它们取消暂存(在提交之前):

git reset dir/*.txt

当运行该命令时,它会显示以下内容:
unstaged changes after reset:
dir2/file.h
dir4/file2.cc
...

这些文件与重置通配符无关。 据我所知,这些文件仍被标记为已修改并准备提交,看起来是完好无损的。Git试图告诉我什么?


git status 说了什么? - Lasse V. Karlsen
它们没有被提交到舞台上。这当然与信息一致,但为什么会出现这种情况呢?它们是已经受到版本控制很久的文件。 - Mastiff
1个回答

6
Git试图在这里提供帮助-或许是过于热心了。术语“未暂存的更改”是一种简化Git使用的思维方式,但这并不总是奏效,因为Git很复杂。
以下是基本事实:Git始终拥有每个文件的三个副本。其中两个副本实际上无法被看到,至少不能用你通常使用的文件导航工具看到。
当你考虑提交的含义和作用时,这三个副本中的两个是有意义的:
1. 每个提交都保存了每个文件的完整快照,永久保存。文件的快照版本是指该文件在提交时的状态(由您或其他人创建)。(每个提交还持有一些其他东西 - 一些元数据或关于提交本身的信息 - 但我们将在此处忽略它们。)
2. 因为每个提交都有每个文件的完整快照,所以存储在提交内部的文件不是作为普通日常文件存储的。如果是这样的话,仓库会在极短的时间内变得非常臃肿。
3. 因此,Git提交中的文件以一种特殊的、只读的、仅限于Git的、压缩和去重的格式存储。因为它们是只读的,所以对于提交来说,共享这些文件的副本是完全安全的。制作一个包含一万个文件的新提交,但其中9999个文件与上一个提交相同,实际上只重新使用了9999个文件,并仅对更改的一个文件进行了快照。如果该文件已被更改为与早期提交中的某个状态相同,则最后一个文件与早期提交共享,因此新的快照根本不占用空间。
以上所有内容的问题在于,提交内部的文件完全无法用于完成任何实际工作:它们只能被Git读取,甚至Git本身也不能将任何内容写入它们。因此,为了使用提交,Git必须将其复制出来,将文件从其特殊的仅限于Git的格式展开为普通日常文件。这些以日常形式存在的文件进入Git称之为你的“工作树”或“工作目录”。
这一切都很有道理:某些文件的两个“活动”副本,例如 README.md 或其他文件,是当前提交版本,只读的,在您选择为当前提交的任何提交中,并且只有 Git 可以看到和读取它,以及您的工作树版本,它实际上不在 Git 中。Git 已将其提取到一个工作区,您现在正在使用它,但它不在存储库中。它从存储库复制出来了;自那时以来,它可能已更改或未更改。
我们真正需要的仅仅是两份副本,而其他不是 Git 的版本控制系统就停在这里,只有两个“活动”副本。但是由于某种原因,好或坏,Git 没有停在这里。Git 在冻结的README.md和有用的版本之间插入第三个副本。这第三个副本位于 Git 称之为索引、暂存区或者罕见情况下称为缓存的东西中。这三个名称都是指同一物体。
大多数情况下是这样的,但如果你把事情分得够细——特别是如果你使用 Git 的一些非用户界面工具——你可以进行有趣的技巧或其他操作。还有所谓的“裸”存储库,它根本没有工作树,除非你暂时分配一个;这是我们在这里忽略的另一个复杂性。
除了保存元数据所需的空间外,其他的都一样。这里的细节也变得非常棘手。所有这些的重点是,通过重用旧文件,Git 可以保持其存储库较小。鉴于文件的压缩方式,在某些情况下,具有许多提交的 Git 存储库有时比任何已检出版本都要小!
Git 的某些内部部件区分索引和缓存,在那个时候,缓存是一个内存数据结构,而索引通常是文件 .git/index。相当古老的git apply命令有两个单独的标志,--index--cached,它们执行不同的操作。但是 git rm --cached 的真正含义是从索引中移除;例如,在这种情况下,“索引”和“缓存”这两个词确实是同义词。

索引中都包含了什么

从技术上讲,索引中的内容并不是文件本身,而是文件的名称——Git所看到的完整文件名称,包括正斜线,例如path/to/file.ext,以及一堆内部信息,其中一些可以使用git ls-files --stage查看。(尝试一下,但要注意它会不间断地输出大量内容。)
然而,把技术细节放在一边,索引实现的是它保存了你的建议的下一个提交。索引中的文件与提交中的文件形式相同,都是预压缩和去重复的,但是与提交副本不同,Git可以通过删除那个去重后的副本并创建新的去重副本来覆盖它们。
最初,当你git checkout某个特定的提交时——例如分支feature上的最新或tip提交——Git将使用来自该提交的文件填充其索引,并在你的工作树中填充这些文件。结果是,所有三个活动副本匹配。只读的提交副本与可替换的索引副本匹配。索引副本与提交副本和你的工作树副本都匹配。
当你做事时,你会修改一些文件。这些自然是你的文件副本,处于可用的格式。Git不使用这些文件! Git早先通过从提交中提取它们来创建它们,但除此之外,Git就把这些文件留给了你。如果你修改了它们,需要告诉Git对其索引/暂存区副本进行操作。
你所做的就是运行git add。这让Git读取你的工作树副本,压缩它,将其与所有存储的文件去重复,并更新其索引副本。现在Git的索引副本与你的工作树副本相匹配。
请注意,由于有三个副本,你可以使它们全部不同步:只需检出一些提交,修改一些文件,在该文件上运行git add,然后再次修改该文件。现在,时光冻结的提交副本与索引副本不同,后者是你先前添加过的副本,而你的工作副本仍然与它不同,因为你又没有运行git addgit status通过执行两个差异来工作
在运行git status命令时,它首先会输出一些人们认为有用的总体信息,例如当前分支名称4,此分支与其他分支或远程跟踪名称之间的差距等。然后它会进入到文件
它列出的第一组文件(如果有)是它所称的暂存区文件。但实际上,它在比较当前提交和索引。对于每个相同的文件,它根本不会输出任何内容。对于每个不同的文件,它会显示已暂存以备提交
如果有第二组文件,它再次列出的是被称为工作区未暂存文件的文件。同样,它实际上是将其索引与您的工作树进行比较。对于每个相同的文件,它根本不会输出任何内容。对于每个不同的文件,它会显示未暂存以备提交
4Git将当前分支名称存储在称为HEAD的某个位置。每个工作树都有一个HEAD和一个索引;主要的HEAD和索引通常是.git/HEAD.git/index,而任何添加的工作树都会获得一对新的HEAD和索引。您不应该需要知道这一点,但有时只需查看.git/HEAD(它目前只是一个普通文本文件)即可了解情况。但是,所有这些都可能在未来发生变化:例如,HEAD曾经是符号链接。

git reset

git reset命令比较复杂5。我们将忽略大部分复杂性,只关注您运行以下命令时所使用的git reset:
git reset dir/*.txt

这种特定的git reset现在可以通过新的(自Git 2.23以来)git restore实现。它将文件从当前提交复制到Git的索引中,而不触及您的工作树。6 当您这样做时,您需要找到一个文件名列表。这有点复杂,因为“某物”可能是您的shell,也可能是Git,如果是Git,则Git找到的文件名集可能与Shell找到的集合不同。为简单起见,让我们假设双方发现的文件集是相同的:Shell使用dir/*.txt发现的所有文件是Git在当前提交中与dir/*.txt匹配发现的文件的同一组文件。因此,Git将所有这些文件从当前提交复制到Git的索引中。
如果它们已经在Git的索引中 - 作为该文件的那个版本 - 则没有影响。但是,无论Git索引/暂存区中的任何文件是否不同 - 可能是因为您更改了自己的副本后使用了git add - 这都会覆盖更新的索引副本,并将其设置回与提交的副本相匹配。因此,这会撤销您先前运行的任何与dir/*.txt匹配的git add操作。
完成上述操作后,git reset会执行部分的git status。也就是说,它比较Git索引中每个文件与您工作树中相同文件的副本。对于那些不同的文件,Git将它们列出为“未暂存”。Git没有触及Git索引中的dir2/file.h,但是它在您的工作树中已经与Git索引中的版本不同。因此,git reset在此处的输出包括它。其他列出的文件也是如此。 完成上述步骤后,git reset现在执行部分的git status也就是说,它比较Git索引中每个文件与您工作树中相同文件的副本。对于那些不同的文件,Git将它们列出为“未暂存”。Git没有触及Git索引中的dir2/file.h,但是它在您的工作树中已经与Git索引中的版本不同。因此,git reset在此处的输出包括它。其他列出的文件也是如此。
5我认为它太复杂了,应该像将git checkout拆分为git switchgit restore一样进行拆分。当然,为了兼容性,仍然会有git reset,就像在分裂之后Git 2.23及更高版本仍然有git checkout一样。

6git restore实际上更强大,因为您可以选择任何一个提交,而不仅仅是当前的提交,并且您可以选择将其复制到Git的索引、您的工作树或两者兼而有之。因此,如果git reset按照我在脚注5中考虑的方式进行拆分,其中一条命令将是git restore


结论

成功修改一些文件的索引副本后,git reset会运行部分的git status。它会比较文件的索引副本和工作树副本。它不仅仅比较单个重新设置的文件,而是比较所有的索引条目。由于索引列出了下一次提交中的每个文件,所以这可能是很多文件。

请注意,当您修改文件然后运行git add时,您正在安排每个文件的中间副本,在Git的“暂存区”(索引)中,以便为下一次提交做好一切准备。这就是我们称之为“暂存区”的原因:我们把特定的文件副本放在“舞台”上,然后拍摄一张称为提交的照片快照。该提交是从“舞台”上的内容构建的,这可能与您正在使用的内容不同。

其他版本控制系统不会这样做:它们没有单独的暂存区,当您进行新的提交时,它们会对您的工作树进行快照。这有其优点和缺点,并需要一个文件列表(通常称为“清单”),因为工作树往往有许多不应提交的文件。Git使用其索引来实现此目的:如果您没有将文件复制到索引中,即未将其放在稍后的快照中的“舞台”上,它将不会出现在快照中。

但是,由于索引具有每个文件的完整副本,因此存在三个副本,这导致了这些奇怪的情况。由于您无法“看到”索引副本,因此需要一些东西(通常是git status),以比较索引副本和工作树副本,并让您知道是否要更新拟议的下一次提交。我们通过它的影子“看到”索引:当它匹配当前提交和/或工作树时,我们什么也看不到。较少的影子使那些存在的影子更加突出,因此这相当有效。但是这很棘手!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接