想晉級高階工程師只知道表面是不夠的!Git內部原理介紹

qcloud發表於2019-01-18

> 本文由雲 + 社群發表

> 作者:騰訊工蜂使用者:王二衛

從不一樣的視角瞭解 git,以便更好的使用 git

一、git & git 版本庫認識

git 是一個內容定址的檔案系統,其核心部分是一個簡單的鍵值對資料庫 (key-value data store),可以向該資料庫插入任意型別的內容,它會返回一個 40 位長的雜湊鍵值。並在此基礎上提供了一個版本控制系統的使用者介面。

git 版本庫其實只是一個簡單的資料庫,其中包含所有用來維護與管理專案的修訂版本和歷史資訊。其不同於 subversion,git 版本庫不僅提供版本庫中所有檔案的完整副本,還提供版本庫本身的副本。在 git 版本庫中,git 維護兩個主要資料結構:物件庫 (object store),索引 (index)。

從整體來看,一個專案的 git 倉庫,就如一張帶節點的漁網(該漁網是一張有向網),隨著專案的不斷推進,該漁網也將不斷的向四周擴散。

漁網上的節點就像一個個的提交,從某一個正常的節點都能漫遊至專案最開始的起點。而分支就如該網上不同節點上的一個特殊標記,分支的演變就是該標記不斷的移至其他節點。 分支的合併,根據合併方式的不同,使得這一張網的交叉緊密度越來越高。

1.1git 物件型別

物件庫是 git 版本庫實現的心臟,包含四種型別:

塊 (blob,binary lare object),檔案的每一個版本表示為一個塊。一個 blob 被視為一個儲存任意資料,且內部結構被程式忽略的變數或檔案的黑盒。一個 blob 儲存一個檔案的資料,但不包含任何關於這個檔案的後設資料 (Metadata,描述資料的資料)。

目錄樹 (tree), 一個目錄樹物件代表一層目錄資訊。它記錄 blob 識別符號、路徑名和在一個目錄裡所有檔案的一的後設資料。它也可以遞迴引用其他目錄樹或子樹物件,從而建立一個包含檔案和子目錄的完整層次結構。

提交 (commit),一個提交物件儲存版本庫中每一次變化的後設資料,每一個提交物件指向一個目錄樹物件,這個樹物件在一張完整的快照中補貨提交時版本庫的狀態。

標籤 (tag) ,一個標籤物件分配一個可讀的名字給一個特定的物件,通常是一個提交物件。

為了有效的利用磁碟空間和網路頻寬名,git 把物件壓縮並儲存在打包檔案 (pack file) 裡,這些檔案也在物件庫裡。

1.2 索引

索引是一個臨時的、動態的二進位制檔案,不包含任何檔案內容,它僅僅追蹤你想要提交的那些內容。使得開發的推進與提交的變更之間能夠分離開來。

1.3 引用

引用 (ref) 是一個儲存 SHA-1 值的檔案,該檔案的名字指標來替代原始的 SHA-1 值,一般指向提交物件。本地分支名稱、遠端跟蹤分支名稱和標籤名都是引用。

.git/refs
.git/refs/heads
.git/refs/tags

1.3.1 建立一個引用

$ echo “1a410efbd13591db07496601ebc7a059dd55cfe9” > .git/refs/heads/master

現在可以通過新建的引用來代替 SHA-1 的值: $ git log —pretty=oneline master 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

不提倡直接編輯引用檔案,可以通過update-ref更新某個引用 $ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

比如新建一個分支(git 分支的本質:一個指向某一系列提交之首的指標或引用) $git update-ref refs/heads/feature-zhangsan cac0ca

1.3.2 符號引用

符號引用 (symbolic reference),間接指向 git 物件,其實際也是一個引用,不像普通引用那樣包含一個 SHA-1 值,它是一個指向其他引用的指標。 git自動維護幾個用於特定目的的特殊符號引用,這些引用可以在使用提交的任何地方使用。

  • HEAD 始終指向當前分支的最近提交,不像普通引用那樣包含一個 如: $ cat .git/HEAD ref: refs/heads/master

若執行 $ git checkout test,git 會這樣更新 HEAD 檔案 ref:refs/heads/test

  • ORIG_HEAD 某些操作 (如:merge、reset),會把調整為新值之前的先前版本的 HEAD 記錄到 OERG_HEAD 中,只用其可以恢復或回滾之前的狀態或做個比較
  • FETCH_HEAD git fech 命令將所有抓取分支的頭記錄到.git/FETCH_HEAD 中
  • MERGEHEAD 正在合併進 HEAD 的提交

1.3.3 遠端引用

如果你新增了一個遠端版本庫並對其執行過推送操作,Git 會記錄下最近一次推送操作時每一個分支所對應的值,並儲存在 refs/remotes 目錄下。 如:$cat .git/refs/remotes/origin/master ca82a6dff817ec66f44342007202690a93763949 發現新增的遠端 origin 遠端庫的 master 分支鎖對應的 SHA-1 值,就是最近一次與伺服器通訊時 master 分支所對應的 SHA-1 值。 遠端引用和分支(位於 refs/heads 目錄下的引用)之間最主要的區別在於,遠端引用是隻讀的。 雖然可以git checkout 到某個遠端引用,但是 Git 並不會將 HEAD 引用指向該遠端引用。 因此,你永遠不能通過commit 命令來更新遠端引用。 Git 將這些遠端引用作為記錄遠端伺服器上各分支最後已知位置狀態的書籤來管理。

二、git 底層命令

  • cat-file 展示 git 倉庫物件實體的型別、大小和內容
  • ls-remote 顯示遠端庫資訊
  • ls-files 顯示由工作目錄中新增到快取中的檔案的相關資訊
  • ls-tree 列出樹物件內容
  • read-tree 將給出的樹寫入索引但不寫入快取
  • write-tree 按照索引區內容建立樹物件
  • symbolic-ref 同步引用資訊
  • update-index 更新樹物件內容至索引

三、.git 結構說明

  • HEAD 指示目前被檢出的分支
  • index 儲存暫存區資訊
  • config* 包含專案特有的配置選項
  • description 僅供 gitweb 程式使用,使用者一般不需要關注。
  • hooks 包含客戶端和服務端的鉤子
  • info 包含全域性排除 (global excude) 檔案,存放那些不希望被記錄在.gitignore 中的忽略模式
  • objects 儲存所有資料內容
  • refs 儲存指向資料 (分支) 的提交物件的指標

四、git 版本演變

準備工作:建立一個沒有任何檔案的 git 初始庫 $ git init test Initialized empty Git repository in /data/work/test/test/.git/

4.1 git 資料儲存演示

  • hash-object 儲存任意型別資料至資料庫,並返回 hash 鍵值

$ echo ‘test conten’ | git hash-object -w —stdin

d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w 執行寫入資料庫操作,若不指定該選項,只會返回hash,不會寫入資料庫。

--stdin 標準輸入輸出讀取

預設存入是blob型別,通過-t 引數指定

$ find .git/objects/ -type f .git/objects//d6/870460b4b4aece5915caf5c68d12f560a9fe3e4

  • 一個檔案對應一條內容,這個內容的名稱以該檔案內容加上特定頭部資訊一起的 sha-1 校驗和。

頭部資訊-物件型別(blob 或 tree 或 commit)+ 一個空格 + 資料內容長度 + 一個空位元組 git 會通過 zlib 將檔案內容和頭部資訊拼接一起的內容進行壓縮寫入磁碟某個物件,並用計算出的 sha-1 值的前兩個字串作為目錄名稱,後 38 個字串作為子目錄內檔案的名稱。

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4

test content

4.2 簡單版本控制演示

4.2.1 建立初始版本

$ echo ‘version 1’ > test.txt

$ git hash-object -w ./test.txt 83baae61804e65cc73a7201a7252750c76066a30

4.2.2 更新版本

$ echo ‘version 2’ > test.txt

$ git hash-object -w ./test.txt 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

此時資料庫已經儲存了 test.txt 兩個不同的版本,如下:

$ find .git/objects/ -type f .git/objects//1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects//83/baae61804e65cc73a7201a7252750c76066a30

可以通過cat-file -p檢視內容,以上都是資料 (blob) 物件。可以使用 cat-file -t檢視。

4.3 樹物件引入

樹對像 (tree object) 解決檔名和目錄儲存問題。一個樹物件包含了一條或多條樹物件記錄,每條記錄包含一個指向資料物件或子樹物件的 sha-1 指標,以及相應的模式/型別/檔案資訊。

如下所示:


img

$ git cat-file -p master^{tree}

100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README 100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb

master^{tree}指向 master 分支最新提交所指的樹物件。 資料物件幾種型別

  • 100644: 表示一般檔案
  • 100755: 表示可執行檔案
  • 120000: 表示 指標
  • —add: 將未跟蹤檔案加入快取區
  • —cacheinfo 將資料物件檔案加入工作區

4.3.1 將檔案加入暫存區

$ git update-index —add —cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt

4.3.2 生成樹物件

建立第一個樹 $ git write-tree 將暫存區內容生成一個樹物件,並輸出樹物件 SHA-1 d8329fc1cc938780ffdd9f94e0d364e0ea74f579

4.3.3 演變一個複雜的樹

$ echo ‘new file’ > new.txt

$echo ‘test file2’ > test.txt

$git update-index —cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

$ git update-index test.txt

$ git update-index —add new.txt

建立第二個樹

$ git write-tree 0155eb4229851634a0f03eb265b69f5a2d56f341

$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

此時發現,第一個樹丟了,並沒有跟第一個樹有關係,通過 read-tree 進行連結 $ git read-tree —prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579

$ git write-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614

$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

4.3.4 檢視我們生成的樹

img

4.4 提交物件引入

通過 commit 物件將這些樹物件串起來。 建立第一個提交 $ echo ‘first commit’ | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 fdf4fc3344e67ab068f836878b6c4951e3b15f3d

建立第二個提交 $ echo ‘second commit’ | git commit-tree 0155eb -p fdf4fc3 cac0cab538b970a37ea1e769cbbde608743bc96d

建立第三個提交 $ echo ‘third commit’ | git commit-tree 3c4e9c -p cac0cab 1a410efbd13591db07496601ebc7a059dd55cfe9

版本庫目錄變化` **$ find .git/objects -type f** .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # ‘test content’ .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 `提交版本圖

img

沒有執行read-tree
$ git log --stat 92387
commit 923879712b02f980a2edbe1cee315d883ee72503
Author: erweiwang <erweiwang@tencent.com>
Date:   Tue Jul 17 15:55:53 2018 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit e624badd39a25484a08ae74231be65ea50a0fe32
Author: erweiwang <erweiwang@tencent.com>
Date:   Tue Jul 17 15:54:20 2018 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

五、包檔案

Git 最初向磁碟中儲存物件時所使用的格式被稱為 “鬆散(loose)” 物件格式。 但是,Git 會時不時地將多個這些物件打包成一個稱為 “包檔案(packfile)” 的二進位制檔案,以節省空間和提高效率。 當版本庫中有太多的鬆散物件,或者你手動執行 git gc 命令,或者你向遠端伺服器執行推送時,Git 都會這樣做。

git 打包物件時,會查詢命名及大小相近的檔案,並只儲存檔案不同版本之間的差異內容和檔案最新版本的完整內容。

六、引用規格

引用規格的格式由一個可選的 + 號和緊隨其後的 : 組成,其中 是一個模式(pattern),代表遠端版本庫中的引用; 是那些遠端引用在本地所對應的位置。 + 號告訴 Git 即使在不能快進的情況下也要(強制)更新引用。

[remote "origin"]
  url = https://github.com/schacon/simplegit-progit
  fetch = +refs/heads/*:refs/remotes/origin/*

如果想讓 git 每次只拉取遠端 master 分支,而不是所有分支,可以將引用規格那一行修改為: fetch = +refs/heads/master:refs/remotes/origin/master

七、git clone 程式碼庫過程

執行 git clone 後,

  • 拉取 info/refs 檔案 => GET info/refs ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
  • 確定 HEAD 引用,明確檢出至工作目錄的內容 => GET HEAD ref: refs/heads/master 以上說明完成抓取後需要檢出 master 分支
  • 從 info/refs 檔案中所提到的 ca82a6 提交物件開始 => GET objects/ca/82a6dff817ec66f44342007202690a93763949 (179 bytes of binary data)
  • 根據 ca82a6 提取的的父提交物件和樹物件開始遍歷整個完整版本庫。

在遍歷過程中,若是未能直接找到(非鬆散物件)某些物件,會去替代版本庫或某個包檔案獲取。

八、git 推送遠端庫過程

為了上傳資料至遠端,Git 使用 send-pack 和 receive-pack 程式。 執行在客戶端上的 send-pack 程式連線到遠端執行的 receive-pack 程式。

九、擴充套件知識

9.1 維護

git gc —auto //整理鬆散物件並放置包檔案,將多個包檔案合併為一個大的包檔案,移除與任何提交不相關的陳舊物件

9.2 資料恢復

  • 確定需要恢復的版本 git reflog 檢視 git 默默記錄的每一次你改變的 HEAD 的值。 git log -g 可以詳細的檢視引用日誌中各個版本的資訊,風方便確定要恢復的提交。 如下所示 commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Reflog: HEAD@{0} Reflog message: updating HEAD third commit commit ab1afef80fac8e34258ff41fc1b867c702daa24b Reflog: HEAD@{1} Reflog message: updating HEAD modified repo.rb a bit
  • 建立用於恢復的臨時分支

$ git branch recover-branch ab1afef

  • 通過 git fsck 檢查資料庫的完整性(當 reflog 也不存在需要恢復的版本)

當引用日誌所在目錄.git/logs/ 被不小心清空時

$ git fsck —full Checking object directories: 100% (256/256), done. Checking objects: 100% (18/18), done. dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9 dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

9.3 移除物件

該操作使用須謹慎,會導致提交歷史不被重寫。應用場景,必須對已上庫的某些檔案(因檔案太大或保密資訊)進行徹底移除可以使用。

  • 定位出問題檔名 保密檔案一般是已知的,若是誤提交的檔案較大需要刪除,但又不知道是哪些檔案,且又執行過 git gc 可以通過類似以下命令定位: $ git verify-pack -v .git/objects/pack-29…69 .idx | sort -k 3 -n | tail -3 dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696 82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438 $ git rev-list —objects —all | grep 82c99a3 82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
  • 從過去所有樹中移除這個檔案 檢視哪些提交對這個檔案做過改動 $ git log —oneline —branches — git.tgz dadf725 oops - removed large tarball 7b30847 add git tarball 從 7b30847 之後的所有提交歷史中完全移除該檔案 $ git filter-branch —index-fileter ‘git rm —ignore-unmatch —cached git.tgz’ — 7b30847^… Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2) rm ‘git.tgz’ Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2) Ref ‘refs/heads/master’ was rewritten --index-filter 只修改暫存區或索引中的檔案 --cached 需要從索引中移除,使得在執行過濾器是,並不會將每個修訂版本檢出到磁碟 --ignore-unmatch 如果嘗試刪除的模式不存在時,不提示錯誤 filter-branch 用於指定從那個提交以來的歷史
  • 重新打包日誌 執行上面操作,本地歷史不在包含那個檔案的引用,但是,引用日誌和 .git/refs/original 通過 filterbranch 選項新增的新引用中還存有對這個檔案的引用,必須移除它們後重新打包資料庫。 $ rm -Rf .git/refs/original $ rm -Rf .git/logs/** $ git gc
  • 徹底移除 $ git prune --expire now $ git count-objects -v

此文已由騰訊雲 + 社群在各渠道釋出

獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號

更多原創文章乾貨分享,請關注公眾號
  • 想晉級高階工程師只知道表面是不夠的!Git內部原理介紹
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章