【问题标题】:How does git status work internally?git status 在内部是如何工作的?
【发布时间】:2016-04-28 17:59:15
【问题描述】:

git object model 开始,文件和文件夹通过它们的 sha1 哈希值保存到 .git 文件夹中的位置。

git 如何在内部知道文件是否已被删除、添加或编辑(具体来说,它如何计算您在键入 git status 时看到的更改)?系统是否纯粹根据 sha1 确定此信息?

【问题讨论】:

  • 基本上,它扫描磁盘。如果它看到一个新文件,则报告为“未跟踪”;如果 repo 中有一个不在树中的文件,那么它会将其报告为“已删除”。如果文件同时存在于 repo 和磁盘上,则它计算磁盘上文件的 sha-1,如果它与 repo 中的 sha-1 不同,则报告为“已修改”。执行了很多缓存和其他技巧来保持这个速度。
  • 我已经给你写了一个详细的答案,并附有截图来澄清答案。如果有什么不够清楚,请随时询问。
  • 索引列出了跟踪的路径名,并在每条路径上跟踪内容的 sha1 以及元数据和有关动态合并的源数据等内容的注释。 git status 扫描文件系统并将当前元数据与索引进行比较,只要它检测到潜在的不匹配,它就会在必要时重新扫描内容。

标签: git


【解决方案1】:

答案有点太长了,需要一段时间才能写出来,所以这里是摘要。

简短的回答是 git 使用SHA-1 来检查跟踪更改,但文件名存储在其他地方。


内容存储在pack(*1-阅读下文),而名称存储在idx中。
当你运行git status 时,git 检查 idx 文件(元数据)中是否已经有这个路径,并根据结果决定它是否是一个新文件。

如果不是新文件,git 比较 SHA-1 以跟踪更改。


为什么我必须使用 git mv 来移动文件而不是简单的 mv?

当您执行git status git 时,搜索您的工作目录,寻找 idx 文件中的 "已注册" 路径与您的工作目录之间的匹配项。

当您移动带有mv 的文件时,您的工作目录没有 git 存储的“原始”路径,并且由于 git 无法再找到“注册”路径,该文件被标记为已删除。

但是在同一时间 git 看到一个新文件,new 路径你刚刚将文件移动到,所以 new 文件将被标记为新文件。

另一方面,当使用git mv 时,git 会更新元数据以指向新名称,并且内容将被标记为rename。在这种情况下,git 会更新 idx 文件中文件的注册路径。
如果您移动和更新,它也会被标记为重命名 + 修改。


如何找出文件的 SHA-1 是什么?

使用 git ls-tree 内部命令找出文件的 SHA-1 是什么。


注意

当您将内容添加到暂存区域时,Git 开始跟踪内容。
添加文件后,git将以下信息存储在文件中

[blob][1 white space][content length][null][content]

If you have a file with the string `hello` it will look like this:
blob 5\0Hello

现在 git 计算这个文件的 SHA-1(使用 sha1sumn)用 z-lib 压缩它,并将这个 SHA-1 的文件保存为 `.git/objects' 的名称。

当 git 打包存储库时,它将进入打包文件。

如何查看文件内容?

由于文件是用 z-lib 压缩的,我们有几个选项来获取内容:

  • 第一个选项是查看文件中存储的压缩包
  • 二是查看解压后的内容(如上所述)
  • 仅查看文件内容,不包含任何 git 元数据。

为了表明git实际上使用了上面描述的内容,这里与git在后台执行计算SHA-1的命令相同


这是提交以及它如何处理文件名的说明。

(http://shafiulazam.com/gitbook/1_the_git_object_model.html)

【讨论】:

  • 您将 git 索引(跟踪路径)和包文件的索引(提供对象 ID 到包文件中位置的映射)混为一谈。
  • 我同意,它太长了,所以我试图总结它而不涉及太多细节。随意编辑和添加您的内容。应用
【解决方案2】:

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-indexgit-diff-treegit-diff-filesgit 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 中的foobar)和另一个对象ID。如果组件表示一个目录,则对象 ID 定位另一个树对象;如果它表示一个文件,则对象 ID 定位一个 blob 对象。无论哪种情况,ID 都是 Git 的内部名称,这使 Git 能够在存储库中找到它。

(对于索引/暂存区域本身而言,情况并非如此,其格式没有很好地记录。它是所有文件的平面列表,具有完整路径名,但也使用名称压缩技术,因此@ 987654353@后跟VeryLongDirectory/AnotherLongDirectory/baz不必每次都拼出VeryLongDirectory/AnotherLongDirectory。)

(树对象还存储 Git 在提取时应分配给文件的模式,除了在树对象中,每个文件模式都只有 100644100755;最后的 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 mvcleartool mv,以通知 VCS 文件已移动,而不是让 VCS 稍后找出它。)

【讨论】:

  • 投票支持我从中学到的惊人的东西。但我有一个问题:最后一段 - you don't have to use git mv。如果您在没有 git mv 的情况下重命名文件 (mv),则 git 会将其作为新文件进行跟踪。 (参见这个例子 - i.stack.imgur.com/yCUeT.png)所以根据你的 git 它应该被视为重命名,但它没有。我在这里想念什么
  • 如果你在不通知 Git 的情况下重命名文件(例如,mv foo bar),除非你告诉它这样做,否则 Git 不会知道 git add bargit rm --cached foo。一旦你 do 告诉它,Git 将再次被赶上。使用git mv foo bar 将重命名文件 告诉 git 取出旧的索引条目并添加新的索引条目,所有这些都在一个方便的传递中,所以这是要走的路——只是技术上不是 必需,因为 Git 会在您稍后处理其余的事情后解决。 (在 Mercurial 中,hg mv 是必需的,但如果您已经在 mved 之外的 hg 之外拼写了 hg mv --after,则可以拼写它。[继续]
  • [...continued] 这是 Git 和 Mercurial 之间的一个显着区别:Git reconstructs 更改,而 Mercurial tracks 更改。 Mercurial 的提交实际上是变更集而不是快照。)
  • 好的,我不清楚你的意思是自己添加//删除文件。从您的回答中,听起来 git 会自行跟踪它,而我们知道它不会。它将需要 2 个单独的手动命令。
猜你喜欢
  • 1970-01-01
  • 2015-05-03
  • 2022-01-06
  • 2015-10-06
  • 2019-04-12
  • 2015-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多