简单易懂理解Git的工作原理(一)Git数据模型

可能你每天都在用Git,但是你真的了解Git的内部是如何工作的吗?也许你跟之前的我一样,对Git的工作原理不甚了解,导致出现什么问题都必须通过搜索Stack Overflow来解决。为了摆脱这些困扰,我阅读了Scott Chaccon写的《Pro Git》。这本书不仅讲了很多平常使用Git的技巧,还深入剖析了Git的原理,是一本非常值得一看的好书。正因为读了这本书,我对Git原理的理解增进了许多,现在的我对Git的操作更有自信,不再像以前一样每步操作都心中没数。今天我就想通过例子来跟大家分享我的理解,希望能对各位有所帮助。

Git是怎么储存数据的

想要了解Git的工作原理,就必须首先了解Git是怎么储存数据的。你可以把Git理解成一个存放着不同对象(object)的hash table, 而这个hash table的键值(key)则就是对象的hash值。这样保证了不同的对象不会互相覆盖。Git当中一共有3种主要的对象:

  • blob
  • tree
  • commit

每个blob类型的对象对应的就是repository里面的一个文件。对象的内容就是该文件的内容。现在我们来验证一下。首先建立一个新的Git repo:

$ mkdir git_test
$ cd git_test
$ git init

我们创建一个新文件,并把文件添加到Git里面:

$ echo "version 1" > index.txt
$ git add index.txt

注意,当我们运行git add以后,该文件实际上已经被加入到了Git的Hash table中了,而并不是仅仅在cache当中。我们可以运行一下命令来列出Git的hash table里面的对象:

$ git cat-file --batch-check --batch-all-objects
16f4c48319f2bb70f5289fc1ea325dabc9d14729 blob 8

我们可以看出,在Git的hash table中有一个对象,他的Hash值是16f4c48319f2bb70f5289fc1ea325dabc9d14729,类型是blob。我们可以运行git cat-file来查看这个对象的内容:

$ git cat-file -p 16f4c48319f2bb70f5289fc1ea325dabc9d14729
version 1

那么,从这里我们可以看出,该文件确实被作为blob被存储起来了。

那么我们接下来试着在一个文件夹里面建立一个新文件:

$ mkdir dir
$ echo "version 1, inside" > ./dir/inside.txt
$ git add ./dir/inside.txt

我们再来看看现在的hash table:

$ git cat-file --batch-check --batch-all-objects
16f4c48319f2bb70f5289fc1ea325dabc9d14729 blob 8
bf97e71de76bcff2bd8aba44710aa5e665eacb99 blob 21

因为新添加了一个文件的缘故,现在有两个blob了。一切都符合我们的预期。现在让我们尝试着git commit,然后再查看hash table:

$ git commit -m "Commit 1"
$ git cat-file --batch-check --batch-all-objects
16f4c48319f2bb70f5289fc1ea325dabc9d14729 blob 8
312940d2b3d55f429bd796ddb3155566dc06b0c7 tree 67
39bdea2300491cc90f47551887bfe9503cd4a9d9 tree 38
bddef1a9efb1a84bcc6de74882ebe93b6336ade1 commit 203
bf97e71de76bcff2bd8aba44710aa5e665eacb99 blob 21

Wow,怎么突然冒出了那么多东西?除了我们之前的两个blob以外,还多了两个tree和一个commit。我们来看看tree是什么东西:

$ git cat-file -p 312940d2b3d55f429bd796ddb3155566dc06b0c7
040000 tree 39bdea2300491cc90f47551887bfe9503cd4a9d9    dir
100644 blob 16f4c48319f2bb70f5289fc1ea325dabc9d14729    index.txt

很明显,这一个tree中指向另一个叫dirtree,和一个叫index.txtblob。这么这个tree不就是跟我们的根目录结构一样吗?同样,再看看另一个tree的内容:

$ git cat-file -p 39bdea2300491cc90f47551887bfe9503cd4a9d9
100644 blob bf97e71de76bcff2bd8aba44710aa5e665eacb99 inside.txt

这个tree不就是代表我们dir文件夹吗?确实没错,tree类型的对象在Git里面代表的就是一个文件夹,而这个对象的内容就是面向它所包含的文件夹和文件的指针。

还有最后一个commit对象,我们也来看看:

$ git cat-file -p bddef1a9efb1a84bcc6de74882ebe93b6336ade1
tree 312940d2b3d55f429bd796ddb3155566dc06b0c7
author Jo <jo@gmail.com> 1541076281 +0100
committer Jo <jo@gmail.com> 1541076281 +0100

Commit 1

我们可以看到,这个commit对象首先包含了一个面向根目录tree对象的指针,而且还包含了committer的个人信息还有commit的注释。

说到这里,你应该大致了解这三个对象之间的指向关系。总结来说是这样的:
committreetree 或者 blob

好了,现在让我们尝试修改一下文件的内容并做一个新的commit:

$ echo "version 2" > index.txt
$ git add index.txt
$ git commit -m "Commit 2"

我们再来看看新的hash table:

$ git cat-file --batch-check --batch-all-objects
16f4c48319f2bb70f5289fc1ea325dabc9d14729 blob 8
1c5c8a007e7e4abd6fc7c80d9ef01effe30fd7b1 tree 67
312940d2b3d55f429bd796ddb3155566dc06b0c7 tree 67
39bdea2300491cc90f47551887bfe9503cd4a9d9 tree 38
8231f0fdc862f06b2bd7b7bfd2f42082d3086b71 blob 13
82b70486b425018a6f9250f5e50ffdbc3db24359 commit 251
bddef1a9efb1a84bcc6de74882ebe93b6336ade1 commit 203
bf97e71de76bcff2bd8aba44710aa5e665eacb99 blob 21

比起之前,现在我们又多了一个blob,一个tree和一个commit。我们先看看新添加的blob是什么:

$ git cat-file -p 8231f0fdc862f06b2bd7b7bfd2f42082d3086b71
version 2

也就是说,我们新版本的index.txt是被当做一个独立的文件添加到了hash table中了。这里我们得出一个很重要的结论:Git并不储存一个文件不同版本之间的diff,Git把同一个文件的每个版本都当做一个独立的文件来储存

然后我们来看看新的tree对象:

$ git cat-file -p 1c5c8a007e7e4abd6fc7c80d9ef01effe30fd7b1
040000 tree 39bdea2300491cc90f47551887bfe9503cd4a9d9    dir
100644 blob 8231f0fdc862f06b2bd7b7bfd2f42082d3086b71    index.txt

这个tree对象指向的dir的指针还是同一个,因为文件夹dir里的内容没有变化。指向index.txt的指针却变成了新的blob的Hash值了。正因为如此,这个tree对象的内容变了,它的Hash值也变了,成为了一个新的对象。

最后来看看新的commit对象:

$ git cat-file -p 82b70486b425018a6f9250f5e50ffdbc3db24359
tree 1c5c8a007e7e4abd6fc7c80d9ef01effe30fd7b1
parent bddef1a9efb1a84bcc6de74882ebe93b6336ade1
author Jo <jo@gmail.com> 1541076281 +0100
committer Jo <jo@gmail.com> 1541076281 +0100

Commit 2

跟第一个commit不同的是,这里的tree项指向了新的根目录tree对象,而且还多了一个parent项,指向的正是第一个commit。从此以后每个新的commit都会指向一个原有的commit,形成了一个单向链表。

我们之前所建立的repo中对象的关系,可以用以下的图来概括:

Git对象关系图

Branch, Head和Tag

从上面的结论我们得知,commit之间就是一个单向链表,新的commit总是会指向一个原有的commit。如果我们知道最新的commit的位置,就能顺藤摸瓜地找到之前所有的commit。以上图为例,如果我们知道Commit 2的所在,那么我们就能够通过指针获得所有对象的数据。问题是我们总不能靠记住Hash值来定位它吧,这也太不科学了。因此,我们有了branch这个概念。所谓的branch,其实就是一个指向某个commit的指针文件,一般被存放在./.git/refs/heads里面。我们知道,一个新建的Git repo会有一个默认的master branch。这个branch的信息实际上就存在./.git/refs/heads/master这个文件里。回到我们上文的例子中,我们可以直接查看这个文件:

$ cat ./.git/refs/heads/master
82b70486b425018a6f9250f5e50ffdbc3db24359

很显然,master就是一个普通指针,指向的正正是我们的Commit 2。每当我们想定位Commit 2,只需要通过master就可以了。

HEAD是一个指向目前工作目录(working tree)所基于的commit的指针。它的信息存储在./.git/HEAD这个文件中。让我们来查看一下这个文件:

$ cat ./.git/HEAD
ref: refs/heads/master

正如我们看到的,HEAD通常指向一个branch,在这里是我们的master。如果我们执行一个新的commit,那么HEAD所指向的branch就会自动往前移。就这样Git保证了branch的指针一直指向最新的commit。

但有时候HEAD也可能不指向一个branch。例如当我们执行:

$ git checkout bddef1a9efb1a84bcc6de74882ebe93b6336ade1
Note: checking out 'bddef1a9efb1a84bcc6de74882ebe93b6336ade1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at bddef1a Commit 1

$ cat ./.git/HEAD
bddef1a9efb1a84bcc6de74882ebe93b6336ade1

$ git status
HEAD detached at bddef1a
nothing to commit, working tree clean

这个时候HEAD直接指向一个commit,Git会警告我们出现了所谓的detached HEAD现象。如果你在这种情况下创建新的commit,由于HEAD没有指向任何branch,Git没法通过移动branch来追踪最新的commit。因此一旦你切换到另外一个branch,那么你在detached HEAD情况下创建的commit将很难被找回。解决方法是创建一个新的branch:

$ git checkout -b new-branch
$ git status
On branch new-branch
nothing to commit, working tree clean

上面所说的情况可以概括为下图:

Detached HEAD示意图

最后我们稍微提一下tagtag相当于是静态的 branch ,被存储在./.git/refs/tags里面的文件中。它并不跟随HEAD移动。通常用于标记一个特定的commit,例如某个版本的代码,以方便checkout。

好的,Git的基础数据模型讲到这里就差不多了。在下一篇文章里,我们来探讨一下Git的index空间。

简单易懂理解Git的工作原理(二)Index空间

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容