引用Linus Torvalds在2007年谷歌的Tech Talk中被问及Git可以处理多少文件时的回答:

Git会跟踪你的内容。它从来没有跟踪过一个文件。在Git中无法跟踪文件。你能做的是跟踪一个只有一个文件的项目,但如果你的项目只有一个文件,你当然可以这样做,但如果你跟踪10,000个文件,Git永远不会把它们视为单个文件。Git认为一切都是完整的内容。Git中的所有历史都是基于整个项目的历史…

(成绩单)。

然而,当您深入阅读Git书籍时,首先被告知的是Git中的文件既可以跟踪也可以不跟踪。此外,在我看来,整个Git体验都是面向文件版本控制的。当使用git的差异或git的状态输出显示在每个文件的基础上。当使用git add时,你也可以在每个文件的基础上进行选择。您甚至可以以文件为基础查看历史记录,速度非常快。

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


当前回答

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

我们可以这么看。

在其他版本控制系统(SVN, Rational ClearCase)中,您可以右键单击一个文件并获得它的变更历史。

在Git中,没有直接的命令可以做到这一点。看这个问题。你会惊讶于有这么多不同的答案。没有一个简单的答案,因为Git不像SVN或ClearCase那样简单地跟踪文件。

其他回答

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

但情况并非总是如此,只有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)性能更好的原因:

更重要的是,它的内部结构被设计为通过允许从同一个提交中获得多个路径来更容易地支持内容移动(即剪切和粘贴)。

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

make 'git add' a first class user friendly interface to the index This brings the power of the index up front using a proper mental model without talking about the index at all. See for example how all the technical discussion has been evacuated from the git-add man page. Any content to be committed must be added together. Whether that content comes from new files or modified files doesn't matter. You just need to "add" it, either with git-add, or by providing git-commit with -a (for already known files only of course).

这就是为什么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,添加不相关栏 边B:重命名foo->bar(但不修改模式或内容) 在这种情况下,原始foo、A的foo和B的bar的三向合并将导致bar的期望路径名具有与A的foo相同的模式/内容。 这样,A就有了文件的正确模式和内容,并且有了正确的路径名(即bar)。

提交37b65ce, Git v2.21.0-rc0, 2018年12月,最近改进了碰撞冲突解决方案。 通过改进rename/rename(2to1)冲突的处理,commit bbafc9c进一步说明了考虑文件内容的重要性:

Instead of storing files at collide_path~HEAD and collide_path~MERGE, the files are two-way merged and recorded at collide_path. Instead of recording the version of the renamed file that existed on the renamed side in the index (thus ignoring any changes that were made to the file on the side of history without the rename), we do a three-way content merge on the renamed path, then store that at either stage 2 or stage 3. Note that since the content merge for each rename may have conflicts, and then we have to merge the two renamed files, we can end up with nested conflict markers.

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

我们可以这么看。

在其他版本控制系统(SVN, Rational ClearCase)中,您可以右键单击一个文件并获得它的变更历史。

在Git中,没有直接的命令可以做到这一点。看这个问题。你会惊讶于有这么多不同的答案。没有一个简单的答案,因为Git不像SVN或ClearCase那样简单地跟踪文件。

"git does not track files" basically means that git's commits consist of a file tree snapshot connecting a path in the tree to a "blob" and a commit graph tracking the history of commits. Everything else is reconstructed on-the-fly by commands like "git log" and "git blame". This reconstruction can be told via various options how hard it should look for file-based changes. The default heuristics can determine when a blob changes place in the file tree without change, or when a file is associated with a different blob than before. The compression mechanisms Git uses don't care a whole lot about blob/file boundaries. If the content is somewhere already, this will keep the repository growth small without associating the various blobs.

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

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

由于Git不跟踪文件历史和重命名,它的效率也不依赖于它们,有时你必须尝试几次不同的选项,直到Git生成你感兴趣的历史/差异/错误。

这与Subversion这类记录历史而非重建历史的系统不同。如果没有记录,你就没机会听到。

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

我同意brian m. carlson的回答:Linus确实区分了面向文件和面向提交的版本控制系统,至少在一定程度上是这样。但我认为事情远不止于此。

在我的书中,我试图提出一个版本控制系统的分类学,这本书被搁置了,可能永远也写不完。在我的分类法中,这里我们感兴趣的术语是版本控制系统的原子性。看看现在第22页是什么。当VCS具有文件级原子性时,实际上每个文件都有历史记录。VCS必须记住文件的名称以及它在每个点上发生了什么。

Git doesn't do that. Git has only a history of commits—the commit is its unit of atomicity, and the history is the set of commits in the repository. What a commit remembers is the data—a whole tree-full of file names and the contents that go with each of those files—plus some metadata: for instance, who made the commit, when, and why, and the internal Git hash ID of the commit's parent commit. (It is this parent, and the directed acycling graph formed by reading all commits and their parents, that is the history in a repository.)

Note that a VCS can be commit-oriented, yet still store data file-by-file. That's an implementation detail, though sometimes an important one, and Git does not do that either. Instead, each commit records a tree, with the tree object encoding file names, modes (i.e., is this file executable or not?), and a pointer to the actual file content. The content itself is stored independently, in a blob object. Like a commit object, a blob gets a hash ID that is unique to its content—but unlike a commit, which can only appear once, the blob can appear in many commits. So the underlying file content in Git is stored directly as a blob, and then indirectly in a tree object whose hash ID is recorded (directly or indirectly) in the commit object.

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

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

Git真正做的是遍历提交历史,这是Git拥有的唯一历史,但不会显示任何这些提交,除非:

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

(但是这些条件中的一些可以通过额外的git日志选项来修改,并且有一个非常难以描述的副作用,称为历史简化,它会使git完全忽略历史遍历中的一些提交)。在某种意义上,您在这里看到的文件历史并不完全存在于存储库中:相反,它只是真实历史的一个合成子集。如果你使用不同的git日志选项,你会得到一个不同的“文件历史”!

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

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

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