git bisect: как найти коммит, сломавший прод, за десяток шагов
Регрессия найдена, а виновный коммит неизвестен. Разбираем, как бинарный поиск git bisect сокращает перебор сотен коммитов до десяти-пятнадцати проверок в ручном и автоматическом режиме.

Регрессия, которая появилась неизвестно когда, обычно находится перебором: открыть git log, смотреть на даты, пытаться вспомнить, что менялось на прошлой неделе. На проекте с сотней коммитов между последним рабочим релизом и текущим состоянием такой перебор занимает часы. Git bisect решает ту же задачу бинарным поиском и обычно укладывается в десять-пятнадцать шагов даже на тысяче коммитов.
Почему перебор коммитов вручную не масштабируется
Линейный просмотр истории работает, пока коммитов немного и память разработчика свежая. На практике регрессию часто замечают через недели после того, как она попала в код, когда автор изменения уже не помнит деталей, а между «последний раз работало» и «сейчас не работает» могли накопиться сотни коммитов от нескольких человек. Перебирать их по одному линейно, это количество проверок, равное числу коммитов, бинарный поиск сокращает его до логарифма от этого числа.
Как работает бинарный поиск внутри git bisect
Идея та же, что в поиске элемента в отсортированном массиве: вместо перебора по одному коммиту, git bisect каждый раз берёт середину диапазона между известной «хорошей» и известной «плохой» точкой, просит проверить её и по ответу сужает диапазон вдвое. На тысячу коммитов между good и bad потребуется не больше десяти проверок, чтобы найти точный коммит, где поведение изменилось.
| Коммитов в диапазоне | Проверок вручную | Проверок через bisect |
|---|---|---|
| 100 | до 100 | около 7 |
| 1000 | до 1000 | около 10 |
| 10000 | до 10000 | около 14 |
Ручной режим: start, bad, good и reset
Ручной режим подходит, когда проверку «работает или не работает» нельзя автоматизировать, например баг виден только глазами в интерфейсе.
git bisect start
git bisect bad HEAD
git bisect good v2.3.0После этих трёх команд git переключит рабочую копию на коммит из середины диапазона. Нужно собрать проект, проверить баг и сообщить результат одной из двух команд: git bisect good, если на этом коммите баг не воспроизводится, или git bisect bad, если воспроизводится. Git снова переключится на новую середину уже суженного диапазона, и так до тех пор, пока не останется один коммит, виновный в регрессии. Завершить сессию и вернуть рабочую копию в исходное состояние: git bisect reset.
В процессе удобно держать перед глазами команду git bisect log, она печатает все уже сделанные шаги с их результатами, это особенно полезно, если сессию пришлось прервать и вернуться к ней позже, например на следующий день. Вывод этой команды, это одновременно и готовый сценарий: его можно сохранить в файл и прогнать через git bisect replay, чтобы воспроизвести тот же поиск на другой машине без повторного ручного прохождения всех шагов.
Автоматический режим: bisect run со скриптом проверки
Если проверку можно выразить как команду с кодом выхода (тест, сборка, запрос с проверкой ответа), весь процесс можно автоматизировать одной командой.
git bisect start HEAD v2.3.0
git bisect run ./scripts/check-regression.shСкрипт check-regression.sh должен возвращать код выхода 0, если коммит хороший, и любой ненулевой код, кроме 125, он зарезервирован для «не могу проверить этот коммит», если коммит плохой. Git bisect сам выполнит весь бинарный поиск без участия человека и остановится на найденном коммите.
#!/usr/bin/env bash
set -e
npm ci --silent
npm run build --silent
curl -sf http://localhost:3000/health | grep -q 'status":"ok'Такой скрипт собирает проект и проверяет, что эндпоинт здоровья отдаёт ожидаемый ответ. Если сборка или проверка не пройдёт, скрипт завершится с ненулевым кодом, и git bisect run отметит коммит как плохой автоматически.
Частые ошибки: шумные коммиты и флаки в проверке
Два источника ложных результатов портят бинарный поиск чаще всего. Первый: в диапазоне между good и bad есть коммиты, которые вообще не собираются по причинам, не связанным с искомым багом, например сломанная зависимость или временный форматирующий коммит. Для таких случаев и существует код выхода 125, он говорит bisect пропустить этот коммит и попробовать соседний, не разрушая общий диапазон поиска. Второй источник: нестабильные тесты, которые иногда проходят, а иногда нет, независимо от кода. Если проверка не детерминирована, bisect run будет указывать на случайный коммит, и доверять результату нельзя, тест нужно сначала стабилизировать.
- Если точно известно, что коммит не собирается по независимой причине: git bisect skip в ручном режиме, выход 125 в скрипте для автоматического
- Перед запуском bisect run проверьте скрипт проверки на заведомо хорошем и заведомо плохом коммите, чтобы убедиться, что он действительно различает их
- Если тест нестабилен, прогоните проверку несколько раз подряд на одном коммите перед тем, как доверять ей в bisect
Что делать после находки коммита
Git bisect находит коммит, не причину. Дальше обычный процесс: просмотр полного диффа найденного коммита часто сразу показывает подозрительную строку. Если коммит крупный и трогает много файлов, дальше можно сузить поиск вручную внутри одного файла или написать более точечный тест. После того как причина понятна, фикс делается обычным коммитом сверху текущей ветки, bisect не предполагает отмену найденного коммита целиком, разве что это и есть правильное исправление.
Bisect на тестах, а не только на ручной проверке в браузере
Самый надёжный вариант автоматической проверки, это уже существующий тест, который воспроизводит баг. Если такого теста нет, часто разумнее сначала написать минимальный тест, который падает на текущем HEAD и проходит на старом good-коммите, а потом запускать под ним bisect run, чем вручную собирать и проверять десять промежуточных версий проекта в браузере. Написание такого теста занимает время один раз, а дальше бинарный поиск отрабатывает полностью без участия человека, и тест остаётся в репозитории как защита от повторения той же регрессии в будущем.
Для регрессий в производительности, а не в логичном поведении, проверкой может быть сравнение числа с порогом: например, время ответа эндпоинта не должно превышать заданную границу. Такой скрипт для bisect run просто измеряет метрику и возвращает ненулевой код, если она хуже порога, это превращает нечёткое «стало как-то медленнее» в точный бинарный ответ, на котором бинарный поиск отработает так же предсказуемо, как на любом другом тесте.
Стоит заранее учитывать погрешность измерений: однократный замер времени ответа колеблется от запуска к запуску даже без изменений в коде, и скрипт проверки, который считает единичный замер окончательным ответом, может случайно отметить хороший коммит как плохой из-за шума окружения. Надёжнее усреднить несколько замеров подряд внутри самого скрипта и сравнивать с порогом уже среднее значение, это стоит нескольких лишних секунд на каждый шаг бинарного поиска, но заметно снижает риск найти не ту регрессию.
Отдельно стоит сказать про коммиты слияния веток в истории: по умолчанию git bisect проверяет и их как обычные точки истории, что иногда приводит к неинтуитивному результату, когда найденным «виновником» оказывается сам коммит слияния, а не конкретное изменение внутри влитой ветки. Флаг --first-parent у команды git bisect start заставляет поиск двигаться только по основной линии разработки, пропуская внутренние коммиты веток, это обычно ближе к тому, что реально нужно понять: после какого слияния в main появилась проблема.
Частые вопросы
Можно ли использовать git bisect, если регрессия в одном из нескольких модулей монорепозитория?
Да, скрипт для bisect run просто должен проверять именно тот модуль, который сломался, остальной код в диапазоне коммитов не важен для результата. Главное, чтобы проверка была детерминированной и быстрой, иначе бинарный поиск займёт больше времени, чем сэкономит.
Что делать, если на части коммитов в диапазоне проект физически не собирается из-за отдельной несвязанной проблемы?
В ручном режиме используйте git bisect skip, в автоматическом верните из скрипта код выхода 125. Bisect пропустит этот коммит и попробует соседний, сохранив корректность бинарного поиска, вместо того чтобы ошибочно засчитать несобираемый коммит как плохой.
Чем git bisect отличается от простого просмотра истории с ручным разбором диффов?
История показывает список изменений, но не помогает определить, какой именно коммит изменил поведение, это требует проверки каждого кандидата. Git bisect автоматизирует именно стратегию выбора следующего коммита для проверки, всегда середину диапазона, сокращая число проверок с линейного до логарифмического.



