为什么 git diff 与 git diff --staged 下的输出不同?

Why does output differ under git diff vs. git diff --staged?

为什么我们需要这两个?在什么情况下它们在命令行上给出的输出不同?

能否解释一下两者在添加文件、暂存、修改等不同场景下的区别。那么分阶段和非分阶段的变化是什么?

git diff 报告当前文件和上次提交之间未暂存的更改。
git diff --cached 报告分阶段更改(即,您已 git add 编辑的更改)。

我认为你被误导了。 Git 根本不存储 更改 。整个事情看起来很神秘,直到你意识到 Git 只是完整地存储所有内容,但以一种奇怪的方式存储。

Git 永久存储的内容

首先也是最重要的,Git 并不完全存储 文件 。它最终会这样做,但那是因为 Git 存储 提交 ,并且每个单独的提交都包含(所有!)文件。也就是说,在开发过程中的某个早期阶段,您或某人告诉 Git:这是整个 file-tree,一些包含文件和 sub-directories 的文件夹/目录集包含更多文件和 sub-directories 等等。对它们现在的样子做一个快照。那个快照,所有内容的完整副本,进入一个新的提交。

接下来,提交一旦完成,大部分都是永久性的,而且完全是 100% read-only。您不能更改提交中的任何内容。您可以将它们视为永久性的:提交真正消失的唯一时间是,如果您仔细地运行ge 确保没有人——不是你自己,也不是任何其他人——可以找到 稍后,使用 git reset 或类似工具。

出于多种原因,包括如果您进行多次提交并保留 re-using 大多数文件的大部分旧版本,存储库不会变得非常庞大,存储在提交中的文件保存在一个特殊的文件中, 压缩, Git-only 格式。由于提交中的文件被冻结,如果新提交 C9 除了一个文件外与之前的提交 C8 一样,则两次提交将 共享所有相同的文件。

Git 可以让您暂时使用什么

因为你根本无法更改任何提交,Git如果没有办法将毫无用处从某个提交中提取 所有文件。提取提交会将其所有文件从 deep-freeze 中复制出来,然后 de-compresses 文件并将它们转换回您和您的计算机可以使用的普通 every-day 文件。这些文件是那个 Git 提交中内容的 副本 ,但是在这里,在这个工作区 - work-tree工作树—它们对您和您的计算机很有用,您可以随意更改它们。

Git 索引

使事情复杂化

棘手的部分来了。其他版本控制系统可能会到此为止:它们也有提交,以冻结形式永远保存文件,还有一个 work-tree,让您以普通形式处理文件。要进行新的提交,那些其他版本控制系统会慢慢地、痛苦地、一个接一个地获取每个 work-tree 文件,将其压缩以准备冻结,然后 然后 检查冻结的文件是否与旧文件相同。如果是这样,他们可以 re-use 旧文件!如果没有,他们会尽一切努力保存新文件。这非常慢,并且有多种方法可以加快速度,他们通常会使用这些方法,但是在这些 non-Git 版本控制系统中,使用他们的 "commit" 命令后,您通常可以起床并去喝杯咖啡,或者去散步,或者吃午饭什么的。

Git 做了一些完全不同的事情,这就是 git commit 与其他系统相比如此之快的原因。当 Git 从 deep-freeze 中取出文件放入你的 work-tree 时,Git 保持一种 semi-frozen—"slushy",如果你将——每个文件的副本,准备进入下一个提交。最初,这些副本都与冻结的提交副本相匹配。

这些 sort-of-slushy 文件副本位于 Git 调用的不同位置,即 index暂存区 缓存 ,具体取决于谁或 Git 的哪一部分正在执行调用。每个文件的这些索引副本与当前提交中的冻结副本之间的主要区别在于,提交的副本确实被冻结了。它们无法更改。索引副本仅几乎冻结:它们可以更改,方法是将新文件写入索引以代替旧文件。

最后,这意味着对于提交中的每个文件,当您告诉 Git 使该提交成为当前提交,使用 git checkout <em>someb运行ch</em>。 (此结帐 selects somebranch 作为当前 b运行ch name 因此也提取了什么Git 将其 tip commit 称为 current 提交。当前提交总是有一个名称:Git calls它 HEAD.) 例如,假设 master 的 tip 提交有两个文件,名为 README.mdmain.py:

   HEAD           index         work-tree
---------       ---------       ---------
README.md       README.md       README.md
main.py         main.py         main.py

此时,每个文件matc的所有三个副本彼此。也就是说,所有三个 README.md 都是相同的,只是它们的格式不同:HEAD 中的那个被冻结,而 Git-only 中的那个;索引中的一个是 semi-frozen 和 Git-only;并且您 work-tree 中的那个对您有用;但所有三个代表相同的 文件内容 main.py.

的三份也是如此

现在假设您更改了一个(或两个)work-tree 文件。例如,假设您更改 work-tree README.md。让我们用 (2) 标记它以表明它不同,并用 (1) 标记旧的以记住哪些旧的是:

    HEAD            index         work-tree
------------    ------------    ------------
README.md(1)    README.md(1)    README.md(2)
main.py(1)      main.py(1)      main.py(1)

您现在可以要求 Git 比较每个文件的索引副本与每个文件的 work-tree 副本,这一次,您'我会看到您的 更改 README.md

当您 运行 git add 时,您实际上是在告诉 Git:获取我正在添加的文件的 work-tree 副本,然后准备冻结。 Git 会将 README.mdmain.py(或两者)的 work-tree 副本复制回索引,Git-ifying内容,为下一次冻结做好准备:

    HEAD            index         work-tree
------------    ------------    ------------
README.md(1)    README.md(2)    README.md(2)
main.py(1)      main.py(1)      main.py(1)

这一次,要求 Git 将 index 副本(所有内容)与 work-tree 进行比较副本(所有内容)什么也没显示!毕竟,它们是一样的。要查看差异,您必须要求 Git 将 HEAD 提交与索引进行比较,或者将 HEAD 提交与 work-tree 进行比较。现在两者都足够了,因为 现在 索引和 work-tree 再次匹配。

但是请注意,您可以在使用 git add 后再次更改 work-tree 副本 。假设您再次修改 README.md,得到:

    HEAD            index         work-tree
------------    ------------    ------------
README.md(1)    README.md(2)    README.md(3)
main.py(1)      main.py(1)      main.py(1)

现在 main.py 的三个副本都匹配,但是 README.md 的三个副本都不同。所以现在重要的是你是否有 Git 比较 HEAD 与索引,或 HEAD 与 work-tree,或索引与 work-tree:每个都会显示 不同的变化README.md

Git 从索引

进行 new 提交

何时以及如果您确实选择进行新提交——所有文件的新永久快照现在——Git制作新提交的快照使用索引中的 semi-frozen 文件。 commit 动词与它们有关的所有事情就是完成冻结过程(在技术层面上,包括制作 tree 对象来保存它们,但你不需要知道这个)。所以 git commit 收集你的姓名、电子邮件、时间、你的日志消息和当前提交的哈希 ID,冻结索引,并将所有这些放在一起到一个新的提交中。新提交 成为 HEAD 提交,因此现在 HEAD 指的是 new 提交。如果旧提交是 C8 而新提交是 C9,那么 HEAD 曾经表示 C8,但现在是 C9.

提交完成后,每个文件的 HEAD 和索引副本 自动匹配 。很明显他们必须这样做,因为新的 HEAD 来自 的索引。因此,如果您使用包含中间版本 README.md 的索引进行新提交,您将得到:

    HEAD            index         work-tree
------------    ------------    ------------
README.md(2)    README.md(2)    README.md(3)
main.py(1)      main.py(1)      main.py(1)

注意在这个过程中Git完全忽略了work-tree!有一种方法可以告诉 git commit 它应该自动查看 work-tree 和 运行 git add,但让我们稍后再说.

这个特定部分的总结是,考虑索引的一个好方法是:索引包含您打算进行的 下一个 提交。 git add 命令意味着:更新我建议的下一次提交。 这解释了为什么你必须一直 git add

Git的diff动词

因为每个文件的这三个同时的、活动的副本——一个永久的,一个提议用于下一个提交,一个您可以实际看到和使用的东西——Git 需要一种方法来 比较 这些东西。 diff 动词是你如何要求 Git 比较两个事物,它的选项是你如何 select 比较哪个 两个事物:

  • git diff <em>commit-A commit-B</em> 告诉 Git: 提取commit A中的快照到一个临时区域;将提交 B 中的快照提取到一个临时区域,然后比较它们并告诉我有什么不同。 这通常很有用,但在制作 new 时就没那么有用了提交,因为它是关于现有的、冻结的、不可更改的提交。

  • git diff—根本没有选项或提交说明符—告诉 Git:将索引与 work-tree 进行比较。 Git 不查看任何实际提交,它只查看索引 - 提议的 下一次提交 - 并与您可用的文件副本进行比较。每当有不同时,您 可以 使用 git add 复制 i进入索引。因此,这会告诉您可以 git add,如果您愿意的话。

  • git diff --cachedgit diff --staged—选项具有完全相同的含义—告诉 Git:比较 HEAD 提交到索引。 这一次,Git 根本不看你的 work-tree。它只是找出 当前提交 提议的下一次提交 之间的不同之处。也就是说,如果您现在提交,这将是不同的

  • git diff HEAD(或者更一般地说,git diff <em>commit</em>)告诉Git:比较我命名的提交中的内容,例如 HEAD,与 work-tree. 中的内容这次,Git 忽略您的 index,并且只与特定的提交(例如 HEAD)和 work-tree 的内容一起使用。这不如 HEAD-vs-index 或 index-vs-work-tree 比较有用,但您可以根据需要进行比较。

当然,您可能希望通过更多方式来比较任意两个项目,因此 git diff 有很多选择。但这些是目前主要的兴趣点。

git status运行s 两个git diffs

请注意,上面两个 最有用的 git diffgit diff --cached,它告诉您 如果您现在 git diff 没有选择, 会有所不同,这告诉您还有什么不同 如果你现在 运行 git add。您应该经常使用的 git status 命令,运行 为您提供了这两个差异!它 运行 在内部设置了 --name-status 标志,这样它不会显示实际差异,如果文件发生更改,它只会显示文件名。1

让我们再看一遍:git status 运行s 两个 git diff命令。第一个是 git diff --cached,即 提议的提交 有何不同。这些是 为提交 准备的更改。第二个是普通的 git diff,即索引(建议的提交)和 work-tree 的不同之处。这些是 未准备提交的更改。

所以现在您知道 git status 告诉您什么,以及何时您想要使用 git diff 有或没有 --staged 来查看 名称以外的内容 个文件。请记住,git diff 向您显示的更改是 Git 计算出: 索引中或 work-tree 中的文件已满, 完整副本。它们可能彼此不同 and/or 不同于 HEAD 中的完整副本。


1--name-status 的 "status" 部分可以改为表示文件 added—is in索引,但不在 HEAD 提交中,例如。或者,在某些情况下,它可以说文件已重命名或进行了一些其他辅助更改,但我们不在这里讨论。