一直用git,你瞭解git的內部機制嗎?

在工作過程中我們會不可避免的使用Git,但是你知道Git是如何存儲你的文件、如何保存你的提交信息嗎?等等 瞭解這些也便於我們更好的理解和記憶命令,更好的排查問題和使用Git,下面就讓我們來看一下吧~

本文主要依照官網的介紹根據真實項目中的變化總結整理而成

首先,我們要明確 Git 是一個分佈式版本控制系統 其本質是一套 內容尋址文件系統

通俗點說,Git 從核心上來看不過是簡單地存儲鍵值對(key-value)。它允許插入任意類型的內容,並會返回一個鍵值,通過該鍵值可以在任何時候再取出該內容。

ps : 下面所說的SHA-1碼 和 commit_id 是同一種


首先,Git存儲在本地的表現形式

當你在一個新目錄或已有目錄內執行 git init 時,Git 會創建一個 .git 目錄,幾乎所有 Git 存儲和操作的內容都位於該目錄下。如果你要備份或複製一個庫,基本上將這一目錄拷貝至其他地方就可以了。如下圖:

一直用git,你瞭解git的內部機制嗎?

  • info 目錄保存了一份不希望在 .gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可執行文件
  • hooks 目錄保存了客戶端或服務端鉤子腳本
  • config 文件包含了項目特有的配置選項
  • objects 目錄存儲所有數據內容
  • refs 目錄存儲指向數據 (分支) 的提交對象的指針
  • HEAD 文件指向當前分支
  • index 文件保存了暫存區域信息

其中,HEAD 及 index 文件,objects 及 refs 目錄是 Git 的核心部分。


接下來,說一下Git的存儲方式

如上述所說,objects 目錄存儲所有數據內容,objects 目錄下的每一個文件是Git為每份存儲數據內容生成一個文件,取得該內容與頭信息的 SHA-1 校驗和,創建以該校驗和前兩個字符為名稱的子目錄,並以 (校驗和) 剩下 38 個字符為文件命名 (保存至子目錄下)。如下圖:

一直用git,你瞭解git的內部機制嗎?

打開00文件夾可以看到裡面保存的內容:

一直用git,你瞭解git的內部機制嗎?

Git 以一種類似 UNIX 文件系統但更簡單的方式來存儲內容。所有內容以 tree 或 blob 對象存儲,其中 tree 對象對應於 UNIX 中的目錄,blob 對象則大致對應於 inodes 或文件內容。

一個單獨的 tree 對象包含一條或多條 tree 記錄,每一條記錄含有一個指向 blob 或子 tree 對象的 SHA-1 指針,並附有該對象的權限模式 (mode)、類型和文件名信息。

正如 Git的每一次提交都是對代碼倉庫的完整備份,也就是保存了一份代碼倉庫完整的快照所說,每一個commit都是存儲為一個Tree,如下圖:

一直用git,你瞭解git的內部機制嗎?

具體在git中為:

一直用git,你瞭解git的內部機制嗎?

可以看到,目錄作為tree存儲,文件作為blob存儲

之後,我們通過 git cat-file-p命令可以發現存儲是樹型的,也就是對應於git的tree對象,保存的都是指向下一個部分的索引id 如下圖,每一步都是查看的上一步中的某個id:

一直用git,你瞭解git的內部機制嗎?


上述所說每個commit創建一個樹快照,那麼是通過什麼創建的呢?

這就是我們上述說的 用於存儲暫存區信息的index文件了。

通常 Git 根據你的暫存區域或 index 來創建並寫入一個 tree 。因此要創建一個 tree 對象的話首先要通過將一些文件暫存從而創建一個 index 。

這也是為什麼commit前必須要有文件被add到暫存區,如果暫存區為空,commit會報錯停止執行。


這個時候就有一個問題了,我們有多個快照樹,它們指向了你要跟蹤的項目的不同快照,其中也沒有關於誰、何時以及為何保存了這些快照的信息

此時,commit對象就出場了~ 每次commit提交後就會創建一個對應commit 對象,這個對象就是為你保存了這些基本信息的。

一般情況下,一次commit提交就可以理解為創建了一個tree樹,以commit_id為根節點的tree,該樹包含了當前項目的整體快照

當我們使用 git log命令查看提交歷史的時候,就展示了commit對象的一些基本信息,如下圖:

其中:commit 後跟的id就是當前commit快照的樹根節點id 其餘的還包含作者,作者郵箱,創建時間等基本信息


Git每次commit提交會保存項目快照,難道是將所有的文件重新複製一份嗎?

當然不可能,在git的文件系統中,是存在共用文件的。

比如有三次commit提交,產生了三個tree樹,它們在向下引用的時候,如果兩個commit中的整個文件夾或者某個文件沒有改變,這兩個commit的tree會指向同一個對象。對於兩次提交修改了的文件,則會創建一個該文件的一個新的版本的文件,上一次提交指向舊的文件,修改文件的提交指向新版本的文件。

整體情況如下圖:

一直用git,你瞭解git的內部機制嗎?

另外,Git 用 zlib 壓縮文件內容,因此存儲的文件並不會佔用太多空間


瞭解了git整體存儲方式之後,我們再看一下前面提到的存儲指向數據 (分支) 的提交對象的指針的 refs目錄

refs目錄內容如下圖:

一直用git,你瞭解git的內部機制嗎?

首先,也是思考一個問題:在項目開發中,有許多分支,每個分支的提交記錄都不相同,我們也不可能去記住每個commit_id,去執行像 git log1a410e 這樣的命令來查看完整的歷史,這樣的話你就要記得 1a410e 是你最後一次提交併且記得這個id,這樣才能在提交歷史中找到這些對象,git是怎樣的應對這個問題的呢?

這時候,我們需要一個文件來用一個簡單的名字來記錄這些 SHA-1 值,這樣就可以用這些指針而不是原來的 SHA-1 值去檢索了。在 Git 中,稱之為“引用”(references 或者 refs)。

可以在 .git/refs 目錄下面找到這些包含 SHA-1 值的文件。如下圖refs中heads文件下的文件,其中 每個文件存儲的是與文件名同名的分支的最新提交的commit_id:

一直用git,你瞭解git的內部機制嗎?

添加上refs文件夾下的文件後,我們的Git存儲結構就看起來像下圖:

一直用git,你瞭解git的內部機制嗎?


接下來,再思考一個問題,git是怎麼標識當前是在什麼分支,從而找到refs中對應的映射文件獲取SHA-1值呢?

那就是前面所說的HEAD文件了,我們打開文件可以看到以下內容:ref:refs/heads/test_branch

這裡標識的是當前指向的是test_branch分支,並且指定了要是用的映射文件的路徑,這樣就解決了上述問題,是不是特別簡單~


上述已經介紹了Git的三個主要類型:tree樹、commit對象、HEAD。下面我們說一下Git中另外一個重要的東西:Tag(標籤)Tag 對象比較簡單,Tag對象非常像一個 commit 對象---包含一個標籤,一組數據,一個消息和一個指針。最主要的區別就是 Tag 對象指向一個 commit 而不是一個 tree。它就像是一個分支引用,但是不會變化,永遠指向同一個 commit,僅僅是為了提供一個更加友好的名字。

好了,通過介紹了git的核心組成元素 HEAD及index文件,objects及refs目錄 , 你應該會對git的存儲和一些機制有一個簡單的整體瞭解,這對我們更好的理解git命令和更好的使用git是有幫助的。希望本片文章會對大家有些許幫助~


分享到:


相關文章: