GIT原理介紹

  • 2019 年 10 月 12 日
  • 筆記

Git 是一套內容尋址文件系統。很不錯。不過這是什麼意思呢? 這種說法的意思是,Git 從核心上來看不過是簡單地存儲鍵值對(key-value)。它允許插入任意類型的內容,並會返回一個鍵值,通過該鍵值可以在任何時候再取出該內容。

我們都知道當我們初始化一個倉庫的時候,也就是執行以下命令後,文件夾內會生成一個.git文件夾,

git init

內部會包含,以下文件夾。

file

  • hooks //鉤子文件夾,內部文件實際上就是一些特定時間觸發的shell腳本,我們可以簡單的做一個部署系統,每次提交特定tag的時候,則部署最新的代碼到服務器。
  • objects //真正的內容存放的文件夾,下面重點講下這裡。
  • refs //refs目錄存放了各個分支(包括各個遠端和本地的HEAD)所指向的commit對象的指針(引用),也就是對應的sha-1值;同時還包括stash的最新sha-1值
  • config //git配置信息,包括用戶名,email,remote repository的地址,本地branch和remote branch的follow關係
  • HEAD //存放的是一個具體的路徑,也就是refs文件夾下的某個具體分支。意義:指向當前的工作分支。項目中的HEAD 是指向當前 commit 的引用,它具有唯一性,每個倉庫中只有一個 HEAD。在每次提交時它都會自動向前移動到最新 的 commit
  • index //存放的索引文件,可使用 git ls-files –stage 查看。應該zlib加密後的,PHP可使用gzdeflate()函數

這是objects文件夾,可以看到都是些數字和字符,實際上就是十六進制數。

file
下圖是進入00文件夾後所有文件。

file

認識下GIT對象: blob對象tree對象commit對象

1.創建blob對象

  下面我們直接上底層命令, 運行此命令後,會在 .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算法是一樣的道理。

注意:此hash值就像是GIT的指針,能唯一對應某一個具體的內容或提交,hash值作為尋址作用,不作為內容存儲用,具體的文件內容存儲方式是GIT更底層的存儲方式決定。(sha-1和md5一樣,均是不可逆的)

通過Linux find 命令查看所有已存儲的hash文件:

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 3aaa

2.創建tree對象

  上面說的創建blob對象,僅僅只是對某一個文件進行的計算與存儲,而我們實際項目中,可能每一次操作都是好幾個,甚至十幾個文件一起,那如何才能把他們組織到一起,這就是 tree 對象的作用了。
要創建tree對象,需要使用 update-indexwrite-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

3.創建commit對象

  平時我們都是用** 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

4.應用

以上基本上就可概括平時使用git add 和 git commit 命令時GIT的工作。

  1. 保存已修改文件成blob格式對象: git hash-object -w 各個文件
  2. 更新索引: git update-index –add 各個文件名 或者 git update-index –add –cacheinfo mode sha-1 文件名 或者 git read-tree –prefix=test sha-1(某個tree的sha-1) ,作用在於把某個tree讀入索引中
  3. 創建樹對象: git write-tree
  4. 最後創建commit對象: git commit-tree sha-1 -m "提交信息" 或者 echo "提交信息" | git commit-tree sha-1 -p 父級sha-1

4.1 git add

  平時我們在使用的時候,使用 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

4.2 git diff

  同上, 使用 git diff後, 會把文件的差異給列出來,而對比對象即是 索引中的內容,並不是HEAD指向的內容。 當對某文件執行了 git add 後,之後再進行修改,再使用git diff 查看區別, 你會發現已經存在區別了。也就是說,git diff 實際上是把當前文件與索引中的文件進行比較(通過sha-1值比較),當有不同的情況,則列出對應的改變。

4.3 git status

  使用 git status 後,GIT會對所有文件進行sha-1值計算,若計算到與前面講到的 索引中得對應文件的sha-1值不同了,則代表有所改動,則標記為 Modify,若發現索引中不存在對應文件的sha-1值, 則標記為 Untracked files。

4.4 git branch 分支名

  該命令會生成一個新分支,也就是在 .git/refs/heads裏面生成一個新的文件,文件名為分支名,如果有前綴feature之類的。則feature是文件夾名,其內是文件名。文件內容為當前的 commit 對象對應的sha-1值。所以實際上分支,也是一個 commit 對象的引用。只是在GIT中專門有文件記錄了分支名和指向。我們甚至可以通過創建文件的方式,直接創建branch。

cd .git/refs/head/  echo 'e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea' > test_aaa

4.5 git checkout 某分支

  當使用 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 文件名      //該命令就會把某文件給恢復到某個提交的時候,不加文件名的話,就是恢復整個項目到某個提交的時候

4.6 git commit -m "提交信息"

  見如上信息。

4.7 git log

  使用該命令後,去 .git/logs 下尋找當前分支對應的文件名,文件中的內容即為每一次提交的信息。

4.8 git push

  使用git push 是把當前的分支上傳到遠程倉庫,並把這個 branch 的路徑上的所有 commits 也一併上傳。 我認為實際就是修改了.git中的文件,因為這些文件里實際上就已經包含了壓縮後的代碼,等你切換分支的時候,GIT會根據這些內容把代碼給檢索出來。

4.9 git tag [version name]

  使用 git tag 實際上和 git branch 類似,branch 是指向某一個commit的指針,但是branch會隨着每次提交而移動, 但是tag不會, 當打了tag後, 那這個 tag 對應的commit對象指針就固定了,不會移動了。它和 git branch 一樣,都不會產生blob或 tree 對象, git tag 只會在 .git/refs/tag 下生成一個 tag名的文件,內容為指向當前commit的sha-1

4.10 git stash

  使用git stash 實際上是創建了一個新的commit對象,為什麼這麼說呢?在 .git/refs 目錄下,當第一次stash後,會生成一個 stash文件, 內容即為一個sha-1值, 通過 git cat-file -p查看到具體內容為一個 commit對象的內容, 還能看到其有兩個 父級 commit, 一個是前一個 git commit 的sha-1值, 一個是執行stash後,新生成的commit。最後把這兩個commit對象作為父親,再生成一個commit對象存放於stash文件中。
file

疑問:

  1. git commit-tree的時候,是怎麼指定父級commit對象的,以什麼為參考,才能指定對應的父級commit對象。我從平時工單的log記錄來看,有些commit對象有兩個父級commit對象(可能是合併操作的時候自動生成的commit對象),但不一定就是前一個commit。(git stash 有兩個父級對象)

  2. 我們都知道git stash 存的是一個棧的結構,但是 .git/refs/stash 文件里 只有一個sha-1值,只對應一個commit對象,我查看了commit對象的具體內容,他的兩個父親均不是我前一個創建的stash對應的commit對象,不知道這個棧的結構怎麼來的。
  3. 看某項目的樹結構,會發現,每一個commit對象內部對應的 tree,都是一整個項目,而不是某一個文件或者某幾個文件夾,這就解決了我的疑惑, 每次只需 git update-index –add 後,我想新創建一個 tree對象, tree對象內部一直會存在之前加入index中的blob對象。 那麼GIT實際上就是每一個commit,都應該能從tree和 p-tree上追溯到整個項目文件。

  4. 在手動創建分支的過程中,發現在執行 git init後,看起來你是在master分支, 但是實際執行 git branch,看不到任何輸出,這說明在這個時候實際上master分支是沒有創建的,必須要有第一次提交後,master分支才會創建,因為只有這樣 , .git/refs/head/master 文件中才有可寫的 commit 對象的 sha-1值。

需要注意:對commit對象的跟蹤,commit對象能跟蹤到具體哪一次修改,改了哪些具體文件,通過對commit的切換,就能找到某個時間點的文件記錄了。GIT每次提交文件,實際上都是提交的整個文件,而不僅僅是修改的部分。所以當我們執行一些回退操作的時候能回到某個時間點的文件,即直接指定某個commit對象,查到commit對象中包含的各類tree對象和blob對象,把這些對象中壓縮內容給取出來覆蓋當前的同級,同名文件即可;同時新增的,給刪除了。