这实际上是一个很好的问题。
提交的内部存储形式部分相关,所以让我们考虑一下。个人提交实际上很小。这是 Git 的 Git 存储库中的一个,即 commitb5101f929789889c2e536d915698f58d5c5c6b7a:
$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800
Fourth batch after 2.20
Signed-off-by: Junio C Hamano <gitster pobox.com>
(the sed 's/@/ /'
也许只是为了减少 Junio Hamano 必须收到的垃圾邮件数量:-) )。正如您在这里所看到的,提交对象通过另一个提交的哈希 ID 引用其父提交对象,a562a11983...
。它还指的是一个tree对象的哈希ID,树对象的哈希ID开头3f109f9d1a
。我们可以使用以下命令查看这个树对象git cat-file -p
too:
$ git cat-file -p 3f109f9d1a | head
100644 blob de1c8b5c77f7566d9e41949e5e397db3cc1b487c .clang-format
100644 blob 42cdc4bbfb05934bb9c3ed2fe0e0d45212c32d7a .editorconfig
100644 blob 9fa72ad4503031528e24e7c69f24ca92bcc99914 .gitattributes
040000 tree 7ba15927519648dbc42b15e61739cbf5aeebf48b .github
100644 blob 0d77ea5894274c43c4b348c8b52b8e665a1a339e .gitignore
100644 blob cbeebdab7a5e2c6afec338c3534930f569c90f63 .gitmodules
100644 blob 247a3deb7e1418f0fdcfd9719cb7f609775d2804 .mailmap
100644 blob 03c8e4c613015476fffe3f1e071c0c9d6609df0e .travis.yml
100644 blob 8c85014a0a936892f6832c68e3db646b6f9d2ea2 .tsan-suppressions
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42 COPYING
(该树有相当多的数据,所以我在这里只复制了前十行)。
在树内部,您可以看到模式 (100644
), 类型 (blob
——这是模式所暗示的,并且也记录在内部 Git 对象中;它实际上并没有存储在树对象中),哈希ID(de1c8b5c77f...
) 和姓名 (.clang-format
)的一个斑点。您还可以看到tree
可以参考补充tree
对象,就像这样的情况.github
子树。
如果我们获取这个特定的 blob 对象哈希 ID,我们也可以通过哈希 ID 查看该对象的内容:
$ git cat-file -p de1c8b5c77f | head
# This file is an example configuration for clang-format 5.0.
#
# Note that this style definition should only be understood as a hint
# for writing new code. The rules are still work-in-progress and does
# not yet exactly match the style we have in the existing code.
# Use tabs whenever we need to fill whitespace that spans at least from one tab
# stop to the next one.
#
# These settings are mirrored in .editorconfig. Keep them in sync.
(由于文件相当长,我再次将副本截断为 10 行)。
只是为了说明,让我们看一下.github
子树也是:
$ git cat-file -p 7ba15927519648dbc42b15e61739cbf5aeebf48b
100644 blob 64e605a02b71c51e9f59c429b28961c3152039b9 CONTRIBUTING.md
100644 blob adba13e5baf4603de72341068532e2c7d7d05f75 PULL_REQUEST_TEMPLATE.md
那么,Git 对这些内容所做的就是根据需要递归地读取tree来自提交的对象。 Git 会将这些读入一个数据结构,它称之为index or cache。 (从技术上讲,内存版本是cache数据结构,尽管 Git 文档对于何时使用哪些名称往往有点松散。)因此通过读取提交构建的缓存b5101f929789889c2e536d915698f58d5c5c6b7a
例如,会说那个名字.clang-format
有模式100644
和 blob 哈希de1c8b5c77f7566d9e41949e5e397db3cc1b487c
,而名字.github/CONTRIBUTING.md
有模式100644
和 blob 哈希64e605a02b71c51e9f59c429b28961c3152039b9
.
请注意,各种名称组件(.github
plus CONTRIBUTING.md
)实际上已经被加入到内存缓存中。 (在磁盘格式中,它们是通过算法技巧进行压缩的。)
帮助 Git 匹配文件名的内存缓存
最后,内部(内存中)缓存保存 元组。如果你要求 Git 比较提交b5101f929789889c2e536d915698f58d5c5c6b7a
对于其他提交,Git 也会将其他提交读取到内存缓存中。其他缓存有一个名为.github/CONTRIBUTING.md
,或者没有。
如果两个提交都有包含以下内容的文件相同的名字,Git 假设(为了 Git 现在正在进行的这一比较,见下文)这些是相同的文件。无论 blob 哈希值是否相同,都是如此。
我们在这里回答的真正问题与identity。在版本控制系统中,文件的标识确定该文件是否是两个不同版本中的“相同”文件(但是版本控制系统本身定义了版本)。这涉及身份的基本哲学问题,如这篇关于忒修斯之船思想实验的维基百科文章:我们怎么知道某事,甚至一些one,我们认为他们是谁或是什么?如果你在你和你的表弟鲍勃都很年轻的时候认识了他,然后你又遇到了一个叫鲍勃的人,他是你的表弟吗?那时你和他还很小;现在你长大了,年纪大了,经历也不同了。在现实世界中,我们从环境中寻找线索:鲍勃是你父母的兄弟姐妹的孩子吗?如果是这样,鲍勃可能is你很久以前见过的同一个表弟鲍勃,即使他(和你)现在看起来很不同。
当然,Git 不会做这些事情。在大多数情况下,两个文件都被命名的简单事实.github/CONTRIBUTING.md
足以将它们识别为“同一文件”。名字是一样的,所以我们就完成了。
git diff
提供额外服务
在我们的日常开发中,有时我们会遇到这样的情况:rename一份文件。一个名为a/b.c
可能renamed to d/e.f
or d/e.c
因为某些原因。
假设我们正在提交a123456
文件名为a/b.c
。然后我们开始承诺f789abc
。第二次提交没有a/b.c
但确实有一个d/e.f
。 Git 会简单地删除a/b.c
从我们的索引(缓存的磁盘形式)和工作树中,并填充一个新的d/e.f
进入我们的索引和工作树,一切都很好。
但是假设我们要求 Gitcompare a123456
with f789abc
. Git could只需告诉我们:改变a123456
to f789abc
, 消除a/b.c
并创建一个新的d/e.f
与这些内容。 That is what git checkout
做到了,这就足够了。但如果内容完全匹配怎么办?还有更多高效的Git 告诉我们:改变a123456
to f789abc
, 改名a/b.c
to d/e.f
.事实上,只要有正确的选择,git diff
will就这样做:
git diff --find-renames a123456 f789abc
Git 是如何做到这一点的呢?答案在于计算文件标识.
查找文件标识
假设提交L(左侧)有一些文件(a/b.c
)不在提交中R(对于右侧)。进一步假设提交R有一些文件(d/e.f
)不在提交中L。而不是立即告诉我们:您应该删除 L 文件并使用 R 文件,Git 现在可以比较contents两个文件的。
由于 Git 对象哈希的性质——它们是完全确定性的,基于文件内容——它是真的很容易让 Git 检测到a/b.c
in L100% 相同d/e.f
in R。在这种特殊情况下,它们将具有完全相同的哈希 ID!所以 Git 会这样做:如果有某个文件消失了L以及出现在的其他一些文件R,并且 Git 被要求find重命名时,Git 检查哈希 ID 匹配。如果它找到一些文件,它会将这些文件配对(并将它们从不匹配文件的队列中取出,该队列保存来自L and R,是“重命名检测队列”)。
那些具有不同名称的文件已被识别为同一个文件。毕竟,小表弟鲍勃和大表弟鲍勃是一样的——只不过在这种情况下,你们俩都还需要很小。
所以,如果这个重命名检测没有yet将文件配对L与一R,Git会更加努力。现在它将提取实际的斑点,并计算某种“匹配百分比”。这使用了一个复杂的小算法,我不会在这里描述,但如果两个文件中有足够多的子字符串匹配,Git 将声明文件为 50、60、75 或更多百分比similar.
在重命名队列中找到一对彼此相似度为 72% 的文件后,Git 继续将这些文件与所有其他文件进行比较。如果它发现这两者之一与另一个相似度为 94%,则该相似配对会击败 72% 相似配对。如果不是,72% 的相似度就足够了——至少 50%——所以 Git 会将这两个文件配对并声明它们具有相同的身份。
无论如何,如果比赛足够好的话and是所有未配对文件中最好的一个,则采用该特定匹配。再说一遍,小鲍勃表弟毕竟和鲍勃大表弟是一样的。
运行此测试后all不匹配的文件对,git diff
获取匹配结果并调用这些文件renamed。同样,只有当您使用时才会发生这种情况--find-renames
(or -M
),并且您可以设置临界点如果您愿意,可以选择 50% 以外的其他值。
打破不正确的匹配
The git diff
命令提供另一种服务。请注意,我们开始于assuming如果提交L and R有相同的文件name,这些文件是相同的file,即使内容不同。但如果他们不是呢?如果什么file
in L被重命名为bettername
in R, and有人创建了一个新的file
in R?
为了处理这个问题,git diff
提供-B
(或“中断配对”)选项。和-B
实际上,如果一开始通过名称标识的文件太不匹配,那么它们的配对就会被破坏。dis-相似的。也就是说,Git 将检查两个 blob 哈希值是否匹配,如果不匹配,Git 将计算相似度索引。如果指数下跌below达到某个阈值后,Git 将中断配对并将两个文件放入重命名检测队列中,然后再运行--find-renames
样式重命名检测器。
作为一个特殊的变化,Git 将re-pair损坏的配对,除非它们非常不同以至于您不希望这样做。因此对于-B
你实际上指定two相似度阈值:第一个数字是何时暂时中断配对,第二个数字是何时永久中断配对。
git merge
uses git diff --find-renames
当你使用git merge
要执行三向合并,需要三个输入:
- 合并基础提交,它是两个提示提交的祖先;和
- 左右提交,
--ours
and --theirs
.
Git 运行two git diff
内部命令。一个将基数与L另一个将基数与R.
这两个差异都与--find-renames
已启用。如果从基数到L找到一个重命名,Git 知道使用changes显示在该重命名上。同样,如果从基数到R找到重命名后,Git 就知道要使用这些更改。它将组合两组更改,并尝试(但通常会失败)组合两个重命名(如果两个差异都显示重命名)。
git log --follow
还使用重命名检测器
使用时git log --follow
,Git 遍历提交历史记录,一次一个提交对(子项和父项),从父项到子项进行差异。它会打开一种有限形式的重命名检测代码,以查看您正在使用的文件是否是--follow
-ing 在该提交对中被重命名。如果是这样,尽快git log
移动到父级,它更改它查找的名称。这一技术工作得相当好,但在合并时存在一些问题(因为合并提交有多个父项)。
结论
文件标识这就是全部。因为 Git 事先不知道该文件a/b.c
提交中L是或不是“相同”的文件作为文件d/e.f
提交中R,Git可以使用重命名检测决定。在某些情况下——例如检查提交L or R——这一点也不重要。在某些情况下,例如区分两个提交,这很重要,但仅限于我们作为人类试图理解发生了什么。但在某些情况下,例如合并,很重要.