Hầu hết chúng ta dùng Git như một hộp đen: git add, git commit, git push, và cầu cho nó chạy. Nhưng khi có sự cố (merge rối, mất commit, .git phình to), hiểu bên trong Git cứu bạn. Tin tốt: lõi Git đơn giản đến bất ngờ — nó chỉ là một key-value store nội dung địa chỉ hoá (content-addressed). Bài đầu của loạt "Git nội bộ" này mổ xẻ ba loại object nền tảng bằng lệnh git thật, và tự tay tính ra khoá SHA của Git.
Content-addressed: khoá chính là nội dung
Trong Git, "địa chỉ" của một dữ liệu chính là hàm băm SHA-1 của nội dung nó. Lệnh git hash-object cho thấy điều đó:
$ echo -n "xin chao git" > f.txt
$ git hash-object -w f.txt # -w = ghi object vào .git
c8c3736359f89d7fd2a7a0f3f80eb4712012c7e1
Điểm mấu chốt: SHA băm nội dung, không phải tên file. Cùng nội dung luôn cho cùng SHA, dù ở file nào:
$ echo -n "xin chao git" | git hash-object --stdin
c8c3736359f89d7fd2a7a0f3f80eb4712012c7e1 # KHỚP
Đọc lại object bằng git cat-file:
$ git cat-file -t c8c3736 # -t = loại -> blob
$ git cat-file -p c8c3736 # -p = nội dung -> xin chao git

Hình 1: git hash-object băm nội dung thành khoá SHA; git cat-file đọc lại object; SHA tính từ header blob <len>\0<nội dung> rồi nén zlib; một commit trỏ tree, tree trỏ blob.
Tự tính khoá SHA của Git (không phải phép màu)
Object của Git không chỉ là nội dung thô — nó có một header nhỏ: <loại> <độ dài>\0 đứng trước nội dung, rồi cả cụm được băm SHA-1 và nén zlib. Ta tự tính ra được đúng khoá của Git:
$ printf "blob 12\0xin chao git" | sha1sum
c8c3736359f89d7fd2a7a0f3f80eb4712012c7e1 # khớp SHA của git!
Object thô trên đĩa nằm ở .git/objects/c8/c3736... (2 ký tự đầu SHA làm tên thư mục, phần còn lại làm tên file), và được nén zlib — byte đầu tiên là 78, magic byte của zlib. Không có gì huyền bí: Git = SHA + zlib + một quy ước header.
Ba loại object trỏ nhau thành cây
Khi bạn commit, Git sinh đủ ba loại object:
$ git add f.txt && git commit -m "bai dau tien"
$ git cat-file -p HEAD
tree ee36608525da4eec8c770bd03f3d0b2ff3fe2542
author demo <a@b.c> 1790740945 +0000
...
bai dau tien
$ git cat-file -p ee36608 # tree = một thư mục
100644 blob c8c3736359f89d7fd2a7a0f3f80eb4712012c7e1 f.txt
- blob: nội dung một file (không có tên, không có thời gian — chỉ nội dung).
- tree: một thư mục — ánh xạ tên → SHA (trỏ tới blob hoặc tree con). Tên file sống ở đây, không ở blob.
- commit: một ảnh chụp (snapshot) — trỏ tới một tree gốc, kèm tác giả, thời gian, message, và (các) commit cha.
Chuỗi trỏ: commit → tree → blob. Quan trọng: commit lưu snapshot toàn bộ cây, không lưu "diff". Diff mà bạn thấy khi git show là Git tính ra lúc đó bằng cách so hai snapshot — không phải thứ được lưu.

Hình 2: Chạy thật trên git 2.39 — cùng nội dung cho cùng SHA, công thức sha1("blob <len>\0...") tự tính ra đúng khoá của git, object nén zlib (byte đầu 78), và một commit trỏ tree trỏ blob.
Vì sao thiết kế này lại hay đến vậy
Content-addressing không phải chi tiết vụn — nó là nền của mọi tính năng Git (theo Pro Git — Git Internals):
- Toàn vẹn dữ liệu: đổi một byte nội dung là đổi SHA. Git so SHA khi đọc object nên phát hiện hỏng dữ liệu (đĩa lỗi, sửa lén) ngay lập tức.
- Khử trùng (dedup) tự nhiên: cùng một nội dung chỉ lưu một object, dù nó xuất hiện ở nhiều file hay nhiều commit. Đây là lý do
.gitkhông phình theo kiểu ngây thơ (bài packfile sau còn nén thêm nữa). - Bất biến (immutable): object không sửa được — "thay đổi một file" thực chất là tạo object mới rồi tạo tree/commit mới trỏ tới nó. Lịch sử vì thế là một chuỗi snapshot bất biến, không phải một file bị ghi đè.
Đánh đổi cần cân nhắc
SHA-1 và va chạm. Git dùng SHA-1 (đang chuyển dần sang SHA-256). SHA-1 về lý thuyết có thể va chạm, nhưng Git thêm phòng vệ (collision detection) và trong thực tế nguy cơ cực nhỏ với mã nguồn. Đừng lo va chạm SHA cho công việc thường ngày; nhưng biết Git đang di trú sang SHA-256.
Snapshot, không phải diff — đánh đổi chỗ và tốc độ. Lưu snapshot mỗi commit nghe như tốn chỗ, nhưng nhờ dedup (blob không đổi được chia sẻ) và packfile (nén delta sau), Git rất tiết kiệm. Đổi lại, git show/git diff phải tính diff lúc chạy — nhanh cho file thường, chậm hơn cho file khổng lồ.
Đừng lưu file nhị phân lớn vào Git. Vì mỗi phiên bản một file lớn tạo một blob mới toàn phần (nội dung khác = SHA khác = object mới), Git phình nhanh với binary hay đổi. Dùng Git LFS cho tài nguyên lớn — đó là hệ quả trực tiếp của mô hình content-addressed.
Ba ý mang về
- Git là key-value store nội dung địa chỉ hoá: khoá = SHA của nội dung, không phải tên file — đo thật cùng nội dung cho cùng SHA, và ta tự tính ra đúng khoá bằng
sha1("blob <len>\0<nội dung>"). - Ba loại object trỏ nhau thành cây:
commit → tree → blob; tên file sống ở tree, nội dung ở blob, và commit là snapshot toàn bộ cây (không phải diff — diff được tính lúc chạy). - Content-addressing là nền của mọi thứ: cho toàn vẹn dữ liệu (đổi 1 byte = đổi SHA), dedup tự nhiên, và tính bất biến — mọi tính năng Git (branch, merge, history) đều xây trên ba loại object này.
Nguồn
- Pro Git (sách chính thức) — Git Internals: Git Objects: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
- Git Documentation — git-hash-object / git-cat-file: https://git-scm.com/docs
Phần sau ta mổ xẻ ba vùng của Git — working directory, index (staging) và repository — và làm rõ index thực chất là gì, thứ mà git add thao tác vào.