深入Git-中篇
「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战」
前言
上篇文章深入Git-上篇我们介绍了Git的目录结构,对于仓库的核心实现objects我们选择先跳过了。本篇文章就主要讲讲objects,通过本篇文章可以了解Git的仓库存储,其版本的实现逻辑等。
Git的三个分区
在开始学习前,我们先简单了解下Git的三个分区。
- 工作区
顾名思义我们进行编辑操作的就是工作区的内容
- 暂存区(索引)
可以理解为暂存着下一次commit的版本仓库
- 版本库
存放所有版本内容的仓库,不同版本的文件及内容都可以在其中找到
objects结构
objects ├── info └── pack 复制代码
初始化时objects下面仅包含两个目录,info和pack,其中pack将存储ojects下其它文件的打包压缩后的结果。
仓库文件类型
刚刚初始化的仓库objects下面是没有任何东西的,下面我们将通过添加文件等操作,为其添加不同类型的文件。
在工作区添加文件及内容
echo 'hello world' > a.txt 复制代码
blob
此刻,我们的objects还是为空的,我们将其添加到暂存区
git add a.txt 复制代码
我们再通过tree打印下objects目录,可以发现多了文件夹3b
,下面有文件18e512dba79e4c8300dd08aeb37f8e728b8dad
。文件名和我们平常使用的commitId
类似。
├── 3b │ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad ├── info └── pack 复制代码
实际上文件名是根据文件内容通过SHA1
哈希算法生成的,完整值为3b18e512dba79e4c8300dd08aeb37f8e728b8dad
,存储在objects的时候会将前两位字符提取用于分桶
存储,而文件内容则是个二进制文件。
00000000: 7801 4bca c94f 5230 00000008: 3462 c848 cdc9 c957 00000010: 28cf 2fca 49e1 0200 00000018: 4411 0689 复制代码
为了更加清楚地展示其内容,我们再介绍一个命令
git cat-file -t <fileId> # 文件类型 git cat-file -p <fileId> # 文件内容 复制代码
我们来看看刚才在objects下生成的文件
git cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # blob git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # hello world 复制代码
tree
接着我们将文件添加到版本库中
git commit -m firstcommit 复制代码
可以发现objects下面生成了两个文件
5ce458a111f86b77eb9399931b0391d27f75cfac
及ebaa691b5554f29ac9d4f37811a1da6f24d376a1
. ├── 3b │ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad ├── 5c │ └── e458a111f86b77eb9399931b0391d27f75cfac ├── eb │ └── aa691b5554f29ac9d4f37811a1da6f24d376a1 ├── info └── pack 复制代码
我们先看看ebaa691b5554f29ac9d4f37811a1da6f24d376a1
的内容
git cat-file -t ebaa691 # tree 复制代码
因为
SHA1
可以通过前7位确定寻找到唯一值,所以我们平常工作中都可以使用简短ID即前7位
区别于前面的blob类型,这边新出现一个tree类型(实际其内容仍然是通过二进制保存)
git cat-file -p ebaa691 # 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt 复制代码
tree文件实际对应当前commit操作时的暂存区快照。因为我们只添加了一个文件,所以只会输出一行内容,也就是a.txt
相关信息。从左至右依次为文件权限-文件类型-文件哈希-文件名
值得注意的是tree文件保存了文件的文件名而blob仅仅保存了文件内容。这样做的好处是,在通常情况下文件的内容是比目录结构大得多的情况下。当文件名修改时,我们仅仅需要生成新的tree文件,而不用因为文件名的修改而更新blob。
commit
我们接着看另外一个文件
git cat-file -t 5ce458a # commit 复制代码
出现新的文件类型commit
git cat-file -p 5ce458a 复制代码
其文件内容包括刚才生成的treeID,提交用户相关信息及提交信息。因为我们这是第一次提交,所以不会有parentId。在之后的commit文件中则会在parentId中保存上次的commitId。
tree ebaa691b5554f29ac9d4f37811a1da6f24d376a1 author xxx <xxx@qq.com> 1643509265 +0800 committer xxx <xxx@qq.com> 1643509265 +0800 firstcommit 复制代码
小结
通过上面的实践,我们认识了Git版本库中三种文件类型
类型 | 存储信息 |
blob | 文件内容 |
tree | 目录快照(包括文件权限,文件类型,文件ID,文件名) |
commit | 版本信息(包括提交用户信息,treeID,parentID,提交信息) |
以上文件的相同点在于
- 存储在objects,通过前两位哈希值进行分桶
- 文件实际类型都为blob文件
- 文件名都是通过
SHA1
哈希得到
版本控制
前面我们分析了在版本库中不同文件类型及其保存的内容。在其基础上,我们可以分析出Git版本控制的大致流程。我们通过实例来分析
- 在工作区添加内容
echo 'a' > a.txt echo 'b' > b.txt 复制代码
- 将工作区内容添加到暂存区,在版本库中生成blob文件
git add . 复制代码
- 对暂存区生成快照tree文件
git commit -m first 复制代码
- 生成第一个版本commit文件,其文件名则是我们平常使用的commitID
- 我们更新
a.txt
echo a2 > a.txt 复制代码
- 将更新同步到暂存区
git add a.txt 复制代码
- 生成新的版本
git commit -m second 复制代码
抛出几个思考
- 会不会有重复的commitID
在我们上面分析版本实现的逻辑上,实际可以发现版本库实际实现了一颗哈希树。在哈希树中,其哈希值通过子节点进行哈希算法得到且不会重复。
- 我们能否篡改某个commit的内容而不被发现?
实际Git是提供了命令让我们回退到中途某个版本进行修改的,但是我们修改内容后没法不被发现。同样因为哈希树的实现,当我们修改某个commit版本的时候,其comnitId肯定是会更改的,此时前后面对应的commit文件中保存的parentId会改变,所以此时参与哈希计算的内容(parentId)实际会改变导致后面版本的commitID都会跟着改变。
- 对于修改内容,版本中是保存增量还是全量数据?
通过上文的分析,实际已经可以知道答案。Git中保存了每次更新后的全量文件,这也是为什么我们切换版本或者切换分支会非常快的原因,省去了遍历不同版本应用patch的操作。当然每次保存更新后的全量文件会使我们的仓库变得非常大,而Git相应的会对仓库下的文件进行打包压缩来减小仓库体积,打包后的结果放在前文所说的pack文件夹下面。
结语
本篇文章主要分析了Git仓库的存储机制,其是如何保存不同版本的内容