Dennis Hendricks

git bisect: Bugs in Minuten auf den richtigen Commit zurückführen

Kennst du diesen Moment, wenn jemand sagt: „Das hat letzte Woche noch funktioniert“? Und du schaust in die Git-Historie und siehst: vierhundert Commits seit letzter Woche. Irgendeiner davon hat den Bug eingebaut. Aber welcher?

Die naive Methode kennt jeder. Du springst zu einem alten Commit, testest, springst weiter, testest wieder, hangelst dich so durch die Historie. Per Hand. Commit für Commit. Und nach einer Stunde sitzt du immer noch da, hast den Überblick verloren und bist dir nicht mal sicher, welche du schon geprüft hast.

Es gibt einen besseren Weg. Er steckt schon in Git drin, kostet dich keine Installation, und die meisten Entwickler benutzen ihn trotzdem nie: git bisect.

Die Idee dahinter: rate clever, nicht stur

Stell dir vor, du suchst in einem Telefonbuch nach einem Namen. Du blätterst doch nicht Seite für Seite von vorne durch. Du schlägst in der Mitte auf, schaust: zu weit hinten oder zu weit vorne? Und machst mit der entsprechenden Hälfte weiter. Wieder Mitte, wieder halbieren. So bist du in ein paar Schritten am Ziel.

Genau das macht git bisect mit deiner Commit-Historie. Du sagst ihm: Dieser alte Commit war gut, dieser neue ist kaputt. Git nimmt den Commit genau in der Mitte, du testest ihn, und je nachdem, ob er funktioniert oder nicht, weißt du, in welcher Hälfte der Übeltäter steckt. Die andere Hälfte? Kannst du komplett vergessen.

Das ist eine binäre Suche. Und die Mathematik ist hier auf deiner Seite: Bei 1000 Commits brauchst du nicht 1000 Tests, sondern ungefähr zehn. Bei 10.000 Commits sind es vierzehn.

So läuft es in der Praxis

Du startest die Suche und sagst Git, was du schon weißt:

git bisect start
git bisect bad                  # der aktuelle Stand ist kaputt
git bisect good v1.4.0          # diese Version lief noch sauber

bad markiert einen Commit, in dem der Bug da ist, meistens den, auf dem du gerade sitzt. good markiert einen, in dem alles noch in Ordnung war. Das kann ein Tag sein, ein Commit-Hash, irgendein Bezugspunkt, von dem du sicher bist, dass er funktioniert hat.

In dem Moment springt Git automatisch zu einem Commit irgendwo in der Mitte zwischen den beiden. Jetzt bist du dran: Bau das Projekt, starte den Test, reproduziere den Bug, oder eben nicht. Und dann sagst du Git das Ergebnis:

git bisect good   # Bug nicht da, dieser Commit ist sauber
# oder
git bisect bad    # Bug ist da

Und sofort springt Git zum nächsten Commit, wieder in die Mitte des verbliebenen Bereichs. Du testest, du urteilst, Git springt weiter. Mit jeder Antwort halbiert sich der Suchraum.

Nach ein paar Runden meldet sich Git mit dem Ergebnis:

a3f9c12 is the first bad commit

Da ist er. Der erste Commit, in dem der Bug auftaucht. Mit Autor, Datum und Commit-Message. Oft sieht man dann schon am Diff, was passiert ist.

Aufräumen nicht vergessen

Während des Bisects bist du in einem etwas eigenartigen Zustand. Git hat dich von deinem Branch losgelöst und springt zwischen alten Commits hin und her. Wenn du fertig bist, holst du dich mit einem Befehl zurück:

git bisect reset

Das bringt dich wieder dahin, wo du vorher warst, auf deinen eigentlichen Branch. Diesen Schritt vergisst man am Anfang gerne und wundert sich dann, warum der Code so alt aussieht. Also: immer schön resetten, wenn die Jagd vorbei ist.

Der eigentliche Trick: Git testet selbst

Bis hierhin war das schon hilfreich. Aber jetzt kommt der Teil, wegen dem man sich fragt, warum man git bisect nicht längst benutzt.

Du musst nämlich nicht bei jedem Schritt selbst von Hand testen. Wenn du einen Befehl hast, der den Bug automatisch prüft (ein Test, ein Script, irgendwas, das mit Exit-Code 0 für „gut“ und allem anderen für „kaputt“ antwortet), dann übergibst du Git einfach diesen Befehl:

git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run pytest tests/test_login.py

Und dann lehnst du dich zurück. Git macht jetzt alles allein: Es springt zu einem Commit, führt den Test aus, liest den Exit-Code, entscheidet gut oder schlecht, springt zum nächsten. Immer weiter, bis es den schuldigen Commit isoliert hat. Du schaust nur noch auf das Ergebnis.

Das funktioniert mit allem, was sich über den Exit-Code ausdrücken lässt. Ein pytest, ein npm test, ein selbstgeschriebenes Shell-Script, das ein paar Bedingungen prüft. Konvention ist: Exit-Code 0 heißt „gut“, Codes von 1 bis 124 heißen „schlecht“. Solange dein Befehl sich daran hält, übernimmt Git die ganze stumpfe Arbeit.

Ein paar Dinge, die das Leben leichter machen

Was die Suche manchmal stört, sind Commits, die du gar nicht sinnvoll testen kannst, weil das Projekt da grad nicht baut oder ein anderer, unzusammenhängender Fehler im Weg steht. Für die gibt es git bisect skip. Git nimmt dann einen Nachbar-Commit und lässt den kaputten in Ruhe.

git bisect skip

Und der wichtigste Punkt überhaupt, der entscheidet, ob das Ganze funktioniert: Du brauchst eine verlässliche Reproduktion. Wenn dein Test mal anschlägt und mal nicht, also ein flaky Test, ein Timing-Problem, irgendwas Wackeliges, dann führt dich auch die beste binäre Suche in die Irre. Git glaubt jedem deiner Urteile blind. Sind die Urteile unzuverlässig, ist das Ergebnis Müll. Bevor du also bisectest, stell sicher, dass du den Bug auf Kommando reproduzieren kannst. Immer gleich.

Die wichtigsten git-bisect-Befehle auf einen Blick

Damit du nicht mitten in der Jagd nachschlagen musst, hier die Befehle, die du im Alltag brauchst:

Befehl Was er tut
git bisect start startet eine neue Bisect-Session
git bisect bad [commit] markiert einen Commit als kaputt (Standard: aktueller)
git bisect good [commit] markiert einen Commit als funktionierend
git bisect skip überspringt einen Commit, der sich nicht testen lässt
git bisect run <script> automatisiert die Suche per Test-Script
git bisect log zeigt das Protokoll der bisherigen Schritte
git bisect replay <datei> spielt ein gespeichertes Protokoll wieder ab
git bisect reset beendet die Session und bringt dich zurück

Die meiste Zeit kommst du mit den ersten fünf aus. Aber log und replay sind ein unterschätztes Sicherheitsnetz, dazu gleich mehr.

Wenn „gut“ und „schlecht“ nicht passen

Manchmal sucht man gar keinen Bug. Vielleicht willst du wissen, ab wann ein bestimmtes Feature funktioniert, oder ab wann ein Test plötzlich grün wurde. Da fühlen sich die Wörter „good“ und „bad“ verkehrt an. Funktionierend wäre ja das neue, nicht das gute.

Für genau diesen Fall kannst du eigene Begriffe vergeben:

git bisect start --term-old funktioniert-nicht --term-new funktioniert

Oder du nimmst die eingebauten Alternativen old und new:

git bisect old   # statt good
git bisect new   # statt bad

Inhaltlich ändert sich nichts an der binären Suche. Es liest sich nur sauberer, wenn der gesuchte Übergang keine Verschlechterung, sondern eine Veränderung ist. Kleinigkeit, aber sie macht die Session weniger verwirrend.

Das Sicherheitsnetz: log und replay

Stell dir vor, du bist acht Schritte tief in einer Bisect-Session und vertippst dich. Du sagst good, obwohl der Commit eigentlich kaputt war. Jetzt führt dich Git in die völlig falsche Richtung, und du merkst es vielleicht erst am sinnlosen Endergebnis.

Dafür gibt es das Protokoll. Mit git bisect log siehst du jeden Schritt, den du bisher gemacht hast. Und du kannst das Protokoll in eine Datei schreiben, von Hand die falsche Zeile korrigieren und die ganze Session damit neu abspielen:

git bisect log > bisect-protokoll.txt
# Datei bearbeiten, den Fehler rausnehmen
git bisect reset
git bisect replay bisect-protokoll.txt

So musst du nicht von vorn anfangen, nur weil eine einzige Antwort daneben lag. Bei langen Sessions mit teuren Tests spart dir das eine Menge Zeit.

git bisect oder git blame – was wann?

Eine berechtigte Frage: Es gibt doch git blame und git log -S, warum dann noch bisect? Weil die drei unterschiedliche Probleme lösen.

git blame sagt dir, wer welche Zeile zuletzt angefasst hat. Super, wenn du schon weißt, welche Zeile schuld ist. git log -S "suchbegriff" (die sogenannte Pickaxe) findet Commits, in denen ein bestimmter Textschnipsel hinzugefügt oder entfernt wurde. Gut, wenn du den Code kennst, der sich geändert hat.

git bisect brauchst du für den dritten Fall, und der ist gemein: Du weißt nicht, welche Zeile, welche Datei, welcher Codeabschnitt. Du weißt nur, dass sich das Verhalten geändert hat. Der Bug zeigt sich vielleicht in einem Modul, das gar nicht der Auslöser war. Bei so einem Verhaltensfehler über viele Commits hinweg ist die binäre Suche oft die einzige Methode, die dich verlässlich ans Ziel bringt.

Warum sich das wirklich lohnt

Das Schöne an git bisect ist, dass es ein menschliches Problem in ein mathematisches verwandelt. Du musst nicht raten, du musst nicht den Code verstehen, du musst nicht mal eine Ahnung haben, wo der Fehler sitzt. Du musst nur eine einzige Frage beantworten können: Ist der Bug da oder nicht?

Den Rest erledigt die Halbierung. Und aus einer Stunde frustrierten Durchhangelns durch hunderte Commits wird eine Sache von ein paar Minuten. Beim nächsten „das lief letzte Woche noch“ weißt du, was zu tun ist.

Häufige Fragen

Was macht git bisect? git bisect findet per binärer Suche den Commit, der einen Bug eingeführt hat. Du markierst einen funktionierenden und einen kaputten Commit, und Git arbeitet sich durch Halbierung zum schuldigen Commit vor. Bei 1000 Commits sind das rund zehn Schritte statt 1000 Tests.

Wie automatisiere ich git bisect? Mit git bisect run <befehl>. Übergib einen Test oder ein Script, das mit Exit-Code 0 für „funktioniert“ und einem Wert von 1 bis 124 für „kaputt“ antwortet. Git führt es bei jedem Schritt selbst aus und isoliert den schuldigen Commit komplett allein.

Was, wenn sich ein Commit nicht testen lässt? Dann nutzt du git bisect skip. Git wählt einen Nachbar-Commit und lässt den nicht testbaren (etwa weil er nicht baut) außen vor.

Wie komme ich nach dem Bisect wieder zurück? Mit git bisect reset. Das beendet die Session und bringt dich auf den Branch und Stand zurück, auf dem du vor der Suche warst.

Was ist der Unterschied zwischen git bisect und git blame? git blame zeigt, wer eine bekannte Zeile zuletzt geändert hat. git bisect findet einen Commit, wenn du nur das geänderte Verhalten kennst, aber nicht die Stelle im Code. Für Verhaltensfehler über viele Commits ist bisect meist das einzige verlässliche Werkzeug.

Vorschaubild: Pieter Benedictus, Unsplash