前言
尽管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数据库能够存储的对象类型有三种
- blob:数据对象
- tree:树对象
- 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 问题
解决了数据的读写问题,还存在以下问题:
- 无法记录文件名,文件夹的变化
- 无法得知文件的变更时间顺序
- 缺少对每一次版本变化的说明
- 记忆每一个版本对应的 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 add 和 git 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