在前篇中已經講了關於git的經常使用命令,這一篇咱們就更進一步來說講git的存儲原理,看看git葫蘆裏面究竟賣的什麼藥。git
當咱們使用git init
命令時,一般git會提示:Initialized empty Git repository in /xxxx/.git/
。能夠看到在初始化本地倉庫時,git新建了一個.git的文件夾(.git是一個隱藏的文件夾),而git全部記錄的版本控制的信息都藏在這個文件夾中。bash
先讓咱們看看.git中有什麼:數據結構
▶ ls .git
HEAD config description hooks/ index info/ objects/ refs/
複製代碼
其中,config
包含了項目的特有的配置信息;description
包含了項目的描述信息,僅供GitWeb程序使用;info/
文件夾中包含了一份執行文件,該文件用於指定不但願在.gitignore文件中管理的忽略模式;hooks
文件包含了各個git生命週期的鉤子。以上列出的這些文件大多都是一些配置文件。ide
git中最重要的四個文件是:HEAD
,index
,objects/
,refs/
。它們能夠稱之爲git的核心內容。其中:post
HEAD
:存儲HEAD指針當前的指向refs
:存儲全部branch,tag的信息index
:存儲暫存區的信息(在第一次git add
後纔會出現這個文件)objects
:存儲全部文件數據信息git本質上就是一套內容尋址文件系統,它使用簡單的key-value
形式進行數據的存儲和查找。其中的value指的是不只僅是某一個文件自己內容,還有git的一些附件信息;key這是git對value進行SHA-1後獲得的長度爲40的字符串(還記得以前的commit-id,沒錯,它就是如今所說的key)。優化
如今咱們新建一個空的倉庫,而且新建一個readme.txt
文件,可使用git hash-object <file-name>
命令查看該文件將會生成的key。ui
▶ git hash-object readme.txt
# 不信你數一數,真的是40位長
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
複製代碼
git使用這種機制的好處在於:對於相同的value,老是可以獲得相同的key。this
git內部包含了3個和存儲機制相關的對象(或者說是數據結構):blob
,tree
,commit
。idea
上圖顯示的是blob
,tree
以及commit
三個對象的結構。spa
blob
對象是用於儲存每一個文件的內容,因此它的內容就是文件的內容tree
對象用於存儲文件的結構以及文件名,因此它包含多個條目,每一個條目包含類型,key,名字等信息commit
對象用於存儲每一個提交的信息,tree是指本次提交的結構,parent是上一次提交的key,auther和commmitter兩個信息可能有點混淆,簡單解釋一下:auther是指最先提交的人,因爲git擁有修改歷史的功能,全部後續可能還要其餘的人對這個歷史做修改,因此committer是指本次修改該提交的人咱們如今經過一些實際操做來看看三者之間的關係,以及git的存儲機制。
在進行實際操做以前,咱們先來看看objects/
裏面的內容:
# 如今objects中什麼文件都沒有
▶ find .git/objects -type f
複製代碼
咱們先新建兩個文件:readme.txt
和.gitignore
而且提交,在來看看objects/
中的內容:
▶ git log --oneline
9dc03b3 (HEAD -> master) add readme.txt & .gitignore
# 一會兒多了這麼多內容
▶ find .git/objects -type f
.git/objects/9d/c03b327b7f7898d64a21a80d5ec7aea34930f5
.git/objects/9c/b6bc282361e81f326d93cc0007be1d5424d8a7
.git/objects/62/c893550adb53d3a8fc29a1584ff831cb829062
.git/objects/63/c3cc5127ce2c42f670e07c597428716a62cb7d
複製代碼
看到相似於9d/c03b327b7f7898d64a21a80d5ec7aea34930f5
這個文件,其實就是以40位的key命名的,key的前兩位做爲文件夾名,後38位做爲文件名,若是此時咱們直接打開這些文件看,只能看到一些亂碼,這是由於git對這些文件進行了壓縮儲存,以節約空間。
可使用一下命令查看這些文件的信息:
git cat-file -t <key>
:查看該文件類型git cat-file -p <key>
:查看該文件內容git cat-file -s <key>
:查看該文件大小# 咱們先看看上一次提交的信息
▶ git cat-file -t head
commit
▶ git cat-file -s head
188
▶ git cat-file -p head
tree 9cb6bc282361e81f326d93cc0007be1d5424d8a7
author wenjun <wjxu@thoughtworks.com> 1570514009 +0800
committer wenjun <wjxu@thoughtworks.com> 1570525472 +0800
add readme.txt & .gitignore
# 再看看上一次提交所指向的tree
▶ git cat-file -p 9cb6bc2
100644 blob 62c893550adb53d3a8fc29a1584ff831cb829062 .gitignore
100644 blob 63c3cc5127ce2c42f670e07c597428716a62cb7d readme.txt
# .gitignore內容
▶ git cat-file -p 62c893
.idea/%
# readme.txt內容
▶ git cat-file -p 63c3c
this is readme.txt%
複製代碼
因此它們之間的關係如圖所示:
因爲當前提交是一個提交,全部commit沒有parent信息。相信你也主要到了在author一欄中其實包含了user name,user email以及提交時間,這也是爲何每次生成的git commit id不同的緣由,由於即便其餘全部的信息都相同,可是隻要提交的時間不一樣,就會產生不一樣的key(還記得以前咱們所說的相同的value產生相同的key這個結論嗎)
若是此時咱們新建一個文件夾dir
,並在其中新建一個reamde.copy.txt
文件複製readme.txt
文件的內容。而且將這些改動提交。此時的結構將會變成:
readme.txt
以及
.gitignore
文件,可是這個最新的提交依舊包含了這些沒有改變的內容,這和廣泛的認識好像是有出入的。因此其實每個commit都包含了當前項目的全部文件,只是有些文件指向了以前的key,有些指向了新的key。當時無論怎麼樣,每個文件都是指向提交時最新的狀態。
仍是一個更新就是,明明咱們複製了一份新的readme.txt
文件。當時git中新的提交readme.copy.txt
文件內容依舊指向的是readme.txt
的blob。這就是以前提到了對於相同的value,老是生成相同的key。而對於相同的key,git只會保存一份,這是git的優化機制,用於減小儲存的開銷。
若是此時,咱們改動readme.copy.txt
文件內容並提交,此時的結構將會變成:
因爲readme.copy.txt
已經更新了,因此生成了一個新的blob。從這裏也能看出其實每個blob都是保存的文件的當前狀態的全部內容。
經過上面的內容,其實也能看出每個blob都是保存的所有的數據內容,雖然git自己會對內容進行壓縮以減小存儲體積。可是設想這樣一個場景:有個大文件,它一個有2M,咱們將它提交到倉庫;後面須要在這個文件結尾加入一行數據,而後再提交,此時由於數據有改動,會保存一份新的blob,而這個blob有2M的內容和以前是如出一轍的。這其實就形成了不少的浪費。而git能夠統統過packfile只存儲一份完整的內容,而隨後的提交只須要保存二者之間的差別。
咱們以前所提的git默認的存儲方式使用的是鬆散對象格式。git會時不時的將這個文件打包到packfile中。當倉庫有太多鬆散對象,或者手動調用git gc
,或者push代碼時;git都會進行這樣的操做。
因爲packfile也是被壓縮的二進制文件,沒法直接查看,能夠經過git verify-pack
查看打包內容。
# 手動調用gc命令
▶ git gc
Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (20/20), done.
Total 20 (delta 2), reused 12 (delta 1)
# 此時的objects中多了pack,info文件夾
▶ find .git/objects -type f
.git/objects/pack/pack-891394b403ab48f1b2264486397661f55895a867.idx
.git/objects/pack/pack-891394b403ab48f1b2264486397661f55895a867.pack
.git/objects/info/packs
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
# 在此次提交中,readme.copy.txt文件加入大量數據,看看此時的大小
▶ git cat-file -s db53d
203632
# 只在以前的基礎上在末尾加入一行新數據
▶ git cat-file -s 2d0077
203642
▶ git verify-pack -v ▶ git verify-pack -v .git/objects/pack/pack-891394b403ab48f1b2264486397661f55895a867.pack
d453ecc64baf5775532f5c42b8bbc082a9e1ed14 commit 250 175 12
dd6cdadafdf3fcf1278e5ab85c3d3c3694cccf9f commit 249 170 187
96f352cdde7dd887f219a3724139d9f4c5405e88 commit 231 158 357
70e0b10e339780007f27454b48b87ad534222f22 commit 224 155 515
9dc03b327b7f7898d64a21a80d5ec7aea34930f5 commit 188 140 670
f4bd8c7bd40a2cb07611dd87cdc0b001c84d2186 commit 18 30 810 1 9dc03b327b7f7898d64a21a80d5ec7aea34930f5
62c893550adb53d3a8fc29a1584ff831cb829062 blob 6 15 840
2d0077acefe0c82f2b1751797930d61854ddedef blob 203642 1610 855
63c3cc5127ce2c42f670e07c597428716a62cb7d blob 18 26 2465
8abbb9d2066b01124935168578575bda84c611c4 tree 106 114 2491
90af1bd85bf9caefe7f2a896bde462e73197fe8a tree 43 54 2605
f4e13c3ae0262786fe5e6d6732364e8fb0a3c479 tree 106 114 2659
adc38899d876c80633deeb231f61c1680577edfb tree 43 54 2773
db53d7cf02340f626f717030979f2d8d1cd03dec blob 15 26 2827 1 2d0077acefe0c82f2b1751797930d61854ddedef
00176f1c402309625b9757f54c228e19c53f6705 tree 106 113 2853
6c0c493d71b5fa1052bb92835fd0a24e54540db7 tree 43 53 2966
a02a0d302c13ed72406a60cac8dc0b927c1f0a47 blob 34 42 3019
62f678e1e758528a7b5450cf7c225c684be08390 tree 106 113 3061
0dcf59da0270a619704a7fd02e64f624efc8d954 tree 43 53 3174
9cb6bc282361e81f326d93cc0007be1d5424d8a7 tree 76 84 3227
non delta: 18 objects
chain length = 1: 2 objects
.git/objects/pack/pack-891394b403ab48f1b2264486397661f55895a867.pack: ok
複製代碼
其中每個數據表明什麼能夠查看git-verify。咱們須要關注的是:此時,2d0077的大小是203642,而db53d的大小隻有15。同時git會在最新版本保存完成的數據內容,而在以前的版本只保留差別。這也是由於大部分狀況下,最新版本須要被快速的訪問。
index文件保存了全部暫存區的信息,它也是一個二進制文件,能夠經過git ls-files -s
查看暫存區的狀態。
▶ git ls-files -s
100644 62c893550adb53d3a8fc29a1584ff831cb829062 0 .gitignore
100644 2d0077acefe0c82f2b1751797930d61854ddedef 0 dir/readme.copy.txt
100644 63c3cc5127ce2c42f670e07c597428716a62cb7d 0 readme.txt
複製代碼
若是此時我更改一下readme.txt
文件的內容,而且提交到暫存區:
▶ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt
▶ git ls-files -s
100644 62c893550adb53d3a8fc29a1584ff831cb829062 0 .gitignore
100644 2d0077acefe0c82f2b1751797930d61854ddedef 0 dir/readme.copy.txt
100644 fc1e0c2ac5dca872def36e9a5ca0c9a30cb27ad9 0 readme.txt
複製代碼
能夠看到readme.txt
的文件key已經改變了,此時若是commit改動,暫存區依舊是保留這樣的信息,這其實也是和一般的認知有些不一樣的地方(一般,咱們認爲當commit後,暫存區就會被狀況)。其實暫存區保存的信息和commit
對象保存的內容,都是保存每個文件最新的狀態。git根據這些文件信息才能分辨出每一個區域不一樣的狀態。
在git中,除了須要保存版本信息外,還須要保存分支,tag,當前HEAD的信息。 其實refs/
用於保存分支和tag的信息;HEAD保存當前HEAD指針信息。
在git中新建一個分支second
,並查看分支信息
▶ git branch second
▶ cd .git/refs/heads
# 有幾個分支就會有幾個同名的文件
▶ ls
master second
# 每一個文件包含了當前分支所指向的commit id
▶ cat master
ef059bba1983be625c6cb619af7b7bd26966181d
▶ cat second
ef059bba1983be625c6cb619af7b7bd26966181d
# 若是HEAD和某一個分支指向的key相同
▶ cat HEAD
ref: refs/heads/master
# 若是HEAD指向歷史的某一個commit
▶ cat HEAD
d453ecc64baf5775532f5c42b8bbc082a9e1ed14
複製代碼
此時在新建兩個tag,並查看tag信息:
# 分別使用不一樣的命令新建tag
▶ git tag v1.0
▶ git tag -a v2.0 -m "this is v2.0"
# 有幾個tag就會有幾個同名的文件
▶ cd .git/refs/tags
▶ ls
v1.0 v2.0
▶ cat v1.0
ef059bba1983be625c6cb619af7b7bd26966181d
# 不使用-a參數的tag直接保存commit id
▶ git cat-file -t ef059
commit
# 使用-a參數的tag直接保存tag對象
▶ cat v2.0
5b6990bc6916f95f757090a411faa02b0b332178
▶ git cat-file -t 5b699
tag
# 能夠看到tag對象所包含的信息
▶ git cat-file -p 5b699
object ef059bba1983be625c6cb619af7b7bd26966181d
type commit
tag v2.0
tagger wenjun <wjxu@thoughtworks.com> 1570547502 +0800
this is v2.0
複製代碼
其實從這裏就能夠看到,在git中不論是分支,HEAD,仍是tag,本質是都是指向某一次提交的指針。
其實,經過上面的這些關鍵點,咱們就能大概搞清楚git是如何存儲的。能夠這樣打一個比方:git會將全部的更改以相似單鏈表的方式鏈接起來,串成一條或者多條路線;而分支和tag等指針就像是這些路上的地標;而HEAD則是代表在這些路中,當前你在哪一個位置。
固然,git內部的原理還遠不止此,若是你們有興趣也能夠繼續探索,歡迎交流。