Регрессия (regression) — это когда «вчера работало, сегодня нет». Для админов/DevOps и вебмастеров это часто выглядит так: после мержа падают тесты, ломается деплой, миграции БД перестают проходить или приложение начинает отдавать 500. В таких ситуациях git bisect — один из самых прикладных инструментов: он делает бинарный поиск по истории и помогает найти конкретный «плохой» коммит быстрее, чем ручное перебирание.
Ниже — практическая инструкция: как запустить bisect вручную, как подключить git bisect run с автопроверкой, чем полезны skip и --first-parent, и какие ловушки чаще всего ломают результат (особенно при CI).
Что делает git bisect и почему это быстрее ручного поиска
Логика простая:
- вы задаёте «хорошую» точку (
good) — там бага нет; - задаёте «плохую» точку (
bad) — там баг есть; - Git выбирает коммит посередине диапазона, вы проверяете и помечаете
goodилиbad; - диапазон сужается вдвое, пока не останется один коммит-кандидат.
Это бинарный поиск: даже на тысяче коммитов обычно хватает около 10 проверок. При инциденте в проде это часто разница между «полдня раскопок» и «15 минут до конкретного SHA».
Подготовка: как правильно выбрать good и bad точки
Точность результата упирается в качество входных данных. Перед стартом полезно сделать две вещи.
- Зафиксировать критерий «сломано/не сломано». Это должна быть проверка с однозначным исходом: тест, команда, smoke-запрос, конкретная ошибка в логах.
- Выбрать две точки истории. Чаще всего:
bad— текущийHEADветки, где вы видите regression;good— последний известный релизный тег или коммит до большого мержа, где всё работало.
Если релизы отмечены тегами — используйте их как good: так проще объяснить результат команде и привязать к релизному окну.
Ручной git bisect: пошаговая инструкция
Базовый сценарий выглядит так.
1) Стартуем bisect и задаём плохую точку
git bisect start
git bisect bad
git bisect bad без аргументов помечает текущий HEAD как плохой.
2) Задаём хорошую точку
Например, по тегу или SHA:
git bisect good v1.8.3
После этого Git переключится на коммит-кандидат для проверки.
3) Проверяем и отмечаем результат
Запускаете критерий (тест/скрипт/ручная проверка) и отмечаете:
git bisect good
или
git bisect bad
Git переместит вас на следующий коммит. Повторяйте, пока не увидите сообщение вида is the first bad commit.
4) Завершаем bisect и возвращаемся на исходную точку
git bisect reset
Команда reset возвращает рабочую директорию к исходной ветке/коммиту и снимает состояние bisect. Это важно, чтобы случайно не продолжить работу «в середине истории».
Практика: нашли SHA — сразу зафиксируйте его в тикете/заметке и сделайте
git bisect reset. Чем дольше вы живёте в bisect-состоянии, тем проще перепутать контекст и сломать себе рабочую копию.

Критерий «good/bad»: примеры, которые удобно автоматизировать
Идеальный критерий выполняется одной командой и даёт однозначный exit code. Примеры для типовых стеков:
- падает юнит-тест:
pytest -q,phpunit,go test ./...; - ломается сборка:
npm test,npm run build,composer test; - ломается миграция: прогон миграций на временной БД (контейнер/ephemeral instance) и проверка результата;
- регрессия в конфиге: генерация итогового конфига из шаблонов и его проверка линтером/валидатором.
Ключевой момент: bisect «любит» автоматизацию. Как только вы превратили проверку в «одну команду» — подключайте git bisect run.
Если вы вылавливаете регрессии именно в пайплайне, полезно держать рядом отдельный воспроизводимый сценарий сборки/деплоя. В этом помогает дисциплина CI/CD: вынесенные шаги, быстрые smoke-тесты и понятный rollback. По теме можно дополнять процесс статьёй про деплой по SSH с откатом в CI/CD.
git bisect run: автоматический поиск «плохого» коммита через тест/скрипт
git bisect run запускает команду (или скрипт) на каждом выбранном коммите и сам проставляет good/bad по коду возврата.
Правило такое:
- exit code
0=good; - exit code
1..127=bad; - exit code
125=skip(невозможно проверить этот коммит в текущем окружении).
Пример: тесты как критерий regression
git bisect start
git bisect bad
git bisect good v1.8.3
git bisect run sh -c 'pytest -q'
Если тест прошёл — вернётся 0, если упал — ненулевой код, и bisect корректно сузит диапазон.
Пример: скрипт-проверка (сборка + smoke)
Часто нужна «обвязка»: подготовить окружение, поставить зависимости, собрать проект, прогреть кэш, а потом запустить проверку. Делайте это скриптом и возвращайте корректный exit code.
cat > ./bisect-test.sh <<'SH'
#!/bin/sh
set -eu
npm ci
npm run build
npm test
SH
chmod +x ./bisect-test.sh
git bisect start
git bisect bad
git bisect good v1.8.3
git bisect run ./bisect-test.sh
Как корректно «пропускать» коммиты (skip)
В ручном режиме:
git bisect skip
В автоматическом — возвращайте 125, если коммит объективно непроверяем (например, зависимость больше не доступна, сборка несовместима с текущим runtime):
cat > ./bisect-test.sh <<'SH'
#!/bin/sh
set -eu
if ! npm ci; then
exit 125
fi
if ! npm run build; then
exit 125
fi
npm test
SH
Важно: если вы пропускаете слишком много точек, bisect может завершиться не одним SHA, а несколькими кандидатами.
Ускоряем bisect: приёмы из практики
Сужайте диапазон заранее
Чем ближе выбран good к моменту поломки, тем меньше итераций. Если вы примерно знаете «после чего началось» (релиз, обновление зависимостей, конкретный PR) — ставьте good максимально близко.
Ищите по «магистрали» (first-parent), если много merge-коммитов
В репозиториях с крупными PR/merge иногда полезно получить ответ уровня «какой merge принёс regression»:
git bisect start --first-parent
Дальше вы либо бисектите уже внутри PR-ветки, либо просто смотрите список коммитов в составе мержа.
Фиксируйте окружение, иначе будете ловить не regression, а дрейф
Тяжёлые проверки (сборка фронтенда, интеграционные тесты) ускоряются и стабилизируются, если:
- использовать кеши пакетных менеджеров (npm/pip/composer) в том окружении, где идёт bisect;
- запускать bisect в контейнере/виртуалке с уже установленными системными пакетами;
- закрепить версии runtime (Node/Python/PHP/Go), иначе часть «падений» окажется несовместимостью окружения.
Если вы делаете проверку в изолированном окружении, часто удобнее гонять её на отдельной машине или виртуалке. Для таких задач обычно берут VDS, чтобы не мешать рабочей станции и иметь воспроизводимое окружение под конкретный проект.

Типовые ошибки и как их обходить
Ошибка 1: воспроизведение зависит от окружения
Классика: локально всё зелёное, а в CI — красное (или наоборот). Решение одно: приблизить bisect-окружение к тому, где проявляется regression. Тот же контейнер/образ, те же версии интерпретаторов, те же флаги сборки.
Ошибка 2: тест «плавающий» (flaky) и bisect обвиняет случайный коммит
Если тест иногда падает «сам по себе», git bisect может указать на невиновный SHA. Что помогает:
- делать проверку более детерминированной (фиксированный seed, отключение параллелизма, стабилизация данных);
- в
bisect runзапускать тест несколько раз и считатьbadтолько при повторяемом падении.
Пример на три попытки: если прошёл хотя бы раз — считаем good; если упал трижды — bad.
cat > ./bisect-test.sh <<'SH'
#!/bin/sh
set -eu
i=1
while [ $i -le 3 ]; do
if pytest -q; then
exit 0
fi
i=$((i + 1))
done
exit 1
SH
Ошибка 3: «плохой» коммит — это merge, а истинный виновник внутри
Если bisect показал merge-коммит, это не всегда означает, что проблема «в самом merge». Часто виноват один из коммитов в составе PR. Дальше варианты такие:
- посмотреть, какие коммиты принёс merge, и пробисектить диапазон внутри ветки;
- повторить bisect без
--first-parent(если вы его использовали), чтобы Git ходил по всем родителям.
Ошибка 4: старые коммиты не собираются
Для длинной истории это нормально: менялись компиляторы, lock-файлы, структура репозитория. В таких случаях обычно комбинируют:
skipдля реально непроверяемых точек;- перенос
goodближе (например, на первый релиз после обновления toolchain); - запуск bisect в «историческом» окружении (если оно у вас сохранено в CI как образ).
Как подружить git bisect с CI и разбором инцидентов
Самый прикладной сценарий: в CI появился regression, нужно быстро локализовать bad commit и открыть точный тикет/PR на фикс.
Что обычно работает:
- Хранить воспроизводимую команду из CI. Не «где-то в пайплайне», а конкретный шаг, который можно выполнить локально или на раннере.
- Иметь быстрый smoke test. Не e2e на 30 минут, а минимальную проверку на 1–3 минуты, которая ловит конкретную регрессию.
- Держать “bisect script” в репозитории. Скрипт из CI удобно положить, например, в
tools/и использовать как команду дляgit bisect run.
Если у вас доступен тот же раннер, где падает CI, часто быстрее выполнить bisect прямо там: совпадёт окружение, и вы не потратите время на «почему локально не воспроизводится».
Чтобы ускорять такие разборы системно, помогает «приземлённый» пайплайн деплоя (в том числе с rsync и понятными шагами). Если у вас PHP-проекты, посмотрите статью про CI/CD для PHP на GitHub Actions с деплоем через rsync — оттуда удобно забирать идею «одна команда, один результат».
Мини-чеклист: когда git bisect — лучший выбор
- есть regression и понятный критерий «good/bad»;
- понятно, где находится «точно хорошо» и «точно плохо» (тег, релиз, коммит до мержа);
- проверка достаточно быстрая, чтобы сделать 8–15 итераций.
Если критерий не формализуется или воспроизводимость слабая — сначала стабилизируйте симптом/тест. Bisect полезен тем, что заставляет договориться: что именно считается поломкой, и как это проверять без «магии».
Итог
git bisect сокращает время поиска причины regression и переводит разговор из «кто виноват» в «какой именно коммит сломал и почему». Начните с ручного режима, а как только появится автоматизируемый критерий — переходите на git bisect run. Это хорошо ложится на CI-процессы и экономит часы на каждом неприятном «вчера работало».


