Git通讲-第二章(2):对象数据库
前言
理解了上篇文章的两大模型(快照和不可变对象)后,让我们看看Git 的核心——对象数据库,快照存储在 .git/objects
目录中,Git 通过这种方式管理项目的所有历史和数据。
Git对象数据库
下面是 .git/objects
目录的基本结构:
1 | .git/objects/ |
说明:
e5/
和12/
等目录是根据对象SHA-1哈希值的前两位字符命名的,里面存储对应的对象文件。**- **
pack/
目录包含打包的对象文件和相应的索引文件,用于优化存储和访问。 info/
目录用于存储对象数据库的元信息,通常包括关于Pack文件的信息,以提高对象访问的效率。
高效存储
Git 采用快照存储模型,表面上看似每次提交都存储了项目的完整副本,但实际上 Git 在存储方面非常高效,通过多种技术手段优化了存储空间的使用,即使代码量大,Git 也不会占用过多空间:
1. 重复文件共享引用
Git在每次提交时会为每个文件生成一个唯一的哈希值(SHA-1)。如果某个文件没有发生变化,Git不会为它创建新的副本,而是直接引用上一个版本的文件。因此,尽管Git每次记录的是项目的完整快照,但实际上对于未修改的文件,Git只是存储对之前版本的引用,不会重复存储相同的数据。
示例:
- 如果一个项目中有100个文件,但提交中只修改了3个,Git只会存储这3个修改过的文件的快照,其他97个文件会引用之前的快照。这极大节省了存储空间。
2. Zlib压缩存储
Git在内部使用了高效的压缩算法(zlib)来处理文件的存储。Git会将文件内容压缩后再进行存储,这样即使是完整快照,它也会占用较少的空间。Git的压缩机制对于文本文件(如代码)尤其有效,可以显著减少存储空间的消耗。
3.Pack文件
Git将提交、树、文件内容等对象存储在.git/objects
目录中。随着时间推移,Git会将相似的对象打包成Pack文件存储在.git/objects/pack
目录中,这是一种优化的存储格式,用来减少存储开销。Pack文件将多个对象进行增量压缩存储,使得整个仓库更紧凑,并进一步节省磁盘空间。
4. Delta压缩(在Pack文件中)
尽管Git不使用增量存储模型,但在Pack文件中,它会对文件内容进行增量压缩(类似于BitKeeper和CVS的方式)。在创建Pack文件时,Git会尝试寻找相似的文件内容并只存储它们之间的差异(delta),这样即使是快照模型,也会对相似的文件版本进行差异存储,进一步减少空间占用。
5. 轻量分支和标签
Git的分支和标签实际上是轻量指针,它们只是指向具体的提交对象,并不需要额外存储代码内容。这意味着即使有多个分支和标签,Git也不会因此浪费大量空间,因为每个分支或标签只是引用已有的提交记录。
6. 垃圾回收(Git GC)
Git有一个内置的垃圾回收机制(git gc
),定期清理不再使用的对象,回收存储空间。比如,删除某些分支或进行其他操作后,Git会自动运行垃圾回收,移除无用的数据,进一步优化存储空间,这项工作有助于保持 .git/objects
目录的整洁,并释放存储空间。
7. 大文件管理:Git LFS
对于那些包含大量二进制文件或者非常大的文件(如图片、视频、数据集等)的大型项目,Git提供了Git LFS(Large File Storage)来管理这些大文件。Git LFS将大文件存储在外部的存储系统中,Git仓库只会存储指向这些文件的引用,从而避免了大文件占用过多的空间。
结论
- 高效引用机制:Git仅存储有变化的文件,并引用未修改的文件,减少了存储冗余。
- 压缩与打包:Git使用压缩技术,并通过增量压缩和Pack文件进一步优化存储。
- 轻量分支:Git的分支和标签是轻量的,不会显著增加存储空间。
因此,尽管Git采用快照模型,代码量增大时也不会占用过多存储空间。相比其他传统的版本控制系统(如Subversion和CVS),Git在管理大型代码库时依然具有出色的存储效率。
对git的高效存储有了大致的认识之后我们会对一些值得玩味的减少内存占用的操作(Zlib、Pack、Delta、GC、LFS)进行逐个分析,以求有更深刻的认识。
Zlib压缩存储
Git 使用 Zlib 压缩存储来优化存储和传输数据,尤其是在处理大型代码库时。以下是对 Zlib 压缩存储及其相关源代码的详细讲解:
1. Zlib 压缩简介
Zlib 是一种数据压缩库,基于 DEFLATE 算法,这种算法结合了 LZ77 和 Huffman 编码,提供了良好的压缩比和速度。Git 利用 Zlib 对其对象(如提交、树、blob 和标签)进行压缩存储,以减少占用空间和提高传输效率。
DEFLATE 是一种无损压缩算法,意味着在解压缩时可以完美还原原始数据,这对 Git 中存储和传输版本控制数据至关重要。
2. 压缩流程
在 Git 中,压缩过程通常分为以下几个步骤:
- 对象创建:当你创建一个新对象(例如,提交或文件),Git 会首先将其内容转换为字符串,并计算其 SHA-1 哈希。
- 压缩:然后,Git 使用 Zlib 对该对象进行压缩。压缩后的数据与哈希一起存储。
- 存储:压缩后的对象存储在
.git/objects
目录中,以其哈希值的前两位作为目录名,剩下的部分作为文件名。
3. 源代码分析
Git 的 Zlib 压缩主要涉及以下几个文件和函数:
- **
hash-object.c
**:负责将文件添加到对象存储的逻辑。 - **
object.c
**:管理对象的创建和存储。 - **
pack-objects.c
**:用于打包多个对象,利用 Zlib 进行压缩以减少空间。
以下是一些关键的函数和它们的作用: zlib_deflate()
: 将数据压缩为 Zlib 格式。zlib_inflate()
: 解压缩数据。write_sha1_file()
: 将对象写入到对象存储,包括压缩和存储步骤。
4. 示例
以下是使用 Zlib 压缩的简单示例代码(不是 Git 的直接代码):
1 |
|
Zlib 被广泛应用于 Git 中,因为它提供了一种高效、灵活的压缩方案,能够与其他技术(如 Delta 编码)结合,优化数据存储和传输的整体效率。
Pack文件
自动打包是 Git 用于优化存储和提高性能的一项关键机制。随着时间推移,Git 仓库中的文件和对象会越来越多,特别是当项目有大量提交、分支、文件时,这些松散对象(单独存储的文件)会占据较多的磁盘空间并导致操作变慢。
为了应对这一问题,Git 会定期将这些松散对象打包成更紧凑的 Pack 文件,从而提高存储效率和访问速度。打包过程可以通过以下两种方式发生:
1. Git的自动打包机制
- 定期自动执行:Git在处理一些操作时,会在后台自动检查对象的数量和仓库的状态。当松散对象数量达到一定阈值后,Git会自动将这些对象打包为Pack文件。这种机制确保Git仓库随着时间推移保持紧凑,而无需用户干预。
例如,当你频繁提交、拉取、推送、克隆等操作时,Git可能会自动触发打包操作。
2.打包过程
打包过程主要涉及以下几个步骤:
a. 选择对象
- Git 首先选择要打包的对象。这些对象可以是自从上次打包以来新增的对象,也可以是历史版本中不再单独存储的对象。
b. 压缩对象
- 使用 Delta 编码:在打包过程中,Git 还会检查选择的对象之间的相似性,并可能使用 Delta 编码来进一步减少存储空间。Delta 编码只存储对象之间的差异,而不是完整对象。
- 最终压缩:为了提高效率,Git 会再次对打包后的数据使用 Zlib 压缩。这是因为 Delta 编码后的数据结构可能会导致较大的数据块,使用 Zlib 可以进一步压缩这些数据,减少最终的 packfile 大小。
c. 创建 Packfile
- 创建一个新的 packfile,将压缩后的对象写入其中。Git 会在 packfile 中使用特定的格式记录对象信息,包括对象类型、大小、SHA-1 哈希等。
d. 更新索引
- 为了快速查找 packfile 中的对象,Git 会生成一个索引文件(
.idx
)。该文件记录了每个对象在 packfile 中的位置和元数据。
3. 手动运行 git gc
命令
git gc
(garbage collection) 是 Git 的垃圾回收命令,它可以主动触发对象的打包和仓库清理。执行git gc
时,Git 会:- 打包松散对象:将单独存储的文件对象打包成更紧凑的 Pack 文件。
- 清理无用数据:删除不再引用的对象、过时的提交、旧的日志文件等,回收存储空间。
运行
git gc
可以手动优化仓库,特别适用于仓库文件较大、历史提交较多的项目。你可以通过命令来强制执行垃圾回收Git 会自动进行对象打包、清理,确保仓库保持良好的状态和高效的存储。
4. 相关源代码
Git 的打包逻辑主要涉及以下几个文件和函数:
- **
pack-objects.c
**:负责管理对象的选择、压缩和打包过程。 - **
packfile.h
**:定义与 packfile 相关的结构体和函数。
以下是一些关键函数: pack_objects()
: 主要入口,负责选择和打包对象。write_pack_file()
: 将对象写入到 packfile 中。write_index_file()
: 生成索引文件。
5. 示例代码
以下是一个简化的示例,展示了如何使用 Zlib 压缩数据并写入 packfile(非 Git 源代码):
1 |
|
6.打包的优势
- 减少文件数量:松散对象每个都占据单独的文件,随着提交的增多会导致文件系统中存在大量小文件,降低操作效率。打包将多个对象合并成一个或多个Pack文件,减少文件数量,提升文件系统的访问速度。
- 节省空间:Pack文件会对相似的对象进行压缩存储,尤其是采用Delta压缩技术,使得仓库体积大大减少。
- 提高性能:Pack文件结构紧凑,Git可以更快速地进行检索和读取操作,提升整体操作性能。
.git/objects/pack
在执行了 git gc
后,Git 会将对象打包成 Pack文件,并存储在 .git/objects/pack
目录中。这个目录中的文件包括:
1. Pack文件(.pack)
- 描述:Pack文件是打包好的Git对象集合,包含了提交对象(commit)、树对象(tree)、文件对象(blob)等。
- 文件名格式:Pack文件的文件名以
.pack
结尾,文件名前面是一长串SHA-1哈希值,用来唯一标识这个Pack文件的内容。 - 作用:Pack文件通过增量压缩存储相似的对象,以减少存储空间。它包含了实际的打包对象。
1
.git/objects/pack/pack-xxxxxx.pack
2. 索引文件(.idx)
- 描述:索引文件是与Pack文件配套的文件,它存储了Pack文件中各个对象的索引信息。
- 文件名格式:与Pack文件相同,前面的哈希值相同,但扩展名为
.idx
。 - 作用:索引文件允许Git快速查找Pack文件中的对象。通过这个索引,Git不需要逐个解压所有对象,而是可以直接定位到特定对象的位置,提升查找效率。
例如:1
.git/objects/pack/pack-xxxxxx.idx
3. 松散对象(Loose Objects)
在打包之前,Git中的对象是以松散对象(Loose Objects)的形式存储的,每个对象存储为单独的文件。这些松散对象位于 .git/objects/
目录下,根据对象的SHA-1哈希值前两位创建子目录,例如:
1 | .git/objects/ab/123456... |
打包后,这些松散对象会被删除,只有Pack文件中的对象会被保留。如果你之前没见到Pack文件,可能是因为你的仓库还没有达到打包的条件,或者执行 git gc
后才生成了Pack文件。
如何查看Pack文件
如果你想查看Pack文件中的内容,可以使用以下Git命令:
- 列出Pack文件内容:
1 | git verify-pack -v .git/objects/pack/pack-xxxxxx.idx |
这个命令会显示Pack文件中的所有对象及其大小、类型等信息。
- 解压Pack文件: 如果你想解压Pack文件查看其中的对象,可以使用:这个命令会解压出所有Pack文件中的对象,重新存储为松散对象。
1
git unpack-objects < .git/objects/pack/pack-xxxxxx.pack
总结
在Git中,Pack文件及其索引文件是为了优化存储和查找效率而引入的。如果你没有执行 git gc
或者仓库没有达到一定规模,可能没有注意到它们的存在。Pack文件将仓库的对象打包并压缩,有助于提高Git的性能,尤其是在处理大量对象时。
Delta压缩
- 在 Git 的具体实现中,Zlib 和 Delta 编码可以结合使用。Git 首先会使用 Zlib 压缩单个对象,然后在打包过程中,如果有相似的对象,它会考虑将它们的差异存储为 Delta,以进一步节省空间。
- 当你从远程仓库拉取时,Git 会使用 Delta 编码来减少网络传输的数据量,而在本地存储时,它会使用压缩后的 packfile。
Git 中的 Delta 压缩是一种用于存储对象之间差异的技术,旨在进一步优化存储空间。以下是对 Delta 压缩的详细讲解以及相关的源码分析。
1. Delta 压缩的概念
- Delta 压缩(差分压缩)用于存储对象之间的差异,而不是完整的对象。对于相似或相同的对象,这可以显著减少所需的存储空间。
- 在 Git 中,通常用于 blob 对象(文件内容)和 tree 对象(目录结构)。
2. Delta 压缩的工作流程
a. 选择基准对象
- 在进行 Delta 压缩时,Git 选择一个“基准”对象(通常是一个较旧的版本)作为比较对象。
b. 生成 Delta
- Git 使用一种算法计算当前对象与基准对象之间的差异。这个过程包括:
- 查找相同部分(称为 “context”)。
- 记录插入、删除和替换操作,以生成 delta 数据。
c. 存储 Delta
- Delta 数据会与基准对象一起存储,Git 在解压缩时使用基准对象来重建原始对象。
3. Delta 压缩的源码
Git 的 Delta 压缩主要涉及以下几个文件和函数:
a. 关键文件
- **
delta.c
**:实现 Delta 编码和解码的核心逻辑。 - **
pack-objects.c
**:负责选择对象并管理打包过程,包括 Delta 压缩。
b. 重要函数
- **
create_delta()
**:用于生成 delta 数据的主要函数。 - **
apply_delta()
**:用于将 delta 应用到基准对象以重建原始对象。
c. 示例代码
以下是一个简化的示例,展示如何在 Git 中生成和应用 delta(不是 Git 的实际实现):
1 |
|
4. 总结
Git 的 Delta 压缩技术通过存储对象之间的差异,显著减少了存储空间的需求。其实现结合了对象选择、差异计算和有效存储策略。
垃圾回收机制(git gc
)
Git 中的垃圾回收机制(git gc
)旨在优化存储,清理不再需要的对象,并合并多个对象以减少磁盘空间的占用。下面是对垃圾回收机制的详细讲解以及相关的源码分析。
1. 垃圾回收的概念
- Git 在运行时会生成许多对象(例如提交、blob、tree 和标签),这些对象在某些情况下可能不再被引用(即没有被任何分支或标签指向)。
- 垃圾回收的目的就是清理这些未引用的对象,从而释放存储空间。
2. git gc
命令
git gc
是执行垃圾回收的命令。其主要功能包括:- 删除未引用的对象。
- 压缩和打包对象,以减少磁盘占用。
- 更新引用和索引文件。
3. 垃圾回收的工作流程
a. 发现未引用对象
- Git 首先扫描对象库,找出所有未被引用的对象。这些对象包括:
- 存在于
.git/objects
目录中的对象,但不再被任何分支、标签或其他引用所指向。
- 存在于
b. 删除未引用对象
- Git 会删除这些未引用的对象,以释放存储空间。
c. 打包对象
- Git 会将多个对象打包成一个或多个 packfile,以提高存储效率。这个过程会使用 Delta 压缩来进一步减少占用空间。
4. 相关源码
Git 的垃圾回收机制主要涉及以下几个文件和函数:
a. 关键文件
- **
gc.c
**:实现垃圾回收的核心逻辑。 - **
object.c
**:管理对象的创建、查找和删除。 - **
pack-objects.c
**:负责对象的打包和压缩。
b. 重要函数
- **
git_gc()
**:执行垃圾回收的主要函数。 - **
prune_objects()
**:查找并删除未引用的对象。 - **
pack_objects()
**:将对象打包成 packfile。
5. 示例代码
以下是一个简化的示例,展示了如何在 Git 中实现垃圾回收的基本逻辑(非 Git 的实际实现):
1 |
|
6.相关指令
1. git gc
- 用途:执行垃圾回收,清理未引用的对象,压缩并打包对象。
- 用法:直接在终端运行
git gc
。
2. git prune
- 用途:删除未被任何引用所指向的对象。这个命令通常是
git gc
的一部分,但可以单独运行。 - 用法:直接在终端运行
git prune
。
3. git repack
- 用途:重新打包已经存在的 packfile,将多个 packfile 合并成一个,并进行压缩。可以通过选项来指定行为。
- 用法:
git repack
,可以加上不同的选项,例如-a
(打包所有对象)或-d
(删除旧的 packfile)。
4. git fsck
- 用途:检查 Git 仓库的完整性,找出丢失的对象和不一致的引用。在进行垃圾回收之前,通常会建议先运行这个命令。
- 用法:直接在终端运行
git fsck
。
5. git config
- 用途:可以配置 Git 的垃圾回收行为,例如设置自动垃圾回收的时间间隔。
- 用法:
git config --global gc.auto <n>
:设置当对象数量超过n
时自动运行git gc
。git config --global gc.autoPackLimit <n>
:设置触发自动打包的对象数量限制。
7. 总结
Git 的垃圾回收机制通过清理未引用对象和打包对象来优化存储,确保磁盘空间得到有效利用。
Git LFS
Git LFS(Large File Storage)是一个用于管理 Git 中大文件的扩展,解决了 Git 本身在处理大文件时的性能问题。下面是对 Git LFS 的详细讲解,包括其使用方法和相关源码分析。
1. Git LFS 的概念
- Git LFS 旨在处理大文件(如图像、音频、视频等),避免将它们直接存储在 Git 仓库中,这样可以减少 Git 仓库的大小,提高操作速度。
- LFS 将大文件的实际内容存储在外部服务器上,而在 Git 仓库中只保留指向这些文件的指针。
2. Git LFS 的工作流程
a. 安装 Git LFS
- 在使用 Git LFS 之前,首先需要安装它。可以通过以下命令进行安装:
1
git lfs install
b. 跟踪大文件
- 使用
git lfs track
命令来指定需要使用 LFS 管理的大文件类型。例如:1
git lfs track "*.psd"
- 这将创建或更新
.gitattributes
文件,指明哪些文件类型应该使用 LFS。
c. 添加和提交大文件
- 将大文件添加到 Git 仓库时,使用普通的 Git 命令:
1
2git add mylargefile.psd
git commit -m "Add large file"
d. 推送和拉取
- 当你推送更改时,大文件的内容会被上传到 LFS 服务器,而 Git 仓库中只会存储指向这些大文件的指针。
- 拉取时,Git LFS 会自动下载这些大文件。
3. Git LFS 的源码
Git LFS 是一个独立的工具,包含多个关键组件和功能。以下是 Git LFS 的一些核心源码概念:
a. 关键组件
- Pointer File:LFS 用于替代大文件的指针文件,包含指向实际大文件的元数据(如文件的哈希和大小)。
- Storage Backend:负责存储和检索大文件,可以是本地或远程存储。
b. 重要函数
- Track Files:用于更新
.gitattributes
文件的函数。 - Upload:将大文件上传到 LFS 存储的函数。
- Download:从 LFS 存储下载大文件的函数。
c. 示例代码
以下是一个简化的示例,展示如何使用 LFS 处理大文件(非 Git LFS 的实际实现):
1 | class LFS: |
4. 总结
Git LFS 提供了一种高效的方式来管理大文件,避免了将它们直接存储在 Git 仓库中,从而提升了仓库的性能和可管理性。
回看.git文件
.git
文件夹存储了整个项目的版本历史和所有提交记录。也就是说,这个文件夹中包含了项目的完整变更记录、提交的快照、分支信息等内容。只要 .git
文件夹保留完整,你可以通过 git checkout
或 git reset
等命令,恢复到项目的任何一个历史版本,甚至恢复整个项目的文件内容。
### .git文件夹
.git
文件夹的主要内容包括:
1. objects 文件夹:存储了每个提交的对象(包括提交对象、树对象、和 blob 对象),这些对象通过哈希值(commit ID)来索引,确保文件内容不会重复存储。
2. refs 文件夹:保存了指向分支、标签的指针,帮助 Git 了解当前分支的 HEAD 指针等信息。
3. HEAD 文件:用于指向当前检出的分支,告诉 Git 哪个提交是当前版本。
因此,.git
文件夹可以看成是一个精简的数据库,记录了项目的所有历史状态和操作记录。如果你复制 .git
文件夹到另一个目录,并执行 git checkout
,就可以恢复出项目的完整文件夹结构和内容。
复制 .git
文件夹后,可以按照以下步骤来确保项目文件正确恢复:
- 进入目标目录:在新目录下,确认
.git
文件夹已经被正确复制到根目录。 - 恢复所有文件:
- 运行以下命令,将工作区恢复到最新的提交状态:这条命令会将
1
git checkout HEAD -- .
.git
文件夹中的记录应用到工作区,恢复项目的所有文件。
- 运行以下命令,将工作区恢复到最新的提交状态:
- 验证恢复的文件:你可以使用
git status
查看文件状态,确认所有文件已恢复至项目的最新提交版本。
这种方法利用.git
文件夹的提交记录来重新生成项目的完整文件结构,是一种手动的恢复方式。这种恢复操作的前提是,.git
文件夹内所有提交记录都是完整的。
版本回退
如果你想回退到之前某一次的提交状态,可以使用以下几种方法,具体选择取决于你是否希望保留回退后的更改记录。
1. 使用 git checkout <commit>
(回到指定提交查看文件,但不修改提交历史)
1 | git checkout <commit_hash> |
- 这里的
<commit_hash>
是你想要回退到的那个提交的哈希值。 - 这种方式会让工作区进入“分离头指针”(detached HEAD)状态,你可以查看那个版本的文件内容,但此时不会更新分支的最新提交记录。
- 如果你只是想临时查看某个历史版本,这个方法非常适合。要回到最新的提交,可以运行
git checkout main
或git checkout <branch_name>
切换回原来的分支。
2. 使用 git reset
(更改分支历史)
git reset
有不同的选项,具体取决于你是否要保留当前工作区的更改:
回退并保留更改 (--soft
)
1 | git reset --soft <commit_hash> |
--soft
会将 HEAD 移动到指定的提交,但会保留之后的更改,并将这些更改保存在暂存区中,方便你再次提交。
回退并丢弃暂存区的更改 (--mixed
)1
git reset --mixed <commit_hash>
--mixed
是默认选项,它会回退到指定提交,并将之后的更改从暂存区中移除,但会保留在工作区中,这样你可以再次进行调整。
回退并丢弃所有更改 (--hard
)1
git reset --hard <commit_hash>
--hard
会完全回退到指定的提交,之后的所有更改都会被清除,不再保留。
3. 使用 git revert
(回退特定提交,保留历史)
如果你想回到之前的状态,但不希望丢失提交记录,可以使用 git revert
:
1 | git revert <commit_hash> |
git revert
会创建一个新的提交,撤销指定的提交内容,但保留原有的提交历史。这样既能恢复到之前的状态,也不会破坏历史记录,适用于团队协作。
总结
- 临时查看旧版本:
git checkout <commit_hash>
- 修改分支历史:
git reset
(根据需要选择--soft
、--mixed
或--hard
) - 保持提交记录,创建回退提交:
git revert
这些方法可以帮助你在不同时期的提交之间灵活回退和恢复。
后记
呼,这篇文章确实有点干,但是我写下来也是收获满满,只是不知道有什么用🫠
主要介绍了下Git的对象数据库.git/object
文件夹和其相关一系列的减少内存占用的设计。
下一篇估计是会介绍git的分支,还没想好具体要涵盖什么知识点。