git add, git commit — hai lệnh ai cũng gõ hàng chục lần mỗi ngày. Nhưng câu hỏi "git add thực sự làm gì?" và "index/staging là cái gì?" thì ít người trả lời được. Bài này (phần 2 của loạt "Git nội bộ") mổ xẻ ba vùng của Git bằng lệnh thật, làm rõ index thực chất là gì — thứ mà mọi thao tác staging đụng vào. Hiểu nó giúp bạn đọc đúng git status và dùng đúng git reset/git restore.
Ba vùng và hai lệnh chuyển giữa chúng
Git có ba vùng, và chỉ hai lệnh cơ bản để chuyển dữ liệu giữa chúng:
WORKING DIR ──(git add)──> INDEX (staging) ──(git commit)──> REPOSITORY (HEAD)
file thật danh sách file sẽ commit lịch sử bất biến
Điểm hiểu lầm phổ biến: git add không chỉ "đánh dấu" file. Nó ghi blob (như bài trước) và cập nhật index. git commit thì đóng băng toàn bộ index hiện tại thành một commit — commit lưu đúng những gì đang ở index, không phải working directory.
Index thực chất là gì
Index (staging area) là một file nhị phân ở .git/index. Nó không lưu nội dung file — nó lưu một danh sách: mode + SHA blob + path. Xem bằng git ls-files --stage:
$ git ls-files --stage
100644 626799f0f85326a8c1fc522db584e86cdfccd51f 0 a.txt
# mode SHA của blob (trỏ tới object store) stage path
$ ls -l .git/index -> 137 byte (nhị phân, không đọc thẳng)
Vậy index là một "ảnh chụp phẳng" của lần commit sắp tới: mỗi file trỏ tới một blob đã nằm trong object store. Đây là lý do git add phải ghi blob trước — index chỉ trỏ SHA, không giữ nội dung.

Hình 1: Ba vùng của Git và hai lệnh git add/git commit chuyển giữa chúng; git ls-files --stage cho thấy index lưu SHA blob + mode + path; và hai lệnh diff soi hai "khe" khác nhau.
Ba vùng lệch nhau: đọc git status và git diff cho đúng
Ta sửa một file nhưng chưa add — working khác index, index vẫn bằng HEAD:
$ echo "v2" > a.txt # working thay đổi
$ git status -s -> M a.txt (M ở cột 2 = working khác index, CHƯA add)
$ git diff -> a.txt | 2 +- 1 file changed (WORKING vs INDEX)
$ git diff --cached -> (rỗng) (INDEX vẫn == HEAD)
Giờ git add — nó ghi blob mới và đổi SHA trong index:
$ git add a.txt
$ git ls-files --stage
100644 8c1384d825dbbe41309b7dc18ee7991a9085c46e 0 a.txt # blob "v2" — SHA ĐỔI
$ git status -s -> M a.txt (M ở cột 1 = index khác HEAD, sắp commit)
$ git diff -> (rỗng) (working == index)
$ git diff --cached -> 1 file changed, 1 insertion(+), 1 deletion(-)
Hai điều quan trọng lộ ra. SHA blob trong index đổi từ 626799f (v1) sang 8c1384d (v2) khi add — chứng minh git add thực sự ghi một object mới. Và hai lệnh diff soi hai khe khác nhau: git diff so working vs index (thay đổi chưa add), git diff --cached so index vs HEAD (thay đổi đã add, sắp commit). Đây là lý do nhiều người bối rối "sao git diff rỗng mà vẫn có gì để commit" — vì thay đổi đã ở index rồi.

Hình 2: Chạy thật — index lưu SHA blob (đổi 626799f→8c1384d khi git add); status hai cột ( M=chưa add, M =đã add); git diff so working-vs-index còn --cached so index-vs-HEAD.
Đọc git status hai cột như một chuyên gia
git status -s in hai cột cho mỗi file: cột X = index vs HEAD (đã staged gì), cột Y = working vs index (chưa staged gì). Nhờ đó:
M(cách + M): working khác index — đã sửa, chưa add.M(M + cách): index khác HEAD — đã add, sẵn sàng commit.MM: đã add một phiên bản, rồi lại sửa tiếp — index có v2, working có v3.git commitsẽ commit v2, không phải v3.??: file chưa được Git theo dõi (untracked).
Chỉ cần nhớ "trái = staged, phải = chưa" là bạn đọc được mọi trạng thái.
Đánh đổi cần cân nhắc
Index cho quyền soạn commit gọn — nhưng dễ quên "MM". Sức mạnh của staging là bạn commit đúng thứ đã chọn (thậm chí git add -p add từng đoạn). Cái bẫy: add rồi sửa tiếp thì commit chỉ lấy bản đã add (MM). Nếu định commit trọn thay đổi, nhớ add lại sau khi sửa.
git reset vs git restore — đúng vùng. "Unstage" (đưa từ index về lại working, giữ thay đổi) là git reset HEAD <file> hoặc git restore --staged <file>. Còn "vứt thay đổi ở working" là git restore <file> (mất dữ liệu). Nhầm hai cái là mất việc — hiểu ba vùng giúp chọn đúng lệnh cho đúng vùng.
Một số file lớn trong working không ảnh hưởng index cho tới khi add. Index chỉ chứa thứ được theo dõi/đã add; file untracked (??) không nằm trong index. Đây là lý do .gitignore (bài sau) chỉ tác động tới việc đưa vào index, không đụng file đã theo dõi.
Ba ý mang về
- Git có ba vùng — working, index, repository — và
git add/git commitchỉ chuyển dữ liệu giữa chúng:git addghi blob và cập nhật index (đo thật SHA blob trong index đổi 626799f→8c1384d),git commitđóng băng index thành một commit. - Index là một file nhị phân lưu SHA blob + mode + path (không lưu nội dung): xem bằng
git ls-files --stage; nó là "ảnh chụp phẳng" của commit sắp tới. - Đọc đúng status/diff nhờ hiểu vùng:
git diff= working vs index,git diff --cached= index vs HEAD; status hai cột (trái=staged, phải=chưa) — và chọn đúngreset/restorecho đúng vùng để không mất việc.
Nguồn
- Pro Git — Git Basics: Recording Changes / Git Tools: Reset Demystified: https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
- Git Documentation — git-ls-files / git-status: https://git-scm.com/docs/git-ls-files
Phần sau ta mổ xẻ ref, HEAD và branch — và chứng minh một branch chỉ là một con trỏ 41 byte tới commit, không phải một "bản sao" nặng nề như nhiều người tưởng.