iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Software Development

深入一點點認識 Git系列 第 9

Day 9-深入一點點認識 Git:分支與 HEAD 的本質:參考(refs)

  • 分享至 

  • xImage
  •  

講到「分支」的時候,大家腦中浮現的是什麼畫面?

是不是類似這樣:
https://ithelp.ithome.com.tw/upload/images/20250908/201785130iJTe8egpE.png

但在 git 內部,分支並不是這樣運作的,並沒有「一個一個 commit 連線成分支」這種結構,所謂 mainfeature 這些分支名稱其實都是在不同 commit 之間移動的「參考(references,簡稱 refs)」,其相關資訊都儲存在 .git/refs/ 資料夾中。

「分支」的本質:直接從 ./git/refs 觀察

現在讓我們回顧《Day 6-上層瓷器指令複習(本地端分支管理)》的部分操作,觀察下這些指令後,.git/ 資料夾發生的變化。

觀察的對象包含:

.git/
├── hooks/
├── info/
├── objects/
├── refs/
│   ├── heads/      # 觀察這個資料夾
│   └── tags/
│
├── config
├── description
└── HEAD            # 觀察這個檔案

在經過 git init 指令後,預設的分支只有 mastermain(我們的範例用 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 的雜湊碼一樣:
https://ithelp.ithome.com.tw/upload/images/20250908/20178513tl1Hhim58n.png

至於那個 HEAD 是什麼?資料夾中的 HEAD 檔案寫著 ref: refs/heads/main,透過 git log 則顯示 HEAD -> main,兩者看起來有一些關聯。

HEAD

HEAD 本身其實是「指向另外一個參考的參考(reference to another reference)」,表示「當下在哪個分支上」,所以 HEAD 裡面寫 ref: refs/heads/main 會造就 git log 跑出 HEAD -> main,表示「目前在 main 分支上。

結合剛剛的 main 參考與 HEAD 參考,整體結構可透過下圖理解:

https://ithelp.ithome.com.tw/upload/images/20250908/20178513l4WxLTpKHY.png

由於 HEAD 本身就是一個參考,而它指的對象 main 也是一個參考,因此說 HEAD 是「指向另外一個參考的參考」。

新增 & 切換分支觀察

我們再透過 git branch 做出新分支 featureAfeatureB,則 ./git 資料夾變成這樣:

.git/
├── ...
├── refs/
│   ├── heads/          # 新建什麼分支,這裡就多了以該分支為名的檔案
│   │    └── main       # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureA   # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureB   # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│   └── tags/
│
├── ...
└── HEAD                # 一樣為ref: refs/heads/main

看來 mainfeatureAfeatureB 都指向 87a0474... 這個 commit,而 HEAD 檔案為 ref: refs/heads/main,表示現在在 main 這個分支上,圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250908/20178513e5gW1R90S3.png

經過 git switch 改變分支,其實就是在改變 HEAD 指的對象,例如透過以下指令:

git switch featureA

則 .git/ 資料夾結構變成:

.git/
├── ...
├── refs/
│   ├── heads/          # 沒新建分支,因此裡面的檔案不變
│   │    └── main       # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureA   # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureB   # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   └── tags/
│
├── ...
└── HEAD                # 變成ref: refs/heads/featureA

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250909/201785135d1uNORlx7.png

如果再輸入以下指令切換到 featureB 分支:

git switch featureA

則 .git/ 資料夾變成:

.git/
├── ...
├── refs/
│   ├── heads/          # 這裡一樣沒有變化
│   │    └── main       # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureA   # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureB   # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   └── tags/
│
├── ...
└── HEAD                # 變成ref: refs/heads/featureB

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250909/20178513tG7nBqtjwN.png

在不同分支上新建 commit 觀察

現在我們在 featureA 分支上建立一個新 commit,再下一次 git log 指令會觀察到什麼?
https://ithelp.ithome.com.tw/upload/images/20250908/20178513dVpxwnuMCR.png

接著觀察資料夾結構:

.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/mainrefs/heads/featureB 都不變。

而透過 git switch 切換分支,會改變 HEAD 檔案的內容,導致 git log 出來的結果跟著改變,但不變的原則是:

  • HEAD:始終指向當下所在的分支。
  • mainfeatureAfeatureB:始終指向該分支上最新的 commit,圖示如下:

https://ithelp.ithome.com.tw/upload/images/20250908/201785134yqGY5FFMX.png

那如果在不同分支上都新建一個 commit,會發生什麼事?

情境一:在 featureA 分支新增一個 commit:

因為是在 featureA 分支上新增一個 commit,featureA 標籤會移到該分支最新的 commit 8a4de7e... 上,而 HEAD 指向當前分支,因此會隨 featureA 這個參考移動:

.git/
├── ...
├── refs/
│   ├── heads/
│   │    └── main       # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   │    └── featureA   # 變成8a4de7e7cb983354be514221ba78c5fcf8534152
│   │    └── featureB   # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│   └── tags/
│
├── ...
└── HEAD                # ref: refs/heads/featureA

https://ithelp.ithome.com.tw/upload/images/20250908/20178513tbeEnWRIEE.png

情境二:在 main 分支新增一個 commit:

切換回 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

https://ithelp.ithome.com.tw/upload/images/20250908/20178513OewJdpIELc.png

情境三:在 featureB 分支新增一個 commit:

切換到 featureB 分支新建一個 commit,跟剛剛在 main 分支上新建 commit 效果相同:

.git/
├── ...
├── refs/
│   ├── heads/
│   │    └── main       # 530c49adabe0faacc6a6f429358110b123dd5dd4
│   │    └── featureA   # 8a4de7e7cb983354be514221ba78c5fcf8534152
│   │    └── featureB   # 271c26d3a74da11d09d9a28db56318f46592a140
│   └── tags/
│
├── ...
└── HEAD                # ref: refs/heads/featureB

小結

在這篇文章中,我們探究了 .git/ 資料夾中的 refs/heads/ 資料夾與 HEAD 檔案,整理出以下兩大重點:

  1. refs/heads:存放分支的名字,每個分支內容為該分支上最新 commit 的雜湊碼,代表指向對應分支上的最新 commit。
  2. HEAD:寫著當下所在的分支。

簡單來說,兩者都是「參考」,或者可以想成指標(pointer),分支的名字指向對應分支的最新 commit、HEAD 則指向當下所在的分支名稱。

參考資料

  1. Git Internals - Branches
  2. 10.3 Git Internals - Git References

上一篇
Day 8-深入一點點認識 Git:打完 git init 指令後,發生了什麼事?
系列文
深入一點點認識 Git9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言