Git 内部原理-对象、分支以及底层命令[翻译]

23-08-06 编程 #git #翻译

我们中的许多人每天都在使用 git,但是有多少人知道它的内部是怎么运作的呢?

例如我们使用 git commit 时发生了什么?提交(commit)与提交之间保存的是什么?两次提交之间难道只是文件的差异(diff)吗?如果是,这个差异是如何编码的?还是说每次提交都会保存一个当前仓库的完整快照(snapshot)呢?我们使用 git init 时到底发生了什么?

发现一篇非常精彩的 Git 内部原理文章Git 内部原理图解——对象、分支以及如何从零开始建仓库中文翻译。文章作者甚至制作了配套讲解视频

Git 对象

git 内部有三种对象:

  1. blob: 文件的内容,不包含 metadata 信息(创建时间,修改时间,作者等)
  2. tree: 一个目录,包含 blobs 或者 trees
  3. commit: a snapshot of the working tree,一个 tree 的快照

三种 git 对象都是通过 SHA-1 哈希值来唯一标识,如下图所示。每个 commit 对象中,对于 tree 里面那些没有改动的内容,继续通过原 hash 引用。

git 对象以及关系示意图

分支

A branch is just a named reference to a commit.

在上面的图片中,可以通过哈希值来引用一个 commit,但是不方便,所以分支用来引用 commit。可以理解为分支是一个指针,指向一个 commit,一般默认是指向最后一个 commit(也可以不是最后一个 commit)。

git 通过HEAD指针来确认当前所在分支。HEAD指针其实是.git目录下的一个HEAD文件,内容如下

> cat .git/HEAD
ref: refs/heads/master

git 如何记录变化

  1. repository 是一系列 commit 的集合
  2. working dir 是一个包含.git的目录
  3. staging area 是存放那些被 git 跟踪但是没有 commit 的内容

三者的关系如下图所示

git-repo-workingdir

git 底层命令 (plumbing) 和上层命令 (porcelain)

区分 底层(plumbing)和 上层(porcelain)两类 git 命令会对你很有帮助。这两个术语的应用奇怪地来自于马桶(没错,就是🚽)。马桶通常是用陶瓷(porcelain)做的,它的基本结构是管道(plumbing,上水道和下水道)。
上层命令就是git init、git add、 git commit等,下面介绍一下底层命令。

# 创建git对象
>echo "git is awesome" | git hash-object --stdin -w
# 查看.git目录的变化
>tree .git
# 查看一个git object类型 -t type
>git cat-file -t [obj-hash]
# blob|tree|commit

# 查看一个git object内容 -p pretty-print
>git cat-file -p [obj-hash]

# 添加object到staging area
>git update-index --add --cacheinfo 100644 <blob-hash> <filename>

# 创建一个tree对象 在tree对象中记录index内容
>git write-tree

# 为tree对象创建一个commit对象
>git commit-tree <tree-hash> -m <commit message>

git 实际上是使用 SHA-1 哈希值的前两个字符作为目录的名字,剩余字符用作 blob 所在文件的文件名。

.git 目录

一个.git目录至少包含三个内容

✔ tree .git
.git
├── HEAD        当前指向分支,默认内容是 ref: refs/heads/main
├── objects     git对象 blob、tree、commit的一种,其中对象的hash值前两个字符用于目录名,剩余的用于对象名
└── refs        分支和tag
    └── heads   当前working dir所有分支 默认分支不展示,只有多于一个分支才会展示 

添加一个文件并 commit,然后创建一个新分支,再次检查.git 目录

✔ tree .git
.git
├── HEAD
├── index
├── objects
│        ├── 8d
│        │   └── 0e41234f24b6da002d962a26c2495ea16a425f
│        ├── af
│        │   └── 7e0d93b83f49f601f5ef35edf5f9330fb4d7fd
│        └── c8
│            └── bcfef1da123a980537a5fa4cf9b7c4f387d451
└── refs
    └── heads
        ├── main
        └── test_branch

7 directories, 7 files

上面删除了 logs 目录,index 文件保存的是 staging area 信息。打印 objects 目录下的三个文件

✔ git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f
hello git

 :~/code/temp (main)
✔ git cat-file -p c8bcfef1da123a980537a5fa4cf9b7c4f387d451
100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f	file.txt

 :~/code/temp (main)
✔ git cat-file -p af7e0d93b83f49f601f5ef35edf5f9330fb4d7fd
tree c8bcfef1da123a980537a5fa4cf9b7c4f387d451
author zhimoe <[email protected]> 1691313901 +0800
committer zhimoe <[email protected]> 1691313901 +0800

first commit

可以看到分别是一个 blob 对象(file.txt)、一个 tree 对象和一个 commit 对象,后者依次引用前者。

参考

文章里面提到了很多 git 内部原理和概念:
Git 内部原理 - 底层命令与上层命令
Git 内部原理 - Git 对象