Git 是一套內容尋址文件系統。很不錯。不過這是什麼意思呢? 這種說法的意思是,Git 從核心上來看不過是簡單地存儲鍵值對(key-value)。它容許插入任意類型的內容,並會返回一個鍵值,經過該鍵值能夠在任什麼時候候再取出該內容。php
咱們都知道當咱們初始化一個倉庫的時候,也就是執行如下命令後,文件夾內會生成一個.git文件夾,git
git init
內部會包含,如下文件夾。算法
這是objects文件夾,能夠看到都是些數字和字符,實際上就是十六進制數。shell
下圖是進入00文件夾後全部文件。數據庫
下面咱們直接上底層命令, 運行此命令後,會在 .git/objects 文件夾下生成一個 兩個字符 的文件夾,文件夾內部文件即相似上圖中文件同樣。服務器
echo 'test' | git hash-object -w --stdin git hash-object -w test.txt
分解命令:函數
hash-object: 計算文本內容的sha-1(哈希值) -w : 加上此參數後,會把內容寫入/objects文件夾,不加則僅僅是計算(不可以使用此法單純作計算用,由於GIT計算的HASH,其基礎內容與原內容有所區別) --stdin : 此參數接收來自於標準輸入的內容,即前面的 echo 'test'; 不加此參數,則直接寫入某個文本
因此實際上咱們看到的,objects 文件夾下的內容,文件名其實是 hash 值。文件夾是40個字符的前兩個(擁有相同前2位的hash值會被分配到同一個文件夾中), 具體文件名則是後面38個字符。使用hash值的緣由就在於,位數夠多,而且hash值惟一,一點小變化,都會生成新的hash值,和md5算法是同樣的道理。測試
<font color="red">注意:此hash值就像是GIT的指針,能惟一對應某一個具體的內容或提交,hash值做爲尋址做用,不做爲內容存儲用,具體的文件內容存儲方式是GIT更底層的存儲方式決定。(sha-1和md5同樣,均是不可逆的)</font>加密
經過Linux find 命令查看全部已存儲的hash文件:spa
find .git/objects -type f
經過 cat-file 命令能夠將數據內容取回。該命令是查看 Git 對象的瑞士軍刀。傳入 -p 參數可讓該命令輸出數據內容的類型:
git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content
經過 hash-object 命令,會把每個文件的內容都給記錄下來, 以今生成一個blob對象。可經過如下命令查看對象的類型
git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4 blob
在實際項目過程當中,不會這麼簡單,由於咱們每次提交都是一個多文件的提交。不多的時候是單文件的,那此時Git就不是單單存儲一個 blob對象了,而是 tree對象,
tree對象,見名知意,就是一個樹對象,相似於操做系統目錄,tree的分支,可能仍是tree,也多是blob,這就看實際的場景了。
對象存儲方法:
GIT使用 zlib 庫 的 deflate方法對數據內容進行壓縮,但內容爲 "blob 字符串長度+空字節+字符串自己"; 如:
blob 3\0aaa
上面說的建立blob對象,僅僅只是對某一個文件進行的計算與存儲,而咱們實際項目中,可能每一次操做都是好幾個,甚至十幾個文件一塊兒,那如何才能把他們組織到一塊兒,這就是 tree 對象的做用了。
要建立tree對象,須要使用 update-index,write-tree 命令:
git update-index --add a.txt //此命令便可將a.txt加入到暫存區, git write-tree //此命令即寫入tree對象。 or git update-index --add --cacheinfo 100164 sha-1 a.txt git write-tree
--cacheinfo 會從已存在的數據庫(Object)中取得對應的內容給添加到索引中。
實際生產中,通常狀況下,會把末尾文件夾中的全部修改文件建立,blob對象,再對該文件夾(也就是全部的blob對象總體)進行write-tree的操做,獲得一個tree對象,反覆進行此操做,最後獲得多個tree對象和多個blob對象。
如上所說,若須要對某個存在三級文件夾的二級文件夾進行write-tree操做, 在把三級文件夾下的全部修改文件生成blob後,進行總體tree對象化,以後再與二級文件夾同級的文件夾和文件進行相同操做。此時就須要用到: read-tree 命令。如:
git read-tree --prefix=test_add_tree c08670e3f77cae748fbda5c0b83613d5f5995655 //該操做會把tree對象b822ff7272492f12b211d3b9c0f90163f48383bb 加入暫存區中,並取名test,以後再進行write-tree就把tree對象b822ff7272492f12b211d3b9c0f90163f48383bb 給加入了 //從實際生產來看,GIT會把此prefix默認爲文件夾的名字 git cat-file -p dc054e0c59565791c70a1f6d6ad7d6676baf0349 100644 blob 765dc741c088b3baef0314a457f74c877a43405b a.txt 100644 blob 7609a432a0ba538cfe3d7bbdb107096c2f010577 b.txt 100644 blob b114c2d776f5dd25dc75a2c7a81f99262d618bc3 c.txt 040000 tree c08670e3f77cae748fbda5c0b83613d5f5995655 test_add_tree
平時咱們都是用 git commit -m "xxxx" 提交了信息, 在這以前,會暫存相關文件的改動, 在提交後,會生成對應的tree對象,返回tree所對應的 sha-1值, 再進行一次 commit-tree 操做,最後會把剛保存的tree對象所對應的sha-1值 賦值給 commit-tree, 即生成了一個commit 對象。用法:
echo '提交信息' | git commit-tree b822ff7272492f12b211d3b9c0f90163f48383bb (對應的tree對象返回的 sha-1值) f7bc39001ff6cb183022234c94aa61ddedee44e0
經過 git cat-file -p f7bc39001ff6cb183022234c94aa61ddedee44e0 獲得:
tree b822ff7272492f12b211d3b9c0f90163f48383bb //該commit對象指向的tree對象 author max.hua <****@****.cn> 1563847402 +0800 //config中指定的user.name信息 committer max.hua <****@****.cn> 1563847402 +0800 //config中指定的user.email信息 first commit
咱們還能夠給某一個commit對象指定它的父commit對象:
echo 'second commit' | git commit-tree b822ff7272492f12b211d3b9c0f90163f48383bb -p f7bc39001ff6cb183022234c94aa61ddedee44e0 (父級commit對象sha-1值) 42e08b70c341b7e60944de6dffc342b77f94f6e4
經過 git cat-file -p 42e08b70c341b7e60944de6dffc342b77f94f6e4獲得:
tree b822ff7272492f12b211d3b9c0f90163f48383bb parent f7bc39001ff6cb183022234c94aa61ddedee44e0 //指向的父級commit對象 author max.hua <****@****.cn> 1563848153 +0800 committer max.hua <****@****.cn> 1563848153 +0800 second commit
想要查看咱們使用管道命令生成的log記錄: git log --stat 42e08b70c341b7e60944de6dffc342b77f94f6e4 ,獲得:
git log --stat 42e08b70c341b7e60944de6dffc342b77f94f6e4 commit 42e08b70c341b7e60944de6dffc342b77f94f6e4 Author: max.hua <****@****.cn> Date: Tue Jul 23 10:15:53 2019 +0800 second commit commit f7bc39001ff6cb183022234c94aa61ddedee44e0 Author: max.hua <****@****.cn> Date: Tue Jul 23 10:03:22 2019 +0800 first commit a.php | 6 ++++++ b.txt | 1 + c.txt | 1 + 3 files changed, 8 insertions(+)
從上面的用法能夠獲得, git commit-tree 生成的 commit對象,只會包含 tree對象,參數選項中沒有能夠指定blob對象的參數。
以下:在測試時,強制使用blob對象的 sha-1值,會出現報錯現象。
echo '第一次提交' | git commit-tree e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea fatal: e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea is not a valid 'tree' object
以上基本上就可歸納平時使用git add 和 git commit 命令時GIT的工做。
平時咱們在使用的時候,使用 git add c.txt 後,把 c.txt 放入了暫存區, 而實際上此時已經生成了blob對象,並保存了相應的sha-1值命名的文件,同時添加到了索引文件中;以後當咱們修改了以前添加到暫存區的文件並使用 git status 查看狀態的時候,GIT會再對文件進行一次 hash運算,若是發現和已存在與索引中的內容產生了變化(sha-1值不一樣),則又會呈現出一個 Modify 狀態。
經過如下命令可查看到 .git/index 文件中的內容,其中存放了每個被追蹤的文件,對應的blob對象最新的sha-1值, 經過這裏便可很直接的判斷出哪一個文件是否被修改,哪些沒有被追蹤了。
git ls-files --stage 100644 45c2647671db4e9d426c2085eba814fea16f6b9a 0 b.txt 100644 177308c04fc55b0d9985a7dfb545f6cebb7ea432 0 c.txt
同上, 使用 git diff後, 會把文件的差別給列出來,而對比對象便是 索引中的內容,並非HEAD指向的內容。 當對某文件執行了 git add 後,以後再進行修改,再使用git diff 查看區別, 你會發現已經存在區別了。也就是說,git diff 其實是把當前文件與索引中的文件進行比較(經過sha-1值比較),當有不一樣的狀況,則列出對應的改變。
使用 git status 後,GIT會對全部文件進行sha-1值計算,若計算到與前面講到的 索引中得對應文件的sha-1值不一樣了,則表明有所改動,則標記爲 Modify,若發現索引中不存在對應文件的sha-1值, 則標記爲 Untracked files。
該命令會生成一個新分支,也就是在 .git/refs/heads裏面生成一個新的文件,文件名爲分支名,若是有前綴feature之類的。則feature是文件夾名,其內是文件名。文件內容爲當前的 commit 對象對應的sha-1值。因此實際上分支,也是一個 commit 對象的引用。只是在GIT中專門有文件記錄了分支名和指向。咱們甚至能夠經過建立文件的方式,直接建立branch。
cd .git/refs/head/ echo 'e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea' > test_aaa
當使用 git checkout 的時候, GIT內部實際上就是把當前的HEAD指針給指向了另外一個分支,而實際上也就是把 .git/HEAD 文件內容修改成切換的分支,而 .git/HEAD 內容指向的就是 .git/refs/heads中的分支,此文件內容又是一個 commit 對象的 sha-1值,因此也就間接指向了某個具體的 commit對象了, 從這個commit對象可獲得它的父級對象,依次類推,便可獲得完整的代碼。
git update-ref HEAD <newvalue>
有時候,咱們在使用PHPStorm的時候,會用到"Annotate", 就是查看本文件的GIT提交記錄,還會查看某個提交下之前的版本的文件,看具體是修改了啥。"Amnotate previous revision",實際上就是作了
git checkout sha-1 文件名 //該命令就會把某文件給恢復到某個提交的時候,不加文件名的話,就是恢復整個項目到某個提交的時候
見如上信息。
使用該命令後,去 .git/logs 下尋找當前分支對應的文件名,文件中的內容即爲每一次提交的信息。
使用git push 是把當前的分支上傳到遠程倉庫,並把這個 branch 的路徑上的全部 commits 也一併上傳。 我認爲實際就是修改了.git中的文件,由於這些文件裏實際上就已經包含了壓縮後的代碼,等你切換分支的時候,GIT會根據這些內容把代碼給檢索出來。
使用 git tag 實際上和 git branch 相似,branch 是指向某一個commit的指針,可是branch會隨着每次提交而移動, 可是tag不會, 當打了tag後, 那這個 tag 對應的commit對象指針就固定了,不會移動了。它和 git branch 同樣,都不會產生blob或 tree 對象, git tag 只會在 .git/refs/tag 下生成一個 tag名的文件,內容爲指向當前commit的sha-1
使用git stash 其實是建立了一個新的commit對象,爲何這麼說呢?在 .git/refs 目錄下,當第一次stash後,會生成一個 stash文件, 內容即爲一個sha-1值, 經過 git cat-file -p查看到具體內容爲一個 commit對象的內容, 還能看到其有兩個 父級 commit, 一個是前一個 git commit 的sha-1值, 一個是執行stash後,新生成的commit。最後把這兩個commit對象做爲父親,再生成一個commit對象存放於stash文件中。
疑問:
須要注意:對commit對象的跟蹤,commit對象能跟蹤到具體哪一次修改,改了哪些具體文件,經過對commit的切換,就能找到某個時間點的文件記錄了。GIT每次提交文件,實際上都是提交的整個文件,而不只僅是修改的部分。因此當咱們執行一些回退操做的時候能回到某個時間點的文件,即直接指定某個commit對象,查到commit對象中包含的各種tree對象和blob對象,把這些對象中壓縮內容給取出來覆蓋當前的同級,同名文件便可;同時新增的,給刪除了。