Git 内部原理

了解 Git object,如blob、tee、 commit等?

知道 git reset –soft、git reset、 git reset –hard 的区别?

听说过 git reflog ?

实验1

  1. linux系统中,使用mkdir git-test创建文件夹。
  2. cd git-test
  3. tree .git
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
lautung@PC:~/git-test$ tree .git
.git
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

9 directories, 16 files

注意.git目录下的objects目录。

  1. 使用 echo 命令,输入添加文件

    1
    2
    lautung@PC:~/git-test$ echo 111111 > a.txt
    lautung@PC:~/git-test$ echo 222222 > b.txt

    此时目录下存在文件a.txtb.txt

  2. git add a.txt b.txt 添加到暂存区。

  3. 使用tree .git命令打印目录结构,如下:

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
33
34
lautung@PC:~/git-test$ tree .git/
.git/
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── 87
│   │   └── 6c799506a9f7f24febd177d22352477f10fab0
│   ├── 90
│   │   └── d2950097fa1850b6f692ab1095ec9cdd3f7fae
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

11 directories, 19 files

我们发现objects目录下,多了两个文件目录87和90,87目录下有一个文件6c799506a9f7f24febd177d22352477f10fab0,90目录下有一个文件d2950097fa1850b6f692ab1095ec9cdd3f7fae。

  1. 尝试使用cat .git/objects/87/6c799506a9f7f24febd177d22352477f10fab0打印其中一个文件。我们只是一段乱码,因为git对存储的内容进行了二进制的压缩。
  2. cat命令行不通。不过无须担心,git为我们提供了合适的命令,使用git cat-file [-t] [-p]。使用命令,如下:
    1
    2
    3
    4
    5
    6
    lautung@PC:~/git-test/.git/objects/87$ git cat-file -t 876c799506a9f7f24febd177d22352477f10fab0
    blob
    lautung@PC:~/git-test/.git/objects/87$ git cat-file -t 876c
    blob
    lautung@PC:~/git-test/.git/objects/87$ git cat-file -p 876c
    222222
    注意文件名是文件目录名+文件名文件目录名+文件名简写的形式,上述代码876c799506a9f7f24febd177d22352477f10fab0876c指的是同一个文件。
    除此之外,我们查看打印结果,blob和222222,blob我们姑且当做不知道,但是222222,细心的同学一定记得我们在步骤4中的文件b.txt的内容吧。

好,这个小实验到此为止,到底是为了证明什么呢?请往下看。

Git Object

什么是 Git object ?git object 我们可以理解为git存储信息的最小单元。我们上面小实验最后的blob其实就是 git object的类型,它表示文件存储具体内容,比如我们打印的222222。

所以,我们在小实验最开始,添加了两个文件,git就创建了两个object,类型都是blob,内容分别是111111和222222。

实验2

我们已经前面已经知道了 Git Object 和 blob类型,下面我们开始实验2:

  1. 在实验一的基础上,我们用git commit -m "[+] init"提交暂存区内容。
  2. 使用tree .git 观察文件目录,如下:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
lautung@PC:~/git-test$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 48
│   │   └── e24744cfdd74856b23de73122c702ad9b58b3c
│   ├── 87
│   │   └── 6c799506a9f7f24febd177d22352477f10fab0
│   ├── 90
│   │   └── d2950097fa1850b6f692ab1095ec9cdd3f7fae
│   ├── fd
│   │   └── 7e0f5ecf96bb392415b43b034b7108cd175b94
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags

16 directories, 25 files

我们能够发现,多了两个目录(48和fd)极其对应的文件。

  1. 使用 git cat-file [-t] [-p]分别查看两个文件。
1
2
3
4
5
lautung@PC:~/git-test/.git/objects/48$ git cat-file -t fd7e
tree
lautung@PC:~/git-test/.git/objects/48$ git cat-file -p fd7e
100644 blob 90d2950097fa1850b6f692ab1095ec9cdd3f7fae a.txt
100644 blob 876c799506a9f7f24febd177d22352477f10fab0 b.txt

文件类型:tree,值有两个行,每行从左到右分别是:权限、文件类型、SHA1值、文件名。这个说起来像是快照,通过tree类型的文件,可以找到相对应的文件。

1
2
3
4
5
6
7
8
lautung@PC:~/git-test/.git/objects/48$ git cat-file -t 48e2
commit
lautung@PC:~/git-test/.git/objects/48$ git cat-file -p 48e2
tree fd7e0f5ecf96bb392415b43b034b7108cd175b94
author lautung <lautung@foxmail.com> 1629644757 +0800
committer lautung <lautung@foxmail.com> 1629644757 +0800

[+] init

文件类型:commit,文件内容:commit文件对应的快照指针(sha1值),作者信息,空行,提交的描述信息。

通过实验二,想必已经对commit、tree、blob各自的用途了解了。那么branch(分支)和tag存在哪里呢?

分支和Tag存在哪里?

git 其实把他两以明文的形式存储,以下:

1
2
3
4
lautung@PC:~/git-test$ cat .git/HEAD
ref: refs/heads/master
lautung@PC:~/git-test$ cat .git/refs/heads/master
48e24744cfdd74856b23de73122c702ad9b58b3c

48e24744cfdd74856b23de73122c702ad9b58b3c是实验二的那个commit类型文件。所以,HEAD、分支、普通的Tag可以简单的理解成是个指针,指向对应 commit的SHA1值。我们修改实验二的图,可得以下图形:

Q1:为什么要把文件的权限和文件名储存在
Tree object里面而不是 Blob object呢?
每次提交,如果有一个文件没有变更,Tree直接复用即可,能节省很多麻烦和内存。

Git的三个分区及变更历史的形成

在工作区,添加文件及其内容,只有工作区会发生变化,其它两区是没有变化的,如图:

第二步,我们执行git add .的时候,首先,git将修改的或新的文件在git仓库创建一个blob文件,其次,将暂存区的索引指向那个新的blob文件。

第三部,我们执行git commit -m "commit massenge"的时候,有如下三步:

  1. 新建一个tree object文件,记录包含的文件索引。
  2. 新建一个commit object文件,指向步骤1中的tree object文件。
  3. 将master指针(当前分支指针),指向最新的commit object。

Q2:每次 commit,Git储存的是全新的文件快照还是储存文件的变更部分?
全新的文件快照。虽然会比较占用空间,但是这是一个空间换时间的策略,时间复杂度O(1),如何存储变更,就需要大量的算法,在网络传输也不好用。

Q2:git 如何保证历史记录不被篡改?
Git和区块链的数据结构非常相似,两者都基于哈希树和分布式。,一旦修改所有都会修改。

参考

  1. Git 内部原理揭秘
  2. 图解