Git 文件对象存储实现方式

分布式的Git相对于svn优势已经很明显了。本文花几分钟时间简单探讨下Git 的思想和主要的对象类型。

首先来看下Git的文件目录。某个工程目录中,执行在git init 之后,打开.git文件夹可以看到 git 目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Git 目录结构
.git/
├── HEAD 当前branch的head
├── config 配置信息
├── description
├── hooks
├── info
│ └── exclude
├── objects 保存git对象 <commit tag tree blob>
│ ├── info
│ └── pack
└── refs 保存branch和tag对应的commit
├── heads branch对应的commit
└── tags tag对应的commit
Git Objects

git 对象是git实现的基础。

Git show –help 的时候可以看到Git里面有四种对象的描述 : blobs, trees, tags, commits。这就是Git 内部主要的四种对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git show --help

DESCRIPTION
Shows one or more objects (blobs, trees, tags and commits).

For commits it shows the log message and textual diff. It also presents
the merge commit in a special format as produced by git diff-tree --cc.

For tags, it shows the tag message and the referenced objects.

For trees, it shows the names (equivalent to git ls-tree with
--name-only).

For plain blobs, it shows the plain contents.
1
2
3
4
5
6
7
8
9
10
.git/objects
objects
├── 42
│ ├── d90ab79a2adc936230082d718385485fe11f00

├── 92
│ ├── 5a3a85c97272b3307960c89d993238ff377b00

├── info
└── pack

上面的目录可以看到objects下面是两层目录,这么设计主要是为了是防止对象太多。其中文件的文件名是其 sha-1 后 base16 编码的字符串。

下面分别来看下四种对象:

  • blob:存储实际的文件 (Blob: binary large object

    blob文件只存储文件内容,对应的文件名并没有存在 blob 对象中。好处是相同内容的文件,即便拷贝多份,依然只存储一份数据 。所以blob 只和内容相关,更改文件名只是生成一个新的 tree,不会新增blob。也就是说任何人,在任何硬件环境下,相同的内容都会生成相同的 blob。

    blob可以被组织成树,一次 提交(commit)就是根据更改的文件的信息生成新的树的过程,新树和老树共享相同的子树,只有变化的部分才会分叉。通过使用引用,比如 HEAD, heads/master,tags/v0.1,git 可以很方便地追踪用户关心的每一棵树的状态。

  • tree:存储文件的目录结构

    tree,代表目录,记录文件路径 文件名。通过tree找到Blob,任何人,在任何硬件环境下,相同的目录结构,包含相同的内容,都会生成相同的 tree。

  • commit:存储提交信息(主要是当前的树根和上一棵树的树根)

    因为commit不仅存有Tree,还存有 Author Email Date 等信息,所以每个Commit 哈希值都不同。每次commit 相当于当前工程的一个snapshot。(通过 Commit -> tree -> blob 定位对应的文件)

  • tag:存储版本信息,相当于对对象库中的某个 commit 显式标记了一下。

我们可以使用 git show <文件名> 命令来查看Object的类型。打开工程下面的 .git/objects目录,由于objects下面是二级目录,所以里面的 子文件夹名字+文件名,就是一个完整的对象ID。下面是一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
用来展示对象内容
Git show --pretty=raw <文件名>


tree:
git show --pretty=raw a29d
tree a29d
README.md

使用git ls-tree 命令,展示tree内容:
git ls-tree a20d
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d README.md


blob,git show后输出文件内容的,就是blob:
git show --pretty=raw d00491
1


commit,一般一个commit会有tree,parent,author等等内容,如果是第一条commit应该不会有parent:
git show --pretty=raw 66ec445
commit 66ec445dfc9094d408382193df0bc3c612012729
tree a29d072c7904ec17a3fc5cc37310e0ebc6be1805
author HuanLiu <iosboog@163.com> 1574929078 +0800
committer HuanLiu <iosboog@163.com> 1574929078 +0800
add
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/README.md
(除第一条commit外,都会有一个parent,指向上一次commit)

由此可见,基本上一个commit就是一次snapshot。记录了当时的目录结构以及压缩保存的文件。同时,由于

  • 每个文件一旦写入对象数据库中都是不可更改的
  • 任何微小的修改,都会在数据库中形成一个新的对象
  • blob不记录文件名,只要文件内容相同,就会使用同样的blob。

通过commit -> tree -> blob,我们可以找到任意一个文件,以及修改记录。

一些git命令原理也同样利用这个规则。

比如, git stash 同样以git object对象实现。stash命令实际上创建了一个新特殊commit对象,该commit对象有两个parent,一个是前面一次git commit命令生成的commit,另一个对应于保存到stage中的commit。git branchgit tag 也都存储了对应的commit。不同的是tag创建后其指向的commit不能变化,而branch创建后,会在提交新的commit后向前移动。