A regresszió természetesen péntek délután kerül elő, és természetesen „valamikor az elmúlt három hónapban” még működött. Ilyenkor a commitlistát felülről lefelé olvasni nem hibakeresés, hanem vezeklés. A git bisect felezi a gyanús történetet, a git bisect run pedig a tesztelést is átveszi. Van azonban egy apró szerződés, amelyen könnyű elrontani az egész nyomozást: a buildhiba nem jelent rossz commitot. Erre való a 125-ös kilépési kód.

Ezer commitból nagyjából tíz kérdés
A Git hivatalos leírása szerint két biztos pont kell: egy régi commit, ahol a vizsgált tulajdonság még „jó”, és egy új, ahol már „rossz”. A Git választ egy köztes commitot, mi tesztelünk, majd válaszolunk. Lineáris, ezer commitos történetben ez nagyjából tíz kör, nem ezer.
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
# tesztelés után:
git bisect good # vagy: git bisect bad
git bisect reset
A „jó” és „rossz” itt nem erkölcsi ítélet a kollégáról, csak két állapot. Fixet, teljesítményromlást vagy akár egy fájlnév-váltást is kereshetünk; ehhez a Git enged saját kifejezéseket, például --term-old fast --term-new slow. A részletes algoritmus nem pusztán dátum szerint felez: a commitgráf elérhetőségét súlyozza, amit a projekt bisect-design jegyzete bont ki.
A 125 nem kudarc, hanem „ezt nem tudom megítélni”
Ha van géppel ellenőrizhető reprodukciónk, a kézi körök helyett jöhet a run:
git bisect start HEAD v2.4.0 --
git bisect run /tmp/bisect-check.sh
git bisect reset
A futtatott program kilépési kódja a válasz. A 0 jó állapotot jelent; az 1–127 közötti érték rosszat, egyetlen kivétellel. A 125 azt mondja: ez a commit nem tesztelhető, ugord át. A 126 és 127 a POSIX shellben külön hibáknak van fenntartva, ezért esett a választás 125-re. Na, ezt nem tudtam, amíg először automatizáltam egy bisectet.
#!/bin/sh
npm run build >/dev/null 2>&1 || exit 125
npm test
A különbség nem akadémiai. Ha egy régi commit egy teljesen más okból nem fordul, és a script simán továbbadja az npm run build 1-es kódját, a Git azt a keresett regresszió bizonyítékaként kezeli. Udvariasan, gyorsan és teljesen tévesen megvádol egy ártatlan commitot.
A fenti scriptet egy 16 commitos mesterséges történeten futtattam. A 7. commit szintaktikailag hibás volt, a valódi regresszió a 11.-ben jelent meg. A run a hetediket skip állapotúnak jelölte, majd helyesen a 11.-et adta vissza első rossz commitként. A Git dokumentációja azt is javasolja, hogy a tesztscript a repositoryn kívül legyen: különben a checkout közben saját maga is eltűnhet vagy megváltozhat. Ez az a fajta geg, amelyet egyszer nevetve mesélünk, előtte viszont húsz percig nem nevetünk.
A kihagyás nem felmentés
A 125 nem varázspor. Ha a kihagyott commit közvetlenül a jó és a rossz határán áll, a Git nem tudja bizonyítani, hogy ő vagy a következő commit hozta a hibát. Ilyenkor több lehetséges első rossz commitot sorol fel. A Git felhasználói kézikönyve ezért a git bisect visualize lehetőségét is ajánlja: választhatunk egy közeli, tesztelhető commitot a vak kihagyás helyett.
Három feltételt érdemes komolyan venni:
- A teszt legyen determinisztikus. Egy flakey E2E-teszt véletlent felez, nem történetet.
- A vizsgált állapot legyen nagyjából monoton. Ha a hiba eltűnt, majd később visszajött, a bisect találhat egy határt, de nem garantálja a legelső valaha rossz commitot.
- A történelmi környezet legyen reprodukálható. Régi lockfile, eltűnt csomag, megváltozott adatbázisséma vagy külső API könnyen gyárt hamis 125-öket.
Szűkíteni lehet, de csak őszintén
Nagy monorepóban megadhatunk pathspecet, így csak az adott útvonalakat érintő commitok maradnak jelöltek:
git bisect start HEAD v2.4.0 -- \
src/payments package.json package-lock.json
Ez gyors, amíg biztosak vagyunk benne, hogy egy közös logger, buildkonfiguráció vagy infrastruktúrafájl nem okozhatta a hibát. A --first-parent másképp szűkít: merge esetén csak az első szülő láncát követi. Release-ágon jól megmondja, melyik merge hozta be a regressziót, de a feature branch konkrét commitját szándékosan nem keresi meg.
Használnám-e production hibánál?
Igen, ha a hibából tudok olcsó, determinisztikus tesztet készíteni, és van ismert jó határpontom. Ilyenkor a git bisect run az egyik leghasznosabb unalmas robot: éjszaka végigkattintja helyettem a történetet, és reggelre hoz egy commitot.
Nem használnám önmagában időzítésfüggő teljesítményhibára, külső szolgáltatástól függő reprodukcióra vagy olyan migrációs hibára, amelyhez minden commitnál más adatbázisállapot kell. Ott előbb hermetikus tesztkörnyezetet építenék. A bisect ugyanis nem érti a programot; csak vakon megbízik a kilépési kódunkban. Legalább most már tudjuk, hogyan mondjuk neki azt is, hogy „passz”.