趁着最近工作清闲,学习一下Git的源码。搜了一下网上没有这方面的资料,只能自己慢慢的看。为了降低难度,并且更好的理解Git的发展历程,我决定从最初版的Git开始看起,跟着Git学Git。
选择Git v0.99开始学习。从https://github.com/git/git/tree/v0.99/处下载源码,这个版本的Git非常简单,功能也很少,可以从中看到初期Git设计的初衷。Clone下来之后,reset到第一次commit,开始源码的阅读之旅。
第一次Commit的文件很少,目录结构如下所示:
编译
依赖包:libssl-dev、zlib
修改编译选项:Makefile中LIBS= -lcrypto -lz
编译完成之后在~/bin/下会产生以下命令:cat-file、commit-tree、init-db、read-tree、show-diff、update-cache、write-tree。
用法
1、使用init-db初始化工作目录,类似于git init的作用。
2、项目编写,增删改各种文件等等。
3、使用update-cache [file-path],保存更改至缓存中。这会生成一个index文件,改文件用于保存当前的cache。
4、使用write-tree提交缓存中的更改。这会生成一个tree文件,当前的cache中的文件会写入到tree文件中去。命令结果会返回tree文件的sha1值。
5、使用commit-tree
6、工具命令show-diff,用来比较当前工作目录下的文件和cache中(即index文件)记录的文件的区别。
7、工具命令cat-file
概念
1、The Object Database(SHA1_FILE_DIRECTORY)
这是一个用于存储SHI1文件的文件数据库,其实本质上只是一个文件夹,用于存放所提交的文件。文件包含Metadata信息和Blob内容,经由Zlib压缩后算出SHA1,该SHA1的前2位作为子文件夹名,后38位作为文件名。Directory如下图所示。
这里存放的文件包括三种:tree、blob和commit。
Tree:tree文件中存放的是所提交的文件列表,每一行描述所记录的一个文件,包括:文件的权限、路径名、SHA1值。这个就能够用于保存每一次提交的具体内容,通过查询tree文件,可以知道该次提交时所含有的所有文件,然后根据每一个文件的SHA1,可以在object database中搜索出该文件。这样就达到了保存每一次提交的具体内容的目的。
BLOB:blob文件是指具体的文件内容,即我们所提交的文件。Blob文件会被压缩,然后计算SHA1值,所以如果文件的内容没有发生变化,那么就不会产生新的Blob文件。因为它们算出的SHA1是相同的,而SHA1值就是它们实际的存放路径。
Commit:commit文件是用于记录每一次提交的文件。包含的内容有:tree、parents、author、committer、changelog。其中tree是指用于保存此次提交的tree文件。Parents是指此次提交的父分支是哪些,也是对应的tree文件。Author、committer、changelog是提交的记录信息。
一个commit文件的例子:
源码分析
init-db.c
该文件的工作很简单,就是在工作目录下创建一个.dircache的目录,并在其中创建objects目录作为database的主目录,在objects中,创建了从00到ff一共255个子目录。这些子目录将被用来存储SHA1文件。如计算出sha1值为b7140ba6976a2a2bf3cb31bf3aa2bd3e3619d521的文件,将被保存为.dircache/objects/b7/140ba6976a2a2bf3cb31bf3aa2bd3e3619d521
Update-cache.c
该文件实现了update-cache命令,主要流程为:从index中读取cache,根据传入的file,添加或者替换cache,讲新的cache写回index文件。
下面分几个主要部分记录主要的代码工作。
1、index文件的更新。
这里使用的方式是先更新内存中的cache内容,然后create一个index.lock文件,将新的cache写入index.lock文件中,然后将index.lock改名为index。
2、Cache的更新。
Cache在内存中使用一个数组保存,本地使用index文件存储。通常是从index文件中读取到数组中。Cache中的条目根据cache->name有序排放,更新cache时,会通过二分查找检查该条目是否在cache中,如果已存在则进行替换,否则直接加入到cache中(注意有序排放)。
Show-diff.c
这里需要解释的是,实际的工作是通过diff命令来实现的。为了避免所有的文件都会调用diff,先通过比较文件的属性来判断文件是否发生了改变。如果已经能够判断出文件未发生改变,就无需再调用diff来比较新旧文件。此外,没有加入到cache中的文件不会被比较,也就是说对于添加和删除文件来说,这里是不能看到diff的结果的。并且如果是删除文件的话,使用show-diff还会报segment fault.
代码的大致逻辑:从index中读取cache到数组中,根据cache_entry->name找到工作目录下的文件(即新的文件),根据cache_entry->sha1从objects中读取旧的文件,使用cache_entry中存放的旧文件的属性和新文件的stat比较,如果不同则调用diff来显示两者的差异。
总结:
从这个最初版的git中可以看到我们日常使用的git的一点影子。它能够跟踪项目的版本,但是还没有实现控制的功能。使用文件database的方式存储历史文件,并且利用sha1来简单的实现文件的复用,即相同的blob只需要存储一份。目前能够查看历史版本的文件,能够手动的记录提交的信息,手动的跟踪版本更新的信息。从今天的角度来看,但是这些功能都有待优化,优化的初衷就是更加易用。期待着它一步一步变成今天强大的Git!
附上Linus在这个版本README中对Git的描述:
"git" can mean anything, depending on your mood.
- random three-letter combination that is pronounceable, and not
actually used by any common UNIX command. The fact that it is a
mispronounciation of "get" may or may not be relevant.
- stupid. contemptible and despicable. simple. Take your pick from the
dictionary of slang.
- "global information tracker": you're in a good mood, and it actually
works for you. Angels sing, and a light suddenly fills the room.
- "goddamn idiotic truckload of sh*t": when it breaks