所以,我将稍微扩展一下这个主题并解释如何Git 存储什么。这样做将解释存储了哪些信息,以及对于存储库的大小究竟有什么重要意义。作为一个公平的警告:这个答案相当长:)
Git 对象
Git 本质上是一个对象数据库。这些对象有四种不同的类型,都由其内容的 SHA1 哈希标识。这四种类型分别是blob、trees、commits和tags。
斑点
blob 是最简单的对象类型。它存储文件的内容。因此,对于您存储在 Git 存储库中的每个文件内容,对象数据库中都存在一个 blob 对象。由于它只存储文件内容,而不存储文件名等元数据,这也是防止具有相同内容的文件被多次存储的机制。
树
再往上一层,树就是将blob放入目录结构的对象。一棵树对应一个目录。它本质上是一个文件和子目录的列表,每个条目都包含一个文件模式、一个文件或目录名称,以及对属于该条目的 Git 对象的引用。对于子目录,这个引用指向描述子目录的树对象;对于文件,此引用指向存储文件内容的 blob 对象。
提交
Blob 和树已经足以代表一个完整的文件系统。为了在此之上添加版本控制,我们有 commit 对象。每当您在 Git 中提交某些内容时,都会创建提交对象。每个提交都代表修订历史中的一个快照。
它包含对描述存储库根目录的树对象的引用。这也意味着每次实际引入一些更改的提交至少需要一个新的树对象(可能更多)。
提交还包含对其父提交的引用。虽然通常只有一个父级(对于线性历史),但提交可以有任意数量的父级,在这种情况下,它通常称为 合并提交。大多数工作流程只会让您与两个父母合并,但您也可以拥有任何其他数字。
最后,提交还包含您希望提交具有的元数据:作者和提交者(姓名和时间),当然还有提交消息。
这就是拥有完整版本控制系统所必需的一切;但当然还有另外一种对象类型:
标签
标签对象是存储标签的一种方式。准确地说,标签对象存储带注释的标签,这些标签具有——类似于提交——一些元信息。它们由git tag -a(或在创建签名标签时)创建,并且需要标签消息。它们还包含对它们指向的提交对象的引用,以及一个标记器(名称和时间)。
参考文献
到目前为止,我们有一个完整的版本控制系统,带有注释标签,但我们所有的对象都由它们的 SHA1 哈希标识。这当然用起来有点烦人,所以我们有一些其他的东西可以让它更容易:参考。
引用有不同的风格,但最重要的是:它们是包含 40 个字符的简单文本文件——它们指向的对象的 SHA1 哈希值。因为它们是如此简单,所以它们非常便宜,因此使用许多引用完全没有问题。它不会产生任何开销,也没有理由不使用它们。
通常有三种“类型”的引用:分支、标签和远程分支。它们确实工作相同,并且都指向提交对象;除了指向标签对象的 annotated 标签(普通标签也只是提交引用)。它们之间的区别在于您如何创建它们,以及它们存储在/refs/ 的哪个子路径中。不过我现在不会介绍这个,因为几乎每个 Git 教程都对此进行了说明;请记住:引用(即分支)非常便宜,因此请毫不犹豫地为几乎所有内容创建它们。
压缩
现在因为 torek 在他的回答中提到了一些关于 Git 的压缩,我想澄清一下。不幸的是,他把一些事情搞混了。
因此,通常对于新存储库,所有 Git 对象都存储在 .git/objects 中,作为由其 SHA1 哈希标识的文件。前两个字符从文件名中去掉,用于将文件划分到多个文件夹中,这样导航起来会更容易一些。
在某些时候,当历史变大或被其他东西触发时,Git 将开始压缩对象。它通过将多个对象打包到一个 pack 文件 中来做到这一点。这究竟是如何工作的并不是那么重要。它将减少单个 Git 对象的数量并有效地将它们存储在单个索引存档中(此时,Git 将使用增量压缩顺便说一句。)。然后将包文件存储在.git/objects/pack 中,并且可以轻松获得几百 MiB 的大小。
对于参考,情况有些相似,但要简单得多。所有当前引用都存储在.git/refs,例如.git/refs/heads 中的分支,.git/refs/tags 中的标签和.git/refs/remotes/<remote> 中的远程分支。如上所述,它们是简单的文本文件,仅包含它们指向的对象的 40 个字符的标识符。
在某些时候,Git 会将任何类型的旧引用移动到单个查找文件中:.git/packed-refs。该文件只是一长串哈希和参考名称,每行一个条目。保存在其中的引用将从refs 目录中删除。
引用日志
Torek 也提到了这些,reflogs 本质上只是用于参考的日志。他们跟踪引用发生的情况。如果你做了任何影响引用的事情(提交、签出、重置等),那么就会添加一个新的日志条目来简单地记录发生的事情。它还提供了一种在您做错事后返回的方法。例如,一个常见的用例是在意外地将分支重置到不应该去的地方后访问 reflog。然后,您可以使用git reflog 查看日志并查看引用之前指向的位置。由于松散的 Git 对象不会立即被删除(属于历史的对象永远不会被删除),因此您通常可以轻松恢复之前的情况。
但是,Reflogs 是本地的:它们只跟踪本地存储库发生的情况。它们不与遥控器共享,并且永远不会转移。一个新克隆的存储库将有一个带有单个条目的 reflog,它是克隆操作。它们也被限制在一定的长度内,在此之后旧的操作将被修剪,因此它们不会成为存储问题。
一些最后的话
所以,回到您的实际问题。当您克隆存储库时,Git 通常已经以打包格式接收存储库。这已经完成以节省传输时间。参考非常便宜,因此它们永远不是大型存储库的原因。然而,由于 Git 的特性,单个当前提交对象中包含一个完整的非循环图,最终将到达第一个提交、第一个树和第一个 blob。因此,存储库将始终包含所有修订的所有信息。这就是使具有悠久历史的存储库变得庞大的原因。不幸的是,您对此无能为力。好吧,您可以在某些部分切断较旧的历史记录,但这会留下一个损坏的存储库(您可以通过使用 --depth 参数进行克隆来做到这一点)。
关于你的第二个问题,正如我上面解释的,分支只是对提交的引用,而引用只是指向 Git 对象的指针。所以不,实际上没有任何关于分支的元数据可以从它们那里获得。唯一能给你一个想法的是你在历史上分支时所做的第一次提交。但是拥有分支并不自动意味着历史中确实存在一个分支(快速合并和变基对其不利),并且仅仅因为历史中存在一些分支并不意味着该分支(引用,指针)仍然存在。