前端漫谈Git-内部原理-Git-对象

5次阅读

共计 4662 个字符,预计需要花费 12 分钟才能阅读完成。

### 导读
这篇文章是对 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,此时只有infopack两个文件夹,我们也不关注它,我们只关注 objects 下除了 infopack之外的变化。

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,由于制定了 -wgit 会存储这次的计算,查看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文件类型一共有三种:

  • blobhash-object生成,表示一个文件
  • treewrite-tree生成,表示缓存区的文件列表
  • commitcommit-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 的现象级微场景编辑器。

正文完
 0