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 哈希标识。这四种类型分别是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 对象的指针。所以不,实际上没有任何关于分支的元数据可以从它们那里获得。唯一能给你一个想法的是你在历史上分支时所做的第一次提交。但是拥有分支并不自动意味着历史中确实存在一个分支(快速合并和变基对其不利),并且仅仅因为历史中存在一些分支并不意味着该分支(引用,指针)仍然存在。
【讨论】:
我刚刚在寻找关于 git 对象和对象压缩的现有 *** 讨论时遇到了这个答案,我想补充一点:我故意完全跳过了对象压缩,因为最初的问题是 仅关于参考。pack-refs
代码只做引用打包;对象打包使用git pack-objects
完成。 (不过,我可能应该提到 reflog 过期。)
做到这一点,pack-object
和 repack
。【参考方案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 => ref)
映射的支持。 此地图可以加快可见性和可达性检查。 尤其是,Gerrit 中沿 fetch/push 路径的各种操作已通过使用此结构加速。
【讨论】:
还有:***.com/a/26962349/6309 和 github.com/git/git/commit/…以上是关于git 分支和标签如何存储在磁盘中?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 git log --graph 显示标签名称和分支名称