Bài 1 chúng ta thấy commit trỏ tới 1 tree snapshot. Nhưng giữa lúc bạn sửa file trên đĩa và lúc snapshot đó thực sự được tạo, Git không nhảy thẳng một bước — nó đi qua 3 cây (three trees) tách biệt. Hiểu rõ 3 cây này giải thích được gần như mọi câu hỏi "tại sao git status lại nói vậy" mà người mới hay gặp.

1. Ba Cây Của Git: Working Directory, Staging Index, HEAD

Tại bất kỳ thời điểm nào, Git đang theo dõi 3 phiên bản song song của repo:

  • Working Directory — các file thật trên đĩa, nơi bạn mở editor và gõ code. Đây là thứ duy nhất bạn có thể sửa trực tiếp.
  • Staging Index (còn gọi là "the index", lưu tại .git/index) — một bản nháp của snapshot tiếp theo, được xây dần bằng lệnh git add.
  • HEAD — snapshot của commit gần nhất, tức nội dung repo tại lần commit cuối cùng.

Luồng đi luôn theo một chiều: Working Directory → (add) → Staging Index → (commit) → HEAD. Không có đường tắt nào bỏ qua Staging Index.

2. Working Directory — Nơi Bạn Thực Sự Chỉnh Sửa File

Working Directory chỉ là các file bình thường trên ổ đĩa. Git liên tục so sánh nội dung ở đây với Staging Index để biết file nào "đã sửa nhưng chưa add" — nhưng bản thân việc sửa file không tự động ghi gì vào .git/objects cả. Cho tới khi bạn chạy git add, Git coi như chưa có chuyện gì xảy ra.

3. Staging Index — Bản Nháp Trước Khi Commit

Đây là khái niệm hay bị bỏ qua nhất: Staging Index là một file thật (.git/index), không phải một khái niệm trừu tượng. Nó lưu một danh sách phẳng: mỗi entry gồm đường dẫn file + hash blob tương ứng + vài metadata (mode, kích thước, thời gian sửa đổi gần nhất). Khi bạn git add, Git hash nội dung NGAY LÚC ĐÓ và ghi entry mới vào index — nó không giữ tham chiếu "sống" tới file trên đĩa.

🔬 Đào sâu: Vì sao Git cần lưu cả mtime/kích thước trong index?
Nếu git status phải băm lại NỘI DUNG của mọi file trong Working Directory mỗi lần chạy để so sánh với index, thao tác này sẽ chậm dần theo số lượng file trong repo. Thay vào đó, Git lưu kèm mtime + kích thước của file tại thời điểm stage. Lần status tiếp theo, nếu mtime/kích thước trên đĩa khớp với giá trị đã lưu, Git bỏ qua việc băm lại và coi như "chắc chắn không đổi" — chỉ băm lại khi có dấu hiệu khả nghi. Đây là lý do git status vẫn nhanh trên repo hàng chục nghìn file.

4. git add — Chụp Nhanh Một File Vào Staging

Chạy git add <file> làm đúng 2 việc: (1) băm nội dung file hiện tại và ghi 1 blob object mới vào .git/objects (nếu nội dung đó chưa tồn tại — xem lại content-addressable ở Bài 1), và (2) cập nhật entry tương ứng trong .git/index để trỏ tới blob hash đó:

terminal
$ echo "version 1" > notes.txt
$ git add notes.txt        # hash "version 1" NGAY LÚC NÀY, ghi vào index

$ echo "version 2" >> notes.txt   # sửa TIẾP file trên đĩa
$ git status
# notes.txt xuất hiện ở CẢ 2 mục:
#   Changes to be committed:    notes.txt   (staging vs HEAD — bản "version 1")
#   Changes not staged for commit: notes.txt (working dir vs staging — bản "version 2")
🕳️ Cạm bẫy thường gặp: "Tôi add rồi, sao commit thiếu dòng mới?"
Đây là cạm bẫy phổ biến nhất với người mới: git add chụp nhanh nội dung file tại đúng thời điểm chạy lệnh. Nếu bạn sửa thêm file sau đó rồi git commit ngay mà không add lại, commit sẽ chỉ chứa phiên bản đã add trước đó — hoàn toàn bỏ sót các dòng sửa thêm gần nhất. Luôn git add lại (hoặc dùng git commit -a cho file đã theo dõi) trước khi commit lần cuối.

5. git commit — Đóng Băng Staging Thành Một Snapshot Vĩnh Viễn

Khi chạy git commit, Git xây một tree object từ toàn bộ entry hiện có trong Staging Index (xem cách xây tree ở Bài 1), tạo commit object trỏ tới tree đó và commit cha là HEAD hiện tại, rồi di chuyển con trỏ HEAD/branch tới commit mới. Ngay sau bước này, Staging Index và HEAD lại khớp nhau hoàn toàn — cho tới lần add kế tiếp.

6. git status & git diff: So Sánh 3 Cây

Mọi thông tin git status hiển thị đều là kết quả so sánh cặp giữa 3 cây — không có phép màu nào khác:

  • Changes not staged for commit = so sánh Working Directory với Staging Index (đúng như git diff không tham số làm).
  • Changes to be committed = so sánh Staging Index với HEAD (đúng như git diff --staged làm).

7. So Sánh 3 Cây

Cây Đại diện cho Cập nhật bởi lệnh nào
Working Directory File thật trên đĩa, đang chỉnh sửa Editor của bạn (Git không can thiệp)
Staging Index Bản nháp của snapshot tiếp theo git add, git rm --cached, git reset
HEAD Snapshot của commit gần nhất git commit, git checkout, git reset --hard

Sân chơi tương tác: Three Trees Visualizer

Sửa nội dung 2 file, bấm "Add" cho từng file rồi "Commit", và quan sát cả 3 cây cập nhật trực tiếp. Thử đúng kịch bản cạm bẫy ở mục 4: add 1 file, sửa thêm nội dung, rồi xem trạng thái "vừa staged vừa unstaged" xuất hiện thế nào.

🌳 Sân chơi tương tác: Three Trees Visualizer

Working Directory

Nhật ký

Sửa file KHÔNG tự add — bấm "Add" mới cập nhật Staging Index.

three-trees-live.js

Trắc nghiệm ôn tập

Câu 1: Bạn git add file.txt, sau đó sửa thêm 1 dòng trong file.txt (KHÔNG add lại), rồi git commit ngay. Commit chứa nội dung nào?

Trắc nghiệm ôn tập

Câu 2: git diff (không tham số) và git diff --staged so sánh những cặp cây nào?

Trắc nghiệm ôn tập

Câu 3: Vì sao Git cần một Staging Index riêng, thay vì commit thẳng mọi thứ đang có trên Working Directory?

📖 Tài liệu tham khảo / References

Bài viết liên quan trong series

Bài 3: Branch & HEAD Bài 1: Mô Hình Đối Tượng Git Quay lại Lộ trình Series Git