Git Deep Dive #2: Staging Area không đơn giản là nơi git add — Hiểu Git Index
Series: Git Deep Dive – Từ Command đến Internal Part 2: Git Index & Staging Area
1. Introduction
Ở Part 1, chúng ta đã xây dựng mental model về Git Object Model:
Blob
Tree
Commit
Branch
HEAD
Reference
Chúng ta cũng đã biết:
Branch → Commit
Commit → Tree
Tree → Blob
HEAD → Current Branch
Nhưng vẫn còn một thành phần cực kỳ quan trọng chưa được giải thích kỹ:
Git Index là gì?
Hầu hết developer đều sử dụng:
git add .
git commit -m "feat: add payment API"
nhưng thường có mental model đơn giản:
Working Tree
↓
git add
↓
Staging
↓
git commit
↓
Repository
Mental model này đúng ở mức high-level nhưng chưa đủ.
Git thực sự có ba trạng thái quan trọng mà chúng ta cần phân biệt:
HEAD
↓
Index
↓
Working Tree
Git documentation gọi Index là staging area và mô tả rằng đây là nơi chuẩn bị nội dung cho commit tiếp theo. git commit mặc định chỉ commit những thay đổi đang được staged.
Trong bài này chúng ta sẽ tìm hiểu:
- Working Tree là gì?
- HEAD là gì?
- Index là gì?
git addthực sự làm gì?git diffkhácgit diff --cachedthế nào?- Vì sao một file có thể vừa staged vừa unstaged?
git add -phoạt động như thế nào?git resetliên quan gì tới Index?- Index thực sự được lưu ở đâu?
- Vì sao hiểu Index rất quan trọng khi làm việc với Git?
2. Ba trạng thái cần nhớ: HEAD, Index và Working Tree
Đây là mental model quan trọng nhất của Part 2:
┌──────────────┐
│ HEAD │
│ last commit │
└──────┬───────┘
│
│ git diff --cached
▼
┌──────────────┐
│ INDEX │
│ STAGING │
└──────┬───────┘
│
│ git diff
▼
┌──────────────┐
│ WORKING TREE │
│ current file │
└──────────────┘
Có hai comparison quan trọng:
HEAD ↔ Index
và:
Index ↔ Working Tree
Git status sử dụng chính hai vùng difference này để cho biết cái gì sẽ được commit và cái gì vẫn chưa được stage.
Đây chính là chìa khóa để hiểu:
git status
git diff
git diff --cached
3. HEAD là gì?
Ở Part 1, chúng ta đã biết:
HEAD
↓
main
↓
Commit C
HEAD đại diện cho state mà Git đang xem là current committed state của branch hiện tại.
Ví dụ:
A ─── B ─── C
↑
main
↑
HEAD
Ở thời điểm này:
HEAD = Commit C
Nếu bạn không thay đổi file:
HEAD
↓
Index
↓
Working Tree
có cùng nội dung.
Nhưng ngay khi bạn sửa file, ba trạng thái này có thể bắt đầu khác nhau.
4. Working Tree là gì?
Working Tree chính là những file bạn đang nhìn thấy và chỉnh sửa trong project.
Ví dụ:
payment-service/
├── src/
│ └── PaymentService.java
├── application.yml
└── README.md
Bạn mở:
PaymentService.java
và thêm:
validatePayment(request);
Lúc này Working Tree đã thay đổi.
Nhưng:
Git chưa tự động đưa thay đổi đó vào Index.
Có thể hình dung:
HEAD
↓
Version 1
Index
↓
Version 1
Working Tree
↓
Version 2
Vì vậy:
git status
sẽ cho biết file có thay đổi nhưng chưa được staged.
5. Index thực sự là gì?
Đây là phần quan trọng nhất.
Index còn được gọi là:
Index
=
Staging Area
Git documentation định nghĩa Index là nơi bạn chuẩn bị nội dung cho commit tiếp theo. Khi chạy git commit mà không cung cấp option đặc biệt, Git commit nội dung đã được staged trong Index.
Mental model:
Working Tree
│
│ git add
▼
INDEX
│
│ git commit
▼
COMMIT
Nhưng Index không chỉ đơn giản là:
"Một cái temporary folder."
Nó là một data structure/file mà Git sử dụng để lưu trạng thái được chuẩn bị cho commit tiếp theo.
Thông thường nó nằm tại:
.git/index
Bạn có thể kiểm tra:
ls -l .git/index
6. git add thực sự làm gì?
Đây là misconception rất phổ biến:
❌ Sai
git add
=
copy file vào staging folder
✅ Mental model chính xác hơn
Working Tree
│
│ current content
▼
Index
Git documentation mô tả git add là command dùng để add file contents to the index. Nó cập nhật Index bằng content hiện tại của file được chỉ định.
Ví dụ:
git add PaymentService.java
Sau command này:
HEAD
↓
Version 1
INDEX
↓
Version 2
Working Tree
↓
Version 2
Lúc này:
HEAD ≠ Index
Index = Working Tree
Do đó thay đổi đã được staged.
7. git commit lấy dữ liệu từ đâu?
Đây là một điểm rất quan trọng:
git commit
không đơn giản lấy trực tiếp tất cả file trong Working Tree.
Nó commit state đã được chuẩn bị trong Index.
Ví dụ:
HEAD
↓
Version 1
INDEX
↓
Version 2
WORKING TREE
↓
Version 3
Nếu chạy:
git commit -m "feat: payment validation"
commit sẽ phản ánh Version 2, không phải toàn bộ Version 3.
Git documentation xác nhận git commit mặc định chỉ commit staged changes.
Đây chính là lý do Index tồn tại.
8. Tại sao cùng một file có thể vừa staged vừa unstaged?
Đây là câu hỏi trong bộ trắc nghiệm của chúng ta:
Bạn modify file → stage → modify file lần nữa.
git statushiển thị gì?
Đáp án:
File xuất hiện ở cả staged và unstaged changes.
Hãy đi từng bước.
Step 1 — File ban đầu
HEAD
↓
Version 1
INDEX
↓
Version 1
WORKING TREE
↓
Version 1
Tất cả giống nhau.
Step 2 — Modify file
Bạn thay:
Version 1
thành:
Version 2
Lúc này:
HEAD
↓
Version 1
INDEX
↓
Version 1
WORKING TREE
↓
Version 2
Git thấy:
INDEX ≠ WORKING TREE
→ unstaged change
Step 3 — git add
git add PaymentService.java
Index được cập nhật:
HEAD
↓
Version 1
INDEX
↓
Version 2
WORKING TREE
↓
Version 2
Lúc này:
HEAD ≠ INDEX
→ staged change
Step 4 — Modify lần nữa
Bạn tiếp tục sửa file:
Version 2
thành:
Version 3
Bây giờ:
HEAD
↓
Version 1
INDEX
↓
Version 2
WORKING TREE
↓
Version 3
Có hai difference khác nhau:
HEAD
│
│ Version 1 → Version 2
▼
INDEX
│
│ Version 2 → Version 3
▼
WORKING TREE
Do đó:
git status
có thể hiển thị:
Changes to be committed:
modified: PaymentService.java
Changes not staged for commit:
modified: PaymentService.java
Đây không phải Git bị duplicate.
Git đang nói:
"Có một phần thay đổi đã được stage và một phần thay đổi mới chưa được stage."
Git documentation mô tả git status chính xác theo hai comparison: HEAD → Index cho staged changes và Index → Working Tree cho unstaged changes.
9. git diff đang so sánh cái gì?
Đây là chỗ rất nhiều developer nhầm.
git diff
git diff
so sánh:
Index
↕
Working Tree
Nó trả lời câu hỏi:
"Tôi đã sửa gì nhưng chưa
git add?"
Ví dụ:
INDEX = Version 2
WORKTREE = Version 3
thì:
git diff
cho bạn:
Version 2 → Version 3
10. git diff --cached
Ngược lại:
git diff --cached
hoặc:
git diff --staged
so sánh:
HEAD
↕
Index
Nó trả lời:
"Tôi đang chuẩn bị commit những gì?"
Ví dụ:
HEAD = Version 1
INDEX = Version 2
thì:
git diff --cached
cho:
Version 1 → Version 2
Mental model:
git diff
↓
Index ↔ Working Tree
↓
"Chưa stage cái gì?"
git diff --cached
↓
HEAD ↔ Index
↓
"Sắp commit cái gì?"
Đây là một trong những command pair nên thuộc lòng khi làm việc với Git.
11. git status thực chất đang giúp bạn xem gì?
Có thể đơn giản hóa:
HEAD
│
│ staged?
▼
INDEX
│
│ unstaged?
▼
WORKING TREE
git status cho bạn cả hai loại information.
Ví dụ:
Changes to be committed:
modified: PaymentService.java
Changes not staged for commit:
modified: PaymentController.java
Có nghĩa:
PaymentService.java
HEAD → INDEX
có difference.
Trong khi:
PaymentController.java
INDEX → WORKING TREE
có difference.
Git status documentation mô tả chính xác hai loại path này.
12. Tại sao Index hữu ích?
Giả sử bạn sửa cùng lúc:
PaymentService.java
PaymentController.java
PaymentRepository.java
README.md
Nhưng commit hiện tại chỉ muốn:
PaymentService.java
PaymentRepository.java
Bạn có thể:
git add PaymentService.java
git add PaymentRepository.java
Sau đó:
git commit -m "feat: add payment validation"
Commit sẽ không bao gồm:
PaymentController.java
README.md
Index cho phép bạn construct chính xác snapshot mà commit sẽ chứa.
Đây là một trong những điểm khác biệt quan trọng giữa Git và mental model:
"commit tất cả những gì đang nằm trong folder"
Git không hoạt động đơn giản như vậy.
13. git add -p — Stage theo từng phần
Đây là một command rất hữu ích khi làm việc thực tế:
git add -p
-p là:
--patch
Git sẽ hiển thị từng hunk của diff và hỏi bạn có muốn stage hay không.
Ví dụ một file có:
+ validate request
+ calculate fee
+ add logging
Bạn có thể chọn:
y → stage hunk
n → không stage
s → split hunk
e → edit patch
Git documentation xác nhận git add -p cho phép chọn từng phần của thay đổi để đưa vào Index.
Đây là workflow rất hữu ích:
Một file
│
├── Bug fix
├── Refactor
└── Debug logging
Bạn có thể stage:
Bug fix
nhưng không stage:
Refactor
Debug logging
Sau đó tạo commit sạch hơn:
git commit -m "fix: validate payment request"
14. Tại sao git add -p quan trọng trong production?
Hãy tưởng tượng bạn đang xử lý production bug.
Trong lúc debug, bạn vô tình thay đổi:
PaymentService.java
1. Fix timeout bug
2. Refactor method
3. Add debug logging
4. Change variable naming
Nếu:
git add .
git commit
bạn có thể tạo một commit chứa:
bug fix
+
refactor
+
debug code
+
unrelated changes
Điều này khiến:
- Code review khó hơn
git revertkhó hơngit cherry-pickkhó hơn- Debug history khó hơn
- Rollback khó hơn
Thay vào đó:
git add -p
giúp bạn tạo atomic commits.
Ví dụ:
Commit 1
fix: handle payment timeout
Commit 2
refactor: simplify payment validation
Đây là một trong những kỹ năng Git thực tế đáng giá hơn việc nhớ hàng chục command.
15. Index và git reset
Ở Part 3 chúng ta sẽ đi sâu vào:
git reset
nhưng ở đây cần hiểu một phần quan trọng:
git resetcó thể thay đổi HEAD và/hoặc Index tùy mode.
Ví dụ:
git reset HEAD~1
mặc định là mixed reset.
Mental model đơn giản:
HEAD
↓
previous commit
INDEX
↓
được cập nhật theo HEAD
WORKING TREE
↓
giữ lại changes
Trong khi:
git reset --soft HEAD~1
di chuyển HEAD nhưng giữ Index.
HEAD
↓
previous commit
INDEX
↓
giữ staged state
WORKING TREE
↓
giữ nguyên
Và:
git reset --hard HEAD~1
có thể đưa cả Index và Working Tree về state của commit được chọn.
Phần này sẽ được phân tích đầy đủ ở Part 3.
16. Index có phải là Commit không?
Không.
Đây là distinction rất quan trọng:
Commit
=
immutable object trong Git object database
Index
=
mutable staging state
Có thể hình dung:
Commit
/ \
/ \
immutable history
↑
│
Index
↑
│
Working Tree
Index có thể thay đổi nhiều lần trước khi commit:
git add A.java
git add B.java
git add -p
git reset A.java
git add C.java
Sau tất cả những thao tác này:
HEAD
có thể vẫn không thay đổi.
Nhưng:
INDEX
đã thay đổi nhiều lần.
17. Index có phải là object trong .git/objects không?
Không nên hiểu Index như một Git object kiểu:
blob
tree
commit
tag
Index là một file dữ liệu riêng trong .git:
.git/index
Nó lưu thông tin mà Git cần để biết:
"Snapshot nào đang được chuẩn bị cho commit tiếp theo?"
Đây là lý do khi học Git internals cần phân biệt:
Git Object Database
├── blob
├── tree
├── commit
└── tag
Git Repository State
├── HEAD
├── refs
└── index
Không nên gom tất cả thành "Git object".
18. git ls-files --stage — Nhìn vào Index
Bạn có thể inspect Index bằng:
git ls-files --stage
Ví dụ:
100644 8a7c... 0 src/PaymentService.java
100644 b72d... 0 src/PaymentController.java
Có thể đọc mental model:
mode
+
blob object ID
+
stage number
+
path
Điều này cho thấy Index đang giữ references tới content objects.
Ở mức đơn giản:
Index
│
├── PaymentService.java
│ ↓
│ Blob A
│
├── PaymentController.java
│ ↓
│ Blob B
│
└── application.yml
↓
Blob C
Đây là cầu nối trực tiếp giữa:
Part 1 — Object Model
và:
Part 2 — Index
19. Một experiment hoàn chỉnh
Hãy thử một repository nhỏ:
mkdir git-index-demo
cd git-index-demo
git init
Tạo file:
echo "version 1" > app.txt
git add app.txt
git commit -m "init"
Bây giờ:
HEAD
↓
version 1
INDEX
↓
version 1
WORKTREE
↓
version 1
Modify lần 1
echo "version 2" > app.txt
Kiểm tra:
git status
Bạn có:
HEAD = version 1
INDEX = version 1
WORKTREE = version 2
Chạy:
git diff
Bạn thấy:
version 1
→ version 2
Stage
git add app.txt
Bây giờ:
HEAD = version 1
INDEX = version 2
WORKTREE = version 2
Chạy:
git diff
→ không còn unstaged diff.
Nhưng:
git diff --cached
→ thấy:
version 1
→ version 2
Modify lần 2
echo "version 3" > app.txt
Bây giờ:
HEAD = version 1
INDEX = version 2
WORKTREE = version 3
Chạy:
git diff
→
version 2
→ version 3
Chạy:
git diff --cached
→
version 1
→ version 2
Và:
git status
→ file xuất hiện ở cả:
Changes to be committed
và:
Changes not staged for commit
Đây chính là câu hỏi số 18 trong bộ trắc nghiệm.
20. Một mental model cực kỳ hữu ích
Thay vì nhớ:
git diff
git diff --cached
git status
hãy nhớ:
HEAD
│
│
git diff --cached
│
▼
INDEX
│
│
git diff
│
▼
WORKING TREE
Sau đó mọi command trở nên dễ suy luận hơn.
Ví dụ:
Muốn xem thứ sắp commit?
git diff --cached
Muốn xem thứ chưa stage?
git diff
Muốn xem tổng quan?
git status
Muốn đưa Working Tree vào Index?
git add
Muốn đưa một phần Working Tree vào Index?
git add -p
21. Những misconception cần loại bỏ
❌ git add = commit
Không.
git add
→ Working Tree → Index
git commit
→ Index → Commit
❌ Index = Working Tree
Không.
Working Tree là file bạn đang chỉnh sửa.
Index là snapshot đang được chuẩn bị cho commit tiếp theo.
❌ git commit luôn commit toàn bộ file đã sửa
Không.
Commit mặc định sử dụng content đã staged trong Index.
❌ git diff cho biết tất cả thay đổi
Không chính xác.
git diff mặc định tập trung vào:
Index ↔ Working Tree
Trong khi:
git diff --cached
cho:
HEAD ↔ Index
❌ Nếu file xuất hiện hai lần trong git status, Git bị duplicate
Không.
Nó có thể có:
HEAD = Version 1
INDEX = Version 2
WORKTREE = Version 3
nên có hai different states.
22. Production Perspective
Index tưởng như là một concept nhỏ nhưng ảnh hưởng trực tiếp tới chất lượng Git workflow.
22.1 Atomic Commit
Một commit nên trả lời được:
"Commit này thay đổi một logical unit nào?"
Ví dụ tốt:
fix: handle payment timeout
refactor: simplify provider validation
test: add payment timeout tests
thay vì:
fix things
và chứa 20 loại thay đổi không liên quan.
22.2 Code Review
Nếu commit được chuẩn bị bằng:
git add -p
reviewer có thể dễ dàng hiểu:
Commit A
→ business logic
Commit B
→ refactor
Commit C
→ tests
thay vì một commit khổng lồ.
22.3 Cherry-pick
Atomic commit đặc biệt hữu ích khi cần:
git cherry-pick <commit>
Ví dụ production branch chỉ cần:
fix: handle payment timeout
Nếu commit chứa:
fix
+
refactor
+
format
+
unrelated changes
thì cherry-pick trở nên risky hơn.
22.4 Rollback
Commit nhỏ và có scope rõ ràng giúp:
git revert <commit>
dễ kiểm soát hơn.
Đây là lý do Git skill không chỉ là:
"Biết command nào."
Mà còn là:
Biết cách construct một clean history.
23. Senior Developer sẽ nhìn Index như thế nào?
Junior thường nghĩ:
git add .
git commit
Senior thường nghĩ:
What exactly should this commit contain?
Sau đó sử dụng:
git status
git diff
git diff --cached
git add -p
git restore --staged
để kiểm soát commit boundary.
Đây là một khác biệt nhỏ về command nhưng khá lớn về Git workflow.
24. Cheat Sheet
| Command | So sánh / Thao tác |
|---|---|
git status |
Xem HEAD ↔ Index và Index ↔ Working Tree |
git diff |
Index ↔ Working Tree |
git diff --cached |
HEAD ↔ Index |
git add file |
Working Tree → Index |
git add -p |
Chọn từng hunk → Index |
git commit |
Index → Commit |
git restore --staged file |
Loại file khỏi staged state |
git reset |
Có thể thay đổi HEAD và/hoặc Index tùy mode |
git ls-files --stage |
Inspect Index |
25. Key Takeaways
Sau Part 2, bạn nên có mental model:
HEAD
│
│ "What was committed?"
▼
INDEX
│
│ "What will be committed?"
▼
WORKING TREE
│
│ "What am I currently editing?"
▼
Developer
Và:
git add
↓
Working Tree → Index
git commit
↓
Index → Commit
git diff
↓
Index ↔ Working Tree
git diff --cached
↓
HEAD ↔ Index
Quan trọng nhất:
Index không phải một bước trung gian vô nghĩa giữa
git addvàgit commit.
Nó là cơ chế cho phép Git tách:
"những gì tôi đang sửa"
khỏi:
"những gì tôi muốn commit"
Chính khả năng này cho phép chúng ta tạo atomic commits, kiểm soát history và thực hiện các workflow phức tạp hơn.
26. Kết luận
Ở Part 1, chúng ta học:
Git Object Model
Ở Part 2, chúng ta thêm một layer quan trọng:
HEAD
│
▼
INDEX
│
▼
WORKING TREE
Nếu Part 1 giúp trả lời:
Git lưu dữ liệu như thế nào?
thì Part 2 trả lời:
Git quyết định nội dung nào sẽ đi vào commit như thế nào?
Và đây chính là nền tảng để bước sang phần khó hơn:
reset
revert
rebase
merge
Series tiếp theo
Git Deep Dive #3: reset, revert, rebase, merge — 4 cách thay đổi Git History và vì sao chúng dễ gây nhầm lẫn
Ở Part 3, chúng ta sẽ không học từng command một cách rời rạc.
Thay vào đó, chúng ta sẽ nhìn chúng dưới góc độ:
HEAD
INDEX
WORKING TREE
COMMIT GRAPH
và trả lời những câu hỏi thực tế:
reset --soft,--mixed,--hardkhác nhau ở đâu?- Khi nào
resetlàm mất staged changes? - Tại sao
revertan toàn hơn trên shared branch? - Tại sao
rebasetạo commit mới? - Merge commit thực sự xuất hiện khi nào?
- Khi nào nên dùng
merge, khi nào nên dùngrebase? - Tại sao
git push --forcethường là một bad idea trên shared branch?
Đây sẽ là phần quan trọng nhất của series về Git workflow.
All rights reserved