git bisect: cách tìm commit làm hỏng code
git bisect tìm commit đã đưa lỗi vào bằng tìm kiếm nhị phân trong lịch sử. Chạy git bisect start, đánh dấu commit lỗi bằng git bisect bad và commit chắc chắn đúng bằng git bisect good <hash>, sau đó kiểm tra các commit được đề xuất cho tới khi Git chỉ ra thủ phạm; cuối cùng chạy git bisect reset.
git bisect tìm commit đã đưa lỗi vào bằng phương pháp tìm kiếm nhị phân. Bạn chỉ định một commit có lỗi và một commit không có lỗi — Git chuyển bạn tới điểm giữa của khoảng và hỏi: «ở đây đúng hay sai?». Mỗi câu trả lời loại bỏ một nửa số ứng viên, nên trong 1000 commit, thủ phạm được tìm ra chỉ sau khoảng 10 lần kiểm tra.
Công cụ này đặc biệt có giá trị khi lỗi không rõ ràng: test bị đỏ, hiệu năng giảm, giao diện bị lệch, còn diff thì chẳng cho thấy gì. Thay vì đọc hàng trăm thay đổi, bạn đặt cùng một câu hỏi đơn giản vài lần.
Tìm kiếm nhị phân trong lịch sử hoạt động thế nào
Giả sử giữa phiên bản chạy đúng v2.3 và main hiện tại có 200 commit. Git lấy commit ở giữa và kiểm tra. Nếu ở đó đã lỗi, lỗi xuất hiện trong nửa đầu; nếu không, lỗi nằm ở nửa sau. Số ứng viên giảm một nửa sau mỗi bước: 200, 100, 50, 25, 13, 7, 4, 2, 1. Tám lần kiểm tra thay cho hai trăm.
Điều kiện then chốt: tính chất «bị lỗi» phải đơn điệu. Nếu lỗi xuất hiện, biến mất rồi lại xuất hiện, bisect sẽ tìm ra một trong các điểm chuyển, nhưng không chắc đó là điểm bạn cần.
git bisect từng bước: chế độ thủ công
1. Tìm commit đúng và commit sai
Commit sai thường là HEAD hiện tại. Commit đúng là bất kỳ commit nào chắc chắn hoạt động: thẻ của bản phát hành trước, commit của tuần trước. Để tìm ứng viên, hãy dùng git log --oneline, git tag hoặc git log --since="2 weeks ago".
2. Bắt đầu phiên làm việc
git bisect start
git bisect bad # commit hiện tại bị lỗi
git bisect good v2.3 # ở bản phát hành v2.3 mọi thứ hoạt động
Bisecting: 99 revisions left to test after this (roughly 7 steps)
[4f5e6d7c...] Cập nhật phụ thuộc frontend
Có thể viết gọn trên một dòng: git bisect start HEAD v2.3 — commit lỗi trước, commit đúng sau. Git ngay lập tức chuyển thư mục làm việc tới commit ở giữa (ở trạng thái detached HEAD).
3. Kiểm tra và đánh dấu
Build dự án, chạy test hoặc tái hiện lỗi bằng tay. Sau đó báo kết quả:
git bisect good # ở đây hoạt động bình thường
Bisecting: 49 revisions left to test after this (roughly 6 steps)
git bisect bad # ở đây bị lỗi
Bisecting: 24 revisions left to test after this (roughly 5 steps)
4. Nhận kết quả
Khi chỉ còn một ứng viên, Git sẽ hiển thị nó:
a1b2c3d4e5f6... is the first bad commit
commit a1b2c3d4e5f6...
Author: Ivan Petrov <ivan@example.com>
Date: Tue Sep 29 11:40:12 2026 +0300
Lưu đệm kết quả tính giỏ hàng
src/cart.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
5. Kết thúc phiên
git bisect reset
Lệnh này đưa bạn về nhánh mà bạn đã bắt đầu. Đừng quên bước này: khi phiên chưa đóng, bạn vẫn ở detached HEAD, và các commit mới rất dễ bị mất. Lệnh git bisect reset HEAD giữ bạn ở commit vừa tìm được, còn git bisect reset <commit> chuyển tới commit bạn chỉ định.
Tìm kiếm tự động: git bisect run
Nếu việc kiểm tra có thể viết thành một lệnh, hãy giao toàn bộ công việc cho Git. git bisect run chạy một script ở mỗi bước và tự đánh dấu dựa trên mã thoát:
| Mã thoát | Ý nghĩa |
|---|---|
| 0 | good — commit tốt |
| 1–127, trừ 125 | bad — commit lỗi |
| 125 | skip — không thể kiểm tra commit này, bỏ qua |
| trên 127 (hoặc <0) | dừng bisect (ví dụ script bị kết thúc bởi tín hiệu) |
Ví dụ chạy:
git bisect start HEAD v2.3
git bisect run npm test
git bisect run pytest tests/test_cart.py -x
git bisect run make check
Với kiểm tra phức tạp, hãy viết một script nhỏ. Quan trọng là phân biệt «không build được» với «lỗi đã tái hiện», nếu không bisect sẽ coi bản build hỏng là lỗi cần tìm:
#!/bin/sh
# check.sh
npm ci --silent || exit 125 # không cài được phụ thuộc — bỏ qua
npm run build || exit 125 # không build được — bỏ qua
node scripts/check-cart-total.js # 0 — ổn, 1 — có lỗi
chmod +x check.sh
git bisect run ./check.sh
Hãy đặt script ngoài các tệp được theo dõi hoặc ở thư mục có trong mọi commit được kiểm tra: bisect chuyển thư mục làm việc, nên tệp mới thêm gần đây sẽ biến mất ở các commit cũ. An toàn nhất là đặt nó trong /tmp hoặc bất kỳ thư mục nào nằm ngoài kho.
Test chưa tồn tại ở các commit cũ
Tình huống thường gặp: bạn viết test tái hiện lỗi, nhưng file này không có ở các commit cũ. Giải pháp là sao chép test ra ngoài kho:
cp tests/test_regression.py /tmp/
git bisect run pytest /tmp/test_regression.py
git bisect skip: khi commit không thể kiểm tra
Đôi khi commit ở giữa không build được hoặc hỏng vì lý do khác. Không thể đánh dấu nó good hay bad — kết quả sẽ sai. Hãy bỏ qua nó:
git bisect skip
git bisect skip v2.4..v2.4.3 # bỏ qua cả khoảng
Git sẽ chọn một commit lân cận. Nếu các commit bị bỏ qua nằm đúng ở ranh giới, cuối cùng bạn sẽ thấy danh sách «thủ phạm có thể» thay vì một commit duy nhất — khi đó hãy xem diff của từng cái bằng tay.
Tự đặt thuật ngữ thay cho good và bad
Bisect không chỉ dùng để tìm lỗi. Ví dụ, bạn muốn tìm commit sau đó trang tải nhanh hơn. Gọi các commit nhanh là «xấu» thì khó hiểu, vì vậy có thể đặt từ khác:
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.3
Các từ đồng nghĩa có sẵn là old và new: git bisect new, git bisect old. Chỉ cần nhớ một quy tắc: trạng thái «cũ» phải thuộc commit sớm hơn.
Nhật ký phiên: log, replay, visualize
git bisect log— in ra toàn bộ các đánh dấu của bạn. Hãy lưu vào tệp nếu phiên dài.git bisect replay tệp— phát lại phiên từ nhật ký. Đánh dấu nhầm ở bước thứ sáu? Lưu nhật ký, xóa các dòng sai, chạygit bisect resetrồireplay.git bisect visualize(hoặcgit bisect view) — hiển thị các ứng viên còn lại; với--onelinesẽ in qua git log.
git bisect log > /tmp/bisect.log
# chỉnh sửa nhật ký
git bisect reset
git bisect replay /tmp/bisect.log
Bisect và commit gộp (merge)
Trong lịch sử có nhiều lần gộp, bisect có thể đưa bạn vào bên trong một nhánh đã được gộp — tới các commit được tạo trước khi nhánh đồng bộ với main, và do đó được xây trên một nền khác. Nếu cần biết trước hết lần gộp nào đưa lỗi vào, hãy dùng:
git bisect start --first-parent
Khi đó Git chỉ đi theo cha thứ nhất của các commit gộp, nghĩa là theo lịch sử của nhánh chính. Khi đã tìm được lần gộp gây lỗi, bạn có thể chạy bisect thứ hai ngay bên trong nhánh tương ứng.
Các lỗi thường gặp
- Nhầm good và bad. Kết quả sẽ vô nghĩa. Khôi phục phiên bằng
git bisect logvàreplay, đừng làm lại từ đầu. - Thư mục làm việc chưa sạch. Thay đổi chưa commit cản trở việc chuyển commit và ảnh hưởng tới kiểm tra. Trước khi bắt đầu hãy commit hoặc
git stash. - Artifact build còn sót. Cache cũ hoặc
node_modulestừ commit khác cho kết quả sai. Trong script, hãy build sạch. - Test không ổn định (flaky). Nếu test đôi khi tự hỏng, bisect sẽ đánh dấu một commit ngẫu nhiên. Hãy chạy kiểm tra nhiều lần hoặc sửa test trước khi tìm kiếm.
- Quên reset. Bạn vẫn ở detached HEAD và bắt đầu làm việc. Có thể tìm lại các commit bị mất nhờ reflog.
Ví dụ: tìm một hồi quy từ đầu đến cuối
Người dùng báo cáo tổng giỏ hàng bị tính sai khi có giảm giá. Ở bản v2.3 mọi thứ đúng, còn trên main thì không. Viết một kiểm tra tối thiểu và chạy tìm kiếm:
cat > /tmp/check.sh <<'SH'
#!/bin/sh
npm ci --silent || exit 125
node -e "const c=require('./src/cart'); process.exit(c.total([{price:100}], 0.1) === 90 ? 0 : 1)"
SH
chmod +x /tmp/check.sh
git stash # cất các thay đổi chưa commit
git bisect start HEAD v2.3
git bisect run /tmp/check.sh
...
a1b2c3d4e5f6... is the first bad commit
git bisect log > /tmp/cart-bisect.log
git bisect reset
git stash pop
Toàn bộ việc tìm kiếm trên một trăm commit diễn ra mà bạn không cần can thiệp, trong lúc bạn làm việc khác. Nhật ký đã lưu hữu ích khi bạn cần cho đồng nghiệp thấy chính xác thủ phạm được tìm ra như thế nào.
Làm gì với commit đã tìm được
Khi đã tìm ra thủ phạm, hãy xem toàn bộ commit: git show a1b2c3d. Một commit nhỏ với thông điệp rõ ràng thường cho biết ngay nguyên nhân — đây là một lý do khiến các nhóm đề cao commit nguyên tử: bisect trên lịch sử toàn những commit «fix stuff» khổng lồ gần như vô dụng. Sau đó có hai hướng: sửa lỗi bằng một commit mới, hoặc, nếu thay đổi đó hoàn toàn sai và đã được công bố, hoàn tác bằng git revert a1b2c3d.
Tóm lại
git bisect biến việc tìm hồi quy từ đọc hàng trăm diff thành vài câu trả lời «chạy / không chạy», và với git bisect run thì thành một lệnh duy nhất. Bạn chỉ cần hai mốc — một commit tốt và một commit xấu — cùng một kiểm tra có thể lặp lại. Đọc lịch sử, điều hướng giữa các commit và tìm lỗi trong đó được hướng dẫn thực hành trong khóa học Git.
Câu hỏi thường gặp
git bisect cần bao nhiêu bước?
Khoảng log2 của số commit trong khoảng: khoảng 7 bước cho 100 commit và khoảng 10 bước cho 1000 commit. Git tự hiển thị ước tính roughly N steps.
Có chạy bisect được khi không biết commit nào tốt không?
Cần ít nhất một commit tốt. Hãy chọn một commit cũ chắc chắn hoặc thẻ của bản phát hành và kiểm tra thủ công; vài bước thừa do khoảng quá rộng hầu như không đáng kể.
git bisect có thay đổi lịch sử kho không?
Không. Nó chỉ chuyển thư mục làm việc giữa các commit và lưu các đánh dấu trong tham chiếu tạm refs/bisect, các tham chiếu này bị xóa khi chạy git bisect reset.
Có thể chỉ định nhiều commit tốt không?
Có, bạn có thể truyền nhiều commit cho git bisect good, điều này thu hẹp khoảng tìm kiếm, nhất là trong lịch sử có nhiều nhánh.
Bisect chỉ dùng để tìm lỗi thôi sao?
Không. Nó có thể tìm thời điểm bất kỳ mà một thuộc tính thay đổi: khi một tệp xuất hiện, khi build nhanh hơn hoặc khi hành vi thay đổi. Với mục đích này, các thuật ngữ old/new hoặc tự đặt qua --term-old và --term-new rất tiện.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.
Đến khóa học