Прямой ответ
«Работало в прошлом релизе, теперь сломано» — git bisect находит виновный коммит бинарным поиском: 1000 коммитов между good и bad — ~10 шагов (log₂), каждый шаг — checkout середины и проверка «сломано ли»; интерактивно: git bisect start; git bisect bad HEAD; git bisect good v1.4.0 — git чекаутит середину — проверяете — git bisect good/bad — через 10 итераций виновник назван (abc1234 is the first bad commit — с автором/сообщением/diff — «кто и что сломал» за 15 минут против дней ручного перебора). Автоматизация (главная сила): git bisect run ./check.sh — скрипт возвращает 0 (good) / 1–127 кроме 125 (bad) / 125 (skip — коммит непроверяем, не собирается) — bisect проходит всю историю сам (ночной прогон на 5000 коммитов — утром виновник в логе); скрипт проверки — тест, воспроизводящий баг (pytest-тест «до/после» — фикстуры — идеальная машина-оракул), curl-проверка эндпоинта, скрипт «собери и прогони сценарий» — всё, что различает good/bad автоматически. Требования к успеху: (1) воспроизводимость бага (плавающий баг — bisect бессилен, пока не станет детерминированным — сначала стабилизировать: seed рандома/фиксированное время/изоляция сети — тест-гигиена); (2) быстрая проверка (полный набор 40 минут × 10 шагов — полдня; целевой тест 30 с — 5 минут на весь bisect — выделение минимального теста — часть искусства); (3) сборка на каждом коммите (промежуточные коммиты с незакоммиченными зависимостями/миграциями — skip или чинить проверку — «половина истории не запускается» — цена неразвитой CI-культуры — trunk-based с зелёной историей делает bisect тривиальным); (4) чистое рабочее дерево (bisect чекаутит коммиты — незакоммиченная работа пострадает — stash/отдельный worktree — git bisect в связанном worktree — основная работа не прерывается — продвинутый, но простой приём). Побочная ценность: найденный коммит — готовый контекст для фикса (diff виновника сужает задачу в сотни раз) и материал для разбора («почему пропустило ревью/тесты» — ревью-процесс).
Интерактивный bisect: процедура и тонкости
Полный цикл: git bisect start → git bisect bad (текущий HEAD сломан — или bad <sha>) → git bisect good v1.4.0 (последний заведомо рабочий — тег/релиз/дата — чем точнее граница, тем меньше шагов) → git сообщает «Bisecting: N revisions left to test after this (roughly M steps)» и чекаутит середину (detached HEAD — нормально — работа продолжается командами good/bad) → проверка (тест/ручной сценарий) → git bisect good|bad → повтор до «... is the first bad commit» (полный show виновника) → git bisect reset (возврат на исходную ветку — обязательно — забыть reset — остаться в detached HEAD — классика). Тонкости: промежуточные состояния — git bisect skip (коммит не проверить — не собирается/нужна инфраструктура которой нет — git выберет соседний; skip v1.4.1..v1.4.3 — диапазон), несколько bad-симптомов одним прогоном — не смешивать (два разных бага — два bisect — «иногда одно, иногда другое» в одной проверке — результаты каша), first bad commit ≠ единственный виновник (регрессия могла быть внесена серией — bisect находит первую — остальное покажет diff/контекст), границы good/bad перепутаны (good на самом деле bad — bisect сойдётся «не туда» — проверка границ первыми двумя шагами — «точно ли v1.4.0 хорош» — минута сомнения экономит час), визуализация — git bisect log (история шагов — воспроизведение git bisect replay log.txt — поделиться/повторить), git bisect visualize/git log --oneline HEAD ^good — что осталось. Merge-коммиты: bisect проходит по первому родителю (слияния фич-веток — виновник «внутри» ветки будет найден как первый плохой на main — стратегии слияния — squash-мержи делают историю линейной — bisect ещё проще).
Автоматизация: bisect run и скрипт-оракул
git bisect run <script> — полный автопилот; контракт скрипта: exit 0 — good (проверка прошла — бага нет), 1–124/126/127 — bad (баг воспроизведён), 125 — skip (промежуточное состояние непроверяемо — не собралось/миграция несовместима — зарезервированный код, не путать с «тест упал» — упавшая сборка ≠ найденный баг), 128+ — авария (bisect остановится). Шаблон скрипта-оракула (pytest-регресс): #!/bin/sh; python -m venv /tmp/bisect-env --clear || exit 125; . /tmp/bisect-env/bin/activate; pip install -q -e . || exit 125; pytest tests/test_regress_1234.py -x -q || exit 1; exit 0 — нюансы: установка окружения на каждом шаге (зависимости менялись между коммитами — venv --clear — медленно, но корректно; неизменные зависимости — переиспользовать env — ×5 быстрее), -x (первый провал — выход — скорость), || exit 125 на установочных шагах (не собралось — skip, не bad — критично для чистоты результата), таймаут на проверку (timeout 300 pytest ... — зависший тест = bad навсегда — ложный результат — timeout даёт 124 → bad? нет — лучше обработать явно: timeout → exit 125 — зависание не диагноз). Продвинутые сценарии: бинарники/фронтенд — скрипт «собери (npm build) + прогони playwright-тест» (e2e — медленнее, но автоматизировано); производительностные регрессии — скрипт-бенчмарк с порогом («p95 > 200 мс → bad» — шум замеров требует повторов — медиана трёх прогонов — иначе флаппинг good/bad — замеры-дисциплина); «баг в UI без автотеста» — написать тест специально для bisect (30 минут на pytest/playwright-тест против часов ручных шагов — тест остаётся в наборе как регресс-страж — двойная окупаемость — тест-пирамида); CI-интеграция — bisect-джобы на ночных прогонах («к утру виновник в тикете» — зрелый уровень).
Практический сценарий: «отчёты стали вдвое медленнее» — поиск за вечер
Исходник: жалоба — ночные отчёты (экспорт-конвейер) между релизами v2.8 (45 мин) и v2.10 (98 мин) замедлились вдвое; 430 коммитов между тегами; «в логах всё штатно» — классический перформ-регресс без ошибки. Подготовка: (1) воспроизведение — синтетический датасет 200k строк в локальной БД (testcontainers — фиксированный сид — детерминированная длительность ±3%); (2) оракул-скрипт: поднимает контейнерную БД, прогоняет генерацию, порог — «>75 с = bad» (между 45 и 98 — середина диапазона; медиана 3 прогонов против шума; timeout 400 → 125); (3) git bisect start; git bisect bad v2.10; git bisect good v2.8; git bisect run ./bench_regress.sh — в отдельном worktree (основная работа не прерывается — bisect живёт рядом); прогон: 9 шагов × ~4 мин (3 запуска по ~80 с ± сборка) — 38 минут — виновник: d4f9a12 "рефакторинг выборки заказов на ORM-relationship" — diff: списочная выборка заменена на ленивый доступ к relationship внутри цикла (N+1 на 200k итераций — проблема — в обзоре не поймали: код стал «чище», производительность упала в 40 раз на больших объёмах — на тестовых 500 строках незаметно). Фикс: joinedload+батчинг (30 минут — паттерн из статьи-ссылки), проверка — 41 мин (лучше исходного — батчинг оптимизировал и старое); регресс-тест производительности (тот же оракул, порог 60 с) — в ночной CI-набор (пайплайн — перформ-бюджет как код — следующие регрессы ловятся до релиза, не после); разбор: тестовые датасеты слишком малы для перформ-эффектов — добавлен «объёмный» набор в интеграционные тесты; случай в runbook: «регрессия без ошибки = bisect run с бенчмарк-оракулом». Общее время: вечер против недели «ручного археологического перебора», который команда уже начала (двое полдня читали diff-ы между релизами — bisect сделал то же за 38 минут автоматически).
FAQ
Что делать, если баг плавающий (воспроизводится через раз)? Bisect требует детерминированного оракула — сначала стабилизация: фиксировать рандом (seed в тестах/коде — pytest-фикстуры), изолировать время/сеть/гонки (freeze time, моки), прогон проверки N раз на шаге (5 запусков: ≥4 провала = bad, ≥4 успеха = good, иначе — повтор/skip — статистический оракул — bisect run поддерживает через скрипт); если «плавает» из-за гонки — гонка и есть баг (bisect по её коммиту — оракул с многократными запусками работает, медленно); совсем не воспроизводится — bisect отложить, собрать данные (логи/частота — error-tracking) — бинарный поиск по невоспроизводимому — гадание.
Bisect по огромной истории (годы, десятки тысяч коммитов)?
Математика щадит: 30 000 коммитов — 15 шагов (log₂) — выполнимо за день с быстрым оракулом; узкое место — не число шагов, а скорость/сборка промежуточных состояний (древние коммиты не собираются современными зависимостями — skip-зоны огромны — сужать good-границу «когда точно работало» по релизам/датам — git log --before — дата как граница); ускорение — параллельный bisect невозможен (последовательная природа), но оракул — в контейнере с кешем зависимостей; «история после squash-слияний» — короче и чище — trunk-based-дисциплина окупается и здесь.
Можно ли бисектить не код, а конфиг/инфраструктуру? Да, при версионированности (конфиг в git — IaC-принцип/ansible-репо (инвентарь) — bisect по нему идентичен — оракул проверяет поведение инфраструктуры); «конфиг менялся в рантайме без git» — сначала восстановление истории (бэкапы конфигов с таймстампами/аудит-логи) — невосстановимая история = небисектируема (аргумент за конфиг-как-код); внешние изменения (обновление зависимостей у провайдера/ОС-патчи) — не в вашем git — bisect не поможет — диагностика по времени события (корреляция с деплоями/журналами — инцидент-процесс).
Что лучше: bisect или «откатить последний деплой»? Разные инструменты: откат деплоя — мгновенное восстановление сервиса (инцидент горит — сначала тушим — SLO-приоритет); bisect — поиск виновника для понимания и правильного фикса (после стабилизации — «откатили, но что именно сломало» — без ответа регресс вернётся); зрелый порядок: откат → стабилизация → bisect в спокойной обстановке → фикс виновника → возврат функциональности; «откатили и забыли» — технический долг знания (фикс-откат — не фикс причины — долг-учёт) — и наоборот, «ищем виновника пока прод горит» — нарушение приоритетов инцидента.
Источники: документация git (git-bisect, git-worktree), Pro Git (Debugging with Git) — ссылки в шапке.