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

Posted

技术标签:

【中文标题】git 分支和标签如何存储在磁盘中?【英文标题】:How git branches and tags are stored in disks? 【发布时间】:2014-01-07 02:44:56 【问题描述】:

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

    分支和标签如何存储在磁盘中,例如使用什么数据结构,每个分支存储什么信息? 如何获取有关分支的元数据?比如那个分支创建的时候,这个分支的大小是多少。

【问题讨论】:

Git 分支实际上只是指向提交的指针。 所以这是每个分支或标签的 SHA,在 .git/refs 的文件中,所有文件(HEAD 除外),41 字节 = SHA(40 字节)+ NEWLINE(1 字节)跨度> 是的,除了典型的文件系统分配一些最小块大小(如 512 字节或 4k 或其他)。此外,名称本身会占用目录中的空间。因此,参考包装。 注意:使用 Git 2.2+(2014 年 11 月)创建 pack-refs 应该更快:参见 my answer below 【参考方案1】:

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

Git 对象

Git 本质上是一个对象数据库。这些对象有四种不同的类型,都由其内容的 SHA1 哈希标识。这四种类型分别是blobtreescommitstags

斑点

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 对象的指针。所以不,实际上没有任何关于分支的元数据可以从它们那里获得。唯一能给你一个想法的是你在历史上分支时所做的第一次提交。但是拥有分支并不自动意味着历史中确实存在一个分支(快速合并和变基对其不利),并且仅仅因为历史中存在一些分支并不意味着该分支(引用,指针)仍然存在。

【讨论】:

我刚刚在寻找关于 git 对象和对象压缩的现有 *** 讨论时遇到了这个答案,我想补充一点:我故意完全跳过了对象压缩,因为最初的问题是 关于参考。 pack-refs 代码只做引用打包;对象打包使用git pack-objects 完成。 (不过,我可能应该提到 reflog 过期。) 做到这一点,pack-objectrepack【参考方案2】:

所有 git 引用(分支、标签、注释、存储等)都使用相同的系统。它们是:

引用本身,以及 “引用日志”

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

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

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

【讨论】:

【参考方案3】:

你有:

packed-refs, reftable。 (请参阅此答案的最后一部分)

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

见commit 9540ce5Jeff King (peff):

refs: 使用 stdio 写入packed_refs 文件

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

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

在一个有 850 万引用的病态存储库上,这将运行 git pack-refs 的时间从 20 秒缩短到 6 秒


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

同样的 Git 2.11 现在将使用完全 pack bitmap

见commit 645c432、commit 702d1b9(2016 年 9 月 10 日)Kirill Smelkov (navytux)。 帮助者:Jeff King (peff)。(由 Junio C Hamano -- gitster -- 合并于 commit 7f109ef,2016 年 9 月 21 日)

pack-objects: 生成非标准输出包时使用可达位图索引

包位图是在 Git 2.0 中引入的(commit 6b8fda2,2013 年 12 月),来自 google's work for JGit。

我们使用位图 API 来执行Counting Objects 打包对象的阶段,而不是传统的遍历对象 图表。

现在(2016 年):

从6b8fda2 (pack-objects: use bitmaps when packing objects) 开始,如果存储库具有位图索引,则包对象可以很好地加速“计数对象”图遍历阶段。但这仅适用于将结果包发送到标准输出的情况,未写入文件

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


注意:GIt 2.12 说明使用位图对git gc --auto 有副作用

参见David Turner (csusbdt) 的commit 1c409a7、commit bdf56de(2016 年 12 月 28 日)。(由 Junio C Hamano -- gitster -- 合并到 commit cf417e2,2017 年 1 月 18 日)

位图索引仅适用于单个包,因此请求 使用位图索引进行增量重新打包毫无意义。

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


Git 2.14 精炼pack-objects

参见Jeff King (peff) 的commit da5a1f8、commit 9df4a60(2017 年 5 月 9 日)。(由 Junio C Hamano -- gitster -- 合并到 commit 137a261,2017 年 5 月 29 日)

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

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

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

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


使用 Git 2.27(2020 年第二季度),对非位图包的测试进行了改进。

见Jeff King (peff)commit 14d2778(2020 年 3 月 26 日)。(由 Junio C Hamano -- gitster -- 合并于 commit 2205461,2020 年 4 月 22 日)

p5310: 停止计时非位图打包到磁盘

签字人:杰夫·金

Commit 645c432d61 ("pack-objects: 在生成非标准输出包时使用可达性位图索引", 2016-09-10, Git v2.11.0-rc0 -- merge 列在batch #4) 增加了两个打包到磁盘文件的计时测试,无论有位图和没有位图。

但是,将非位图作为 p5310 回归套件的一部分并不有趣。它可以用作基准来展示位图情况的改进,但是:

t/perf 套件的重点是查找性能回归,但它对此无济于事。 我们不比较两个测试之间的数字(性能套件甚至不知道它们是否相关),其数字的任何变化都与位图无关。

它确实展示了645c432d61 的提交消息的改进,但在那里甚至没有必要。 位图情况已经显示出改进(因为在补丁之前,它的行为与非位图情况相同),性能套件甚至能够显示测量前后的差异。

最重要的是,它是套件中最昂贵的测试之一,在我的机器上 linux.git 的时钟约为 60 秒(而位图版本为 16 秒)。默认情况下,当使用“./run”时,我们会运行 3 次!

所以让我们放下它。它没有用,而且会增加性能运行的时间。


Reftables

使用 Git 2.28(2020 年第三季度),对 refs API 进行初步清理,以及 reftable 后端的文件格式规范文档。

请参阅commit ee9681d、commit 10f007c、commit 84ee4ca、commit cdb73ca、commit d1eb22d(2020 年 5 月 20 日)Han-Wen Nienhuys (hanwen)。 请参阅 Jonathan Nieder (artagnon) 的 commit 35e6c47(2020 年 5 月 20 日)。(由 Junio C Hamano -- gitster -- 合并于 commit eebb51b,2020 年 6 月 12 日)

reftable: 文件格式文档

签字人:Jonathan Nieder

肖恩·皮尔斯解释道:

一些存储库包含大量引用(例如 866k 的 android,31k 的 rails)。 reftable 格式提供:

对任何单个引用进行近乎恒定的时间查找,即使存储库处于冷态且不在进程或内核缓存中。 如果 SHA-1 被至少一个引用引用,则验证接近恒定时间(对于 allow-tip-sha1-in-want)。 整个命名空间的高效查找,例如refs/tags/。 - 支持原子推送O(size_of_update) 操作。 - 将 reflog 存储与 ref 存储相结合。

此文件格式规范最初由 Shawn Pearce 于 2017 年 7 月编写。

此后,Shawn 和 Han-Wen Nienhuys 根据实施和试验该格式的经验进行了一些改进。

(所有这些都是在我们在 Google 工作的背景下进行的,Google 很高兴将结果贡献给 Git 项目。)

从JGit 的当前版本(c217d33ff,“文档/技术/参考表:改进 repo 布局”,2020-02-04,JGit v5.7.0.202002241735-m3)导入Documentation/technical/reftable.md

并且适配为SHA2

reftable:定义规范的第 2 版以适应 SHA256

签字人:Han-Wen Nienhuys

版本将哈希 ID 附加到文件头,使其稍大。

此提交还在许多地方将“SHA-1”更改为“对象 ID”。


在 Git 2.35(2022 年第一季度)中,添加了 refs API 的“reftable”后端,未集成到 refs 子系统中。

见commit d860c86,commit e793168,commit e48d427,commit acb5334,commit 1ae2b8c,commit 3b34f63,commit ffc97f1,commit 46bc0e7,commit 17df8db,commit f14bd71,@98 @、commit a322920、commit e303bf2、commit 1214aa8、commit ef8a6c6、commit 8900447、commit 27f7ed2(2021 年 10 月 7 日)和 commit 27f3796(2021 年 8 月 30 日)Han-Wen Nienhuys (hanwen)。(于 2021 年 12 月 15 日由 Junio C Hamano -- gitster -- 合并于 commit a4bbd13)

reftable: 一个通用的二叉树实现

签字人:Han-Wen Nienhuys

可引用格式包括对(OID =&gt; ref) 映射的支持。 此地图可以加快可见性和可达性检查。 尤其是,Gerrit 中沿 fetch/push 路径的各种操作已通过使用此结构加速。

【讨论】:

还有:***.com/a/26962349/6309 和 github.com/git/git/commit/…

以上是关于git 分支和标签如何存储在磁盘中?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 git log --graph 显示标签名称和分支名称

如何将分支内容移动到另一个存储库保留历史记录并避免复制原始存储库的完整历史记录?

如何在 Git 的当前分支中获取最新的标签名称?

如何在谷歌云构建中访问 git 标签?

Git:如何找出标签在哪个分支上?

你如何恢复到 Git 中的特定标签?