Tình huống ai cũng gặp: một tính năng hôm nay hỏng, tuần trước còn chạy, và ở giữa là mấy trăm commit. Commit nào làm nó hỏng? Cách thủ công là git checkout từng commit rồi test — với 300 commit đó là 300 lần build và chạy, cả ngày không xong. git bisect biến bài toán đó thành tìm kiếm nhị phân: mỗi lần kiểm thử loại được một nửa số commit khả nghi. 300 commit chỉ còn khoảng 9 lần. Bài này đo thật quá trình đó trong Git 2.39, và chỉ cách để máy tự chạy toàn bộ.
Ý tưởng: nhị phân trên lịch sử commit
Điều kiện để bisect chạy được:
- Một mốc good: commit cũ mà bạn biết tính năng còn chạy đúng.
- Một mốc bad: commit mà nó đã hỏng (thường là
HEAD). - Lỗi phải đơn điệu: từ good tới bad là
good...good | bad...bad— một khi hỏng thì hỏng luôn, không nhấp nháy. (Lỗi lúc có lúc không thì bisect cho kết quả sai — sẽ nói ở phần đánh đổi.)
Git checkout đúng điểm giữa giữa good và bad, bạn (hoặc một script) phán xử "tốt hay xấu", và Git loại nửa không chứa lỗi. Lặp lại, khoảng cách giảm một nửa mỗi lần — độ phức tạp O(log n).
git bisect start
git bisect bad HEAD # commit hiện tại hỏng
git bisect good d784778 # commit cũ còn chạy đúng
# Git checkout điểm giữa -> bạn test -> trả lời:
git bisect good # hoặc git bisect bad

Hình 1: Bisect cần một mốc good, một mốc bad, và lỗi đơn điệu. Git checkout điểm giữa cho bạn phán xử; git bisect run để một script tự phán xử qua exit code.
Đo thật: 31 commit, tìm ra lỗi sau 5 bước
Dựng một repo có 31 commit của một file calc.go. Hàm tinh(10) trả về 20 đúng cho tới commit c20 — commit đó "refactor" và vô tình đổi công thức khiến tinh(10) thành 21. Từ c20 trở đi mọi commit đều sai. Xác nhận: commit đầu c00 (d784778) chạy ra 20, HEAD chạy ra 21.
Thay vì phán xử tay, viết một script test và để git bisect run tự chạy — nó gọi script ở mỗi điểm giữa, đọc exit code (0 = tốt, 1..124 = xấu, 125 = bỏ qua), tự quyết good/bad cho tới khi tìm ra:
cat > test.sh <<'EOF'
#!/bin/bash
out=$(go run calc.go 2>/dev/null)
[ "$out" = "20" ] # exit 0 nếu đúng, exit 1 nếu sai
EOF
chmod +x test.sh
git bisect start
git bisect bad HEAD
git bisect good d784778
git bisect run ./test.sh

Hình 2: git bisect run tự chia đôi qua các điểm c15 → c22 → c18 → c20 → c19, rồi kết luận 82afb8f (c20 "refactor tinh()") là commit đầu tiên gây lỗi — chỉ sau 5 lần chạy test.
Kết quả chính xác: Git kiểm thử c15, c22, c18, c20, c19 — 5 lần — rồi in 82afb8f... is the first bad commit, đúng là c20 refactor tinh() (VO TINH GAY LOI). Nó còn in luôn diff của commit thủ phạm (calc.go | 22 ++...) để bạn thấy ngay thay đổi nào có tội. So với dò tuần tự tối đa 30 lần, bisect nhanh gấp 6. Và sức mạnh thật sự lộ ra khi quy mô lớn: 1.000 commit chỉ cần ~10 lần, 1 triệu commit ~20 lần — vì log2 tăng cực chậm.
git bisect run là điểm biến nó thành công cụ tự động
Phần thủ công (git bisect good/bad) vẫn tốt khi tiêu chí "hỏng" là thứ chỉ mắt người thấy (giao diện lệch, màu sai). Nhưng khi bạn viết được một script trả lời đúng/sai, git bisect run biến toàn bộ thành một lệnh chạy không cần trông. Vài điểm cần nhớ về exit code của script:
0= commit tốt.1đến124(trừ125) = commit xấu.125= không kiểm được (ví dụ commit này không build) — Git bỏ qua, chọn commit khác. Cực hữu ích khi vài commit ở giữa vốn đã hỏng build vì lý do khác.128trở lên = dừng bisect (lỗi nghiêm trọng).
Mẹo thực tế: dùng chính bộ test có sẵn làm script (go test ./... -run TestCaiBiHong), hoặc một lệnh grep trên output. Chỉ cần nó trả exit code đúng quy ước.
Đánh đổi và cạm bẫy
Lỗi không đơn điệu làm bisect sai. Nếu lỗi "lúc có lúc không" (phụ thuộc thời gian, thứ tự test, dữ liệu ngẫu nhiên), một commit tốt có thể ngẫu nhiên test ra "xấu" và bisect chỉ nhầm thủ phạm. Trước khi bisect, cố làm cho phép tái hiện lỗi ổn định. Nếu không thể, git bisect run với một script chạy test nhiều lần rồi lấy kết quả đa số có thể giảm nhiễu, nhưng không triệt để.
Chi phí là thời gian mỗi lần test, không phải số commit. log n lần nghe ít, nhưng nếu mỗi lần build + test mất 10 phút thì 12 bước vẫn là 2 tiếng. Tối ưu script test cho nhanh (chỉ build phần cần, chỉ chạy đúng test tái hiện lỗi) quan trọng hơn là lo về số bước.
git bisect reset để về trạng thái ban đầu. Trong lúc bisect, Git để bạn ở trạng thái "detached HEAD" (bài sau của sê-ri) trên các commit giữa. Xong việc phải git bisect reset để quay về nhánh cũ — quên bước này dễ tưởng mình "mất" nhánh đang làm.
Ba ý mang về
- git bisect biến việc tìm commit lỗi thành tìm kiếm nhị phân: mỗi lần kiểm thử loại một nửa số commit — đo thật, tìm ra thủ phạm trong 31 commit chỉ sau 5 lần (log2), nhanh gấp 6 so với dò tuần tự; 1 triệu commit cũng chỉ ~20 lần.
git bisect run <script>để máy tự chạy hết: script trả exit code (0tốt,1..124xấu,125bỏ qua commit không build được) — Git tự chia đôi tới khi in... is the first bad commitkèm diff của thủ phạm.- Điều kiện và cạm bẫy: cần một mốc good, một mốc bad, và lỗi phải đơn điệu (không nhấp nháy) — lỗi chập chờn làm bisect nhầm; chi phí thật là thời gian mỗi lần test; nhớ
git bisect resetkhi xong.
Nguồn
- Pro Git — Git Tools: Debugging with Git (mục "Binary Search"): https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git
- Git Documentation — git-bisect (kèm quy ước exit code của
bisect run): https://git-scm.com/docs/git-bisect
Phần sau ta xử lý hai khái niệm hay đi cùng nhau: git cherry-pick để bê một commit lẻ sang nhánh khác, và trạng thái detached HEAD mà bạn vừa gặp trong lúc bisect — vì sao nó không đáng sợ như tên gọi.