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-pickmột commit hoặc một range - Khai thác
git stashnâng cao với named stashes - Hiểu
git worktree,submodulevssubtree,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
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.
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
-
git bisect runcầ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 testexit 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). -
git cherry-pick A^..Bcó 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 rangeA^..B= "từ A (inclusive) đến B (inclusive)". Nếu không có^(viếtA..B), range bắt đầu từ commit sau A (A exclusive). Ví dụ:git cherry-pick abc^..defcherry-pick abc, def và tất cả commits ở giữa. -
Sự khác biệt giữa
git stash applyvàgit 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ùngapplykhi muốn áp dụng cùng stash cho nhiều branches; dùngpopkhi chắc chắn chỉ cần apply một lần. -
Khi nào nên dùng
git worktreethay vìgit stash?Xem đáp án
Dùng
git worktreekhi 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ùnggit stashcho 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. -
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
- git bisect documentation
- git cherry-pick documentation
- git stash documentation
- git worktree documentation
- git submodule documentation
- Husky — Git Hooks
- lint-staged
- Turborepo Documentation
Tiếp theo: Ngày 7 — Quiz Tuần 2