之前做版本管理,我使用最多的是SVN,而且也只是在用一些最常用的操作。最近公司里很多项目都开始上Git,借这个机会,我计划好好学习一下Git的操作和原理,以及蕴含在其中的设计思想。同事推荐了一本《Pro Git》,读起来感觉很好,在这里分享下阅读时的思考。此书的在线阅读地址:http://iissnan.com/progit/


第一章 起步

  这一章介绍了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 本地操作:创建和克隆仓库,做出修改,暂存并提交这些修改,以及查看所有历史修改记录。这些操作的命令不再一一列出,来看看第一章提到但没有详细讲述的文件状态。

文件状态

  梳理一下文件各个状态的转换过程和逻辑,可以画出下面的图示。在这张图中,常用的本地的文件操作命令以及将会导致的状态变更就很清楚了:

  除了文件状态,简单说下Git里标签的意义。众所周知,SVN里每个版本都是有版本号的,从1开始,每次提交都会升高。而在Git中,每次提交只会返回一个SHA-1 校验和其他的信息,是没有版本号的。

  发布时,如何指定Git上的代码版本?这时就可以用tag来做标记了。tag相当于为一个特定的版本增加的标记,可以替代SVN里版本号的功能,而且更强大。


第三章 Git分支

用链表组织分支

  如果要理解Git,要理解Git的分支;如果要理解Git的分支,首先要理解Git中的四个基本的对象模型:blob、tree、commit、tag。这部分原书写的比较简单,具体可以参考《Git Community Book》第一章。幸运的是,该书也有网络版,这一部分内容的地址是:http://gitbook.liuhui998.com/1_2.html。简单地说,这四种对象分别对应于:

  •   blob:表示文件内容,是指向文件的索引。
  •   tree:表示目录层级关系,保存了其他blob对象和tree对象的指针。
  •   commit:保存一次提交的信息,以及一个tree对象根目录。
  •   tag:标记一次commit。

  分支是把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还是rebase?

  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调试”一节,提到了git bisect进行各个commit的二分查找。众所周知,单链表本身是不支持二分查找的,推测Git可能使用了以下两种方式支持:

  (1)将起始和结束的两个commit中间所有节点的指针保存到一个临时数组中,二分查找基于这个临时数组进行;

  (2)git使用的了类似于跳表的链表。跳表可参考http://www.cnblogs.com/liuhao/archive/2012/07/26/2610218.html。

  进一步地推测,在进行二分查找时,对commit进行修改,可能会导致查询错误。


第九章 Git内部原理

底层命令究竟做了些什么

  在第一次阅读这一章时,我从第二节开始就有点晕头转向,不知道究竟行文的思路是什么。第二次阅读时才有点眉目,并发觉第一次没看懂的原因是,原文很多地方只是描述底层命令执行后发生的现象,并没有完整地告诉读者这个命令执行的结果。网上很多对git的介绍文章偏实用主义,对这些底层命令并没有花费多少笔墨。好在git自身的文档很完善,git -help <command>对底层命令也有效,可以自行查看。不过方便起见,这里会简单介绍下这些底层命令。以下介绍底层命令时,实际用法为git XXX,如git hash-object,简记为hash-object。

  当然,这里的介绍不是文档的翻译,其中也加入了一些个人的理解,因此,各个命令的介绍可能有少量的连续性。

  另外一个有趣的事实:Git高层命令是可以自动补全的,而底层命令不行。

hash-object

  计算一个文件(可以通过--stdin指定为从标准输入读取)的对象ID,这个对象ID实际上是git这个内容寻址文件系统的K-V关系的键值。可以使用-w选项将该对象添加到git的文件对象库,而不仅仅是把对象的键值显示在屏幕上。

cat-file

  显示git对象的内容或类型,需要指定对象ID。-p用于输出格式化内容。你会发现,通过hash-object生成的文件直接打开是乱码,想要查看原始内容,必须用git cat-file。可以推测,git对象中不仅保存了文件内容,还应该保存了结构信息等,并有被压缩的可能。这节的结尾便证实了这点:先写文件头(包括文件类型和内容长度)、内容正文,再计算SHA-1校验和(作为文件路径和文件名,不参与文件本身的保存),最后进行压缩。

  如果对一个tree对象使用cat-file -p,可以看到这个tree对象包括了其他tree或blob对象的引用(同样是对象ID)。

update-index

  为文件创建或更新index。这样做会导致文件被放入暂存区域(回想下git中文件的staged状态)。运行这个命令之后,往往接下来要运行git write-tree。对同一个文件重复运行也没有任何提示。

write-tree

  为当前的index(注意:此时暂存区可能有多个文件)创建一个tree对象。把update-index和write-tree分开的目的,我认为是Git为了获得更细粒度的控制能力。

read-tree

  将一个的tree对象(可以以--prefix指定其对应的目录名,这个目录此时还不存在)读入index。通过这个命令以及update-index、write-tree,我们就可以任意地装配出任何目录-文件的结构了。注意我这里使用的是“装配”而非“组装”,是因为这三个命令是无法进行目录结构的拆分的。

commit-tree

  指定一个tree对象,以此创建一个commit对象。如果对一个commit对象再次运行该命令,可以git log看到完整的提交历史,也即这两个commit对象。

update-ref

   安全地更新一个用对象ID表示的文件的引用。其结果和git branch指定某个分支(对应于update-ref的引用)为某个commit(对应于update-ref的对象ID)一样。

  轻量级tag对象是可以通过update-ref进行创建的。

symbolic-ref

  给一个标记(如最常见的HEAD)指定一个引用。不指定引用则读取这个标记当前的引用。

gc

  清理文件。实际上是将文件进行打包压缩。

verify-pack

  查看通过gc进行的已打包的git对象。

阅读的时候遇到了两处翻译错误,我已提交了pull request:

1.第9-2节“-stdin 指定从标准输入设备 (stdin) 来读取内容,若不指定这个参数则需指定一个要存储的文件的路径。”应为“要读取的文件的路径”。

2.第9-4节 “然后可以用git cat-file命令...”,下面用的是du命令。实际原文中在这里没有提到“git cat-file”。

《Pro Git》阅读随想的更多相关文章

  1. 【Tools】Pro Git 一二章读书笔记

    记得知乎以前有个问题说:如果用一天的时间学习一门技能,选什么好?里面有个说学会Git是个很不错选择,今天就抽时间感受下Git的魅力吧.   Pro Git (Scott Chacon) 读书笔记:   ...

  2. 《Pro git》

    可以通过阅读 CODING 工程师参与翻译的 <Pro Git> 进一步掌握 Git 版本控制系统. https://git-scm.com/book/zh/v2

  3. 《Pro Git》轻松学习版本控制

    转自 https://kindlefere.com/post/333.html 什么是“版本控制”?我为什么要关心它呢?版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统.在 ...

  4. [Git00] Pro Git 一二章读书笔记

    记得知乎以前有个问题说:如果用一天的时间学习一门技能,选什么好?里面有个说学会Git是个很不错选择,今天就抽时间感受下Git的魅力吧.   Pro Git (Scott Chacon) 读书笔记:   ...

  5. 【重要】Pro Git 第二版 简体中文

    不管是入门还是精通git,下面这本书都是必读,同时它也是官方推荐书籍.   Pro Git 第二版 简体中文     我自己还收集了一份网页版的progit,但可能不是progit第二版. 下载地址  ...

  6. Pro Git 第一章 起步 读书笔记

    Pro Git 笔记 第1章 起步 1.文件的三种状态. 已提交:文件已经保存在本地数据库中了.(commit) 已修改:修改了某个文件,但还没有提交保存.(vim) 已暂存:已经把已修改的文件放在下 ...

  7. Pro Git(中文版)

    Pro Git(中文版) 返回 Git @ OSC 目录 1.起步 1.1 关于版本控制 1.2 Git 简史 1.3 Git 基础 1.4 安装 Git 1.5 初次运行 Git 前的配置 1.6 ...

  8. 《Pro Git》笔记3:分支基本操作

    <Pro Git>笔记3:Git分支基本操作 分支使多线开发和合并非常容易.Git的分支就是一个指向提交对象的可变指针,极其轻量.Git的默认分支为master. 1.Git数据存储结构和 ...

  9. Pro Git CN Plus

    Git — The stupid content tracker, 傻瓜内容跟踪器.Linus 是这样给我们介绍 Git 的. Git 是用于 Linux 内核开发的版本控制工具.与常用的版本控制工具 ...

随机推荐

  1. Kylin Java RESTful API

    最近在做大数据方面的开发, 学习研究了一段时间的kylin系统, 对于前端开发需要使用 RESTful API ,但是官网并没有提供详细的Java  API. 经过几天的看文档,最终写出了 Java ...

  2. How Garbage Collection Really Works

    Java Memory Management, with its built-in garbage collection, is one of the language's finest achiev ...

  3. NodeJs:module.filename、__filename、__dirname、process.cwd()和require.main.filename

    转载于: http://blog.csdn.net/bugknightyyp/article/details/17999473 module.filename:开发期间,该行代码所在的文件. __fi ...

  4. java class的property的get和set方法生成规则

    package rh.intellicareAppServer.dao; public class test { String aA; String aa; public String getaA() ...

  5. XML中& <> 单引号' 双引号 " 报错

    由于xml中 这些字符是特殊字符,所以把&改成&  就行了 ,注意后面一定要带一个分号; <         <         小于号>         >  ...

  6. javascript type操作

    var a; type a;//结果为undefined type b;//结果也是undefinedalert(a);//弹出undefinedalert(b);//错误代码  上述代码,type ...

  7. stickUp让页面元素“固定”位置

    stickUp能让页面目标元素“固定”在浏览器窗口的顶部,即便页面在滚动,目标元素仍然能出现在设定的位置. http://www.bootcss.com/p/stickup/

  8. nyoj 708 ones 动态规划

    http://acm.nyist.net/JudgeOnline/problem.php?pid=708 状态转移方程的思路:对于一个数N,可以是N - 1的状态+1 得到,另外,也可以是(n / 2 ...

  9. Python基础篇【第5篇】: Python内置模块(二)

    内置模块 1. OS os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname") 改变当前脚本工作目录:相当于shell ...

  10. Scale和Resolution的含义及转换算法

    当我们在用arcgis server 构建切片时,我们会发现在缓存生成的conf.xml中有这样的片段: 在上述片段中<LODInfo>代表了每一级切片的信息,<LevelID> ...