以前作版本管理,我使用最多的是SVN,并且也只是在用一些最经常使用的操做。最近公司里不少项目都开始上Git,借这个机会,我计划好好学习一下Git的操做和原理,以及蕴含在其中的设计思想。同事推荐了一本《Pro Git》,读起来感受很好,在这里分享下阅读时的思考。此书的在线阅读地址:http://iissnan.com/progit/html
这一章介绍了Git的相关历史和基本特色,以及安装配置方法。这里提到的Git的特色包括“直接记录快照,而非差别比较”、“近乎全部操做都是本地执行”、“时刻保持数据完整性”、“多数操做仅添加数据”、“文件的三种状态”,除了最后一点我会放在下一章里梳理,下面会对其中一部分进行一些思考的分享。git
直接记录快照,而非差别比较算法
Git 和其余版本控制系统的主要差异在于,Git 只关心文件数据的总体是否发生变化,而大多数其余系统则只关心文件内容的具体差别。数据库
这个策略要求Git记录每一个版本的完整文件。若是须要对比同一个文件的两个连续版本间差别,Git会直接比较两个文件,而其余系统能够直接把保存的具体差别取出来;可是若是比较间隔版本的文件,后者须要将差别所有合并,才能显示。这意味着版本的间隔越多,基于差别的系统在比较差别所须要的计算量会越大,而Git彻底不会受到这个影响。编程
能够把这个策略看做是空间换时间的实践。如今单位存储空间的费用愈来愈低,TB级硬盘也已沦为白菜价,即便是开发人员使用的百兆级SSD也已经普及,额外的空间消耗彻底能够不作考虑。数组
时刻保持数据完整性浏览器
在保存到 Git 以前,全部数据都要进行内容的校验和(checksum)计算,并将此结果做为数据的惟一标识和索引。换句话说,不可能在你修改了文件或目录以后,Git 一无所知。这项特性做为 Git 的设计哲学,建在总体架构的最底层。因此若是文件在传输时变得不完整,或者磁盘损坏致使文件数据缺失,Git 都能当即察觉。安全
Git 使用 SHA-1 算法计算数据的校验和,经过对文件的内容或目录的结构计算出一个 SHA-1 哈希值,做为指纹字符串。该字串由 40 个十六进制字符(0-9 及 a-f)组成,看起来就像是:网络
24b9da6552252987aa493b52f8696cd6d3b00373
Git 的工做彻底依赖于这类指纹字串,因此你会常常看到这样的哈希值。实际上,全部保存在 Git 数据库中的东西都是用此哈希值来做索引的,而不是靠文件名。架构
使用SHA-1产生的hash值而不是文件名作索引的好处是,hash值的长度固定,而且随机性很好,符合哈希充分散列的要求。SHA-1自己就是一种经常使用的hash函数,其应用不在这里重述。前一段时间Google宣布“将在Chrome浏览器中逐渐下降SHA-1证书的安全指示”,但它这样作的缘由是出于安全考虑,并不意味着Git使用SHA-1作hash函数不合适,有兴趣的读者能够看看相关的分析,如:深度:为何Google急着杀死加密算法SHA-1。
文件名作索引有什么坏处呢?长度不固定并非主要的问题。以用maven管理的代码为例,若是依赖比较复杂,那么各个package中都有各自的pom.xml,它们的文件名是彻底同样的,会致使严重的hash碰撞。
这章介绍了最基本的 Git 本地操做:建立和克隆仓库,作出修改,暂存并提交这些修改,以及查看全部历史修改记录。这些操做的命令再也不一一列出,来看看第一章提到但没有详细讲述的文件状态。
梳理一下文件各个状态的转换过程和逻辑,能够画出下面的图示。在这张图中,经常使用的本地的文件操做命令以及将会致使的状态变动就很清楚了:
除了文件状态,简单说下Git里标签的意义。众所周知,SVN里每一个版本都是有版本号的,从1开始,每次提交都会升高。而在Git中,每次提交只会返回一个SHA-1 校验和其余的信息,是没有版本号的。
发布时,如何指定Git上的代码版本?这时就能够用tag来作标记了。tag至关于为一个特定的版本增长的标记,能够替代SVN里版本号的功能,并且更强大。
若是要理解Git,要理解Git的分支;若是要理解Git的分支,首先要理解Git中的四个基本的对象模型:blob、tree、commit、tag。这部分原书写的比较简单,具体能够参考《Git Community Book》第一章。幸运的是,该书也有网络版,这一部份内容的地址是:http://gitbook.liuhui998.com/1_2.html。简单地说,这四种对象分别对应于:
分支是把commit对象组织成了链表的形式,不一样的分支指向了对应的commit对象,每次在分支上提交,都会在链表表头上插入新的对象,以下图上的master和testing两个分支,图中的绿色方框表明一个commit对象。此时能够经过控制HEAD指针所在位置来指明使用了哪一个分支。
简单回忆下链表的相关操做能够发现,只要保存各个分支对应的表头,咱们能够很容易的经过给HEAD赋值在各个分支之间切换。同时对于每次提交,链表插入的操做也很简单。
在理解了Git版本管理的链表式的实现方式以后,只要具备基本的算法知识,其余操做原理的理解会变得很是迅速。如下各图来自于《Pro Git》。
1.从master拉新分支iss53,只需新增该分支的指针,未作修改后的提交时,iss53指向master。提交新内容时,建立对应commit对象,iss53指针前移。
2.当分支hotfix的祖先节点中包括master分支,将hotfix分支merge回master分支,只须要把master的指针移动hotfix上,没有任何文件处理工做,于是称之为Fast forward。
3.当分支iss53合并回master分支,可是master不是iss53的祖先时,先计算两者的最近一个的公共祖先,把它和这两个分支的commit进行合并计算,建立新的commit对象。这个对象有两个祖先。若是合并时遇到冲突,不会提交,而是等人工处理完冲突并git add后才能进行提交。
如何寻找交叉链表的第一个公共节点?这是一个常见算法问题,能够参考旧做《编程之美》3.6判断链表是否相交之扩展:链表找环方法证实的扩展问题2。
4.(我的推测)查看某个分支是否已合并到master分支:比较两个分支指针是否指向同一个对象。
5.(我的推测)删除已合并master的分支:直接删除该分支的指针;删除未合并的分支(git branch -d XXX):删除该分支不在master上全部commit对象及相关的对象、删除分支指针。
merge是直接将两个分支合并到一块儿:建立一个合并后的commit节点,祖先有两个,是被合并的两个分支A和分支B,节点内容是三方(分支A、分支B、分支A和B的共有最近祖先)合并的结果。原有的链表上的节点保留,分支上的提交历史没有发生改变。以下图(来自《Pro Git》)所示:
rebase则是将一个分支A中的内容产生的补丁在另外一个分支B上从新打一遍,打完以后,分支A的节点变成了分支B的后继。rebase完成后,分支A的特有节点发生了变更。以下图(来自《Pro Git》)所示,C3和C3`是不一样的节点:
实际上,merge和rebase产生的节点的内容上是同样的,发生冲突时仍须要人工解决,不一样点只是提交的历史节点。rebase更适用于未公开提交(能够理解为push到远程仓库)的对象,清理提交历史;若是对已公开的提交对象rebase,而且已经有人对这些已提交对象开展了后续开发,会使得提交历史很是混乱。详细的例子能够查看原书“分支的衍合”一节。
在“使用Git调试”一节,提到了git bisect进行各个commit的二分查找。众所周知,单链表自己是不支持二分查找的,推测Git可能使用了如下两种方式支持:
(1)将起始和结束的两个commit中间全部节点的指针保存到一个临时数组中,二分查找基于这个临时数组进行;
(2)git使用的了相似于跳表的链表。跳表可参考http://www.cnblogs.com/liuhao/archive/2012/07/26/2610218.html。
进一步地推测,在进行二分查找时,对commit进行修改,可能会致使查询错误。
在第一次阅读这一章时,我从第二节开始就有点晕头转向,不知道究竟行文的思路是什么。第二次阅读时才有点眉目,并发觉第一次没看懂的缘由是,原文不少地方只是描述底层命令执行后发生的现象,并无完整地告诉读者这个命令执行的结果。网上不少对git的介绍文章偏实用主义,对这些底层命令并无花费多少笔墨。好在git自身的文档很完善,git -help <command>对底层命令也有效,能够自行查看。不过方便起见,这里会简单介绍下这些底层命令。如下介绍底层命令时,实际用法为git XXX,如git hash-object,简记为hash-object。
固然,这里的介绍不是文档的翻译,其中也加入了一些我的的理解,所以,各个命令的介绍可能有少许的连续性。
另一个有趣的事实:Git高层命令是能够自动补全的,而底层命令不行。
计算一个文件(能够经过--stdin指定为从标准输入读取)的对象ID,这个对象ID其实是git这个内容寻址文件系统的K-V关系的键值。可使用-w选项将该对象添加到git的文件对象库,而不只仅是把对象的键值显示在屏幕上。
显示git对象的内容或类型,须要指定对象ID。-p用于输出格式化内容。你会发现,经过hash-object生成的文件直接打开是乱码,想要查看原始内容,必须用git cat-file。能够推测,git对象中不只保存了文件内容,还应该保存告终构信息等,并有被压缩的可能。这节的结尾便证明了这点:先写文件头(包括文件类型和内容长度)、内容正文,再计算SHA-1校验和(做为文件路径和文件名,不参与文件自己的保存),最后进行压缩。
若是对一个tree对象使用cat-file -p,能够看到这个tree对象包括了其余tree或blob对象的引用(一样是对象ID)。
为文件建立或更新index。这样作会致使文件被放入暂存区域(回想下git中文件的staged状态)。运行这个命令以后,每每接下来要运行git write-tree。对同一个文件重复运行也没有任何提示。
为当前的index(注意:此时暂存区可能有多个文件)建立一个tree对象。把update-index和write-tree分开的目的,我认为是Git为了得到更细粒度的控制能力。
将一个的tree对象(能够以--prefix指定其对应的目录名,这个目录此时还不存在)读入index。经过这个命令以及update-index、write-tree,咱们就能够任意地装配出任何目录-文件的结构了。注意我这里使用的是“装配”而非“组装”,是由于这三个命令是没法进行目录结构的拆分的。
指定一个tree对象,以此建立一个commit对象。若是对一个commit对象再次运行该命令,能够git log看到完整的提交历史,也即这两个commit对象。
安全地更新一个用对象ID表示的文件的引用。其结果和git branch指定某个分支(对应于update-ref的引用)为某个commit(对应于update-ref的对象ID)同样。
轻量级tag对象是能够经过update-ref进行建立的。
给一个标记(如最多见的HEAD)指定一个引用。不指定引用则读取这个标记当前的引用。
清理文件。其实是将文件进行打包压缩。
查看经过gc进行的已打包的git对象。
阅读的时候遇到了两处翻译错误,我已提交了pull request:
1.第9-2节“-stdin
指定从标准输入设备 (stdin) 来读取内容,若不指定这个参数则需指定一个要存储的文件的路径。”应为“要读取的文件的路径”。
2.第9-4节 “而后能够用git cat-file命令...”,下面用的是du命令。实际原文中在这里没有提到“git cat-file”。