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
放置分支引用的目录
其中description
、config
和hooks
这些不在讨论中,后文会直接忽略。
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 Area
。index
本身是一个二进制文件,有自己专有的存储格式,详情可见。
我们可以通过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
目录下多了两个对象8823efd7fa394844ef4af3c649823fa4aedefec5
和910fc16f5cc5a91e6712c33aed4aad2cfffccb73
- 多了一个
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 <[email protected]> 1472309876 +0800
committer yjiyjige <[email protected]> 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 <[email protected]> 1472311564 +0800
committer yjiyjige <[email protected]> 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 <[email protected]> 1472317420 +0800
committer yjiyjige <[email protected]> 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。
未完,可能会续~