12k
All articles

Mit git bisect einen fehlerhaften Commit aufspüren

Mit git bisect den Commit finden, der eine Regression verursacht hat. Zuverlässige Referenzen wählen, Tests automatisieren, flüchtige Ergebnisse behandeln und den Verursacher überprüfen.

OpenReplay Team
OpenReplay Team
Mit git bisect einen fehlerhaften Commit aufspüren

git bisect findet den Commit, der eine Regression verursacht hat, per binärer Suche. Sie markieren einen Commit, von dem Sie wissen, dass er funktioniert, und einen, von dem Sie wissen, dass er fehlerhaft ist. Jeder Test halbiert den verbleibenden Bereich, sodass bei 300 Commits nur etwa acht bis neun Prüfungen nötig sind.

Letzten Monat hat das Feature noch funktioniert. Seitdem sind 300 Commits auf main gelandet, niemand erinnert sich, den Warenkorb-Code angefasst zu haben, und jeden Diff durchzulesen würde den ganzen Nachmittag dauern.

5 Git-Befehle jenseits von Commit und Push stellt start, good, bad, run, skip und reset vor. Dieser Artikel geht einen Schritt weiter. Er zeigt, wie Sie einen guten Commit wählen, dem Sie vertrauen können, und wie Sie ein git bisect run-Skript schreiben, dessen Exit-Codes Git nicht in die Irre führen können. Außerdem erfahren Sie, was zu tun ist, wenn ein Flaky Test oder eine falsche Markierung die Suche auf die falsche Fährte lenkt.

Die wichtigsten Erkenntnisse

  • Jede Verdopplung des Bisect-Bereichs kostet nur eine zusätzliche Prüfung. Die eigentlichen Kosten eines weit zurückliegenden guten Commits sind alte Commits, die sich nicht mehr bauen lassen und übersprungen werden müssen.
  • Bei git bisect run markiert Exit-Code 0 einen Commit als gut und 125 überspringt ihn. Jeder andere Code von 1 bis 127 markiert ihn als schlecht, und 128 oder höher bricht das Bisecting ab.
  • Bei sporadisch auftretenden Fehlern markieren Sie einen Commit als schlecht, wenn auch nur einer von mehreren Durchläufen fehlschlägt. Als gut gilt er nur, wenn alle Durchläufe erfolgreich sind.
  • Eine falsche Markierung erfordert keinen Neustart: Sichern Sie git bisect log, löschen Sie die falsche Zeile und alles danach und führen Sie dann git bisect reset und git bisect replay aus.
  • Bestätigen Sie ein Ergebnis, indem Sie den genannten Commit und seinen Parent testen. Der Parent sollte den Test bestehen, der genannte Commit sollte fehlschlagen.

Die manuelle git-bisect-Schleife

Die manuelle Schleife richten Sie mit drei Befehlen ein: zuerst git bisect start, dann git bisect bad auf einem fehlerhaften Commit und schließlich git bisect good <ref> auf einem funktionierenden. Danach testen und markieren Sie jeden Commit, den Git auscheckt, bis Git den Verursacher benennt. Das Kapitel zum Debugging mit Git im Pro-Git-Buch beschreibt denselben Ablauf. Falls Git noch neu für Sie ist, finden Sie in 10 Git-Befehle, die jeder Entwickler kennen sollte die Grundlagen für den Alltag.

git bisect start
git bisect bad              # HEAD has the bug
git bisect good v4.12.0     # last release known to work
Bisecting: 149 revisions left to test after this (roughly 7 steps)
[9e41c07b2d85a3f16c0e7b94d2a58f3e1c6b0d27] Refactor cart line-item formatter

Git hat den Commit in der Mitte des Bereichs ausgecheckt. Führen Sie Ihren Test aus und melden Sie das Ergebnis:

git bisect bad
Bisecting: 74 revisions left to test after this (roughly 6 steps)
[2b7f0d9a4c16e83b5f2a07d9c4e1b68a3f05c9d1] Update currency util types

Testen und markieren Sie weiter. Wenn nur noch ein Kandidat übrig ist, gibt Git das Ergebnis aus (SHAs und Namen unten sind Platzhalter):

3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92 is the first bad commit
commit 3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92
Author: Example Dev <dev@example.com>
Date:   <date>

    Round line totals before applying discount

 src/cart/total.ts | 4 ++--

Wie wählen Sie den guten Commit aus?

Der beste gute Commit für git bisect ist das jüngste Release-Tag, das nachweislich ohne den Bug ausgeliefert wurde. Testen Sie es, bevor Sie es markieren. Eine als gut markierte Referenz, die in Wahrheit fehlerhaft ist, liefert trotzdem ein scheinbar eindeutiges Ergebnis. Meist ist das ein Commit kurz nach Ihrer guten Referenz, und dieses Ergebnis ist falsch.

Testen Sie das Tag mit derselben Prüfung, die auch Bisect verwenden wird (Vitest dient hier nur als Beispiel-Runner):

git switch --detach v4.12.0
npm ci && npx vitest run src/cart/total.test.ts   # must pass
git switch -

Verkleinern Sie den Bereich nicht auf Kosten der Gewissheit. Jede Verdopplung kostet nur eine zusätzliche Prüfung: etwa 8 bis 9 Prüfungen bei 300 Commits, 9 bis 10 bei 600 und 11 bis 12 bei 2.400. Weit zurückzugehen hat jedoch einen anderen Preis. Alte Commits benötigen möglicherweise eine ältere Node-Version, eine inzwischen aus der Registry entfernte Dependency oder ein anderes Lockfile-Format. Jeder Commit, der sich nicht bauen lässt, muss übersprungen werden, und eine lange Strecke übersprungener Commits kann den Verursacher verbergen.

Session-Replays zeigen, wann eine Regression zum ersten Mal echte Nutzer erreicht hat. Damit lässt sich der gute Commit auf das Release eingrenzen, das unmittelbar davor deployt wurde.

Wie automatisiert git bisect run die Suche?

git bisect run <script> führt Ihr Skript auf jedem Kandidaten-Commit aus und wertet dessen Exit-Code als Urteil. Die git-bisect-Dokumentation definiert die Zuordnung:

Exit-CodeBedeutung
0Gut
1–127, außer 125Schlecht
125Überspringen (nicht testbar)
128 und höherBisect abbrechen

Legen Sie das Skript außerhalb des Repositorys ab. So kann das Auschecken älterer Commits es nicht verändern, und Aufräumbefehle wie git clean -fdx können es nicht löschen:

cat > ../bisect-test.sh <<'EOF'
#!/usr/bin/env bash
npm ci --silent        || exit 125   # can't install: skip
npm run build --silent || exit 125   # can't build: skip
npx vitest run src/cart/total.test.ts || exit 1
EOF
chmod +x ../bisect-test.sh
git bisect run ../bisect-test.sh

Schlägt die Installation oder der Build fehl, endet das Skript mit 125. Ein davon unabhängiger Defekt wird also übersprungen, statt als schlecht gewertet zu werden. Das abschließende || exit 1 bildet jeden Testfehler auf 1 ab. Ein Test-Runner, der zufällig 125 oder 128+ zurückgibt, kann so nicht versehentlich einen Commit überspringen oder den Lauf abbrechen.

Die Exit-Codes 126 (nicht ausführbar) und 127 (Befehl nicht gefunden) zählen normalerweise als schlecht. Ein falscher Skriptpfad oder ein fehlendes Execute-Bit könnte also jeden Commit als schlecht markieren. Die Release Notes zu Git 2.36 beschreiben die entsprechende Schutzmaßnahme: Git versucht, ein nicht ausführbares Skript zu erkennen, und bricht frühzeitig ab, statt einen Verursacher zu benennen. Wenn git bisect run vorzeitig stoppt, prüfen Sie zuerst Skriptpfad und Berechtigungen, bevor Sie sich den Code ansehen.

Typische Stolpersteine: Skip, Reset und ein unsauberer Working Tree

Die meisten Unterbrechungen haben drei Ursachen: ein Commit, den Sie nicht testen können, eine Session, die Sie beenden müssen, oder lokale Änderungen, die im Weg sind.

git bisect skip

git bisect skip legt den aktuellen Commit beiseite, und Git wechselt zu einem benachbarten Commit. Liegt der Verursacher am Ende innerhalb einer Reihe übersprungener Commits, listet Git alle Kandidaten auf, statt einen einzelnen zu benennen.

git bisect reset              # back to the branch you started on
git bisect reset 3c9d2e7      # end the session on a specific commit

Committen oder stashen Sie lokale Änderungen vor git bisect start. Mit git bisect start -- src/cart/ können Sie die Suche auf bestimmte Pfade beschränken. git bisect visualize öffnet gitk mit den verbleibenden Verdächtigen. Findet Git keine grafische Desktop-Session, wird stattdessen git log verwendet.

Wie gehen Sie mit Flaky Tests und falschen Markierungen um?

Um einen sporadisch auftretenden Fehler per Bisect einzugrenzen, führen Sie den Test auf jedem Commit mehrmals aus. Markieren Sie den Commit als schlecht, wenn auch nur ein Durchlauf fehlschlägt, und nur dann als gut, wenn alle Durchläufe bestehen. Der Grund: Eine einzige falsche Markierung lenkt die Suche in die falsche Hälfte, und Git meldet trotzdem einen ersten fehlerhaften Commit. Ein tatsächlich fehlerhafter Commit kann zufällig bestehen, ein guter Commit sollte bei diesem Test jedoch nie fehlschlagen.

#!/usr/bin/env bash
npm ci --silent        || exit 125
npm run build --silent || exit 125
for i in $(seq 1 10); do
  npx vitest run src/cart/total.test.ts || exit 1   # any failure = bad
done
exit 0                                              # all passed = good

Ein hypothetisches Rechenbeispiel: Schlägt ein fehlerhafter Commit in 20 % der Fälle fehl, liegt die Wahrscheinlichkeit für zehn zufällig fehlerfreie Durchläufe bei 0,8¹⁰ ≈ 0,11. Mehr Durchläufe verringern dieses Risiko. Mehrdeutige Commits zu überspringen löst das Problem nicht: Das unzuverlässige Ergebnis bleibt bestehen, und die finale Antwort wird lediglich unschärfer. Diese Regel setzt voraus, dass die Instabilität neu ist. War der Test schon vor der Regression flaky, stabilisieren Sie ihn zuerst oder schreiben Sie eine gezieltere Prüfung. Andernfalls markiert er gute Commits als schlecht.

Wenn Sie bereits falsch markiert haben, können Sie das korrigieren, ohne neu anzufangen:

git bisect log > ../bisect.log
# edit ../bisect.log
git bisect reset
git bisect replay ../bisect.log

Das gespeicherte Log enthält zusätzlich Kommentarzeilen. Hier sind nur die Befehlszeilen dargestellt. Vor der Bearbeitung:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect good 9e41...    <- wrong: this commit was flaky
git bisect bad 2b7f...

Löschen Sie beim Bearbeiten die falsche Zeile und alles, was danach folgt. Das Ergebnis:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...

git bisect replay stellt die Session bis zur letzten korrekten Markierung wieder her, und Sie machen von dort aus weiter.

Wie bestätigen Sie das Ergebnis?

Betrachten Sie den Commit, den git bisect benennt, als Hinweis, bis Sie ihn überprüft haben. Lesen Sie den Diff mit git show 3c9d2e7 und testen Sie dann den Parent des Commits sowie den Commit selbst:

git switch --detach 3c9d2e7^   # parent: test should pass
git switch --detach 3c9d2e7    # culprit: test should fail

Besteht der Parent den Test und schlägt der Commit fehl, haben Sie die Regression gefunden. Anschließend können Sie mit git blame nachvollziehen, warum sich die umliegenden Zeilen geändert haben, und mit git history Commits korrigieren, die noch nicht gepusht wurden. Bei der nächsten Regression starten Sie direkt mit einem getesteten Release-Tag und einem Skript, das den Test auf jedem Commit mehrmals ausführt.

Häufig gestellte Fragen (FAQs)

Kann git bisect statt des Commits, der einen Bug eingeführt hat, auch den Commit finden, der ihn behoben hat?

Ja. Verwenden Sie die Begriffe old und new statt good und bad. Führen Sie git bisect start aus und markieren Sie einen behobenen Commit mit git bisect new sowie einen älteren fehlerhaften Commit mit git bisect old. Git meldet dann den ersten neuen Commit, also den Fix. Für aussagekräftigere Bezeichnungen führen Sie git bisect start --term-old broken --term-new fixed aus und markieren Commits anschließend mit git bisect fixed und git bisect broken.

Wie geht git bisect mit Merge-Commits aus Feature-Branches um?

Standardmäßig testet git bisect auch Commits innerhalb gemergter Branches, einschließlich Work-in-Progress-Commits, die sich möglicherweise nicht bauen lassen. Mit git bisect start --first-parent, eingeführt in Git 2.29, bleibt die Suche auf der Hauptlinie der Historie. Auf main stoppt sie beim Merge, der den Bug eingebracht hat, und testet nie die einzelnen Commits des Branches. Ein zweites Bisect über diesen Branch findet dann den exakten Commit.

Was passiert, wenn der gute Commit kein Vorfahre des schlechten Commits ist?

Git checkt eine Merge-Base der beiden Commits aus und fordert Sie auf, diese zuerst zu testen. Ist die Merge-Base gut, läuft das Bisecting normal über die Commits zwischen ihr und dem schlechten Commit weiter. Ist die Merge-Base bereits fehlerhaft, bricht Git ab, statt zu suchen. Der gute Commit liegt dann auf einer separaten Linie der Historie, auf der der Bug nicht existiert oder behoben wurde.

Was ist der Unterschied zwischen git bisect und git blame?

git blame zeigt Zeile für Zeile, welcher Commit eine Datei zuletzt geändert hat. Es beantwortet also nur, wer eine bestimmte Zeile zuletzt bearbeitet hat. git bisect testet das Verhalten über mehrere Commits hinweg. Damit findet es eine Regression auch dann, wenn die Ursache ein Dependency-Update, eine Konfigurationsänderung oder eine andere Datei als die mit dem Symptom ist. Nutzen Sie bisect, um den Commit zu finden, und anschließend blame, um die von ihm geänderten Zeilen nachzuverfolgen.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.