Git 版本控制 › 第 3 章

commit、HEAD 與分支的真相

2026-06-11  ·  commit · parent · DAG · branch · HEAD · detached HEAD

第 2 章把鏡頭對準「一個 commit 內部長什麼樣」。本章把鏡頭拉遠:看一堆 commit 怎麼用 parent 串成一張歷史圖,以及 branchHEAD 這兩個東西——它們聽起來很玄,實際上只是各存著一個 hash 的小指標。看懂這章,你會發現 branch、merge、reset、rebase 全是「在這張圖上挪指標」而已。

3.1commit 的不可變性與 parent → DAG

回想第 2 章:commit 的 hash 是從它全部內容(根 tree、parent、作者、時間、訊息)算出來的。這帶來一個關鍵性質——commit 一旦建立就不可變。你沒辦法「修改」一個 commit;任何改動都會算出不同的 hash,那其實是另一個新 commit。舊的那顆原封不動還在那裡。

每個 commit 都記著自己的 parent(上一顆 commit 的 hash)。順著 parent 一路往回,就是專案的歷史線。因為每顆 commit 都「指向過去」,這些箭頭永遠朝後、不會形成迴圈,於是整段歷史是一張 DAG(有向無環圖,Directed Acyclic Graph):

commit 以 parent 指標串成 DAG,合併 commit 有兩個 parent A 接 B 接 C,從 C 分出 D;E 合併 C 與 D 兩個 parent,箭頭一律指向較早的 commit A B C D E 最早 最新(合併) feature 上的 D
箭頭 = parent 指標,一律指向「更早」的 commit;合併 commit(E)有兩個 parent
「不可變 + 指向過去」就是 Git 歷史可信的根基:你能往歷史追加新 commit,但動不了已存在的 commit。所謂「改歷史」(amendrebase)其實都是造一批新 commit、再把指標移過去,舊 commit 仍短暫留在物件庫裡(這正是第 9 章 reflog 能救命的原因)。

3.2branch:只存一個 hash 的輕量指標

很多人以為「分支」是一份檔案的複本,或一條被框起來的 commit 鏈。都不是。一條分支的真身,就是第 2 章看過的 .git/refs/heads/ 底下一個小文字檔,檔名是分支名,內容只有一行——它所指向那顆 commit 的 40 字元 hash:

$ cat .git/refs/heads/main  →  a1b2c3d4…(就這一行,40 字元)

所以「分支」輕到不行——它不擁有任何 commit,只是一張寫著 hash 的便利貼,貼在某顆 commit 上。一顆 commit 上可以同時貼好幾張(好幾條分支指到同一顆),也可以一張都沒有。

branch 是貼在 commit 上的指標,commit 串成歷史線 main 指向 C,feature 從 C 分出指向 D,兩條分支都只是指向某顆 commit 的標籤 A B C D main feature
commit 是實體(灰圓),branch 只是指向某顆 commit 的標籤(紫框)
這解釋了「為什麼 Git 開分支這麼快」——別的版控工具開分支要複製整個專案,Git 只是新增一個 41 byte 左右的小檔(一行 hash + 換行)。建立、刪除分支幾乎零成本,因為你動的從來不是 commit,只是那張便利貼。

那麼,「我新 commit 時,分支指標怎麼自己往前移?」——靠的是下一節的 HEAD

3.3HEAD:你現在站在哪

如果說 branch 是貼在 commit 上的便利貼,那 HEAD 就是貼在便利貼上的便利貼——它記錄「我現在在哪一條分支上」。.git/HEAD 通常不是一個 hash,而是一行指向某條分支的引用:

$ cat .git/HEAD  →  ref: refs/heads/main

這一層「間接」是整個機制的關鍵。HEAD → 分支 → commit 三段接力,讓 git commit 時發生這串連鎖反應:

  1. 用 HEAD 找到目前分支(main),再找到它指的 commit ——這就是新 commit 的 parent
  2. 建立新 commit。
  3. 分支指標移到新 commit。
  4. HEAD 沒動——它還是指著 main,於是「自動」跟著前進了。
正常 HEAD 指向分支,detached HEAD 直接指向 commit 左:HEAD 指向 main、main 指向 commit;右:HEAD 直接指向某顆舊 commit,稱為 detached HEAD 正常:HEAD → 分支 → commit HEAD main C detached:HEAD → commit(略過分支) HEAD A 沒有分支貼著它
左:一般情況多一層間接;右:detached HEAD 少了中間那層分支

detached HEAD:沒有分支的那一刻

當你 git checkout <某顆 commit 的 hash>(而不是分支名)時,HEAD 會直接指向那顆 commit,跳過中間的分支那一層。這就是 detached HEAD(分離的 HEAD)。它本身不是錯誤——你可以四處看、可以試做幾顆 commit;但要小心:

在 detached HEAD 下做的新 commit,沒有任何分支指著它們。一旦你切回別的分支,這些 commit 就「無人認領」,在畫面上消失,日後會被垃圾回收清掉。想保住它們,就在離開前用 git switch -c <新分支名> 立刻貼一張便利貼上去。(真的忘了?第 9 章的 reflog 還有機會救。)

下一節給你一個可以親手玩的圖譜,把這三件事——新 commit 讓分支前移HEAD 跟著走detached 時的孤兒 commit——一次看個清楚。

3.4互動:親手挪指標

按按鈕下 Git 指令,看 commit(灰圓)、分支(紫標籤)、HEAD(珊瑚標籤)怎麼在圖上移動。重點觀察:commit 時,HEAD 所在的那條分支往前跳一格,HEAD 黏著它一起走。試試開 feature 分支、各自 commit 看歷史分岔;再試 checkout 舊 commit 進入 detached HEAD。

珊瑚色圓框 = HEAD 目前解析到的 commit(下一次 commit 的 parent)
試這個流程體會 detached 的「孤兒」:① 連按兩次 git commit → ② 按 git checkout A 進 detached → ③ 再 git commit(做一顆沒有分支的 commit)→ ④ 按 git switch main。注意第 ③ 步那顆 commit 在第 ④ 步後就沒有任何分支指著了——它正是上面警告說的「無人認領」commit。

3.5本章小結

概念一句話記住
commit 不可變hash 涵蓋全部內容,動不了舊 commit;「改歷史」=造新 commit + 移指標
parent → DAG每顆 commit 指向過去的 parent,串成有向無環圖;合併 commit 有多個 parent
branch一個只存 40 字元 hash 的小檔,指向某顆 commit;開分支幾乎零成本
HEAD記「我在哪條分支」,通常是 ref: refs/heads/… 這層間接
commit 時分支指標前移一格,HEAD 黏著分支一起走
detached HEADHEAD 直接指 commit、跳過分支;此時做的 commit 無分支認領,易遺失
現在你手上有了完整的心智模型:物件圖(第 2 章)+ 指標(本章)。下一章回到地面,看 init / add / commit / log / diff 這些每天在用的指令,究竟在這張圖和三大區域上做了什麼。