Баг появился «где-то за последний месяц», а в истории триста коммитов. Перебирать их вручную бессмысленно: 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 обязателен после поиска;
  • метод не подходит для нестабильных багов без надёжного воспроизведения.