深入Git-中篇

282 阅读7分钟

深入Git-中篇

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

前言

上篇文章深入Git-上篇我们介绍了Git的目录结构,对于仓库的核心实现objects我们选择先跳过了。本篇文章就主要讲讲objects,通过本篇文章可以了解Git的仓库存储,其版本的实现逻辑等。

Git的三个分区

在开始学习前,我们先简单了解下Git的三个分区。

屏幕快照 2022-01-30 下午12.33.38.png

  • 工作区

顾名思义我们进行编辑操作的就是工作区的内容

  • 暂存区(索引)

可以理解为暂存着下一次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

屏幕快照 2022-01-30 上午11.46.26.png

tree

接着我们将文件添加到版本库中

git commit -m firstcommit

可以发现objects下面生成了两个文件5ce458a111f86b77eb9399931b0391d27f75cfacebaa691b5554f29ac9d4f37811a1da6f24d376a1

.
├── 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。

屏幕快照 2022-01-30 上午11.46.52.png

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

屏幕快照 2022-01-30 上午11.48.31.png

小结

通过上面的实践,我们认识了Git版本库中三种文件类型

类型存储信息
blob文件内容
tree目录快照(包括文件权限,文件类型,文件ID,文件名)
commit版本信息(包括提交用户信息,treeID,parentID,提交信息)

以上文件的相同点在于

  1. 存储在objects,通过前两位哈希值进行分桶

  2. 文件实际类型都为blob文件

  3. 文件名都是通过SHA1哈希得到

版本控制

前面我们分析了在版本库中不同文件类型及其保存的内容。在其基础上,我们可以分析出Git版本控制的大致流程。我们通过实例来分析

  1. 在工作区添加内容
echo 'a' > a.txt
echo 'b' > b.txt

屏幕快照 2022-01-30 下午12.34.20.png

  1. 将工作区内容添加到暂存区,在版本库中生成blob文件
git add .

屏幕快照 2022-01-30 下午12.43.28.png

  1. 对暂存区生成快照tree文件
git commit -m first

屏幕快照 2022-01-30 下午12.47.20.png

  1. 生成第一个版本commit文件,其文件名则是我们平常使用的commitID

屏幕快照 2022-01-30 下午12.48.36.png

  1. 我们更新a.txt
echo a2 > a.txt

屏幕快照 2022-01-30 下午12.52.09.png

  1. 将更新同步到暂存区
git add a.txt

屏幕快照 2022-01-30 下午1.24.37.png

  1. 生成新的版本
git commit -m second

屏幕快照 2022-01-30 下午1.01.14.png

抛出几个思考

  1. 会不会有重复的commitID

在我们上面分析版本实现的逻辑上,实际可以发现版本库实际实现了一颗哈希树。在哈希树中,其哈希值通过子节点进行哈希算法得到且不会重复。

  1. 我们能否篡改某个commit的内容而不被发现?

实际Git是提供了命令让我们回退到中途某个版本进行修改的,但是我们修改内容后没法不被发现。同样因为哈希树的实现,当我们修改某个commit版本的时候,其comnitId肯定是会更改的,此时前后面对应的commit文件中保存的parentId会改变,所以此时参与哈希计算的内容(parentId)实际会改变导致后面版本的commitID都会跟着改变。

  1. 对于修改内容,版本中是保存增量还是全量数据?

通过上文的分析,实际已经可以知道答案。Git中保存了每次更新后的全量文件,这也是为什么我们切换版本或者切换分支会非常快的原因,省去了遍历不同版本应用patch的操作。当然每次保存更新后的全量文件会使我们的仓库变得非常大,而Git相应的会对仓库下的文件进行打包压缩来减小仓库体积,打包后的结果放在前文所说的pack文件夹下面。

结语

本篇文章主要分析了Git仓库的存储机制,其是如何保存不同版本的内容。

参考