### 导读
这篇文章是对 Git Pro 10.2 Git 内部原理 – Git 对象章节的解读和转化,主要介绍两个东西:1)使用 Git 底层命令完成提交,2)尝试使用 NodeJS 解析 Git 对象(文章中提供的是 Ruby)。
### 0x001 初始化
初始化一个本地仓库:
$ mkdir git-test
$ cd git-test
$ git init
Initialized empty Git repository in ...
查看文件结构:
+ git-test
+ .git
+ branches
- config
- description
- HEAD
+ hooks
+ info
+ objects
+ info
+ pack
+ refs
其他暂时不关注,只关注 objects
,此时只有info
和pack
两个文件夹,我们也不关注它,我们只关注 objects
下除了 info
和pack
之外的变化。
0x002 hash-object
这个命令用来为一个文件计算对象 ID 并可能创建一个 blob 文件。这里有两层含义:1)计算对象 ID,对象 ID 是什么呢?2)blob 文件是啥?为啥是可能?接下来将给出答案。
执行命令:
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
-w
指示 hash-object
存储数据对象,如果不指定,则返回计算的 objectId。--stdin
指示从标准输入读取内容,也就是将 test content
作为内容计算。
当我们执行这个这个命令的时候,会返回一个 40 个字符长度的 SHA1 哈希值。d670460b4b4aece5915caf5c68d12f560a9fe3e4
,由于制定了 -w
,git
会存储这次的计算,查看objects
下的文件:
+ objects
+ d6
- 70460b4b4aece5915caf5c68d12f560a9fe3e4
会发现,多了一个文件夹 d6
,而d6
中有一个文件70460b4b4aece5915caf5c68d12f560a9fe3e4
,这两者拼接起来正好是刚刚生成的objectID
。
如果我们多次执行这个命令,会发现,这个文件没有发生变化,因为已经存在,这也就是之前说可能生成的原因了。
如果我们改变内容,则会生成一个新的 objectID
和一个新的 blob
文件。
0x003 cat-file
我们已经直到如何存储文件,那如何读取呢?可以使用cat-file
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
0x004 文件存储和版本恢复
接下来我们使用文件,而不直接使用内容
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
然后更新这个文件并存储
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
此时的objects
+ objects
+ 1f
- 7a7a472abf3dd9643fd615f6da379c4acb3e3a
+ 83
- baae61804e65cc73a7201a7252750c76066a30
+ d6
- 70460b4b4aece5915caf5c68d12f560a9fe3e4
然后吧文件内容恢复到第一个版本
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1
或者第二个版本
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2
0x005 树对象 和 write-tree
将文件加入缓存区
$ git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txt
将缓存区内容写入树对象
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
新建一个新的数对象,包含 test.txt
的第二个版本和一个新的文件:
$ 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
0x006 提交 commit 和 commit-tree
有了树对象之后,就可以提交该树对象,生成一个commit
$ echo 'first commit' | git commit-tree d8329f
b51096bf62fa145c0b95ce18dc3020daa1f2556e
查看这个commit
$ git cat-file -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
first commit
接着提交第二个树对象,并使用 -p
指定这个提交的上一个commit
$ echo 'second commit' | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
bf41fa3700a67914b3b45eefced02fffcdaf4464
使用 git log
查看记录
commit bf41fa3700a67914b3b45eefced02fffcdaf4464
Author: lyxxxx <lyxxxx@yeah.net>
Date: Sun Nov 17 22:14:36 2019 +0800
second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit b51096bf62fa145c0b95ce18dc3020daa1f2556e
Author: lyxxxx <lyxxxx@yeah.net>
Date: Sun Nov 17 22:07:01 2019 +0800
first commit
test.txt | 1 +
1 file changed, 1 insertion(+)
以上,便是使用底层命令创建 Git 提交历史的过程,主要涉及 5 个命令:
-
hash-object
:计算objectID
,创建blob
文件 -
cat-file
:读取生产的object
文件 -
update-index
:更新暂存区文件 -
write-tree
:将暂存区文件写入tree
文件 -
commit-tree
:提交tree
文件
0x007 object
文件类型
object
文件类型一共有三种:
-
blob
:hash-object
生成,表示一个文件 -
tree
:write-tree
生成,表示缓存区的文件列表 -
commit
:commit-tree
生成,表示本次提交的文件列表
他们的关系是:
-
commit
包含一个tree
对象 -
tree
包含多个blob
对象和tree
对象
0x008 objectID
如何生成
接下来使用 NodeJS
演示如何生成objectID
,
假设我们要存储的内容是what is up, doc?
const content = 'what is up, doc?'
const type = 'blob'
object
对象的存储的格式是:
const store = `${type} ${content.length}\0${content}`
然后计算sh1 值
:
const crypto = require('crypto');
const hash = crypto.createHash('sha1');
hash.update(store);
const objectID = hash.digest('hex');
最后计算的结果是:
bd9dbf5aae1a3862dd1526723246b20206e5fc37
接着是存储,在存储的时候,会执行压缩之后再存储:
const zlib = require('zlib');
const result = zlib.deflateSync(Buffer.from(store))
然后按照 objectID
分割存储到 objects
文件夹下就行了:
+ objects
+ bd
- 9dbf5aae1a3862dd1526723246b20206e5fc37
完整源码:
const zlib = require('zlib');
const fs = require('fs');
const Buffer = require('buffer').Buffer
const crypto = require('crypto');
const type = 'blob'
const content = process.argv[2]
const store = `${type} ${content.length}\0${content}`
const hash = crypto.createHash('sha1');
hash.update(store)
const objectID = hash.digest('hex')
const result = zlib.deflateSync(Buffer.from(store))
const path = '.git/objects'
const [a, b, ...file] = objectID
const dirPath = `${path}/${a}${b}`
const filePath = `${dirPath}/${file.join('')}`
fs.mkdirSync(dirPath)
fs.writeFileSync(filePath)
0x009 资源
- 官方原文
- Git 原理入门 - 阮一峰
- 本章源码:这个项目我继续维护,慢慢尝试使用 NodeJS 写一个 miniGit 出来玩玩。
0x010 带货
最近发现一个好玩的库,作者是个大佬啊 – 基于 React 的现象级微场景编辑器。