M3 物件與指標
.git 資料夾,你只會找到兩樣東西:一個只增不改的物件資料庫,和幾張指向物件的標籤。看懂這兩樣,之後的分支、合併、撤銷都只是在移動標籤。- 說出 blob、tree、commit、tag 四種物件各存甚麼;
- 解釋雜湊值和「內容定址」,以及為甚麼歷史不能被偷偷修改;
- 說出 ref 和 HEAD 是甚麼,並解釋為甚麼建立分支幾乎不花成本;
- 用
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 |
2四種物件 Four kinds of objects
M0 的讀書筆記專案有三個檔案:README.md,以及 notes/ 資料夾裏的 ch1.md 和 ch2.md。第一次提交後,Git 在物件資料庫裏寫入了六個物件:
| 物件 | 存甚麼 | 對應 |
|---|---|---|
| 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 位的十六進位數字。這叫內容定址:物件的地址就是它內容的指紋。
內容定址帶來三個重要性質:
- 內容相同,編號就相同。兩個內容一樣的檔案只存一份;沒改過的檔案,下一個 commit 直接沿用舊 blob。這就是 M0 說「沒有改動的檔案不重複保存」的原因。
- 改一個字,編號就完全不同。所以物件無法被修改:「修改」只能產生一個新物件。
- 編號同時是防偽印記。commit 的內容包含 tree 和 parent 的編號,tree 又包含 blob 的編號。改動歷史上任何一個字,從那裏往上的所有編號都會改變,別人一比對就知道。
4沙盒:看着物件資料庫長大 Watch the object database grow
5指標:ref 與 HEAD Refs and HEAD
40 位的編號沒有人記得住,所以 Git 用 ref 給 commit 取名字。一個 ref 就是 .git/refs/ 裏的一個純文字檔,內容只有一個編號(加一個換行,共 41 bytes)。
HEAD:你現在在哪裏
.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
| commit | branch(分支) | |
|---|---|---|
| 是甚麼 | 物件:一張快照加上作者和說明 | 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 位當作檔名。
hello 在每部電腦上都是 ce01362。commit 的內容卻包含作者名字、電郵和時間(精確到秒),所以每個人的 commit 編號都不一樣。如果你的 Git 設定了簽署提交,簽名也會寫進 commit,編號同樣會變。
8小測 Quiz
本單元重點 Summary
.git裏只有兩樣核心東西:只增不改的物件,和可以移動的指標(ref)。- 四種物件:blob(內容,不含檔名)、tree(資料夾)、commit(根 tree+parent+作者+說明)、tag(附註標籤)。
- 物件的編號由內容計算(內容定址):內容相同就是同一個物件,改一個字就是另一個物件,所以歷史無法被偷偷修改。
- 提交時只新增改過的 blob 和沿途的 tree,其餘沿用。
- 分支和標籤都是 41 bytes 的 ref;分支會隨提交前進,標籤不動。HEAD 通常指向目前的分支。
延伸閱讀
- Pro Git 10.1 Plumbing and Porcelain
- Pro Git 10.2 Git Objects
- Pro Git 10.3 Git References
- 有問題?記下來,它很可能也是其他同學的問題 → 學生問題庫