Skip to content

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.hooksPath thì 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-pick ché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 worktree mở 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-commit và chia sẻ nó cho cả nhóm bằng core.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ần stash.

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.

HookChạy lúcHay dùng để
pre-commitTrước khi tạo commit, sau khi bạn gõ git commitChặn file có bí mật, chạy formatter
commit-msgSau khi viết xong messageKiểm tra quy ước message (bài 3.10)
pre-pushTrước khi pushChạ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:

bash
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 clone

Ví 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):

sh
#!/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
fi

Trê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:

  1. Người chưa chạy git config core.hooksPath (hoặc chưa npm install nếu dùng husky) thì hook không tồn tại.
  2. git commit --no-verify bỏ qua pre-commit và commit-msg.
  3. 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.

bash
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.

bash
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-hotfix

git 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):

stashworktree
Thư mục đang làm dởBị dọn sạch tạm thờiKhông đụng tới
Kết quả build, file đang mở trong IDEMất, phải build lạiGiữ nguyên
Hợp vớiViệc xen ngang vài phútViệ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ầmHậu quảThay bằng
Đặt hook trong .git/hooks rồi tưởng cả nhóm cóChỉ máy bạn chạyThư 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 quaKiểm tra lại ở CI
Cherry-pick không có -xKhông biết commit nào là bản sao của commit nàoLuôn dùng -x khi backport
Xoá thư mục worktree bằng tayGit 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 worktree cho việc gấp kéo dài, stash -u cho 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):

bash
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"
text
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.

bash
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"
text
commit OK — hook không chạy

Vì 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):

bash
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 --oneline
text
4afd08f fix: reject orders with negative total
bda0527 feat: add tax calculation
2ef1fa3 feat: add order rules
bash
git switch release/1.0
git cherry-pick -x 4afd08f
text
[release/1.0 84df7f7] fix: reject orders with negative total
 Date: Sat Oct 17 21:58:41 2026 +0700
 1 file changed, 1 insertion(+)
bash
git log --oneline
git log -1 --format=%B
ls
text
84df7f7 fix: reject orders with negative total
2ef1fa3 feat: add order rules

fix: reject orders with negative total

(cherry picked from commit 4afd08f32e293fd950fbecc091dc54505ca63ee7)

OrderRules.txt

Khô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):

bash
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
text
 M order.txt
?? report.txt
bash
git worktree add ../crm-hotfix -b hotfix/negative-total main
text
Preparing worktree (new branch 'hotfix/negative-total')
HEAD is now at 0688728 feat: add order validation
bash
cd ../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
text
.../crm         0688728 [feature/revenue-report]
.../crm-hotfix  88db0ee [hotfix/negative-total]
bash
git status --short
git worktree remove ../crm-hotfix
git worktree list
text
 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:

  1. 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.
  2. cherry-pick -x cho bản sửa, không cho việc gộp nhánh hằng ngày.
  3. worktree giữ nguyên việc đang dở khi phải sửa gấp ở nhánh khác.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.