0

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 add thực sự làm gì?
  • git diff khác git diff --cached thế nào?
  • Vì sao một file có thể vừa staged vừa unstaged?
  • git add -p hoạt động như thế nào?
  • git reset liê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 status hiể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

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 revert khó hơn
  • git cherry-pick khó 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 reset có 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 addgit 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, --hard khác nhau ở đâu?
  • Khi nào reset làm mất staged changes?
  • Tại sao revert an toàn hơn trên shared branch?
  • Tại sao rebase tạ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ùng rebase?
  • Tại sao git push --force thườ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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí