【问题标题】:What does Linus Torvalds mean when he says that Git "never ever" tracks a file?当 Linus Torvalds 说 Git“从不”跟踪文件时,他是什么意思?
【发布时间】:2019-08-31 08:00:23
【问题描述】:

当被问及 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 等其他源代码控制系统有何不同?

【问题讨论】:

  • reddit.com/r/git/comments/5xmrkv/what_is_a_snapshot_in_git - “对于你目前所处的位置,我怀疑 更重要的是要意识到 Git 向用户呈现文件的方式与其内部处理文件的方式之间存在差异“ (这与 Subversion 等形成鲜明对比。)
  • Git 不跟踪文件,它跟踪 changesets。大多数版本控制系统跟踪文件。作为一个如何/为什么这很重要的例子,尝试将一个空目录签入 git(spolier:你不能,因为那是一个“空”变更集)。
  • @ElliottFrisch 这听起来不对。您的描述更接近于例如darcs 确实如此。 Git 存储快照,而不是变更集。
  • 我认为他的意思是 Git 不会直接跟踪文件。文件包括其名称和内容。 Git 将内容作为 blob 进行跟踪。仅给定一个 blob,您无法知道其对应的文件名是什么。它可能是不同路径下具有不同名称的多个文件的内容。路径名和 blob 之间的绑定在树对象中描述。
  • 相关:Randal Schwartz'followup to Linus' talk(也是 Google 技术讲座)-“...... Git 的真正意义...... Linus 说了 Git 不是什么”。

标签: git version-control


【解决方案1】:

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

情况并非总是如此,只有 Git 1.4(2006 年 5 月)通过 commit 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 开始将未跟踪的内容限制在非空文件夹中,commit 2074cb0

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

跟踪内容使 git blame 能够在很早的时候(Git 1.4.4,2006 年 10 月,commit cee7f24)提高性能:

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

这(跟踪内容)也是将 git add 放入 Git API 的原因,使用 Git 1.5.0(2006 年 12 月,commit 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,commit 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 月,最近改进了冲突解决方案。
commit bbafc9c 通过改进重命名/重命名(2to1)冲突的处理进一步说明了考虑文件内容的重要性:

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

【讨论】:

    【解决方案2】:

    令人困惑的地方在这里:

    Git 永远不会将它们视为单独的文件。 Git 认为一切都是完整的内容。

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

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

    如果您更改文件内容的状态,则其哈希值会更改。但是如果它的哈希值改变了,与文件名内容相关的哈希值也会改变。这反过来又改变了“目录树”的哈希值。

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

    它被组织成一个树结构,带有指向 blob 或其他树的(不可变的、可重用的)指针,但从逻辑上讲,它是整个树的全部内容的单个快照。 git 数据库中的 表示 不是平面数据内容,但从逻辑上讲,它是它的所有数据,仅此而已。

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

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

    如果您围绕它构建了一个应用程序,那么一个文档就是一堆页面,这些页面具有层、组和对象。

    当你想改变一个对象时,你必须为它创建一个全新的组。如果要更改组,则必须创建一个新层,这需要一个新页面,这需要一个新文档。

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

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

    git repo 很像。

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

    这些父更改包含它们的父更改,一直返回。

    git repo 中包含 history 的部分是更改链。该链在“目录”树上方的级别对其进行更改-从“目录”树中,您无法唯一地访问更改集和更改链。

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

    有时文件不见了;但是,“目录”树可能有另一个文件具有相同的内容(相同的哈希码),所以我们可以这样跟踪它(注意;这就是为什么你想要一个提交移动文件与一个提交-编辑)。或者是相同的文件名,并且经过检查文件是否足够相似。

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

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

    【讨论】:

      【解决方案3】:

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

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

      当 Git 提到跟踪一个文件时,它只是意味着它将被包含在项目的历史中。 Linus 的演讲不是指在 Git 上下文中跟踪文件,而是将 CVS 和 RCS 模型与 Git 中使用的基于快照的模型进行对比。

      【讨论】:

      • 你可以补充一点,这就是为什么在 CVS 和 Subversion 中,你可以在文件中使用像 $Id$ 这样的标签。同样在 git 中不起作用,因为设计不同。
      • 内容并没有像您期望的那样绑定到文件。尝试将一个文件的 80% 的代码移动到另一个文件。 Git 会自动检测文件移动 + 20% 的更改,即使您只是在现有文件中移动了代码。
      • @allo 这样做的副作用是,git 可以做其他人做不到的一件事:当合并两个文件并且您使用“git blame -C”时,git 可以查看两个历史记录.在基于文件的跟踪中,您必须选择哪些原始文件是真正的原始文件,而其他行都显示为全新的。
      • @allo, Izkata - 查询实体通过在查询时分析 repo 内容(提交历史和引用的树和 blob 之间的差异)来解决所有这些问题,而不是要求 提交实体 及其人类用户在提交时正确指定或综合此信息 - 也不要求 repo 工具开发人员在部署工具之前设计和实现此功能和相应的元数据模式。 Torvalds 认为,随着时间的推移,这种分析只会变得更好,并且从第一天起每个 git repo 的所有历史都会受益。
      • @allo 是的,为了强调 git 不能在文件级别上工作的事实,您甚至不必一次提交文件中的所有更改;您可以提交任意范围的行,同时将文件中的其他更改留在提交之外。当然,UI 并没有那么简单,所以大多数人都不会这样做,但它确实很少有它的用途。
      【解决方案4】:

      “git 不跟踪文件”基本上意味着 git 的提交由将树中的路径连接到“blob”的文件树快照和跟踪 commits 历史记录的提交图组成。其他一切都是通过“git log”和“git blame”等命令即时重建的。这种重建可以通过各种选项告诉它应该多难查找基于文件的更改。默认启发式方法可以确定 blob 何时更改文件树中的位置而没有更改,或者文件何时与与以前不同的 blob 关联。 Git 使用的压缩机制并不关心 blob/文件的边界。如果内容已经在某个地方,这将使存储库的增长保持较小,而不会关联各种 blob。

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

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

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

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

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

      【讨论】:

        【解决方案5】:

        我同意brian m. carlson's answer 的观点:Linus 确实至少部分区分了面向文件和面向提交的版本控制系统。但我认为不止于此。

        my book 中,它已经停滞并且可能永远不会完成,我试图为版本控制系统提出一个 taxonomy。在我的分类中,我们感兴趣的术语是版本控制系统的原子性。查看当前第 22 页的内容。当 VCS 具有文件级原子性时,实际上每个文件都有一个历史记录。 VCS 必须记住文件的名称以及每个点发生的事情。

        Git 不这样做。 Git 只有一个提交历史——提交是它的原子性单位,而历史存储库中的一组提交。提交记住的是数据——一整棵文件名和每个文件的内容——加上一些元数据:例如,谁进行了提交,何时,为什么,以及内部 Git 哈希 ID提交的提交。 (正是这个父节点,以及通过读取所有提交及其父节点形成的有向循环图,存储库中的历史记录。)

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

        当你要求 Git 显示文件的历史时,使用:

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

        Git 真正在做的是遍历 commit 历史记录,这是 Git 拥有的唯一历史记录,但不会向您显示这些提交中的任何一个 除非:

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

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

        【讨论】:

        • 要添加的另一件事是,这允许 Git 执行诸如浅克隆之类的操作。它只需要检索头部提交和它引用的所有 blob。它不需要通过应用更改集来重新创建文件。
        • @WesToleman:它确实让这变得更容易了。 Mercurial 存储增量,偶尔会重置,虽然 Mercurial 的人打算在那里添加浅克隆(由于“重置”的想法,这可能是可能的),但他们实际上还没有这样做(因为这更像是一个技术挑战)。
        • @torek 我对你关于 Git 回答文件历史请求的描述有疑问,但我认为它值得自己提出正确的问题:stackoverflow.com/questions/55616349/…
        • @torek 谢谢你的书的链接,我没有看到其他类似的东西。
        • @Paolo 不;我需要在某个时候重新开始工作。
        【解决方案6】:

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

        这是一种看待它的方式。

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

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

        【讨论】:

        • 我想我明白你想说的话,但是“在 Git 中,没有直接的命令可以做到这一点”与你所链接的问题的答案直接矛盾。虽然版本控制确实发生在整个存储库的级别,但通常有很多方法可以在 Git 中实现任何事情,因此使用多个命令来显示文件的历史并不能证明太多。
        • 我浏览了您链接的问题的前几个答案,他们都使用git log 或在此基础上构建的一些程序(或一些做同样事情的别名)。但即使有很多不同的方式,正如乔所说,展示分支历史也是如此。 (同样git log -p <file> 是内置的并且正是这样做的)
        • 您确定 SVN 在内部存储每个文件的更改吗?我已经有一段时间没有使用它了,但我隐约记得有文件命名为版本 id,而不是反映项目文件结构。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-09
        • 2011-06-22
        • 1970-01-01
        • 2013-09-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多