</>Học Dev
Bài học

Tuần 1 - Ngày 2: Staging Area Chi Tiết và Nghệ Thuật Commit

Tuần 1 – Ngày 2

Tuần 1 - Ngày 2: Staging Area Chi Tiết và Nghệ Thuật Commit

Mục tiêu học tập

  • Nắm vững git add, git restore --staged, git diff, git diff --staged
  • Viết commit message tốt theo Conventional Commits
  • Hiểu nguyên tắc atomic commit
  • Sử dụng git log để đọc lịch sử

1. Staging Area — Bộ lọc trước khi commit

Staging area (còn gọi là Index hay Cache) là vùng trung gian giữa Working Directory và Repository. Đây là nơi bạn chọn lọc những thay đổi nào sẽ đi vào commit tiếp theo.

Tình huống thực tế

Working Directory:
- src/auth.js       ← fix bug login (liên quan ticket AUTH-42)
- src/dashboard.js  ← refactor UI (việc khác, ticket UI-15)
- README.md         ← update docs (chưa xong)

Bạn muốn commit ticket AUTH-42 trước mà không commit UI-15 hay docs dở.
→ git add src/auth.js
→ git commit -m "fix: resolve login token expiry bug (AUTH-42)"

Staging Area lúc commit chỉ có src/auth.js.

2. Các lệnh cốt lõi

git add — Đưa thay đổi vào staging

# Stage một file cụ thể
git add src/auth.js

# Stage nhiều file
git add src/auth.js src/utils.js

# Stage tất cả file trong thư mục hiện tại (cẩn thận)
git add .

# Stage tất cả file đã track (bỏ qua untracked)
git add -u

# Stage tất cả thay đổi và untracked files
git add -A

# Interactive staging — chọn từng hunk (đoạn code)
git add -p src/auth.js
# → Git hỏi: Stage this hunk? (y/n/s/q/?)
# y = yes, n = no, s = split nhỏ hơn, q = quit

git add -p là công cụ mạnh mẽ: cho phép bạn stage từng block thay đổi trong một file, giúp tạo commit nguyên tử dù bạn đã thay đổi nhiều thứ trong cùng file.

git restore — Bỏ thay đổi

# Discard thay đổi trong working directory (KHÔNG thể undo!)
git restore src/auth.js

# Unstage file (đưa về unstaged nhưng GIỮ thay đổi trong working dir)
git restore --staged src/auth.js

# Unstage tất cả
git restore --staged .

# Khôi phục về trạng thái của commit cụ thể
git restore --source=HEAD~2 src/auth.js

Cảnh báo: git restore <file> (không có --staged) xoá thay đổi trong working directory vĩnh viễn (không vào trash). Chỉ dùng khi chắc chắn không cần.

git diff — Xem thay đổi

# Xem thay đổi CHƯA staged (working dir vs staging area)
git diff

# Xem thay đổi ĐÃ staged (staging area vs HEAD commit)
git diff --staged
# hoặc
git diff --cached   # tương đương

# Xem diff giữa hai commit
git diff abc123 def456

# Xem diff giữa hai branches
git diff main feature/auth

# Chỉ xem tên file thay đổi (không xem nội dung)
git diff --name-only
git diff --stat

# Diff của một file cụ thể
git diff src/auth.js

Nhớ quy tắc:

  • git diff = working directory vs staging
  • git diff --staged = staging vs last commit
  • git diff HEAD = working directory vs last commit (unstaged + staged)

3. git commit — Lưu snapshot

# Commit với message ngắn
git commit -m "feat: add user login"

# Mở editor để viết message dài (subject + body)
git commit

# Stage tất cả tracked files và commit (bỏ qua untracked)
git commit -am "fix: correct typo in auth module"
# ⚠️ Dùng -am cẩn thận — có thể commit thứ không muốn

# Xem sẽ commit những gì (dry run)
git diff --staged --stat

4. Conventional Commits — Viết commit message đúng cách

Conventional Commits là một convention phổ biến, được nhiều tool tự động hóa dựa vào (changelog, version bump, CI).

Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Các type phổ biến

TypeKhi nào dùng
featThêm tính năng mới
fixSửa bug
docsThay đổi documentation
styleFormat code, không thay đổi logic
refactorTái cấu trúc code, không fix bug hay add feature
testThêm hoặc sửa test
choreCập nhật build, dependency, tooling
perfCải thiện performance
ciThay đổi CI/CD config
revertRevert commit trước

Ví dụ thực tế

# Ngắn gọn — đủ cho hầu hết trường hợp
git commit -m "feat(auth): add JWT refresh token endpoint"
git commit -m "fix(api): handle null user in profile route"
git commit -m "docs: update deployment guide for production"
git commit -m "chore: upgrade eslint to v9"

# Có body — khi cần giải thích lý do
git commit -m "$(cat <<'EOF'
refactor(db): migrate connection pool to pg-pool v3

pg-pool v2 has a memory leak when connections timeout.
v3 fixes the issue and adds better error handling.

Closes #234
EOF
)"

# Breaking change — thêm ! sau type
git commit -m "feat(api)!: change response format to JSON:API spec"
# → Báo hiệu MAJOR version bump trong Semantic Versioning

Quy tắc viết description tốt

# BAD — không rõ thay đổi gì
fix bug
update file
WIP

# BAD — quá dài, không phải imperative
Fixed the bug where users couldn't login when their session expired

# GOOD — ngắn, imperative, rõ ràng
fix: handle expired session in login flow
feat: add dark mode toggle to settings page
perf: cache database queries in product listing

# Quy tắc: dùng động từ imperative (add, fix, update, remove)
# không phải "added", "fixes", "updating"

5. Atomic Commits — Nguyên tắc quan trọng

Mỗi commit nên làm đúng một việc và làm hoàn chỉnh việc đó.

Tại sao atomic commits quan trọng?

# BAD — một commit làm nhiều thứ không liên quan
"feat: add login page, fix navbar bug, update README, refactor utils"

Hậu quả:
- Khó review (reviewer phải hiểu 4 context khác nhau)
- Khó revert (nếu revert commit này, cả 4 thứ bị revert)
- git bisect không hoạt động đúng
- Lịch sử không đọc được như documentation

# GOOD — mỗi commit là một đơn vị logic
commit 1: "feat: add login page with form validation"
commit 2: "fix: correct active state in navbar"
commit 3: "docs: update README with new auth setup"
commit 4: "refactor: extract common utils to shared module"

Pattern: commit nhiều lần thay vì ít lần

# Workflow nguyên tử
# 1. Làm xong một việc nhỏ → commit ngay
git add src/auth/login.js
git commit -m "feat(auth): add login form component"

# 2. Viết test cho việc đó → commit
git add src/auth/login.test.js
git commit -m "test(auth): add tests for login form validation"

# 3. Sửa CSS → commit
git add src/styles/auth.css
git commit -m "style(auth): add login page responsive styles"

6. git log — Đọc lịch sử

# Log cơ bản
git log

# Một dòng mỗi commit
git log --oneline

# Graph view — thấy được nhánh
git log --oneline --graph --all

# Log của một file cụ thể
git log --oneline src/auth.js

# Log theo tác giả
git log --author="Giang"

# Log theo thời gian
git log --since="2024-01-01" --until="2024-06-30"

# Log với diff (patch)
git log -p src/auth.js

# Số lượng commit gần nhất
git log -5 --oneline

# Tìm commit chứa keyword trong message
git log --grep="AUTH-42"

# Format tùy chỉnh
git log --pretty=format:"%h %an %ar %s"
# %h = short hash, %an = author name, %ar = relative time, %s = subject

# Xem chi tiết một commit cụ thể
git show abc1234
git show HEAD       # commit hiện tại
git show HEAD~1     # commit trước đó

7. .gitignore — Loại trừ file không muốn track

# .gitignore — đặt ở root project
node_modules/          # thư mục dependency
.env                   # secrets — KHÔNG BAO GIỜ commit
.env.local
.env.*.local
dist/                  # build output
build/
*.log                  # log files
.DS_Store              # macOS metadata
Thumbs.db              # Windows thumbnail cache
.idea/                 # IDE config (JetBrains)
.vscode/               # VS Code settings (trừ khi muốn share team)
coverage/              # test coverage reports
*.pyc                  # Python compiled
__pycache__/
.pytest_cache/
target/                # Java/Rust build output
*.class
# Kiểm tra file có bị ignore không
git check-ignore -v node_modules/lodash

# Xem tất cả ignored files
git status --ignored

# Force add file bị ignore (hiếm khi cần)
git add -f .env.example

Câu hỏi ôn tập

  1. Sự khác biệt giữa git diffgit diff --staged là gì?

    Xem đáp án

    git diff so sánh working directory với staging area — hiện thay đổi chưa được stage. git diff --staged (hoặc --cached) so sánh staging area với HEAD commit — hiện thay đổi đã staged và sẽ vào commit tiếp theo. Nếu muốn xem tất cả thay đổi so với HEAD (cả staged lẫn unstaged), dùng git diff HEAD.

  2. Lệnh nào để stage chỉ một phần thay đổi trong file (không stage cả file)?

    Xem đáp án

    git add -p <file> (hoặc --patch). Git chia file thành các "hunks" và hỏi từng cái: y để stage, n để skip, s để split nhỏ hơn, q để thoát. Đây là công cụ quan trọng để tạo atomic commits dù bạn đã thay đổi nhiều thứ trong cùng một file.

  3. Commit message "fix bug in login" vi phạm Conventional Commits ở điểm nào?

    Xem đáp án

    Vi phạm hai điểm: (1) Thiếu type prefix — phải là fix: theo format <type>: <description>. (2) Description dùng noun phrase thay vì imperative verb — nên là "fix: resolve login token expiry bug" không phải "fix bug in login". Ngoài ra, "bug" quá mơ hồ — nên mô tả cụ thể hơn để lịch sử có thể đọc như documentation.

  4. Tại sao atomic commits quan trọng khi debug bằng git bisect?

    Xem đáp án

    git bisect dùng binary search để tìm commit đầu tiên gây ra bug — nó checkout từng commit và chạy test. Nếu commit lớn chứa nhiều thay đổi không liên quan, khi bisect tìm ra "commit này là bad", bạn vẫn không biết phần nào trong đó gây bug. Atomic commits (mỗi commit làm đúng một việc) đảm bảo khi bisect tìm được commit, bạn biết ngay chính xác thay đổi nào là nguyên nhân.

  5. git restore src/app.jsgit restore --staged src/app.js khác nhau thế nào?

    Xem đáp án

    git restore src/app.js (không có flag) xoá thay đổi trong working directory — file trở về trạng thái của staging area. Không thể undo — thay đổi bị mất vĩnh viễn (không qua trash).

    git restore --staged src/app.js unstage file — đưa thay đổi từ staging area về unstaged, nhưng giữ nguyên thay đổi trong working directory. An toàn hơn nhiều, chỉ ảnh hưởng đến Index.

Bài tập thực hành

# Setup
mkdir git-commit-lab && cd git-commit-lab
git init
git config user.name "Your Name"
git config user.email "you@example.com"

# 1. Tạo vài files
echo "# App" > README.md
echo "console.log('hello')" > app.js
echo "PORT=3000" > .env
echo "*.log\n.env\nnode_modules/" > .gitignore

# 2. Quan sát git status
git status  # thấy gì?

# 3. Stage README.md và commit
git add README.md
git status   # README staged, app.js untracked, .env ignored

# 4. Thử diff
git diff         # không có gì (working = staging cho README)
echo "console.log('world')" >> app.js
git diff         # thấy thay đổi app.js (unstaged)
git add app.js
git diff         # không có gì
git diff --staged  # thấy cả README.md và app.js staged

# 5. Commit atomic
git commit -m "feat: add initial app files"

# 6. Unstage và thử git add -p
echo "// auth function" >> app.js
echo "// utils function" >> app.js
git add -p app.js   # thử stage từng hunk riêng

# 7. Xem log
git log --oneline
git log --oneline --graph --all

Tài liệu tham khảo chính thức


Tiếp theo: Ngày 3 — Branching