Git内部原理
[ Git ]

1 低层命令与上层命令

由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统, 所以它还包含了一部分用于完成底层工作的子命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或藉由脚本调用,来完成工作。 这部分命令一般被称作“底层(plumbing)”命令。而那些更友好的命令,如checkout、branch、remote 等约 30 个 Git 的子命令则被称作“上层(porcelain)”命令。

2 .git 目录的典型结构

新初始化的 .git 目录的典型结构如下:

config			### config 文件包含项目特有的配置选项
description		### description 文件仅供 GitWeb 程序使用
HEAD			### HEAD 文件指向目前被检出的分支
hooks/			### hooks 目录包含客户端或服务端的钩子脚本(hook scripts)
info/			### info 目录包含一个全局性排除(global exclude)文件 ,用以放置那些不希望被记录在
 			    .gitignore文件中的忽略模式(ignored patterns)
objects/		### objects 目录存储所有数据内容
refs/			### refs 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针

3 Git对象

Git 是一个内容寻址文件系统。 这意味着,Git 的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的键通过该键可以在任意时刻再次取回该内容

Git具体如何存储对象见《ProGit》中 Git内部原理 章节的 Git对象小节。

类型及作用

Git对象类型 作用
数据对象(blob object) 保存文件的内容
树对象(tree object) 保存目录名和文件名
提交对象(commit object) 保存项目快照、父提交、作者/提交者信息和提交注释
标签对象(tag object) 保存标签创建者信息、日期、注释,以及指针

一个例子

每次我们运行 git add 和 git commit 命令时,Git 所做的工作实质就是将被改写的文件保存为数据对象, 更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。

在test0目录下新建test.txt,然后git add,git commit 在test1目录下新建test.txt,将test.txt保存为blob,更新缓存区,记录树对象,创建提交对象,创建引用
$ cd ~ && mkdir test0 && cd test0 $ cd ~ && mkdir test1 && cd test1
$ git init $ git init
===== 在test.txt中写入test.txt ==== =============================
$ echo ‘version 1’ > test.txt $ echo ‘version 1’ > test.txt
===== 把test.txt添加到暂存区 ==== =============================
$ git add test.txt $ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
  $ git cat-file -p 83baae
version 1
  $ git update-index –add –cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
===== 提交 ==== =============================
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
// 当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段  
$ git commit -m ‘first commit’
[master (root-commit) 352b30e] first commit
1 file changed, 1 insertion(+)
create mode 100644 test.txt
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ find .git/objects -type f
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
.git/objects/35/2b30e1f0d5b47bad3bd5999ba2c59b4acb2c03
$ git cat-file -p d8329f
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
  $ echo ‘first commit’ | git commit-tree d8329f
baced8777d850523523cc5ed1528273ccd7744c4
  $ find .git/objects -type f
.git/objects/ba/ced8777d850523523cc5ed1528273ccd7744c4
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
  $ git update-ref refs/heads/master baced8777d850523523cc5ed1528273ccd7744c4
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/heads/master
.git/refs/tags
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/heads/master
.git/refs/tags

相关命令

数据对象

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30 
# 根据文件test.txt创建一个新的数据对象,并返回指向该数据对象的唯一的键(即 对应的 SHA-1 值),-w 选项的作用是
  将该对象写入.git/objects 目录(即 对象数据库)中。

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 # 输出该 SHA-1 值所对应的对象的内容

version 1

$ git cat-file -s 83baae61804e65cc73a7201a7252750c76066a30 # 查看该对象有多大

// 该SHA-1值(长度为40个字符)是一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。

树对象

通常,Git 根据某一时刻暂存区(即 index 区域)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象。 因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。

$ git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt   
# 将.git/objects 目录(即 对象数据库)中的test.txt文件加入到一个新的暂存区,加--add选项是因为test.txt文件并不在
暂存区中,加--cacheinfo选项是因为将要添加的文件test.txt位于 Git 数据库(即 对象数据库)中。
83baae61804e65cc73a7201a7252750c76066a30是test.txt存储到Git数据库后所对应的 SHA-1 值。
# 本例中,我们指定的文件模式为 100644,表明这是一个普通文件。 其他选择包括:100755,
表示一个可执行文件;120000,表示一个符号链接。 这里的文件模式参考了常见的 UNIX 文件模式,
但远没那么灵活——上述三种模式即是 Git 文件(即数据对象)的所有合法模式(当然,还有其他一些模式,
但用于目录项和子模块)。

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt

提交对象

$ echo 'first commit' | git commit-tree d8329f
baced8777d850523523cc5ed1528273ccd7744c4
# 创建一个提交对象。d8328f是某个树对象的 SHA-1 值。有父提交的话还要在命令后面加“-p 父提交的SHA-1值”。

标签对象

标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用(永远指向同一个提交对象),只不过给这个提交对象加上一个更友好的名字罢了。注意,标签对象并不是只能指向某个提交对象,我们可以对任意类型的Git对象打标签。

标签有两种:附注标签和轻量标签。创建一个轻量标签:git update-ref refs/tags/v1.0 baced8777d850523523cc5ed1528273ccd7744c4。可以看到轻量标签其实就是一个固定的引用。

$ git tag -a v1.1 352b30e1f0d5b47bad3bd5999ba2c59b4acb2c03 -m 'test tag'
# 创建一个附注标签。352b30e1f0d5b47bad3bd5999ba2c59b4acb2c03为某个提交对象的 SHA-1 值。-a表明创建一个附注标签
# 若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,该标签对象再指向提交对象,而不是直接指向提交对象。

4 Git 引用

如果你对仓库中从一个提交(比如 baced8)开始往前的历史感兴趣,那么可以运行 git log baced8 这样的 命令来显示历史,不过你需要记得 baced8 是你查看历史的起点提交。 如果我们有一个文件保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针替代原始的 SHA-1 值的话会更加简单。

在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”。你可以在 .git/refs 目录下找到 这类含有 SHA-1 值的文件。

Git 分支的本质:一个指向某一系列提交之首的指针或引用。

类型 作用
HEAD 引用 指向目前所在的分支
标签引用 永远指向同一个对象
远程引用 记录远程服务器上各分支最后已知位置状态
################# 相关命令 ###############

$ git update-ref refs/heads/master baced8
# 创建一个引用,baced8为某个提交对象的 SHA-1 值。
# 使用git update-ref创建引用要比直接将 SHA-1 的值写入引用文件中更加安全。是因为引用日志(reflog)会通过git update-ref命令更新。通过git reflog可以查到我们提交或改变分支后git记录的一些信息。

$ git symbolic-ref HEAD refs/heads/test
# 设置 HEAD 引用的值

$ git symbolic-ref HEAD
# 查看 HEAD 引用对应的值

5 包文件

Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。在打包这些对象时,Git 只完整保存最新版的那个,再保存旧版本对象与最新版本的差异内容。因为大部分情况下需要快速访问文件的最新版本,所以只有最新版的对象保存完整的。当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。

$ git gc
# 让Git对对象进行打包

$ git verify-pack -v .git/objects/pack/pack-a7bb32334eb3fe7a3c5e9f8b1d6e8ad47cd9bfa5.idx
# 查看包文件的内容

6 数据恢复

在一些情况下我们可能会丢失一些提交,如使用git reset –hard硬重置仓库的某个分支到一个旧提交后,这时可使用以下方法来恢复丢失的提交:

1,找到丢失提交的SHA-1值。

2,创建一个新的分支指向这个丢失的提交。例如,创建一个名为recover的分支指向这个丢失的提交(123456):git branch recover 123456

参考资料:

[1] 《Pro Git》第10章 Git内部原理