Linus Torvalds说Git“从不”跟踪文件是什么意思?


283

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

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

这里的成绩单。)

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

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


20
reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git- “对于您目前的位置,我怀疑更重要的是要意识到,Git向用户呈现文件与内部处理文件之间存在差异。正如呈现给用户的快照一样,快照包含完整的文件,不仅是差异文件,而且在内部,是的,Git使用差异文件来生成可有效存储修订的打包文件。” (这与例如Subversion形成鲜明的对比。)
user2864740

5
Git不跟踪文件,它跟踪变更集。大多数版本控制系统都会跟踪文件。作为说明如何/为什么如此重要的一个示例,请尝试将一个空目录检入git(spolier:您不能,因为这是一个“空”的变更集)。
艾略特·弗里施

12
@ElliottFrisch听起来不对。您的描述更接近darcs。Git存储快照,而不存储变更集。
melpomene '19

4
我认为他的意思是Git不会直接跟踪文件。文件包含其名称和内容。Git将内容跟踪为斑点。仅给定一个Blob,您无法确定其对应的文件名是什么。它可能是在不同路径下具有不同名称的多个文件的内容。路径名和Blob之间的绑定在树对象中描述。
ElpieKay

3
相关内容:Randal Schwartz 对Linus演讲跟进(也是Google Tech演讲)-“ ... Git的真正含义是……Linus说的不是Git”。
彼得·莫滕森

Answers:


316

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

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

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


4
您可以补充一点,这就是为什么在CVS和Subversion中可以像$Id$在文件中那样使用标记的原因。在git中,这是行不通的,因为设计是不同的。
gerrit

58
内容并没有像您期望的那样绑定到文件。尝试将一个文件的80%的代码移到另一个文件。即使您只是在现有文件中移动代码,Git也会自动检测到文件移动+ 20%的变化。
allo

13
@allo的一个副作用是,git可以做其他事情不能做的事情:当两个文件合并并且您使用“ git blame -C”时,git可以查看两个历史记录。在基于文件的跟踪中,您必须选择哪个原始文件是真实的原始文件,其他所有行都是全新的。
Izkata

1
@ allo,Izkata-它是查询实体,它通过在查询时分析回购内容(提交历史记录以及引用的树和Blob之间的差异)来解决所有问题,而不是要求提交实体及其人类用户正确地指定或综合在提交时获取此信息-仓库工具开发人员也不能在部署工具之前设计和实现此功能以及相应的元数据架构。Torvalds认为,这种分析只会随着时间的流逝而变得更好,而且从第一天开始,每个 git repo的所有历史记录都会受益。
杰里米(Jeremy)

1
@allo Yep,要弄清楚git在文件级别上不起作用的事实,您甚至不必一次将所有更改提交到文件中;您可以提交任意行范围,同时在提交之外保留文件中的其他更改。当然,该用户界面并不是那么简单,因此大多数人都不会这样做,但很少有它的用途。
阿尔文·汤普森

103

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

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

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

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

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

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

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

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

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


要添加的另一件事是,这允许Git执行类似浅克隆的操作。它只需要检索头提交及其引用的所有blob。它不需要通过应用更改集来重新创建文件。
Wes Toleman

@WesToleman:它绝对使这更容易。Mercurial存储三角洲,偶尔会重置,而Mercurial的人们打算在此添加浅表克隆(由于“重置”的想法,这是可能的),但他们实际上还没有这样做(因为这更多是技术上的挑战)。
torek '19

@torek我有一个关于你对Git的应答文件历史请求,但我认为它值得其固有的问题,说明一个疑问: stackoverflow.com/questions/55616349/...
西蒙·拉米雷斯阿马亚

@torek感谢您提供本书的链接,但我没有看到其他类似的东西。
gnarledRoot

17

令人困惑的地方在这里:

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

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

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

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

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

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

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

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

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

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

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

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

一个git repo很像那样。

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

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

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

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

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

因此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的方式来跟踪它。


5
我想我知道您想要说的是什么,但是“在Git中,没有直接的命令可以执行此操作”与您所链接的问题的答案直接矛盾。虽然版本控制确实发生在整个存储库的级别,但是在Git 中通常有很多方法可以实现任何功能,因此使用多个命令来显示文件的历史记录并不能证明很多。
Joe Lee-Moyet

我浏览了您所链接问题的前几个答案,它们全部使用git log或在此基础上构建的某个程序(或具有相同功能的别名)。但是,即使有很多不同的方式,如Joe所说,这也适用于显示分支历史。(也是git log -p <file>内置的,并且确实做到了)
Voo

您确定SVN内部存储每个文件的更改吗?我已经有一段时间没有使用它了,但我隐约记得记得有一些文件名为版本ID,而不是反映项目文件的结构。
Artur Biesiadowski

3

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

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

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

默认情况下,我们用于--others --directory显示不感兴趣的目录(以引起用户的注意),而不显示其内容(以使输出整洁)。
显示空目录没有任何意义,因此请通过--no-empty-directory

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

几年后,在2011年1月,commit 8fe533(Git v1.7.4)对此进行了回应:

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

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

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

跟踪内容是git怪的原因,很早(Git 1.4.4,2006年10月,提交cee7f24)才更有表现:

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

(跟踪内容)也是git在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)。

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

  • 而不是将文件存储在collide_path~HEADcollide_path~MERGE,而是将文件双向合并并记录在collide_path
  • 与其在索引中记录重命名的文件存在的版本的版本(从而忽略对历史文件的更改而不进行重命名),我们对重命名的文件进行三步内容合并路径,然后将其存储在阶段2或阶段3。
  • 请注意,由于每个重命名的内容合并可能会有冲突,然后我们必须合并两个重命名的文件,因此最终可能会出现嵌套冲突标记。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.