Regression adalah fitur yang dulu jalan, lalu rusak setelah ada perubahan. git bisect mencari commit pertama yang merusaknya dengan cara membelah dua riwayat commit berulang kali, dan tiap belahan dicek pakai tes. Bayangin kamu memasak kuah soto sambil menambah bumbu sepuluh kali, dan tiap habis menambah bumbu kamu menyisihkan sesendok kuah ke gelas kecil. Di awal rasanya pas, di akhir keasinan, dan begitu garamnya kebanyakan, kuahnya gak akan pernah pas lagi. Daripada mencicipi sepuluh gelas berurutan, kamu cicipi gelas tengah dulu: kalau masih pas, masalahnya ada di gelas-gelas sesudahnya.
🧪 Tebakan cepat: ada 10 commit, index 0 sampai 9, dan bug-nya mulai di index 7.
findFirstBaddi latihan memanggilisBadberapa kali: 4, 7, atau 10? Jawabannya ada di bagian "Menelusuri findFirstBad".
Regression dan sifat monoton
Toko kopi online punya kotak pencarian menu. Di rilis v1.0, mencari kopi menemukan "Kopi Susu" dan "Es Kopi". Sekarang hasilnya kosong. Ini riwayatnya, dari yang paling lama, dengan hasil tes pencarian di tiap commit:
| index | commit | tes cariMenu(menu, 'kopi') |
|---|---|---|
| 0 | rilis v1.0 | OK: 2 hasil |
| 1 sampai 6 | tambah menu teh, rapikan tampilan menu, tambah harga di kartu menu, perbaiki typo footer, tambah filter kategori, ganti ikon keranjang | OK: 2 hasil |
| 7 | optimasi pencarian | GAGAL: 0 hasil |
| 8 | tambah halaman promo | GAGAL: 0 hasil |
| 9 | perbarui README (HEAD) |
GAGAL: 0 hasil |
Polanya OK OK OK OK OK OK OK GAGAL GAGAL GAGAL. Begitu satu commit rusak, semua commit sesudahnya juga rusak. Sifat ini disebut monoton, dan cuma karena sifat inilah membelah dua boleh dipakai: cek satu commit di tengah, dan separuh riwayat langsung bisa dibuang. Kalau tesnya kadang lulus kadang gagal di commit yang sama (flaky), atau bug-nya sempat hilang lalu muncul lagi, bisect bisa menunjuk commit yang salah.
Bikin tes yang bisa diulang dulu
Sebelum bisect, kamu butuh cara mengecek "rusak atau gak" di commit mana pun, cepat, dan hasilnya selalu sama. Untuk toko kopi tadi:
// test.js: cek pencarian menu
const { cariMenu } = require('./app.js');
const hasil = cariMenu(['Kopi Susu', 'Teh Manis', 'Es Kopi'], 'kopi');
if (hasil.length === 2) {
console.log('OK: 2 hasil');
process.exit(0); // 0 = good
}
console.log(`GAGAL: ${hasil.length} hasil, harusnya 2`);
process.exit(1); // 1 sampai 127, selain 125 = bad
process.exit(0)artinya commit ini bagus.process.exit(1)artinya rusak.- Simpan
test.jsdi luar commit (untracked) selama bisect. Kalau di-commit belakangan, commit lama gak punya file ini.
Biar Git yang mencari: git bisect run
Perintah-perintahnya sudah kamu kenal dari pelajaran Git Bisect. Untuk regression yang punya tes otomatis, cukup tiga baris:
git bisect start HEAD v1.0 # HEAD rusak, v1.0 masih bagus
git bisect run node test.js # Git checkout, tes, dan menandai sendiri
git bisect reset # kembali ke branch semula
git bisect runmenjalankannode test.jsdi tiap commit yang dipilihnya. Exit code0jadi good,125dilewati (skip),1sampai127yang lain jadi bad, dan exit code lain menghentikan bisect.- Di akhir Git mencetak
<hash> is the first bad commitdiikuti detail commit "optimasi pencarian". Itu tersangkanya. - Commit yang gak bisa dites sama sekali, misalnya build-nya rusak, dilewati dengan
git bisect skipkalau kamu bisect manual, atau denganexit 125dari script. bisect runpaling berguna kalau tesnya cepat dan deterministik (hasilnya selalu sama). Kamu tinggal menunggu.
Menelusuri findFirstBad (yang dikerjakan latihan)
Di latihan, commit-nya cuma angka index, dan isBad(commit) adalah tesnya. Kamu menulis binary search yang dipakai bisect:
function findFirstBad(commits, isBad) {
let low = 0;
let high = commits.length - 1;
let jawaban = -1; // belum ketemu yang rusak
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (isBad(commits[mid])) {
jawaban = mid; // kandidat: rusak pertama ada di sini atau di kirinya
high = mid - 1;
} else {
low = mid + 1; // mid bagus, jadi semua di kirinya juga bagus
}
}
return jawaban;
}
function countCalls(commits, isBad) {
let calls = 0;
const wrapped = (c) => { calls++; return isBad(c); };
const firstBad = findFirstBad(commits, wrapped);
return { firstBad, calls };
}
const commits10 = Array.from({ length: 10 }, (_, i) => i);
const r1 = countCalls(commits10, (c) => c >= 7);
console.log(r1.firstBad); // 7
console.log(r1.calls <= 5); // true
Ini jejaknya untuk 10 commit dengan bug mulai di index 7:
| Langkah | low | high | mid | isBad(mid) |
Sesudahnya |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | false | low = 5 |
| 2 | 5 | 9 | 7 | true | jawaban = 7, high = 6 |
| 3 | 5 | 6 | 5 | false | low = 6 |
| 4 | 6 | 6 | 6 | false | low = 7, loop selesai |
Cuma 4 panggilan, dan hasilnya 7. Itu jawaban tebakannya.
jawaban = middisimpan sebelumhighdigeser, karena commit yang rusak itu mungkin justru yang pertama.- Untuk 1.000 commit dengan bug mulai di 742, hasilnya 742 dengan 10 panggilan, jadi
r2.calls <= 11bernilaitrue. Cek satu per satu butuh 743 panggilan. - Gak ada yang rusak:
jawabangak pernah terisi, hasilnya-1. Semua rusak: pencarian terus ke kiri, hasilnya0. countCallsmembungkusisBaddengan penghitung, lalu menjalankanfindFirstBadmemakai pembungkus itu.
Git sendiri butuh tes lebih sedikit dari ini, karena dia sudah tahu dua ujungnya: v1.0 bagus dan HEAD rusak.
💡 Setelah commit-nya ketemu
Lihat perubahannya dengan git show <hash>. Di commit "optimasi pencarian", ternyata toLowerCase() dibuang, jadi 'Kopi Susu'.includes('kopi') bernilai false. Perbaiki, lalu masukkan pengecekan dari test.js ke test suite proyekmu sebagai regression test, supaya CI langsung gagal kalau bug ini balik lagi.
⚠️ Jebakan yang sering ketemu
- Tes yang flaky. Hasil acak bikin sifat monoton hilang, dan bisect menunjuk commit yang salah tanpa error apa pun. Stabilkan tesnya dulu.
- Salah menandai satu commit. Bisect percaya penuh pada jawabanmu. Satu
badyang harusnyagoodbikin dia menyalahkan commit yang gak bersalah. - Di latihan: lupa menyimpan jawaban. Kalau
jawaban = midgak ditulis sebelumhigh = mid - 1, commit rusak pertama ikut terbuang. Dan kalau gak ada yang rusak, hasilnya harus-1, bukanlow. - Di latihan: mengecek satu per satu. Loop dari depan memang menemukan 742, tapi butuh 743 panggilan. Cek
calls <= 11untuk 1.000 commit, dan paling banyak 7 panggilan untuk 64 commit, cuma lolos kalau kamu membelah dua.
TL;DR: Regression = fitur yang dulu jalan lalu rusak. Bisect bekerja karena sifat monoton: sekali rusak, commit sesudahnya ikut rusak. Siapkan tes yang cepat dan deterministik (exit 0 = good, 1 sampai 127 selain 125 = bad, 125 = skip), lalu git bisect start HEAD v1.0, git bisect run node test.js, dan git bisect reset. Commit yang gak bisa dites dilewati dengan git bisect skip. Di latihan, findFirstBad adalah binary search: isBad(mid) benar berarti simpan jawaban = mid dan geser ke kiri, salah berarti geser ke kanan; 10 commit dengan bug di 7 cuma butuh 4 panggilan, 1.000 commit cuma 10, gak ada yang rusak hasilnya -1, semua rusak hasilnya 0. Setelah ketemu, perbaiki dan tambahkan regression test.