Rất nhiều người dùng Git nhiều năm nhưng chưa từng mở thử thư mục .git xem bên trong có
gì. Sự thật khá bất ngờ: Git không hề biết "file" hay "thư mục" là gì theo nghĩa
thông thường. Với Git, toàn bộ repository chỉ là một cơ sở dữ liệu khoá-giá trị chứa 3 loại
đối tượng — Blob, Tree, Commit — và mọi lệnh bạn gõ chỉ là cách tạo đối tượng mới
hoặc di chuyển vài con trỏ trỏ vào chúng.
1. Git Không Lưu File — Nó Lưu Đối Tượng
Mỗi khi Git cần lưu một thứ gì đó (nội dung file, cấu trúc thư mục, hay một lần commit), nó tạo ra một
đối tượng (object) mới và lưu vào .git/objects. Mỗi đối tượng có một
hash làm định danh, và một khi đã tạo, đối tượng đó không bao giờ bị sửa đổi — chỉ có
thể tạo đối tượng mới. Có 3 loại đối tượng chính:
- Blob (Binary Large OBject) — lưu nội dung thô của 1 file.
- Tree — lưu cấu trúc 1 thư mục: ánh xạ tên → blob hoặc tree con.
- Commit — lưu 1 lần chụp lịch sử: trỏ tới 1 tree gốc + (các) commit cha.
2. Blob — Đối Tượng Nội Dung Thuần Tuý
Điều quan trọng nhất cần nhớ về blob: nó không lưu tên file. Blob chỉ là nội dung thô
của file, không hơn không kém. Hai file tên khác nhau nhưng nội dung giống hệt nhau (vd 2 file
LICENSE giống hệt ở 2 thư mục con) sẽ tạo ra đúng 1 blob — vì hash chỉ
phụ thuộc nội dung, không phụ thuộc tên hay vị trí:
# git hash-object tính hash mà KHÔNG cần tên file làm đầu vào
$ echo "Xin chào Git!" | git hash-object --stdin
a1b2c3d4e5f6... # hash chỉ phụ thuộc nội dung "Xin chào Git!\n"
# Đổi tên file không tạo blob mới — nội dung không đổi, hash không đổi
$ git cat-file -p a1b2c3d4 # xem lại nội dung thô của blob theo hash
3. Tree — Ảnh Chụp Một Thư Mục
Tree là nơi tên file thực sự xuất hiện. Một tree object liệt kê các entry, mỗi entry gồm mode (quyền + loại: file thường, file thực thi, thư mục con...), tên, và hash trỏ tới 1 blob (nếu là file) hoặc 1 tree khác (nếu là thư mục con) — đệ quy hoá toàn bộ cấu trúc thư mục thành một cây các đối tượng:
100644 blob a1b2c3d... README.md
100644 blob 9f8e7d6... app.js
040000 tree 5c4b3a2... src <- thư mục con là 1 TREE khác, đệ quy
4. Commit — Con Trỏ Tới 1 Tree Và (Các) Commit Cha
Commit là lớp mỏng nhất nhưng quan trọng nhất: nó chỉ chứa hash của
1 tree gốc (snapshot toàn bộ repo tại thời điểm đó), hash của
(các) commit cha (rỗng nếu là commit đầu tiên, 2 cha nếu là merge commit), cùng tác
giả, thời gian, và message. Chuỗi các commit nối với nhau qua con trỏ cha chính là
lịch sử mà git log hiển thị:
tree 7d3f9c1...
parent 4a8b2e0...
author Tang Thanh Quang <[email protected]> 1751500000 +0700
committer Tang Thanh Quang <[email protected]> 1751500000 +0700
Sửa lỗi hiển thị sai ở trang chủ
.git/refs/heads/main) chỉ là 1 file văn bản chứa đúng 1 dòng: hash của commit mới
nhất trên nhánh đó. Đây là nền tảng cho Bài 3 — branch rẻ tới mức tạo
hàng trăm nhánh không tốn gì đáng kể.
5. Content-Addressable Storage: Vì Sao Định Danh Bằng Hash?
Git dùng SHA-1 (và đang chuyển dần sang SHA-256) để định danh mọi đối tượng — gọi là content-addressable storage: địa chỉ lưu trữ của 1 đối tượng chính là hash nội dung của nó. Cách chọn này giải quyết đồng thời 3 vấn đề:
- Chống trùng lặp tự động (deduplication): nội dung giống hệt luôn cho cùng 1 hash → chỉ lưu 1 bản duy nhất, bất kể xuất hiện ở bao nhiêu file/thư mục/commit.
- Kiểm tra toàn vẹn miễn phí: chỉ cần băm lại nội dung và so hash — phát hiện ngay dữ liệu bị hỏng hoặc bị thay đổi trái phép.
- So sánh nhanh không cần đọc nội dung: 2 cây thư mục giống hệt nhau sẽ luôn có cùng tree hash — Git có thể bỏ qua toàn bộ cây con mà không cần duyệt sâu vào bên trong.
"<loại> <kích-thước-byte>\0<nội-dung>". Ví dụ blob nội dung "hi\n" (3 byte) sẽ băm chuỗi "blob 3\0hi\n", không
phải chỉ "hi\n". Tiền tố loại + kích thước này là lý do 1 blob, 1 tree, và 1 commit
không bao giờ va chạm hash dù có thể có cùng "nội dung thô" — chúng thuộc 2 không gian băm khác nhau
ngay từ tiền tố.
6. Snapshot, Không Phải Diff
Đây là khác biệt triết lý lớn nhất giữa Git và các hệ quản lý phiên bản cũ hơn (CVS, Subversion):
mỗi commit trỏ tới một snapshot ĐẦY ĐỦ của toàn bộ cây thư mục tại thời điểm đó —
không phải một "bản vá" (diff) so với commit trước. Khi bạn chạy git log -p và thấy diff
đẹp đẽ giữa 2 commit, đó là vì Git tính toán diff đó ngay lúc hiển thị bằng cách so
sánh 2 tree, chứ không phải Git đã lưu sẵn diff đó ở đâu.
7. So Sánh Blob, Tree, Commit
| Đối tượng | Lưu gì | Trỏ tới | Biết tên file? |
|---|---|---|---|
| Blob | Nội dung thô 1 file | Không trỏ tới đâu (là lá) | Không |
| Tree | Danh sách entry (mode + tên + hash) | Blob hoặc Tree con | Có |
| Commit | Metadata + message | 1 Tree gốc + (các) Commit cha | Không (gián tiếp qua tree) |
Sân chơi tương tác: Git Object Explorer
Sửa nội dung 2 file bên dưới rồi bấm "Commit Snapshot" nhiều lần. Quan sát: file không đổi → blob được tái sử dụng (không tạo object mới); file đổi → blob mới xuất hiện, nhưng tree gốc luôn phải tạo mới vì nó phụ thuộc hash của mọi blob con. Hash trong demo dùng SHA-1 thật qua Web Crypto API của trình duyệt.
Working Directory
Nhật ký
Click 1 commit trên đồ thị để xem cây tree/blob của nó.
Trắc nghiệm ôn tập
Câu 1: Vì sao 2 file có tên khác nhau nhưng nội dung giống hệt nhau chỉ tạo ra đúng 1 blob object?
Trắc nghiệm ôn tập
Câu 2: Bạn commit sau khi chỉ sửa 1 trong 5 file. Vì sao tree gốc (root tree) luôn phải tạo object mới, dù 4 blob còn lại không hề thay đổi?
Trắc nghiệm ôn tập
Câu 3: "Git lưu diff (bản vá) giữa các commit, giống SVN/CVS." Nhận định này đúng hay sai?