Баг появился в продукте неделю назад, а заметили его только сейчас. Ручной просмотр сотни коммитов отнимет весь день, и хорошо если не два. Git bisect решает эту задачу за считаные минуты: он ведёт двоичный поиск по истории и проверяет не каждый коммит, а середину оставшегося диапазона. При этом большинство команд пользуются bisect вручную, отмечая коммиты как good и bad на глаз, хотя главная сила инструмента раскрывается в режиме bisect run, где проверку выполняет скрипт. Ниже разобран полный workflow: от разметки известных точек до автоматического прогона, обработки нестабильных тестов и анализа ошибочных результатов, которые способны увести поиск на невиновный коммит.
Ручной запуск bisect и разметка известных точек good и bad
Сеанс поиска начинается с команды git bisect start, после которой нужно указать две опорные точки: заведомо сломанный коммит и заведомо рабочий. Обычно bad это текущий HEAD, где баг воспроизводится, а good подбирают из недавнего прошлого, например прошлый релизный тег.
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
После третьей команды Git переключает рабочее дерево на коммит в середине диапазона и печатает что-то вроде:
Bisecting: 517 revisions left to test after this (roughly 9 steps)
[a1b2c3d4...] Merge branch into main
Первая строка сообщает, сколько коммитов осталось проверить и сколько шагов потребуется. Число 517 даёт порядка 9 итераций, потому что каждая проверка сокращает диапазон вдвое. Вторая строка показывает, на какой коммит выполнен checkout. Дальше разработчик воспроизводит баг вручную и вводит либо git bisect good, либо git bisect bad, и поиск сужается. Честно говоря, ручной режим утомителен уже на пятой итерации, а человеческий фактор здесь дорого стоит: один неверный вердикт, и поиск уйдёт в мёртвую ветку истории. Здесь кроется источник самого коварного артефакта, ложного хорошего коммита, к которому вернёмся ниже.
Полезно знать и терминологию: вместо good и bad Git позволяет применять слова old и new через git bisect start --term-old=fixed --term-new=broken, это выручает, когда ищут не появление бага, а, наоборот, коммит, где баг исчез или где поведение изменилось от желательного к нежелательному в обратную сторону. Отдельная полезная команда это git bisect log. Она печатает всю историю вердиктов сеанса, и её можно сохранить в файл через git bisect log > bisect.log, чтобы позже восстановить сеанс командой git bisect replay bisect.log или найти место, где была допущена ошибка.
Почему тысяча коммитов проверяется за десять шагов
Математика двоичного поиска прямая: каждый вердикт отбрасывает половину оставшихся кандидатов. Для N коммитов число шагов равно log2(N), округлённому вверх. Для 1000 коммитов это log2(1000), примерно 9.97, то есть десять проверок. Для миллиона коммитов потребовалось бы лишь двадцать шагов. Рост логарифмический, а не линейный, и это принципиально: стоимость поиска почти не зависит от длины истории, она зависит от стоимости одной проверки. Если тестирующий человек тратит на каждую итерацию пять минут, сеанс займёт около часа. Если проверку выполняет скрипт за десять секунд, весь поиск уложится в пару минут. По сути цена поиска это цена одного теста, умноженная на десяток, и дальше история масштабируется почти бесплатно. Отсюда вывод, который опытные инженеры делают быстро: bisect почти всегда стоит автоматизировать. Единственное обязательное условие это детерминированный тест, который надёжно отвечает на вопрос, есть баг в этом коммите или нет.
Автоматизация через bisect run и смысл кодов выхода скрипта
Команда git bisect run принимает любую исполняемую программу или скрипт и повторно запускает её на каждом проверяемом коммите, интерпретируя код выхода. Контракт простой, но его нужно знать точно:
- код 0 означает, что коммит хороший, баг не воспроизводится;
- любой код от 1 до 127, кроме 125, означает, что коммит плохой, баг присутствует (в примерах ниже используется 1);
- код 125 означает skip, коммит проверить невозможно, и Git выберет другого кандидата поблизости;
- коды 128 и выше (а также отрицательные) аварийно останавливают весь сеанс: так Git реагирует на падение самого скрипта или завершение по сигналу, и полагаться на такие коды для вердикта нельзя.
Типичный скрипт проверки для бага в CLI-утилите выглядит так:
#!/bin/bash
# test.sh возвращает 0 если баг отсутствует, 1 если воспроизводится
make build >/dev/null 2>&1 || exit 125
./bin/app --check-order | grep -q "sorted" || exit 1
exit 0
Разбор построчно: shebang запускает интерпретатор; сборка выполняется с подавлением вывода, и если она падает, скрипт возвращает 125, потому что коммит просто не собирается, а это не доказательство бага; затем приложение запускается и его вывод сверяется с ожидаемой строкой, при несовпадении возвращается 1. Запуск всего поиска выглядит так:
chmod +x test.sh
git bisect start HEAD v2.4.0
git bisect run ./test.sh
Обратите внимание на форму git bisect start HEAD v2.4.0: она принимает bad и good в одной строке, bad идёт первым. Скрипт обязательно должен лежать вне индекса рабочего дерева или быть неотслеживаемым, иначе при переключении коммитов Git столкнётся с локальными изменениями. Практичнее всего положить его в /tmp или сделать untracked-файлом в корне репозитория.
Контроль кандидатов через visualize и ручной пропуск проблемных коммитов
В середине сеанса полезно посмотреть, какие коммиты ещё остались под подозрением. Команда git bisect visualize запускает gitk с подсветкой текущего диапазона, а git bisect visualize --stat показывает статистику изменений прямо в терминале, без графического окна. Если история содержит merge-коммиты, bisect предпочитает кандидатов, которые не размывают границы диапазона, и иногда выбирает коммит не строго из середины, это нормально, а не признак сбоя. Когда автоматический скрипт неспособен проверить конкретную ревизию, например там сломан входной формат данных, ручной git bisect skip помечает коммит как непроверяемый, и Git подберёт ближайшую замену. У skip есть и пакетная форма: git bisect skip v2.4.1..v2.4.3 выкидывает сразу диапазон коммитов, которые заведомо бессмысленно тестировать, скажем известную серию промежуточных состояний рефакторинга. Полоса пропусков подряд обесценивает поиск, поэтому после трёх-четырёх skip стоит остановиться и понять, почему история не проверяется, а не продавливать сеанс до конца.
Ещё один практический нюанс касается вложенных checkout: bisect сам хранит исходную точку в файле .git/BISECT_START, поэтому прерванный сеанс переживает перезапуск терминала. Прервать и вернуться можно в любой момент: git bisect reset возвращает ветку на место, а файл с логом сохранит прогресс для replay.
Пошаговый workflow реального сеанса с разбором вывода
Соберём всё в один рабочий сценарий. Сначала убеждаемся, что рабочее дерево чистое, через git status, иначе bisect откажется переключать коммиты. Затем воспроизводим баг на HEAD вручную, чтобы подтвердить bad, и проверяем один старый коммит через git checkout v2.4.0, чтобы убедиться, что good точно хороший. Многие пропускают эту валидацию и платят за неё мусорным результатом. Дальше запускаем:
$ git bisect start HEAD v2.4.0
$ git bisect run ./test.sh
running './test.sh'
Bisecting: 517 revisions left to test after this (roughly 9 steps)
[8f3a21c9...] Refactor order processing
running './test.sh'
Bisecting: 258 revisions left to test after this (roughly 8 steps)
...
8f3a21c9f1a2b3c4d5e6f7890abcdef123456789 is the first bad commit
commit 8f3a21c9f1a2b3c4d5e6f7890abcdef123456789
Author: Dev <Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. >
Date: Tue Sep 30 14:22:11 2025 +0300
Refactor order processing
Построчный разбор: каждая строка running фиксирует запуск скрипта на очередном коммите; строки Bisecting показывают остаток диапазона и прогноз шагов, он убывает ровно вдвое; финальный блок это главный результат, полный хеш первого плохого коммита, его автор, дата и сообщение. Дальше полезно выполнить git show 8f3a21c9 и изучить диф: обычно виновные строки видны сразу, особенно если коммиты в проекте атомарные. Перед завершением обязательно выполняют git bisect reset, иначе репозиторий останется в состоянии detached HEAD на середине истории. Начинающие часто забывают reset, а потом удивляются, куда пропали свежие коммиты из ветки: они никуда не пропали, просто HEAD смотрит в прошлое. Кстати, git bisect reset с аргументом возвращает дерево не в исходную точку, а в конкретный коммит, переданный аргументом, что удобно, когда сразу хочется открыть виновную ревизию и править поверх неё.
Отдельно отметим window для bad и good: чем уже стартовый диапазон, тем меньше шагов, поэтому перед запуском стоит уточнить у коллег или по заметкам в трекере, когда функция точно работала. Диапазон в 128 коммитов закрывается за семь проверок, а разница между семью и десятью итерациями на медленном тесте ощутима.
Где сеанс даёт вранье и как это распознать
Ошибочные результаты в bisect почти всегда следствие плохого теста, а не самого Git. Самый громкий случай это ложный хороший коммит: скрипт вернул 0 там, где баг на самом деле был. Причины бывают разные. Тест проверяет симптом, а не причину, и случайно проходит на сломанной ревизии. Скрипт зависит от кэша сборки, который переживает checkout: make без clean счастливо переиспользует старые объектные файлы, и проверка меряет не тот бинарник. Лечится добавлением очистки в скрипт, например make clean или git clean -fdx перед сборкой, хотя последнее агрессивно и трёт всё неотслеживаемое. Ещё одна ловушка это флаки-тесты: если проверка нестабильна, вердикты становятся шумом, и результат сеанса недетерминирован. Для подозрительных тестов стоит прогонять скрипт трижды на одном коммите и принимать решение по большинству. Код 125 тоже коварен: когда подряд идёт много пропущенных коммитов, например из-за длинной полосы несобирающейся истории, Git предупреждает, что не может выбрать хорошего кандидата, и точность результата падает. Если сомнения остались, сеанс легко проверить: сделать git bisect reset, затем вручную переключиться на найденный коммит и его родителя, и прогнать скрипт на обоих. Родитель обязан быть хорошим, коммит плохим. Не сошлось, значит точку good выбрали неверно или тест врёт. Полезный приём это специально пометить git bisect bad заведомо рабочий коммит в тренировочном репозитории и посмотреть, к чему приводит испорченный вердикт: финал укажет на совершенно посторонние изменения, и такая наглядность запоминается надолго.
Профилактика регрессий и наведение порядка после поиска
Самая надёжная профилактика мусорных результатов это порядок в самом репозитории. Атомарные и всегда собирающиеся коммиты сокращают долю skip до нуля, а покрытие критичных сценариев быстрыми smoke-проверками превращает bisect run в рутину на две минуты. Практика зрелых команд такова: маленький регрессионный скрипт пишется сразу при заведении бага, а после поиска конвертируется в постоянный тест в CI, чтобы та же регрессия больше не прошла незамеченной. После git bisect reset стоит удалить временный скрипт или перенести его в каталог тестов, зафиксировать найденный виновный коммит в трекере задач вместе с полным хешем и коротким описанием механики поломки. Отдельно дисциплинирует сообщения коммитов: когда bisect показывает виновный хеш, команда в первую очередь читает именно его текст, и сухая строка вроде "misc fixes" гарантированно вызовет вопросы на разборе. Хорошая привычка это периодически повторять bisect run на свежем диапазоне между релизами, даже когда ничто не горит: так обкатывается и сам скрипт, и дисциплина коммитов. Типичная ошибка новичков это запуск bisect run из подкаталога репозитория: относительный путь ./test.sh перестаёт находиться после checkout в другую ревизию, если сам скрипт отслеживается Git. Либо держите скрипт вне репозитория, либо вызывайте его по абсолютному пути. Стоит держать под рукой и команду git bisect run для нестандартных проверок: скриптом может быть и pytest tests/test_order.py -q, и curl к поднятому локально серверу, и сравнение хеша артефакта. Главное чтобы скрипт был самодостаточным, не читал состояние из внешнего мира и завершался быстро. Двоичный поиск по истории это один из тех редких инструментов, где автоматизация окупается с первого же применения, а вложения сводятся к одному честному скрипту из пяти строк.