深入理解 Git

__zkhCreator__發表於2019-02-19

深入理解 Git

Git 使我們日常使用的開發工具,用於程式碼的版本管理,但是我們常用的各種命令 git add, git commit, git push, git pull 等等究竟是啥樣子,帶著好奇心,趁著空重新讀了下 Git 的官方文件。發現了一些除了剛剛提到的高階命令以外的低階命令,以及 Git 究竟是如何運轉的。(當然不是原始碼解讀)

基本概念

git 對於檔案的儲存位置進行了3層分割,用於不同狀態下的檔案。我們可以理解為3個箱子。

  • Work Directory
  • Index/Stage
  • Git Directory

Work Directory(俗稱工作區)

工作區就是我們檔案正常編輯的地方,我們在這進行程式碼的編寫。當我們完成之後,我們會發現我的的 cli 會提示我們這些檔案被修改,從而方便我們區分哪些檔案被修改了,哪些沒有被修改。

Index/Stage(暫存區)

暫存區就是我們在執行了 git add 之後,檔案存放的地方,說明這些檔案準備進行提交了。之所以有這麼個地方,就是能夠將需要提交的內容進行一次性提交,從而避免了每次提交都存在歧義的情況。同時也方便我們對將要提交的地方進行調整。

Git Directory

Git 的區域,用於將歷史記錄進行統一的進行管理。方便後續迭代的時候進行一定的調整。git 中使用檔案的形式進行管理。

前面所說可能比較抽象,用一個簡單的話來說,當我們大腦中有一個好的 idea 的時候,我們的大腦就是一個 work directory,這個時候為了避免忘記,或者它丟失,我們需要將他用筆寫下來,這個時候紙就是 Index/Stage。同時我們會進行不斷的思考,那麼就是不斷的修改 work directory 的內容,並不斷 add 到 Index/Stage 當中。當這個 idea 整理清楚的時候,我們需要將這張紙整理起來,就像是放到檔案室,這個時候檔案室就是我們的 Git Directory

Git 基本物件

說完這些,就可以進入我們的正題了。

Git 實際上有自己對於物件的定義。 Git 當中存在 3種物件,blobtreecommit。而所有物件都有自己的身份標記 —— SHA-1碼。(對於 SHA-1 的解釋可以參見維基百科

blob

blob 就是我們所說的簡單檔案,在谷歌中的翻譯 blob 指的是大的二進位制檔案。在 git 中你可以認為他就是檔案物件。

tree

既然有了檔案物件,那麼需要對於檔案物件進行層級排列。這個時候就需要引入 tree 的概念了。Git 模仿了 Unix 的檔案管理體系,不過他沒有系統的那麼沉重,相對而言更加輕量級,僅僅包含了裡面有哪些檔案。

commit

當檔案結構已經確定了,剩下的就是將這些內容進行提交了。每次當我們將 tree 提交的時候,就建立了一個 commit,而有 SHA-1 的 commit 便決定了 Git 的整個體系,能夠通過 SHA-1 進行追蹤。

在清楚了上面的幾個關鍵點之後,就可以進入我們的正題了。從我們建立 git 到提交完成整個工程,具體發生了些什麼。

深入理解 Git

建立 git

當我們完成了 git init 之後,我們建立了一個包含了 .git 的檔案目錄

➜  test git:(master) la
total 0
drwxr-xr-x  10 zkhcreator  staff   320B Jan 31 18:40 .git
複製程式碼

這個時候我們會發現 .git 檔案下的結構如下:(附帶功能描述)

.git
├── HEAD    (用於管理 HEAD 所在的位置)
├── branches    (有哪些分支)
├── config  (配置檔案)
├── description (對於這個工程的描述檔案)
├── hooks   (本地的 git 的所有鉤子的配置)
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude (用於在 git 層面告知 git 哪些檔案不需要進行版本控制)
├── objects (所有 git 物件)
│   ├── info    (git 物件的資訊,物件以及他的 SHA-1 儲存的地方)
│   └── pack    (git 執行 gc 操作打包後的儲存地)
└── refs    (所有引用(references)的地方)
    ├── heads   (所有分支頭所在的位置)
    └── tags    (所有標籤所在的位置)
複製程式碼

上圖的檔案樹結構構成了一個簡單的 git

建立 blob

按照往常的思路,我們需要建立檔案並執行 git add filename 但是這裡我們不用這些高階命令,轉而使用低階命令。

首先我們建立一個檔案 echo 'test1' | git hash-object -w --stdin 這樣就建立完成了一個檔案,其中 -w 直接將資料寫入資料庫(準確的說是檔案當中,因為 git 沒有資料庫這個概念,都是以檔案進行儲存的)當中,--stdin 表示使用標準輸入輸出資料流格式進行讀取。

此時我們會發現他返回了一條 SHA-1: a5bce3fd2565d8f458555a0c6f42d0504a848bd5,同時再次列印我們 .git 中的檔案路徑,我們會發現他的 objects 路徑下多了 a5/bce3fd2565d8f458555a0c6f42d0504a848bd5 這樣的一個檔案,這個和生成的 SHA-1 有一些微妙的聯絡,實際上 git 取了 git 的前兩位作為一級目錄,並將後面的位數作為檔名。

如果我們嘗試去開啟這個檔案,我們會發現他是一堆亂碼,因為 git 已經幫我轉我一次了,如果需要讀取裡面的內容,我們需要使用 git cat-file -p a5bce3fd2565d8f458555a0c6f42d0504a848bd5 去進行展示,很明顯,他就會輸出我們剛剛通過資料流輸入輸出的 test1。上面命令中的 git cat-file -p 就是用來列印 SHA-1 物件的實際內容。

有人可能會說,我們 git 不都是新增檔案的麼?你這樣直接寫資料庫算什麼?

那我們來新建一個檔案來重複以上操作。我們執行以下命令

$ echo "version 1" > just_for_test.txt
$ git hash-object -w just_for_test.txt
83baae61804e65cc73a7201a7252750c76066a30
複製程式碼

這個時候我們發現又多了一個檔案 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30,然後我們再執行一次寫入操作,嘗試去修改裡面的內容

echo "version 2" > just_for_test.txt
git hash-object -w just_for_test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
複製程式碼

這個時候我們會發現,結構路徑除了我們剛剛生成的 83baae61(為了方便理解,取前幾位),還有一條新的 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a。很明顯,檔案更新的情況,已經被寫入到 git 的檔案中。

但是我們執行 git status 會發現,檔案還沒有提交,說明 git 在新增到 commit 前,會將檔案進行快取。(不過什麼時候進行儲存資料庫的,可能需要看下原始碼)

既然已經有了 sha-1 並且我們已經將檔案儲存到資料庫,這個時候我們做資料恢復操作就很簡單了。只需要執行 git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt 即可將前面的 version 1 恢復到檔案目錄當中。

這個時候如果我們重新執行 git has-object -w just_for_test.txt 我們會發現目錄結構沒有發生改變,因為 git 對於檔案進行儲存是基於檔案內部的內容的,和其他的東西並無關係。

前面提到,git 當中總共有3種型別,但是 SHA-1 是通的,所以,有時候我們需要確認對應的 SHA-1 是什麼型別,我們就可以使用 git cat-file -t SHA-1 進行列印,很明顯,這個檔案是 blob 物件。

建立 tree

如果單純的檔案,肯定是不能構成樹目錄的,就像我們腦子中的點子,順序很亂,只有當我們寫下來的時候,才能將它的順序理清楚。所以只有當需要提交到 stash 才能確定誰再哪。

那麼我們需要首先給檔案提供一個暫存,故需要執行 git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 just_for_test.txt 將在資料庫當中指定的檔案進行儲存到暫存區。--add 標識將工作區域的檔案註冊到暫存區當中,但是如果已經新增,則不會重複新增。--cacheinfo 標識需要新增的資料的詳細資訊,比如說新增指定檔案,以及他們的許可權。**其中的檔名是放到快取中的檔名,一般我們通過 add 進去的 work directory 和 stage 是一致的,但是可以手動指定不同,SHA-1 用來確定檔案的內容具體是啥。**此時我們執行 git status 會發現,檔案已經被新增到暫存區了。

然後通過 git write-tree 將當前暫存區域的物件寫到樹物件當中。這個時候我們會發現它又給了我們一個 SHA-1:ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 這個是生成的樹物件的識別符號,我們可以通過 git cat-file -t SHA-1 來獲得它的型別就是我們想要的 tree。 當然通過搜尋 $ find .git/objects/ff 也能在 objects 中很容易找到我們需要的檔案。

那麼樹物件能進行巢狀麼?答案當然是肯定的。

我們可以通過 git read-tree --prefix=bak ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 來將原有的 tree 進行備份到當前的 tree 下面,由於老的 tree 是 object 物件,所以當你讀取出來之後,你在 git status 下你會發現他被寫入暫存區了,但是在 work directory 當中是需要刪除的,也就是這個檔案本身是不存在的,這都是因為無中生有,或者說強制讀取物件的造成的。

建立 commit

既然完成了 stage 的寫入,最後一步就是需要將當前的 tree 進行提交,並建立提交物件。這時候只需要執行 echo "first commit" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa 這個時候,我們通過 git log 會發現,我們已經提交了 commit。但是 git status 還是有檔案沒有更新。這個原因也很簡單,就是因為我們沒有建立分支,這就導致 commit 不知道該提交到哪裡。 只需要 git branch new_branch_name $(echo "commit message" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa) 即可。

最後

當我們提交完上面的操作之後,也就是完成了我們最基本的常用命令的底層的操作。這個時候我們重新列印下我們的 .git 檔案。會發現裡面多了好多東西,除了我們自己的 objects 還有很多剛剛註釋裡面提到的,但是文章當中沒有提到的內容。 比如說:

  • COMMIT_EDITMSG:最近一次 commit 的內容
  • HEAD:當前的頭的位置,ref: refs/heads/master 即引用的位置為 refs/heads/master 資料夾中的內容。
  • INDEX:放置過 index 之後,即執行 git update-index 之後 index 的內容,編碼過後的檔案,不能容易的看懂。
  • logs/HEAD:HEAD 切換的日誌檔案。
  • logs/refs/heads/master: master 的 git 修改日誌情況。左邊為 0,右邊有值壽命這個是新新增的。相反右邊為 0說明檔案是移除的。同時數字前4位標識所有內容長度的十六進位制數,主要方便上傳下載的智慧協議的同步操作。具體內容可見 Git 傳輸協議
0000000000000000000000000000000000000000 d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f zkhCreator <zkhCreator@gmail.com> 1548937962 +0800	commit (initial): test
d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f b6542b871b52bdf66481d03127620e1d7b7d37c9 zkhCreator <zkhCreator@gmail.com> 1548938032 +0800	commit: test2
複製程式碼
  • refs/heads/master:當前這個 master 對應的提交物件的 SHA-1 值。
  • refs/tags:對應 tag 的指向的提交物件的 SHA-1 值儲存的地方。

最後的最後

當然其中還存在 objects/infoobjects/pack 這兩個。你可以嘗試在工程中執行 git gc 來對當前內容進行整理。你會發現檔案當中的 objects/ 目錄下乾淨了很多,僅僅留下了 objects/info/packsobjects/pack 。這個時候實際上 git 將你的檔案進行了打包操作,從而減少了整個工程的體積。路徑中的 pack-SHA1.idx 就是這個包的索引,裡面包含了打包之後的檔案資訊(主要用於上傳下載的過程中的不常用的檔案索引,可以通過 git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx 進行檢視檔案目錄),pack-SHA1.pack 則是壓縮的 git 物件的集合。對於細節想了解的同學可以檢視官網的Git 內部檔案