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

Ảnh chụp đoạn mã shell nền tối giải thích git bisect tìm nhị phân commit đầu tiên gây lỗi, ý tưởng nhị phân thay vì dò tuần tự lỗi xuất hiện đâu đó trong 30 commit dò từng cái là 30 lần bisect chia đôi mỗi lần loại nửa số commit nên khoảng log2 30 tức 5 lần, điều kiện có một mốc good chạy đúng và một mốc bad hỏng và lỗi phải đơn điệu good good rồi bad bad không nhấp nháy, thủ công git bisect start rồi git bisect bad HEAD rồi git bisect good d784778 Git checkout điểm giữa bạn test rồi trả lời git bisect good hoặc git bisect bad, tự động git bisect run script máy tự chạy hết test.sh trả exit 0 là tốt exit 1 tới 124 là xấu 127 là bỏ qua ví dụ chạy go run calc.go rồi so kết quả bằng 20 exit code là kết quả so sánh git bisect run ./test.sh Git tự chia đôi tới khi ra thủ phạm

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

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, 31 commit c00 good tinh(10) bằng 20 HEAD bad tinh(10) bằng 21 lỗi cài ở c20, git bisect run test.sh máy tự chia đôi, Bisecting 14 revisions left roughly 4 steps tại c15 running test.sh, 7 revisions left roughly 3 steps tại c22, 3 revisions left roughly 2 steps tại c18, 1 revision left roughly 1 step tại c20, 0 revisions left roughly 0 steps tại c19, thủ phạm chỉ sau 5 lần kiểm thử log2 31, 82afb8f is the first bad commit c20 refactor tinh() VO TINH GAY LOI calc.go 22 dòng đổi 1 file changed 2 insertions 20 deletions bisect found first bad commit, vì sao nhanh 30 commit thành 5 lần thay vì 30 dò tuần tự tối đa 30 lần build test git bisect log2 30 khoảng 5 lần nhanh gấp 6 với 1000 commit khoảng 10 lần với 1 triệu commit khoảng 20 lần

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 đến 124 (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.
  • 128 trở 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ề

  1. 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.
  2. git bisect run <script> để máy tự chạy hết: script trả exit code (0 tốt, 1..124 xấu, 125 bỏ qua commit không build được) — Git tự chia đôi tới khi in ... is the first bad commit kèm diff của thủ phạm.
  3. Đ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 reset khi xong.

Nguồn

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.