✦

Git Bisect — tìm commit gây bug trong vài phút

•Phong Hy

Có lần mình dính một ca khá mệt: test trên CI đỏ, mà đỏ âm thầm từ lúc nào chẳng ai để ý. Repo đã đi qua hơn 400 commit kể từ lần release cuối. Ngồi đọc diff từng commit thì hết cả buổi, mà đọc xong vẫn có thể đoán sai.

Cách mình hay dùng mỗi khi rơi vào cảnh đó là git bisect. Nghe tên có vẻ hàn lâm, nhưng nó chỉ làm đúng một việc: tìm kiếm nhị phân trên lịch sử commit.

Chia đôi thay vì đọc từng cái

Ý tưởng đơn giản tới mức khó tin. Bạn biết một commit cũ còn tốt, và một commit mới đang lỗi. Ở giữa hai mốc đó chắc chắn có ít nhất một commit làm hỏng chuyện. Thay vì kiểm tra lần lượt từ đầu, ta nhảy vào đúng giữa, chạy test, rồi loại bỏ một nửa lịch sử.

Với 400 commit, cách này chỉ tốn khoảng 9 lần chạy test thay vì 400. Mỗi lần chạy vài chục giây, tổng cộng chưa tới mười phút.

Ba lệnh để bắt đầu

git bisect start
git bisect bad              # commit hiện tại đang lỗi
git bisect good v2.5.0      # mốc cuối mình biết là còn ổn

Ngay sau đó Git sẽ checkout một commit nằm giữa hai mốc và in ra số bước còn lại. Bạn chạy test, rồi khai báo kết quả:

git bisect good   # commit này còn tốt
git bisect bad    # commit này đã lỗi

Lặp lại tới khi Git in ra dòng is the first bad commit kèm luôn diff của commit thủ phạm. Xong việc thì dọn dẹp:

git bisect reset

Lệnh cuối này đưa bạn về đúng branch ban đầu, nên đừng lo bị kẹt ở detached HEAD.

Để máy tự chạy: git bisect run

Ngồi gõ good/bad chín lần cũng được, nhưng nhàn hơn là giao hết cho máy. Viết một script trả về mã thoát phù hợp:

#!/usr/bin/env bash
# /tmp/check.sh — 0 = còn tốt, 1 = đã lỗi, 125 = bỏ qua commit này
npm ci --silent || exit 125
npm test -- --grep "order total"

Rồi chạy:

git bisect run /tmp/check.sh

Git tự checkout, tự gọi script, tự đọc mã thoát và tự thu hẹp khoảng. Bạn đi pha ly cà phê, quay lại là có đáp án.

Hai điều đáng lưu ý khi viết script. Thứ nhất, phải chọn bài test hẹp và chạy nhanh, vì nó sẽ chạy cả chục lần. Thứ hai, mã thoát 125 dành cho trường hợp không đủ điều kiện kiểm tra — ví dụ commit đó không build được. Dùng đúng mã này thì Git sẽ skip thay vì kết luận sai.

Mấy cái bẫy mình từng sập

Commit không build được. Không phải commit nào cũng compile trơn tru, nhất là khi giữa đường có người đổi cấu trúc thư mục. Trả về 125 trong script, hoặc gõ tay git bisect skip. Git vẫn tìm ra thủ phạm, chỉ tốn thêm vài bước.

Bug chập chờn. Nếu test có lúc pass lúc fail, bisect sẽ chỉ vào sai commit và bạn mất niềm tin vào cả công cụ. Sửa test cho ổn định trước, hoặc chạy lặp 3 lần rồi lấy kết quả đa số.

Nhánh có merge. Mặc định Git bisect đi qua mọi nhánh. Muốn chỉ men theo nhánh chính, thêm --first-parent:

git bisect start --first-parent

Truy vết một dòng code cụ thể. Không phải lúc nào cũng có test để chạy. Khi đó git log -S "tên-hàm" tìm ra commit đầu tiên thêm hay xoá đoạn code đó — nhanh hơn bisect rất nhiều.

Lưu lại hành trình. git bisect log > /tmp/bisect.log để khỏi làm lại từ đầu, và git bisect replay /tmp/bisect.log khi cần chạy lại đúng các bước cũ. Hữu ích khi bạn lỡ tay chọn sai một lần giữa đường.

Kinh nghiệm thật

Ca gần nhất của mình là một bug giá trị đơn hàng sai lệch vài đồng. Nhìn diff thì không thấy gì bất thường — hoá ra thủ phạm là một commit bump thư viện làm tròn số. git bisect run tìm ra trong 8 phút, sau 7 lần chạy test.

Điều mình thích ở công cụ này là nó biến một việc mò mẫm thành một quy trình có bằng chứng. Không đoán, không "chắc là do commit này". Cứ chia đôi tới khi còn đúng một commit.

Bạn có đang giữ một bug cũ chưa tìm ra thủ phạm không? Thử git bisect một lần xem, rất có thể nó kết thúc nhanh hơn bạn nghĩ.

📋 Phụ lục thuật ngữ

Thuật ngữ Giải thích ngắn
Bisect Chia đôi khoảng tìm kiếm, ở đây là khoảng lịch sử commit
good / bad Hai nhãn mốc: commit còn hoạt động đúng / đã hỏng
git bisect skip Bỏ qua commit không kiểm tra được, Git sẽ chọn commit khác
Mã thoát 125 Quy ước của Git: không kết luận được, xử lý như skip
--first-parent Chỉ đi theo nhánh chính, phớt lờ commit từ nhánh phụ đã merge
Detached HEAD Trạng thái không đứng ở branch nào, Git bisect checkout kiểu này
git log -S Tìm commit đầu tiên thêm hoặc xoá một chuỗi trong mã nguồn
✦