簡介: 本文將系統分享 Git 底層知識:對象生命週期變化,底層數據結構,數據包文件結構,數據包文件索引,以及詳細分析對象查詢流程和算法。webpack
上圖描述了 git 對象的在不一樣的生命週期中不一樣的存儲位置,經過不一樣的 git 命令改變 git 對象的存儲生命週期。git
就是咱們當前工做空間,也就是咱們當前能在本地文件夾下面看到的文件結構。初始化工做空間或者工做空間 clean 的時候,文件內容和 index 暫存區是一致的,隨着修改,工做區文件在沒有 add 到暫存區時候,工做區將和暫存區是不一致的。web
老版本概念也叫 Cache 區,就是文件暫時存放的地方,全部暫時存放在暫存區中的文件將隨着一個 commit 一塊兒提交到 local repository 此時 local repository 裏面文件將徹底被暫存區所取代。暫存區是 git 架構設計中很是重要和難理解的一部分。算法
git 是分佈式版本控制系統,和其餘版本控制系統不一樣的是他能夠徹底去中心化工做,你能夠不用和中央服務器 (remote server) 進行通訊,在本地便可進行所有離線操做,包括 log,history,commit,diff 等等。完成離線操做最核心是由於 git 有一個幾乎和遠程同樣的本地倉庫,全部本地離線操做均可以在本地完成,等須要的時候再和遠程服務進行交互。json
中心化倉庫,全部人共享,本地倉庫會須要和遠程倉庫進行交互,也就能將其餘全部人內容更新到本地倉庫把本身內容上傳分享給其餘人。結構大致和本地倉庫同樣。數組
文件在不一樣的操做下可能處於不一樣的 git 生命週期,下面看看一個文件變化的例子。安全
git 分佈式的一個重要體現是 git 在本地是有一個完整的 git 倉庫也就是 .git 文件目錄,經過這個倉庫,git 就能夠徹底離線化操做。在這個本地化的倉庫中存儲了 git 全部的模型對象。下面是 git 倉庫的 tree 和相關說明:服務器
git 主要有四個對象,分別是 Blob,Tree, Commit, Tag 他們都用 SHA-1 進行命名。網絡
你能夠用 git cat-file -t 查看每一個 SHA-1 的類型,用 git cat-file -p 查看每一個對象的內容和簡單的數據結構。git cat-file 是 git 的瑞士軍刀,是底層核心命令。數據結構
只用於存儲單個文件內容,通常都是二進制的數據文件,不包含任何其餘文件信息,好比不包含文件名和其餘元數據。
對應文件系統的目錄結構,裏面主要有:子目錄 (tree),文件列表 (blob),文件類型以及一些數據文件權限模型等。
以下圖輸出:
→ git cat-file -t ed807a4d010a06ca83d448bc74c6cc79121c07c3 tree → git cat-file -p ed807a4d010a06ca83d448bc74c6cc79121c07c3 100644 blob 36a982c504eb92330573aa901c7482f7e7c9d2e6 .cise.yml 100644 blob c439a8da9e9cca4e7b29ee260aea008964a00e9a .eslintignore 100644 blob 245b35b9162bec4ef798eb05b533e6c98633af5c .eslintrc 100644 blob 10123778ec5206edcd6e8500cc78b77e79285f6d .gitignore 100644 blob 1a48aa945106d7591b6342585b1c29998e486bf6 README.md 100644 blob 514f7cb2645f44dd9b66a87f869d42902174fe40 abc.json 040000 tree 8955f46834e3e35d74766639d740af922dcaccd3 cli_list100644 blob f7758d0600f6b9951cf67f75cf0e2fabcea55771 dep.json 040000 tree e2b3ee59f6b030a45c0bf2770e6b0c1fa5f1d8c7 doc 100644 blob e3c712d7073957c3376d182aeff5b96f28a37098 index.js 040000 tree b4aadab8fc0228a14060321e3f89af50ba5817ca lib040000 tree 249eafef27d9d8ebe966e35f96b3092d77485a79 mock 100644 blob 95913ff73be1cc7dec869485e80072b6abdd7be4 package.json 040000 tree e21682d1ebd4fdd21663ba062c5bfae0308acb64 src 040000 tree 91612a9fa0cea4680228bfb582ed02591ce03ef2 static 040000 tree d0265f130d2c5cb023fe16c990ecd56d1a07b78c task100644 blob ab04ef3bda0e311fc33c0cbc8977dcff898f4594 webpack.config.js 100644 blob fb8e6d3a39baf6e339e235de1a9ed7c3f1521d55 webpack.dll.config.js 040000 tree 5dd44553be0d7e528b8667ac3c027ddc0909ef36 webpack
詳細解釋以下:
是一次修改的集合,當前全部修改的文件的一個集合,能夠類比一批操做的「事務」。是修改過的文件集的一個快照,隨着一次 commit 操做,修改過的文件將會被提交到 local repository 中。經過 commit 對象,在版本化中能夠檢索出每次修改內容,是版本化的基石。
→ git cat-file -t fbf9e415f77008b780b40805a9bb996b37a6ad2c commit → git cat-file -p fbf9e415f77008b780b40805a9bb996b37a6ad2c tree bd31831c26409eac7a79609592919e9dcd1a76f2 parent d62cf8ef977082319d8d8a0cf5150dfa1573c2b7 author xxx 1502331401 +0800 committer xxx 1502331401 +0800 修復增量bug
詳細解釋以下:
tag 是一個"固化的分支",一旦打上 tag 以後,這個 tag 表明的內容將永遠不可變,由於 tag 只會關聯當時版本庫中最後一個 commit 對象。
分支的話,隨着不斷的提交,內容會不斷的改變,由於分支指向的最後一個 commit 不斷改變。因此通常應用或者軟件版本的發佈通常用 tag。
git 的 Tag 類型有兩種:
1 lightweight (輕量級)
建立方式:
git tag tagName
這種方式建立的 Tag,git 底層不會建立一個真正意義上的 tag 對象,而是直接指向一個 commit 對象,此時若是使用 git cat-file -t tagName 會返回一個 commit。
→ git cat-file -t v4 commit → git cat-file -p v4 tree ceab4f96440655b0ff1a783316c95450fa1fb436 parent 7f23c9ca70ce64fc58e8c7507c990c6c6a201d3d author 與水 1506224164 +0800 committer 與水 1506224164 +0800 rawtest2
2 annotated (含附註)
建立方式:
git tag -a tagName -m''
這種方式建立的標籤,git 底層會建立一個 tag 對象,tag 對象會包含相關的 commit 信息和 tagger 等額外信息,此時若是使用 git cat-file -t tagname 會返回一個 tag。
→ git cat-file -t v3 tag → git cat-file -p v3 object d5d55a49c337d36e16dd4b05bfca3816d8bf6de8 //commit 對象SHA-1 type commit tag v3 tagger xxx 1506230900 +0800 與水測試標註型tag
總結:全部對象模型之間的關係大體以下:
git 區別與其餘 vcs 系統的一個最主要緣由之一是:git 對文件版本管理和其餘 vcs 系統對文件版本的實現理念完成不同。這也就是 git 版本管理爲何如此強大的最核心的地方。
Svn 等其餘的 VCS 對文件版本的理念是以文件爲水平維度,記錄每一個文件在每一個版本下的 delta 改變。
Git 對文件版本的管理理念倒是以每次提交爲一次快照,提交時對全部文件作一次全量快照,而後存儲快照引用。
Git 在存儲層,若是文件數據沒有改變的文件,Git 只是存儲指向源文件的一個引用,並不會直接屢次存儲文件,這一點能夠在 pack 文件中看見。
以下圖所示:
隨着需求和功能的不斷複雜,git 版本的不斷更新,可是主要的存儲模型仍是大體不變。以下圖所示:
→ cd .git/objects/ → ls 03 28 7f ce d0 d5 e6 f9 info pack
git 的對象有兩種:
一種是鬆散對象,就是在如上 .git/objects 的文件夾 03 28 7f ce d0 d5 e6 f9 等,這些文件夾只有 2 個字符開頭,其實就是每一個文件 SHA-1 值的前 2 個字母,最多有 #OXFF 256 個文件夾。
一種是打包壓縮對象,打包壓縮以後的對象主要存在的是 pack 文件中,主要用於文件在網絡上傳輸,減小網絡消耗。
爲了節省存儲空間,能夠手動觸發打包壓縮操做 (git gc),將鬆散對象打包成 pack 文件對象。也能夠將 pack 文件解壓縮成鬆散對象 (git unpack-objects)
→ cd pack → ls pack-efbf3149604d24e6ea427b025da0c59245b2c2ea.idx pack-efbf3149604d24e6ea427b025da0c59245b2c2ea.pack
爲了加快 pack 文件的檢索效率,git 基於 pack 文件會生成相應的索引 idx 文件。
pack 文件設計很是精密和巧妙,本着下降文件大小,減小文件傳輸,下降網絡開銷和安全傳輸的原則設計的。
pack 文件設計的概圖以下:
pack 文件主要有三部分組成,Header, Body, Trailer
下面咱們看具體的 pack 文件:
從上圖可知:經過 idx 索引文件在 pack 文件中定位到對象以後,對象的結構主要 Header 和 Data 兩部分。
1 Header 部分
Header 中首 8-bits:1-bit 是 MSB,接着的 3-bits 表示的是當前對象類型,主要有 6種存儲類型,接着的 4-bits 是用於表示該 Object 展開的 (length) 大小的一部分,只是一部分,完整的大小取決於MSB和接下來的多個 bits,完整算法以下:
2 Data 部分
是通過 Zlib 壓縮過的數據。多是所有數據,也有多是 Delta 數據,具體看 Header 部分的存儲類型,若是是 OBJ_OFS_DELTA 或者 OBJ_REF_DELTA 此處存儲的就是增量 (Delta) 數據,此時若是要取得全量數據的話,須要遞歸的找到最 Base Object,而後 apply delta 數據,在 base object 基礎上進行 apply delta 數據也是很是精妙的,此文暫不作介紹。
從上面能夠很清晰知道 pack 文件格式,咱們再從本地倉庫中一探究竟:
不是增量 delta 格式:
SHA-1 type size size-in-packfile offset-in-packfile
增量 delta 格式:
SHA-1 type size size-in-packfile offset-in-packfile depth base-SHA-1
→ git verify-pack -v pack-efbf3149604d24e6ea427b025da0c59245b2c2ea.pack cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804 commit 275 189 12 399334856af4ca4b49c0008a25b6a9f524e40350 commit 69 81 201 1 cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804 e0efbd5121c31964af1615cf24135a7c6c11cc1d commit 268 187 282 7bc9a5e0199bd4a6d4d223ce7e13239631df9635 commit 29 41 469 1 e0efbd5121c31964af1615cf24135a7c6c11cc1d 2e43c62f6ff99c88d20329487137f8dbabc8b3ec commit 220 157 510 b6f173085f49f109a00b2a3f08a7dc499cc47f1f commit 220 157 667 0466b3f1aadde74234f7dd3f4ef7f1505c50fb0c commit 220 157 824 76c5e45f8e295226b1bc5c8c7e2bc98d7eae6be1 commit 74 85 981 1 b6f173085f49f109a00b2a3f08a7dc499cc47f1f 2729f1fa896d384b49a2f5c53d483eacc0929ebb commit 172 127 1066 3cc58df83752123644fef39faab2393af643b1d2 blob 2 11 1193 62189d1a10cc2a544c4e5b9c4aba9493cf5782dc blob 8 15 1204 a9a5aecf429fd8a0d81fbd5fd37006bfa498d5c1 blob 4 13 1219 2b8982f7c281964658d2cd8b6c17b541533dd277 tree 104 105 1232 92c4aafa39ee387a1f8237f00c78c499aebaf0b2 tree 104 105 1337 223b7836fb19fdf64ba2d3cd6173c6a283141f78 blob 2 11 1442 1756ca64f21724f350fe2cc5cfb218883e314c3d tree 71 80 1453 e11ddfa79f01b01a8e1553bbffaa2d6c03ae9f6e tree 71 80 1533 f70f10e4db19068f79bc43844b49f3eece45c4e8 blob 2 11 1613 e982b6207b10a869164e2c8d19d25ffb059e6a16 tree 66 73 1624 f2e9f73f27124916344e0fd03bb449bc6feca59d tree 66 74 1697 d09da444f461d7cee3679666a1ded5ab79832ed0 tree 33 44 1771 non delta: 18 objects chain length = 1: 3 objects pack-efbf3149604d24e6ea427b025da0c59245b2c2ea.pack: ok
如 399334856af4ca4b49c0008a25b6a9f524e40350(SHA-1) 表示對象的 base object SHA-1 是 cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804,base 對象最大深度 (depth) 爲 1,若是 cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804 還有引用對象,則改變 depth 爲 2。
pack Header 中最後 4-bytes 用於表示的 pack 文件中 objects 的數量,最多 2 的 32 次方個對象,因此一些大的工程中有多個 pack 文件和多個 idx 文件。
文件的 size (文件解壓縮後大小) 有什麼用呢,這個是爲了方便咱們進行解壓的時候,設置流的大小,也就是方便知道流有多大。這裏 size 不是說明下一個文件的偏移量,偏移量都是來自索引文件,見下面 idx:
因爲 version1 比較簡單,下面用 version2 爲例子:
分層模式:Header,Fanout,SHA,CRC,Offset,Big File Offset,Trailer。
Header 層
version2 的 Header 部分總共有 8-bytes,version 1 的 header 部分是沒有的,前 4-bytes 老是 255, 116, 79, 99 由於這個也是版本 1 的開頭四個字節,後面 4-bytes 用於表示的是版本號,在當前就是 version 2。
Fanout 層
fanout 層是 git 的亮點設計,也叫 Fanout Table(扇表)。fanout 數組中存儲的是相關對象的數目,數組下標是對應 16 進制數。fanout 最後一個存儲的是整個 pack 文件中全部對象的總數量。Fanout Table 是整個 git 檢索的核心,經過它咱們能夠快速進行查詢,用於定位 SHA 層的數組起始 - 終止下標,定位好 SHA 層範圍以後,就能夠對 SHA 層進行二分查找了,而不用對全部對象進行二分查找。
fanout 總共 256 個,恰好是十六進制的 #0xFF。fanout 數組用 SHA 的前面 2 個字符做爲下標(對應 .git/objects 中的鬆散文件目錄名,將 16 進制的目錄名轉換 10 進制數字),裏面值就是用這兩個字符開頭的文件數量,並且是逐層累加的,後面的數組數量是包含前面數組的數據的個數的一個累加。
舉例以下:
1)若是數組下標爲 0,且 Fanout[0] = 10 表明着 #0x00 開頭的 SHA-1 值的總數爲 10 個。
2) 若是數組下標爲 1,且 Fanout[1] = 15 表明着小於 #0x01 開頭的 SHA-1 值的總數爲 15 個,從 Fanout[0] = 10 知 Fanout[1] = (15-10)
爲何 git 設計上 Fanout[n] 會累加 Fanout[n-1] 的數量?這個主要是爲了快速肯定 SHA 層檢索的初始位置,而不用每次去把前面全部 fanout[..n-1] 數量進行累加。
SHA 層
是全部對象的 SHA-1 的排序,按照名稱排序,按照名稱進行排序是爲了用二分搜索進行查找。每一個 SHA-1 值佔 20-bytes。
CRC 層
因爲文件打包主要解決網絡傳輸問題,網絡傳輸的時候必須經過 crc 進行校驗,避免傳輸過程當中的文件損壞。CRC 數組對應的是每一個對象的 CRC 校驗和。
Offset 層
是由 4 byte 字節所組成,表示的是每一個 SHA-1 文件的偏移量,可是若是文件大於 2G 以後,4 byte 字節將沒法表示,此時將:
4 byte 中的第一 bit 就是 MSB,若是是 1 表示的是文件的偏移量是放在第 6 層去存儲,此時剩下的 31-bits 將表示文件在 Big File Offset 中的偏移量,也就是圖中的,經過 Big File Offset 層 就能夠知道對象在 pack 中的 offset。
4 byte 中的第一 bit 就是 MSB,若是是 0 31-bits 表示的存儲對象在 packfile 中的文件偏移量,此時不涉及 Big File Offset 層
Big File Offset 層
用於存儲大於 2G 的文件的偏移量。若是文件大於 2G,能夠經過 offset 層最後 31 bits 決定在 big file offset 中的位置,big file offset 經過 8 bytes 來表示對象在 pack 文件中的位置,理論上能夠表示 2 的 64 次方文件大小。
Trailer 層
包含的是 packfile checksum 和關聯的 idx 的 checksum。
索引流程
從上面的分層知道 git 設計的巧妙。git 索引文件偏移量的查詢流程以下:
經過 idx 文件查詢 SHA-1 對應的偏移量:
在 pack 文件中經過偏移量找到對象:
若是是普通的存儲類型。定位到的對象就是用 Zlib 壓縮以後的對象,直接解壓縮便可。
若是是 Delta 類型須要 遞歸查出 Delta 的 Base 對象,而後再把 delta data 應用到 base object 上(可參考 git-apply-delta)。