翻译 | 雁惊寒 “pygit是一个大约500行Python代码工具,实现了一些git功能,包括创建库、将文件添加到索引、提交、将自身推送到GitHub上去。 本文给出了一些代码编写过程,并详细介绍了相关代码。” Git因其具有非常简单的对象模型而着称。在学习git时,我发现本地对象数据库只是.git目录中的一堆普通文件。除了索引(.git/index)和打包文件(可有可无)外,这些文件的存放规则和格式相当的简单。 受Mary Rose Cook的程序启发,我也想看看是否能够编写出创建仓库,j2直播,执行提交,并推送到服务器(比如GitHub)的git客户端。 Mary的gitlet程序有着很多可供学习的地方,而我的程序需要把自身推送到GitHub上去,所以具有更多的创新功能。在某些方面,她实现了更多的Git功能(包括基本的合并),但在其他方面实现的功能就比较的少。例如,她使用了一个简单的基于文本的索引格式,而不是用git使用的二进制格式。此外,虽然她的gitlet支持推送,但它只会推送到本地已经存在的仓库中,而不是到远程服务器上。 对于本文涉及的这个练习,我打算编写一个可以执行所有步骤的版本,包括推送到一个真正的Git服务器上去。我也会使用与git相同的二进制索引格式,这样,我就可以在每一步骤上都使用git命令来检查程序的功能。 我的程序叫pygit,用Python(3.5+)编写,并且只使用了标准库模块。它只有500行代码,包括空白行和注释。我至少需要实现init、add、commit和push命令,但pygit还实现了status,diff,cat-file,ls-files和hash-object等命令。后面的命令,本身也非常有用,并且在调试pygit的时候,也起到了帮助作用。 下面,让我们来看看代码吧!您可以在GitHub上查看pygit.py的所有代码,或者在下文中跟着我一起浏览各段代码。 初始化仓库 初始化本地Git仓库只需要创建.git目录以及目录下的几个文件和子目录即可。在定义了read_file和write_file这两个帮助函数之后,我们就可以编写init()了:
你可能注意到这段代码里没有进行优雅的错误处理。毕竟这整个代码只有500行啊。如果仓库目录已经存在,程序会终止,并抛出traceback。 取对象的散列值 hash_object函数用来获取单个文件对象的散列值,并写入.git/objects目录下的“数据库”中。在Git模型中,包含三种对象,分别是:普通文件(blob),提交(commit)和树(tree,也就是目录结构)。 每个对象都有一个文件头,包括文件类型和文件大小,大概几个字节的长度。之后是NUL字符,然后是文件的数据内容。所有这些都使用zlib压缩并写入到文件.git/objects/ab/cd…中,其中ab是40个字符长的SHA-1散列的前两个字符,直播,而cd…则是剩余的部分。 请注意,这里使用了Python标准库(os和hashlib)。
还有个find_object()函数,它通过散列(或散列前缀)找到某个文件对象,然后用read_object()函数读取这个对象及其类型。这实际上是hash_object()的反向操作。最后,cat_file是一个与git cat-file具有相同功能的pygit函数:它将对象的内容(或者大小和类型)进行格式化并打印到标准输出。 git索引 接下来我们要做的事情就是要将文件添加到索引或暂存区中。索引就是文件列表,按路径名排序,每个路径都包含路径名,修改时间,SHA-1散列等等。需要注意的是,索引列出了当前树中的所有文件,而不仅仅是在暂存区中等待提交的文件。 索引以自定义的二进制格式存储在.git/index文件中。这个文件虽然并不是很复杂,但它还是涉及到了结构体的用法,通过一定规则的字节偏移,可以在长度可变的路径名称字段之后获得下一个索引条目。 文件的前12个字节是文件头,最后20个字节是索引的SHA-1散列,在这中间的字节是索引条目,每个索引条目为62个字节加上路径的长度再加上填充的长度。下面是namedtuple类型的IndexEntry和read_index函数:
这个函数后面是ls_files,status和diff函数,这些是打印索引状态的几个不同的方法: ls_files函数只是打印索引中的所有文件(如果指定了-s,则连同一起打印它们的模式和散列) status函数使用get_status()来比较索引中的文件和当前目录树中的文件是否一致,打印有哪些文件被修改,新增或删除 (责任编辑:本港台直播) |