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).

Ảnh chụp đoạn mã nền tối minh hoạ ref HEAD branch chỉ là con trỏ tới commit branch bằng một file 41 byte HEAD bằng con trỏ tới branch chuyển branch bằng đổi con trỏ, một chuỗi trỏ HEAD tới ref branch tới commit SHA cat .git HEAD ref refs heads main HEAD trỏ tới 1 ref cat .git refs heads main 4316622 c155 ref trỏ tới 1 commit HEAD mình đang ở branch nào branch commit mới nhất của nhánh đó, hai tạo branch bằng ghi một file 41 byte 40 ký tự SHA cộng newline git branch tinh-nang ls -l .git refs heads tinh-nang 41 byte cat 4316622 c155 không sao chép file commit nào tạo branch tức thì gần như 0 chi phí lý do Git khuyến khích tạo branch thoải mái, ba chuyển branch bằng đổi con trỏ HEAD rev-parse phân giải tên tới SHA git switch tinh-nang cat .git HEAD ref refs heads tinh-nang chỉ đổi HEAD git rev-parse HEAD main tinh-nang cả 3 có thể trỏ cùng 1 commit git gc gộp ref lẻ vào .git packed-refs 1 file thay vì nhiều file nhỏ

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ị.

Ảnh chụp bảng kết quả chạy thật git 2.39 output thật, HEAD tới ref tới commit chuỗi hai bước cat .git HEAD ref refs heads main cat .git refs heads main 43166223dc8d82140973393047c2ccfda2d4c155, branch bằng file 41 byte chứa SHA git branch tinh-nang ls -l .git refs heads tinh-nang 41 byte cat 43166223 tạo branch bằng ghi 41 byte không sao chép file commit tức thì, chuyển branch bằng đổi HEAD ba tên cùng trỏ 1 commit git switch tinh-nang cat .git HEAD ref refs heads tinh-nang chỉ HEAD đổi 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 1 SHA branch chỉ là nhãn di động trỏ commit, packed-refs sau git pack-refs gộp ref lẻ vào 1 file 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 tag cũng là ref nguồn Pro Git Git References

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ề

  1. 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ì.
  2. 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 switch chỉ đổi .git/HEAD).
  3. Branch là nhãn di động: nhiều nhánh cùng trỏ một commit (đo thật main và tinh-nang cùng SHA), tag/remote cũng là ref cùng cơ chế, và git gc gộp chúng vào packed-refs.

Nguồn

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.