git bisect: поиск сломавшего коммита бинарным поиском
Как за несколько шагов найти коммит, в котором что-то сломалось, даже если их между «было хорошо» и «стало плохо» тысячи.
git bisect — встроенный в Git бинарный поиск по истории: вы отмечаете один «хороший» и один «плохой» коммит, а Git сам расставляет вас по середине диапазона и за
log₂Nпроверок находит коммит, который внёс регрессию.
Зачем это нужно на практике
Знакомая ситуация: вчера тесты были зелёными, сегодня один из них падает, а между этими состояниями — 200 коммитов от пяти человек. Читать diff каждого вручную бессмысленно. Откатываться «на глаз» — лотерея. git bisect превращает поиск в строгий алгоритм: вместо 200 проверок вам понадобится примерно log₂200 ≈ 8. Это та же идея, что и поиск числа в отсортированном массиве — каждый шаг вдвое сокращает зону подозрения.
Ключевое условие: «плохое» свойство должно быть монотонным. То есть существует момент, после которого баг есть всегда, а до него его нет. Если баг то появляется, то исчезает (флапает), бинарный поиск даст неверный результат — об этом ниже.
Базовый ручной сценарий
Сессия начинается командой git bisect start. Дальше вы сообщаете Git два известных факта: текущее состояние плохое, а какой-то старый коммит (тег релиза, вчерашний коммит) — хороший.
git bisect start
git bisect bad # текущий HEAD сломан
git bisect good v1.4.0 # на этом теге всё работало
# Git: Bisecting: 97 revisions left to test after this (roughly 7 steps)
# и переключает рабочее дерево на коммит-середину
Теперь Git сам сделал checkout на коммит посередине диапазона. Ваша задача — проверить именно это состояние (собрать, запустить тест, ткнуть в приложении руками) и вынести вердикт одной командой:
git bisect good # в этой ревизии бага ещё нет
# ИЛИ
git bisect bad # в этой ревизии баг уже есть
После каждого вердикта Git снова делит оставшийся диапазон пополам и переключается на новую середину. Через несколько итераций он печатает виновника:
# 3f9a1c2 is the first bad commit
# Author: ...
# commit message ...
# а ниже — список изменённых файлов
Обязательно завершите сессию, чтобы вернуть HEAD на исходную ветку:
git bisect reset # вернуться туда, где вы были до старта
Автоматизация через bisect run
Самое мощное — git bisect run. Вместо ручного вердикта вы даёте Git скрипт или команду; Git гоняет её на каждой ревизии и читает код возврата: 0 — хорошо, любой ненулевой (кроме 125) — плохо. Весь поиск проходит без вашего участия.
git bisect start
git bisect bad HEAD
git bisect good v1.4.0
git bisect run pytest tests/test_login.py -q
Если проверка сложнее одной команды, оформите её скриптом и верните нужные коды:
#!/usr/bin/env bash
# check.sh — собрать и проверить ровно один симптом регрессии
make build || exit 125 # 125 = «ревизию не собрать, пропусти её»
if ./app --selftest | grep -q "checksum mismatch"; then
exit 1 # симптом есть -> bad
else
exit 0 # симптома нет -> good
fi
chmod +x check.sh
git bisect start HEAD v1.4.0 # порядок: bad good одной строкой
git bisect run ./check.sh
Код 125 — особый: он говорит «эту ревизию невозможно протестировать, исключи её из выборки» (например, код тут не компилируется по другой причине). Git пропустит коммит и возьмёт соседний, не засчитав его ни плохим, ни хорошим.
Как это работает под капотом
Git строит граф предков между «good» и «bad» и на каждом шаге выбирает коммит, максимально близкий к середине по числу достижимых ревизий, а не по дате. Поэтому bisect корректно работает даже с ветвлениями и слияниями: он считает топологию, а не календарь. Рабочее дерево физически переключается на выбранный коммит (detached HEAD) — именно поэтому важно проверять собранный артефакт, а не исходник «на глаз». Состояние сессии Git хранит в файлах внутри .git/ (например, .git/BISECT_LOG), и его можно посмотреть или перезапустить:
git bisect log # полный протокол вердиктов
git bisect log > bisect.txt # сохранить, чтобы повторить позже
git bisect replay bisect.txt # воспроизвести сессию из лога
Если по ходу вы ошиблись с вердиктом, не нужно начинать заново — отмотайте сессию:
git bisect skip # пропустить текущую (нестабильную) ревизию
git bisect good~3 # синтаксис не сработает; используйте replay/log
Частые ошибки
- Перепутаны good и bad. Если отметить сломанное как «good», Git уведёт поиск не туда и найдёт «первый хороший» вместо «первого плохого». Запомните:
bad— то, что болит сейчас;good— заведомо здоровое прошлое. - Флапающий тест. Если симптом непостоянный, проверяйте признак, который воспроизводится стабильно, или прогоняйте тест несколько раз в скрипте и решайте по большинству. Для подозрительных ревизий —
git bisect skip. - Грязное рабочее дерево. Перед
startзакоммитьте или спрячьте изменения:bisectпереключает дерево и упрётся в незакоммиченные правки. - Забыли
reset. Без него вы останетесь вdetached HEADна старом коммите и потом удивитесь «пропавшим» последним изменениям. - Сборка тоже сломана. Если часть диапазона не компилируется по постороннней причине, используйте код
125в скрипте, иначе Git примет ошибку сборки за искомый баг.
Итоги
git bisect start / bad / goodзапускают бинарный поиск; Git сам ставит вас на середину диапазона.- Поиск занимает
~log₂Nпроверок — на тысячах коммитов это меньше десятка шагов. git bisect run <команда>автоматизирует всё: код0— good, ненулевой — bad,125— пропустить ревизию.- Свойство «сломано» должно быть монотонным; флапающие симптомы лечит
skipили повтор теста. - Всегда завершайте сессию
git bisect reset, чтобы вернутьHEADна ветку.