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

时间:2019-04-09 23:40:15

标签: git version-control

当被问及Git在Tech Talk at Google in 2007(43:09)可以处理多少个文件时,引用Linus Torvalds:

  

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

(解说词here。)

但是,当您进入the Git book时,首先会被告知Git中的文件可以被跟踪未跟踪。此外,在我看来,整个Git体验都面向文件版本控制。使用git diffgit status时,将按文件显示输出。使用git add时,您还可以选择每个文件。您甚至可以基于文件查看历史记录,而且速度很快。

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

6 个答案:

答案 0 :(得分:311)

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

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

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

答案 1 :(得分:102)

我同意brian m. carlson's answer的看法:Linus确实至少部分地区分了面向文件的版本控制系统和面向提交的版本控制系统。但是我认为还有很多。

my book中,它停滞了并且可能永远都无法完成,我试图为版本控制系统提出taxonomy。在我的分类法中,我们感兴趣的术语是版本控制系统的 atomicity 。请参阅当前第22页。当VCS具有文件级原子性时,实际上每个文件都有历史记录。 VCS必须记住文件的名称以及在每个点上发生了什么。

Git不会那样做。 Git只有提交历史记录-提交是原子性的单位,而历史记录在存储库中的提交集合。提交记住的是数据(一个完整的树,其中充满了文件名和每个文件的内容)以及一些元数据:例如,进行提交的人,时间和原因以及内部Git哈希ID提交的 parent 提交的数量。 (正是这个父级,以及通过读取所有提交及其父级而形成的有向无环图,它是存储库中的历史记录。)

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

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

git log [--follow] [starting-point] [--] path/to/file

Git真正在做的是沿着 commit 历史记录,这是Git唯一的历史记录,但没有向您展示任何这些提交,除非: / em>

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

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

答案 2 :(得分:15)

令人困惑的地方在这里:

  

Git从未将这些文件视为单个文件。 Git认为一切都是完整的内容。

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

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

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

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

它以树结构进行组织,具有指向blob或其他树的(不可变,可重用)指针,但是从逻辑上讲,它是整个树的整个内容的单个快照。 git数据库中的表示形式不是固定的数据内容,但从逻辑上讲,它是其所有数据,而没有其他内容。

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

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

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

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

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

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

一个git repo很像这样。

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

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

git存储库中包含 history 的部分是该变化链。该更改链在“目录”树的上方 一级-从“目录”树中,您无法唯一地获得更改集和更改链。

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

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

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

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

答案 3 :(得分:12)

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

现在是存储库。 Git也有一个工作树,在这个工作树中有被跟踪和未被跟踪的文件。只有被跟踪的文件才记录在索引中(暂存区?缓存?),只有被跟踪的文件才将其放入存储库中。

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

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

与Subversion之类的系统不同的是,该系统记录而不是重建历史记录。如果没有记录在案,您就不会听到。

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

答案 4 :(得分:7)

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

这是一种查看方式。

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

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

答案 5 :(得分:3)

顺便说一句,导致无法跟踪空目录的原因是跟踪“内容”。
这就是为什么如果git rm文件夹的最后一个文件the folder itself gets deleted

情况并非总是如此,只有Git 1.4(2006年5月)通过commit 443f833强制实施了“跟踪内容”政策:

  

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

     

默认情况下,我们使用--others --directory来显示不感兴趣的目录(以引起用户的注意)而不显示其内容(以使输出整洁)。
  显示空目录没有意义,因此在这样做时请传递--no-empty-directory

     

提供-u(或--untracked)会禁用此整理功能,以使   用户将获得所有未跟踪的文件。

几年后的2011年1月,Git v1.7.4 commit 8fe533回响了这一点:

  

这与一般的UI哲学保持一致:git跟踪内容,而不是空目录。

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

  

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

跟踪内容是git怪的原因,很早(Git 1.4.4,2006年10月,commit cee7f24)表现得更好:

  

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

(跟踪内容)也是git在Git API(带有Git 1.5.0)中添加的内容(2006年12月,commit 366bfcb

  

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

     

这会使用适当的心理模型将索引的功能提前发挥出来,而根本不讨论索引。
  例如,请参见如何从git-add手册页中撤出所有技术讨论。

     
    

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

  

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

  

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

这也是为什么要递归地从目录中删除所有内容的原因,您需要传递-r选项,而不仅仅是传递目录名作为<path>(仍然是Git 1.5.0,{{3 }}。

看到文件内容而不是文件本身,就可以实现commit 9f95069中所述的合并方案(Git v2.18.0-rc0,2018年4月)

  

请考虑以下合并操作,并重命名/添加冲突:

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

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

commit 1de70db,Git v2.21.0-rc0,2018年12月,最近改进了冲突解决方案。
Commit 37b65ce进一步说明了通过改善重命名/重命名(2to1)冲突的处理方式来考虑文件内容的重要性:

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