3.11 - Git hook, cherry-pick và worktree
Tóm tắt: Ba công cụ Git chưa có ở các bài trước nhưng dùng thường xuyên khi làm nhóm. Git hook là script Git tự chạy ở một thời điểm nhất định, ví dụ ngay trước khi commit; đặt hook trong thư mục được commit và bật bằng
core.hooksPaththì cả nhóm dùng chung được. Nhưng hook chạy trên máy người dùng và bỏ qua được, nên kiểm tra quan trọng vẫn phải nằm ở CI.git cherry-pickchép đúng một commit sang nhánh khác — cách đưa một bản sửa lỗi về nhánh phát hành cũ.git worktreemở thêm một thư mục làm việc cho nhánh khác của cùng một kho, để sửa lỗi gấp mà không đụng vào việc đang dở.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết một hook
pre-commitvà chia sẻ nó cho cả nhóm bằngcore.hooksPath. - Giải thích vì sao hook không thay được CI.
- Dùng
git cherry-pick -xđể đưa một bản sửa lỗi sang nhánh phát hành. - Dùng
git worktreeđể làm hai nhánh song song mà không cầnstash.
Cần biết trước: bài 3.4 (commit, git log -S), bài 3.5 (nhánh), bài 3.3 (stash), bài 3.6 (CI). git bisect nằm ở bài 3.9, rebase tương tác ở bài 3.5, --force-with-lease ở bài 3.3; bài này không nhắc lại.
Nội dung bài học
3.11.1 — Git hook là gì
Hook là một file script đặt tên theo sự kiện, Git tự chạy khi sự kiện đó xảy ra. Mã thoát khác 0 thì Git huỷ thao tác.
| Hook | Chạy lúc | Hay dùng để |
|---|---|---|
pre-commit | Trước khi tạo commit, sau khi bạn gõ git commit | Chặn file có bí mật, chạy formatter |
commit-msg | Sau khi viết xong message | Kiểm tra quy ước message (bài 3.10) |
pre-push | Trước khi push | Chạy test nhanh |
Mặc định Git tìm hook trong .git/hooks/. Thư mục .git không được commit, nên hook đặt ở đó chỉ có trên máy bạn. Cách chia sẻ cho cả nhóm:
mkdir .githooks
# viết .githooks/pre-commit, commit thư mục này như mọi file khác
git config core.hooksPath .githooks # mỗi người chạy một lần sau khi cloneVí dụ hook chặn commit có chuỗi Password= (dấu hiệu chuỗi kết nối database lọt vào code):
#!/bin/sh
# .githooks/pre-commit
# Chặn commit nếu phần đã stage có dòng mới chứa "Password=".
# Bỏ qua chính thư mục .githooks, vì file này cũng chứa chữ đó.
if git diff --cached -U0 -- . ':!.githooks' | grep -iE '^\+.*password=' > /dev/null; then
echo "pre-commit: phát hiện 'Password=' trong thay đổi đã stage."
echo "Chuyển bí mật sang user-secrets rồi commit lại."
exit 1
fiTrên macOS và Linux, file hook phải có quyền thực thi: chmod +x .githooks/pre-commit. Trên Windows, Git for Windows chạy hook bằng Git Bash, dòng #!/bin/sh đầu file là đủ.
Dòng ':!.githooks' không phải trang trí: thiếu nó, chính commit thêm hook cũng bị chặn, vì nội dung file hook có chữ password=.
3.11.2 — Giới hạn của hook
Hook chạy ở máy lập trình viên, nên có ba lỗ hổng:
- Người chưa chạy
git config core.hooksPath(hoặc chưanpm installnếu dùng husky) thì hook không tồn tại. git commit --no-verifybỏ quapre-commitvàcommit-msg.- Commit tạo trên giao diện web của GitHub không chạy hook nào.
Vì vậy hook là lớp tiện lợi: bắt lỗi sớm và rẻ. Lớp bảo đảm nằm ở CI và branch protection (bài 3.6), nơi không ai bỏ qua được. Với việc chặn bí mật, GitHub còn có Push protection chặn ngay lúc push.
3.11.3 — git cherry-pick: chép một commit sang nhánh khác
Tình huống: bản 1.0 đang chạy cho khách hàng trên nhánh release/1.0. Trên main đã có thêm tính năng mới chưa muốn phát hành, và vừa có một bản sửa lỗi cần đưa về 1.0 ngay.
git switch release/1.0
git cherry-pick -x <mã commit sửa lỗi>Cherry-pick lấy phần thay đổi của commit đó và tạo một commit mới trên nhánh hiện tại (mã băm khác, vì cha khác — đúng như bài 3.1). Cờ -x thêm dòng (cherry picked from commit ...) vào message, để sau này biết hai commit là một.
Nếu thay đổi đụng vào đoạn code đã khác giữa hai nhánh, cherry-pick dừng với xung đột như merge. Giải như bài 3.7, rồi git cherry-pick --continue; muốn huỷ thì git cherry-pick --abort.
Đừng dùng cherry-pick để "gộp nhánh từng phần" hằng ngày: cùng một thay đổi xuất hiện hai lần với hai mã băm, làm lịch sử khó đọc và dễ xung đột về sau. Nó dành cho việc đưa bản sửa sang nhánh phát hành, hoặc cứu một commit lỡ làm trên nhánh sai.
3.11.4 — git worktree: hai nhánh, hai thư mục, một kho
Tình huống: đang làm dở báo cáo doanh thu, có thay đổi chưa commit và file mới chưa theo dõi. Có lỗi gấp cần sửa trên main.
git worktree add ../crm-hotfix -b hotfix/negative-total main
cd ../crm-hotfix # thư mục mới, đang ở nhánh hotfix/negative-total
# sửa, commit, push
cd ../crm
git worktree remove ../crm-hotfixgit worktree add <thư mục> -b <nhánh mới> <điểm xuất phát> tạo một thư mục làm việc thứ hai dùng chung kho .git với thư mục gốc. So với git stash (bài 3.3):
stash | worktree | |
|---|---|---|
| Thư mục đang làm dở | Bị dọn sạch tạm thời | Không đụng tới |
| Kết quả build, file đang mở trong IDE | Mất, phải build lại | Giữ nguyên |
| Hợp với | Việc xen ngang vài phút | Việc gấp kéo dài, hoặc cần chạy hai nhánh cùng lúc |
Một nhánh chỉ được mở ở một worktree tại một thời điểm; Git từ chối nếu bạn switch sang nhánh đang mở ở thư mục khác. git worktree list cho biết đang có những thư mục nào.
3.11.5 — Lỗi thường gặp
| Sai lầm | Hậu quả | Thay bằng |
|---|---|---|
Đặt hook trong .git/hooks rồi tưởng cả nhóm có | Chỉ máy bạn chạy | Thư mục .githooks + core.hooksPath |
| Coi hook là lớp bảo vệ duy nhất | --no-verify hoặc commit trên web lọt qua | Kiểm tra lại ở CI |
Cherry-pick không có -x | Không biết commit nào là bản sao của commit nào | Luôn dùng -x khi backport |
| Xoá thư mục worktree bằng tay | Git vẫn nhớ worktree "ma" | git worktree remove, hoặc git worktree prune |
Danh sách rà soát
- Hook dùng chung nằm trong thư mục được commit, bật bằng
core.hooksPath, và README ghi lệnh bật. - Mọi kiểm tra quan trọng có ở hook cũng có ở CI.
- Bản sửa đưa về nhánh phát hành bằng
cherry-pick -x. - Dùng
worktreecho việc gấp kéo dài,stash -ucho việc xen ngang ngắn.
Bài tập áp dụng
Bài 1 — Hook dùng chung chặn bí mật
Tạo kho thử, thêm hook pre-commit như mục 3.11.1 vào .githooks/, bật bằng core.hooksPath, commit hook. Thử commit một file appsettings.json có Password=.... Sau đó clone kho sang thư mục khác và thử lại mà không chạy git config core.hooksPath.
Tiêu chí hoàn thành: ở kho gốc commit bị chặn với đúng thông báo của hook; ở bản clone commit lọt qua; bạn giải thích được vì sao.
Gợi ý và lời giải — Bài 1
Gợi ý. core.hooksPath là cấu hình local (nằm trong .git/config), mà .git/config không đi theo git clone.
Lời giải (output thật):
mkdir hook-lab && cd hook-lab && git init -q -b main
mkdir .githooks
# tạo .githooks/pre-commit với nội dung ở mục 3.11.1
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
git add .githooks && git commit -q -m "chore: add shared pre-commit hook"
echo '{ "ConnectionStrings": { "Crm": "Host=db;Username=crm_app;Password=MatKhau123" } }' > appsettings.json
git add appsettings.json
git commit -m "feat: add connection string"pre-commit: phát hiện 'Password=' trong thay đổi đã stage.
Chuyển bí mật sang user-secrets rồi commit lại.git log --oneline vẫn chỉ có chore: add shared pre-commit hook.
cd .. && git clone -q hook-lab hook-lab-2 && cd hook-lab-2
git config core.hooksPath # không in gì: chưa bật
echo 'Password=abc' > x.txt && git add x.txt && git commit -q -m "test" && echo "commit OK — hook không chạy"commit OK — hook không chạyVì sao. Thư mục .githooks đi theo kho, nhưng việc bật nó thì không. Mỗi người phải chạy git config core.hooksPath .githooks sau khi clone — ghi lệnh này vào README. husky (bài 3.10) tự làm bước này qua script prepare của npm install, nhưng người không chạy npm install thì cũng không có hook. Đó là lý do CI vẫn phải kiểm tra lại.
Bài 2 — Đưa bản sửa về nhánh phát hành
Dựng kho: commit feat: add order rules, tạo nhánh release/1.0 tại đó. Trên main thêm feat: add tax calculation (chưa muốn phát hành) rồi fix: reject orders with negative total. Đưa chỉ bản sửa về release/1.0.
Tiêu chí hoàn thành: release/1.0 có bản sửa nhưng không có file của tính năng thuế; message của commit trên release/1.0 có dòng (cherry picked from commit ...).
Gợi ý và lời giải — Bài 2
Gợi ý. Merge main vào release/1.0 sẽ kéo theo cả tính năng thuế. Bạn cần đúng một commit.
Lời giải (output thật):
mkdir cherry-lab && cd cherry-lab && git init -q -b main
printf 'if (quantity <= 0) return Error;\n' > OrderRules.txt
git add . && git commit -q -m "feat: add order rules"
git branch release/1.0
printf 'Tax = Total * 0.1;\n' > Tax.txt && git add . && git commit -q -m "feat: add tax calculation"
printf 'if (quantity <= 0) return Error;\nif (total < 0) return Error;\n' > OrderRules.txt
git commit -q -am "fix: reject orders with negative total"
git log --oneline4afd08f fix: reject orders with negative total
bda0527 feat: add tax calculation
2ef1fa3 feat: add order rulesgit switch release/1.0
git cherry-pick -x 4afd08f[release/1.0 84df7f7] fix: reject orders with negative total
Date: Sat Oct 17 21:58:41 2026 +0700
1 file changed, 1 insertion(+)git log --oneline
git log -1 --format=%B
ls84df7f7 fix: reject orders with negative total
2ef1fa3 feat: add order rules
fix: reject orders with negative total
(cherry picked from commit 4afd08f32e293fd950fbecc091dc54505ca63ee7)
OrderRules.txtKhông có Tax.txt: tính năng thuế ở lại trên main. Commit mới 84df7f7 khác mã với 4afd08f vì cha khác, nhưng dòng cherry picked from nối hai commit lại cho người đọc lịch sử.
Bài 3 — Sửa lỗi gấp bằng worktree
Trong một kho thử, tạo nhánh feature/revenue-report, sửa một file đã theo dõi và tạo thêm một file mới, không commit. Dùng git worktree tạo nhánh hotfix/negative-total từ main ở thư mục bên cạnh, commit một bản sửa ở đó, rồi xoá worktree.
Tiêu chí hoàn thành: sau khi xong, git status --short ở thư mục gốc vẫn hiện đúng hai thay đổi dở dang như trước; nhánh hotfix/negative-total có commit sửa lỗi; git worktree list chỉ còn một dòng.
Gợi ý và lời giải — Bài 3
Gợi ý. Thư mục worktree mới phải nằm ngoài thư mục kho gốc (ví dụ ../crm-hotfix), nếu không nó sẽ hiện ra như một thư mục chưa theo dõi trong kho gốc.
Lời giải (output thật, đã rút gọn đường dẫn):
mkdir crm && cd crm && git init -q -b main
printf 'kiem tra so luong\n' > order.txt && git add . && git commit -q -m "feat: add order validation"
git switch -q -c feature/revenue-report
echo "báo cáo đang viết dở" > report.txt
echo "sửa dở" >> order.txt
git status --short M order.txt
?? report.txtgit worktree add ../crm-hotfix -b hotfix/negative-total mainPreparing worktree (new branch 'hotfix/negative-total')
HEAD is now at 0688728 feat: add order validationcd ../crm-hotfix
git status --short # sạch: đây là main, không có việc dở
echo "kiem tra tong am" >> order.txt
git commit -q -am "fix: reject orders with negative total"
cd ../crm
git worktree list.../crm 0688728 [feature/revenue-report]
.../crm-hotfix 88db0ee [hotfix/negative-total]git status --short
git worktree remove ../crm-hotfix
git worktree list M order.txt
?? report.txt
.../crm 0688728 [feature/revenue-report]Thư mục gốc không bị đụng tới: vẫn đúng hai thay đổi dở dang. Commit sửa lỗi nằm trên nhánh hotfix/negative-total (xoá worktree không xoá nhánh), sẵn sàng để push và mở PR.
Tự kiểm tra
Vì sao hook đặt trong .git/hooks không chia sẻ được cho cả nhóm?
Vì thư mục .git không được commit hay push. Muốn chia sẻ, đặt hook trong một thư mục thường như .githooks, commit nó, và mỗi người bật bằng git config core.hooksPath .githooks sau khi clone.
Có hook pre-commit rồi thì còn cần kiểm tra ở CI không?
Có. Hook chạy trên máy người dùng nên bỏ qua được bằng --no-verify, không chạy nếu người đó chưa bật core.hooksPath, và không chạy với commit tạo trên giao diện web. CI và branch protection là lớp không ai bỏ qua được.
git cherry-pick làm gì, và cờ -x để làm gì?
Nó lấy phần thay đổi của một commit và tạo một commit mới với nội dung tương tự trên nhánh hiện tại; mã băm khác vì cha khác. Cờ -x thêm dòng cherry picked from commit kèm mã gốc vào message, để biết hai commit là một.
Khi nào dùng worktree thay vì stash?
Khi việc chen ngang kéo dài hoặc cần chạy hai nhánh cùng lúc. Worktree mở thư mục riêng cho nhánh kia nên thư mục đang làm dở, kết quả build và file đang mở đều giữ nguyên. Stash hợp với việc xen ngang vài phút.
Vì sao không nên cherry-pick qua lại giữa các nhánh hằng ngày?
Vì cùng một thay đổi xuất hiện hai lần với hai mã băm, lịch sử khó đọc và dễ xung đột khi sau này merge hai nhánh. Cherry-pick dành cho việc đưa bản sửa sang nhánh phát hành hoặc cứu một commit lỡ làm trên nhánh sai.
Kết luận
Ba điều đáng nhớ nhất:
- Hook là lớp tiện lợi, CI là lớp bảo đảm. Chia sẻ hook bằng
core.hooksPath, nhưng đừng trông vào nó một mình. cherry-pick -xcho bản sửa, không cho việc gộp nhánh hằng ngày.worktreegiữ nguyên việc đang dở khi phải sửa gấp ở nhánh khác.
Tham khảo
- githooks — danh sách hook và thời điểm chạy
- git-config: core.hooksPath
- git-cherry-pick
- git-worktree
Điều hướng
- Bài trước: 3.10 — Conventional Commits
- Bài tiếp theo: 3.12 — Mini case study: lộ mật khẩu database lên GitHub
- Về module: Trang mục lục