RSS Feed

Posts Tagged ‘git’

  1. 如何让Git的clone实现断点续传

    February 10, 2012 by xudifsd

    在下载大文件时最重要的功能恐怕就是断点续传了。如果在一个网络环境十分不稳定的环境下使用git克隆一个大型代码库绝对是个痛苦的经历,一旦下载中断,之前下载的内容全都不复存在。

    git提供断点续传的难处

    根据git的传输协议,在使用非HTTP进行传输时客户端和服务器端会进行一个复杂的通信:首先服务器端会列出它的所有内容,然后客户端根据服务器端的提示找出自己需要哪些数据,再向服务器索取,之后服务器端再调用“git upload-pack”将客户端想要的commits进行打包并传输。整个过程比较复杂,而且传输协议也不像HTTP协议那么简单,并且最麻烦的就是git传输的不是静态文件而是动态生成的,这样就造成了git的传输很难修改成支持断点续传。

    解决方法

    用HTTP来获取静态的文件时使用断点续传实在是再简单不过的了,只要在HTTP头加一行RANGE属性即可。所以如果能将git的整个仓库转换成一个静态文件再使用普通的HTTP协议传输就能非常简单地实现断点续传了。

    git其实有packfile这样的文件格式,这种文件会在运行“git gc”时或进行传输时生成,生成之后的文件包括了整个git所管理的全部内容,但仅仅只靠这样一个文件还是不能解决问题,因为这种文件只包含仓库中所有对象的内容和SHA1值,除此之外没有任何仓库元信息(分支,tag等)。所以就算得到了packfile也不能重建一个仓库。

    但是git除了packfile外还有另外一种格式的文件:bundle,这种文件只是在packfile的基础上再包括刚刚提到的仓库的元信息,所以根据bundle文件我们可以完整地clone出一个仓库。

    不过这和断点续传有什么关系呢?因为bundle可以用来将整个仓库变成一个静态文件,所以如果在服务器上创建bundle再用普通的HTTP来得到这个文件,就可以重创整个仓库,这也是android仓库的做法。不过这个方法需要服务器端提供clone.bundle文件,一旦服务器提供不一样的文件名这样的方法就失效了。而且这样的方法需要用户对服务器有控制权,如果不能控制服务器这一切都是空谈。一般用户常常只是在github这样的网站托管仓库,或者想要克隆一个Linux内核代码,这样只要服务器端不提供bundle,就算用户知道有这个东西也不能利用它来进行断点续传。不过为何不让别人帮你创建bundle呢?最终的解决方案就是这个网站,它接受你提供的代码仓库的url,之后它会将你想要的仓库克隆并创建一个bundle文件,之后你只需从该服务器上下载静态的bundle文件即可。下载完整个bundle文件后根据这里的举例就能得到整个仓库了,不过之后需要使用“git remote”修改一下远程仓库的地址,修改完后为了确保仓库内容最新最好还要执行一下“git pull”。


  2. Git源码阅读——文件路径

    October 5, 2011 by xudifsd

    最开始在学习git用法的时候就听说了git不像其他版本管理软件(subversion)把版本信息分别放在各个子目录,而是将整个版本信息放在项目的根目录,当时就觉得这种实现似乎很有意思,但是却不知道如何实现这一点,因为每次运行git add等命令时都有可能在任意子目录。唯一能想到的方法就是绝对路径:所有版本的文件的路径都使用绝对路径来表示,但是这样一来将项目的目录进行移动或者分布式开发就有问题,而且使用od(1)来查看git的index文件也发现存的是相对路径而不是绝对路径。

    git的实现

    在源代码中就可以找到实现原理:几乎所有的git命令都会在启动时调用setup_git_directory,这是git的内部API,函数原型在setup.c中,这个函数就负责检查git环境并统一所有的文件路径:它尝试调用access(2)来判断该目录是否是git项目的根目录,如果不是则调用chdir(2)切换工作目录到父亲目录,如此循环直到到达根目录或者到达git项目根目录,如果成功到达项目根目录该API会返回从git根目录到之前工作目录的prefix,之后所有的对路径的操作就可以用prefix和执行命令时的相对路径来进行,git也提供了进行这样操作的API:prefix_path,这个函数负责去掉命令行参数路径中的.和..,并返回正确的路径。通过将路径统一成相对项目根目录的形式可以减轻对路径处理的工作量,所有上层命令只需在程序开始时调用setup_git_directory取得prefix,在操作路径时调用prefix_path,就能正确处理路径问题了。

    Linux环境中对路径的处理

    《APUE》可以得知所有的进程都有工作目录,所有的相对路径的操作都会基于工作目录,而chdir(2)可以让进程在不同的工作目录中进行切换,由内核负责保存进程的工作路径,想要取得当前工作路径只要使用getcwd(2)即可,但是需要注意的是工作目录是进程的属性,任何在子进程中修改的属性父进程都无法获得,所以在使用gdb进行调试时不可能使用getcwd来得到调试项目的工作路径的,因为调试项目是gdb的子进程。还有一点需要注意的是PWD环境变量,git在切换目录之后不会修改PWD环境变量,似乎也没有哪个应用程序会修改这个变量,这种环境变量似乎只是shell为了给shell脚本提供的,在应用程序中这样的变量是靠不住的。


  3. Git源码阅读——git的cache结构

    September 26, 2011 by xudifsd

    在git的源码中有这样一句

    #define cache_entry_size(len) ((offsetof(struct cache_entry,name)+(len)+8)\
    &~7)

    开始我死活看不懂这什么意思,8这样的幻数已经够吓人的了,再来个对7取反码,实在看不懂。但是经过几天的研究终于明白这是什么意思了。

    背景

    前面的代码是描述cache的,cache在git中可以类比于CPU的高速缓存——为了加快访问速度,cache主要考虑的是工作目录和暂存对象(已经加到cache中但是并没有提交的对象)的差别,因此它只要将工作目录中的文件的stat信息保存即可。在新版的git中这样的概念改名叫做index了,所以现在所有的cache信息都存在.git/index文件中。保存信息的结构体原型如下:

    struct cache_entry {
    	struct cache_time ce_ctime;
    	struct cache_time ce_mtime;
    	unsigned int ce_dev;
    	unsigned int ce_ino;
    	unsigned int ce_mode;
    	unsigned int ce_uid;
    	unsigned int ce_gid;
    	unsigned int ce_size;
    	unsigned char sha1[20];
    	unsigned short ce_flags;
    	char name[0];
    };

    其中需要注意的是结构体的最后一个成员name,它用来存暂存对象的相对路径,是一个长度为0的数组,在gcc中使用这样的定义来让name的长度不定长,不用指针的原因就是git总是使用mmap(2)来读取index文件,之后直接将cache_entry指针指向正确的位置就行了,而且写到磁盘的时候也不需要再对文件名取指针内容,直接write(2)就行。而路径的长度信息就存在ce_flags中,但是它仅仅用低12位来存长度(这些已经足够存路径名了,因为在Linux系统中PATH_MAX == 4096 == 212),其他位用做其他用途,这就是为什么程序源码中还有一句:

    #define ce_namelen(ce) (CE_NAMEMASK & ntohs((ce)->ce_flags))

    这样一个宏函数可以求出ce指针的文件名长度。如果有一个cache_entry的指针ce,那么就可以通过cache_entry_size(ce_namelen(ce))来求得ce的实际大小。

    正由于cache是为了快速访问的,所以它的设计需要最大限度的考虑速度,这就是文章开头定义存在的原因:为了内存对齐——CPU访问对齐的变量更快,之所以必须要进行手工的对齐就是因为git是使用mmap(2)进行文件的读取。如果考虑到这一点那么最开始的代码也好解释了:对7取反码可以让结果能被8整除,但是如果一开始就不能被8整除那么结果就会小于另一个运算子,这样一来加8就是为了防止结果变小。但是这样怎么保证内存对齐的呢?因为在内存中所有cache元素都是存在数组中的,前一个元素的长度能被8整除,那么后一个元素的起始位置就能被8整除,而在mmap(2)时内核会选择一个对齐的地址放整个数组,这样一来数组中所有的元素都是对齐的了。

    读源码就像做侦探游戏一样,总是要猜作者到底要表达什么,而读好的代码更可以让人学到很多。


  4. Git源码阅读——对象数据库

    September 22, 2011 by xudifsd

    git其实就是一个CAS(Content-addressable storage)系统,要想取得一个对象就必须提供该对象的哈希值而非对象的路径,这样特性的实现方法就是使用对象的哈希值作为对象的路径。git使用的哈希算法是sha1算法。git的对象数据库还有一个属性就是所有的对象只能添加、读取,不能删除、修改,这样保证了整个代码历史的完整性。

    git对象文件

    所有git对象都保存在.git/objects中,例如

    .git/objects/03/7dc5e0f2cd4e0f4ff2385843beab28170c31f5

    表示一个对象,.git/objects目录下的目录名03是对象sha1值的前两位,文件名则是剩余的sha1值,这样类似于数据桶的结构可以让打开文件更快。由于git不保存文件的差别而是保存整个文件,所以对于一个只修改了几行的文件git也会保存两份几乎完全相同的内容,当跟踪的历史变长时数据冗余也会越来越多,这样的对象叫松散对象。不过git可以对所有对象进行打包,保存每个文件的不同,这样的动作叫垃圾收集,git会在每次进行网络传输或运行git gc时进行。

    git中所有对象都有一个公共的文件头,格式是:

    type size\0

    其中type是对象对应的类型,而size则是整数的字符串表示,表示对象内容的长度,’\0’之后的内容就是各个对象的内容。

    读取操作

    在git中我们只能通过20个字节的sha1哈希值来取得文件的内容,在代码中可以使用

    read_sha1_file(sha1, type, &size)

    来进行读取,参数type和size分别用于存从文件头中读出的对象类型和内容大小。但是read_sha1_file并不仅仅只是打开,解压再读取,由于可能对象已经被打包,read_sha1_file也包括了unpack之类的函数用来解析打包后的文件。

    写操作

    我们通过

    write_sha1_file(buffer, buffer_len, type, return_sha1)

    来进行写操作,其中buffer是对象的内容,buffer_len是内容的大小,type是对象的类型,最后通过return_sha1来接收刚刚写入对象的sha1值。需要注意的是写操作的实现:由于I/O可能会被中断,如果直接创建sha1文件,一旦中断会导致麻烦的数据不完整,而产生硬连接的link则很难被打断,所以为了最大限度地保证数据完整性,git使用临时文件:先调用mkstemp产生一个临时文件,并将内容写入,成功之后会调用link产生一个文件名为对应sha1值的指向临时文件的硬连接,再unlink临时文件。

    对象的属性

    git共有四种类型的对象:blob,tree,commit和tag。

    blob是最基本的对象,不能引用任何其他对象,它类似于文件系统中的文件,但与文件不同的是它自身没有任何文件名,它所包含的信息只有文件内容。这样的文件名和文件内容的分离可以实现对象的重用:如果你将一个文件加到git,然后对文件名进行修改但是没有修改文件内容,之后再将其加到git,这样并不会产生另一个blob对象,修改的只是tree对象中的引用名而已。

    tree对象的内容包括对其他tree对象或blob对象的引用和对应对象的名字及模式,这样的属性让tree非常类似于文件系统中的目录,tree引用tree类似于目录中的子目录,tree引用blob类似于目录中的文件。

    commit对象可以引用一个tree对象和零个或多个commit对象,commit引用的tree对象表示整个项目的根目录,而被引用的commit对象表示父commit,如果没有引用其他的commit,则表示该commit对象是整个项目的初次提交,而多个commit引用则表示由多个commit合并而来。

    tag对象可以引用任意类型的一个对象,它的作用就是给某个对象一个方便人们记忆的名字,比如可以对一个项目某个commit对象标为v1.0来表示这个项目的1.0版本,也可以将存有项目公钥的blob对象打上标签。