如果你熟悉几乎任何其他版本控制系统(VCS),那么很难理解Git对文件历史记录的处理方式。
事实上,Git没有文件历史记录。在这里,它可能是VCS中独一无二的(尽管我没有使用过许多更晦涩的VCS)。它最接近的表兄弟Mercurial有文件历史记录:Mercurial中添加的每个文件都被分配一个唯一的编号,称为“清单”,并确定文件的身份。如果更改文件的名称或整个目录中的文件,则它们保留其身份,因为此信息存在于清单中。
Git完全放弃了这种概念。Git根本没有文件历史记录。Git只有提交。
每个提交存储源树的完整快照。每个提交还具有一些父提交,通常只有一个。这更像传统的基于提交的VCS:可以跟踪各个提交,或查看文件历史记录。但是由于Git没有文件历史记录,它唯一拥有的就是提交历史记录。
为了实现`git log --follow`和其他有用的项目,Git提供了重命名检测,而不是文件历史记录。Git可以查看任何一个特定的提交,并将该提交与其父提交进行比较,或者对于合并提交,与所有父提交进行比较。当它进行此比较时,它提供了检测通过该提交重命名的文件的选项:在父级中具有一个名称但在子级中具有不同名称的文件。
甚至可以在比较两个任意提交时使用此重命名检测,而这两个提交不仅是父子关系。执行以下操作:
git diff --find-renames $hash1 $hash2
比较两个提交,当“路径为
a/b/c.txt
的文件在
$hash1
中与路径为
d/e/f.log
的文件在
$hash2
中非常相似时,Git 可能会声称该文件已被重命名(然后可能被修改)。但是要记住,Git 仅仅是“合成一种方式将第一个文件转换为第二个文件”。在这两个提交中的实际文件以这种方式永久存储。它们永远不会改变:只要这些提交存在,这两个文件就以这种方式存储在这两个提交中。除非你想让它们相关,否则这两个文件实际上没有任何关系。通过比较它们的相似性,Git 才“发现”了重命名。给 Git 不同的“相似性”标准 - 例如
-M75%
而不是
-M50%
- Git 可能会选择一组不同的“足够相似”的文件。
没有任何提交发生过任何变化。它们都冻结在时间中。但是使用不同的“重命名阈值”,“断裂阈值”等,Git 可能会匹配不同的路径名。如果给定
--no-renames
,Git 永远不会匹配不同的路径名(尽管它仍将匹配相同名称的文件)。
(这种动态重命名检测在合并时非常重要,因为合并运行了两个
git diff --find-rename
操作,从合并基础提交到正在合并的两个分支提示提交中的每个提交。如果 Git 找到了一次重命名,它就会相信。如果它没有找到一个重命名,它就会认为基础文件已被删除,并且在提示中创建了一个不同的文件。您可以控制重命名阈值,但是至少在 Git 版本(2.15)中,您无法设置断裂或复制阈值。)
这个含义对于合并提交来说不太清楚,因为有多个父级:在父级#1中,文件
child.txt
的名称为
p1.txt
,而在父级#2中则为
p2.txt
,这意味着什么?传统的版本控制系统具有唯一的内部编号系统,用于确定文件标识,因此在这里分配了一个明确的含义,但实际上,这个含义并不总是有用的,Linus Torvalds 在这里的选择是完全放弃这个概念,可能部分是对此的反应。
--follow
的实现很差,它一次只能处理一个文件名(例如,你不能--follow
整个目录中的文件),而且在跟踪合并时效果不佳。这可能就是为什么它不是默认选项的原因。至于历史重写,它实际上是创建了一组新的提交(而不是更改旧的提交):你基本上是将原始存储库复制到一个新的“好像它们一直以那种方式命名”的存储库中,然后放弃原始存储库。 - torek