• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

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

Git winrains 来源:Jo的工作笔记 12个月前 (11-09) 65次浏览
可能你每天都在用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空间。

作者:Jo的工作笔记

来源:https://www.jianshu.com/p/ae3f7c954061


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)