目录
- 一、知识回顾
- 1.1 Linux 基础命令
- 1.2 .git 文件夹解析
- 二、git 对象(数据对象)
- 2.1 hash-object 存储对象
- 2.2 cat-file 查看对象
- 三、树对象
- 3.1 ls-files 查看暂存区
- 3.2 update-index 创建暂存区
- 3.3 write-tree 生成树对象
- 3.4 更新暂存区,生成第二棵树
- 3.5 read-tree 树A加入树B,生成第三棵树
- 3.6 查看树对象
- 四、提交对象
- 4.1 commit-tree 创建提交对象
- 4.2 查看提交对象
- 4.3 创建第二次提交对象
- 五、总结
- 官网地址: https://www.git-scm.com/
- 官方文档: https://www.git-scm.com/docs
- 官方电子书: https://git-scm.com/book/zh/v2
- GitHub: https://github.com/git/git
一、知识回顾
1.1 Linux 基础命令
在开始学习 Git 的底层命令之前,我们先来回顾一下 Linux 的基础命令:
-
clear
:清除屏幕。 -
echo 'test content'
:将信息输出到控制台。 -
echo 'test content' > test.txt
:将信息输出到 test.txt 文件中。 -
ll
:查看当前目录下的文件详细信息。 -
find 目录名
:查看对应目录下的子孙目录和文件。 -
find 目录名 -type f
:查看对应目录下的子孙文件。 -
rm 文件名
:删除文件 -
mv 文件名A 文件名B
:移动、重命名文件A为文件B。 -
cat 文件名
:查看文件内容。 -
vim 文件名
:查看、编辑文件。按 i 进入插入模式,进行文件的编辑;
按 Esc 退出插入模式;
输入
:q!
强制退出(不保存);输入
:wq
写入退出;输入
:set nu
查看行号。
1.2 .git 文件夹解析
.git 文件夹内容如下:
二、git 对象(数据对象)
Git 的核心部分是一个简单的 键值对数据库。我们可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次取回该内容。我们根据在数据库中存储的数据类型进行了如下分类:
数据类型 | 对象类型 |
---|---|
blob | git 对象(数据对象) |
tree | 树对象 |
commit | 提交对象 |
2.1 hash-object 存储对象
git hash-object
命令可将任意数据保存于 .git/objects
目录(即 对象数据库),并返回指向该数据对象的唯一键值。
-w
选项会指示该命令不要只返回键,还要将该对象写入数据库中。--stdin
选项则指示该命令从标准输入读取内容;若不指定此选项,则在命令尾部给出带存储文件的路径。
示例一:echo 'test content' | git hash-object -w --stdin
示例二:git hash-object -w test.txt
向数据库写入内容,并返回对应键值:
首先,我们需要初始化一个新的 Git 版本库,并确认 .git/objects
目录不包含文件:
# 初始化新仓库
$ git init test
Initialized empty Git repository in /tmp/test/.git/
# 进入目录
$ cd test
# 查看目录和文件
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
# 确认 objects 目录中不包含文件
$ find .git/objects -type f
可以看到 Git 对 objects
目录进行了初始化,并创建了 pack
和 info
子目录,但均为空。接着,我们用 git hash-object
创建一个新的数据对象并将它手动存入新 Git 数据库中:
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
在这种最简单的形式中,git hash-object
会接受你传给它的东西,而它只会返回可以存储在 Git 仓库中的唯一键。
git hash-object
命令输出一个长度为 40 个字符的校验和。这是一个 SHA-1 哈希值——一个将带存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。现在我们查看 Git 是如何存储数据的:
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
如果再次查看 objects
目录,那么可以在其中找到一个与新内容对应的文件。这就是开始时 Git 存储内容的方式——一个文件对应一条内容,以改内容加上特定头部信息一起的 SHA-1 校验和为文件命名。校验和的前两个字符用于命名子目录,余下的 38 个字符用作文件名。
2.2 cat-file 查看对象
cat-file
命令可以从 Git 那里取回存储在对象数据库中的内容。
-p
选项可指示该命令自动判断内容的类型,并为我们显示大致的内容。-t
选项可显示内部存储的对象类型。
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
至此,我们已经掌握了如何向 Git 中存入内容,以及如何将它们取出。我们同样可以将这些操作应用与文件中的内容。例如,可以对一个文件进行简单的版本控制。首先,创建一个新文件并将其内容存入数据库:
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
接着,向文件里写入新内容,并再次将其存入数据库:
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
对象数据库记录下了该文件的两个不同版本,当然之前我们存入的第一条内容也还在:
$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
现在可以再删掉 test.txt
的本地副本,然后用 Git 从对象数据库中取回它的第一个版本:
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1
或者取回第二个版本:
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2
然而,记住文件的每一个版本所对应的 SHA-1 值并不现实;另一个问题是:再这个简单的版本控制系统中,文件名并没有被保存——我们仅保存了文件的内容。上述类型的对象我们称之为 数据对象(blob object)。利用 git cat-file -t
命令,我们就可以根据对象的 SHA-1 值获取 Git 内部存储的对象类型。
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
问题:
- 记住文件的每一个版本所对应的 SHA-1 值并不现实。
- 在 Git 中,我们仅保存了文件内容,文件名没有被保存。
解决方案:树对象
三、树对象
树对象(tree object)
,它能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git 以一种类似于 UNIX
文件系统的方式存储内容,但做了些许简化。所有内容均以树对象和数据对象(git对象)的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象(git对象)则大致上对应了 inodes 或文件内容。一个树对象包含了一条或多条树对象记录(tree entry)
,每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。一个树对象
我们可以通过 update-index
、write-tree
、read-tree
等命令来构建树对象并塞入到暂存区。
在讲构建树对象之前,我们需要先了解一个命令 git ls-files
。
3.1 ls-files 查看暂存区
git ls-files -s
:查看暂存区当前的样子。
-s
选项来显示文件的状态。
3.2 update-index 创建暂存区
首先,清空目录,初始化一个新的仓库,并将 test.txt 的首个版本放入数据库。
# 初始化新的仓库
$ git init
Initialized empty Git repository in C:/Users/lenovo/Desktop/test/.git/
# 创建首个版本
$ echo 'test.txt v1' > test.txt
# 放入数据库
$ git hash-object -w test.txt
warning: in the working copy of 'test.txt', LF will be replaced by CRLF the next time Git touches it
560a3d89bf36ea10794402f6664740c284d4ae3b
# 查看暂存区
$ git ls-files -s
我们可以看到,此时暂存区为空。然后我们使用 update-index
命令为 test.txt 的首个版本创建一个暂存区:
# 创建暂存区
$ git update-index --add --cacheinfo 100644 560a3d89bf36ea10794402f6664740c284d4ae3b test.txt
-
--add
选项:因为此前该文件并不在暂存区中,首次需要 --add。 -
--cacheinfo
选项:因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下,所以需要 --cache。 -
文件模式:
100644
,表明这是一个普通文件;100755
,表示一个可执行文件;120000
,表示一个符号链接。
再次查看暂存区:
$ git ls-files -s
100644 560a3d89bf36ea10794402f6664740c284d4ae3b 0 test.txt
这样我们的 SHA-1 键值和文件名就对应上了。然后,我们再去数据库看一下有没有新增内容:
$ find .git/objects/ -type f
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b
发现没有新增内容,还是只有首个版本的 test.txt,这就说明 update-index
只在暂存区内生成数据,并不会操作数据库。
3.3 write-tree 生成树对象
git write-tree
命令是将暂存区做一个快照。相当于给暂存区拍张照,生成一个树对象,放到数据库里面去。
继续实战,将 3.2 中暂存区中的内容生成一个树对象:
$ git write-tree
06e21bb0105e2de6c846725a9a7172f57dd1af96
接下来,我们看一下返回的哈希所对应的数据类型:
$ git cat-file -t 06e21bb0105e2de6c846725a9a7172f57dd1af96
tree
我们可以看到,write-tree
命令生成的数据类型为 树对象
。然后我们看一下这个树对象在不在我们的版本库(数据库)里面:
$ find .git/objects/ -type f
.git/objects/06/e21bb0105e2de6c846725a9a7172f57dd1af96
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b
可以看到数据库中的对象增加了一个刚放进去的数据对象。
由此可知,我们可以通过多次执行 update-index
命令,在缓存区中存放多个内容,然后通过 write-tree
命令一次性将所有暂存区的内容生成快照,保存为树对象。
总结:
git 对象
,代表文件的初始版本;树对象
,代表项目的初始版本。
补充:上面执行了 write-tree
生成树对象后,我们可以再去看一下暂存区中还有没有内容:
$ git ls-files -s
100644 560a3d89bf36ea10794402f6664740c284d4ae3b 0 test.txt
由执行结果可知,write-tree
命令将暂存区生成快照保存为树对象后,并不会将暂存区清空。
3.4 更新暂存区,生成第二棵树
接下来,我们继续操作:
1)新增 new.txt 到数据库:
# 新增一个 new.txt 文件
$ echo "new v1" > new.txt
# 生成 git 对象
$ git hash-object -w new.txt
warning: in the working copy of 'new.txt', LF will be replaced by CRLF the next time Git touches it
eae614245cc5faa121ed130b4eba7f9afbcc7cd9
# 查看数据库
$ find .git/objects/ -type f
.git/objects/06/e21bb0105e2de6c846725a9a7172f57dd1af96
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9
2)创建 test.txt 的第二个版本:
# 编辑 test.txt
$ vi test.txt
# 查看 test.txt
$ cat test.txt
test.txt v1
test.txt v2
# 生成 git 对象
$ git hash-object -w test.txt
warning: in the working copy of 'test.txt', LF will be replaced by CRLF the next time Git touches it
c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba
3)查看数据库:
$ find .git/objects/ -type f
.git/objects/06/e21bb0105e2de6c846725a9a7172f57dd1af96 # workspace项目的第一个版本(树对象)
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b # test.txt文件的第一个版本(git对象)
.git/objects/c3/1fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba # test.txt文件的第二个版本(git对象)
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9 # new.txt文件的第一个版本(git对象)
4)生成项目的第二个版本
数据库中的前两个对象属于项目的第一个版本,我们可以将后面两个对象生成项目的第二个版本:
# 更新暂存区 test.txt
$ git update-index --cacheinfo 100644 c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba test.txt
# 查看暂存区
$ git ls-files -s
100644 c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba 0 test.txt
可以发现由于暂存区中 test.txt 的更新,对应的哈希值也随之更新了。然后我们继续将 new.txt 放入暂存区,并生成项目第二个版本的树对象:
# 创建缓存区 new.txt
$ git update-index --add --cacheinfo 100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 new.txt
# 查看缓存区
$ git ls-files -s
100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 0 new.txt
100644 c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba 0 test.txt
# 生成树对象
$ git write-tree
9d74ec4055e0f1edc1921d749c250380ca7b5ebd
# 查看数据库
补充: git update-index
命令后面加 --add
选项,可以直接将文件放到数据库中,然后再为文件创建、更新缓存区。例如:上面 new.txt 文件的 git hash-object
命令和 git update-index
命令可以合并为:
$ git update-index --add new.txt
5)查看数据库
$ find .git/objects/ -type f
.git/objects/06/e21bb0105e2de6c846725a9a7172f57dd1af96 # workspace项目的第一个版本(树对象)
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b # test.txt文件的第一个版本(git对象)
.git/objects/9d/74ec4055e0f1edc1921d749c250380ca7b5ebd # workpace项目的第二个版本(树对象)
.git/objects/c3/1fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba # test.txt文件的第二个版本(git对象)
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9 # new.txt文件的第一个版本(git对象)
此时,数据库中共 5 个对象,项目的两个版本的对象关系如下:
3.5 read-tree 树A加入树B,生成第三棵树
git read-tree
命令是将一棵树读取到暂存区。然后配合 write-tree
命令再将暂存区重新做一个快照,生成一个树对象,放到数据库里面去。
--prefix=bak
选项指定了树的前缀为“bak”。
继续实战,将 3.3 中的树对象加入到 3.4 中的树对象:
# 从第一棵树读取到暂存区
$ git read-tree --prefix=bak 06e21bb0105e2de6c846725a9a7172f57dd1af96
# 查看暂存区
$ git ls-files -s
100644 560a3d89bf36ea10794402f6664740c284d4ae3b 0 bak/test.txt
100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 0 new.txt
100644 c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba 0 test.txt
我们可以看到,读取后,暂存区中增加了一个 bak
前缀的 test.txt 文件。我们继续将暂存区生成第三棵树:
# 生成新的树对象
$ git write-tree
17d1ee3eac87d38448e7ff2cc92e88ed4e9aa094
# 查看数据库
$ find .git/objects/ -type f
.git/objects/06/e21bb0105e2de6c846725a9a7172f57dd1af96 # workspace项目的第一个版本(树对象)
.git/objects/17/d1ee3eac87d38448e7ff2cc92e88ed4e9aa094 # workspace项目的第三个版本(树对象)
.git/objects/56/0a3d89bf36ea10794402f6664740c284d4ae3b # test.txt文件的第一个版本(git对象)
.git/objects/9d/74ec4055e0f1edc1921d749c250380ca7b5ebd # workpace项目的第二个版本(树对象)
.git/objects/c3/1fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba # test.txt文件的第二个版本(git对象)
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9 # new.txt文件的第一个版本(git对象)
目前上面的数据结构,我们可以整理为下图:(图中同时包含了三棵树)
补充: read-tree 命令仅作了解,实际过程中,我们并不会将一棵树加入另一棵树中。
3.6 查看树对象
由于树对象和 git 对象一样,都是存储在数据库中,我们使用 git cat-file
命令查看即可:
$ git cat-file -p 17d1ee3eac87d38448e7ff2cc92e88ed4e9aa094
040000 tree 06e21bb0105e2de6c846725a9a7172f57dd1af96 bak
100644 blob eae614245cc5faa121ed130b4eba7f9afbcc7cd9 new.txt
100644 blob c31fb1e89d8b6b3ef34cdb5a2f999d6e29b822ba test.txt
问题:
- 树对象的提交只有哈希和文件名,我们看不到文件版本的相关说明。
解决方案:提交对象
四、提交对象
4.1 commit-tree 创建提交对象
我们可以通过调用 commit-tree
命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果有的话,需要指定。第一次将暂存区做快照就没有父对象)。
-p
选项可以用于指定父提交对象的 SHA-1 值。
注意:
git commit-tree
不但生成提交对象,而且会将对象的快照(树对象)提交到本地库中。
我们先来创建第一棵树的提交对象:
# 创建提交对象
$ echo 'first commit' | git commit-tree 06e21bb0105e2de6c846725a9a7172f57dd1af96
92f28813cce27fa514cf3be80a4d4b158afdca27
4.2 查看提交对象
由于提交对象和 git 对象、树对象一样,都是存储在数据库中,我们使用 git cat-file
命令查看即可:
# 查看数据类型
$ git cat-file -t 92f28813cce27fa514cf3be80a4d4b158afdca27
commit
# 查看数据内容
$ git cat-file -p 92f28813cce27fa514cf3be80a4d4b158afdca27
tree 06e21bb0105e2de6c846725a9a7172f57dd1af96 # 树对象
author acgkaka <acgkaka@example.com> 1698337169 +0800 # 作者
committer acgkaka <acgkaka@example.com> 1698337169 +0800 # 提交者
first commit # 提交注释
4.3 创建第二次提交对象
接下来,我们可以创建第二次提交对象,然后将第一次提交对象作为父对象:
补充: 这里可以只用树对象和父提交对象 SHA-1 值的前部分(一般6位)即可。
# 创建提交对象
$ echo 'second commit' | git commit-tree 9d74ec -p 92f288
1be40a21a3cc7c92e73721967abe30c9ce2a5a51
# 查看提交对象
$ git cat-file -p 1be40a
tree 9d74ec4055e0f1edc1921d749c250380ca7b5ebd # 树对象
parent 92f28813cce27fa514cf3be80a4d4b158afdca27 # 父提交对象
author acgkaka <acgkaka@example.com> 1698338744 +0800 #
committer acgkaka <acgkaka@example.com> 1698338744 +0800
second commit
第三次提交对象以此类推。三次提交对象创建完毕后,图示如下:
五、总结
git对象
里面存储的不是增量,而是一次快照。树对象
才是真正的一个项目版本的快照;提交对象
只是对树对象做了一次封装。
补充: 提交对象也说明了 为什么 Git 回滚如此简单,因为每个分支名实际相当于一个指向提交对象的指针,当进行版本回退时,只需要将指针调整到需要的提交对象即可,其他地方均无需改动。
整理完毕,完结撒花~ 🌻
参考地址:
1.深入Git底层原理丨一套掌握git版本控制系统,https://www.bilibili.com/video/BV1Yi4y137eF