Một hiểu lầm khiến nhiều người ngại dùng nhánh trong Git: "tạo branch chắc tốn kém, giống sao chép cả dự án". Sự thật ngược lại hoàn toàn — và hiểu tại sao sẽ đổi cách bạn làm việc. Bài này (phần 3 loạt "Git nội bộ") chứng minh bằng lệnh thật rằng một branch chỉ là một con trỏ 41 byte tới commit, HEAD là con trỏ tới branch, và chuyển nhánh chỉ là đổi một con trỏ.
HEAD → ref → commit: một chuỗi con trỏ
Trong bài trước ta thấy commit là các object bất biến trỏ nhau. Vậy làm sao Git biết "commit mới nhất" của một nhánh, và "mình đang ở nhánh nào"? Bằng ref (reference) — những con trỏ có tên tới commit. HEAD là con trỏ đặc biệt trỏ tới ref đang hoạt động:
$ cat .git/HEAD -> ref: refs/heads/main # HEAD trỏ tới một ref
$ cat .git/refs/heads/main -> 4316622...c155 # ref đó trỏ tới một commit
Chuỗi: HEAD → refs/heads/main → commit SHA. HEAD trả lời "mình đang ở branch nào"; branch trả lời "commit mới nhất của nhánh đó là gì". Cả hai chỉ là file text nhỏ trong .git.
Branch = một file 41 byte
Đây là điểm cốt lõi. Một branch không phải bản sao dự án — nó là một file chứa đúng 40 ký tự SHA + một newline = 41 byte:
$ git branch tinh-nang
$ ls -l .git/refs/heads/tinh-nang -> 41 byte
$ cat .git/refs/heads/tinh-nang -> 4316622...c155
Tạo branch = ghi 41 byte, không sao chép file hay commit nào. Đó là lý do git branch tức thì kể cả trên repo khổng lồ, và vì sao văn hoá Git khuyến khích tạo nhánh thoải mái (khác hẳn SVN/CVS xưa nơi branch tốn kém).

Hình 1: HEAD → ref → commit; tạo branch chỉ ghi một file 41 byte chứa SHA; chuyển branch chỉ đổi con trỏ HEAD; git gc gộp ref vào packed-refs.
Chuyển branch = đổi con trỏ HEAD
git switch (hay git checkout) chỉ đổi HEAD trỏ sang ref khác (và cập nhật working directory cho khớp):
$ git switch tinh-nang
$ cat .git/HEAD -> ref: refs/heads/tinh-nang # chỉ HEAD đổi
Và vì branch chỉ là nhãn, nhiều nhánh có thể cùng trỏ một commit — git rev-parse (phân giải tên → SHA) chứng minh:
$ git rev-parse HEAD -> 43166223...c155
$ git rev-parse main -> 43166223...c155
$ git rev-parse tinh-nang -> 43166223...c155
main và tinh-nang cùng một SHA — branch chỉ là nhãn di động trỏ tới commit. Khi bạn commit trên tinh-nang, chỉ ref tinh-nang được cập nhật để trỏ commit mới; main đứng yên. "Nhánh phân kỳ" thực chất là hai nhãn trỏ hai commit khác nhau trong cùng một đồ thị.

Hình 2: Chạy thật — HEAD trỏ ref, ref trỏ commit; branch là file 41 byte; git switch chỉ đổi HEAD; main/tinh-nang cùng SHA (nhãn di động); pack-refs gộp ref vào packed-refs.
Ref còn hơn cả branch: tag, remote, và packed-refs
Mọi thứ trong .git/refs/ đều là ref cùng cơ chế:
refs/heads/*— branch cục bộ.refs/tags/*— tag (tag nhẹ chỉ là ref trỏ commit; tag chú thích trỏ tới một tag object riêng).refs/remotes/*— vị trí branch trên remote (nhưorigin/main).
Khi số ref nhiều, git gc/git pack-refs gộp các file ref lẻ vào một file .git/packed-refs (đỡ tốn inode, đọc nhanh):
refs/heads còn file lẻ: 0
.git/packed-refs:
43166223...c155 refs/heads/main
43166223...c155 refs/heads/tinh-nang
43166223...c155 refs/tags/v1.0
Git tra ref theo thứ tự: file lẻ trong refs/ trước, rồi tới packed-refs — nên bạn không cần bận tâm nó đang ở dạng nào.
Đánh đổi cần cân nhắc
Branch rẻ, nhưng đừng để "branch mồ côi" tích tụ. Vì tạo branch gần như miễn phí, người ta tạo rất nhiều và quên xoá. Bản thân ref 41 byte không tốn gì, nhưng nó giữ commit sống (không bị GC), nên nhánh cũ có thể giữ lại dữ liệu bạn tưởng đã bỏ. Dọn nhánh đã merge (git branch --merged) để repo gọn.
HEAD tách rời (detached) là hợp lệ nhưng dễ mất việc. Nếu git switch <SHA> (không phải tên branch), HEAD trỏ thẳng tới một commit thay vì một ref — "detached HEAD". Commit tạo ở đó không có nhánh nào trỏ tới, nên dễ bị GC sau này (bài reflog/cherry-pick sẽ bàn cách an toàn).
Đừng sửa file trong .git/refs bằng tay. Bạn có thể cat để hiểu, nhưng ghi ref nên dùng lệnh Git (git update-ref, git branch) để Git ghi reflog và xử lý đúng packed-refs. Sửa tay dễ tạo trạng thái không nhất quán.
Ba ý mang về
- Branch chỉ là một con trỏ 41 byte tới commit, không phải bản sao dự án — đo thật
.git/refs/heads/tinh-nangđúng 41 byte chứa SHA; tạo branch là ghi 41 byte nên tức thì. - HEAD → ref → commit là chuỗi con trỏ: HEAD nói "đang ở nhánh nào", ref nói "commit mới nhất của nhánh"; chuyển branch chỉ đổi HEAD (đo thật
git switchchỉ đổi.git/HEAD). - Branch là nhãn di động: nhiều nhánh cùng trỏ một commit (đo thật
mainvàtinh-nangcùng SHA), tag/remote cũng là ref cùng cơ chế, vàgit gcgộp chúng vàopacked-refs.
Nguồn
- Pro Git — Git Internals: Git References: https://git-scm.com/book/en/v2/Git-Internals-Git-References
- Git Documentation — git-rev-parse / git-update-ref / gitrevisions: https://git-scm.com/docs
Phần sau ta mổ xẻ vì sao lịch sử Git là một đồ thị có hướng không chu trình (DAG): commit trỏ tới commit cha, và mỗi commit lưu một snapshot toàn bộ cây chứ không phải diff.