Git 原理入门

Git 如何储存档案内容?

Git 储存内容时,都是透过 git hash-object 取得 SHA1 并储存起来,如下所示,只要档案内容稍有变动,git hash-object 的结果就会天差地远:

$ echo 'f' | git hash-object --stdin6a69f92020f5df77af6e8813ff1232493383b708$ echo 'fi' | git hash-object --stdine133fadec9f9f77fd6add0e533715d74d393eace

备注:事实上 git hash-object 是将「档案内容和 Header」做 SHA1。Header 是 “blob” 加上空格、内容长度、结尾 null bytes。

SHA1("blob 2\u0000fi")e133fadec9f9f77fd6add0e533715d74d393eace

了解了 Git 产生 SHA1 的方式,我们可以进一步把这个 SHA1 储存起来,依循前面的例子,不一样的是这次我们将 SHA1 储存起来:

$ echo 'f' | git hash-object --stdin -w6a69f92020f5df77af6e8813ff1232493383b708$ echo 'fi' | git hash-object --stdin -we133fadec9f9f77fd6add0e533715d74d393eace

看起来跟前面例子输出一样,要怎么验证 SHA1 已经被 Git 储存起来了呢?我们可以列出 .git 目录下的 objects 资料夹内容:

$ tree .git/objects/.git/objects/├── 6a│   └── 69f92020f5df77af6e8813ff1232493383b708├── e1│   └── 33fadec9f9f77fd6add0e533715d74d393eace├── info└── pack4 directories, 2 files

我们可以看到,git hash-object 所产生的 SHA1,前面 2 个数字、字母会被切出来当成资料夹,剩余的 38 个数字、字母当成档名,被放置在 .git/objects 资料夹底下。

那这两个档案储存的内容是什么呢?

首先我们用 cat 这个指令来看一下内容:

$ cat .git/objects/6a/69f92020f5df77af6e8813ff1232493383b708xK��OR0bH�nb

你没看错,档案内容是乱码。原因是因为 git 用二进制的方式将内容储存起来,我们可以用 git 提供的指令来轻易地检视这些内容。

$ git cat-file -p 6a69f92020f5df77af6e8813ff1232493383b708f$ git cat-file -p e133fadec9f9f77fd6add0e533715d74d393eacefi

不一样的是,git cat-file 指令要直接给整串 SHA1,而不再是目录、档名。然后你可以看到,结果就是我们前面给的档案内容。

至此,你了解了 Git 是将档案内容,加上 Header 后,产生对应的 SHA1,当成目录、档名,而其内容就是原始档案的二进制内容。一旦档案内容有所更动,SHA1 就会不一样,反之,内容相同,SHA1 就相同。

了解这样不够,我们会有些疑问:

万一我有档名、资料夹呢?哪里会纪录的我档名、资料夹名称?还有 Commit 呢?我 Commit 的内容储存在哪?Branch、Tag 储存在哪?…

不急,我们先继续往下读,你会在最后不经意说出「哦!原来是这样」。

Blob

首先,我们先介绍 git cat-file 的另一个功能,也就是检视该 SHA1 的「类型」:

$ git cat-file -t 6a69f92020f5df77af6e8813ff1232493383b708blob$ git cat-file -t e133fadec9f9f77fd6add0e533715d74d393eaceblob

可以看到,前面我们所建立的两个 SHA1 都是 blob 类型,由此可知,Git 将档案内容转成二进制,并产生 SHA1 储存起来的「物件」,称为 blob,这个很重要,请先记得。

再来,我们延续前面的内容,有了 blob 还不够,人是贪心的,我想要存档名、资料夹名称时,该怎么办?

因此 Git 提供了另外一种「类型」,称为 Tree。

Tree

在了解 Tree 以前,我们先偷窥一下 Tree 的範例内容(我们没有这些档案,只是让你看一下可能的内容):

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

上图 Tree 里面,纪录了两个档案(Blob)档名及其对应的 SHA1,如果我们用 git cat-file 加上 README 的 SHA1,就可以取得 README 这个档案的内容。而 lib 是一个资料夹,资料夹里面可能还有其他档案,所以用另一个 Tree 储存这些资料。

Tree 本身也是跟 Blob 一样,用 SHA1 当档名,然后储存二进制内容,但不一样的是,Blob 储存了我们的「档案内容」,而 Tree 则是储存了我们的「档名」,甚至还包含了另一个 Tree!如下图所示:

http://img2.58codes.com/2024/20107332UGwa87NTKz.png

如此一来,有了 Tree 这个物件,我们就可以解决前面提到的问题,我们可以储存档案的名称、资料夹名称、档案权限等等。也因为 Tree 记录了相关连的 Blob、Tree 的 SHA1,所以我们可以轻鬆地透过 Tree,找到子目录、子子目录的档案内容、名称。

至此我们就可以大致窥探到 Git 是如何储存一整个资料夹的内容、关联。首先要先有 SHA1,然后再透过 Tree 的方式记录关联,透过 Blob 记录档案内容!

了解 Tree、Blob 后,我们进一步来看看 Commit 是怎么回事。

Commit

Commit 就和 Tree、Blob 一样,也是拥有 SHA1 档名,储存在 .git/objects 目录下,我们来看看一个 Commit 物件可能的内容:

$ git cat-file -p fb1298cab9b794b251e3f8ece5bc4380ab9328f8tree d7ab14dd76c4990b64031c890f95fd9eae042c9aauthor shavenking <shavenking@gmail.com> 1512874376 +0800committer shavenking <shavenking@gmail.com> 1512874376 +0800awesome

就如同 Tree、Blob 一样,我们用 git cat-file 这个指令可以看到其储存的内容,第一行是 Tree,接着的是该 Commit 的相关资讯(作者、Commit 讯息)。

备注:Commit 只能纪录 Tree,不能纪录 Blob。

让我们直接用图来看看加上 Commit 之后的样子:

http://img2.58codes.com/2024/20107332PTHfGSlTRA.png

如果我们再新增一个 Commit 呢?请看下图:

http://img2.58codes.com/2024/2010733254mEk7hJqx.png

如果用 git cat-file 可以看到档案内容如下:

$ git cat-file -p 5ed902ec9ef442ec0a9dd5fd2bf9639175539c8btree a031edc7aa1c34a9e46b86f64d2798d9fcb8ad03parent fb1298cab9b794b251e3f8ece5bc4380ab9328f8author shavenking <shavenking@gmail.com> 1512874387 +0800committer shavenking <shavenking@gmail.com> 1512874387 +0800awesome again

除了 Tree 以外,多了一个 Parent,纪录前一个 Commit 的 SHA1,因此所有 Commit 就能串连在一起,我可以透过 git cat-file 一个一个往回找。

特别注意:Commit、Tree、Blob 都是除存在 .git/objects 里面,以 SHA1 当档名的二进制档案!

了解这些之后,接下来我们再进一步说明 Branch 及 Tag 的原理。

Branch and Tag

Branch 和 Tag 就稍微不太一样了,他们不再是以 SHA1 当档名的二进制档案,取而代之的是纪录「Commit」的 SHA1,并储存在 .git/refs 里面。

我们知道,git init 初始化后,Git 会帮我们建立一个 master,在我们 git commit 后,这个 master 就会出现在 .git/refs 底下,如下:

$ tree .git/refs/.git/refs/├── heads│   └── master└── tags2 directories, 1 file

可以看到 heads 资料夹下有一个名为 master 的档案,其内容如下:

$ cat .git/refs/heads/master5ed902ec9ef442ec0a9dd5fd2bf9639175539c8b

注意到了吗?master 这个档案(也就是 Branch),记录了 Commit 的 SHA1,所以如果我们用 git cat-file 继续追蹤的话,你就会看到下列内容:

$ git cat-file -p 5ed902ec9ef442ec0a9dd5fd2bf9639175539c8btree a031edc7aa1c34a9e46b86f64d2798d9fcb8ad03parent fb1298cab9b794b251e3f8ece5bc4380ab9328f8author shavenking <shavenking@gmail.com> 1512874387 +0800committer shavenking <shavenking@gmail.com> 1512874387 +0800awesome again

由此可见,分支只不过是一个指标,指向某一个 Commit,所以此时如果我建立另外一个分支,就会长这样:

$ git branch dev$ tree .git/refs.git/refs├── heads│   ├── dev│   └── master└── tags2 directories, 2 files$ cat .git/refs/heads/dev5ed902ec9ef442ec0a9dd5fd2bf9639175539c8b$ git cat-file -p 5ed902ec9ef442ec0a9dd5fd2bf9639175539c8btree a031edc7aa1c34a9e46b86f64d2798d9fcb8ad03parent fb1298cab9b794b251e3f8ece5bc4380ab9328f8author shavenking <shavenking@gmail.com> 1512874387 +0800committer shavenking <shavenking@gmail.com> 1512874387 +0800awesome again

可以看到,Git 只是帮我们新增了一个「Branch」档案,纪录了 Commit 的 SHA1!如果用图表示的话,就会长这样:

http://img2.58codes.com/2024/20107332hPtPukCoza.png

那 Tag 呢?也是同样的道理,我们直接用实例来看:

$ git tag v1$ tree .git/refs.git/refs├── heads│   ├── dev│   └── master└── tags    └── v12 directories, 3 files$ cat .git/refs/tags/v15ed902ec9ef442ec0a9dd5fd2bf9639175539c8b

可以看到,Tag 也只是一个档案,纪录了某个 Commit 的 SHA1,那 Branch 和 Tag 差在哪里呢?

简单来说,Branch 会一直跟着新的 Commit 成长,而 Tag 则是永远记录同一个 Commit,直到海枯石烂。

我们可以简单地再新增一个 Commit 来看看就能明白了。

首先看到我增加了第三个 Commit:

$ git logcommit ec40ddb953522ca3cba864736e3b2a01e426992bAuthor: shavenking <shavenking@gmail.com>Date:   Sun Dec 10 11:14:33 2017 +0800    new commitcommit 5ed902ec9ef442ec0a9dd5fd2bf9639175539c8b (tag: v1, dev)Author: shavenking <shavenking@gmail.com>Date:   Sun Dec 10 10:53:07 2017 +0800    awesome againcommit fb1298cab9b794b251e3f8ece5bc4380ab9328f8Author: shavenking <shavenking@gmail.com>Date:   Sun Dec 10 10:52:56 2017 +0800    awesome

此时我们看看 .git/refs 目录结构:

$ tree .git/refs.git/refs├── heads│   ├── dev│   └── master└── tags    └── v12 directories, 3 files

没错,还是维持一样,改变的只是档案内容,还记得我们的 master 跟 v1 都指向第二个 Commit 吗?一旦我们新增第三个 Commit,我们就能看出差异了:

$ cat .git/refs/heads/masterec40ddb953522ca3cba864736e3b2a01e426992b$ cat .git/refs/tags/v15ed902ec9ef442ec0a9dd5fd2bf9639175539c8b

可以清楚地看到,Branch 随着我的新 Commit 而随时更改,反之 Tag 则是待在原地呆呆地守住他的幸福。此时的图长这样:

http://img2.58codes.com/2024/201073328hrM6BU3g7.png

由以上描述可见,一旦你在某个 Branch 有新的 Commit 时,Git 就会自动更新该 Branch 档案,纪录最新的 Commit。而 Tag 则是一旦指定 Commit 后,就不会因为有任何新 Commit 而影响。

最后,还有一个关键的档案,就是 .git 目录下的 HEAD。

HEAD

我们有那么多 Branch、Tag、Commit,我们怎么知道我们现在在哪里?Git 又怎么知道该显示哪些档案?

所以才需要 HEAD 这个档案,记录我们目前所在的位置。

我们可以看一下,如果我们在 master 时,HEAD 的档案内容显示:

$ cat .git/headref: refs/heads/master

就如同 Branch、Tag 的运作方式,HEAD 这个档案仅仅是储存了一个指标,告诉我们目前所在的位置,如果用图表示就会长这样:

http://img2.58codes.com/2024/201073323E1FyWpIl8.png

回想基本指令

我们了解以上内容后,我们可以来回想一下日常生活中,我们用的 Git 指令。

git add:建立 Tree、Blob 物件,準备稍后 git commit 时使用。git commit:就如同该指令名称,我们建立了一个 Commit 的档案储存在 .git/objects。git branch:建立 Branch 档案,纪录某个 Commit SHA1,并储存在 .git/refs。git tag:建立 Tag 档案,纪录某个 Commit SHA1,并储存在 .git/refs。git checkout:这个指令可以指定 Branch、Tag、Commit 的 SHA1,也就是说你能把目前的位置(HEAD),指向任一个 Branch、Tag、Commit,让你拥有穿梭未来、过去的能力。

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章