Zoom-in: Git Commit

git commit -m "fix bug" — một lệnh. Bên dưới là một cấu trúc dữ liệu bất biến đã tồn tại gần 20 năm mà không cần thay đổi gì lớn. Hiểu nó giải thích tại sao rebase, cherry-pick, merge hoạt động như chúng vốn có — và tại sao "xóa commit" trong Git thực ra không xóa gì cả.
Phóng to dần vào đó.
Layer 1 — Blob: lưu nội dung file
Git không lưu diff. Git lưu snapshot toàn bộ nội dung.
graph LR
F["📄 hello.txt\n'Hello, world!'"] -->|"SHA-1 hash"| B["🗂️ Blob\nsha: a8c3f..."]
style F fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style B fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
Mỗi file được hash bằng SHA-1 (Git mới hơn dùng SHA-256). Kết quả là một blob — object lưu nội dung file, không có tên, không có metadata. Hai file có nội dung giống nhau → cùng một blob, không lưu hai lần.
Nội dung thay đổi → hash thay đổi → blob mới. Blob cũ không bị xóa — Git là append-only bởi thiết kế.
Layer 2 — Tree: lưu cấu trúc thư mục
Tree object là snapshot của một thư mục tại một thời điểm.
graph TD
T["🌳 Tree (root)\nsha: 9f2a1..."]
T -->|"README.md"| B1["🗂️ Blob\nsha: a8c3f..."]
T -->|"src/"| T2["🌳 Tree\nsha: 4d7e8..."]
T2 -->|"index.ts"| B2["🗂️ Blob\nsha: c91b2..."]
T2 -->|"utils.ts"| B3["🗂️ Blob\nsha: 7f4a9..."]
style T fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style T2 fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style B1 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style B2 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style B3 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
Tree lưu danh sách các entry: tên, permissions, và SHA-1 của blob hoặc tree con. Thư mục lồng nhau = tree trỏ đến tree khác.
Nếu chỉ thay đổi utils.ts, Git tạo blob mới cho utils.ts, tree mới cho src/, tree mới cho root — nhưng index.ts và README.md vẫn trỏ về blob cũ. Không copy, không duplicate.
Layer 3 — Commit object: metadata và lịch sử
Commit object bọc tree lại và thêm context.
graph LR
C["📦 Commit\nsha: 1a2b3c...\n---\ntree: 9f2a1...\nparent: 8e4f7...\nauthor: Nam\ndate: 2026-06-25\nmessage: fix bug"]
C -->|"trỏ đến"| T["🌳 Tree\nsha: 9f2a1..."]
C -->|"trỏ đến"| P["📦 Parent commit\nsha: 8e4f7..."]
style C fill:#1a3a2a,stroke:#22c55e,color:#86efac
style T fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style P fill:#1a3a2a,stroke:#22c55e,color:#86efac
Commit chứa: SHA-1 của tree (snapshot hiện tại), SHA-1 của parent commit (commit trước đó), author, timestamp, và message.
SHA-1 của commit được tính từ toàn bộ nội dung trên — bao gồm cả parent SHA. Thay đổi bất cứ thứ gì (message, timestamp, parent) → SHA-1 của commit thay đổi. Đây là lý do git commit --amend tạo ra một commit mới, không sửa commit cũ.
Layer 4 — Branch: chỉ là một con trỏ
Branch không phải container, không phải copy. Branch là một file chứa một SHA-1.
graph LR
Main["🔖 main\n→ C3"] --> C3["📦 Commit C3\nsha: 1a2b3c"]
Feature["🔖 feat/login\n→ C4"] --> C4["📦 Commit C4\nsha: 7d9e2f"]
C3 --> C2["📦 Commit C2\nsha: 8e4f7a"]
C4 --> C2
C2 --> C1["📦 Commit C1\nsha: 3c8d1e"]
style Main fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Feature fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style C1 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style C2 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style C3 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style C4 fill:#1a3a2a,stroke:#22c55e,color:#86efac
Tạo branch mới: Git tạo một file nhỏ trong .git/refs/heads/ chứa SHA-1 của commit hiện tại. Tốn đúng vài byte. Commit mới: con trỏ branch được cập nhật sang SHA-1 mới.
Điều này giải thích tại sao:
- Tạo/xóa branch trong Git rất nhanh — chỉ tạo/xóa một file nhỏ
git rebasethay đổi parent → toàn bộ chuỗi commit sau đó có SHA-1 mới → lịch sử "viết lại"git cherry-picktạo commit mới với cùng diff nhưng SHA-1 khác (vì parent khác)- "Xóa" commit thực ra chỉ là di chuyển con trỏ — commit cũ vẫn còn trong object store cho đến khi
git gcdọn dẹp
Full picture
graph TD
subgraph "Object Store (.git/objects)"
B1["🗂️ Blob: hello.txt"]
B2["🗂️ Blob: index.ts"]
T1["🌳 Tree: src/"]
T2["🌳 Tree: root"]
C1["📦 Commit C1"]
C2["📦 Commit C2"]
end
subgraph "Refs (.git/refs)"
Main["🔖 main → C2"]
HEAD["👁️ HEAD → main"]
end
HEAD --> Main --> C2
C2 -->|"tree"| T2
C2 -->|"parent"| C1
T2 -->|"src/"| T1
T1 -->|"index.ts"| B2
T2 -->|"hello.txt"| B1
style B1 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style B2 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style T1 fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style T2 fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style C1 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style C2 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style Main fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style HEAD fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
Takeaway
Git là một content-addressed object store. Blob lưu nội dung, tree lưu cấu trúc, commit lưu snapshot và lịch sử, branch là con trỏ. SHA-1 là định danh bất biến — thay đổi bất cứ thứ gì đều tạo object mới.
Điều này có hệ quả thực tế: rebase an toàn trên branch cá nhân nhưng nguy hiểm trên branch được share vì nó viết lại SHA-1 của mọi commit — người khác đang dùng SHA-1 cũ sẽ bị conflict. merge giữ nguyên lịch sử, rebase làm lịch sử tuyến tính hơn. Cả hai đều đúng — tùy ngữ cảnh.
Bài viết được hỗ trợ bởi Amy 🌸 - AI Assistant. Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
Zoom-in: TCP
Mọi HTTP request đều đi trên TCP — nhưng trước khi byte đầu tiên của dữ liệu đi qua, đã có 3 gói tin trao đổi mà không mang dữ liệu nào. TCP giải quyết vấn đề mà Internet không giải quyết được.
Zoom-in: OAuth 2.0
'Đăng nhập bằng Google' ẩn sau đó một cơ chế ủy quyền mà không bao giờ để password rời khỏi Google. OAuth 2.0 giải quyết bài toán delegation mà không hi sinh bảo mật.
Zoom-in: Load Balancer
Một domain, hàng triệu request mỗi ngày. Load balancer không chỉ phân phối traffic — nó là điểm quyết định routing, health check, và session management cho toàn bộ hệ thống.