普通Tree对象
Root Tree是一种逻辑含义上特殊的 tree 对象, 因为它是由 Git 自动生成的对某个目录的映射或索引. 但是在物理含义上, 它与普通的tree对象并没有本质差别, 我们尝试通过新增目录的方式, 来进一步了解 tree 对象:
➜ mkdir src docs
➜ echo "abc" > src/main.c
➜ echo "efg" > docs/project.txt
➜ git add src docs
➜ git commit -s
[master 87902f9] COMMIT B
2 files changed, 2 insertions(+)
create mode 100644 src/main.c
create mode 100644 src/project.txt
➜
➜ git cat-file -p master
tree 2e6cdc032db0ecfa4e3e898b8f8551acce10db11
parent 3de31e208ae26c6b44d9e6c4a3f0adb32d0c68b6
author Dyrone Teng <tenglong.tl@alibaba-inc.com> 1633681481 +0800
committer Dyrone Teng <tenglong.tl@alibaba-inc.com> 1633681627 +0800
COMMIT B
Signed-off-by: Dyrone Teng <tenglong.tl@alibaba-inc.com>
➜
➜ git cat-file -p 2e6cdc032db0ecfa4e3e898b8f8551acce10db11
100644 blob 8178c76d627cade75005b40711b92f4177bc6cfc README.md
040000 tree 99b20b290a90e1137e0487f955620db05b229abe docs
040000 tree 8db636fa1dbc4d704179895bdb6e4322f32cbda6 src
➜
➜ git cat-file -p 99b20b290a90e1137e0487f955620db05b229abe
100644 blob 1505b408c2ef47655f603b9045464e642ab28d97 project.txt
➜
➜ git cat-file -p 8db636fa1dbc4d704179895bdb6e4322f32cbda6
100644 blob 8baef1b4abc478178b004d62031cf7fe6db6f903 main.c
我们在工作空间中, 新创建了两个文本文件为 main.c 和 project.txt 并分别存放在新的目录 src 和 docs 下, 随后对当前工作空间的更改进行提交。
通过 git cat-file -p master 来查看 master 分支 (Git自动为我们生成的默认分支名) 上最近提交的root-tree 对象, 随后进一步通过该子命令进行递归查看. 我们可以从输出结果分析出以下绩点(仅分析 tree 和 blob 对象):
· 创建 blob 对象 1505b408c2ef47655f603b9045464e642ab28d97 用于存放 project.txt 文件的文本内容;
· 创建 blob 对象 8baef1b4abc478178b004d62031cf7fe6db6f903 用于存放 main.c 文件的文本内容;
· 创建 tree 对象 8db636fa1dbc4d704179895bdb6e4322f32cbda6 用于存放 src 目录索引信息, 其中包含了 main.c 文件的名称、类型和模式等信息;
· 创建 tree 对象 99b20b290a90e1137e0487f955620db05b229abe 用于存放 docs 目录索引信息, 其中包含了 project.txt 文件的名称、类型和模式等信息;
· 创建 tree 对象 (root-tree) 2e6cdc032db0ecfa4e3e898b8f8551acce10db11 用于存放当前仓库目录的索引信息, 其中包含了 src 以及 docs 目录以及 README.md 文件的名称、类型和模式等信息;
大家是否注意到, 当我们使用 cat-file 查看一个tree对象时, 其中不仅包含了文件名、OID和对象类型, 还输出了类似 100644 以及 040000 等信息, 这些信息则代表着文件的 类型 和 模式 .。
文件的类型(type)与模式(mode)
文件类型 (https://en.wikipedia.org/wiki/Unix_file_types) 和 模式 (https://en.wikipedia.org/wiki/File-system_permissions), 是源于Unix文件系统中的设计, 而在Git中则进行了选择性的沿用, 可以认为是一个Unix文件结构设计的迷你版。
Git采用32位长存储文件模式,从高位到低位依次:
· 4位: 保存对象类型, 二进制的有效值为 1000(常规文件)、1010(符号链接) 和 1110 (gitlinks)
· 3位: 保留
· 9位: unix 权限格式, 只有 0755 和 0644 对常规文件有效, 符号链接和 gitlinks 在该字段中的值为0.
目前的模式有以下几种:
· 0100000000000000 (040000): 目录;
· 1000000110100100 (100644): 普通文件(非可执行);
· 1000000110110100 (100664): 普通文件(非可执行、group可写);
· 1000000111101101 (100755): 普通文件(可执行);
· 1010000000000000 (120000): 符号链接(Symbolic link);
· 1110000000000000 (160000): Gitlink(用于Git Submodule);
Commit
通过上面的介绍, 我们已经大体了解了blob和tree的用途, 其中treee区别了不同状态下仓库内容的快照, 但光有这些还是不够的, 因为这时候还缺少如下的信息:
· 无法追溯 : 仅有快照, 相当于有了 What . 但快照和快照之间的联系没有建立起来, 如前面介绍它们只是松散存储的Tree对象, 没有形成历史结构存储下来。
· Who : 不知道快照的参与者是谁。
· Why/How : 不知道当初创建快照的原因。
· When : 不知道什么时间创建了快照。
提交 (commit) 对象用来解决这些问题
· 关联快照 : commit引用了一个 _root-tree_对象, 即仓库顶层目录的快照, 从而将提交对象和树对象关联。
· Who : commit对象中记录了作者、提交人以及其他参与者的信息, 例如可以通过 git commit 子命令的 -signoff 选项, 在提交末尾添加 Signed-off-by (授权提交) 信息. 诸如此类的还有:
o Reported-and-tested-by: reported and tested the issue (I assume);
o Acked-by: acknowledged by the owner of the code, “looks good to me” ;
o Reviewed-by: reviewed;
o Cc: was notified about the patch;
· When : 提交中通过时间戳记录了创建的时间以及提交的时间. 这是因为, Git的作者和提交者可以是不同的人, 对应的创作时间和提交时间自然就可能不同, 我们可以执行 git log --format=fuller 查看包含了上述全部信息的提交历史列表。
· Why/How : 提交中还保存了提交的描述信息, 其中可以记录本次提交的原因、背景和实现策略等. 一般包含三部分 (关于如何写好你的提交信息, 在后续的文章中我的同事赵鹏飞会为大家详细介绍):
o 一行标题;
o 一行空行;
o 可以包含多个段落的具体描述;
· 可追溯 : Git commit中可以存储包含指向父提交的引用(parent), 通过这种方式, 新的 commit 和旧的 commits 之间就构成了一个有向无环图 (DAG), 我们可以从图中搜索 commit 并递归追溯其所有的祖先提交 (ancestors)。 通常, 一个提交可以有0个或n个父提交, 情况分别为:
o 0个 : 当提交为根提交(root-commit) 或者 使用 git commit-tree 子命令(不加 -p 选项)直接创建的commit对象。
o 1个 : 非根提交, 或非合并提交的情况下
o 2个 : 合并提交(merged-commit)
· 一个较好的参考示例:
为了很好的理解上面的几点, 我们可以在刚才创建的 test.git 中执行:
➜ git log --parents --pretty=fuller
commit 6ae8bbeeb4dba091d3c6295d0b3d0f9a3863d32f 3de31e208ae26c6b44d9e6c4a3f0adb32d0c68b6 (HEAD -> master)
Author: Dyrone Teng <tenglong.tl@alibaba-inc.com>
AuthorDate: Fri Oct 8 16:24:41 2021 +0800
Commit: Dyrone Teng <tenglong.tl@alibaba-inc.com>
CommitDate: Fri Oct 8 16:27:07 2021 +0800
COMMIT B
Signed-off-by: Dyrone Teng <tenglong.tl@alibaba-inc.com>
commit 3de31e208ae26c6b44d9e6c4a3f0adb32d0c68b6
Author: Dyrone Teng <tenglong.tl@alibaba-inc.com>
AuthorDate: Fri Oct 8 15:45:01 2021 +0800
Commit: Dyrone Teng <tenglong.tl@alibaba-inc.com>
CommitDate: Fri Oct 8 15:45:01 2021 +0800
COMMIT A
Signed-off-by: Dyrone Teng <tenglong.tl@alibaba-inc.com>
我们通过添加 --parents 选项, 让 git log 子命令输出 parents commit的相关信息, 同时还添加了 --pretty=fuller 选项, 让输出结果中同时展示作者, 提交者, 创作时间和提交时间。
小结
Git对于每一次的文件内容变化, 都会单独保存为一个zlib压缩过的blob对象(文件), 进一步通过commit和tree将文件和目录组织起来形成快照, 并将提交和快照关联起来, 建立提交之间的DAG。
这种设计, 让版本管理变得简单可靠, 但同时, 也不可避免的会造成 Git 仓库存储的 快速膨胀 。 Git可以通过将松散的对象打包为 pack 文件, 来解决这个问题。 除此以外, pack中还支持保存delta的方式(delta compression), 保存blobs之间的 delta 差异 (2进制则会失效)。
Tag
标签 (tags) 的概念并非Git独创, 通常用来记录被认为很重要的提交, 例如软件的发行版本。
轻量级标签 (Lightweight Tags)
我们可以创建一个不带有任何额外信息的tag, Git称这种类型的标签为 轻量级tag (lightweight)。 "签如其名", 轻量级tag看上去与一个 commit 并无太多差别, 事实也确实是这样, 因为其不会保存额外的信息, 只是一个commit的指针, 我们可以在 _test.git 中创建一个名为 v1.0 的轻量级tag:
➜ git tag v1.0
➜ git cat-file -t v1.0
commit
可以看到, 标签名也支持使用 git cat-file 子命令, 我们可以看到其对象类型为 commit。
附注标签 (Annotated Tags)
另外一类标签叫做 附注标签 (Annotated Tags) , 附注标签可以记录额外的信息。 例如: 如果一个 tag 被用来标记发行版本, 就可以通过附注的信息记录本次发布了什么内容。 此外, 附注标签还可以记录标签的 作者 (tagger) 、邮箱 (tagger email) 以及 时间戳 , 这些都可以理解为标签说被附注的 元数据(meta-data) 。 最后, 附注标签还支持使用默认配置的邮箱信息, 用于生成 GPG 签名信息并附注在tag中。
附注标签与轻量级标签不同的是, 附注标签会以tag对象类型进行保存:
➜ git tag -a v2.0 -m "create v2.0"
➜
➜ git cat-file -t v2.0
tag
➜
➜ git cat-file tag v2.0
object 6ae8bbeeb4dba091d3c6295d0b3d0f9a3863d32f
type commit
tag v2.0
tagger Dyrone Teng <tenglong.tl@alibaba-inc.com> 1633919318 +0800
create v2.0
如上所示, 我们创建了一个名为 v2.0 的附注标签, 其描述信息为 create v2.0 。我们可以使用 cat-file 子命令查看该标签会发现其对象类型为 tag 。 进一步, 如果我们希望查看tag的附注信息, 我们可以执行 git cat-file tag <tag_name> 子命令完成。