git分支和标签如何存储在磁盘中?

时间:2013-12-18 19:04:17

标签: git tags branch git-branch

我最近在工作中检查了我的一个git存储库,它有超过10,000个分支和超过30000个标记。新鲜克隆后,回收的总大小为12Gigs。我相信没有理由拥有10000个分支机构。所以我相信它们会在磁盘中占用相当大的空间。所以,我的问题如下

  1. 分支和标记如何存储在磁盘中,例如使用的数据结构,为每个分支存储哪些信息?
  2. 如何获取有关分支的元数据?就像创建分支时一样,分支的大小是什么。

3 个答案:

答案 0 :(得分:16)

所以,我将稍微扩展一下这个主题并解释如何 Git存储什么。这样做将解释存储哪些信息,以及存储库大小的确切重要性。作为一个公平的警告:这个答案相当长:)

Git对象

Git本质上是一个对象数据库。这些对象有四种不同的类型,并且都由其内容的SHA1哈希标识。这四种类型是 blobs 提交标记

斑点

blob 是最简单的对象类型。它存储文件的内容。因此,对于存储在Git存储库中的每个文件内容,对象数据库中都存在一个blob对象。由于它只存储文件 content ,而不存储文件名等元数据,因此这也是防止具有相同内容的文件被多次存储的机制。

向上一级,是将blob放入目录结构的对象。单个树对应于单个目录。它本质上是一个文件和子目录列表,每个条目包含一个文件模式,一个文件或目录名,以及对属于该条目的Git对象的引用。对于子目录,此引用指向描述子目录的树对象;对于文件,此引用指向存储文件内容的blob对象。

提交

Blob和树已足以代表完整的文件系统。要在其上添加版本控制,我们有提交对象。无论何时在Git中提交内容,都会创建提交对象。每次提交都代表修订历史中的快照。

它包含对描述存储库根目录的树对象的引用。这也意味着每次实际引入一些更改的提交至少需要一个新的树对象(可能更多)。

提交还包含对其父提交的引用。虽然通常只有一个父级(对于线性历史记录),但提交可以包含任意数量的父级,在这种情况下,它通常称为合并提交。大多数工作流程只会让你与两个父母合并,但你也可以拥有任何其他数字。

最后,提交还包含您希望提交的元数据:作者和提交者(名称和时间),当然还有提交消息。

这就是拥有完整版本控制系统所需的一切;但当然还有一种对象类型:

标记

标记对象是存储标记的一种方法。确切地说,标记对象存储带注释的标记,它们是类似于提交的标记 - 某些元信息。它们由git tag -a创建(或在创建签名标记时)并需要标记消息。它们还包含对它们指向的提交对象的引用,以及标记器(名称和时间)。

参考

到目前为止,我们有一个完整的版本控制系统,带有带注释的标签,但我们所有的对象都由它们的SHA1哈希标识。这当然有点烦人,因此我们还有其他一些方法可以让它变得更容易:引用。

引用有不同的风格,但最重要的是它们:它们是包含40个字符的简单文本文件 - 它们指向的对象的SHA1哈希值。因为它们很简单,所以它们非常便宜,因此使用许多引用都没有问题。它不会产生任何开销,也没有理由不使用它们。

通常有三种“类型”的引用:分支,标签和远程分支。他们真的工作相同,都指向提交对象;除了指向标记对象的带注释的标记之外(普通标记也只是提交引用)。它们之间的区别在于您如何创建它们,以及它们存储在/refs/的子路径中。我现在不会介绍这个,因为几乎每个Git教程都会对此进行解释;请记住:引用,即分支,非常便宜,所以不要犹豫为几乎所有东西创建它们。

压缩

现在因为torek在他的回答中提到了关于Git压缩的一些内容,我想稍微澄清一下。不幸的是,他混合了一些东西。

因此,通常对于新的存储库,所有Git对象都存储在.git/objects中,作为由SHA1哈希标识的文件。前两个字符从文件名中删除,用于将文件分区为多个文件夹,以便更容易导航。

在某些时候,当历史变得更大或者被其他东西触发时,Git将开始压缩对象。它通过将多个对象打包到一个包文件中来实现。这究竟如何起作用并不是那么重要;它将减少单个Git对象的数量,并有效地将它们存储在单个索引存档中(此时,Git将使用delta压缩btw。)。然后将包文件存储在.git/objects/pack中,并且可以轻松获得几百MiB的大小。

对于参考文献,情况有点类似,虽然简单得多。所有当前引用都存储在.git/refs中,例如.git/refs/heads中的分支,.git/refs/tags中的标记和.git/refs/remotes/<remote>中的远程分支。如上所述,它们是简单的文本文件,仅包含它们所指向的对象的40个字符标识符。

在某些时候,Git会将任何类型的旧引用移动到单个查找文件中:.git/packed-refs。该文件只是一个很长的哈希和引用名称列表,每行一个条目。保留在那里的引用将从refs目录中删除。

Reflogs

Torek也提到了这些, reflogs 基本上只是引用的日志。他们跟踪引用的内容。如果您执行任何影响引用(提交,检出,重置等)的操作,则会添加一个新的日志条目,以便记录发生的事情。它还提供了一种在出错之后返回的方法。例如,一个常见的用例是在意外地将分支重置到不应该去的地方之后访问reflog。然后,您可以使用git reflog查看日志并查看引用之前指向的位置。由于松散的Git对象不会立即删除(永远不会删除属于历史记录的对象),因此通常可以轻松恢复以前的情况。

然而,

Reflog是本地:它们只跟踪本地存储库发生的情况。它们不与遥控器共享,也不会被转移。新克隆的存储库将具有带有单个条目的reflog,它是克隆操作。它们也被限制在一定长度之后,旧的操作被修剪,因此它们不会成为存储问题。

一些最后的话

所以,回到你的实际问题。克隆存储库时,Git通常已经以压缩格式接收存储库。这已经完成以节省传输时间。引用非常便宜,因此它们永远不是大型存储库的原因。但是,由于Git的本质,单个当前提交对象中有一个完整的非循环图,最终将到达第一个提交,第一个树和第一个blob。因此,存储库将始终包含所有修订的所有信息。这就是使历史悠久的存储库变大的原因。不幸的是,你无法做到这一点。好吧,你可以在某些部分切断旧的历史记录,但是这会让你有一个破损的存储库(你通过使用--depth参数进行克隆来实现这一点。)

至于你的第二个问题,正如我上面所解释的,分支只是对提交的引用,而引用只是指向Git对象的指针。所以不,没有关于可以从中获取分支的任何元数据。唯一可以给你一个想法的是你在历史中分支时所做的第一次提交。但是拥有分支并不会自动意味着历史中确实存在一个分支(快速合并和重新定位对其起作用),并且仅仅因为历史中存在一些分支并不意味着分支(引用,指针)仍然存在。

答案 1 :(得分:5)

所有git引用(分支,标签,注释,stashes等)都使用相同的系统。这些是:

  • 引用本身,
  • “reflogs”

Reflog根据引用名称存储在.git/logs/refs/中,但有一个例外:HEAD的reflog存储在.git/logs/HEAD而不是.git/logs/refs/HEAD

引用来自“松散”或“打包”。打包的引用位于.git/packed-refs,这是简单引用的(SHA-1,refname)对的平面文件,以及带注释标记的额外信息。 “宽松”的引用位于.git/refs/name。这些文件包含原始SHA-1(可能是最常见的)或文字字符串ref:,后跟符号引用的另一个引用的名称(通常仅用于HEAD,但您可以创建其他文件) 。符号引用没有打包(或者至少,我似乎无法实现这一点:-))。

包装标签和“空闲”分支头(未主动更新的那些)可节省空间和时间。您可以使用git pack-refs执行此操作。但是,git gc会为您调用git pack-refs,因此通常您不需要自己执行此操作。

答案 2 :(得分:1)

注意:关于pack-refs,使用Git 2.2+(2014年11月)创建它们的过程应该快得多

commit 9540ce5Jeff King (peff)

refs:使用stdio

编写packed_refs文件
  

我们使用write()系统调用单独编写新的packed-refs文件的每一行(如果ref被剥离,有时为2)。由于每行只有大约50-100字节长,因此会产生大量的系统调用开销。

     

我们可以在我们的描述符周围打开stdio句柄并使用fprintf写入它。额外的缓冲对我们来说不是问题,因为在我们调用commit_lock_file之前,没有人会读取我们的新的packed-refs文件(此时我们已经刷新了所有内容)。

     

在一个拥有850万参考资料的病态资料库中,这使得git pack-refs的运行时间从20秒减少到6秒


2016年9月更新:Git 2.11+将包含链式标签inpack-refs(&#34; chained tags and git clone --single-branch --branch tag&#34;)

同样的Git 2.11现在将使用完全 打包位图

commit 645c432commit 702d1b9Kirill Smelkov (navytux)(2016年9月10日) 帮助:Jeff King (peff)
(由Junio C Hamano -- gitster --合并于commit 7f109ef,2016年9月21日)

  

pack-objects:生成非stdout pack时使用可达性位图索引

来自commit 6b8fda2的Git 2.0(google's work for JGit,2013年12月)中引入了

打包位图

  

我们使用位图API执行Counting Objects   包装对象中的阶段,而不是传统的遍历对象   曲线图。

现在(2016年):

  

6b8fda2 (pack-objects: use bitmaps when packing objects)开始,如果存储库具有位图索引,则包对象可以很好地加速&#34;计数对象&#34;图遍历阶段。
  但是,仅在将结果包发送到stdout而不是写入文件的情况下才这样做。

     

有人可能希望为专门的对象传输生成磁盘包文件   有一些方法可以覆盖这种启发式方法:   告诉pack-objects即使它应该生成磁盘文件,使用可达性位图进行遍历仍然可以。


注意:GIt 2.12表明使用位图会对git gc --auto产生副作用

commit 1c409a7commit bdf56deDavid Turner (csusbdt)(2016年12月28日) (由Junio C Hamano -- gitster --合并于commit cf417e2,2017年1月18日)

  

位图索引仅适用于单个包,因此请求一个   带位图索引的增量重新打包没有任何意义。

     

增量重新打包与位图索引不兼容


Git 2.14改进了pack-objects

commit da5a1f8commit 9df4a60Jeff King (peff)(2017年5月9日) Junio C Hamano -- gitster --于2017年5月29日commit 137a261合并)

  

pack-objects:禁用对象选择选项的包重用

     

如果某些选项(例如--honor-pack-keep--local--incremental)与包对象一起使用,那么我们需要将每个潜在对象都提供给want_object_in_pack()以查看是否应该过滤掉   但是当位图reuse_packfile优化生效时,我们不会调用   该功能,实际上完全跳过将对象添加到to_pack列表中。

     

这意味着我们有一个错误:对于某些请求,我们将默默地忽略这些选项,并在该包中包含不应该存在的对象。

     

6b8fda2中的包重用代码开始以来,问题一直存在(pack-objects:在打包对象时使用位图,2013-12-21),但实际上不太可能出现。
  这些选项通常用于磁盘包装,而不是转移包(转到stdout),但我们从未允许包装重复用于非标准包装(直到   645c432,我们甚至没有使用重用优化所依赖的位图;在那之后,我们在没有打包到stdout)时明确地将其关闭。