Олег Марков
Поиск в истории - git bisect
Введение
Когда в проекте появляется баг, который раньше не проявлялся, естественный вопрос — в каком коммите он появился. Если история проекта короткая, вы можете просмотреть несколько последних изменений вручную. Но как только в репозитории сотни или тысячи коммитов, ручной поиск превращается в долгий и утомительный процесс.
И вот тут вам помогает git bisect — встроенный инструмент Git, который реализует двоичный поиск по истории коммитов. Он позволяет по результатам ваших проверок автоматически сужать диапазон между «хорошим» и «плохим» коммитом, пока не останется один конкретный коммит, который и вызвал проблему.
В этой статье мы разберем, как работает git bisect, как его запускать и останавливать, как автоматизировать проверку с помощью скриптов, что делать с мердж-коммитами и как не допустить типичных ошибок при использовании этого инструмента.
Что такое git bisect и зачем он нужен
Основная идея git bisect
Git bisect реализует двоичный поиск по истории коммитов. Смотрите, как это работает:
Вы указываете два коммита:
- один, где баг еще не проявлялся (good);
- другой, где баг уже есть (bad).
Git автоматически находит промежуточный коммит между ними и переключает рабочую директорию на него.
Вы проверяете, воспроизводится ли баг:
- если баг есть — помечаете этот коммит как bad;
- если бага нет — помечаете его как good.
Git снова делит оставшийся диапазон коммитов пополам и повторяет процесс.
Так повторяется до тех пор, пока диапазон не сузится до одного коммита. Именно этот коммит Git покажет вам как первый «плохой».
Главное достоинство подхода — скорость. Вместо линейного просмотра N коммитов вы получаете сложность O(log N). На практике:
- при 1000 коммитах вам потребуется порядка 10–11 проверок;
- при 1 000 000 коммитов — около 20.
Когда git bisect особенно полезен
Давайте обозначим типичные сценарии, когда стоит использовать git bisect:
- Баг появился «когда-то недавно», но неизвестно, в каком именно коммите.
- Автотесты начали падать, но неясно, какое изменение их сломало.
- Регрессия проявляется только при специфическом окружении, и не получается сразу понять, какая правка кода ее вызвала.
- Наблюдается падение производительности, и нужно найти первый коммит, где метрика ухудшилась.
Вместо того чтобы гадать и читать коммиты по одному, вы используете систематический бинарный поиск.
Базовый рабочий процесс git bisect
Минимальный пример использования
Давайте разберем базовый сценарий. У вас есть ветка main, в которой где‑то появился баг. Вы знаете:
- текущая HEAD — с багом;
- какой‑то старый коммит — еще без бага.
Пошаговая последовательность:
# 1. Запускаем режим bisect
git bisect start
# 2. Помечаем текущий коммит как "плохой"
git bisect bad
# 3. Помечаем известный "хороший" коммит
git bisect good <хэш_коммита_без_бага>
Комментарии:
- git bisect start — включает специальный режим в Git, при этом репозиторий продолжает существовать как обычно, но Git начинает «управлять» вашим HEAD, переключая его на разные коммиты;
- git bisect bad без аргументов — значит, что плохим считается текущий HEAD;
- git bisect good
— вы указываете коммит, в котором баг не воспроизводился.
После этих команд Git выберет промежуточный коммит и автоматически сделает checkout на него. В выводе вы увидите что‑то вроде:
Bisecting: 7 revisions left to test after this (roughly 3 steps)
Это означает, что осталось 7 возможных кандидатов и примерно 3 шага до нахождения виновного коммита.
Отмечаем коммиты как good или bad
Теперь на каждом шаге вы:
- Запускаете тест/проверку.
- Оцениваете результат.
- Сообщаете Git, был ли этот коммит good или bad.
# Если баг есть
git bisect bad
# Если бага нет
git bisect good
Каждый раз Git:
- обновляет внутренний диапазон поиска;
- делает checkout на следующий промежуточный коммит;
- сообщает, сколько шагов осталось.
Цикл продолжается до тех пор, пока виновный коммит не будет найден. Когда это произойдет, Git выведет примерно:
<hash> is the first bad commit
commit <hash>
Author: ...
Date: ...
Сообщение коммита
Теперь вы видите конкретный коммит и его изменения (можно посмотреть через git show
Завершение bisect и возврат к исходному состоянию
После окончания поиска важно вернуть репозиторий в исходное состояние. Для этого предусмотрена команда:
git bisect reset
Она:
- отменяет режим bisect;
- возвращает HEAD туда, где вы были до git bisect start (обычно на вашу рабочую ветку).
Если вы забудете выполнить git bisect reset, продолжите работать на одном из промежуточных коммитов, что может ввести в заблуждение. Обратите внимание на вывод git status: если bisect еще активен, Git покажет соответствующее сообщение.
Подготовка к использованию git bisect
Требования для корректного поиска
Перед запуском git bisect стоит убедиться в нескольких вещах:
У вас есть:
- как минимум один известный хороший коммит;
- как минимум один известный плохой коммит.
Проект собирается и запускается на всех промежуточных состояниях или хотя бы на большинстве. Если некоторые коммиты не собираются, это не критично, но потребует дополнительного шага (об этом поговорим позже).
У вас есть воспроизводимая проверка, которая однозначно определяет:
- есть баг или нет;
- или тест падает, или проходит.
Чем более детерминированна проверка, тем надежнее будет результат bisect.
Как найти «хороший» и «плохой» коммиты
Часто вы знаете только:
- баг есть в текущем состоянии;
- баг отсутствовал «какое‑то время назад».
Вот простой подход:
- Убедитесь, что текущий HEAD действительно содержит баг.
Найдите примерно ту точку по времени, когда вы уверенны, что бага еще не было. Это может быть:
- релизный тег;
- дебет ветки перед большим рефакторингом;
- коммит, после которого вы помните, что система работала.
Переключитесь туда и проверьте, что бага и правда нет.
git checkout v1.2.0
# Запустить проверку, убедиться, что баг не воспроизводится
- Если все так, фиксируйте этот тег/коммит как good при запуске bisect.
Пошаговый разбор типичного сценария
Давайте посмотрим на упорядоченный пример использования git bisect в «ручном» режиме.
Предположим:
- вы находитесь на ветке main;
- баг воспроизводится в HEAD;
- вы знаете, что в теге v1.0.0 баг отсутствовал.
Шаг 1. Запуск bisect
git bisect start
git bisect bad # текущий HEAD плохой
git bisect good v1.0.0 # тег v1.0.0 хороший
Git сделает checkout на промежуточный коммит.
Шаг 2. Проверка промежуточного коммита
Вы запускаете свои тесты или ручную проверку. Допустим, у вас есть скрипт test.sh, который запускает нужные проверки.
./test.sh
Предположим, в этом состоянии баг еще не проявляется.
Тогда:
git bisect good
Git ответит, сколько шагов осталось, и переключит вас на следующий коммит.
Если бы баг проявился, вы бы написали:
git bisect bad
Шаг 3. Повторение цикла
Продолжайте повторять:
- Проверка (тест/ручной сценарий).
- git bisect good или git bisect bad.
Через несколько итераций Git сообщит:
<hash> is the first bad commit
Теперь можно изучать этот коммит:
git show <hash>
Шаг 4. Возврат к нормальной работе
После анализа (и, возможно, фикса бага) завершите bisect:
git bisect reset
Теперь вы снова на главной ветке (main, develop или другой, в зависимости от того, откуда вы начинали).
Автоматизация проверки с git bisect run
Ручной bisect хорош, если:
- баг легко проверяется «на глаз»;
- или нужно что‑то посмотреть в интерфейсе.
Но если у вас есть скрипт/команда, которая может:
- завершиться с кодом 0, если все ок;
- завершиться с ненулевым кодом, если баг воспроизводится (или тесты падают),
то git bisect можно полностью автоматизировать.
Как работает git bisect run
Общий формат:
git bisect start
git bisect bad
git bisect good <коммит_или_тег>
git bisect run <команда> [аргументы]
Git будет:
- Выбирать очередной коммит.
- Делать checkout на него.
- Запускать указанную команду.
- Считать коммит:
- bad, если команда завершилась с ненулевым кодом возврата;
- good, если команда вернула 0.
В конце Git автоматически сообщит первый плохой коммит и завершит bisect. Но режим bisect останется активным, поэтому в большинстве случаев вы все равно дополнительно делаете:
git bisect reset
Пример с запуском тестов
Давайте разберем на примере. Допустим, ваш проект на Node.js, и тесты запускаются так:
npm test
Вы хотите, чтобы:
- если тесты проходят — коммит считался good;
- если тесты падают — коммит считался bad.
Последовательность:
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# Запускаем автоматический bisect
git bisect run npm test
Git будет переключаться между коммитами и запускать npm test. Вам не нужно вручную ничего помечать.
Пример с кастомным скриптом
Иногда вам нужно проверить не все тесты, а только специфический сценарий. В таком случае удобно написать небольшой скрипт. Смотрите, вот пример скрипта на Bash:
#!/usr/bin/env bash
# Скрипт должен завершиться:
# - кодом 0, если коммит хороший (баг не воспроизводится)
# - ненулевым кодом, если коммит плохой (баг есть)
# Собираем проект
make build > /dev/null 2>&1
if [ $? -ne 0 ]; then
# Если проект не собирается, можно считать коммит "пропущенным"
# или "плохим" — это уже зависит от вашей стратегии
echo "Сборка не удалась - пропускаем коммит"
exit 125 # специальный код, чтобы git bisect пропустил коммит
fi
# Запускаем сценарий, который должен падать при наличии бага
./run_scenario.sh > output.log 2>&1
# Проверяем, есть ли в выводе ожидаемая ошибка
if grep -q "Unexpected error" output.log; then
# Ошибка есть - коммит плохой
exit 1
else
# Ошибки нет - коммит хороший
exit 0
fi
Комментарии к коду:
- exit 0 — тест прошел, коммит good;
- exit 1 — тест упал, коммит bad;
- exit 125 — специальный код для git bisect, чтобы пометить коммит как пропущенный (об этом ниже).
Теперь вы можете:
git bisect start
git bisect bad
git bisect good <старый_коммит_без_бага>
git bisect run ./check_bug.sh
Git сам выполнит скрипт для всех нужных коммитов и выдаст результат.
Работа с «плохими» коммитами — skip и нестабильные состояния
Зачем нужен git bisect skip
В реальном репозитории могут встречаться коммиты, которые:
- не собираются;
- требуют особой конфигурации окружения;
- зависят от внешних сервисов, которые сейчас недоступны.
В этом случае вы не можете корректно проверить, есть ли баг. Вам не подходит ни good, ни bad. Для таких случаев есть команда:
git bisect skip
Она говорит Git: этот коммит невозможно проверить, игнорируй его при расчете результата. Git попытается найти ответ на оставшихся коммитах.
В автоматическом режиме skip можно инициировать через код возврата 125, как в примере со скриптом.
Как ведет себя bisect при пропущенных коммитах
Если вы пропустили несколько коммитов, итоговый результат может выглядеть так:
Bisecting: 0 revisions left to test after this (roughly 0 steps)
Some good and some bad revisions were skipped
The first bad commit could be any of:
<hash1>
<hash2>
We cannot bisect more!
Это значит, что:
- в области между good и bad были пропуски;
- Git не смог однозначно определить единственный виновный коммит;
- список из нескольких хэшей — возможные кандидаты.
В такой ситуации вы:
- либо пытаетесь все же проверить пропущенные коммиты вручную;
- либо смотрите диффы нескольких кандидатов и анализируете их по содержанию.
Распространенные сценарии использования git bisect
Поиск коммита, сломавшего конкретный тест
Допустим, у вас есть unit-тест:
go test ./... -run TestCriticalFeature
Он раньше проходил, а теперь падает. Вы хотите найти первый коммит, где он начал падать.
- Убедитесь, что тест сейчас падает.
- Найдите релиз/коммит, где вы уверены, что он проходил.
- Запустите bisect:
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run go test ./... -run TestCriticalFeature
Как только bisect завершится, он покажет вам первый коммит, в котором этот тест стал падать.
Поиск регрессии в производительности
Иногда баг — это не падение тестов, а, например, ухудшение времени отклика или повышение использования памяти. Здесь подход тот же, но ваш скрипт должен измерять метрику и по какому‑то порогу определять bad/good.
Пример наброска скрипта:
#!/usr/bin/env bash
# Запускаем бенчмарк и сохраняем результат
TIME=$(./run_benchmark.sh | grep "Total time" | awk '{print $3}')
# Порог - 200 миллисекунд
THRESHOLD=200
# Сравниваем время
if [ "$TIME" -gt "$THRESHOLD" ]; then
# Время больше порога - считаем это регрессией
exit 1 # коммит плохой
else
exit 0 # коммит хороший
fi
Комментарии:
- Скрипт должен быть достаточно стабильным — старайтесь минимизировать влияние случайных факторов (нагрузка системы, сеть, кэш).
- Иногда имеет смысл несколько раз повторять измерения и усреднять результат.
Поиск причины «случайно исчезнувшей» функциональности
Бывает, что функция «вроде бы была», а теперь ее нет. Например, какой‑то API эндпоинт перестал возвращать нужные данные. Вы можете написать:
- небольшой интеграционный тест или запрос к API;
- скрипт, который:
- делает запрос;
- проверяет, что ответ содержит ожидаемые поля;
- по результату возвращает 0 или 1.
Дальше вы ставите этот скрипт в git bisect run, как в предыдущих примерах.
Тонкости работы с ветками, merge-коммитами и диапазонами
Можно ли использовать bisect с мердж-коммитами
Да, git bisect работает и с линейной историей, и с историей с merge-коммитами. Git рассматривает всю DAG (ориентированный ациклический граф) коммитов и находит «середину» между good и bad.
Особенность:
- Маркировать good/bad вы можете как на обычных коммитах, так и на merge-коммитах;
- Внутренний алгоритм все равно разделяет область поиска по количеству коммитов.
Если merge-коммиты сильно усложняют анализ, иногда бывает удобно запускать bisect не по всей истории, а по конкретной ветке, но это скорее организационный вопрос.
Bisect по конкретному диапазону
Вместо того чтобы всегда брать HEAD как bad и старый тег как good, вы можете задавать произвольный диапазон. Например:
git bisect start <плохой_коммит> <хороший_коммит>
Это сокращенная форма:
git bisect start
git bisect bad <плохой_коммит>
git bisect good <хороший_коммит>
Такой формат удобен, если, например:
- вы знаете, что баг появился где‑то между двумя релизами;
- хотите ограничить поиск только этим интервалом.
Практические советы и типичные ошибки
Убедитесь, что критериям good/bad можно доверять
Одна из главных проблем при использовании git bisect — нестабильные проверки:
- флапающие тесты (то проходят, то нет);
- зависимость от случайных значений;
- нестабильная сеть или внешние сервисы.
Если один и тот же коммит может быть то good, то bad, алгоритм может запутаться. В худшем случае вы получите неверный виновный коммит.
Советы:
- по возможности делайте проверки детерминированными;
- при нестабильных тестах повторяйте запуск несколько раз;
- если состояние нельзя достоверно определить — используйте skip.
Не забывайте про git bisect reset
После нахождения виновного коммита легко увлечься анализом и забыть, что вы все еще в режиме bisect. Это может привести к тому, что вы начнете коммитить изменения «внутри» bisect.
Обратите внимание:
- git status подсказывает, что сейчас активен bisect;
- git bisect reset всегда безопасно возвращает вас в исходное состояние.
Фиксируйте сценарий проверки отдельным скриптом
Даже если вы используете git bisect в ручном режиме, хорошая идея — оформить сценарий проверки в виде отдельного скрипта. Причины:
- сценарий останется повторяемым;
- его можно будет легко использовать с git bisect run;
- проще учитывать особые случаи (сборка не удалась, пропуск и т. д.).
Краткая сводка команд git bisect
Основные команды
git bisect start
Включить режим bisect.git bisect bad [<коммит>]
Пометить текущий или указанный коммит как плохой.git bisect good [<коммит>]
Пометить текущий или указанный коммит как хороший.git bisect skip [<коммит>]
Пропустить текущий или указанный коммит (невозможно проверить).git bisect run <команда> [аргументы]
Автоматически выполнять bisect, используя команду как проверку.git bisect reset [<коммит>]
Выйти из режима bisect, вернуть HEAD в исходное состояние (или в указанный коммит).
Дополнительная команда
git bisect log
Показать журнал того, какие коммиты вы помечали good/bad/skip.git bisect replay <файл>
Повторить процесс bisect, используя лог (это удобно, если вы хотите воспроизвести те же шаги на другом репозитории или машине).
Заключение
Git bisect — мощный инструмент для поиска первого коммита, в котором появился баг, регрессия или изменение поведения. Вместо линейного сканирования истории вы используете двоичный поиск, что позволяет локализовать проблему за логарифмическое число шагов даже в очень длинной истории.
Ключевые моменты:
- всегда определяйте один хороший и один плохой коммит;
- используйте простое и надежное правило, по которому вы решаете, считать текущий коммит good или bad;
- по возможности автоматизируйте проверку через git bisect run и небольшой скрипт;
- не забывайте про git bisect skip для неприменимых состояний и git bisect reset для выхода из режима bisect.
Если вы добавите git bisect в свой стандартный набор приемов, поиск источника сложных багов и регрессий станет значительно более управляемым и предсказуемым.
Частозадаваемые технические вопросы по теме и ответы
Как использовать git bisect, если баг проявляется только иногда (флаповый тест)
В такой ситуации важно снизить влияние случайности. Сделайте скрипт, который несколько раз подряд запускает тест и считает коммит bad, только если тест упал, скажем, 3 раза из 3. Пример:
#!/usr/bin/env bash
N=3
FAILS=0
for i in $(seq 1 $N); do
go test ./... -run TestFlaky || FAILS=$((FAILS+1))
done
if [ "$FAILS" -eq "$N" ]; then
exit 1 # всегда падает - однозначно bad
else
exit 0 # считаем коммит приемлемым
fi
Потом запускайте git bisect run на этот скрипт.
Можно ли использовать git bisect в «грязном» рабочем дереве
Лучше нет. Перед запуском bisect убедитесь, что у вас нет незакоммиченных изменений. Если они есть, либо закоммитьте их, либо сделайте git stash. В противном случае при переключении между коммитами возникнут конфликты и ошибки. Типичный порядок действий:
git stash
git bisect start ...
# после окончания
git bisect reset
git stash pop
Как поступать, если коммит требует определенной версии зависимостей
Если какие‑то старые коммиты требуют другие версии библиотек или языков, чем текущие, лучше обернуть настройку окружения в скрипт, который:
- пытается подготовить нужную версию зависимостей;
- при неудаче возвращает код 125 (skip), чтобы git bisect пропустил такой коммит.
Так вы сохраните корректность поиска, не ломая окружение вручную для каждого шага.
Можно ли искать «первый хороший» коммит, а не «первый плохой»
Да. Вы можете инвертировать логику:
- считать старый коммит bad (там, например, тест всегда падал);
- более новый good;
- скрипт сделать так, чтобы код 0 означал «ошибка ушла» (good), а ненулевой — «ошибка еще есть» (bad).
Алгоритм bisect от этого не меняется — он просто ищет границу между двумя состояниями.
Как повторить тот же bisect на другом репозитории
Используйте git bisect log и git bisect replay. На первой машине:
git bisect log > bisect.log
На второй машине, имея тот же набор коммитов:
git bisect start
git bisect replay bisect.log
Git воспроизведет все шаги и придет к тому же результату, что и на исходной машине.
Постройте личный план изучения Git до уровня Middle — бесплатно!
Git — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Git
Лучшие курсы по теме

Основы Git
Антон Ларичев
TypeScript с нуля
Антон Ларичев