Git 全景課 / 第一部分 建立心智模型 · M3

M3 物件與指標

打開 .git 資料夾,你只會找到兩樣東西:一個只增不改的物件資料庫,和幾張指向物件的標籤。看懂這兩樣,之後的分支、合併、撤銷都只是在移動標籤。
約 60 分鐘互動 ×2 · 小測 4 題先修:M2頁面上的編號與真實 Git 完全相同
學完本單元,你能夠:
  1. 說出 blob、tree、commit、tag 四種物件各存甚麼;
  2. 解釋雜湊值和「內容定址」,以及為甚麼歷史不能被偷偷修改;
  3. 說出 ref 和 HEAD 是甚麼,並解釋為甚麼建立分支幾乎不花成本;
  4. 用 git cat-file 親手查看倉庫裏的物件。

1打開 .git Inside the .git folder

M2 說過,倉庫就是專案裏那個隱藏的 .git 資料夾。執行 git init 並提交過一次之後,它裏面最重要的是這幾樣:

位置是甚麼本課何時細看
objects/物件資料庫:所有檔案內容、資料夾結構和 commit,都以物件形式存在這裏本單元
refs/heads/分支:每條分支一個小檔案,內容是一個 commit 的編號本單元、M4
refs/tags/標籤:同樣是小檔案本單元
HEAD「你現在在哪裏」:通常寫着 ref: refs/heads/main本單元、M4
index暫存區(M1),一個二進位檔M1
config這個倉庫的設定,例如遠端的位址M9
logs/每個 ref 移動過的記錄(reflog),是救援的最後防線M6
本單元的核心 Git 的全部歷史只由兩樣東西組成:物件(object)一旦寫入就永不改變,只會增加;指標(ref)是貼在物件上的標籤,可以隨時移動。之後學的每一條命令,不是新增物件,就是移動標籤。

2四種物件 Four kinds of objects

M0 的讀書筆記專案有三個檔案:README.md,以及 notes/ 資料夾裏的 ch1.md 和 ch2.md。第一次提交後,Git 在物件資料庫裏寫入了六個物件:

第一個 commit 的物件 commit 128f05c 指向根 tree 2727a46。根 tree 列出 README.md(blob 6317cce)和 notes 資料夾(tree 16b6e28)。notes tree 列出 ch1.md(blob 37c79ff)和 ch2.md(blob 441c911)。 commit 128f05c 「建立讀書筆記」 tree(根目錄) 2727a46 README.md · notes/ tree(notes/) 16b6e28 ch1.md · ch2.md blob 6317cce blob 37c79ff blob 441c911 README.md notes/ ch1.md ch2.md
圖 1. 第一個 commit 的六個物件,編號是真實的雜湊值(第 7 節可以親手重現)。留意檔名寫在 tree 的箭頭上,不在 blob 裏。
物件存甚麼對應
blob一個檔案的內容,沒有檔名、沒有日期檔案內容
tree一個資料夾:每一行是「模式、類型、編號、名字」,指向 blob 或下一層 tree資料夾
commit根 tree 的編號、上一個 commit(parent)的編號、作者、時間、說明一次提交=一張快照
tag附註標籤:指向某個 commit,加上標籤名、作者、說明版本記號(如 v1.0)

所以 M0 說的「快照」,具體來說就是:一個 commit 指向一個根 tree,根 tree 層層指向所有檔案的 blob。commit 裏還記着上一個 commit 的編號,一個接一個串起來,就是歷史。

3雜湊值與內容定址 Hashes and content addressing

每個物件的編號,是用它的內容計算出來的:Git 在內容前面加上「類型 空格 長度 \0」,再用 SHA-1 算出一串 40 位的十六進位數字。這叫內容定址:物件的地址就是它內容的指紋。

互動 1 親手算一個 blob 的編號

輸入任何文字,下面即時顯示 Git 會給它的編號。試試改一個字,看看編號變了多少。預設的 hello 在全世界任何一部電腦上算出來都是 ce01362。

Git 實際雜湊的內容:
blob 編號:

內容定址帶來三個重要性質:

  1. 內容相同,編號就相同。兩個內容一樣的檔案只存一份;沒改過的檔案,下一個 commit 直接沿用舊 blob。這就是 M0 說「沒有改動的檔案不重複保存」的原因。
  2. 改一個字,編號就完全不同。所以物件無法被修改:「修改」只能產生一個新物件。
  3. 編號同時是防偽印記。commit 的內容包含 tree 和 parent 的編號,tree 又包含 blob 的編號。改動歷史上任何一個字,從那裏往上的所有編號都會改變,別人一比對就知道。
嚴謹一點 Git 預設使用 SHA-1(已加入防碰撞檢查),也支援改用 SHA-256 的倉庫。平常引用編號時寫頭 7 位就夠,Git 會自動找出完整的那個。

4沙盒:看着物件資料庫長大 Watch the object database grow

互動 2 物件資料庫

這裏省略了 add:按「提交」等於 git add -A 再 git commit。藍色是這一步新增的物件或改變了的 ref。點任何一個物件,看看它的真實內容。

應用線:
  1. 先按「提交」,數數產生了幾個物件;點 commit 方塊看它的內容。
  2. 編輯 notes/ch1.md 再提交:哪些物件是新的?哪些被沿用?
  3. 按「建立分支」:物件資料庫有沒有變大?
  4. 按「建立附註標籤」,再點那個 tag 物件。

工作目錄

指標(.git 裏的小檔案)

commit/tagtreeblob這一步新增虛線=parent(上一個 commit)

點一個物件,這裏會顯示 git cat-file -p 的結果。
看到了甚麼 改一個檔案再提交,新增的是:那個檔案的新 blob、它所在資料夾的新 tree、往上每一層的新 tree,再加一個新 commit。其他沒改過的 blob 和 tree 原封不動地被沿用。而建立分支,一個物件也沒有新增,只多了一個小檔案。

5指標:ref 與 HEAD Refs and HEAD

40 位的編號沒有人記得住,所以 Git 用 ref 給 commit 取名字。一個 ref 就是 .git/refs/ 裏的一個純文字檔,內容只有一個編號(加一個換行,共 41 bytes)。

HEAD、分支與標籤 HEAD 指向分支 main;main 指向最新的 commit 1060da1,它的 parent 是 128f05c。標籤 v1 指向一個 tag 物件 ca60c0b,tag 物件再指向 1060da1。 HEAD main commit 1060da1 commit 128f05c v1 tag 物件 ca60c0b parent .git/HEAD:ref: refs/heads/main .git/refs/heads/main:1060da1… .git/refs/tags/v1:ca60c0b…
圖 2. 深色標籤是 ref(純文字檔),方塊是物件。HEAD 不直接指向 commit,而是指向分支。

.git/HEAD 通常不寫編號,而是寫 ref: refs/heads/main,意思是「我現在在 main 這條分支上」。提交時,Git 做的事依次是:寫入新物件 → 新 commit 的 parent 設為 main 目前指着的 commit → 把 refs/heads/main 的內容改成新 commit 的編號。HEAD 本身沒有變,它仍然指着 main,只是 main 往前走了一步。M4 會看到 HEAD 直接指向 commit 的特殊情況(detached HEAD)。

為甚麼建立分支幾乎不花成本

建立分支=在 refs/heads/ 寫一個 41 bytes 的檔案,不複製任何檔案、不新增任何物件。這和「複製整個資料夾」是天壤之別,也是 Git 鼓勵大量使用分支的原因。下一單元 M4 專門講分支。

標籤:輕量與附註

輕量標籤 git tag v1附註標籤 git tag -a v1 -m "說明"
是甚麼只是一個 ref,直接指向 commit一個 ref,指向一個 tag 物件,tag 物件再指向 commit
記錄只有名字名字、作者、時間、說明
用途私人、臨時的記號正式版本(建議用這種)

分支和標籤都是 ref,分別在於:提交時,目前所在的分支會自動往前移,標籤永遠不動。

6容易混淆的對照 Easily confused

commitbranch(分支)
是甚麼物件:一張快照加上作者和說明ref:一張指向 commit 的標籤
可以改嗎不可以,「修改」只會產生新 commit可以,隨時移到另一個 commit
存在哪裏.git/objects/.git/refs/heads/
刪除後只要還有 ref 或 reflog 指向它就不會消失只是撕掉標籤,commit 仍在
常見誤解
  • 「commit 存的是改動(差異)。」不是。commit 指向一個完整的 tree;差異是 git show 即時比較兩個 tree 算出來的。
  • 「分支是專案的一份複本。」不是。分支只是一張 41 bytes 的標籤(見 FAQ-001:分支與 worktree 的分別)。
  • 「把檔案改名,Git 會存一個新的 blob。」不會。內容不變,blob 就是同一個;改變的只有 tree 裏那一行的名字。
  • 「改 commit 說明(git commit --amend)會修改原來的 commit。」不會。它產生一個編號不同的新 commit,再把分支移過去(M6、M7)。

7動手驗證 Try it in a real terminal

在一個空資料夾裏,依次執行下面的命令。hash-object、cat-file 屬於 Git 的「底層命令」(plumbing),平常用不着,但最適合用來看清內部。

mkdir m3-lab && cd m3-lab
git init -b main
echo 'hello' > a.txt
git hash-object a.txt          # ce013625030ba8dba906f756967f9e9ca394464a
git add a.txt
find .git/objects -type f      # .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
git cat-file -t ce01362        # blob
git cat-file -p ce01362        # hello
git commit -m "第一個 commit"
cat .git/HEAD                  # ref: refs/heads/main
cat .git/refs/heads/main       # 你的 commit 編號
git cat-file -p HEAD           # tree、author、committer、說明
git cat-file -p 'HEAD^{tree}'  # 100644 blob ce01362…  a.txt

留意:物件存放的路徑,是把編號的頭兩位當作資料夾名、其餘 38 位當作檔名。

為甚麼你的 commit 編號和別人不同 blob 的編號只取決於內容,所以 hello 在每部電腦上都是 ce01362。commit 的內容卻包含作者名字、電郵和時間(精確到秒),所以每個人的 commit 編號都不一樣。如果你的 Git 設定了簽署提交,簽名也會寫進 commit,編號同樣會變。

8小測 Quiz

1. 把 a.txt 改名為 b.txt(內容不變)並提交,會產生新的 blob 嗎?
2. 建立一條新分支,物件資料庫會新增多少個物件?
3. 專案有 README.md 和 notes/ch1.md、notes/ch2.md。只修改 notes/ch1.md 再提交,會新增幾個物件?
4. 在 main 分支上提交後,.git/HEAD 檔案的內容通常是?

本單元重點 Summary

延伸閱讀