Git命令的背后

git init

使用git init初始化一个新的目录时,会生成一个.git的目录,该目录即为本地仓库。一个新初始化的本地仓库是这样的:

├── HEAD
├── branches
├── config
├── description
├── hooks
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags
  • description 用于GitWeb程序
  • config 配置特定于该仓库的设置(还记得git config的三个配置级别么)
  • hooks 放置客户端或服务端的hook脚本
  • HEAD 传说中的HEAD指针,指明当前处于哪个分支
  • objects Git对象存储目录
  • refs Git引用存储目录
  • branches 放置分支引用的目录

其中descriptionconfighooks这些不在讨论中,后文会直接忽略。

git add

Gitcommit之前先要通过git add添加文件,这个操作Git内部会做些什么呢?

执行如下操作:

  • echo "Hello Git" > a.txt生成一个a.txt
  • 再通过git add a.txt添加文件
  • 查看.git目录
├── HEAD
├── branches
├── index
├── objects
│   ├── 9f
│   │   └── 4d96d5b00d98959ea9960f069585ce42b1349a
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

可以看到,多了一个index文件。并且在objects目录下多了一个9f的目录,其中多了一个4d96d5b00d98959ea9960f069585ce42b1349a文件。

其实9f4d96d5b00d98959ea9960f069585ce42b1349a就是一个Git对象,称为blob对象

这个文件名(或者叫对象名)是怎样来的呢?简单的说,就是Git会先生成一个文件头,其中包含这个对象的类型(比如blob)和原始文件长度加上一个空字节。文件头再加上原始文件内容,然后算出一个SHA-1。这个SHA-1有40位,前两位会用于新建目录,后38位用于文件名。所以,完整的对象名应该把上一级目录名给包含进去的。

可以通过Git的底层命令git cat-file -p查看其内容:

$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git

可以看到,其中的内容和a.txt文件是一模一样的。

通过git cat-file -t查看对象的类型:

$ git cat-file -t 9f4d96d5b00d98959ea9960f069585ce42b1349a
blob

确实是blob类型。那index文件又是什么鬼?

通过git add的文件会先放到Staging Area(有些书也叫Cached Area)。而index文件就是这个Staging Areaindex本身是一个二进制文件,有自己专有的存储格式,详情可见

我们可以通过git ls-files --stage查看index文件的内容:

$ git ls-files --stage
100644 9f4d96d5b00d98959ea9960f069585ce42b1349a 0   a.txt

小结:git add命令会将我们的文件保存成一个blob对象,然后更新index文件表明该文件已经暂存。

git commit

通过git commit -m "first commit"提交,然后再查看.git目录:

├── HEAD
├── branches
├── index
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 88
│   │   └── 23efd7fa394844ef4af3c649823fa4aedefec5
│   ├── 91
│   │   └── 0fc16f5cc5a91e6712c33aed4aad2cfffccb73
│   ├── 9f
│   │   └── 4d96d5b00d98959ea9960f069585ce42b1349a
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags
  • objects目录下多了两个对象8823efd7fa394844ef4af3c649823fa4aedefec5910fc16f5cc5a91e6712c33aed4aad2cfffccb73
  • 多了一个log目录,里边存了好些东西,先不管它~~

.git/refs/heads下多了一个master文件,可以直接查看:

$ cat .git/refs/heads/master 
910fc16f5cc5a91e6712c33aed4aad2cfffccb73

该文件是一个文本文件,里边保存着一个对象的名称。从上文可以看到,该对象是新增加的。查看一下它的类型和内容:

$ git cat-file -t 910fc16f5cc5a91e6712c33aed4aad2cfffccb73   
commit

$ git cat-file -p 910fc16f5cc5a91e6712c33aed4aad2cfffccb73
tree 8823efd7fa394844ef4af3c649823fa4aedefec5
author yjiyjige <475500230@qq.com> 1472309876 +0800
committer yjiyjige <475500230@qq.com> 1472309876 +0800

first commit

可以看到该对象的类型是commit,而它的内容包含了另外的一个对象的引用(tree对象),还有就是作者信息、提交者信息和提交的日志。

现在来看看8823efd7fa394844ef4af3c649823fa4aedefec5这个对象:

git cat-file -t 8823efd7fa394844ef4af3c649823fa4aedefec5
tree

$ git cat-file -p 8823efd7fa394844ef4af3c649823fa4aedefec5
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a    a.txt

该对象类型是tree,而该对象指向了我们一开始add生成的那个blob对象,并且保存着文件名。

接下来执行如下操作:

$ mkdir temp

$ echo "Second file" > temp/b.txt

$ git add temp/b.txt 

$ git commit -m "second commit"

然后看下.git目录的变化:

├── HEAD
├── branches
├── index
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 20
│   │   └── d5b672a347112783818b3fc8cc7cd66ade3008
│   ├── 80
│   │   └── 0910d78c39017816173b00d3a1074800854612
│   ├── 88
│   │   └── 23efd7fa394844ef4af3c649823fa4aedefec5
│   ├── 8e
│   │   └── 19c6677af0c3a80d5e2a3d1c1dffe9934431a5
│   ├── 91
│   │   └── 0fc16f5cc5a91e6712c33aed4aad2cfffccb73
│   ├── 9f
│   │   └── 4d96d5b00d98959ea9960f069585ce42b1349a
│   ├── e8
│   │   └── b5b9a992fe8b5d24b09ef55b97739f35221b1d
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

可以看到多了4个对象,但我们先看下.git/refs/heads/master文件:

$ cat .git/refs/heads/master 
800910d78c39017816173b00d3a1074800854612

可以看到引用了一个新的对象,再看看这个对象是什么:

$ git cat-file -t 800910d78c39017816173b00d3a1074800854612
commit

$ git cat-file -p 800910d78c39017816173b00d3a1074800854612
tree 8e19c6677af0c3a80d5e2a3d1c1dffe9934431a5
parent 910fc16f5cc5a91e6712c33aed4aad2cfffccb73
author yjiyjige <475500230@qq.com> 1472311564 +0800
committer yjiyjige <475500230@qq.com> 1472311564 +0800

second commit

还是一个commit对象,该对象又引用了一个新的tree对象,而且有一个parent后面跟着的是我们上次提交的commit对象。看看所引用的tree对象是怎样的:

$ git cat-file -p 8e19c6677af0c3a80d5e2a3d1c1dffe9934431a5
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a    a.txt
040000 tree e8b5b9a992fe8b5d24b09ef55b97739f35221b1d    temp

tree对象包含了第一次add的生成的blob对象(对应于a.txt文件)和另一个tree对象。几乎可以想到,这个tree对象中应该包含了一个blob对象,对应于b.txt文件:

$ git cat-file -p e8b5b9a992fe8b5d24b09ef55b97739f35221b1d
100644 blob 20d5b672a347112783818b3fc8cc7cd66ade3008    b.txt

如果我们把这些对象的引用关系,包括master文件用图画出来,大概是这个样子:

小结:

  • tree对象相当于一个目录(或者叫文件夹),其中包含blob对象和其他tree对象。
  • 每一次提交都会有一个commit对象,commit对象中会有一个tree对象和一个指和上一次提交的引用。
  • master分支其实就是一个引用而已,指向某一个提交对象。

Q&A

怎么理解每次提交都是一个“快照”

从上文中我们可能看到,每一个commit对象所引用的tree对象最终可以递归得出提交时的所有的文件,并不是说会把所有的文件都重新备份一次。而Git在add文件时,确实会把文件完整地保存成一个新的blob对象,我们可以验证:

$ echo "Third" > a.txt

$ git add a.txt

$ git commit -m "third commit"

会多几个对象呢?

├── HEAD
├── branches
├── index
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 16
│   │   └── df5eafaccb32649a890005b3f693fed266fc3d
│   ├── 20
│   │   └── d5b672a347112783818b3fc8cc7cd66ade3008
│   ├── 56
│   │   └── 9f012efac9a65ee515e488e244b89cbe795d6e
│   ├── 80
│   │   └── 0910d78c39017816173b00d3a1074800854612
│   ├── 88
│   │   └── 23efd7fa394844ef4af3c649823fa4aedefec5
│   ├── 8e
│   │   └── 19c6677af0c3a80d5e2a3d1c1dffe9934431a5
│   ├── 91
│   │   └── 0fc16f5cc5a91e6712c33aed4aad2cfffccb73
│   ├── 9f
│   │   ├── 4d96d5b00d98959ea9960f069585ce42b1349a
│   │   └── 7da334be98d63c78ccf1e94414b0664e649e5f
│   ├── e8
│   │   └── b5b9a992fe8b5d24b09ef55b97739f35221b1d
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

多了三个对象,直接通过master一步步看:

$ cat .git/refs/heads/master
569f012efac9a65ee515e488e244b89cbe795d6e

$ git cat-file -p 569f012efac9a65ee515e488e244b89cbe795d6e
tree 9f7da334be98d63c78ccf1e94414b0664e649e5f # 新的tree对象
parent 800910d78c39017816173b00d3a1074800854612
author yjiyjige <475500230@qq.com> 1472317420 +0800
committer yjiyjige <475500230@qq.com> 1472317420 +0800

third commit

$ git cat-file -p 9f7da334be98d63c78ccf1e94414b0664e649e5f
100644 blob 16df5eafaccb32649a890005b3f693fed266fc3d    a.txt # 文件名一样,但blob对象已经不一样了
040000 tree e8b5b9a992fe8b5d24b09ef55b97739f35221b1d    temp # 和上次的tree对象是一样的

$ git cat-file -p 16df5eafaccb32649a890005b3f693fed266fc3d
Third

$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git
# 可以看到老blob对象还在

可以发现,新生成一个tree对象,指向了一个新的blob对象(还是对应于a.txt)只不过内容变了。原来的temp目录对应的tree对象没有变化,所以直接引用。

等等,如果每次修改都保存一个完整的文件,那仓库不是很快就变得巨大?

理论上来说,每次修改只需要保存这个文件diff就行了,但那样就实现不了Git这么优雅的设计了。Git是通过“打包”来实现的。我们调用git gc,然后看下仓库的文件:

├── HEAD
├── branches
├── index
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── info
│   │   └── packs
│   └── pack
│       ├── pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.idx
│       └── pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.pack
├── packed-refs
└── refs
    ├── heads
    └── tags

WTF!!!所有对象都不见了!甚至master都不见了!

莫方,我们看看packed-refs是什么:

$ cat packed-refs 
# pack-refs with: peeled fully-peeled 
569f012efac9a65ee515e488e244b89cbe795d6e refs/heads/master

看来至少master还是在的。再通过git verify-pack -v看看.idx文件是什么东西:

$ git verify-pack -v objects/pack/pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.idx
569f012efac9a65ee515e488e244b89cbe795d6e commit 215 147 12
800910d78c39017816173b00d3a1074800854612 commit 216 148 159
910fc16f5cc5a91e6712c33aed4aad2cfffccb73 commit 167 117 307
16df5eafaccb32649a890005b3f693fed266fc3d blob   6 15 424
20d5b672a347112783818b3fc8cc7cd66ade3008 blob   12 21 439
9f7da334be98d63c78ccf1e94414b0664e649e5f tree   64 75 460
e8b5b9a992fe8b5d24b09ef55b97739f35221b1d tree   33 44 535
8e19c6677af0c3a80d5e2a3d1c1dffe9934431a5 tree   64 75 579
9f4d96d5b00d98959ea9960f069585ce42b1349a blob   10 19 654
8823efd7fa394844ef4af3c649823fa4aedefec5 tree   33 44 673
non delta: 10 objects
objects/pack/pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.pack: ok

原来.idx文件记录了之前的所有对象,而现在的数据保存在了.pack文件中。通过.idx文件记录的起始值、文件长度这些信息就可以把原有的对象提取出来了。如果文件相似,其实是会保留新版本,而老版本保留diff的形式存在!

回到“快照”这个概念,Git在底层做了脏活,只要通过当时提交的文件对应的blob对象引用,就可以还原出原始文件。所以,从用户角度,blob文件相当于原始文件

$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git

这部分不好理解,甚至很多书都会直接说“Git保留文件快照,而其他VCS是保存diff”。其实Git底层也会保存diff的,只不过我们感觉不到diff的存在而已。

关于打包这部分,详细请见Pro git

未完,可能会续~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容

  • 本片内容转自CSDN http://blog.csdn.net/ithomer/article/details/7...
    五娃儿阅读 4,837评论 2 88
  • 职业选择 批评家和导演不是一回事,绝大多数批评家都拍不出电影,也不能设计出美观的界面或者好用的产品。 分析能力有两...
    Savian阅读 233评论 0 0
  • M最近陷于关于人到中年的苦闷中,今天的他跟我分享的了他的瓶颈。在公司这样的单位工作,压力不算大,待遇还可以,别人眼...
    默默然然阅读 243评论 1 0
  • 走在沙漠里,看不到希望和路,很容易迷茫。 走在岔路口,前头有太多路,也容易迷茫。 走在岔路口,前头的路不多,但是向...
    Walter所罗猫阅读 548评论 0 5