jas0nhuang

[筆記] Git 運作原理

要瞭解 Git 的詳細運作原理可能要花不少時間,以下只是粗淺的研究。如果內容有任何錯誤還請老師與同學不吝指正。

Git 的一切都在 .git/

讓我們從頭開始。
先建立一個空的資料夾,然後進入這個資料夾。
在按下 git init 這個指令之後,git 就會在所在的資料夾內創建一個 .git/ 資料夾,然後整個資料夾裡有任何風吹草動都會被 Git 給嚴密監控,
翻一遍 .git 資料夾就可以對 Git 有更深入一點點的瞭解,在 init 之後 .git/ 資料夾初始的內部組織長的像這樣:(只列出我懂的一些內容,以及這個討論主題會提到的資料,並非完整資料夾結構。)

.git
├── HEAD
├── objects/
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

題外話,強力推薦 tree 這個軟體,Linux 上 apt install treeyum install tree 就可以安裝了,Mac 上面 brew install tree 應該也可以,然後執行 tree 資料夾名稱 就可以把想查看的資料夾以樹狀結構表示出來,超好用!

git status 與 index

資料夾初始化之後,git 就開始運作了,這時候運行 git status 可以查詢目前 git 資料夾的狀態,官方文件 git-status 是這樣寫的:

顯示:
1. index 檔與目前 HEAD commit 之間的路徑差異。
2. index 檔與目前工作目錄(working tree)的路徑差異。
3. 在工作目錄中沒有被 Git 追蹤的路徑。

所以,顯然它輸出的資料與 index 這個檔案的內容很有關系,但是初始化 Git 的時候還不會建立 index 檔,要在第一次執行 git add 之後才會被建立。

讓我們建立一個檔案 X.file 並且加入一些內容。接著執行 git add ,現在就可以 vim .git/index 查看 index 檔案的內容,可以發現與 git status 傳給我們看的內容幾乎一模一樣。

在第一次建立 index 檔之後:

  1. 只要資料夾內有新增的檔案就會被記入 index 檔 Untracked 這個區塊。
  2. 已存在的檔案如果有變動則會被記入 index 檔 Unstaged 這個區塊。
  3. 執行 git add 之後該檔案會被記入 Staged 區塊。
  4. git commit 之後,Staged 區塊的記錄就會被清空。

至於 add 與 commit 位底是在幹什麼?我們下面聊聊。

add 與 commit 到底幹了什麼事

把檔案加(add)到 Git 裡

當我們執行 git add ,Git 會被通知要開始工作了:

  1. 創建一個 blob 物件(這個字真的蠻不知所云的,一個水滴?一團東西?一個胖子?拜託知道為什麼要這麼取名的人提點我一下……後來查了一下資料,發現它應該是 Binary large object 的縮寫),其實可以當成 Git 幫我們建立了一個檔案備份,給他一個雜湊值(SHA-1)然後存到 .git/object/ 資料夾裡面。
  2. 修改 .git/index 檔,將加入的檔案記入 Staged 區塊。

這時 .git/ 資料夾結構會變成:(比初始狀態多了 index 檔以及 object/eX/9dXXXX 這兩個檔案。)

.git
├── HEAD
├── index
├── objects/
│   ├── eX
│   │   └── 9dXXXX (檔案 blob 物件)
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

提交(commit)目前工作的檔案快照

檔案加入 Git 的控制之下了,這時候我們想要對目前工作的狀態提交一個檔案快照,執行 git commit,Git 又要上工了:

  1. 建立一個最上層資料夾的 tree 物件,指向其下第一層的 blob 以及 tree 物件,下層資料夾以此類推。(就是將每一個存在的資料夾當成一個節點建立一個資料夾的樹狀結構)
  2. 建立一個 commit 物件, 可以把它想像為一個快照連結,裡面會記錄最上層 tree 物件雜湊值,上一個檔案快照(commit)的雜湊值以及其它相關的資料。
  3. 創建/更新 refs/ 資料夾裡相應的參照(reference)檔。
  4. 創建/更新 logs/ 資料夾內相應的檔案,保存提交歷史記錄。

接續剛剛的 .git/ 資料夾狀態,在提交之後資料夾結構會變成:

.git
├── HEAD
├── index
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects/
│   ├── eX
│   │   └── 9dXXXX (檔案 blob 物件)
│   ├── dX
│   │   └── f1XXXX (資料夾 tree 物件)
│   ├── Xd
│   │   └── 6dXXXX (commit 物件)
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags

有一件事大家可以玩玩看,就是從上面資料夾結構的變化看來,在還沒有第一次 commit 之前,logs/ 資料夾以及 heads/master 檔案都是不存在的,所以這時候有奇怪的想法也是很正常的!是不是可以把 master 分支變成麥斯特分支?然後帥氣的生出 .git/refs/heads/麥斯特 資料夾?以及相應的 log 檔?

隨著上面這個問題總算要帶到我這一切 Git 探源之旅的出發點了:分支到底是什麼?

分支(branch)與誰的頭(HEAD)

從 Git 盤古開天開始講了那麼多,其實就是為了搞懂 Git 到底是怎麼儲存檔案的。
瞭解了之後,我們就可以比較清楚的討論分支到底是什麼,又跟那些檔案有關系。

只要有 commit 就有分支

是的,只要在 Git 監控的資料夾裡提交過,分支就會自動被建立,而這個自動被建立的分支就是 master,上面提到 commit 所做的工作第 3 步,就是在建立一個分支指標/連結檔。(.git/refs/heads/master)如果我們不再手動建立分支,那 master 就會是這個程式庫裡面唯一的分支。
這個新建的檔案(.git/refs/heads/master)內容也非常簡單,就是最後一個 commit 的雜湊值。

那這個「預設」分支到底可不可以改名字為「麥斯特」呢?答案是 「Yes I do.」,在還沒初始 commit 之前,地是空虛混沌,淵面黑暗,嗯,反正分支是還沒有實際存在的,唯一可以找到痕跡的地方就是在 HEAD 檔裡面,在我們執行 commit 之後,Git 就會跑去找 HEAD 問他:「嘿!咱們現在是到那個分支了阿?」HEAD 就會默默的把資料吐出來:ref: refs/heads/master,然後 Git 就盡責的去建立 refs/heads/master 這個資料以及相關的 log 檔了,這,就是 master 分支誕生的故事。
所以如果今天,我們不想要生成這個 master 分支,而是想要叫他「麥斯特」,其實只要修改預設的 HEAD 檔內容為 ref: refs/heads/麥斯特 就可以了,從此以後你就擁有與眾不同的「麥斯特」分支了。

扯了這麼一堆,扯出一個 HEAD,這個 HEAD 又是什麼東西了?讓我們先建立一個新的分支再慢慢討論 HEAD 是什麼。

建立分支

所以現在我們知道,分支其實就只是一個「參照」檔案,一個在 .git/refs/heads/ 資料夾裡面的指標/連結檔。
昨天介紹過 git branch 這個指令了,建立分支就是這麼簡單,執行 git branch 新婚吱.git/refs/heads/ 資料夾裡就會多出一個新婚吱檔案,記錄目前所在 commit 的雜湊值,分支也建立完成啦!

我們也知道,master 並沒有比其它分支特別,也不是真的什麼 master,只是因為 Git 預設會自動產生,所以就變成幾乎所有 git 程式庫都會有的一個分支,而且為了大家好溝通、好協作,所以最好還是不要把它改成什麼「麥斯特」、「麥當勞」之類的奇怪名字。

頭在哪人就在哪

HEAD 這個「頭」 ,記錄的是我們現在所處的位置,預設狀態下它就是指向 refs/heads/master,所以在沒有特別移動的狀態之下,我們的位置就是在 master 這個分支上最新的提交記錄。
切換分支的實際運作,就是我們把 HEAD 指向另一個分支,執行 git checkout 新婚吱 就是在切換 HEAD 指向 新婚吱,而這時 HEAD 檔的內容就會被修改為 ref: refs/heads/新婚吱

這個「頭」其實還可以有更複雜的運用,還可以讓他指向某個提交變成斷頭狀態……等等,以後再慢慢研究吧!

看了那麼多網上的教學,還是覺得 Git 官網上的資料講的最仔細,Pro Git 上有關分支介紹(Git Branching - Branches in a Nutshell)的部分有一些圖片解釋 ,整理成 GIF 圖感覺就像這樣:
git branching

python 寫一個查看 object 內容的小程式

import zlib
filename = ‘file/path’
compressed_contents = open(filename, ‘rb’).read()
decompressed_contents = zlib.decompress(compressed_contents)

Reading git objects
The Git Object Model GOM XD
Merging VS Rebase