第 06 天:解析 Git 资料结构 - 物件结构
在 Git 的资料结构中,「物件」是一种「不可变的」 (immutable) 档案类型,所有储存在「物件储存区」的档案通常只进不出,也不会被修改内容。原因在于,如果你窜改了档案了内容,新的内容所运算出来的 SHA1 杂凑值将会与原有物件的档名不一样,这导致 Git 无法继续执行,相对地也对 Git 储存库产生了一定程度的保护作用。本篇文章,将更加仔细地介绍 Git 的物件结构,最后也会透过一则影片,详细解说整个物件被产生的过程与逻辑。
关于物件资料库
前一篇文章有提到,无论 blob 物件与 tree 物件,这些都算是物件,这些物件都会储存在一个所谓的「物件储存区」 (object storage) 之中,而这个「物件储存区」预设就在「储存库」的 objects 目录下,如下图示:
然而 Git 储存库中的每一个「物件」,都是以「档案内容」进行 SHA1 杂凑运算出一个 hash 值,并用这个 hash 值当作物件的名称 (档名)。我们以 8a6b275638f3cf164395e65066a1132bb36b7896 为例,Git 会先拿前两个字元(8a)当作目录,然后把剩下的 hash 值当成档名 (6b275638f3cf164395e65066a1132bb36b7896),这些物件的实体目录与档案也都会放在 .git\objects 目录下,如下图示:
物件的类型
在这些「物件资料库」裡面,又包含了 4 种物件类型,分别是:
- blob 物件:就是工作目录中某个档案的 "内容",且只有内容而已,当你执行 git add 指令的同时,这些新增档案的内容就会立刻被写入成为 blob 物件,档名则是物件内容的杂凑运算结果,没有任何其他其他资讯,像是档案时间、原本的档名或档案的其他资讯,都会储存在其他类型的物件裡 (也就是 tree 物件)。
- tree 物件:这类物件会储存特定目录下的所有资讯,包含该目录下的档名、对应的 blob 物件名称、档案链接(symbolic link) 或其他 tree 物件等等。由于 tree 物件可以包含其他 tree 物件,所以浏览 tree 物件的方式其实就跟档案系统中的「资料夹」没两样。简单来说,tree 物件这就是在特定版本下某个资料夹的快照(Snapshot)。
- commit 物件:用来记录有哪些 tree 物件包含在版本中,一个 commit 物件代表著 Git 的一次提交,记录著特定提交版本有哪些 tree 物件、以及版本提交的时间、纪录讯息等等,通常还会记录上一层的 commit 物件名称 (只有第一次 commit 的版本没有上层 commit 物件名称。
- tag 物件:是一个容器,通常用来关联特定一个 commit 物件 (也可以关联到特定 blob、tree 物件),并额外储存一些额外的参考资讯(metadata),例如: tag 名称。使用 tag 物件最常见的情况是替特定一个版本的 commit 物件标示一个易懂的名称,可能是代表某个特定发行的版本,或是拥有某个特殊意义的版本。
Git 会将每一个版本中的档案建立一个对应的 blob 物件,一样的,该 blob 物件的档名就是用上述的方式计算出来的,从这些 blob 档案,你看不出跟版本有任何关系,你必须透过 tree 物件 (资料夹的快照) 与 commit 物件 (每一个版本的快照) 才能关联出这些 blob 与版本的关系。
所有的物件都会以 zlib 演算法进行压缩,不但可以有效的提升档案存取效率,在日后进行封装(pack)的时候也可以利用差异压缩(delta compression)演算法来节省空间。他会自动找出相似的 blobs,并自动计算出 blob 之间的变化差异,再将这些差异储存在一个名为 packfile 的档案中,这样就可以大幅节省磁碟空间的耗用)。通常 packfile 会置于 .git\objects\pack 目录下,如下图示:
上述这四种物件之间的关系,可参考以下图示:
然而,光是观看文字与图示,或许还是难以看出这几种物件类型之间的关系,没关系,笔者特别录製了一段教学影片,试图用 git 指令的方式解释 Git 的物件结构与产生物件的过程,也让各位更清楚的了解到底 Git 如何产生与管理这些档案。
YouTube 影片链接:认识 Git 资料结构中的物件资料库与物件之间的关系
物件结构的优点
你应该可以渐渐了解 Git 的「物件」设计是如此的漂亮,我们在第一篇文章曾经提到几个 Git 重要的设计,我们重新列出几点与「物件」特性有关的设计来看看:
- 有效率的处理大型专案
- 不仅仅是完整的版本库会複製(clone)一份在本机,由于所有的 blob 物件都是透过「内容」来做定址的 (content addressable),因此,若在不同版本之间找寻相同的内容,效率是非常高的。
- 历史纪录保护
- Git 版控的过程,每次提交变更都会产生一个 commit 物件,而这个 commit 物件的名称又是透过 commit 物件的内容产生。再者,commit 物件会关连到 tree 物件,tree 物件的名称又是透过 tree 物件的内容所产生。tree 物件又会关联到 blob 与 tree 物件,这些物件的名称也是透过内容产生。就这样一层一层的关联下去,如果你今天真的想窜改某个版本的历史纪录,困难度也是挺高的!
- 由于 Git 储存库经常会被 clone 或 fork,只要是被 clone 过的储存库,来源的储存库只要任何一个物件被修改,这些 clone 出去的储存库就很难再合併回来,所以你几乎不可能任意窜改版本纪录。
- 定期的封装物件
- 我们在 Git 中提到的 "物件" 其实就是代表版本库中的一个档案。而在版本异动的过程中,专案中的程式码或其他档案会被更新,每次更新时,只要档案内容不一样,就会建立一个新的 "物件",这些不同内容的档案全部都会保留下来。
- 你应该可以想像,当一个专案越来越大、版本越来越多时,这个物件会越来越多,虽然每个档案都可以各自压缩让档案变小,不过过多的档案还是会档案存取变得越来越没效率。因此 Git 的设计有个机制可以将一群老旧的 "物件" 自动封装进一个封装档(packfile)中,以改善档案存取效率。
- 那些新增的档案还是会以单一档案的方式存在著,也代表一个 Git 版本库中的 "档案" 就是一个 Git "物件",但每隔一段时间就会需要重新封装(repacking)。
- 照理说 Git 会自动执行重新封装等动作,但你依然可以自行下达指令执行。例如: git gc
- 如果你要检查 Git 维护的档案系统是否完整,可以执行以下指令: git fsck
今日小结
Git 裡的「物件」十分重要,其特性也十分重要,虽然我们在操作 Git 指令的过程中通常不太需要直接接触这些档案,不过了解这些物件的存在,也确实有助于让你更加理解 Git 的运作模式,与 Git 独到的设计概念。
参考链接