Die Test-Suite ist seit drei Wochen grün. Heute schlägt npm test bei einer Assertion fehl, die nichts mit Ihrer letzten Änderung zu tun hat, und git log v1.4.0..HEAD zeigt 340 Commits von vier Personen seit dem letzten Release, von dem jemand sicher war, dass er funktioniert. Jeden Diff zu lesen kostet Sie einen ganzen Tag. Wahllos Commits auszuchecken geht schneller, ist aber immer noch kein Plan.
git bisect macht daraus eine Binärsuche. Sie nennen ihm einen Commit, der nachweislich gut ist, und einen, der nachweislich kaputt ist; das Tool checkt den Commit in der Mitte dazwischen aus und wartet, bis Sie genau diesen Commit testen und das Ergebnis melden. Jede Antwort halbiert den Bereich. 340 Commits lassen sich in höchstens 9 Tests auflösen, denn 2^9 = 512.
Wie git bisect die Suche bei jedem Schritt halbiert
Stellen Sie sich die 340 Commits als Linie vor, vom ältesten zum neuesten, das bekannte gute Tag an einem Ende und das kaputte HEAD am anderen. git bisect läuft diese Linie nicht ab. Es springt in die Mitte, wartet auf ein Urteil und verwirft dann die Hälfte, die dieses Urteil ausschließt:
| |
Angenommen, der mittlere Commit besteht den Test. Der Bug liegt in der neueren Hälfte, also verschwindet die ältere Hälfte endgültig, und Sie sehen diese Commits in dieser Sitzung nie wieder. Wiederholen: Mitte des Verbleibenden, testen, Hälfte verwerfen. Nach etwa log2(n) Runden ist der Bereich auf einen einzigen Commit geschrumpft – den, bei dem sich das Verhalten von funktionierend zu fehlerhaft geändert hat. Das ist nicht immer der Commit, der den Bug im tieferen Sinne “verursacht” hat. Es ist der Commit, bei dem sich das Verhalten geändert hat, und das ist fast immer genau das, was Sie brauchen.
So starten Sie eine git-bisect-Sitzung
Führen Sie dies in einem sauberen Arbeitsverzeichnis aus. Bisect wechselt während der Suche eigenmächtig zwischen Commits und weigert sich zu starten, wenn nicht committete Änderungen vorliegen.
| |
Git checkt den Mittelpunkt aus und sagt Ihnen, wie viel noch übrig ist:
| |
Führen Sie Ihren Test bei diesem Commit aus und melden Sie dann, was Sie gefunden haben:
| |
oder
| |
Git checkt nach jeder Antwort automatisch den neuen Mittelpunkt aus. Testen und melden Sie weiter, bis nichts mehr einzugrenzen ist:
| |
Das ist Ihre Antwort. Lesen Sie diesen Diff, nicht die acht Commits drumherum. git bisect reset bringt Sie zurück zu dem Branch und Commit, von dem Sie gestartet sind, und die Checkouts, die bisect unterwegs gemacht hat, hinterlassen keine Spuren.
| |
Die Schleife mit git bisect run automatisieren
Von Hand good oder bad zu beantworten, ist in Ordnung für einen Bug, den Sie in wenigen Sekunden erkennen. Für alles, was einen echten Testlauf braucht, übergeben Sie die gesamte Schleife an ein Skript:
| |
bisect-test.sh muss nur mit dem richtigen Code beenden: 0 bedeutet gut, jeder Code von 1 bis 127 außer 125 bedeutet kaputt, und 125 bedeutet “dieser Commit lässt sich nicht testen, überspringen”.
| |
Die meisten Test-Runner beenden sich bei Erfolg bereits mit 0 und bei einem Fehlschlag mit einem Code ungleich null, sodass sich das Skript oft auf einen einzigen Aufruf des Runners reduziert. git bisect run checkt jeden Mittelpunkt aus, führt das Skript aus, liest den Exit-Code und meldet das Ergebnis selbst, mit derselben “first bad commit”-Zusammenfassung am Ende.
Das Skript kann mehr tun als nur einen Test-Runner aufzurufen. Zeigt sich die Regression nur in einem gebauten Artefakt, ist ein Skript, das docker build ausführt und danach das Image auf die Probe stellt, etwas ganz Normales für git bisect run – die Build-Schritte aus Docker-Image mit GitHub Actions bauen und veröffentlichen funktionieren in einem Skript genauso gut wie in einem Workflow. Rechnen Sie damit ein: Jede Runde kostet jetzt einen vollständigen Image-Build.
Eine Einschränkung betrifft das Skript selbst. Halten Sie es außerhalb des Bereichs, den Sie durchsuchen, oder stellen Sie sicher, dass es sich über diesen Bereich hinweg nicht geändert hat. Liegt das Skript in der durchsuchten Historie, checkt ein alter Commit zusammen mit dem alten Code auch eine alte Version des Skripts aus, und Sie führen bei jedem Schritt nicht mehr denselben Test aus.
Commits überspringen, die sich nicht testen lassen, mit git bisect skip
Manche Commits lassen sich nicht für sich allein testen: ein Commit mitten in einem Refactoring, der nicht baut, eine Schema-Migration, die einen Datenbankzustand braucht, den Sie nicht haben. Ein good/bad-Urteil auf so einem Commit zu erzwingen, verfälscht das Ergebnis. Schließen Sie ihn stattdessen aus:
| |
Bisect checkt einen nahegelegenen Commit aus und macht um die Lücke herum weiter. Bei bisect run erledigt der Exit-Code 125 automatisch dasselbe, sodass ein Skript, das erkennt “dieser Commit baut nicht einmal”, überspringen kann, statt ein falsches Bad zu melden. Überspringen Sie zu viele benachbarte Commits, bleibt am Ende zu wenig testbarer Bereich übrig: Die endgültige Antwort wird dann “der Bug stammt aus einem dieser N Commits” statt aus einem einzigen.
Wann git bisect nicht das richtige Werkzeug ist
Bisect setzt voraus, dass Ihr Test deterministisch ist: gleicher Commit, gleiches Ergebnis, jedes Mal. Ein flakiger Test bricht diese Annahme stillschweigend. Bisect kann nicht unterscheiden zwischen “dieser Commit ist kaputt” und “dieser Test hat geflackert”, und eine falsche Antwort am Anfang schickt jede spätere Runde in die falsche Hälfte. Beheben oder schließen Sie die flakige Assertion aus, bevor Sie bisecten, nicht danach.
Es setzt auch voraus, dass ein Commit eine einzige, saubere Verhaltensänderung erzeugt hat. Ein Bug, der sich über mehrere Commits eingeschlichen hat, oder einer, der von angesammeltem Zustand statt von einem einzigen Diff abhängt, konvergiert auf nichts Brauchbares. Eine stark rebasierte Historie ist eine mildere Version desselben Problems (siehe Git Rebase vs Merge: Unterschiede und wann welches): Der erste kaputte Commit, den bisect findet, kann ein gesquashter Commit sein, der fünf logische Änderungen bündelt. Korrekt, aber zu grob, um ihn isoliert zu überprüfen.
Keiner der beiden Fälle verlangt ein anderes Werkzeug, nur einen anderen Ausgangspunkt. Machen Sie zuerst den Test zuverlässig, dann wählen Sie einen good-Commit weit genug zurück, um sicher zu sein, dass der Bug dort noch nicht vorhanden war.
Wenn das nächste Mal eine Regression ohne offensichtliche Ursache auftaucht, markieren Sie die beiden Endpunkte und beantworten Sie ein paar Mal good oder bad – oder schreiben Sie das Drei-Zeilen-Skript und lassen Sie git bisect run die Antworten geben.