2026-06-11 · 內容定址 · SHA · blob · tree · commit · .git
.git 這個黑盒子,看一張快照究竟是用哪些物件、怎麼用 hash 串起來的。看懂這層,後面的 commit、branch、merge、reset 全都只是在這張物件圖上挪指標而已。
Git 的核心是一個小型的鍵值資料庫(key-value store):你丟一段內容進去,它回給你一把鑰匙;日後拿這把鑰匙就能取回一模一樣的內容。特別的是——這把鑰匙不是 Git 隨便編的流水號,而是從內容本身算出來的。這叫內容定址(content-addressable)。
算鑰匙的方法是把內容餵進 SHA-1 雜湊函式,得到一串 40 個十六進位字元(160 bit)的指紋,例如 ce013625030ba8dba906f756967f9e9ca394464a。這串指紋同時就是該物件在資料庫裡的「地址」。
內容定址帶來三個關鍵性質,整個 Git 都建立在它們之上:
| 性質 | 意義 |
|---|---|
| 確定性 | 相同內容 → 永遠相同 hash。所以兩台電腦各自算同一個檔案,會得到同一個地址。 |
| 自動去重 | 內容沒變就是同一個地址 → 同一份物件只存一次。十個 commit 共用沒改的檔案,不會存十份。 |
| 防竄改 | 改任何一個 byte,hash 完全變樣(雪崩效應)。地址對得上,就保證內容一個 bit 都沒被動過。 |
Git 裡有四種物件,最基礎的是 blob(binary large object)。一個 blob 就是一個檔案的內容——只有內容,不含檔名、不含路徑、不含權限。檔名是誰記的?下一節的 tree 才記。
算一個 blob 的 hash 時,Git 不是直接對檔案內容做 SHA-1,而是先加一段標頭:物件型別、一個空格、內容的位元組長度,再接一個 \0,然後才是內容本身:
sha1( "blob" + " " + 長度 + "\0" + 內容 )
下面是一個真的計算器(在你瀏覽器裡跑完整的 SHA-1,不是假動畫)。改改內容,看地址怎麼變:
—hello\n 的 hash 是 ce013625…394464a。你可以在任何裝了 Git 的終端機驗證,結果一模一樣:
printf 'hello\n' | git hash-object --stdinblob 只有內容,那「哪個檔名、哪個資料夾」誰來記?答案是 tree 物件。一個 tree 對應一個目錄,裡面是一張清單,每一列記錄:
100644、可執行檔 100755、子目錄 040000)blob 或 tree)與該物件的 hash子目錄就指向另一個 tree,於是 tree 可以層層巢狀,長成你專案的整棵目錄結構。把滑鼠移到節點上可看到該物件的 hash:
a.txt 改名成 b.txt 但內容不變時,blob hash 不變(同一份內容),只有 tree 那張清單變了——這也是為什麼 Git 能聰明地看出「這其實是改名」。
一棵 tree 描述了「某個瞬間,整個專案長什麼樣」,但它沒有時間、沒有作者、沒有「上一版是誰」。commit 物件就是來補這些的:它指向一棵根 tree(= 這次的完整快照),再加上中繼資料。一個 commit 物件包含:
注意:commit 不直接記檔案,它只記「根 tree 的 hash」。要還原這次的內容,Git 從 commit → 根 tree → 各 blob 一路展開即可。而 parent 欄位指向上一個 commit,一個個串起來,就是專案的歷史線(下一章會深入這條線)。
下面是一個有兩個檔案的小專案。編輯 hello.txt 的內容(哪怕只改一個字),看 blob → tree → commit 的 hash 怎麼連鎖更新。注意:沒被你改到的 readme.md,它的 blob hash 完全不動(去重)。變色的方塊就是這次被牽動的物件:
————hello.txt 改回 hello\n,blob hash 又會變回 ce0136…——因為地址只跟內容有關。
commit 裡記錄的 author 和 committer 是什麼?為什麼要分成兩個?
它們是 commit 物件裡分開記錄的兩個身分,而且各自帶一組時間戳。一句話區分:
‧ author(作者):這份修改內容是誰寫的、什麼時候寫的。
‧ committer(提交者):這個 commit 物件是誰、什麼時候實際建立進版本庫的。
格式都是 姓名 <email> 時間戳 時區:
author Alice <alice@example.com> 1700000000 +0800
committer Alice <alice@example.com> 1700000000 +0800
多數情況兩者相同——你自己 git commit 時,寫程式的人和按下提交的人都是你。會不一樣,是因為某些操作會「重做 / 搬動」commit:內容的原作者不變,但建立 commit 的人和時間變了:
‧ git rebase / git cherry-pick:保留原 author 與原始時間,committer 換成「現在操作的你 + 現在時間」。
‧ git commit --amend:通常保留 author,更新 committer。
‧ 套用別人寄來的 patch(git am)或維護者合併 PR:author 是原貢獻者,committer 是套用 / 合併的人。
這也解釋了 git log 預設的「Date:」顯示的是 author date,所以 rebase 過的歷史常見「日期是當初寫的、但實際是今天才重建」。
跟本章的連結:回想 2.4 ——commit 的 hash 涵蓋它全部內容,包含這兩個欄位和時間戳。所以即使 tree、訊息、parent 都沒變,只要 committer 的時間或身分不同,算出來的 commit hash 就不同。這正是 git rebase 即使「內容沒變」也會產生一串全新 hash 的原因(第 7 章詳談)。
.git/ 目錄:物件與指標住在哪到目前為止的 blob / tree / commit,實際都存在專案根目錄底下那個隱藏的 .git/ 資料夾裡。它就是第 1 章說的「版本庫」本體。點開下面幾個關鍵項目看看它們的角色:
.git/ 裡的檔案。理解它的結構是為了建立心智模型;真正要操作時,一律透過 Git 指令,讓它幫你維持一致性。
| 物件 / 概念 | 一句話記住 |
|---|---|
| 內容定址 | 地址(hash)是從內容算出來的,內容相同→地址相同→自動去重、防竄改 |
| blob | 只存「檔案內容」,沒有檔名 |
| tree | 一個目錄的清單:記名稱、權限、指向 blob 或子 tree |
| commit | 指向一棵根 tree(快照)+ parent + 作者 / 時間 / 訊息 |
| .git/ | objects 存物件、refs 存分支指標、HEAD 記你在哪、index 是暫存區 |
| Merkle 鏈 | blob 變 → tree 變 → commit 變,改一個 byte 整條鏈都變 |
parent 串成一張歷史圖(DAG),以及 branch 和 HEAD 這兩個「指標」如何在圖上移動。