本质:Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。 核心的三个功能模块: (1)内容寻址的文件系统——>Git 的核心部分是键值对数据库。 (2)传输机制 (3)版本管理任务
下面我们通过实际操作,一步步认清 git 内部的工作原理。
首先,我们 git init test
创建一个测试项目,进入 .git
文件夹会看到如下图的目录结构:
Git 的核心组成部分,包括:
事实上, .git
这个目录已经包含了几乎所有的 Git 存储和操作的对象了,所以,如果想要备份或者复制一个版本库,只需要把这个目录拷贝到别处就可以了。
ok~下面我们就逐一的探寻一下这四个部分,希望能对 Git 的运作有更深入的理解~~
Git 是一个内容寻址文件系统。数据存储是它的核心之一,也是最基础的功能,所以我们先来看 Git 对象,相关目录就是 objects
目录。初始化的 objects
目录,里面包含了 pack 和 info 两个子目录,但都是空目录。我们通过命令来尝试向 Git 的数据库写入一些文本,命令:
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
可以看到,该命令输出了一个长度为 40 个字符的校验和——带存储信息与头部信息的 SHA-1 哈希值。Git 是如何存储这个对象的呢?查看一下:
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
一个文件对应一条内容,校验和的前两个字符用于命名子目录,余下的38个字符是文件名。我们通过 cat-file
命令来取回数据验证一下:
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
可以看到这个对象正是存储着我们的输入内容。这个例子是直接存储的命令输入内容,对于文件来说,我们同样可以应用这些操作。 例如:
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
当然,我们可以更新内容:
$echo 'version 2' > test.txt
$git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
查看一下现在的对象格式:
$find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
可以看到两次的操作记录。这是最简单的文件内容存储,没有包含文件名,我们将上面这种对象叫做数据对象(blob object)。然后靠着记忆这种复杂的数据对象来寻找内容是不现实的,我们需要更多的信息。
树对象(tree object)解决了文件名保存的问题,有了它,我们可以组织更多的文件了。 通常的组织结构如下:
Git 是根据某一时刻的暂存区的状态来创建树对象的,所以,为了创建树对象,我们首先需要创建一个暂存区。使用命令 update-index
,为 test.txt 的首个版本创建一个暂存区。
$ git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txt
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
write-tree
命令,将当前暂存区创建一个新的树对象。
重复上述的命令,我们可以构造更复杂的例子,
$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
会得到如下的对象结构:
目前为止,我们依旧会被复杂的哈希值困扰,不可能去记忆哈希值来重用这些树对象。这时候,提交对象(commit object)就杀了出来。通过 commint-tree
命令创建一个提交对象,我从最开始(第一个树对象:d8329fc1cc938780ffdd9f94e0d364e0ea74f579)追溯:
$ echo 'first commit' | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
e86640744342b85fad33f0ff06a37d3b1c7b363e
$ git cat-file -p e86640744342b85fad33f0ff06a37d3b1c7b363e
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author tanganmin 1534753163 +0800
committer tanganmin 1534753163 +0800
first commit
接着我们可以引用上次的提交对象再次提交:
$ echo 'second commit' | git commit-tree 0155eb -p e86640744342b85fad33f0ff06a37d3b1c7b363e
44b72ebb26661896821b7b7ecdeba48ec3062775
$ echo 'third commit' | git commit-tree 3c4e9c -p 44b72ebb26661896821b7b7ecdeba48ec3062775
ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
$ git log --stat ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
commit ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
Author: tanganmin
Date: Mon Aug 20 16:20:09 2018 +0800
third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit 44b72ebb26661896821b7b7ecdeba48ec3062775
Author: tanganmin
Date: Mon Aug 20 16:19:53 2018 +0800
second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit e86640744342b85fad33f0ff06a37d3b1c7b363e
Author: tanganmin
Date: Mon Aug 20 16:19:23 2018 +0800
first commit
test.txt | 1 +
1 file changed, 1 insertion(+)
Git 的提交历史就这样诞生了。现在看一下 objects 的内容:
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/44/b72ebb26661896821b7b7ecdeba48ec3062775 # commit 2
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/dd/cb798c95aa54b5df478fc08e1ed4d1ca4e41b0 # commit 3
.git/objects/e8/6640744342b85fad33f0ff06a37d3b1c7b363e # commit 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
内部指针关系:
至此,Git 对象基本就都介绍完了,上面我们提高过,blob object 的组成会包含一部分 header,那么它到底是如何生成的呢?我可以用 Node 来模拟一下: 新建一个简单的 node 项目,核心依赖有三个,分别是:fs、crypto、zlib。 代码如下:
const fs = require('fs')
const crypto = require('crypto')
const zlib = require('zlib')
function blobHashObject(content, type) {
const header = `${type} ${Buffer.from(content).length}\0`
const store = Buffer.concat([Buffer.from(header), Buffer.from(content)])
const sha1 = crypto.createHash('sha1')
sha1.update(store)
const hash = sha1.digest('hex')
const zlib_store = zlib.deflateSync(store)
fs.mkdirSync(`.git/objects/${hash.substring(0, 2)}`)
fs.writeFileSync(`.git/objects/${hash.substring(0, 2)}/${hash.substring(2, 40)}`, zlib_store)
console.log(hash)
}
blobHashObject(process.argv[2], process.argv[3])
当然,执行前这个目录也需要 git init 一下,
node blob.js 'hello, world' blob
8c01d89ae06311834ee4b1fab2f0414d35f01102
git cat-file -p 8c01d89ae06311834ee4b1fab2f0414d35f01102
hello, world
这样,我们创建出来了一个有效的 Git 数据对象。
git log--stat ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
命令可以看到完整的提交历史,单位了能遍历历史,你还是需要记住 ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
这个最后的提交。我们可以用名字指针来代替这个原始的 SHA-1 值。这就是引用了,推荐使用安全的更新命令 update-ref
$ git update-ref refs/heads/master ddcb798c95aa54b5df478fc08e1ed4d1ca4e41b0
$ git log master
$ git update-ref refs/heads/test 44b72ebb26661896821b7b7ecdeba48ec3062775
$ git checkout test
HEAD 文件,一个符号引用(symbolic reference),指向当前所在分支。如当前分支 master:
$ cat .git/HEAD
ref: refs/heads/master
Ok,Git 管理的内部原理到这里基本就介绍完了,我们下面可以看一些常用的技巧命令。
(1)放弃某个文件的修改 使用命令 git checkout--
就可以放弃掉某个文件的修改。其实,我们在执行 git status
的时候,就提示我们了,如图:
(2)备份或复制一个版本库,只需将 .git 目录拷贝至另一处即可。