Ihre CI ist auf Main rot, aber gestern war sie noch grün. Irgendwo dazwischen hat sich ein Bug eingeschlichen. Sie könnten durch 200 Commits scrollen, Diffs lesen und raten. Sie könnten in Slack fragen und hoffen, dass sich jemand daran erinnert, den relevanten Code angefasst zu haben. Oder Sie lassen Git die Arbeit für Sie erledigen.

git bisect ist ein Binärsuch-Werkzeug für Ihre Commit-Historie. Sie markieren einen Commit als schlecht und einen als gut. Git checkt den Mittelpunkt aus. Sie testen ihn, markieren ihn gut oder schlecht, und Git wiederholt das. In 8 Schritten kann es einen einzelnen Commit aus 256 isolieren. In 12 Schritten kann es die Nadel in 4.096 finden. Es ist das, was Git am nächsten an einen Debugger für die Historie selbst hat.

Warum manuelle Bisektion Ihre Zeit verschwendet

Entwickler bisektieren bereits manuell, ohne es zu merken. Sie checken einen älteren Commit aus, führen die Tests aus, und denken: „Immer noch kaputt, muss weiter zurück.” Dann checken sie einen noch älteren aus, und die Tests sind bestanden. Jetzt wissen Sie, dass der Bug irgendwo zwischen diesen beiden Punkten liegt. Also wählen Sie einen in der Mitte und verengen weiter.

Das ist binäre Suche. git bisect automatisiert die Buchführung, damit Sie nicht den Überblick verlieren, welche Commits Sie bereits getestet haben. Wichtiger noch: Es verhindert die faule Heuristik „es war wahrscheinlich in dem großen Refactor von Dienstag”, die Sie zwei Stunden lang in das falsche Kaninchenloch schickt.

Der echte Wert ist nicht Geschwindigkeit, auch wenn es schneller ist. Der echte Wert ist Korrektheit. Wenn Sie frustriert sind und um 18 Uhr einen Bug jagen, machen Sie Fehler. Sie vergessen, nach dem Checkout neu zu bauen. Sie testen den falschen Commit zweimal. Sie lesen ein Testergebnis falsch und markieren einen schlechten Commit als gut, was Ihren Suchraum zerstört. git bisect erzwingt Disziplin, die Sie nicht haben, wenn Sie genervt sind.

Wie die binäre Suche tatsächlich funktioniert

Git sucht nicht chronologisch. Es sucht topologisch, indem es den Commit-Graphen durchläuft, um den Mittelpunkt zwischen Ihrem bekannt-guten und bekannt-schlechten Commit zu finden. Das ist wichtig, wenn Ihre Historie Merges hat, denn der chronologische Mittelpunkt ist von beiden Endpunkten aus möglicherweise gar nicht erreichbar.

So sieht es unter der Haube aus. Sie starten die Bisektion und geben Grenzen an:

git bisect start
git bisect bad HEAD          # current commit is broken
git bisect good v2.1.0       # this release was fine

Git berechnet die Anzahl der Commits zwischen diesen beiden Punkten. Es checkt den genau in der Mitte aus und wartet, bis Sie ihn testen. Sie führen Ihre Reproduktion aus, sehen, ob der Bug vorhanden ist, und sagen Git:

git bisect bad   # this commit has the bug
git bisect good  # this commit is clean

Git verwirft die Hälfte des Suchraums und wiederholt das. Wenn nur noch ein Commit übrig bleibt, hört es auf und zeigt Ihnen den ersten schlechten Commit. Die Ausgabe enthält den Commit-Hash, Autor, Datum und Nachricht. Es gibt keine Mehrdeutigkeit. Dieser Commit hat den Bug eingeführt, Punkt.

Ein konkreter Durchlauf mit echten Befehlen

Nehmen wir an, Ihre integration tests haben heute Morgen angefangen zu failen. Sie wissen, dass sie im letzten getaggten Release, v1.4.0, noch bestanden haben. Hier ist die vollständige Sitzung:

# Start the session
git bisect start

# Mark the current HEAD as bad
git bisect bad HEAD

# Mark the last known good release
git bisect good v1.4.0

# Git checks out a midpoint commit automatically
# You run your reproduction script or test suite:
npm test -- --grep "checkout flow"

# Tests fail. Mark it bad.
git bisect bad

# Git checks out another midpoint. Run tests again.
npm test -- --grep "checkout flow"

# Tests pass. Mark it good.
git bisect good

# Repeat until Git tells you:
# "<commit-hash> is the first bad commit"

Am Ende lässt Git Sie auf dem schlechten Commit. Sie können ihn mit git show inspizieren, einen Fix erstellen und dann aufräumen:

git bisect reset

Das bringt Sie zurück auf den Branch, auf dem Sie waren, bevor Sie gestartet haben. Wenn Sie vergessen, reset auszuführen, bleiben Sie auf einem detached HEAD und wundern sich, warum Ihr nächster Commit nicht auf Ihrem Branch ist. Ich habe das gemacht. Es ist peinlich.

Automatisieren des gesamten Prozesses mit einem Skript

Die manuelle Version erfordert immer noch, dass Sie Tests ausführen und wiederholt good oder bad eintippen. Wenn Ihre Reproduktion ein einzelner Befehl ist, der bei Erfolg mit 0 und bei Fehler mit Non-Zero exitiert, können Sie das Ganze Git übergeben:

git bisect start
git bisect bad HEAD
git bisect good v1.4.0

# Hand over control to an automated script
git bisect run npm test -- --grep "checkout flow"

Git wird Commits checkouten, Ihren Befehl ausführen und Ergebnisse automatisch klassifizieren. Wenn es fertig ist, bekommen Sie die gleiche „first bad commit”-Ausgabe, ohne Ihre Tastatur zu berühren. Das ist der Punkt, an dem git bisect von nützlich zu unverzichtbar wird.

Ihr Skript muss keine Test-Suite sein. Es kann alles Ausführbare sein, das aussagekräftige Exit-Codes zurückgibt. Hier ist ein minimales Shell-Skript, das auf eine bestimmte Log-Nachricht prüft:

#!/bin/bash
# reproduce-bug.sh
# Exit 0 if the bug is NOT present (good)
# Exit 1 if the bug IS present (bad)

if curl -s http://localhost:3000/api/health | grep -q "database_timeout"; then
    exit 1  # bug is present
fi
exit 0  # bug is not present

Führen Sie es aus:

chmod +x reproduce-bug.sh
git bisect run ./reproduce-bug.sh

Der Exit-Code-Vertrag ist strikt. Exit 0 bedeutet „gut”, Exit 1 bis 124 bedeutet „schlecht”, und Exit 125 bedeutet „überspringe diesen Commit, er ist nicht testbar.” Exit 125 ist nützlich, wenn ein Commit nicht kompiliert oder der Server wegen einer unabhängigen Konfigurationsänderung nicht starten kann. Git überspringt diesen Commit und sucht drumherum.

Wo Bisektion zusammenbricht: die Gotchas

git bisect geht davon aus, dass Ihr Bug monoton ist. Sobald ein Commit ihn eingeführt hat, ist jeder Nachfahre-Commit ebenfalls schlecht. Wenn der Bug flackert, also über Commits hinweg erscheint und verschwindet, fällt die binäre Suche auseinander. Sie bekommen Unsinn-Ergebnisse oder Git beschwert sich, dass es keinen einzelnen Commit isolieren kann.

Nichtdeterministische Bugs sind die schlimmsten Übeltäter. Eine Race Condition, die 10 % der Zeit fehlschlägt, markiert gelegentlich einen schlechten Commit als gut und korrumpiert die Suche. Wenn Ihre Reproduktion flaky ist, beheben Sie die Flakiness zuerst. Oder führen Sie den Test in Ihrem Skript mehrmals aus und markieren Sie einen Commit nur dann als gut, wenn er jedes Mal besteht. Das ist langsamer, aber zuverlässiger.

Build-Artefakte sind eine weitere Falle. Wenn Sie von einem Commit, der eine Build-Tool-Version ändert, zu einem wechseln, der das nicht tut, stimmen Ihre veralteten node_modules oder kompilierten Binärdateien möglicherweise nicht mit dem ausgecheckten Code überein. Bereinigen und bauen Sie immer innerhalb Ihres Reproduktions-Skripts neu, wenn Ihr Projekt einen Build-Schritt hat.

#!/bin/bash
# safer-reproduce.sh
rm -rf node_modules dist
npm ci
npm run build
npm test -- --grep "checkout flow"

Das fügt pro Iteration ein paar Sekunden hinzu, aber es eliminiert eine ganze Kategorie von False Positives, bei denen der Bug tatsächlich in veralteten Artefakten steckt.

Was Bisektion nicht tut

git bisect findet den Commit. Es sagt Ihnen nicht, warum der Commit schlecht ist. Ein 5.000-Zeilen-Refactor, der vierzig Dateien berührt, kann der erste schlechte Commit sein, und Sie müssen trotzdem das Diff lesen, um zu verstehen, welche Zeile der Übeltäter ist. Bisektion verengt Ihren Scope von „irgendwo im letzten Monat” auf „irgendwo in diesem Diff”. Der Rest ist immer noch Ihre Aufgabe.

Es hilft auch nicht bei Bugs, die im Codebase existieren, aber nie von Tests erfasst wurden. Wenn Ihre Test-Suite bereits grün war, als der Bug eingeführt wurde, rettet Sie Bisektion nicht. Sie brauchen zuerst eine Reproduktion. Bisektion ist ein Such-Werkzeug, kein Erkennungs-Werkzeug.

Wann Sie zu Bisektion statt blame oder log greifen sollten

git blame ist großartig, wenn Sie bereits wissen, welche Datei den Bug enthält. git log --grep ist großartig, wenn Sie sich an etwas in der Commit-Nachricht erinnern. Bisektion ist für den Fall, dass Sie beides nicht wissen. Sie wissen nur, dass es bei Punkt A funktioniert hat und bei Punkt B fehlschlägt, und der Raum dazwischen ist zu groß, um ihn zu durchdenken.

Wenn Ihr Team auf jedem Commit CI ausführt, können Sie manchmal schneller bisektieren, indem Sie die CI-Historie lesen. Aber CI erkennt nur, was sie testet. Eine Performance-Regression, ein visueller Bug oder ein subtiler Logikfehler, den Ihre Tests verpassen, taucht nicht in CI-Logs auf. Bisektion funktioniert für jede Reproduktion, die Sie skripten können, unabhängig davon, was Ihre CI validiert.

Wie Sie Bisektion in Ihren Arbeitsablauf integrieren

Sie brauchen kein spezielles Setup. Die Befehle sind in Git eingebaut. Aber es gibt zwei Gewohnheiten, die Bisektion reibungsloser machen, wenn Sie sie tatsächlich brauchen.

Erstens: Taggen Sie Ihre Releases. Bisektion braucht einen bekannt-guten Commit, und Tags sind der einfachste Weg, einen zu liefern. Wenn Ihr letztes Release vor drei Wochen war und Sie wissen, dass es sauber war, ist git bisect good v1.4.0 ein einzeiliger Ankerpunkt.

Zweitens: Halten Sie Ihre Reproduktion minimal. Eine Reproduktion, die dreißig Sekunden läuft, ist für acht Iterationen erträglich. Eine Reproduktion, die zehn Minuten dauert, ist Folter. Bevor Sie bisektieren, verbringen Sie fünf Minuten damit, die Reproduktion auf den kleinstmöglichen Befehl zu verengen. Ihr zukünftiges Ich wird es Ihnen danken.

Wenn Sie fertig sind, führen Sie sofort git bisect reset aus. Ein detached HEAD mit uncommitted Changes ist eine hervorragende Möglichkeit, Arbeit zu verlieren. Ich habe das auf die harte Weise gelernt, damit Sie es nicht müssen.