Prince Home Stay Follish, Stay Hungry

自底向上理解Git原理

2017-07-07
Git

Git是目前最流行的版本管理工具,但是由于其命令比较复杂,有时候无法很顺利的使用,但是,一旦了解Git的底层原理,那么Git的命令也就变得简单起来。本文是Git From the Bottom Up的总结与拓展

前言

Git是目前最流行的版本管理工具,但是由于其命令比较复杂,有时候无法很顺利的使用,但是,一旦了解Git的底层原理,那么Git的命令也就变得简单起来。本文是Git From the Bottom Up的总结与拓展

Git的基本术语

  • repository: Git仓库,是commit的集合,仅此而已
  • working tree: 当前文件系统的工作目录,不包括.git
  • index: 暂存区,即将成为一个新的commit的临时节点
  • commit: 可以理解为working tree在某个时间点的一个快照(snapshot),当然只记录了增量的文件修改
  • branch: Git分支,实质是指向某个commit的指针,有一个别名如dev,随着这个分支的提交,这个分支对应指针会指向新的HEAD
  • tag: tag和branch都是指向某个commit的指针,也有一个别名,唯一区别是,tag指向一个commit,指针不会再移动。
  • master: 提交主要分支,系统建立的第一个分支
  • HEAD: 重要概念,一个commit指针,用于记录工作区当前checkout的分支状态
    • 若checkout的是一个branch,那么HEAD指针直接指向该branch
    • 若checkout的是一个tag或是一个commit的hash id,那么HEAD处于detatch状态
  • tree: Git中的的树形结构,类似于目录,tree可以包含多个tree,也可以包含blob,blob是叶子节点,tree是非叶子节点

    一个典型的工作流的示意图:

Git文件存储方式与OS的文件系统

文件在Git中存储是使用blob的方式存储,与Linux操作系统中的inode号类似,一个文件对应一个inode号,不同的inode号代表不同的文件,如果两个文件共享一个inode号,那么这两个文件是一个硬链接。

在Git中,每个文件是一个blob,使用SHA1算法计算出一个hash id,作为这个文件的唯一标识,在Git中,hash id相同的两个文件内容一定相同。与文件系统不同的是,Git中,相同内容的文件使用同一个blog表示,因为Git中的文件不记录原信息(如文件修改时间等等信息)。

在Linux中,两个文件如果两个文件内容相同,但是创建于不同的时间,那么可以被OS认为是不同的文件,但是在Git中,由于不存储原信息,那么只要两个文件内容相同,那么他们就是同一个文件。有这种区别的原因是,操作系统保存的文件是常常可变的,而Git管理的文件,一旦进入commit集合,那么就是不可变的

Git的一次提交

Git的典型提交如下:

$ mkdir sample; cd sample
$ echo "Hello, world!" > greeting
$ git init
$ git add greeting
$ git commit -m "Added my greeting"

实质上,内部过程是这样的:

  1. 编辑greeting文件
  2. greeting被计算为blob,添加进入index
  3. Git将index的所有文件组织成tree
  4. 新建一个commit,指向这个tree,这个commit的hashid使用commit的日期,commit的用户计算出来
  5. 把当前分支(master)的HEAD指向这个commit

所以是head -> commit -> tree -> blob Git中,能计算出hashid的东西只有三种

  • blob: 代表单个文件,使用文件内容计算hash
  • tree: 包含多个blob组成的树形结构,使用目录内文件的内容计算hash
  • commit: 指向一个tree,记录commit的metadata,使用这些内容计算hash
    • 提交时间
    • 提交人
    • 提交的comment
    • parent,一个commit可能有多个parent

Git是Commit的世界

Git的世界是由commit组成的一个大图,每个commit是图中一个节点(圆形节点),这个图是一个有向无环图,对于每个节点,可能有一个或多个的祖先,一个或多个的孩子,在Git中,节点只记录它的parent(s):

  • 含有多个祖先: 这是一个merge节点
  • 含有多个孩子: 这是一个branch节点

每个commit都对应着一个tree,每个tree里面还有tree(三角形节点)或是blob(方形节点)

在理解了Git的Commit的世界以后,不需要再纠结于tag,branch,

Say it with me: A branch is nothing more than a named reference to a commit :分支知识一个commit的引用,其他啥也不是

commit的引用方法

你有一千种方法来引用某个commit~不过最常用的的是这些:

  • branchname: 使用分支名来引用commit,引用的是这个分支指向的commit
  • tagname:和branchname一样,引用的是这个tag指向的commit
  • HEAD:使用HEAD引用的是当前正在使用的commit,也就是使用git commit命令产生新的commit后新commit的父节点
  • 93a88bc922dbd…:完整的sha1,代表具体的一个commit
  • 93a88b:在本repo中的可以唯一确定的sha1码,只需要前几位即可,表示具体的一个commit
  • name^:表示name的父节点,name表示任何合法的commit表示方法,如果一个commit有多个父节点(Merge节点)那么取第一个,比如HEAD^
  • name^^:表示name的祖父节点(父亲的父亲)如果是祖父节点的父节点用name^^^表示
  • name^2:表示name的第二个父节点,当name有多个父节点的时候使用,如果还有更多父节点,可以使用name^n
  • name~n:表示name的父亲n代,比如name~5相当于name^^^^^
  • name:path:表示name这个commit快照中的path路径比如在比较两个文件diff的时候可以使用:
$ git diff HEAD^1:Makefile HEAD^2:Makefile # 注意git diff两个参数的关系,git diff old new看变化
  • name^{tree}:引用该commit对应的tree,而不是name本身 其余的引用方式可以参考原文

merge和rebase的区别

merge发生在两个commit之间,当多个commit有着共同的branch base的时候,可以使用git show-branch来查看效果,下面图片产生的是

 $ git show-branch
! [Z] Z
 * [D] D
--
 * [D] D
 * [D^] C
 * [D~2] B
+  [Z]Z
+  [Z^]Y
+  [Z~2] X
+  [Z~3] W
+* [D~3] A

注意:git merge XX命令是将XX分支合并到当前的分支,而不对XX分支改变,git merge命令会在当前分支生成一个新的节点,这个节点的前驱是XX分支的指向的commit和HEAD指向的commit

git rebase的作用: git rebase XX命令是把XX分支与当前分支进行衍合,过程是将当前分支“嫁接”在XX分支上,把W作为D的改变提交,在这个过程中,每一步都有可能出现冲突,可能需要修改多次冲突,rebase的结果是两个分支变为一个分支,称为线性结构

Git 中的index

index就是中文的暂存区,用来生成下一个commit的时候使用,index的唯一作用就是暂时存储下一个commit的blobs,用来构成下一个commit对应的tree。当使用git commit的时候,会将当前暂存区内所有的blob打包成tree,然后形成新的commit节点。

有一个方法可以几乎忽略暂存区的存在

$ git commit -a

这句话相当于把当前的修改全部加入暂存区后commit,不过注意commit -a会将新生成的文件全部忽略,只有在原来基础上修改的文件会被commit,除此之外,commit -a命令几乎可以理解为使svn中的svn commit

index存在的一个必要是,可以通过git add很好的控制一次commit中添加的文件 比如一次working tree中修改的了两个不相关的文件,可能是两个功能,可以分两次commit,增加commit的原子性

Git中的reset

reset是Git版本回退的主要方法,其主要功能是改变HEAD指针指向的commit,当HEAD指向HEAD^的时候,版本将退回上一个版本。不过要注意的是,reset的三种参数:

  • --soft:纯粹改变HEAD指针,而对index和working tree视而不见
  • --hard:这个参数除了移动指针,强制将index和working tree和HEAD进行同步,因为这个时候你的working tree还没有提交到commit上,所以这种舍弃可能是破坏性的,是Git中一个很危险的参数
  • --mix:默认参数,soft和hard的混合,改变HEAD指针后,把Index和HEAD同步,但不改变working tree

参考文章

  • Git From Bottom Up:篇文章是打开我Git新大门的文章,非常有insight,有时间考虑翻译这篇文章
  • 图解Git:比较生动的图解Git,便于记忆

下一篇 Decouple Networks

Comments

Content