GIT原理介紹

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

咱們都知道當咱們初始化一個倉庫的時候,也就是執行如下命令後,文件夾內會生成一個.git文件夾,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文件夾,能夠看到都是些數字和字符,實際上就是十六進制數。shell

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算法是同樣的道理。測試

<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

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格式對象: <font color="red">git hash-object -w 各個文件</font>
  2. 更新索引: <font color="red">git update-index --add 各個文件名 或者 git update-index --add --cacheinfo mode sha-1 文件名 或者 git read-tree --prefix=test sha-1(某個tree的sha-1) ,做用在於把某個tree讀入索引中</font>
  3. 建立樹對象: <font color="red">git write-tree</font>
  4. 最後建立commit對象: <font color="red">git commit-tree sha-1 -m "提交信息" 或者 echo "提交信息" | git commit-tree sha-1 -p 父級sha-1</font>

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. <font color="red">git commit-tree的時候,是怎麼指定父級commit對象的,以什麼爲參考,才能指定對應的父級commit對象。我從平時工單的log記錄來看,有些commit對象有兩個父級commit對象(多是合併操做的時候自動生成的commit對象),但不必定就是前一個commit。(git stash 有兩個父級對象) </font>
  2. <font color="red">咱們都知道git stash 存的是一個棧的結構,可是 .git/refs/stash 文件裏 只有一個sha-1值,只對應一個commit對象,我查看了commit對象的具體內容,他的兩個父親均不是我前一個建立的stash對應的commit對象,不知道這個棧的結構怎麼來的。</font>
  3. <font color="red">看某項目的樹結構,會發現,每個commit對象內部對應的 tree,都是一整個項目,而不是某一個文件或者某幾個文件夾,這就解決了個人疑惑, 每次只需 git update-index --add 後,我想新建立一個 tree對象, tree對象內部一直會存在以前加入index中的blob對象。 那麼GIT實際上就是每個commit,都應該能從tree和 p-tree上追溯到整個項目文件。</font>
  4. <font color="red">在手動建立分支的過程當中,發如今執行 git init後,看起來你是在master分支, 可是實際執行 git branch,看不到任何輸出,這說明在這個時候實際上master分支是沒有建立的,必需要有第一次提交後,master分支纔會建立,由於只有這樣 , .git/refs/head/master 文件中才有可寫的 commit 對象的 sha-1值。</font>

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

相關文章
相關標籤/搜索