git init
使用git init初始化一個新的目錄時,會生成一個.git的目錄,該目錄即為本地倉庫。一個新初始化的本地倉庫是這樣的:
├── HEAD
├── branches
├── config
├── description
├── hooks
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
-
description用于GitWeb程序 -
config配置特定于該倉庫的設(shè)置(還記得git config的三個配置級別么) -
hooks放置客戶端或服務(wù)端的hook腳本 -
HEAD傳說中的HEAD指針,指明當(dāng)前處于哪個分支 -
objectsGit對象存儲目錄 -
refsGit引用存儲目錄 -
branches放置分支引用的目錄
其中description、config和hooks這些不在討論中,后文會直接忽略。
git add
Gitcommit之前先要通過git add添加文件,這個操作Git內(nèi)部會做些什么呢?
執(zhí)行如下操作:
- 用
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)和原始文件長度加上一個空字節(jié)。文件頭再加上原始文件內(nèi)容,然后算出一個SHA-1。這個SHA-1有40位,前兩位會用于新建目錄,后38位用于文件名。所以,完整的對象名應(yīng)該把上一級目錄名給包含進去的。
可以通過Git的底層命令git cat-file -p查看其內(nèi)容:
$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git
可以看到,其中的內(nèi)容和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文件的內(nèi)容:
$ git ls-files --stage
100644 9f4d96d5b00d98959ea9960f069585ce42b1349a 0 a.txt
小結(jié):git add命令會將我們的文件保存成一個blob對象,然后更新index文件表明該文件已經(jīng)暫存。
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
該文件是一個文本文件,里邊保存著一個對象的名稱。從上文可以看到,該對象是新增加的。查看一下它的類型和內(nèi)容:
$ 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,而它的內(nèi)容包含了另外的一個對象的引用(tree對象),還有就是作者信息、提交者信息和提交的日志。
現(xiàn)在來看看8823efd7fa394844ef4af3c649823fa4aedefec5這個對象:
git cat-file -t 8823efd7fa394844ef4af3c649823fa4aedefec5
tree
$ git cat-file -p 8823efd7fa394844ef4af3c649823fa4aedefec5
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a a.txt
該對象類型是tree,而該對象指向了我們一開始add生成的那個blob對象,并且保存著文件名。
接下來執(zhí)行如下操作:
$ 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對象??纯此玫?code>tree對象是怎樣的:
$ git cat-file -p 8e19c6677af0c3a80d5e2a3d1c1dffe9934431a5
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a a.txt
040000 tree e8b5b9a992fe8b5d24b09ef55b97739f35221b1d temp
該tree對象包含了第一次add的生成的blob對象(對應(yīng)于a.txt文件)和另一個tree對象。幾乎可以想到,這個tree對象中應(yīng)該包含了一個blob對象,對應(yīng)于b.txt文件:
$ git cat-file -p e8b5b9a992fe8b5d24b09ef55b97739f35221b1d
100644 blob 20d5b672a347112783818b3fc8cc7cd66ade3008 b.txt
如果我們把這些對象的引用關(guān)系,包括master文件用圖畫出來,大概是這個樣子:

小結(jié):
-
tree對象相當(dāng)于一個目錄(或者叫文件夾),其中包含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對象已經(jīng)不一樣了
040000 tree e8b5b9a992fe8b5d24b09ef55b97739f35221b1d temp # 和上次的tree對象是一樣的
$ git cat-file -p 16df5eafaccb32649a890005b3f693fed266fc3d
Third
$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git
# 可以看到老blob對象還在
可以發(fā)現(xiàn),新生成一個tree對象,指向了一個新的blob對象(還是對應(yīng)于a.txt)只不過內(nèi)容變了。原來的temp目錄對應(yīng)的tree對象沒有變化,所以直接引用。
等等,如果每次修改都保存一個完整的文件,那倉庫不是很快就變得巨大?
理論上來說,每次修改只需要保存這個文件diff就行了,但那樣就實現(xiàn)不了Git這么優(yōu)雅的設(shè)計了。Git是通過“打包”來實現(xiàn)的。我們調(diào)用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文件記錄了之前的所有對象,而現(xiàn)在的數(shù)據(jù)保存在了.pack文件中。通過.idx文件記錄的起始值、文件長度這些信息就可以把原有的對象提取出來了。如果文件相似,其實是會保留新版本,而老版本保留diff的形式存在!
回到“快照”這個概念,Git在底層做了臟活,只要通過當(dāng)時提交的文件對應(yīng)的blob對象引用,就可以還原出原始文件。所以,從用戶角度,blob文件相當(dāng)于原始文件。
$ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
Hello Git
這部分不好理解,甚至很多書都會直接說“Git保留文件快照,而其他VCS是保存diff”。其實Git底層也會保存diff的,只不過我們感覺不到diff的存在而已。
關(guān)于打包這部分,詳細請見Pro git。
未完,可能會續(xù)~