Binärsuche statt endlosem Durchklicken
Wenn irgendwo zwischen hunderten Commits ein Bug eingeschlichen ist, kostet manuelles Durchklicken der Historie wertvolle Zeit. git bisect findet den schuldigen Commit per Binärsuche in wenigen Schritten, egal ob manuell getestet oder vollautomatisch per Testskript, und macht aus stundenlanger Fehlersuche eine Sache von Minuten.
Inhaltsverzeichnis
- 1. Warum lineares Durchklicken bei Regressionen scheitert
- 2. Die Binärsuche hinter git bisect: log2(n) statt n
- 3. Der manuelle Bisect-Workflow Schritt für Schritt
- 4. Eine vollständige Bisect-Session im Terminal
- 5. Automatisierung mit git bisect run und Exit-Codes
- 6. Ein robustes Testskript für bisect run schreiben
- 7. Flaky Tests und git bisect skip richtig einsetzen
- 8. Merge-Commits und --first-parent beim Bisecting
- 9. Manuelles Debugging vs. git bisect im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum lineares Durchklicken bei Regressionen scheitert
Ein Bug taucht auf, der vor zwei Wochen noch nicht da war. Die Historie zwischen dem letzten bekannten guten Zustand und heute umfasst 130 Commits von drei verschiedenen Entwicklern. Der naive Ansatz, jeden Commit einzeln auszuchecken und zu testen, bedeutet im schlechtesten Fall 130 Testläufe, im Schnitt 65. Bei einem Testlauf von fünf Minuten sind das über fünf Stunden reine Wartezeit, nur um den einen Commit zu finden, der die Regression eingeführt hat.
git bisect löst genau dieses Problem, indem es die Suche in eine Binärsuche über die Commit-Historie verwandelt. Statt sequenziell von vorne nach hinten zu testen, springt Git bei jedem Schritt in die Mitte des noch unbekannten Bereichs und lässt dich nur mitteilen, ob dieser eine Commit fehlerhaft ("bad") oder korrekt ("good") ist. Aus 130 möglichen Testläufen werden dadurch im Schnitt sieben, unabhängig davon, ob der Test manuell im Browser passiert oder ein automatisiertes Skript den Exit-Code liefert.
2. Die Binärsuche hinter git bisect: log2(n) statt n
Der mathematische Kern von git bisect ist simpel: Bei n Commits zwischen einem bekannten guten und einem bekannten schlechten Zustand benötigt eine Binärsuche im Schnitt log2(n) Schritte, um den Übergangspunkt zu finden. Bei 100 Commits ist log2(100) ungefähr 6,64, also rund sieben Testschritte. Bei 1000 Commits sind es etwa zehn Schritte, bei einer Million Commits gerade einmal zwanzig. Diese logarithmische Skalierung ist der Grund, warum bisect selbst in riesigen Repositories mit jahrelanger Historie praktikabel bleibt.
Intern verwaltet Git dafür zwei Referenzmengen: alle als "good" markierten Commits und alle als "bad" markierten Commits. Nach jeder Markierung berechnet Git die Commits, die topologisch zwischen der jüngsten guten und der ältesten schlechten Grenze liegen, und checkt automatisch den mittleren Commit dieser Menge aus. Diese Berechnung berücksichtigt auch verzweigte Historie korrekt, solange die Commit-Graph-Topologie eindeutig zwischen good und bad unterscheidbar bleibt. Ist der Graph durch mehrere unabhängige Merges komplexer, kann die Anzahl der Schritte leicht von der reinen log2(n)-Formel abweichen, bleibt aber in derselben Größenordnung.
3. Der manuelle Bisect-Workflow Schritt für Schritt
Der Workflow beginnt immer mit git bisect start, das eine Bisect-Session initialisiert und den aktuellen Branch merkt, um am Ende dorthin zurückkehren zu können. Danach markierst du den bekannten fehlerhaften Zustand, meist den aktuellen HEAD, mit git bisect bad, und einen bekannten funktionierenden Zustand in der Vergangenheit mit git bisect good <sha>, zum Beispiel den letzten Tag eines Releases, von dem sicher ist, dass der Bug dort noch nicht existierte.
Sobald beide Grenzen gesetzt sind, checkt Git automatisch den mittleren Commit aus und meldet, wie viele Revisionen ungefähr noch übrig sind und wie viele Schritte voraussichtlich folgen. Jetzt testest du diesen Zustand, sei es durch manuelles Ausprobieren im Browser, Ausführen einer Testsuite oder Reproduzieren des Bugs per Hand, und teilst Git das Ergebnis mit: git bisect good, wenn der Bug hier noch nicht auftritt, oder git bisect bad, wenn er auftritt. Git checkt daraufhin den nächsten Kandidaten aus, und der Zyklus wiederholt sich, bis Git meldet, welcher Commit als erster den Fehler eingeführt hat.
# Bisect-Session starten, aktueller Branch wird gemerkt
git bisect start
# Aktuellen HEAD als fehlerhaft markieren
git bisect bad
# Bekannten guten Commit als Referenzpunkt markieren
git bisect good v2.4.1
# Git checkt automatisch den mittleren Commit aus:
# Bisecting: 63 revisions left to test after this (roughly 6 steps)
# [a1b2c3d4] Merge branch 'feature/checkout-refactor'
# Nach dem Testen das Ergebnis mitteilen
git bisect good
# oder
git bisect bad
4. Eine vollständige Bisect-Session im Terminal
Um den Ablauf greifbar zu machen, hilft ein realistisches Beispiel: Zwischen Tag v3.1.0 und dem aktuellen main liegen 118 Commits, und irgendwo dazwischen hat sich ein Bug eingeschlichen, bei dem der Warenkorb-Rabatt nicht mehr korrekt berechnet wird. Die folgende Terminal-Ausgabe zeigt eine vollständige Session von Start bis zur Identifikation des schuldigen Commits, inklusive der von Git ausgegebenen Fortschrittsmeldungen nach jedem Schritt.
Bemerkenswert ist, wie schnell sich der verbleibende Suchraum bei jedem Schritt halbiert: von 118 auf etwa 59, dann 29, 14, 7, 3 und schließlich 1 Commit. Nach sieben Iterationen identifiziert Git den exakten Commit, der die Regression eingeführt hat, inklusive vollständiger Commit-Message und Autor, was die anschließende Ursachenanalyse erheblich beschleunigt.
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v3.1.0
Bisecting: 58 revisions left to test after this (roughly 6 steps)
[7f3e9a1] Refactor cart totals calculation service
$ npm test -- cart-discount.spec.js
# Tests schlagen fehl
$ git bisect bad
Bisecting: 29 revisions left to test after this (roughly 5 steps)
[c92d410] Add tiered discount support
$ npm test -- cart-discount.spec.js
# Tests laufen durch
$ git bisect good
Bisecting: 14 revisions left to test after this (roughly 4 steps)
[e04a882] Update discount rounding logic
$ npm test -- cart-discount.spec.js
# Tests schlagen fehl
$ git bisect bad
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[b551f3c] Introduce percentage-based coupon codes
$ npm test -- cart-discount.spec.js
# Tests laufen durch
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 1 step)
[9a03dd7] Cache cart totals per session
$ npm test -- cart-discount.spec.js
# Tests schlagen fehl
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4c81eef] Round discount before cache write instead of after
$ npm test -- cart-discount.spec.js
# Tests schlagen fehl
$ git bisect bad
4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9 is the first bad commit
commit 4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9
Author: M. Berger <m.berger@example.com>
Date: Tue Jun 30 14:12:03 2026 +0200
Round discount before cache write instead of after
$ git bisect reset
5. Automatisierung mit git bisect run und Exit-Codes
Manuelles Testen bei jedem Schritt ist bei reproduzierbaren Bugs unnötiger Aufwand. git bisect run <skript> automatisiert die komplette Session: Git checkt jeden Kandidaten-Commit aus, führt das angegebene Skript aus und interpretiert dessen Exit-Code, um selbstständig zu entscheiden, ob der Commit good oder bad ist. Die Konvention folgt dem üblichen Unix-Muster mit einer wichtigen Erweiterung: Exit-Code 0 bedeutet good, jeder Exit-Code zwischen 1 und 127 außer 125 bedeutet bad, und Exit-Code 125 hat eine Sonderbedeutung, nämlich "diesen Commit überspringen, weil er nicht sinnvoll testbar ist".
Diese 125-Konvention ist entscheidend, weil während einer Bisect-Session zwangsläufig auch Commits durchlaufen werden, die aus anderen Gründen als der gesuchten Regression kaputt sind, etwa weil sich zwischenzeitlich Build-Abhängigkeiten geändert haben oder eine Migration fehlt. Ein Exit-Code von 125 signalisiert Git, diesen Commit aus der Bewertung herauszunehmen und einen benachbarten Commit stattdessen zu testen, ohne die Binärsuche zu verfälschen. Exit-Codes über 127 werden von bisect run als fataler Fehler behandelt und brechen die Automatisierung sofort ab, da sie meist auf Probleme mit dem Testskript selbst statt mit dem getesteten Code hindeuten.
# Bisect-Session mit Grenzen starten
git bisect start
git bisect bad HEAD
git bisect good v3.1.0
# Kompletten Ablauf an ein Skript delegieren
git bisect run ./run-regression-test.sh
# Git testet jeden Kandidaten automatisch und meldet am Ende:
# 4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9 is the first bad commit
git bisect reset
6. Ein robustes Testskript für bisect run schreiben
Ein gutes Bisect-Testskript muss drei Dinge zuverlässig unterscheiden: Baut der aktuelle Commit überhaupt, reproduziert er den Bug, oder ist er aus unabhängigen Gründen nicht testbar. Fehlt diese Unterscheidung, liefert das Skript bei fehlgeschlagenen Builds fälschlich "bad" zurück, obwohl der eigentliche Fehler nichts mit der gesuchten Regression zu tun hat, und die Binärsuche konvergiert auf den falschen Commit.
Die folgende Vorlage zeigt das Muster: Zuerst wird geprüft, ob der Build überhaupt durchläuft, mit einem frühen Return über Exit-Code 125 bei Fehlschlag. Danach folgt der eigentliche Test, der über seinen regulären Exit-Code entscheidet. Wichtig ist außerdem, dass das Skript deterministisch und ohne Seiteneffekte auf den Repository-Zustand läuft, da es potenziell dutzende Male hintereinander ausgeführt wird.
#!/bin/bash
# run-regression-test.sh - Testskript für git bisect run
set -euo pipefail
# Abhängigkeiten für diesen Commit installieren, Fehler = nicht testbar
if ! npm ci --silent > /tmp/bisect-install.log 2>&1; then
echo "Build/Install fehlgeschlagen, Commit wird uebersprungen"
exit 125
fi
# Anwendung bauen, ebenfalls nicht testbar bei Fehlschlag
if ! npm run build --silent > /tmp/bisect-build.log 2>&1; then
echo "Build fehlgeschlagen, Commit wird uebersprungen"
exit 125
fi
# Eigentlichen Regressionstest ausfuehren
npm test -- cart-discount.spec.js --silent
# Exit-Code des Tests wird direkt durchgereicht:
# 0 = good, 1 = bad (durch set -e implizit)
7. Flaky Tests und git bisect skip richtig einsetzen
Flaky Tests, die mal grün und mal rot sind ohne inhaltliche Codeänderung, sind Gift für git bisect, weil sie die Grundannahme verletzen, dass derselbe Commit bei wiederholtem Testen immer dasselbe Ergebnis liefert. Ein fälschlich als "bad" markierter, eigentlich guter Commit kann die gesamte Binärsuche auf einen völlig falschen Bereich der Historie lenken. Die erste Verteidigungslinie ist daher, den Test bei Unsicherheit mehrfach laufen zu lassen, bevor eine Entscheidung getroffen wird, etwa durch eine Retry-Schleife im Testskript, die erst nach drei konsistenten Ergebnissen einen finalen Exit-Code liefert.
Wenn ein Commit grundsätzlich nicht zuverlässig testbar ist, etwa weil eine für den Test benötigte Funktion in diesem Zwischenstand noch gar nicht existiert, ist git bisect skip das richtige Werkzeug, manuell aufgerufen anstelle von good oder bad. Git wählt daraufhin einen benachbarten Commit als nächsten Kandidaten und versucht, die Bisect-Grenzen dennoch einzugrenzen. Bleiben zu viele Commits am Stück übersprungen, kann Git den schuldigen Commit nicht mehr eindeutig isolieren und meldet stattdessen einen Bereich möglicher Commits, was die manuelle Nacharbeit erhöht, aber immer noch weit besser ist als lineares Testen.
# Manuell: Commit ist nicht sinnvoll testbar, z.B. Build kaputt
# aus unabhaengigen Gruenden
git bisect skip
# Im Testskript: Exit-Code 125 hat dieselbe Wirkung wie skip
if grep -q "TODO: not implemented yet" src/discount/engine.js; then
exit 125
fi
# Mehrere Commits gleichzeitig als nicht testbar markieren
git bisect skip v3.2.0..v3.2.5
8. Merge-Commits und --first-parent beim Bisecting
In Repositories mit Feature-Branches und regelmäßigen Merges kann die Standard-Bisect-Strategie problematisch werden, weil Git bei der Berechnung des mittleren Commits auch Commits innerhalb eines Feature-Branches als eigenständige Kandidaten auschecken kann, obwohl diese im Kontext des Merges nie isoliert im Hauptzweig gelaufen sind. Das führt gelegentlich zu Testergebnissen, die im Gesamtverlauf von main so nie aufgetreten wären, etwa weil ein Zwischenstand des Feature-Branches einen unvollständigen, aber isoliert lauffähigen Zustand hatte.
Die Option git bisect start --first-parent löst dieses Problem, indem sie bei Merge-Commits ausschließlich dem ersten Elternteil folgt, also der linearen Historie des Zielbranches, und Commits innerhalb gemergter Feature-Branches komplett überspringt. Das Ergebnis ist gröber, es identifiziert im Zweifel den Merge-Commit selbst statt eines einzelnen Commits darin, aber es bleibt konsistent mit dem tatsächlich deploybaren Zustand von main zu jedem Zeitpunkt. In Workflows mit Squash-Merges ist dieses Problem ohnehin entschärft, da dort pro Feature nur ein einziger Commit im Zielbranch landet.
# Bisect nur entlang der linearen Historie des Zielbranches,
# Feature-Branch-Commits innerhalb von Merges werden uebersprungen
git bisect start --first-parent
git bisect bad HEAD
git bisect good v3.1.0
git bisect run ./run-regression-test.sh
# Ergebnis kann ein Merge-Commit sein statt eines Einzelcommits:
# a7c391f is the first bad commit
# Merge: 4d2a1b0 9f8e7c6
# Merge branch 'feature/tiered-discounts' into main
9. Manuelles Debugging vs. git bisect im Vergleich
Die folgende Übersicht stellt gängige, aber ineffiziente Debugging-Ansätze den empfohlenen bisect-basierten Alternativen gegenüber. Der Unterschied liegt fast immer in der Anzahl der nötigen Testläufe und in der Zuverlässigkeit des Ergebnisses.
| Situation | Ineffizienter Ansatz | Empfohlener bisect-Ansatz |
|---|---|---|
| Regression finden | Commit-Log manuell durchlesen und raten | git bisect mit good/bad-Grenzen |
| 100+ Commits testen | Commit für Commit linear durchtesten | Binärsuche, ca. 7 Schritte statt 100+ |
| Wiederholte Testläufe | Nach jedem Checkout manuell neu testen | git bisect run mit Testskript |
| Feature-Branch-Merges | Merge-Commits ignorieren, wild in Branches springen | git bisect --first-parent |
| Flaky Test schlaegt fehl | Fehlschlag pauschal als "bad" werten | git bisect skip / Exit-Code 125 |
In jeder Zeile ist der Unterschied strukturell derselbe: Der ineffiziente Ansatz behandelt die Commit-Historie linear oder ignoriert ihre Topologie, während git bisect gezielt die Struktur des Commit-Graphen nutzt, um die Anzahl nötiger Testläufe drastisch zu reduzieren und gleichzeitig ein reproduzierbares, dokumentiertes Ergebnis zu liefern.
Mironsoft
Magento- und Hyvä-Entwicklung mit sauberen Git-Workflows
Regressionen im Magento-Code schnell finden?
Wir richten für euer Magento- und Hyvä-Projekt automatisierte Testskripte und CI-Pipelines ein, mit denen sich Regressionen per git bisect run in Minuten statt Stunden isolieren lassen.
Git-Workflow-Audit
Branching-, Merge- und Bisect-taugliche Commit-Historie
Testautomatisierung
Reproduzierbare Testskripte für bisect run und CI/CD
Regressions-Support
Gezielte Ursachenanalyse bei akuten Produktionsfehlern
10. Zusammenfassung
git bisect löst ein Problem, das jedes wachsende Repository irgendwann trifft: eine Regression finden, deren Ursprung irgendwo in dutzenden oder hunderten Commits liegt. Statt linear zu testen, nutzt bisect eine Binärsuche und reduziert die nötigen Testläufe bei 100 Commits auf rund sieben, bei 1000 Commits auf rund zehn. Der manuelle Workflow aus git bisect start, git bisect bad und git bisect good <sha> genügt für gelegentliche Fehlersuche, während git bisect run mit einem Testskript und den Exit-Code-Konventionen 0 für good, ungleich 0 und ungleich 125 für bad, sowie 125 für skip die komplette Session ohne manuelles Eingreifen durchläuft.
Zwei Stolperfallen verdienen besondere Aufmerksamkeit: Flaky Tests können die Binärsuche in die falsche Richtung lenken, weshalb git bisect skip und mehrfach abgesicherte Testläufe im Skript entscheidend sind. Und Merge-lastige Historien profitieren von --first-parent, um konsistent mit dem tatsächlichen Zustand des Zielbranches zu bleiben. Am Ende jeder Session steht git bisect reset, das den Working Directory-Zustand vor Sessionstart wiederherstellt und die interne Bisect-Referenzliste zurücksetzt.
git bisect - Das Wichtigste auf einen Blick
Binärsuche statt linear
log2(n) statt n Testläufe: bei 100 Commits reichen etwa sieben Schritte, statt bis zu 100 einzeln zu testen.
Manueller Workflow
git bisect start, bisect bad, bisect good <sha>, dann iterativ good/bad markieren bis zum Ergebnis.
Automatisierung
git bisect run ./test.sh mit Exit-Code 0 (good), ungleich 0/125 (bad), 125 (skip).
Edge Cases
Flaky Tests mit bisect skip behandeln, Merges mit --first-parent, Session mit bisect reset beenden.