ILD

git internals
作者:Herbert Yuan 邮箱:yuanjp@hust.edu.cn
发布时间:2018-4-27 站点:Inside Linux Development

1 Plumbing and Porcelain

plumbing commands是那些更底层的命令。porcelain commands是那些用户更友好的命令。


在一个新目录执行git init时,Git创建.git目录,几乎所有的信息都包含在.git目录中。

1
2
3
4
5
6
7
8
9
10
11
12
$ git init
Initialized empty Git repository in /work/git/gitlearn/learn1/.git/
 
$ ls .git/ -1 -F
branches/
config
description
HEAD
hooks/
info/
objects/
refs/


更新版本的Git不使用branches目录,description仅被GitWeb程序使用。config包含本项目相关的配置选项。info目录包含全局要排除的pattern,这样不需要记录在.gitignore文件中。hooks目录包含客户端或服务端的hook脚本。


这四个条目是git的核心:HEAD, index, objects/, refs/


objects目录存储数据库的所有内容。refs目录存储commit objects的指针。HEAD文件存储当前check out的branch。index文件存储暂存区信息。


2 Git objects

Git是一个content-addressable filesystem。这意味着Git是一个简单的key-value的data store。plumbing命令hash-object将一些数据存储到.git目录,然后给出数据存储的key。


首先,可以看到objects目录下没有任何东西:

1
2
3
4
5
$ find .git/objects/
.git/objects/
.git/objects/pack
.git/objects/info
$ find .git/objects/ -type f


git初始化了objects目录,并创建了pack和info子目录,但是没有普通文件,现在存储一些文本到Git database。

1
2
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4


-w 选项, 将目标写入到object database。

--stdin 选项,从标准输入而不是文件读取object。


输出是一个40个字符的checksum hash,这是SHA-1哈希,存储内容加一个头的checksum。


然后,可以看到Git存储了一些信息:

1
2
$ find .git/objects/ -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4


在objects目录看到了一个文件。Git一个内容片段存储为一个文件,名字是内容和它的头的SHA-1校验码。子目录的SHA的前2个字符,文件名是剩下的38个字符。


可以使用cat-file命令,参看对象的类型、大小、内容等。这个命令是窥探Git目标的瑞士军刀。

1
2
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content


-t 选项,显示object type

-s 选项,显示object size

-p 选项,打印目标的内容


现在,我们可以添加内容,并把它拉回来,添加同一个文件的两个版本:

1
2
3
4
5
6
7
$ 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目录中,有3个文件:

1
2
3
4
$ find .git/objects/ -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30


现在,把第一个版本拉回来:

1
2
3
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1


文件名并没有存储到系统中,只存储了它的内容,这个目标类型是blob,使用cat-file -t可以查看一个任何目标的类型:

1
2
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob


显然,只有blob类型是不够的。


2.1 Tree Objects

tree object解决了存储文件名的问题,并且允许将一组文件存储为一个group。Git存储内容和Unix文件系统类型,但是更简单。所有的内容存储为tree或者blob。tree对应Unix的目录项,blob或多或少对应了inodes或文件文件内容。


一个tree object包含一个或多个条目,每个条目包含一个SHA-1指针,指向一个blob或者一个subtree。同时包含相应的mode, type, filename。


初始化一个git目录,添加几个文件并commit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ mkdir learn2
$ cd learn2
$
$ git init
Initialized empty Git repository in /work/git/gitlearn/learn2/.git/
$
$ touch README Rakefile
$ mkdir lib/
$ touch lib/simplegit.rb
$
$ git add .
$ git commit -m 'test'
[master (root-commit) 8468ed7] test
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README
 create mode 100644 Rakefile
 create mode 100644 lib/simplegit.rb


然后,查看相关的tree object

1
2
3
4
5
6
7
$ git cat-file -p master^{tree}
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391  README
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391  Rakefile
040000 tree 8ede28eed8c7a5113ba5a8aa9704fa6017a996c7  lib
$ git cat-file -p 8ede28eed8c7a5113ba5a8aa9704fa6017a996c7
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391  simplegit.rb


master^{tree}指定了master分之最后一个commit的tree object。可以看到tree object的组织和文件目录的组织是一样的。概念上,Git存储的数据如下所示:


你可以创建自己的树。Git通常通过暂存区的状态或者索引来创建树。所以,为了创建一个树,首先需要通过暂存一些文件,来设置一个index。 


首先,初始化仓库,并添加一个blob:

1
2
3
4
5
6
7
8
$ mkdir learn3
$ cd learn3
$ git init
Initialized empty Git repository in /work/git/gitlearn/learn3/.git/
$
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4


然后,使用update-index命令来添加index条目:

1
$ git update-index --add --cacheinfo 100644 d670460b4b4aece5915caf5c68d12f560a9fe3e4 test.txt


--add 选项,表示添加新的条目。

--cacheinfo 选项,表示这是数据库中的object。


最后,使用write-tree命令,从index创建tree object

1
2
3
4
5
$ git write-tree
80865964295ae2f11d27383e5f9c0b58a8ef21da
$ git cat-file -p 80865964295ae2f11d27383e5f9c0b58a8ef21da
100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4  test.txt


也可以通过文件,创建树。创建一个test.txt和new.txt,然后更新index

1
2
3
4
$ echo a > test.txt
$ echo b > new.txt
$ git update-index test.txt
$ git update-index --add new.txt


创建树:

1
2
3
4
5
6
$ git write-tree
d78d1044e36bc72f9e1fe142ca6d9a499c9b8fd9
$
$ git cat-file -p d78d1044e36bc72f9e1fe142ca6d9a499c9b8fd9
100644 blob 61780798228d17af2d34fce4cfbdf35556832472  new.txt
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85  test.txt


此时,test.txt已经更新为新的blob

1
2
3
4
$ git cat-file -p 78981922613b2afb6025042ff6bd878ac1994e85
a
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content


可以使用read-tree,将一个tree object读到暂存区,这样该tree变成一个subtree。

1
2
3
4
5
6
7
8
$ git read-tree --prefix=bak d78d1044e36bc72f9e1fe142ca6d9a499c9b8fd9
$ git write-tree
c8a6e3dd3ffb884221f1bbc1eca60448a45b2c9c
$
$ git cat-file -p c8a6e3dd3ffb884221f1bbc1eca60448a45b2c9c
040000 tree d78d1044e36bc72f9e1fe142ca6d9a499c9b8fd9  bak
100644 blob 61780798228d17af2d34fce4cfbdf35556832472  new.txt
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85  test.txt


2.2 Commit Objects

这是一种对象类型,用来记录一个tree,给它打上作者,日志等信息。使用commit-tree创建一个commit object。


创建一个commit:

1
2
$ echo 'first commit' | git commit-tree c8a6e3dd3ffb884221f1bbc1eca60448a45b2c9c
cdd3f811edb3e11947219ad93408f32d2a701dd3


查看其类型和内容:

1
2
3
4
5
6
7
8
9
$ git cat-file -t cdd3f811edb3e11947219ad93408f32d2a701dd3
commit
$ git cat-file -p cdd3f811edb3e11947219ad93408f32d2a701dd3
tree c8a6e3dd3ffb884221f1bbc1eca60448a45b2c9c
author Herbert Yuan <yuanjp@hust.edu.cn> 1524664483 +0800
committer Herbert Yuan <yuanjp@hust.edu.cn> 1524664483 +0800
 
first commit


commit的格式很简单,它指定了tree,这个tree是项目某点的一个快照。以及一个可选的parent commit。作者,当前时间戳,以及日志等。


不同的commit,是通过parent组成一个commit链的。


添加一个commit,指定其parent为上一次的commit:

1
2
$ echo 'second commit' | git commit-tree d78d10 -p cdd3f8
ce4805cbf2579ec317c548a2383c66b99f11668a


使用log命令,查看commit记录:

1
2
3
4
5
6
7
8
9
10
11
12
$ git log ce4805
commit ce4805cbf2579ec317c548a2383c66b99f11668a
Author: Herbert Yuan <yuanjp@hust.edu.cn>
Date:   Wed Apr 25 23:07:24 2018 +0800
 
    second commit
 
commit cdd3f811edb3e11947219ad93408f32d2a701dd3
Author: Herbert Yuan <yuanjp@hust.edu.cn>
Date:   Wed Apr 25 21:54:43 2018 +0800
 
    first commit


可以看到一个链上的提交记录。


2.3 Object Storage

Git把对象的头和对象的内容拼接起来,以zlib压缩,存储为一个文件。文件的名称为压缩前内容的SHA1。


对象头的格式为:“类型 长度\0",如: "blob 16\0"


3 Git References

执行git log ce4805,需要指定commit的SHA1值,这显然很不方便,我们可以使用一个简单的名字来指向对应的commit。


在Git中,这叫做references或者refs。


在上述项目中,还没有任何引用:

1
2
3
4
5
6
7
$ find .git/refs
.git/refs
.git/refs/tags
.git/refs/heads
$ find .git/refs -type f
$


为了创建一个引用,可以在heads中创建一个文件,文件名即是应用名,文件的内容是对应的commit的SHA1

1
2
$ echo 'ce4805cbf2579ec317c548a2383c66b99f11668a' > .git/refs/heads/master
$


然后,我们就可以使用引用来查看log了:

1
2
3
$ git log --pretty=oneline master
ce4805cbf2579ec317c548a2383c66b99f11668a second commit
cdd3f811edb3e11947219ad93408f32d2a701dd3 first commit


不鼓励直接编辑引用文件,使用Git提供的update-ref命令:

1
$ git update-ref refs/heads/test ce4805cbf2579ec317c548a2383c66b99f11668a


这就是branch怎么工作的。


所以,整个Git的组织逻辑如下:


3.1 The HEAD

refs/heads/ 目录中有可能有多个头,也即有多个分支,.git/HEAD文件,表明当前使用那个头,也即那个分支。

1
2
$ cat .git/HEAD 
ref: refs/heads/master


当使用 git checkout test,Git执行切换分支操作,Git更新HEAD文件,当然Git也会更新工作区。

1
2
3
4
5
6
7
$ git checkout test
D   bak/new.txt
D   bak/test.txt
Switched to branch 'test'
$ cat .git/HEAD
ref: refs/heads/test


你可以手动编辑HEAD文件,但是有一个更安全的命令:symbolic-ref 来查看和设置HEAD

1
2
3
4
$ git symbolic-ref HEAD
refs/heads/test
$ git symbolic-ref HEAD refs/heads/master


3.2 Tags

Tags是第4种object type。它和commit object很像,包括一个tagger,一个日期,一个消息,一个指针。主要的不同是,指针是指向一个commit。


它像一个branch reference,但是tags从不移动。它总是指向相同的commit,但是给出一个更友好的名字。


有两种tags,lightweigh 和 annotated。通过下述命令创建一个lightweight tag

1
$ git update-ref refs/tags/v1.0 ce4805cb


一个annotated tag更复杂,创建annotated tag,Git将创建一个tag object,并创建一个引用,指向该tag。


使用git tag创建一个tag:

1
$ git tag -a v1.1 ce4805cb -m 'test tag'


它同时在refs/tags/目录下创建指向新创建tag的引用:

1
2
$ cat .git/refs/tags/v1.1
3e5478a7c44f9758dd725638ceff44ccb07fa248


新创建的tag是一个object

1
2
3
4
5
6
7
$ git cat-file -p 3e5478
object ce4805cbf2579ec317c548a2383c66b99f11668a
type commit
tag v1.1
tagger Herbert Yuan <yuanjp@hust.edu.cn> 1524753381 +0800
 
test tag


需要指出的是:annotated tag不需要指向commit,它可以指向任何Git object。


3.3 Remotes

第3种引用是remote reference。当你添加一个remote,并且push内容给它,Git将每个分之最后一次push到远端的值存储到refs/remotes目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git init learn4
Initialized empty Git repository in /work/git/gitlearn/learn4/.git/
$ cd learn4
$ git remote add origin https://github.com/yuanjianpeng/git_remote_learn.git
$ touch a 
$ git add a
$ git commit -m 'add a'
$ git push origin master
$ cat .git/refs/remotes/origin/master 
40e343b1f559c1896c4f8453608319ca374482af


Remote references和branches (refs/heads references) 不同之处主要是它们不能被check out。Git用它们来记录最后一次提交的内容。


4 Packfiles

Git为了节省磁盘空间,会将同一个文件的不同版本按pack方式存储。


使用git gc命令手动要求Git pack up objects。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ find .git/objects/ -type f
.git/objects/5d/59255cb96a44770cdebc07d6e2b63eeba55f91
.git/objects/5a/6710be7b46b61b2140ac33897bc0baf284c795
.git/objects/b9/8a5b601b8186e2a5435ea6525c600bef16f50f
.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
.git/objects/46/69239851fb55d8167292cec47b3ff49967285b
.git/objects/94/a9ed024d3859793618152ea559a168bbcbb5e2
.git/objects/87/5e65624d52dbc6f4404ecb4c67cfea8ec4842c
$
$ git gc
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), done.
Total 6 (delta 1), reused 0 (delta 0)
$ find .git/objects/ -type f
.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
.git/objects/pack/pack-4f388a936eafb203f39d34ca724e387681a580db.pack
.git/objects/pack/pack-4f388a936eafb203f39d34ca724e387681a580db.idx
.git/objects/info/packs


5 The Refspec

远程分支和本地分支有个映射关系。默认是同名映射。


当添加一个remote时,如3.3中,在.git/config中会添加一个remote配置项:

1
2
3
4
5
6
7
8
9
$ cat .git/config 
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    url = https://github.com/yuanjianpeng/git_remote_learn.git
    fetch = +refs/heads/*:refs/remotes/origin/*


refspec的格式是一个可选的+,跟着<src>:<dst>

<src>是远端的引用格式,<dst>是本地写入的引用格式。+告诉Git更新引用,即使不能fast-forward。


如上refspce,Git会获取服务器上refs/heads/的所有引用。把它们写到本地refs/remotes/origin/


访问log:

1
2
3
$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master

是完全一样的,前2者被扩展成第三者。


如果只想pull down master分支,修改fetch配置为:

1
fetch = +refs/heads/master:refs/remotes/origin/master


当然,也可以在命令行手动指定:

1
$ git fetch origin master:refs/remotes/origin/master


可以在.git/config中指定多个fetch,也可以在命令行指定多个。


refs/heads/qa/* 通配是合法的,而refs/heads/qa*通配是非法的。


5.1 Pushing Refspecs

顺序相反,将master分支推到远端refs/heads/qa/master

1
$ git push origin master:refs/heads/qa/master


也可以在配置文件中配置:

1
2
3
4
[remote "origin"]
    url = https://github.com/yuanjianpeng/git_remote_learn.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    push = refs/heads/master:refs/heads/qa/master


5.2 Deleting References

要删除远端的引用,只需要:

1
$ git push origin :topic


本地的不填即可。


参考资料

Pro Git, GIt Internals Chapter


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.