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

Tuần 2 - Ngày 6: Advanced Git Tools

Tuần 2 – Ngày 6

Tuần 2 - Ngày 6: Advanced Git Tools

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

  • Dùng git bisect để tìm commit gây regression
  • Nắm cherry-pick một commit hoặc một range
  • Khai thác git stash nâng cao với named stashes
  • Hiểu git worktree, submodule vs subtree, sparse-checkout
  • Cài và cấu hình Git hooks với Husky/lint-staged
  • Nhận biết context monorepo (Nx, Turborepo)

1. git bisect — Binary Search Tìm Regression

git bisect dùng binary search để tìm commit đầu tiên gây ra một bug.

Workflow thủ công

# Tình huống: feature X hoạt động ở v1.0.0, bị hỏng ở HEAD
# Cần tìm commit nào gây ra bug trong 200 commits

# Bắt đầu bisect session
git bisect start

# Đánh dấu commit "xấu" (hiện tại bị bug)
git bisect bad HEAD

# Đánh dấu commit "tốt" (lần cuối biết còn OK)
git bisect good v1.0.0

# Git checkout commit ở giữa (khoảng commit 100)
# Bạn test: nếu bug còn → bad, nếu OK → good
npm test
git bisect bad    # hoặc git bisect good

# Git tiếp tục nhị phân → checkout commit tiếp theo
# Lặp lại cho đến khi Git xác định commit gây lỗi:
# abc1234 is the first bad commit
# commit abc1234
# Author: Dev <dev@team.com>
# Date: ...
# feat: refactor payment processor

# Kết thúc bisect, quay về HEAD
git bisect reset

Tự động hóa với bisect run

# Viết test script: exit 0 = good, exit 1 (nonzero) = bad
cat test-bisect.sh
#!/bin/bash
npm install --silent
npm test -- --testNamePattern="payment processor"

chmod +x test-bisect.sh

# Git tự động bisect
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run ./test-bisect.sh
# Git chạy script tự động ở mỗi bước → tìm commit xấu mà không cần can thiệp

git bisect reset

Số bước bisect cần

200 commits → ~8 bước  (log2(200) ≈ 7.6)
1000 commits → ~10 bước
10000 commits → ~14 bước

2. cherry-pick — Chọn Commit Cụ Thể

Cherry-pick áp dụng changes của một commit vào branch hiện tại mà không merge toàn bộ branch.

Cherry-pick một commit

# Tình huống: fix bug ở hotfix/1.2.1 cần được backport lên develop

git switch develop

# Cherry-pick một commit
git cherry-pick abc1234

# Cherry-pick với edit message
git cherry-pick abc1234 --edit

# Cherry-pick mà không tự commit (chỉ stage changes)
git cherry-pick abc1234 --no-commit
# Kiểm tra changes trước khi commit
git diff --staged
git commit -m "fix: backport payment validation fix"

Cherry-pick một range

# Cherry-pick từ commit A đến commit B (inclusive)
git cherry-pick A^..B
# Dấu ^ sau A = "từ A (exclusive)" → tức là từ commit sau A đến B

# Ví dụ: cherry-pick 3 commits abc, def, ghi
git cherry-pick abc^..ghi
# Tương đương với cherry-pick abc, def, ghi theo thứ tự

# Nếu có conflict trong quá trình range cherry-pick:
git cherry-pick --continue    # sau khi resolve
git cherry-pick --abort       # hủy toàn bộ
git cherry-pick --skip        # bỏ qua commit hiện tại

Khi nào dùng cherry-pick thay vì merge/rebase?

Dùng cherry-pick khi:
  ✓ Backport một bug fix cụ thể vào release branch cũ
  ✓ Lấy một feature commit từ branch bị abandon
  ✓ Áp dụng hotfix cho nhiều release versions (1.x, 2.x)

Không dùng cherry-pick khi:
  ✗ Muốn merge toàn bộ feature → dùng merge
  ✗ Sync fork với upstream → dùng rebase
  ✗ Cherry-pick nhiều lần cùng một commit → lịch sử lộn xộn, duplicate SHA

3. git stash Nâng Cao

Named stash và quản lý nhiều stashes

# Stash với tên có ý nghĩa
git stash push -m "WIP: payment refactor — đang làm dở validation"
git stash push -m "experiment: GraphQL schema draft"

# Xem danh sách stash
git stash list
# stash@{0}: On feature/payment: WIP: payment refactor — đang làm dở validation
# stash@{1}: On feature/graphql: experiment: GraphQL schema draft
# stash@{2}: WIP on main: abc1234 chore: update deps

# Xem nội dung stash
git stash show stash@{1}           # tóm tắt files changed
git stash show -p stash@{1}        # xem full diff

# Apply stash mà không xoá (giữ trong stash list)
git stash apply stash@{1}

# Pop stash (apply + xoá khỏi list)
git stash pop stash@{0}

# Pop stash mặc định (stash@{0} — mới nhất)
git stash pop

# Xoá một stash cụ thể
git stash drop stash@{2}

# Xoá tất cả stash
git stash clear

Stash options nâng cao

# Stash cả untracked files (--include-untracked)
git stash push -u -m "WIP: including new files"

# Stash cả untracked + ignored files
git stash push -a -m "WIP: full save including ignored"

# Stash chỉ staged changes (không stash unstaged)
git stash push --staged -m "Only staged changes"

# Tạo branch mới từ stash (hữu ích khi stash xung đột với current code)
git stash branch feature/new-from-stash stash@{1}
# Tạo branch tại commit của stash, apply stash, drop stash

4. git worktree — Nhiều Working Directory

git worktree cho phép checkout nhiều branches cùng lúc vào thư mục khác nhau — không cần clone thêm.

# Tình huống: đang làm feature/new-ui, cần hotfix urgent trên main
# Không muốn stash hoặc commit WIP

# Xem worktrees hiện tại
git worktree list

# Tạo worktree mới cho hotfix
git worktree add ../hotfix-1.2.1 main
# Tạo thư mục ../hotfix-1.2.1/ với branch main checked out
# Branch main ở worktree kia, không ảnh hưởng branch feature/new-ui ở đây

# Làm việc trong worktree mới
cd ../hotfix-1.2.1
git switch -c hotfix/1.2.1
vim src/api/auth.ts
git add . && git commit -m "fix: critical security patch"
git push origin hotfix/1.2.1

# Quay về worktree chính
cd ../original-project
# feature/new-ui vẫn nguyên si, không bị ảnh hưởng

# Xoá worktree khi xong
git worktree remove ../hotfix-1.2.1
# Hoặc force remove nếu còn uncommitted changes
git worktree remove --force ../hotfix-1.2.1

Ưu điểm so với stash

stash:            nhanh, đơn giản, tốt cho interrupt ngắn
git worktree:     song song thực sự, tốt cho:
                  - hotfix dài ngày trong khi feature đang chạy CI
                  - review PR mà không interrupt feature branch
                  - so sánh code giữa 2 branches cùng lúc
                  - test trên nhiều branches cùng lúc

5. Submodule vs Subtree

Hai cách nhúng một repo khác vào repo của bạn.

Git Submodule

# Thêm submodule
git submodule add https://github.com/org/shared-utils.git lib/shared-utils

# Clone repo có submodule
git clone --recurse-submodules https://github.com/org/main-project.git
# Hoặc sau khi clone thường:
git submodule init && git submodule update

# Update submodule lên version mới nhất
cd lib/shared-utils
git pull origin main
cd ../..
git add lib/shared-utils
git commit -m "chore: update shared-utils to latest"

# Xoá submodule (cần nhiều bước)
git submodule deinit lib/shared-utils
git rm lib/shared-utils
rm -rf .git/modules/lib/shared-utils

Git Subtree

# Thêm subtree (không cần file .gitmodules)
git subtree add --prefix lib/shared-utils \
  https://github.com/org/shared-utils.git main --squash

# Update subtree
git subtree pull --prefix lib/shared-utils \
  https://github.com/org/shared-utils.git main --squash

# Đẩy changes ngược lại remote
git subtree push --prefix lib/shared-utils \
  https://github.com/org/shared-utils.git main

So sánh

SubmoduleSubtreeFile.gitmodulesCóKhôngClonethêmbưcgitsubmoduleinitKhôngcnLchsrepoTáchbitLnvàonhauPushchangesngưcD(cd+push)gitsubtreepushĐccodetrongIDEPhiinittrưcSnsàngngayPhùhpchoExternallibrariesSharedcodemìnhshu

6. sparse-checkout — Monorepo Subset

Chỉ checkout một phần của monorepo lớn.

# Clone nhưng chưa checkout file nào
git clone --no-checkout --depth=1 https://github.com/big-org/monorepo.git
cd monorepo

# Bật sparse-checkout
git sparse-checkout init --cone

# Chỉ checkout package cụ thể
git sparse-checkout set packages/frontend packages/shared-ui

# Checkout
git checkout main

# Xem files đã checkout
ls

# Thêm package khác
git sparse-checkout add packages/api-gateway

# Tắt sparse-checkout (checkout tất cả)
git sparse-checkout disable

7. Git Hooks

Git hooks là scripts tự động chạy ở các điểm cụ thể trong Git workflow.

Hooks phổ biến

Client-side hooks (trong .git/hooks/):
  pre-commit      → chạy trước khi tạo commit (lint, format, test)
  commit-msg      → validate commit message (Conventional Commits format)
  pre-push        → chạy trước khi push (test suite, type-check)
  post-merge      → chạy sau khi merge (npm install nếu package.json thay đổi)
  post-checkout   → chạy sau khi checkout branch

Server-side hooks (trên remote server):
  pre-receive     → validate trước khi nhận push
  post-receive    → trigger deploy sau khi nhận push

Viết hook thủ công

# Ví dụ pre-commit hook: chặn commit nếu còn console.log
cat .git/hooks/pre-commit
#!/bin/bash
if git diff --cached --name-only | xargs grep -l "console\.log" 2>/dev/null; then
  echo "Error: Remove console.log before committing"
  exit 1
fi
exit 0

chmod +x .git/hooks/pre-commit

# Ví dụ commit-msg hook: validate Conventional Commits format
cat .git/hooks/commit-msg
#!/bin/bash
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .{1,72}"
if ! echo "$commit_msg" | grep -Eq "$pattern"; then
  echo "Error: Commit message must follow Conventional Commits format"
  echo "Example: feat: add user authentication"
  exit 1
fi
exit 0

chmod +x .git/hooks/commit-msg

Husky — Git Hooks cho Node.js Projects

# Cài đặt Husky (quản lý hooks dễ dàng hơn, shareable qua npm)
npm install --save-dev husky
npx husky init

# Thêm pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

# Thêm commit-msg hook (validate với commitlint)
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

# commitlint config
cat commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };

lint-staged — Chỉ Lint Changed Files

# Cài đặt
npm install --save-dev lint-staged

# Cấu hình trong package.json
{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{js,jsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["prettier --write"],
    "*.md": ["prettier --write"]
  }
}

# Workflow:
# git add src/auth.ts
# git commit -m "feat: add auth"
# → Husky pre-commit: chạy lint-staged
# → lint-staged: chỉ lint src/auth.ts (không lint toàn project)
# → ESLint fix + Prettier format
# → Auto re-stage nếu có fix
# → Commit tiếp tục

8. Monorepo Context — Nx và Turborepo

Monorepo là một single Git repository chứa nhiều packages/applications.

monorepo/apps/web/(Next.jsapp)mobile/(ReactNative)api/(Expressbackend)packages/ui/(sharedcomponentlibrary)utils/(sharedutilities)config/(sharedESLint,TSconfigs)package.json(rootworkspaces)nx.json/turbo.json

Turborepo — Task Caching

# Cài đặt
npx create-turbo@latest

# turbo.json — define task pipeline
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],   # ^ = build dependencies trước
      "outputs": [".next/**", "dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    }
  }
}

# Chạy build cho tất cả packages (với caching)
npx turbo run build

# Chỉ build packages bị ảnh hưởng bởi thay đổi hiện tại
npx turbo run build --filter=[HEAD^1]

# Remote caching (chia sẻ cache với team qua Vercel)
npx turbo login
npx turbo link

Git + Monorepo tips

# Tìm packages bị ảnh hưởng bởi thay đổi
git diff --name-only HEAD~1 HEAD | sed 's|/.*||' | sort -u
# Hoặc dùng Nx:
npx nx affected --target=test --base=main --head=HEAD

# Conventional Commits với scope cho monorepo
git commit -m "feat(ui): add Button variant outline"
git commit -m "fix(api): resolve auth middleware race condition"
git commit -m "chore(config): update shared ESLint rules"

Câu hỏi ôn tập

  1. git bisect run cần script với exit code như thế nào để phân biệt "good" và "bad"?

    Xem đáp án

    Exit code 0 = commit "good" (bug chưa xuất hiện). Exit code khác 0 (nonzero) = commit "bad" (bug đã xuất hiện). Script thường là test runner: npm test exit 0 nếu pass, exit 1 nếu fail. Đặc biệt: exit code 125 = "skip commit này" (ví dụ compile error không liên quan đến bug cần tìm).

  2. git cherry-pick A^..B có nghĩa là gì? Dấu ^ sau A có tác dụng gì?

    Xem đáp án

    Cherry-pick từ commit A đến commit B theo thứ tự, inclusive. Dấu ^ sau A có nghĩa là parent của A — nên range A^..B = "từ A (inclusive) đến B (inclusive)". Nếu không có ^ (viết A..B), range bắt đầu từ commit sau A (A exclusive). Ví dụ: git cherry-pick abc^..def cherry-pick abc, def và tất cả commits ở giữa.

  3. Sự khác biệt giữa git stash applygit stash pop?

    Xem đáp án

    Cả hai đều áp dụng stash vào working directory. Khác biệt: git stash apply áp dụng stash nhưng giữ lại trong stash list. git stash pop áp dụng stash và xoá khỏi stash list (apply + drop). Dùng apply khi muốn áp dụng cùng stash cho nhiều branches; dùng pop khi chắc chắn chỉ cần apply một lần.

  4. Khi nào nên dùng git worktree thay vì git stash?

    Xem đáp án

    Dùng git worktree khi cần làm việc song song lâu dài trên hai branches: ví dụ hotfix dài ngày trong khi feature đang có CI chạy, hoặc cần review PR mà không interrupt feature branch, hoặc cần so sánh code giữa 2 branches cùng lúc trong IDE. Dùng git stash cho interrupt ngắn (< vài giờ) vì đơn giản hơn. Worktree = hai working directories thực sự song song; stash = tạm cất/lấy lại.

  5. Tại sao lint-staged hiệu quả hơn chạy lint toàn project trong pre-commit hook?

    Xem đáp án

    lint-staged chỉ chạy linter trên files đã staged (files sẽ vào commit này), không scan toàn bộ project. Project lớn có thể có hàng nghìn files — lint tất cả mỗi commit mất vài phút, làm developer frustrated và skip hook. lint-staged hoàn thành trong giây vì chỉ lint 1-5 files thay đổi — commit flow không bị cản. Kết quả: hook được tôn trọng và codebase sạch hơn.

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

mkdir advanced-tools-lab && cd advanced-tools-lab
git init
git config user.name "Dev" && git config user.email "dev@test.com"
git config rerere.enabled true

# === BISECT LAB ===
# Tạo lịch sử với một "bug" ở commit thứ 5
for i in 1 2 3 4 5 6 7 8 9 10; do
  echo "code version $i" > app.js
  # Commit 5: thêm "bug"
  if [ $i -eq 5 ]; then
    echo "BUG_INTRODUCED=true" >> app.js
  fi
  git add . && git commit -m "feat: version $i"
done

# Bisect tìm commit gây ra BUG_INTRODUCED
git bisect start
git bisect bad HEAD
git bisect good HEAD~9  # commit 1 là good

# Test script
cat > /tmp/test-bisect.sh << 'EOF'
#!/bin/bash
if grep -q "BUG_INTRODUCED" app.js; then
  exit 1  # bad
fi
exit 0  # good
EOF
chmod +x /tmp/test-bisect.sh

git bisect run /tmp/test-bisect.sh
# Git tìm được commit "feat: version 5"
git bisect reset

# === CHERRY-PICK LAB ===
git switch -c hotfix/backport
echo "critical fix" >> app.js
git add . && git commit -m "fix: critical security patch"
CHERRY_HASH=$(git rev-parse HEAD)

git switch main
git cherry-pick $CHERRY_HASH
git log --oneline -3

# === STASH LAB ===
echo "WIP code" >> app.js
git stash push -m "WIP: important feature in progress"
git stash push -m "experiment: trying new approach"
git stash list
git stash show -p stash@{1}
git stash pop stash@{1}

# === WORKTREE LAB ===
git worktree add ../lab-hotfix main
git worktree list
# Làm việc trong worktree
echo "hotfix content" > ../lab-hotfix/hotfix.js
cd ../lab-hotfix && git add . && git commit -m "fix: hotfix via worktree"
cd ../advanced-tools-lab
git log main --oneline -3
git worktree remove ../lab-hotfix

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


Tiếp theo: Ngày 7 — Quiz Tuần 2