当Linus Torvalds说Git“从不”跟踪文件时,这意味着什么?

Sim*_*aya 280 git version-control

当被问及Git 在2007年Google的技术演讲期间可以处理多少文件时,引用Linus Torvalds的(43:09):

…Git跟踪您的内容。它永远不会跟踪单个文件。您无法在Git中跟踪文件。您可以做的是,您可以跟踪一个文件的项目,但是如果您的项目有一个文件,请确保做到这一点并且可以做到,但是如果您跟踪10,000个文件,Git永远不会将它们视为单个文件。Git认为一切都是完整的内容。Git中的所有历史都基于整个项目的历史…

这里的成绩单。)

然而,当你潜入Git的书,你被告知的第一件事是,在Git的文件既可以跟踪未经跟踪。此外,在我看来,整个Git体验都面向文件版本控制。使用git diffgit status输出时按文件显示。使用时,git add您还可以根据每个文件进行选择。您甚至可以基于文件查看历史记录,而且速度很快。

该陈述应如何解释?在文件跟踪方面,Git与其他源代码控制系统(例如CVS)有何不同?

bk2*_*204 314

在CVS中,历史记录是按文件进行跟踪的。分支可能包含具有不同修订版本的各种文件,每个修订版本都有其自己的版本号。CVS基于RCS(修订控制系统),它以类似的方式跟踪单个文件。

另一方面,Git拍摄整个项目状态的快照。文件不会独立跟踪和版本控制;存储库中的修订是指整个项目的状态,而不是一个文件。

当Git提到跟踪文件时,仅表示该文件将包含在项目历史记录中。Linus的演讲不是指在Git上下文中跟踪文件,而是将CVS和RCS模型与Git中使用的基于快照的模型进行了对比。

  • 内容并没有像您期望的那样绑定到文件。尝试将一个文件的80%的代码移到另一个文件。即使您只是在现有文件中移动代码,Git也会自动检测到文件移动+ 20%的变化。 (56认同)
  • @allo的一个副作用是,git可以做其他事情不能做的事情:当两个文件合并并且您使用“ git blame -C”时,git可以查看两个历史记录。在基于文件的跟踪中,您必须选择哪个原始文件是真正的原始文件,其他所有行都是全新的。 (13认同)
  • 您可以补充一点,这就是为什么在CVS和Subversion中,可以在文件中使用`$ Id $`之类的标签的原因。由于设计不同,因此在git中不起作用。 (4认同)

tor*_*rek 102

我同意布赖恩·米。卡尔森的回答:Linus确实至少部分地区分了面向文件的版本控制系统和面向提交的版本控制系统。但是我认为还有更多。

在停滞不前的书中,我试图为版本控制系统提出一个分类法。在我的分类法中,我们这里感兴趣的术语是版本控制系统的原子性。请参阅当前第22页。当VCS具有文件级原子性时,实际上每个文件都有历史记录。VCS必须记住文件名以及在每个点上发生了什么。

Git不会那样做。Git仅具有提交历史记录-提交是原子性的单位,而历史记录存储库中的一组提交。提交记住的是数据(一个完整的树,里面充满了文件名和每个文件的内容)以及一些元数据:例如,提交的人,何时,为什么以及内部Git哈希ID提交的提交。(正是这种父母,并通过读取所有的提交和他们的父母形成的定向acycling图,这在一个仓库的历史。)

注意,VCS可以面向提交,但仍逐文件存储数据。这是一个实现细节,尽管有时很重要,但是Git也不这样做。相反,每个提交都记录一棵树,其中树对象编码文件模式(即,该文件是否可执行?)以及指向实际文件内容的指针。内容本身独立存储在blob对象中。就像提交对象一样,blob会获得其内容唯一的哈希ID-但与只能提交一次的提交不同,blob可以出现在许多提交中。因此,Git中的基础文件内容直接存储为Blob,然后间接存储 在其哈希ID(直接或间接)记录在提交对象中的树对象中。

当您要求Git使用以下方法显示文件的历史记录时:

git log [--follow] [starting-point] [--] path/to/file
Run Code Online (Sandbox Code Playgroud)

Git真正在做的事情就是浏览提交历史记录,这是Git唯一的历史记录,但是不会显示任何这些提交,除非:

  • 该提交是非合并提交,并且
  • 该提交的父级也具有该文件,但是父级中的内容不同,或者该提交的父级根本没有该文件

(但是其中一些条件可以通过其他git log选项进行修改,并且很难描述称为“历史简化”的副作用,该副作用使Git完全忽略了历史记录中的某些提交)。从某种意义上说,您在此处看到的文件历史记录并不完全存在于存储库中:相反,它只是真实历史记录的综合子集。如果使用其他git log选项,您将获得不同的“文件历史记录” !


Yak*_*ont 15

令人困惑的地方是:

Git从未将它们视为单独的文件。Git认为一切都是完整的内容。

Git经常在自己的仓库中使用160位哈希代替对象。文件树基本上是与每个文件(加上一些元数据)的内容相关联的名称和哈希的列表。

但是160位哈希值唯一地标识了内容(在git数据库的范围内)。因此,以哈希为内容的树包括处于其状态的内容

如果更改文件内容的状态,则其哈希也会更改。但是,如果其哈希值更改,则与文件名的内容关联的哈希值也会更改。依次更改“目录树”的哈希。

当git数据库存储目录树时,该目录树暗含并包括所有子目录的所有内容以及其中的所有文件

它以具有指向Blob或其他树的(不可变,可重用)指针的树结构进行组织,但是从逻辑上讲,它是整个树的整个内容的单个快照。该代表在git的数据库是不平坦的数据内容,但在逻辑上是所有的数据,并没有其他的。

如果将树序列化到文件系统,删除所有.git文件夹,并告诉git将树重新添加到其数据库中,您最终将不会向数据库中添加任何内容-该元素已经存在。

将git的哈希值视为指向不变数据的参考计数指针可能会有所帮助。

如果您以此为基础构建了一个应用程序,则文档就是一堆页面,这些页面具有层,层,组和对象。

要更改对象时,必须为其创建一个全新的组。如果要更改组,则必须创建一个新图层,该图层需要一个新页面,该页面需要一个新文档。

每次更改单个对象时,它都会产生一个新文档。旧文档继续存在。新旧文档共享它们的大部分内容-它们具有相同的页面(除了1)。该页面具有相同的层(除了1)。该层具有相同的组(除了1)。该组具有相同的对象(1个除外)。

同样,从逻辑上讲,我的意思是一个副本,但在实现方面,它只是指向同一不可变对象的另一个引用计数指针。

一个git repo很像那样。

这意味着给定的git changeset包含其提交消息(作为哈希码),其工作树以及其父更改。

这些父级更改一直包含其父级更改。

git repo包含历史记录的部分是该变化链。在一个水平的变化呢那链上面的“目录”树-从“目录”树,你不能唯一地得到一个变化集和变化的链条。

要找出文件发生了什么,请从变更集中的该文件开始。该变更集具有历史。通常,在该历史记录中,存在相同的命名文件,有时具有相同的内容。如果内容相同,则文件没有更改。如果不同,那就有所变化,需要做一些工作才能弄清楚到底是什么。

有时文件不见了;但是,“目录”树可能有另一个具有相同内容的文件(相同的哈希码),因此我们可以通过这种方式进行跟踪(请注意;这就是为什么您希望将提交文件与提交文件分开放置的原因) -编辑)。或相同的文件名,并在检查后文件足够相似。

因此git可以将“文件历史记录”拼凑在一起。

但是,此文件历史记录来自“整个变更集”的有效解析,而不是来自文件一个版本到另一个版本的链接。


小智 12

“ git不会跟踪文件”基本上意味着git的提交包括将树中的路径连接到“ blob”的文件树快照和跟踪提交历史的提交图。其他所有内容都是通过“ git log”和“ git blame”之类的命令即时重建的。可以通过各种选项来告知此重构,以查找基于文件的更改应该有多难。默认启发式方法可以确定Blob在文件树中的位置是否更改而没有更改,或者文件何时与以前不同的Blob相关联。Git使用的压缩机制对Blob /文件边界并不十分在意。如果内容已经存在,这将使存储库的增长变小,而无需关联各种Blob。

现在是存储库。Git也有一个工作树,在这个工作树中有被跟踪和未被跟踪的文件。只有被跟踪的文件才记录在索引中(暂存区?高速缓存?),只有在那里被跟踪的文件才可以进入存储库。

该索引是面向文件的,并且有一些面向文件的命令可以对其进行操作。但是最终在存储库中的内容只是以文件树快照以及相关的blob数据和提交祖先的形式提交。

由于Git不会跟踪文件历史记录和重命名并且其效率不依赖于它们,因此有时您必须尝试使用​​不同的选项几次,直到Git生成您对非平凡历史记录感兴趣的历史记录/差异/责备。

与Subversion这样的系统不同的是,该系统记录而不是重建历史。如果没有记录下来,就不会听到它。

实际上,我一次构建了一个差异安装程序,它通过将发布树检入Git来比较发布树,然后生成一个复制它们效果的脚本。由于有时整棵树都被移动了,因此这产生的差异安装程序比覆盖/删除所有可能产生的结果小得多。


小智 7

Git不会直接跟踪文件,而是跟踪存储库的快照,而这些快照恰好由文件组成。

这是一种查看方式。

在其他版本控制系统(SVN,Rational ClearCase)中,您可以右键单击文件并获取其更改历史记录

在Git中,没有直接的命令可以执行此操作。看到这个问题。您会对有多少种不同的答案感到惊讶。没有一个简单的答案,因为Git不会简单地跟踪文件,而不是以SVN或ClearCase的方式来跟踪文件

  • 我想我知道您要说的是什么,但是“在Git中,没有直接的命令可以做到这一点”与您所链接的问题的答案直接矛盾。虽然版本控制确实发生在整个存储库的级别,但是在Git中通常有很多方法可以实现*任何功能*,因此拥有多个命令来显示文件的历史记录并不能证明很多。 (5认同)

Von*_*onC 5

顺便说一下,跟踪“内容”是导致不跟踪空目录的原因。
这就是为什么,如果你 git rm 文件夹的最后一个文件,文件夹本身会被删除

情况并非总是如此,只有 Git 1.4(2006 年 5 月)通过提交 443f833强制执行“跟踪内容”策略:

git status: 跳过空目录,并添加 -u 以显示所有未跟踪的文件

默认情况下,我们--others --directory用来显示不感兴趣的目录(以引起用户的注意)而不显示其内容(以整理输出)。
显示空目录没有意义,所以--no-empty-directory当我们这样做时通过。

Giving -u(或--untracked) 禁用这种整洁,让用户获得所有未跟踪的文件。

几年后的 2011 年 1 月,提交 8fe533,Git v1.7.4与此相呼应:

这符合一般的 UI 哲学:git 跟踪内容,而不是空目录。

与此同时,在 Git 1.4.3(2006 年 9 月)中,Git 开始将未跟踪的内容限制为非空文件夹,提交 2074cb0

它不应列出完全未跟踪目录的内容,而应仅列出该目录的名称(加上尾随的“ /”)。

跟踪内容允许 git blame 很早就(Git 1.4.4,2006 年10 月,提交 cee7f24)性能更高:

更重要的是,它的内部结构旨在通过允许从同一个提交中采用多个路径来更轻松地支持内容移动(又名剪切和粘贴)。

那(跟踪内容)也是将 git add 放入 Git API 的原因,使用 Git 1.5.0(2006 年 12 月,提交 366bfcb

使“git add”成为索引的一流用户友好界面

这使用适当的思维模型将索引的力量放在前面,而根本不谈论索引。
例如,请参阅如何从 git-add 手册页中撤出所有技术讨论。

任何要提交的内容都必须添加在一起。
该内容是来自新文件还是修改后的文件并不重要。
您只需要使用 git-add 或通过提供 git-commit 来“添加”它-a(当然仅适用于已知文件)。

这就是git add --interactive使用相同的 Git 1.5.0(commit 5cde71d)成为可能的原因

做出选择后,用空行回答以暂存索引中选定路径的工作树文件的内容

这也是为什么要从目录中递归删除所有内容,您需要传递-r选项,而不仅仅是目录名称作为<path>(仍然是 Git 1.5.0,提交 9f95069)。

查看文件内容而不是文件本身是允许合并场景的原因,如commit 1de70db(Git v2.18.0-rc0,2018 年 4 月)

考虑以下合并与重命名/添加冲突:

  • A面:修改foo,添加无关bar
  • B面:重命名foo->bar(但不要修改模式或内容)

在这种情况下,原始 foo、A 的 foo 和 B 的三路合并bar将产生所需的路径名,bar其模式/内容与 A 的相同foo
因此, A 具有文件的正确模式和内容,并且它具有正确的路径名(即,bar)。

Commit 37b65ce,Git v2.21.0-rc0,2018 年 12 月,最近改进了冲突冲突解决方案。
承诺bbafc9c firther说明考虑文件的重要内容,通过提高重命名/重命名(情况下,2to1)冲突的处理:

  • 不是将文件存储在collide_path~HEADcollide_path~MERGE,而是将文件双向合并并记录在collide_path
  • 我们没有记录索引中重命名侧存在的重命名文件的版本(因此忽略对历史侧没有重命名的文件所做的任何更改),我们对重命名进行了三向内容合并路径,然后将其存储在第 2 阶段或第 3 阶段。
  • 请注意,由于每次重命名的内容合并可能会发生冲突,然后我们必须合并两个重命名的文件,因此我们最终可能会出现嵌套的冲突标记。