Баг появился «где-то за последний месяц», а в истории триста коммитов. Перебирать их вручную бессмысленно: Git умеет искать бинарным поиском. Десять проверок сузят диапазон с тысячи коммитов до одного.
Идея метода
Вы сообщаете Git два факта: в коммите A всё работало, в коммите B уже сломано. Инструмент берёт середину диапазона, вы проверяете её и говорите, хорошая она или плохая. Диапазон сокращается вдвое, и так до единственного подозрительного коммита.
Число шагов — логарифм по основанию два. Для 1000 коммитов это около 10 проверок, для 100 — примерно 7. Главное условие: баг должен воспроизводиться детерминированно.
Ручной проход
Начните с текущего состояния и отметьте его как плохое:
git bisect start
git bisect bad
Затем укажите последний коммит или тег, где всё работало:
git bisect good v1.4.0
Git переключит рабочую копию на средний коммит и напишет, сколько шагов осталось. Запустите приложение или тест, проверьте баг и ответьте:
git bisect good # здесь бага нет
git bisect bad # здесь баг есть
Когда диапазон сузится, Git выведет сообщение вида abcd123 is the first bad commit. После этого обязательно вернитесь в исходное состояние:
git bisect reset
Без этой команды репозиторий останется на detached HEAD, и следующие коммиты можно потерять.
Автоматизация через bisect run
Ручные проверки утомляют, поэтому Git умеет запускать скрипт. Он должен возвращать код 0, если коммит хороший, и ненулевой, если плохой:
git bisect start
git bisect bad HEAD
git bisect good v1.4.0
git bisect run ./scripts/check-regression.sh
Внутри скрипта — любая проверка: unit-тест, сборка, HTTP-запрос к локальному серверу. Пример для Laravel:
#!/usr/bin/env bash
php8.3 artisan test --filter=OrderTotalTest > /dev/null 2>&1
Если тест падает, скрипт вернёт 1, и Git пометит коммит плохим. Никакого вывода разбирать не нужно — важен только код возврата.
Перед запуском закоммитьте или спрячьте изменения: git bisect переключает ветки и может отказаться работать с грязным деревом. Зависимости тоже придётся переустанавливать, если они менялись между коммитами, — иначе тест будет падать по ложной причине.
Коды возврата и пропуски
Договорённость простая:
0— коммит хороший;1–127— плохой, кроме особых значений;125— проверить невозможно, коммит пропускается.
Код 125 полезен, когда сборка не запускается из-за инфраструктуры: нет нужной версии PHP, сломан Docker-образ. Коммит исключается из поиска, и Git продолжает. Если пропустить вручную, используйте git bisect skip.
Когда рядом с найденным коммитом несколько пропущенных, Git честно предупредит, что точный виновник может быть среди них. Тогда придётся проверить вручную несколько кандидатов.
Слияния и first-parent
В истории с merge-коммитами поиск идёт по графу, а не по прямой линии. Иногда это приводит к коммиту, который сам по себе ничего не менял, а лишь слил ветку. Помогает режим:
git bisect start --first-parent
Он рассматривает только основную линию истории и пропускает внутренние коммиты боковых веток. Удобно, если баг точно пришёл через merge в основную ветку.
Если баг не воспроизводится
Bisect не работает с плавающими багами: гонками, зависимостью от времени, нестабильными внешними API. Проверка на середине диапазона даст случайный ответ, и поиск уйдёт не туда.
Сначала сделайте воспроизведение надёжным: зафиксируйте данные, отключите сеть, задайте seed. Если это невозможно, ищите иначе — по логам, по датам релизов или через git log -S, который ищет коммиты, добавившие или удалившие строку.
Полезные команды
git bisect log > bisect.log # сохранить ход проверки
git bisect replay bisect.log # повторить на другой машине
git bisect visualize # посмотреть диапазон в gitk
git bisect reset # вернуться на прежнюю ветку
Лог пригодится, если поиск прервался: его можно передать коллеге. А понять, что именно изменил найденный коммит, поможет статья как читать git diff. Для сравнения двух версий файла вручную есть онлайн-сравнение кода.
Коротко
git bisectделит диапазон пополам и находит регрессию за log₂ шагов;bisect runпринимает решение по коду возврата скрипта;- код 125 пропускает непроверяемый коммит;
git bisect resetобязателен после поиска;- метод не подходит для нестабильных багов без надёжного воспроизведения.