Git 原理浅析

97 阅读12分钟

前言

尽管Git实现了数不胜数的功能,但他的基础在于对文件变更过程的存储

所以,如果问“Git能够解决哪些问题?我们可以简单的回答:

  • Git解决了版本控制方面的很多问题,但最核心的是它很好的解决了版本状态存储(即文件变更过程存储) 的问题。
  • 版本状态/文件变更过程状态:谁改的?改了什么?什么时间,什么原因改的?

在开篇分析场景的时候我们提到,对于不同的版本内容,我们会使用存放多个副本的方式存储。事实上,GIt底层的存储方式非常类似。

一、Git数据库

我们在向Git提交版本的时候,Git会把这个版本完整的保存下来,不同于我们我们使用文件来存储,版本提交会被GIt系统保存在Git数据库中。

在这里介绍三个指令:

  • git init 用于创建一个空的git仓库,或重置一个已存在的git仓库
  • git hash-object git底层命令,用于向Git数据库中写入数据
  • git cat-file git底层命令,用于查看Git数据库中数据

1.1 写数据

# 初始化数据库test(或者新建文件夹test,进入文件夹再执行git init)
git init test# 向 git 数据库存入一些数据,返回一串值
echo "version 1" | git hash-object -w --stdin
83baae61804e65cc73a7201a7252750c76066a30
# 返回了什么? ----> 一串长度为40的hash值
# 查看文件夹,观察git数据库的存储方式  ---> 文件
find .git/objects/ -type f
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30

Git数据库是一种键值对(key-value,简称KV,著名的KV数据库有 Redis)数据库

在Git,你输入任何内容到Git数据库,他会为你返回其hash值,该hash只和内容有关,见如下演示。

# 向 file.txt 文件写入version 1
echo "version 1" > file.txt
git hash-object -w file.txt
83baae61804e65cc73a7201a7252750c76066a30
​
# 查看数据库存储情况
find .git/objects -type f
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30

1.2 读数据

# 读数据类型
git cat-file -t  83baa
blob
# 读数据内容
git cat-file -p 83baa
version 1

1.3 数据库存储类型

git数据库能够存储的对象类型有三种

  1. blob:数据对象
  2. tree:树对象
  3. commit:提交对象

1.4 文件变更模拟

掌握了Git数据库的读写方式,我们就可以模拟文件变更在底层的实现过程了。

# 文件写入数据
echo "version 1" > file.txt
# 文件保存进数据库
git hash-object -w file.txt
83baae61804e65cc73a7201a7252750c76066a30
​
# 修改文件内容
echo “version 2” > file.txt
# 文件保存进数据库
git hash-object -w file.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
# 查看数据库
find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
​
# 查看数据
git cat-file -p 83baa
version 1
# 查看数据
git cat-file -p 1f7a7a
version 2
​
# 观察文本
cat file.txt
version 2
# 版本回退
git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > file.txt
# 再次观察文本
cat file.txt
version 1

以上是版本回退的原理

1.5 问题

解决了数据的读写问题,还存在以下问题:

  1. 无法记录文件名,文件夹的变化
  2. 无法得知文件的变更时间顺序
  3. 缺少对每一次版本变化的说明
  4. 记忆每一个版本对应的 hash值 无聊且乏味 且不可能

二、树对象(tree object)

Git利用树对象(tree object)解决文件名保存和文件组织的问题,树对象也能够将多个文件组织在一起。

Git通过树(tree)对象将数据(blob)对象组织起来,这很类似于一种文件系统——blob对象对应文件内容,tree对象对应文件的目录和节点。一个树(tree)对象包含一条或多条记录,每条记录含有一个指向blob对象或tree对象的指针,以及相应的模式、类型、文件名。

有了树对象,我们就可以将文件系统任何时间点的状态保存在git数据库中。

通常,Git根据某一时刻暂存区所表示的状态创建并记录一个对应的树对象,如此重复便可以依次记录一系列的树对象。

Git的暂存区是一个文件——.git/index。下面,我们通过创建树对象的过程来认识暂存区和树对象。

为了创建一个树对象,我们需要通过暂存一些文件来创建一个暂存区。为此我们引入两个命令:

  • git update-index git底层命令,用于创建暂存区
  • git ls-files --stage git底层命令,用于查看暂存区内容
  • git write-tree git底层命令,用于将暂存区内容写入一个树对象

2.1 创建树对象

我们将file.txt的第一个版本放入暂存区,执行

# 寻找.git/index,发现不存在,说明缓存区还没有建立# 创建暂存区·
git update-index --add file.txt
​
# 再次查看暂存区文件,发现已存在# 看暂存区写了什么
cat .git/index
DIRC[���$�;�[���$�;�A����
���a�Ne�s� rRu
               vjfile.txt�݀3%A��,I� �`
​
# 查看数据库,只有之前的 version 1 和 version 2 两个数据文件
find .git/objects/ -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
​
# 查看缓存区
git ls-files --stage
100644 83baae61804e65cc73a7201a7252750c76066a30 0    file.txt
​
# 生成树对象
git write-tree
391a4e90ba882dbc9ea93855103f6b1fa6791cf6
​
# 查看数据库,新增树对象
find .git/objects/ -type f
.git/objects/39/1a4e90ba882dbc9ea93855103f6b1fa6791cf6
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
​
# 查看树对象
git cat-file -t 391a4e90ba882dbc9ea93855103f6b1fa6791cf6
tree
git cat-file -p 391a4e90ba882dbc9ea93855103f6b1fa6791cf6
100644 blob 83baae61804e65cc73a7201a7252750c76066a30    file.txt  

以上我们添加了一个已经存在在git数据库中的文件到暂存区,如果我们新建一个未曾保存到git数据库的文件存入暂存区,进而保存为tree对象,会有什么不同吗? 我们试试看。执行

# 新建文件
echo "new file" > new
​
# 创建暂存区
git update-index --add new
​
# 查看git数据库所有数据
find .git/objects/ -type f
.git/objects/39/1a4e90ba882dbc9ea93855103f6b1fa6791cf6 #tree对象
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a #blob对象
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 #blob对象
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 #新增的blob对象# 生成树对象
git write-tree
228e49bb0bf19df94b49c3474f5d4ee55a371fbe #新生成的tree对象键# 查看数据库
find .git/objects/ -type f
.git/objects/39/1a4e90ba882dbc9ea93855103f6b1fa6791cf6
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92
.git/objects/22/8e49bb0bf19df94b49c3474f5d4ee55a371fbe #新增的tree对象# 查看缓冲区
git ls-files --stage
100644 83baae61804e65cc73a7201a7252750c76066a30 0    file.txt
100644 fa49b077972391ad58037050f2a75f74e3671e92 0    new

由执行结果我们可以看到,这一次执行update-index之后,和上次不同,git数据库发生了变化,新增加了一条hash键为“fa49b0”的数据;暂存区中也多出了文件new的信息。

这说明两个问题:

  • 如果添加git数据库中尚未存储的数据到暂存区,则在执行update-index的时候,会同时把该数据保存到git数据库。
  • 添加文件进入暂存区的操作是追加操作,之前已经加入暂存区的文件依然存在——很多人会有误区,认为变更提交之后,暂存区就清空了。

至此,在git数据库中,我们可以完整的记录文件的状态、文件夹的状态;并且可以把多个文件或文件夹组织在一起,记录他们的变更过程。

接下来,我们只要把数据库中各个版本的时序关系记录下来,再把对每一个版本更新的注释记录下来,不就完成了一个逻辑简单、功能强大、操作灵活的版本控制系统吗?

那么,如何记录版本的时序关系,如何记录版本的更新注释呢?这就要引入另一个git数据对象——提交对象(commit object)。

三、提交对象(commit object)

记录版本间的时序关系和版本注释

commit对象能够帮你记录什么时间,由什么人,因为什么原因提交了一个新的版本,这个新的版本的父版本又是谁。

git提供了底层命令commit-tree来创建提交对象(commit object),我们需要为这个命令指定一个被提交的树对象的hash键,以及该提交对象的父提交对象(如果是第一次提交,不需要指定父对象)。

# 将内存区的数据生成树对象
git write-tree
228e49bb0bf19df94b49c3474f5d4ee55a371fbe
​
# 生成提交(commit)对象
git commit-tree cb0fbcc -m "first commit"
a9deef3921f2470c98dbb6a241c506ca0553086d
​
# 查看提交对象
git cat-file 7020a97
tree 228e49bb0bf19df94b49c3474f5d4ee55a371fbe
author kuku123123 <1150717403@qq.com> 1636896168 +0800
committer kuku123123 <1150717403@qq.com> 1636896168 +0800
​
first commit
​
# 查看指定提交对象的提交记录
git log a9deef3921f2470c98dbb6a241c506ca0553086d

每次我们运行 git addgit commit 命令时, Git 所做的实质工作是将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。

然而,小问题依然存在,截止目前为止,我们对版本和数据对象的操作都是基于hash键的,这些毫无直观含义的字符串让人很头疼,不会有人愿意一直急着最新提交对应的hash键的。git不会允许这样的问题存在的,它通过引入“引用(references)”来解决这一问题。

四、Git的引用

Git的引用(references)保存在.git/refs目录下。git的引用类似于一个指针,它指向的是某一个hash键。

创建一个引用实在再简单不过。我们只需把一个git对象的hash键保存在以引用的名字命名的文件中即可。

# 将hash写入到master文件中,建立了对应hash的引用,引用的名字与文件名对应
echo "491404fa6e6f95eb14683c3c06d10ddc5f8e883f" > .git/refs/heads/master
​
# 查看引用文件
cat .git/refs/heads/master 
491404fa6e6f95eb14683c3c06d10ddc5f8e883f
​
# 查看提交记录,相同
git log a9deef3921f2470c98dbb6a241c506ca0553086d
git log master

参考