git bisect: как найти коммит, который сломал код
git bisect находит коммит, в котором появилась ошибка, бинарным поиском по истории. Выполните git bisect start, отметьте плохой коммит git bisect bad и заведомо рабочий git bisect good <хеш>, затем проверяйте предлагаемые коммиты, пока Git не назовёт виновника; в конце — git bisect reset.
git bisect ищет коммит, который внёс ошибку, методом бинарного поиска. Вы указываете один коммит, где баг есть, и один, где его не было, — Git переключает вас на середину диапазона и спрашивает: «здесь хорошо или плохо?». Каждый ответ отсекает половину кандидатов, поэтому среди 1000 коммитов виновник находится примерно за 10 проверок.
Это особенно ценно, когда ошибка неочевидна: тест упал, производительность просела, вёрстка поехала, а по диффам ничего не понятно. Вместо чтения сотен изменений вы задаёте один и тот же простой вопрос несколько раз.
Как работает бинарный поиск по истории
Предположим, между рабочей версией v2.3 и текущим main 200 коммитов. Git берёт коммит посередине и проверяет его. Если там уже сломано — ошибка появилась в первой половине, если нет — во второй. Число кандидатов уменьшается вдвое при каждом шаге: 200, 100, 50, 25, 13, 7, 4, 2, 1. Восемь проверок вместо двухсот.
Отсюда главное требование: свойство «сломано» должно быть монотонным. Если баг появился, исчез и снова появился, bisect найдёт один из переходов, но не обязательно тот, что вам нужен.
git bisect пошагово: ручной режим
1. Найдите хороший и плохой коммиты
Плохой — обычно текущий HEAD. Хороший — любой коммит, где точно всё работало: тег прошлого релиза, коммит недельной давности. Найти кандидата помогут git log --oneline, git tag или git log --since="2 weeks ago".
2. Начните сессию
git bisect start
git bisect bad # текущий коммит сломан
git bisect good v2.3 # в релизе v2.3 всё работало
Bisecting: 99 revisions left to test after this (roughly 7 steps)
[4f5e6d7c...] Обновить зависимости фронтенда
То же можно записать одной строкой: git bisect start HEAD v2.3 — сначала плохой, затем хороший. Git сразу переключает рабочую директорию на коммит посередине (в состоянии detached HEAD).
3. Проверяйте и отмечайте
Соберите проект, запустите тест или воспроизведите баг вручную. Затем сообщите результат:
git bisect good # здесь работает
Bisecting: 49 revisions left to test after this (roughly 6 steps)
git bisect bad # здесь сломано
Bisecting: 24 revisions left to test after this (roughly 5 steps)
4. Получите результат
Когда кандидат останется один, Git выведет его:
a1b2c3d4e5f6... is the first bad commit
commit a1b2c3d4e5f6...
Author: Иван Петров <ivan@example.com>
Date: Tue Sep 29 11:40:12 2026 +0300
Кэшировать результат расчёта корзины
src/cart.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
5. Завершите сессию
git bisect reset
Команда вернёт вас на ветку, с которой вы начинали. Не забывайте этот шаг: пока сессия не закрыта, вы остаётесь в detached HEAD, и новые коммиты легко потерять. Вариант git bisect reset HEAD оставит вас на найденном коммите, а git bisect reset <коммит> — на любом указанном.
Автоматический поиск: git bisect run
Если проверку можно выразить командой, отдайте всю работу Git. git bisect run запускает скрипт на каждом шаге и сам ставит отметку по коду возврата:
| Код возврата | Значение |
|---|---|
| 0 | good — коммит хороший |
| 1–127, кроме 125 | bad — коммит плохой |
| 125 | skip — коммит невозможно проверить, пропустить |
| выше 127 (или <0) | прервать bisect (например, скрипт упал по сигналу) |
Примеры запуска:
git bisect start HEAD v2.3
git bisect run npm test
git bisect run pytest tests/test_cart.py -x
git bisect run make check
Для сложных проверок напишите небольшой скрипт. Важно отделить «не собралось» от «баг воспроизвёлся», иначе bisect примет сломанную сборку за искомую ошибку:
#!/bin/sh
# check.sh
npm ci --silent || exit 125 # не удалось поставить зависимости — пропустить
npm run build || exit 125 # не собралось — пропустить
node scripts/check-cart-total.js # 0 — ок, 1 — баг
chmod +x check.sh
git bisect run ./check.sh
Держите скрипт вне отслеживаемых файлов или в папке, которая есть во всех проверяемых коммитах: bisect переключает рабочую директорию, и файл, добавленный недавно, исчезнет на старых коммитах. Надёжнее всего положить его в /tmp или в любую папку вне репозитория.
Тест, которого раньше не было
Частая ситуация: вы написали тест, воспроизводящий баг, но в старых коммитах этого файла нет. Решение — та же копия вне репозитория:
cp tests/test_regression.py /tmp/
git bisect run pytest /tmp/test_regression.py
git bisect skip: когда коммит нельзя проверить
Иногда коммит посередине не собирается или падает по другой причине. Отмечать его good или bad нельзя — это исказит результат. Пропустите его:
git bisect skip
git bisect skip v2.4..v2.4.3 # пропустить диапазон
Git выберет соседний коммит. Если пропущенные коммиты окажутся прямо на границе, в конце вы увидите список «возможных виновников» вместо одного — тогда смотрите их диффы вручную.
Свои термины вместо good и bad
Bisect ищет не только баги. Например, вы хотите найти коммит, после которого страница стала грузиться быстрее. Называть быстрые коммиты «плохими» путано, поэтому есть альтернативные слова:
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.3
Встроенные синонимы — old и new: git bisect new, git bisect old. Правило одно: «старое» состояние должно быть у более раннего коммита.
Журнал сессии: log, replay, visualize
git bisect log— выводит все ваши отметки. Сохраните его в файл, если сессия длинная.git bisect replay файл— повторяет сессию по журналу. Ошиблись на шестом шаге? Сохраните лог, удалите неверные строки, выполнитеgit bisect resetиreplay.git bisect visualize(илиgit bisect view) — показывает оставшихся кандидатов; с--onelineвыводит их через git log.
git bisect log > /tmp/bisect.log
# правим лог
git bisect reset
git bisect replay /tmp/bisect.log
Bisect и merge-коммиты
В истории с множеством слияний bisect может завести вас внутрь слитой ветки — в коммиты, которые были созданы до того, как ветку синхронизировали с main, и поэтому собраны на другой базе. Если вам важно сначала понять, какое слияние принесло баг, используйте:
git bisect start --first-parent
Тогда Git идёт только по первым родителям merge-коммитов, то есть по истории основной ветки. Найдя виновное слияние, можно запустить второй bisect уже внутри соответствующей ветки.
Типичные ошибки
- Перепутали good и bad. Результат будет бессмысленным. Восстановите сессию через
git bisect logиreplay, не начиная заново. - Грязная рабочая директория. Незакоммиченные изменения мешают переключению коммитов и влияют на проверку. Перед стартом сделайте коммит или
git stash. - Неочищенные артефакты сборки. Устаревший кэш или
node_modulesот другого коммита дают ложные результаты. В скрипте делайте чистую сборку. - Нестабильный (flaky) тест. Если тест иногда падает сам по себе, bisect отметит случайный коммит. Прогоняйте проверку несколько раз или почините тест до поиска.
- Забыли reset. Остались в detached HEAD и начали работу. Найти потерянные коммиты поможет reflog.
Пример: поиск регрессии от начала до конца
Пользователи сообщили, что итог корзины считается неверно при скидке. В релизе v2.3 всё было правильно, в main — нет. Пишем минимальную проверку и запускаем поиск:
cat > /tmp/check.sh <<'SH'
#!/bin/sh
npm ci --silent || exit 125
node -e "const c=require('./src/cart'); process.exit(c.total([{price:100}], 0.1) === 90 ? 0 : 1)"
SH
chmod +x /tmp/check.sh
git stash # убрать незакоммиченные правки
git bisect start HEAD v2.3
git bisect run /tmp/check.sh
...
a1b2c3d4e5f6... is the first bad commit
git bisect log > /tmp/cart-bisect.log
git bisect reset
git stash pop
Весь поиск по сотне коммитов проходит без вашего участия, пока вы занимаетесь другим. Сохранённый лог пригодится, если в задаче нужно показать коллегам, как именно найден виновник.
Что делать с найденным коммитом
Когда виновник найден, посмотрите его целиком: git show a1b2c3d. Небольшой коммит с понятным сообщением сразу подскажет причину — это одна из причин, почему в командах ценят атомарные коммиты: bisect по истории из огромных «fix stuff» почти бесполезен. Дальше два пути: исправить ошибку новым коммитом или, если изменение целиком неудачное и уже опубликовано, отменить его через git revert a1b2c3d.
Итог
git bisect превращает поиск регрессии из чтения сотен диффов в несколько ответов «работает / не работает», а с git bisect run — в одну команду. Нужны только два ориентира — хороший и плохой коммит — и воспроизводимая проверка. Чтение истории, навигация по коммитам и поиск ошибок в ней разобраны на практике в курсе по Git.
Частые вопросы
Сколько шагов займёт git bisect?
Примерно log2 от числа коммитов в диапазоне: около 7 шагов для 100 коммитов и около 10 для 1000. Git сам показывает оценку roughly N steps.
Можно ли запустить bisect, если хороший коммит неизвестен?
Нужен хотя бы один. Возьмите заведомо старый коммит или тег релиза и проверьте его вручную; лишние шаги из-за широкого диапазона почти ничего не стоят.
Меняет ли git bisect историю репозитория?
Нет, он только переключает рабочую директорию между коммитами и хранит отметки в служебных ссылках refs/bisect, которые удаляются при git bisect reset.
Можно ли указать несколько хороших коммитов?
Да, команде git bisect good можно передать несколько коммитов, это сужает диапазон, особенно в истории с ветвлениями.
Работает ли bisect только для поиска багов?
Нет, им можно найти любой момент изменения свойства: когда появился файл, ускорилась сборка или изменилось поведение. Для этого удобны термины old/new или свои через --term-old и --term-new.
Эта тема подробно разобрана с практикой в курсе «Git с нуля до уверенного разработчика».
Перейти к курсу