用了5年的Git,你竟然還不曉得它的實現(xiàn)原理!

作者 |?楊夕
越了解事物的本質(zhì)就越接近真相。我發(fā)現(xiàn)學習Git內(nèi)部是如何工作的以及Git的內(nèi)部數(shù)據(jù)結(jié)構這部分內(nèi)容,對于理解Git的用途和強大至關重要。若你理解了Git的思想和基本工作原理,用起來就會知其所以然,游刃有余。這是Git系列的第一篇,主要會介紹Git的特點以及內(nèi)部數(shù)據(jù)結(jié)構設計,和完成一次完整提交流程的時候數(shù)據(jù)是如何變化的。
Git有什么特點?
fast,scalable,distributed revision control system(快速,可擴展的分布式版本控制系統(tǒng))
幾乎所有操作都是本地執(zhí)行 每一個clone都是整個生命周期的完整副本 the stupid content tracker(只是一個內(nèi)容追蹤器)
Git追蹤的是內(nèi)容而不是文件 如果兩個文件的內(nèi)容相同,無論是否在相同的目錄,Git在對象庫里只保存一份blob對象 Immutable(不可變性)
Git版本庫中存儲的數(shù)據(jù)對象均為不可變的,一旦創(chuàng)建數(shù)據(jù)對象并放入了數(shù)據(jù)庫中,它們便不可修改。這也意味著存儲在版本數(shù)據(jù)庫中的整個歷史也是不可變的。 Porcelain(高層命令)
init, add, commit, branch, merge. Plumbing(底層命令)
hash-object, update-index, write-tree.

蘋果開源代碼中驚現(xiàn)“wechat”,老外注釋的吐槽亮了!
每一個Client端都可以是Server
Git Version Database是什么?
Git是一個內(nèi)容尋址文件系統(tǒng)。這意味著,Git的核心部分是一個簡單的鍵值對數(shù)據(jù)庫(key-value data store)。你可以向該數(shù)據(jù)庫插入任意類型的內(nèi)容,它會返回一個鍵值,通過該鍵值可以在任意時刻再次檢索該內(nèi)容。而這些數(shù)據(jù)全部是存儲在objects目錄里。key是一個hash,hash前兩個字符用于命名子目錄,余下的38個字符則用作文件名。如果了解tree樹的朋友應該會想明白之所以這樣處理是因為檢索優(yōu)化策略,提高文件系統(tǒng)效率(如果把太多的文件放入同一個目錄中,一些文件系統(tǒng)會變慢)。而這個hash的內(nèi)容(即hash對應的Value)有四種對象類型,commit(提交),tree(目錄樹),blob(塊),tag(標簽)。
Git基本概念:
Content addressable filesystem(內(nèi)容尋址文件系統(tǒng))
Simple key-value data store(鍵值對數(shù)據(jù))
Key:SHA-1散列(hash,哈希)
Everything is hash 這是一個由40個十六進制字符(0-9和a-f)組成字符串 Value:binary files
Commit:Actual git commits(提交) Tree:Directoy(目錄樹) Blob:file content(文件內(nèi)容)
note:可以理解成Commit = Tree + Blob的snapshot
什么是SHA-1:SHA-1(安全散列函數(shù)),是一種密碼散列函數(shù),美國國家安全局設計,并由美國國家標準技術研究所發(fā)布為聯(lián)邦數(shù)據(jù)處理標準。SHA-1可以生成一個被稱為消息摘要的160位(20字節(jié))散列值,散列值通常的呈現(xiàn)形式為40個十六進制數(shù)。用js來理解就是一個純函數(shù),輸入一定輸出也一定,相同的輸入一定有相同的輸出。不相同的輸入一定有不同的輸出(不考慮碰撞 ,比彗星撞擊地球的概率還低)。
Git到底是如何工作呢?
我們知道最簡單的git flow主要有三步:
在工作目錄中修改文件。 暫存文件,將文件的快照放入暫存區(qū)域。 提交更新,找到暫存區(qū)域的文件,將快照永久性存儲到Git倉庫目錄。
對應高層命令是這樣的:
$?git?init
$?git?add?.
$?git?commit
在我們看這三個命令到底做了什么之前,先來了解一下幾個概念:

Working Directory:工作區(qū)(工作目錄) Stageing Area (Index):暫存區(qū) Repository:倉庫區(qū)(本地倉庫)
Git init
我們先用Git init來初始化一個項目,并查看項目的目錄結(jié)構。
$?git?init?demo1?&&?cd?demo1
$?tree?.git
.git
├──?HEAD
├──?config
├──?description
├──?hooks
│???├──?applypatch-msg.sample
│???├──?commit-msg.sample
│???├──?fsmonitor-watchman.sample
│???├──?post-update.sample
│???├──?pre-applypatch.sample
│???├──?pre-commit.sample
│???├──?pre-push.sample
│???├──?pre-rebase.sample
│???├──?pre-receive.sample
│???├──?prepare-commit-msg.sample
│???└──?update.sample
├──?info
│???└──?exclude
├──?objects
│???├──?info
│???└──?pack
└──?refs????
??????├──?heads????
??????└──?tags
description文件僅供GitWeb程序使用。config文件包含項目特有的配置選項。info目錄包含一個全局性排除文件,用以放置那些不希望被記錄在.gitignore文件中的忽略模式。hooks目錄包含客戶端或服務端的鉤子腳本,這些我們暫時都無需關心。最重要的是:HEAD文件、(尚待創(chuàng)建的)index文件,和objects目錄、refs目錄。這些條目是Git的核心組成部分。objects目錄存儲所有數(shù)據(jù)內(nèi)容(hash);refs目錄存儲指向數(shù)據(jù)(分支)的提交對象的指針(commit hash);HEAD文件指示目前被檢出的分支(refs目錄內(nèi)的分支名);index 文件保存暫存區(qū)信息(git ls-files --stage命令查看當前暫存區(qū)信息)。
下面我們就用底層命令來實現(xiàn)git init指令(另創(chuàng)建一個demo2目錄)。
mkdir -p參數(shù)是能直接創(chuàng)建一個不存在的目錄下的子目錄:
$?mkdir?-p?.git/refs/heads?.git/refs/tags?.git/objects
$?echo?'ref:?refs/heads/master'?>?.git/HEAD

可以看到已經(jīng)成功初始化了一個Git項目。
git add
$?echo?'hello?git'?>?index.txt
$?git?add?index.txt
執(zhí)行完這兩句指令后我們再來看.git文件夾發(fā)生了什么變化(為了顯示效果,簡化目錄結(jié)構,之后tree 都忽略hooks文件夾)
.git
├──?HEAD
├──?config
├──?description
├──?index
├──?info
│???└──?exclude
├──?objects
│???├──?8d
│???│???└──?0e41234f24b6da002d962a26c2495ea16a425f
│???├──?info
│???└──?pack
└──?refs????
??????├──?heads????
??????└──?tags
可以看到多了一個index文件,并且objects目錄里面多了一個8d的文件夾,里面有一個0e41開頭的文件、那這個8d0e4這個是什么呢?其實這個就是index.txt文件內(nèi)容的hash。還記得嘛,剛才寫入文件內(nèi)容是hello git,我們來手動輸出這個內(nèi)容的hash。
$?echo?'hello?git'?|?git?hash-object?--stdin
$?8d0e41234f24b6da002d962a26c2495ea16a425f
可以通過cat-file命令從Git那里取回數(shù)據(jù)。為cat-file指定-p選項可指示該命令自動判斷內(nèi)容的類型,并為我們顯示格式友好的內(nèi)容:
$?git?cat-file?-p?8d0e
$?hello?git
為cat-file指定-t選項可以查看文件的類型:
$?git?cat-file?-t?8d0e
$?blob
git add做了兩件事情:
文件內(nèi)容做一個hash存成blob object 把index放入到Staging Area
當為index.txt創(chuàng)建一個對象的時候,git并不關心index.txt的文件名,git 只關心文件里面的內(nèi)容。
按照這個思路,我們用底層命令來實現(xiàn)一下git add指令。
$?echo?'hello?git'?|?git?hash-object?-w?--stdin
$?git?update-index?--add?--cacheinfo?100644?8d0e41234f24b6da002d962a26c2495ea16a425f?index.txt
-w選項指示hash-object命令存儲數(shù)據(jù)對象;若不指定此選項,則該命令僅返回對應的鍵值。我們指定的文件模式為100644,表明這是一個普通文件。其他選擇包括:100755,表示一個可執(zhí)行文件;120000,表示一個符號鏈接。

因為并沒有去創(chuàng)建這個index.txt文件, 所以這邊提示已經(jīng)刪除了,執(zhí)行git checkout -- index.txt取出文件。

可以看到已經(jīng)成功用底層命名實現(xiàn)了git add的功能。
到這里,我們自然就會有個疑問了,那文件名怎么辦?
Git是通過tree對象來跟蹤文件的路徑名的。當使用git add命令時,git會給添加的文件內(nèi)容創(chuàng)建一個blob對象,但是這個時候并不會創(chuàng)建tree對象。而只是更新索引,索引在.git/index中,它跟蹤文件的路徑名和相對應blob,每次執(zhí)行git add 、git rm 、 git mv 的時候,git都會更新索引,我們可以通過命令git ls-files --stage來查看當前的索引信息。
$?git?ls-files?--s
$?100644?8d0e41234f24b6da002d962a26c2495ea16a425f?0?index.txt
git commit
執(zhí)行git commit -m 'init-1'后,查看tree結(jié)構,發(fā)現(xiàn)object 多出了兩個文件:
.git
├──?COMMIT_EDITMSG
├──?HEAD
├──?config
├──?description
├──?index
├──?info
│???└──?exclude
├──?logs
│???├──?HEAD
│???└──?refs
│???????└──?heads
│???????????└──?master
├──?objects
│???├──?75
│???│???└──?0d7c0f7f998d3e2ce2d71ec801902f69bf6a39
│???├──?88
│???│???└──?bc066ebf3d864e34297f7051a0ded16e49813a
│???├──?8d
│???│???└──?0e41234f24b6da002d962a26c2495ea16a425f
│???├──?info
│???└──?pack
└──?refs????
??????├──?heads????
??????│???└──?master
??????└──?tags
$?git?log
$?commit?750d7c0f7f998d3e2ce2d71ec801902f69bf6a39?(HEAD?->?master)
查看這個commit 的文件類型,可以看到這是一個commit:
$?git?cat-file?-t?750d
$?commit
$?git?cat-file?-p?750d
$?tree?88bc066ebf3d864e34297f7051a0ded16e49813a
但是多出來的88bc是什么呢,其實就是當前目錄的tree對象,所以Git是在commit的時候才創(chuàng)建tree對象的(其實是把索引轉(zhuǎn)化成tree對象)。
$?git?cat-file?-t?88bc
$?tree
$?git?cat-file?-p?88bc
$?100644?blob?8d0e41234f24b6da002d962a26c2495ea16a425f??index.txt
這個時候再看HEAD:
$?cat?.git/HEAD
$?ref:?refs/heads/master
繼續(xù)查看refs/heads/master:
$?cat?.git/refs/heads/master
$?750d7c0f7f998d3e2ce2d71ec801902f69bf6a39
所以整個指向關系就是:HEAD里面的內(nèi)容是當前的ref,而當前ref的內(nèi)容是commit hash,commit對象內(nèi)容是tree hash,tree對象的內(nèi)容是文件夾/文件信息,而blob對象存儲著文件的具體內(nèi)容。這樣當完成一次提交的時候,整個狀態(tài)的對應關系也是確定的,所以說commit對象就是當前系統(tǒng)的snapshot。

再來回顧下一次完整的提交流程:

往期推薦

