Git源码解读
TIPS:本文对 Git 的介绍基于 Git(commit e83c5163316f89bfbde7d9ab23ca2e25604af290),这是 Git 的第一次提交。
结构初探
从 Github 上克隆 Git 源代码,将版本切换到第一个 commit,目录结构如下:
├── Makefile
├── README
├── cache.h
├── cat-file.c
├── commit-tree.c
├── init-db.c
├── read-cache.c
├── read-tree.c
├── show-diff.c
├── update-cache.c
├── write-tree.c
目录结构十分简单,先看看 README 文件的内容,了解一下 Linus 最初是怎么介绍 Git 的。
Git 的设计中有两类抽象:object database 和 current directory cache。
1. The Object Database
object database 是一系列 object 的集合。在这个集合中,object 间可以互相引用,它们之间形成一种层级关系。
在 Git 中不会直接操作一个原生的 object,而是操作处理后的 object,一个 object 会经过如下处理:
+------+ zlib +------+ SHA1 +------+
|object| +--------> | ... | +--------> | ... |
+------+ size+type +------+ +------+
TIPS:在压缩 object 前,会将其 size 和 type 写入起始位置一并压缩。
对于 object 来说,有以下两种 type:
BLOB
BLOB 具有下列属性:
- no refer to anything else
- no name
- no permission
Linus 将 BLOB 称为 file contents。既然有 file contents,那就还会有 “file attribute”,这就是 TREE:
TREE
结合 BLOB 不难推测,TREE 应该具有下列属性:
- refer to anything else
- name
- permission
CHANGESET
2. Current Directory Cache
NIL
初始化版本库
GIT - the stupid content tracker.
下面我们一步步来学习 Git 是如何做好一个 content tracker 的。在开始之前,需要对源码进行构建,才会得到相应的 Git 命令。
1. init-db
在 test_git 目录下执行 init_db
,会创建出如下目录结构:
test_git/
└── .dircache
└── objects
├── 00
├── 01
├── fe
...
└── ff
目录 test_git/.dircache/objects/
就是前面提过的 Object Database。
2. update-cache
假设现在有一个学生名单和教室名单:
root@master:~/Github/test_git# ls
students teachers
root@master:~/Github/test_git# cat students
Zhang San
root@master:~/Github/test_git# cat teachers
Mr. Wang
我们使用 Git 来跟踪这两个文件内容的变更,首先,Git 需要知道文件修改前的样子,我们使用 update-cache
来告诉它:
root@master:~/Github/test_git# ../git/update-cache teachers students
root@master:~/Github/test_git# tree .dircache/ -a
.dircache/
├── index
└── objects
├── 00
├── 01
...
├── 2f
│ └── 942698bb0133aa13b46a1001ec7a718dbc32b8
├── 30
...
├── 3a
│ └── c6ffce6b04e98c992a7dde76a5b7fab88be34f
├── 3b
...
└── ff
执行这个命令后,新增了 3 个文件,这 3 个文件描述了名单修改前的样子,看看这文件里的内容是什么:
root@master:~/Github# cat test_git/.dircache/index
CRIDw ??f??{B??7????????_b??-??_b??-????
:???k錙*}?v??????studentsroot@master:~/Github#
root@master:~/Github# cat test_git/.dircache/objects/3a/c6ffce6b04e98c992a7dde76a5b7fab88be34f
x?K??OR04`??H?KN??4?eroot@master:~/Github#
看来内容加密了,读一读源码 update_cache.c
看看到底发生了什么。update_cache.c
主流程如下:
main/
├── read_cache
├── verify_path
├── add_file_to_cache
│ └── index_fd
│ └── write_sha1_buffer
│ └── add_cache_entry
└── write_cache
函数名中的的 cache 也就是上面的 index 文件,也就是 Current Directory Cache。执行 update_cache students teachers
后,将 students 和 teachers 转换成 cache_entry
写入 index 文件中。index 文件的内容格式如下:
/*
+------------+
|cache_header|
+------------+
|cache_entry |
+------------+
| ... |
+------------+
|cache_entry |
+------------+
*/
struct cache_header {
unsigned int signature;
unsigned int version;
unsigned int entries;
unsigned char sha1[20];
};
struct cache_entry {
struct cache_time ctime;
struct cache_time mtime;
unsigned int st_dev;
unsigned int st_ino;
unsigned int st_mode;
unsigned int st_uid;
unsigned int st_gid;
unsigned int st_size;
unsigned char sha1[20]; // 用当前文件内容生成 sha1 的值,用 sha1 生成 blob 文件名
unsigned short namelen;
unsigned char name[0];
};
不难看出,index 文件中记录了 Git 当前跟踪的文件元信息。
update_cache 流程中,最重要的是函数 add_file_to_cache()
,它主要做了以下几件事:
- 在
cache_entry
中记录当下文件的元数据(file attribute?); - 将内容为:
blob ${size of students}${file content of students}
写入文件名:.dircache/objects/03/c6ffce6b04e98c992a7dde76a5b7fab88be34f; - 将文件的
cache_entry
加入 index,如果存在同名文件,则替换(即,index 文件中记录着 Git 追踪文件的最新信息)。
这下,我们总算知道了新增的文件中的内容了。至此,我们可以做一个小总结:
一个 Git 管理的代码库,分为 3 部分:
- 被管理的文件:记录着最新 file attribute 与 file content
- .dircache/index:记录着上一次 update-cache 的状态
- .dicache/objects/:记录着诸多历史 file content,它们由 Git 管理
思考:目前为止,我们已经接触到了 Current Directory Cache、BLOB 这两个概念,并对其有了一定认识。可仔细一想,index 中只是记录了上一次 update-cache 的状态,通过 index,只能找到 Object Database 中最近一个“快照”,无法找到历史 update-cache 产生的 object,而且,在 Object Database 中的 BLOB 是没有层级的。如果仅是这样,Git 还不是一个合格的「content tracker」。回想一下上文提到的 TREE,接下来应该有它的用武之地。
3. cat-file
这里先介绍一个能查看 object 的工具 cat-file
,它读取 object 的文件名,将文件内容以可读的形式写入一个临时文件,通过查看这个临时文件,就能知道 object 文件内容。
4. show-diff
- 读取 index 文件中的
cache_entry
,index 中记录着上一次 Git 追踪的文件元数据。通过 index 中cache_entry.name
找到文件最新的元数据,对比是否有变化:- 如果没有变化,则检查下一个文件
- 如果有变化,通过
cache_entry.sha1
找到相应的 object ,通过 diff 命令对比 object 与最新文件的内容
5. write-tree
root@master:~/Github/test_git# ../git/write-tree
5c58d749d3c05cd4088561011226cee78ca0221c
root@master:~/Github/test_git# tree .dircache/
.dircache/
├── index
└── objects
├── 00
├── 01
...
├── 2f
│ └── 942698bb0133aa13b46a1001ec7a718dbc32b8
├── 30
...
├── 3a
│ └── c6ffce6b04e98c992a7dde76a5b7fab88be34f
├── 3b
...
├── 5c
│ └── 58d749d3c05cd4088561011226cee78ca0221c
├── 5d
...
└── ff
执行 write-tree
后,Object Database 中新增了一个 object 文件,我们通过 cat-file
对比一下 3 个 object 文件内容:
root@master:~/Github/test_git# ../git/cat-file 2f942698bb0133aa13b46a1001ec7a718dbc32b8
temp_git_file_kEdIpF: blob
root@master:~/Github/test_git# ../git/cat-file 3ac6ffce6b04e98c992a7dde76a5b7fab88be34f
temp_git_file_uzqTV2: blob
root@master:~/Github/test_git# ../git/cat-file 5c58d749d3c05cd4088561011226cee78ca0221c
temp_git_file_mk4x5I: tree
root@master:~/Github/test_git# ll
total 32
drwxr-xr-x 3 root root 4096 Nov 29 05:14 ./
drwxr-xr-x 4 root root 4096 Nov 29 05:01 ../
drwx------ 3 root root 4096 Nov 29 05:07 .dircache/
-rw-r--r-- 1 root root 10 Nov 29 05:02 students
-rw-r--r-- 1 root root 9 Nov 29 05:02 teachers
-rw------- 1 root root 9 Nov 29 05:14 temp_git_file_kEdIpF
-rw------- 1 root root 72 Nov 29 05:14 temp_git_file_mk4x5I
-rw------- 1 root root 10 Nov 29 05:14 temp_git_file_uzqTV2
root@master:~/Github/test_git# cat temp_git_file_kEdIpF
Mr. Wang
root@master:~/Github/test_git# cat temp_git_file_uzqTV2
Zhang San
root@master:~/Github/test_git# cat temp_git_file_mk4x5I
100644 students:???k錙*}?v??????O100644 teachers/?&??3??j?zq??2?
可见,新增的 object 正是一个 TREE,它的文件内容包含了两个跟踪文件的一些信息。下面是 write-tree.c
:
int main(int argc, char **argv)
{
...
int i, entries = read_cache();
...
for (i = 0; i < entries; i++) {
...
offset += sprintf(buffer + offset, "%o %s", ce->st_mode, ce->name);
...
memcpy(buffer + offset, ce->sha1, 20);
...
}
...
memcpy(buffer+i, "tree ", 5);
...
write_sha1_file(buffer, offset);
return 0;
}
write-tree 读取 index,将 students 和 teachers 两个文件的文件模式、文件名、cache_entry.sha1
写入 TREE。通过写入一个 TREE,在 Object Database 中建立起了层级关系:
+-------+
| 5c58d |
++----+-+
| |
v-------+ +-------v
+-------+ +-------+
| 3ac6f | | 2f942 |
+-------+ +-------+
6. read-tree
read-tree 读取一个 TREE,打印 TREE 的内容(比 cat-file 的输出更具有可读性):
root@master:~/Github/test_git# ../git/read-tree 5c58d749d3c05cd4088561011226cee78ca0221c
100644 students (3ac6ffce6b04e98c992a7dde76a5b7fab88be34f)
100644 teachers (2f942698bb0133aa13b46a1001ec7a718dbc32b8)
7. commit-tree
先看看这个命令怎么用:
root@master:~/Github/test_git# echo "track teachers and students" > change.log
root@master:~/Github/test_git# ../git/commit-tree 5c58d749d3c05cd4088561011226cee78ca0221c < change.log
Committing initial tree 5c58d749d3c05cd4088561011226cee78ca0221c
a25b317275e2bc71863d2074c70eeb4137d8bb90
root@master:~/Github/test_git# ../git/cat-file a25b317275e2bc71863d2074c70eeb4137d8bb90
temp_git_file_TPjazX: commit
root@master:~/Github/test_git# cat temp_git_file_TPjazX
tree 5c58d749d3c05cd4088561011226cee78ca0221c
author root <root@master> Sun Nov 29 06:19:51 2020
committer root <root@master> Sun Nov 29 06:19:51 2020
track teachers and students
root@master:~/Github/test_git# tree .dircache/ -a
.dircache/
├── index
└── objects
├── 00
├── 01
...
├── 2f
│ └── 942698bb0133aa13b46a1001ec7a718dbc32b8 # BLOB
├── 30
...
├── 3a
│ └── c6ffce6b04e98c992a7dde76a5b7fab88be34f # BLOB
├── 3b
...
├── 5c
│ └── 58d749d3c05cd4088561011226cee78ca0221c # TREE
├── 5d
...
├── a2
│ └── 5b317275e2bc71863d2074c70eeb4137d8bb90 # COMMIT
├── a3
...
└── ff
使用 commit-tree,提交前面生成的 TREE 对象,生成了一个新的 object -> COMMIT。这时 Object Database 中的层级关系如下:
+-------+
| a25b1 |commit
+-------+
|
v
+-------+
| 5c58d |tree
++----+-+
| |
v-------+ +-------v
+-------+ +-------+
| 3ac6f |blob | 2f942 |blob
+-------+ +-------+
变更版本库
现在我们已经完成了对 students 和 teachers 文件的原始追踪。现在,需要向 students 名单内新增一名学生,下面使用前面学习的命令来跟踪名单的变化:
root@master:~/Github/test_git# echo "Li Si" >> students
root@master:~/Github/test_git# cat students
Zhang San
Li Si
root@master:~/Github/test_git# ../git/update-cache students
root@master:~/Github/test_git# tree .dircache/ -a
.dircache/
├── index
└── objects
├── 00
├── 01
...
├── 2f
│ └── 942698bb0133aa13b46a1001ec7a718dbc32b8
├── 30
...
├── 34
│ └── abcd2a605895826c8aee6a845ce630ffc7e4a9 # 新增 blob
├── 35
...
├── 3a
│ └── c6ffce6b04e98c992a7dde76a5b7fab88be34f
├── 3b
...
├── 5c
│ └── 58d749d3c05cd4088561011226cee78ca0221c
├── 5d
...
├── a2
│ └── 5b317275e2bc71863d2074c70eeb4137d8bb90
├── a3
...
└── ff
看一下新增的 BLOB 中是什么内容:
root@master:~/Github/test_git# ../git/cat-file 34abcd2a605895826c8aee6a845ce630ffc7e4a9
temp_git_file_MpII34: blob
root@master:~/Github/test_git# cat temp_git_file_MpII34
Zhang San
Li Si
新增的 BLOB 中是 students 名单的最新内容。下面将这个 BLOB 加入版本库层级:
root@master:~/Github/test_git# ../git/write-tree
e6cba6befd8de134a2b65970f4cc37a123fa3f54
root@master:~/Github/test_git# ../git/commit-tree e6cba6befd8de134a2b65970f4cc37a123fa3f54 -p a25b317275e2bc71863d2074c70eeb4137d8bb90 < change.log
f68cca59ffa6ecd5e04b33accf86cd9931ef848f
root@master:~/Github/test_git# cat f68cca59ffa6ecd5e04b33accf86cd9931ef848f
cat: f68cca59ffa6ecd5e04b33accf86cd9931ef848f: No such file or directory
root@master:~/Github/test_git# ../git/cat-file f68cca59ffa6ecd5e04b33accf86cd9931ef848f
temp_git_file_5JBumK: commit
root@master:~/Github/test_git# cat temp_git_file_5JBumK
tree e6cba6befd8de134a2b65970f4cc37a123fa3f54
parent a25b317275e2bc71863d2074c70eeb4137d8bb90
author root <root@master> Sun Nov 29 07:03:39 2020
committer root <root@master> Sun Nov 29 07:03:39 2020
add student Li Si
在 commit-tree 时,指定父 commit 为前一个 commit,这样我们就将这次修改的内容与修改前的内容联系了起来,下面是此时版本库中的层级关系:
+-------+ +-------+
| a25b1 |commit --> | fbf46 |commit
+---+---+ +---+---+
| |
v v
+---+---+ +---+---+
| 5c58d |tree | e6cba |tree
++----+-+ +-+---+-+
| | | |
v-------+ +-------v v-----+ +----v
+-------+ +-------+ +-------+
| 3ac6f |blob | 2f942 |blob | 34abc |blob
+-------+ +-------+ +-------+
总结
可以看出,最初版本的 Git 有 3 个最核心的命令:
- 通过 update-cache,为修改的文件生成一个副本 BLOB
- 通过 write-tree,将当前整个版本库的状态记录到 TREE
- 通过 commit-tree,记录不同状态间的时序关系
通过这 3 个命令,能完成基本的 content track 的功能。虽然最新版本的 Git 已经新增了很多其它的功能,但是其设计思想没有发生太大的变化,掌握了这些基础的概念,在以后的 Git 学习中会更加游刃有余。