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文件存储暂存区信息。
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类型是不够的。
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 |
这是一种对象类型,用来记录一个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 |
可以看到一个链上的提交记录。
Git把对象的头和对象的内容拼接起来,以zlib压缩,存储为一个文件。文件的名称为压缩前内容的SHA1。
对象头的格式为:“类型 长度\0",如: "blob 16\0"
执行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的组织逻辑如下:
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 |
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种引用是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用它们来记录最后一次提交的内容。
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 |
远程分支和本地分支有个映射关系。默认是同名映射。
当添加一个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*通配是非法的。
顺序相反,将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 |
要删除远端的引用,只需要:
1 | $ git push origin :topic |
本地的不填即可。
参考资料
Pro Git, GIt Internals Chapter