本文以一個具體例子結合動圖介紹了Git的內部原理,包括Git是什麼儲存咱們的代碼和變動歷史的、更改一個文件時,Git內部是怎麼變化的、Git這樣實現的有什麼好處等等。html
經過例子解釋清楚上面這張動圖,讓你們瞭解Git的內部原理。若是你已經可以看懂這張圖了,下面的內容可能對你來講會比較基礎。前端
本文是2019/11/24在深圳騰訊大廈2樓多功能廳舉辦的FCC前端分享會(freeCodeConf 2019 深圳站)上分享的文字版。git
視頻:www.bilibili.com/video/av772…github
近幾年技術發展十分迅猛,讓部分同窗養成了一種學習知識停留在表面,只會調用一些指令的習慣。咱們時常有一種「我會用這個技術、這個框架」的錯覺,等到真正遇到問題,才發現事情沒有那麼簡單。shell
後來我開始沉下心,迴歸一開始接觸編程的時候,那時候學習一個知識都會深刻一點去思考背後的原理。但這並非說掌握並會使用高級Api不重要,他們也很是重要,而且是平常工做中大部分時間都在使用的,快速掌握它們意味着高效學習,能夠快速的應用在開發生產上。數據庫
只是有時候知道一些底層的東西,能夠更好的幫你理清思路,知道你真正在操做什麼,不會迷失在Git大量的指令和參數上面。編程
這裏會用一個簡單的例子讓你們直觀感覺一下git是怎麼儲存信息的。api
首先咱們先建立兩個文件bash
$ git init
$ echo '111' > a.txt
$ echo '222' > b.txt
$ git add *.txt
複製代碼
Git會將整個數據庫儲存在.git/
目錄下,若是你此時去查看.git/objects
目錄,你會發現倉庫裏面多了兩個object。
$ tree .git/objects
.git/objects
├── 58
│ └── c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
├── c2
│ └── 00906efd24ec5e783bee7f23b5d7c941b0c12c
├── info
└── pack
複製代碼
好奇的咱們來看一下里面存的是什麼東西
$ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
xKOR0a044K%
複製代碼
怎麼是一串亂碼?這是由於Git將信息壓縮成二進制文件。可是不用擔憂,由於Git也提供了一個可以幫助你探索它的api git cat-file [-t] [-p]
, -t
能夠查看object的類型,-p
能夠查看object儲存的具體內容。
$ git cat-file -t 58c9
blob
$ git cat-file -p 58c9
111
複製代碼
能夠發現這個object是一個blob類型的節點,他的內容是111,也就是說這個object儲存着a.txt文件的內容。
這裏咱們遇到第一種Git object,blob類型,它只儲存的是一個文件的內容,不包括文件名等其餘信息。而後將這些信息通過SHA1哈希算法獲得對應的哈希值 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c,做爲這個object在Git倉庫中的惟一身份證。
也就是說,咱們此時的Git倉庫是這樣子的:
咱們繼續探索,咱們建立一個commit。
$ git commit -am '[+] init'
$ tree .git/objects
.git/objects
├── 0c
│ └── 96bfc59d0f02317d002ebbf8318f46c7e47ab2
├── 4c
│ └── aaa1a9ae0b274fba9e3675f9ef071616e5b209
...
複製代碼
咱們會發現當咱們commit完成以後,Git倉庫裏面多出來兩個object。一樣使用cat-file
命令,咱們看看它們分別是什麼類型以及具體的內容是什麼。
$ git cat-file -t 4caaa1
tree
$ git cat-file -p 4caaa1
100644 blob 58c9bdf9d017fcd178dc8c0... a.txt
100644 blob c200906efd24ec5e783bee7... b.txt
複製代碼
這裏咱們遇到了第二種Git object類型——tree,它將當前的目錄結構打了一個快照。從它儲存的內容來看能夠發現它儲存了一個目錄結構(相似於文件夾),以及每個文件(或者子文件夾)的權限、類型、對應的身份證(SHA1值)、以及文件名。
此時的Git倉庫是這樣的:
$ git cat-file -t 0c96bf
commit
$ git cat-file -p 0c96bf
tree 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
author lzane 李澤帆 1573302343 +0800
committer lzane 李澤帆 1573302343 +0800
[+] init
複製代碼
接着咱們發現了第三種Git object類型——commit,它儲存的是一個提交的信息,包括對應目錄結構的快照tree的哈希值,上一個提交的哈希值(這裏因爲是第一個提交,因此沒有父節點。在一個merge提交中還會出現多個父節點),提交的做者以及提交的具體時間,最後是該提交的信息。
此時咱們去看Git倉庫是這樣的:
到這裏咱們就知道Git是怎麼儲存一個提交的信息的了,那有同窗就會問,咱們日常接觸的分支信息儲存在哪裏呢?
$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
0c96bfc59d0f02317d002ebbf8318f46c7e47ab2
複製代碼
在Git倉庫裏面,HEAD、分支、普通的Tag能夠簡單的理解成是一個指針,指向對應commit的SHA1值。
其實還有第四種Git object,類型是tag,在添加含附註的tag(git tag -a
)的時候會新建,這裏不詳細介紹,有興趣的朋友按照上文中的方法能夠深刻探究。
至此咱們知道了Git是什麼儲存一個文件的內容、目錄結構、commit信息和分支的。其本質上是一個key-value的數據庫加上默克爾樹造成的有向無環圖(DAG)。這裏能夠蹭一下區塊鏈的熱度,區塊鏈的數據結構也使用了默克爾樹。
接下來咱們來看一下Git的三個分區(工做目錄、Index 索引區域、Git倉庫),以及Git變動記錄是怎麼造成的。瞭解這三個分區和Git鏈的內部原理以後能夠對Git的衆多指令有一個「可視化」的理解,不會再常常搞混。
接着上面的例子,目前的倉庫狀態以下:
這裏有三個區域,他們所儲存的信息分別是:
咱們來看一下更新一個文件的內容這個過程會發生什麼事。
運行echo "333" > a.txt
將a.txt的內容從111修改爲333,此時如上圖能夠看到,此時索引區域和git倉庫沒有任何變化。
運行git add a.txt
將a.txt加入到索引區域,此時如上圖所示,git在倉庫裏面新建了一個blob object,儲存了新的文件內容。而且更新了索引將a.txt指向了新建的blob object。
運行git commit -m 'update'
提交此次修改。如上圖所示
至此咱們知道了Git的三個分區分別是什麼以及他們的做用,以及歷史鏈是怎麼被創建起來的。**基本上Git的大部分指令就是在操做這三個分區以及這條鏈。能夠嘗試的思考一下git的各類命令,試一下你能不可以在上圖將它們「可視化」**出來,這個很重要,建議嘗試一下。
若是不能很好的將平常使用的指令「可視化」出來,推薦閱讀 圖解Git
有興趣的同窗能夠繼續閱讀,這部分不是文章的主要內容
想象一下修改一個文件的命名。
若是將文件名保存在blob裏面,那麼Git只能多複製一份原始內容造成一個新的blob object。而Git的實現方法只須要建立一個新的tree object將對應的文件名更改爲新的便可,本來的blob object能夠複用,節約了空間。
由上面的例子咱們能夠看到,Git儲存的是全新的文件快照,而不是文件的變動記錄。也就是說,就算你只是在文件中添加一行,Git也會新建一個全新的blob object。那這樣子是否是很浪費空間呢?
這實際上是Git在空間和時間上的一個取捨,思考一下你要checkout一個commit,或對比兩個commit之間的差別。若是Git儲存的是問卷的變動部分,那麼爲了拿到一個commit的內容,Git都只能從第一個commit開始,而後一直計算變動,直到目標commit,這會花費很長時間。而相反,Git採用的儲存全新文件快照的方法能使這個操做變得很快,直接從快照裏面拿取內容就好了。
固然,在涉及網絡傳輸或者Git倉庫真的體積很大的時候,Git會有垃圾回收機制gc,不只會清除無用的object,還會把已有的類似object打包壓縮。
經過SHA1哈希算法和哈系樹來保證。假設你偷偷修改了歷史變動記錄上一個文件的內容,那麼這個問卷的blob object的SHA1哈希值就變了,與之相關的tree object的SHA1也須要改變,commit的SHA1也要變,這個commit以後的全部commit SHA1值也要跟着改變。又因爲Git是分佈式系統,即全部人都有一份完整歷史的Git倉庫,因此全部人都能很輕鬆的發現存在問題。
但願你們讀完有所收穫,下一篇文章會寫一些我平常工做中以爲比較實用的Git技巧、常常被問到的問題、以及發生一些事故時的處理方法。
若是你喜歡個人文章,請關注我和個人博客,謝謝。