CodeWizard's answer 在一些重要细节上有错误,如Edward Thomson noted in a comment。
超短版是git status运行git diff。
事实上,它运行它两次,或者更准确地说,它在git diff 上运行两种不同的内部变体:一种用于将HEAD 与索引/暂存区域进行比较,另一种用于比较将暂存区与工作树进行比较。它运行每个 diff 并请求搜索重命名,即设置 -M 标志(见下文)。最后,它以您要求的任何格式向您展示这些差异的结果。但是,在任何情况下,它都不会显示文件之间的实际更改(因此实际上它也使用--name-status 运行这些差异)。
使用各种差异
您可以手动运行这两个内部差异:一个具有拼写为 git diff-index --cached 的前端命令,一个具有拼写为 git diff-files 的前端命令。这个前端选择被捕获在标题为Raw output format 的稍微奇怪的部分中(我不得不稍微修改一下,以便在 StackOverflow 上更好地显示):
git-diff-index、git-diff-tree、git-diff-files 和 git diff --raw 的原始输出格式非常相似。
这些命令都比较两组事物;比较的不同:
git-diff-index <em>tree-ish</em>
比较 tree-ish 和文件系统上的文件。
git-diff-index --cached <em>tree-ish</em>
比较 tree-ish 和索引。
git-diff-tree [-r] <em>tree-ish-1 tree-ish-2</em> [<em>pattern</em> ...]
比较由两个参数命名的树。
git-diff-files [<em>pattern</em> ...]
比较文件系统上的索引和文件。
(您也可以使用常规 git diff 调用这些:git diff --cached 将当前 (HEAD) 提交与暂存区域进行比较,git diff 没有附加参数将暂存区域与工作进行比较-树。)
将树映射回路径
CodeWizard 的答案是这个过程的关键。本质上,tree 对象包含路径名组件(例如foo/bar 中的foo 或bar)和另一个对象ID。如果组件表示一个目录,则对象 ID 定位另一个树对象;如果它表示一个文件,则对象 ID 定位一个 blob 对象。无论哪种情况,ID 都是 Git 的内部名称,这使 Git 能够在存储库中找到它。
(对于索引/暂存区域本身而言,情况并非如此,其格式没有很好地记录。它是所有文件的平面列表,具有完整路径名,但也使用名称压缩技术,因此@ 987654353@后跟VeryLongDirectory/AnotherLongDirectory/baz不必每次都拼出VeryLongDirectory/AnotherLongDirectory。)
(树对象还存储 Git 在提取时应分配给文件的模式,除了在树对象中,每个文件模式都只有 100644 或 100755;最后的 rwx 位已设置基于您的 umask,假设是一个类 Unix 主机,如果存储模式为 100644,则 x 始终清除,否则 set-except-as-cleared-by-umask。)
未暂存的文件和检测重命名
git 如何在内部知道文件是否已被删除、添加或编辑(具体来说,它如何计算您在键入 git status 时看到的更改)?
工作树中的文件,但既不在HEAD 提交也不在索引/暂存区域是未暂存(这是在事实上“未分级”的定义)。 Git 通过查看所有三个文件来查找此类文件(并使用索引/暂存区域获取缓存信息以加快处理速度)。所有未暂存的文件路径通常都提供给“忽略”代码,如果它们在 .gitignore 或任何其他忽略某些路径文件中列出,这会使 git 对它们闭嘴。
放弃未暂存的路径后,让我们考虑剩余的路径,它们(根据定义)至少出现在HEAD 或索引/暂存区域之一中。
一般来说——虽然git status 没有设置任何一个,但有更多的标志可以更详细地控制它——Git 首先将“A”端 (a/foo/bar) 中可用的路径名与“B”侧 (b/foo/bar)。如果相同的路径出现在 both 两侧,很可能是文件被简单地修改了,Git 以该假设开始。如果一条路径出现在 A 中但不在 B 中,并且一些其他路径出现在 B 中但不在 A 中,则这两条路径将配对并提供给 重命名检测器(如果已启用)。
所有内部差异共享一堆代码,并且还共享文档。点击以上链接之一,搜索-M或--find-renames:
-M[n]
--find-renames[=n]
检测重命名。如果指定了 n,则它是相似度索引的阈值(即添加/删除量与文件大小相比)。例如,-M90% 表示如果文件中超过90% 没有更改,Git 应该将删除/添加对视为重命名。如果没有% 符号,则该数字将被读取为分数,其前有一个小数点。即,-M5 变为 0.5,因此与 -M50% 相同。同样,-M05 与 -M5% 相同。要将检测限制为精确重命名,请使用-M100%。默认相似度索引为50%。
通过在您的配置中将diff.renameLimit 设置为0,可以默认启用重命名检测器。否则,目前默认禁用,但将在即将发布的 Git 版本中默认启用(我不确定是哪一个)。
有关相似度匹配的更多详细信息,请参阅this answer from Edward Thomson。
一旦重命名检测器确定某些 A 到 B 更改是重命名,它会将两个名称都从“仅在 A”和“仅在 B”列表中拉出。
添加和删除
运行重命名检测器(如果启用)后,仅在 A 端找到的所有文件都将被“删除”,而仅在 B 端找到的任何文件都将被“添加”。对于git status,整个过程到此结束(显示结果除外)。对于常规的git diff,当某些文件被修改或重命名和修改时,我们通常会继续产生实际的差异输出。
(请注意,所有 Git 的 diff 都共享所有这些机制,因此它们都会找到相同的重命名集,前提是您打开重命名检测并设置相同的阈值。这些也在 git merge 期间使用。)
旁注:重命名是检测到,而不是记录
许多其他版本控制系统(Mercurial、ClearCase、Perforce)要求您向它们注册文件重命名:hg mv 等等。这是因为他们记录每次提交时的重命名。执行此操作的系统必须为每个文件提供某种标识符(这可能是 ClearCase 中的真实对象 ID,或者只是“它在当前提交中的名称”,然后在我们从提交到提交时根据需要进行修改) .该系统的优点是无论文件如何更改,VCS 都可以跟踪文件。一个缺点是你必须记录更改,并且一个文件被意外删除,然后复活,可以获得一个新的ID(参见ClearCase“邪恶双胞胎”)。
Git 只是重新发现重命名,每次它比较一个提交与另一个提交(或对索引的提交,或对工作树的索引等)。这意味着您没有没有使用git mv:您可以git rm --cached 旧路径和git add 新路径,以获得相同的效果。 (当然,您可以在更方便的时候使用git mv,这是大多数时候。但这与版本控制系统有很大的不同,版本控制系统会在每次签入或提交时记录目录修改:使用这些在系统中,您必须调用 VCS 特定的 mv 命令,例如 hg mv 或 cleartool mv,以通知 VCS 文件已移动,而不是让 VCS 稍后找出它。)