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

本文由雲+社區發表

作者:騰訊工蜂用戶:王二衛

從不一樣的視角瞭解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 <[email protected]>
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 <[email protected]>
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

此文已由騰訊雲+社區在各渠道發佈

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章