講到「分支」的時候,大家腦中浮現的是什麼畫面?
是不是類似這樣:
但在 git 內部,分支並不是這樣運作的,並沒有「一個一個 commit 連線成分支」這種結構,所謂 main
、feature
這些分支名稱其實都是在不同 commit 之間移動的「參考(references,簡稱 refs)」,其相關資訊都儲存在 .git/refs/
資料夾中。
現在讓我們回顧《Day 6-上層瓷器指令複習(本地端分支管理)》的部分操作,觀察下這些指令後,.git/ 資料夾發生的變化。
觀察的對象包含:
.git/
├── hooks/
├── info/
├── objects/
├── refs/
│ ├── heads/ # 觀察這個資料夾
│ └── tags/
│
├── config
├── description
└── HEAD # 觀察這個檔案
在經過 git init 指令後,預設的分支只有 master
或 main
(我們的範例用 main
),而當沒有任何 commit 時,資料夾結構長這樣:
.git/
├── ...
├── refs/
│ ├── heads/ # 空
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/main
而當形成第一個 commit 之後,變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 跑出一個main檔案
│ │ └── main # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 一樣為ref: refs/heads/main
.refs/heads/
資料夾多了一個 main
檔案,內容為 87a0474...
,跟用 git log
看到剛剛 commit 的雜湊碼一樣:
至於那個 HEAD
是什麼?資料夾中的 HEAD
檔案寫著 ref: refs/heads/main
,透過 git log
則顯示 HEAD -> main
,兩者看起來有一些關聯。
HEAD
本身其實是「指向另外一個參考的參考(reference to another reference)」,表示「當下在哪個分支上」,所以 HEAD
裡面寫 ref: refs/heads/main
會造就 git log
跑出 HEAD -> main
,表示「目前在 main
分支上。
結合剛剛的 main
參考與 HEAD
參考,整體結構可透過下圖理解:
由於 HEAD
本身就是一個參考,而它指的對象 main
也是一個參考,因此說 HEAD
是「指向另外一個參考的參考」。
我們再透過 git branch
做出新分支 featureA
與 featureB
,則 ./git
資料夾變成這樣:
.git/
├── ...
├── refs/
│ ├── heads/ # 新建什麼分支,這裡就多了以該分支為名的檔案
│ │ └── main # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 一樣為ref: refs/heads/main
看來 main
、featureA
與 featureB
都指向 87a0474...
這個 commit,而 HEAD
檔案為 ref: refs/heads/main
,表示現在在 main
這個分支上,圖示如下:
經過 git switch
改變分支,其實就是在改變 HEAD 指的對象,例如透過以下指令:
git switch featureA
則 .git/
資料夾結構變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 沒新建分支,因此裡面的檔案不變
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 變成ref: refs/heads/featureA
圖示如下:
如果再輸入以下指令切換到 featureB
分支:
git switch featureA
則 .git/
資料夾變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 這裡一樣沒有變化
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 變成ref: refs/heads/featureB
圖示如下:
現在我們在 featureA
分支上建立一個新 commit,再下一次 git log
指令會觀察到什麼?
接著觀察資料夾結構:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 變成67250cdc18cc049fb57d83abcc2eb691cc5d860f
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 在main分支上時為ref: refs/heads/main
# 在featureA分支上時為ref: refs/heads/featureA
# 在featureB分支上時為ref: refs/heads/featureB
在 featureA
多了一個 commit 後,refs/heads/featureA
指向那個新的 commit 67250cd...
,refs/heads/main
跟 refs/heads/featureB
都不變。
而透過 git switch
切換分支,會改變 HEAD
檔案的內容,導致 git log
出來的結果跟著改變,但不變的原則是:
HEAD
:始終指向當下所在的分支。main
、featureA
、featureB
:始終指向該分支上最新的 commit,圖示如下:那如果在不同分支上都新建一個 commit,會發生什麼事?
因為是在 featureA
分支上新增一個 commit,featureA
標籤會移到該分支最新的 commit 8a4de7e...
上,而 HEAD
指向當前分支,因此會隨 featureA
這個參考移動:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 變成8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/featureA
切換回 main
分支,並在 main
分支上新增一個 commit 後,以 git log
指令檢查會發現,main
這項參考移動到該分支的最新 commit 530c49a...
上,而因為 HEAD
會指向當前分支 main
,所以也會跟著移動:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 530c49adabe0faacc6a6f429358110b123dd5dd4
│ │ └── featureA # 8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/main
切換到 featureB
分支新建一個 commit,跟剛剛在 main
分支上新建 commit 效果相同:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 530c49adabe0faacc6a6f429358110b123dd5dd4
│ │ └── featureA # 8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 271c26d3a74da11d09d9a28db56318f46592a140
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/featureB
在這篇文章中,我們探究了 .git/
資料夾中的 refs/heads/
資料夾與 HEAD
檔案,整理出以下兩大重點:
refs/heads
:存放分支的名字,每個分支內容為該分支上最新 commit 的雜湊碼,代表指向對應分支上的最新 commit。HEAD
:寫著當下所在的分支。簡單來說,兩者都是「參考」,或者可以想成指標(pointer),分支的名字指向對應分支的最新 commit、HEAD
則指向當下所在的分支名稱。