Git Deep Dive #1: Git thực sự hoạt động như thế nào? Hiểu Object Model, Commit, Tree, Blob, Branch và HEAD
Series: Git Deep Dive – Từ Command đến Internal Part 1: Git Object Model, Branch và HEAD
1. Introduction
Hầu hết chúng ta sử dụng Git thông qua những command quen thuộc:
git add .
git commit -m "feat: add payment API"
git push origin main
git checkout feature/payment
git merge main
Nhưng có một câu hỏi thú vị:
Git thực sự lưu những gì bên trong
.git?
Ví dụ, khi chạy:
git commit -m "feat: add payment API"
Git có thực sự lưu một bản copy hoàn chỉnh của project ở thời điểm đó không?
Và:
Branch có phải là một bản copy của source code?
Câu trả lời là không.
Ở mức thấp hơn, Git là một content-addressable filesystem với một version control interface được xây dựng bên trên. Git lưu các objects và dùng references để trỏ tới chúng.
Trong bài đầu tiên của series Git Deep Dive, chúng ta sẽ xây dựng mental model về:
- Git Object
- Blob
- Tree
- Commit
- Branch
- HEAD
- Reference (
ref) .git/objects.git/refs- Plumbing vs Porcelain
Mục tiêu không phải nhớ từng command, mà là hiểu:
git add
git commit
git branch
git checkout
git reset
git reflog
thực sự đang thao tác với cái gì.
2. Git không lưu repository theo cách bạn đang nghĩ
Giả sử project:
payment-service/
├── src/
│ ├── PaymentService.java
│ └── PaymentController.java
├── application.yml
└── README.md
Bạn có thể tưởng tượng Git lưu:
Commit 1
└── full project snapshot
Commit 2
└── full project snapshot
Commit 3
└── full project snapshot
Nhưng mental model này không chính xác.
Git xây dựng history từ các objects:
Commit
│
▼
Tree
/ | \
/ | \
Blob Blob Tree
│
Blob
Ba object type quan trọng trong model này là:
- Blob — nội dung file
- Tree — cấu trúc directory
- Commit — metadata + pointer tới tree và parent commit
Git documentation mô tả Git repository như một content-addressable filesystem và các object được lưu trong object database.
3. Git Object là gì?
Git object có thể hiểu đơn giản là:
key → value
Git lấy content, tạo ra một identifier dựa trên content đó và dùng identifier này để truy xuất object.
Ví dụ:
echo "Hello Git" | git hash-object -w --stdin
Git có thể trả về:
8b137891791fe96927ad78e64b0aad7bded08bdc
Đây là object ID.
Có thể kiểm tra object:
git cat-file -t 8b137891791fe96927ad78e64b0aad7bded08bdc
và:
git cat-file -p 8b137891791fe96927ad78e64b0aad7bded08bdc
git cat-file cho phép inspect type, size hoặc nội dung của Git object.
Lưu ý: Git hiện đại có thể sử dụng SHA-256 repository, nhưng SHA-1 vẫn là object format phổ biến và được Git documentation mô tả trong nhiều tài liệu. Khi học Git internals, điều quan trọng là hiểu content-addressing, không phải phụ thuộc vào việc identifier là SHA-1 hay SHA-256.
4. Blob — Git lưu nội dung file như thế nào?
Blob viết tắt của Binary Large Object.
Một blob đại diện cho nội dung của một file, chứ không chứa:
- filename
- directory path
- permission metadata theo nghĩa đầy đủ của file system
Ví dụ:
PaymentService.java
public class PaymentService {
...
}
Git có thể lưu phần content đó dưới dạng:
Blob
│
└── "public class PaymentService { ... }"
Hai file khác nhau nhưng có cùng nội dung có thể reference cùng một blob object.
Ví dụ:
src/A.txt
src/B.txt
cùng chứa:
hello
thì về mặt object model, chúng có thể cùng trỏ tới cùng blob.
Đây là một trong những lý do Git có thể tránh lưu trùng lặp content.
Git documentation minh họa git hash-object tạo và lưu blob object vào .git/objects.
5. Tree — Ai lưu filename và directory?
Nếu Blob chỉ lưu content:
"public class PaymentService..."
thì Git phải biết:
src/PaymentService.java
ở đâu.
Đây là nhiệm vụ của Tree object.
Có thể hình dung:
Tree: project root
│
├── README.md ───────────→ Blob
│
├── application.yml ─────→ Blob
│
└── src/ ────────────────→ Tree
│
├── PaymentService.java → Blob
└── PaymentController.java → Blob
Tree lưu cấu trúc filesystem và references tới các blobs hoặc trees khác.
Do đó:
Blob
→ content
Tree
→ directory structure + filename + references
Git Internals documentation mô tả commit trỏ tới top-level tree và tree tiếp tục trỏ tới các object bên dưới.
6. Commit — Snapshot được mô tả bằng metadata
Một commit không đơn giản là:
"source code version #123"
Commit object chứa các thông tin quan trọng như:
tree <tree-sha>
parent <parent-commit-sha>
author <author>
committer <committer>
commit message
Mental model:
Commit
├── parent → previous Commit
├── tree → root Tree
├── author
├── committer
└── message
Ví dụ:
Commit C
├── parent → Commit B
└── tree → Tree C
Commit B:
Commit B
├── parent → Commit A
└── tree → Tree B
Vì vậy Git history thực chất là một object graph.
Commit A
│
▼
Commit B
│
▼
Commit C
và mỗi commit lại trỏ xuống một tree:
Commit C
│
▼
Tree C
├── Blob
├── Blob
└── Tree
└── Blob
Git's own documentation mô tả ba object chính blob, tree và commit được lưu trong object database và commit object reference tới tree cũng như các commit trước đó.
7. Vậy một git commit thực sự làm gì?
Giả sử bạn sửa:
PaymentService.java
Sau đó:
git add PaymentService.java
và:
git commit -m "feat: add payment validation"
Mental model đơn giản hóa:
Working Tree
│
│ git add
▼
Index
│
│ git commit
▼
Tree
│
▼
Commit
Ở mức thấp, Git có thể sử dụng các plumbing operations để:
content
↓
hash-object
↓
blob
index
↓
write-tree
↓
tree
tree + metadata + parent
↓
commit
Git Book giải thích rằng git add và git commit ở mức conceptual sẽ tạo blobs, update index, write trees và tạo commit objects.
Đây cũng chính là lý do câu hỏi trắc nghiệm về:
git hash-object
git update-index
git write-tree
liên quan trực tiếp tới git commit.
8. Plumbing và Porcelain
Đây là terminology rất quan trọng khi học Git internals.
Porcelain
Đây là những command mà developer thường sử dụng:
git add
git commit
git merge
git rebase
git checkout
git switch
git status
Có thể hiểu:
Porcelain = user-facing Git commands
Plumbing
Đây là các low-level commands để thao tác với Git internals:
git hash-object
git update-index
git write-tree
git commit-tree
git cat-file
git update-ref
Có thể hiểu:
Plumbing = building blocks bên dưới Git
Git documentation gọi các low-level commands này là plumbing commands và giải thích rằng chúng giúp expose cách Git hoạt động bên trong.
Mental model:
Porcelain
┌────────────────────────┐
│ git add │
│ git commit │
│ git merge │
│ git rebase │
└───────────┬────────────┘
│
▼
Plumbing
┌────────────────────────┐
│ hash-object │
│ update-index │
│ write-tree │
│ commit-tree │
│ update-ref │
└───────────┬────────────┘
│
▼
Git Object Database
Không cần dùng plumbing commands hàng ngày.
Nhưng hiểu chúng giúp giải thích tại sao Git hoạt động như hiện tại.
9. .git thực sự chứa gì?
Chạy:
ls -la .git
Bạn sẽ thấy các thành phần như:
.git/
├── HEAD
├── config
├── index
├── objects/
├── refs/
├── hooks/
└── ...
Trong đó bốn thành phần đặc biệt quan trọng:
HEAD
index
objects/
refs/
Git Book mô tả objects, refs, HEAD và index là những thành phần cốt lõi để hiểu Git internals.
10. objects/ — nơi Git lưu dữ liệu
.git/
└── objects/
├── 12/
├── 4f/
├── 8a/
└── ...
Đây là object database.
Các object có thể là:
blob
tree
commit
tag
Git ban đầu có thể lưu objects dưới dạng loose objects và sau đó có thể pack chúng thành packfiles để tiết kiệm storage và cải thiện efficiency.
Vì vậy khi repository lớn, bạn có thể gặp:
.git/objects/pack/
pack-xxxx.pack
pack-xxxx.idx
Phần này sẽ được đào sâu ở Part 6 – Git Garbage Collection & Packfile.
11. Branch thực sự là gì?
Đây là một trong những misconception phổ biến nhất:
Branch không phải là một copy của source code.
Ví dụ:
A ─── B ─── C
↑
main
main chỉ là một reference trỏ tới commit C.
Có thể hình dung:
main
│
▼
Commit C
Khi tạo branch:
git branch feature/payment
Git không copy toàn bộ project.
Nó chỉ tạo một reference mới:
┌── main
│
A ─── B ─── C
│
└── feature/payment
Cả hai branch cùng trỏ tới C.
Git references được dùng để lưu các tên dễ nhớ thay cho việc phải nhớ trực tiếp object ID. Các branch nằm dưới hệ thống refs và trỏ tới commit trong object database.
12. Khi commit trên feature branch thì sao?
Ban đầu:
A ─── B ─── C
↑
main, feature
Bạn checkout:
git switch feature
sau đó commit:
git commit -m "feat: payment"
Git tạo commit mới:
A ─── B ─── C ─── D
↑ ↑
main feature
Điều quan trọng:
main
→ C
feature
→ D
Branch chỉ cần update pointer.
Không cần copy:
src/
target/
resources/
...
Đây là lý do tạo branch trong Git rất lightweight.
13. HEAD là gì?
Nếu branch là pointer:
main → C
thì Git cần biết:
"Hiện tại tôi đang đứng ở branch nào?"
Đó là nhiệm vụ của:
HEAD
Thông thường:
HEAD
↓
main
↓
Commit C
Git documentation mô tả HEAD là symbolic reference trỏ tới branch hiện tại trong trạng thái thông thường.
Bạn có thể xem:
cat .git/HEAD
Có thể nhận:
ref: refs/heads/main
Nghĩa là:
HEAD
│
▼
refs/heads/main
│
▼
commit C
14. HEAD detached là gì?
Thông thường:
HEAD
↓
main
↓
C
Nhưng nếu:
git checkout <commit-sha>
Git có thể đưa bạn vào detached HEAD:
HEAD
↓
Commit C
Không còn:
HEAD → branch → commit
mà là:
HEAD → commit
Đây là lý do Git gọi trạng thái này là detached HEAD.
Mental model này rất quan trọng để hiểu tiếp:
git reset
git checkout
git switch
git rebase
git reflog
15. Refs — Branch thực chất được lưu ở đâu?
Có thể kiểm tra:
find .git/refs -type f
Bạn có thể thấy:
.git/refs/heads/main
.git/refs/heads/feature/payment
Nội dung một ref có thể đơn giản là một commit ID:
abc123...
Mental model:
.git/refs/heads/main
│
▼
commit SHA
│
▼
Commit
Git Book mô tả refs là các named pointers vào object database; local branches được lưu dưới refs/heads/.
16. Ghép tất cả lại
Đây là diagram quan trọng nhất của bài.
HEAD
│
▼
refs/heads/main
│
▼
Commit C
/ \
/ \
parent tree
│ │
▼ ▼
Commit B Tree C
│ / \
▼ / \
Commit A Blob Tree
│
▼
Blob
Nếu có feature branch:
HEAD
│
▼
refs/heads/feature
│
▼
Commit D
│
│ parent
▼
Commit C
/ \
/ \
refs/heads/main Tree C
│
▼
Commit C
Đây chính là mental model bạn cần giữ trong đầu.
17. git log thực chất đang làm gì?
Khi chạy:
git log main
Git có thể bắt đầu từ:
refs/heads/main
↓
Commit C
Sau đó đi theo:
parent
↓
Commit B
↓
Commit A
Vì vậy Git history không phải một danh sách commit độc lập.
Nó là một graph traversal từ một reference.
branch
↓
commit
↓
parent
↓
parent
↓
parent
Đây cũng là nền tảng để hiểu:
git log
git merge-base
git rebase
git cherry-pick
git reset
18. Tại sao Rebase lại tạo commit mới?
Giả sử:
A ─── B ─── C main
\
D ─── E feature
Bạn:
git rebase main
Git không đơn giản "di chuyển D và E".
Nó tạo các commit mới:
A ─── B ─── C ─── D' ─── E'
Trong đó:
D' ≠ D
E' ≠ E
về mặt commit identity.
Commit có metadata và parent references; thay đổi parent sẽ làm commit object khác.
Đây chính là lý do rebase là history rewriting operation.
Và đây cũng giải thích tại sao trong bài trắc nghiệm:
Rebase có thể dẫn tới duplicated commit messages khi rewritten history sau đó được merge/reintegrate không đúng cách.
19. Một experiment nhỏ để tự kiểm chứng
Tạo repository:
mkdir git-internals-demo
cd git-internals-demo
git init
Tạo file:
echo "hello git" > hello.txt
Hash file:
git hash-object -w hello.txt
Git trả về một object ID.
Kiểm tra type:
git cat-file -t <object-id>
Kết quả:
blob
Xem content:
git cat-file -p <object-id>
Kết quả:
hello git
Bạn vừa tạo trực tiếp một Git blob object.
Không cần:
git add
git commit
Đây chính là điểm khác biệt giữa plumbing và porcelain.
20. Sau đó thử git add
git add hello.txt
Kiểm tra:
git status
File hiện tại đã được đưa vào staging area.
Có thể tiếp tục khám phá index:
git ls-files --stage
Bạn sẽ thấy thông tin tương tự:
100644 <blob-sha> 0 hello.txt
Mental model:
hello.txt
│
▼
blob
│
▼
index
Đây sẽ là nội dung trung tâm của Part 2 – Git Index & Staging Area.
21. Một misconception rất phổ biến
❌ Sai
Branch là một copy của project.
✅ Đúng
Branch là một reference/pointer tới một commit.
❌ Sai
Commit chứa toàn bộ source code.
✅ Gần đúng hơn
Commit reference tới một tree đại diện cho snapshot của filesystem tại thời điểm commit, cùng với metadata và parent commit.
❌ Sai
.gitchỉ chứa lịch sử commit.
✅ Đúng
.git chứa nhiều thành phần quan trọng:
objects
refs
HEAD
index
config
hooks
...
❌ Sai
git addchỉ đơn giản copy file vào.git.
✅ Đúng hơn
git add cập nhật index/staging area và Git có thể tạo/đưa content vào object database dưới dạng blob.
22. Production perspective
Tại sao Backend Developer nên quan tâm những thứ này?
Vì các vấn đề Git khó trong production/team workflow thường không nằm ở:
git add .
git commit
git push
mà nằm ở:
"Commit của tôi đâu?"
"Vì sao rebase làm mất commit?"
"Tại sao branch này có duplicate commits?"
"Tại sao reset --hard không thực sự làm mất object ngay?"
"Tại sao reflog có thể cứu commit?"
"Tại sao force push làm thay đổi remote branch?"
"Tại sao repository CI runner ngày càng lớn?"
Nếu hiểu:
Object
+
Reference
+
HEAD
+
Index
thì các vấn đề trên trở nên dễ reasoning hơn.
23. Mental Model cần nhớ
Nếu chỉ nhớ một diagram sau bài này, hãy nhớ diagram này:
HEAD
│
▼
Branch / Ref
│
▼
Commit
/ \
/ \
parent tree
│ │
▼ ▼
Commit Tree
│
┌──────┴──────┐
▼ ▼
Blob Tree
│ │
▼ ▼
file data Blob
Và nhớ:
Branch ≠ copy of code
Branch = pointer/reference → Commit
Commit → Tree
Tree → Blob / Tree
HEAD → current branch
.git/objects → Git objects
.git/refs → references
index → staging area
24. Key Takeaways
Sau Part 1, bạn nên trả lời được 10 câu hỏi sau mà không cần học thuộc:
- Git lưu content ở đâu?
- Blob là gì?
- Tree là gì?
- Commit chứa gì?
- Commit liên kết với commit trước như thế nào?
- Branch có phải copy của project không?
- Branch thực chất trỏ tới cái gì?
- HEAD là gì?
refs/heads/maincó ý nghĩa gì?- Plumbing và Porcelain khác nhau như thế nào?
Nếu trả lời được:
Blob → file content
Tree → directory structure
Commit → snapshot metadata + parent + tree
Branch → pointer/reference → commit
HEAD → current branch/reference
Index → staging area
Objects → object database
Refs → named pointers
thì bạn đã có mental model đủ tốt để bước sang các command khó hơn.
25. Kết luận
Git nhìn bên ngoài có vẻ là một tập hợp command:
git add
git commit
git branch
git merge
git rebase
git reset
Nhưng bên dưới, Git có một model khá elegant:
Content
↓
Objects
↓
Commits + Trees + Blobs
↓
References
↓
Branches
↓
HEAD
Khi hiểu được model này, Git không còn là việc ghi nhớ:
"Command này dùng trong trường hợp nào?"
mà chuyển thành:
"Git đang thay đổi object, reference hay index nào?"
Đó là mental model quan trọng nhất để đi từ Git user → Git power user.
Series tiếp theo
Git Deep Dive #2: Staging Area không đơn giản là nơi git add — Hiểu Git Index
Ở phần tiếp theo chúng ta sẽ đi sâu vào:
Working Tree
↓
Index
↓
HEAD
và giải thích tại sao khi bạn:
modify
git add
modify again
git status
Git lại có thể hiển thị cùng một file ở cả staged và unstaged changes — một trong những câu hỏi quan trọng trong bộ trắc nghiệm ở trên.
Sources
All rights reserved