10.1 git log: больше, чем oneline
Самая базовая команда - git log. Без аргументов выводит
коммиты с автором, датой, полным сообщением. Это годится, чтобы
посмотреть «что было недавно», но для расследования нужны
другие форматы.
git log --oneline # один коммит - одна строка
git log --oneline --graph --all # с графом всех веток
git log --oneline -10 # последние 10
git log --stat # с файлами и числами изменений
git log -p # с полным diff каждого коммита
git log -p -- src/parser.py # diff'ы коммитов, тронувших файл
Особенно полезно --graph --all --oneline в сочетании с
--decorate:
git log --oneline --graph --all --decorate
# * a1b2c3d (HEAD -> main, origin/main) Merge feature/login
# |\
# | * e4f5a6b (feature/login) Fix typo
# | * 7a8b9c0 Add login form
# |/
# * b1c2d3e Initial commit
--decorate показывает ветки и теги. Многие настраивают это
как alias:
git config --global alias.lg \
"log --graph --oneline --decorate --all"
После этого git lg - однокомандный обзор истории.
Кастомный формат через --pretty:
git log --pretty=format:"%h %ad %s [%an]" --date=short
# a1b2c3d 2026-05-15 Add login form [Dmitry]
# e4f5a6b 2026-05-14 Initial commit [Olya]
Все placeholder'ы (%h, %ad, %s, %an, %ae, и т.д.)
- в
man git-log.
См. log.
10.2 Фильтры: по автору, дате, сообщению, пути
git log поддерживает фильтры. Самые частые:
# По автору
git log --author="Dmitry"
git log --author="@example.com" # частичное совпадение в email
# По дате
git log --since="2026-01-01"
git log --until="2026-03-15"
git log --since="2 weeks ago"
# По сообщению коммита
git log --grep="JIRA-1234" # рег.выражение
git log --grep="fix" --grep="auth" --all-match # И-условие
# По пути файла
git log -- src/parser.py
git log -- "**/*.py"
git log --follow src/renamed.py # с историей переименований
# По ветке
git log main..feature # коммиты на feature, не на main
git log feature ^main # то же самое в другой записи
# По количеству изменённых файлов
git log --max-count=20
# Mergi-коммиты только / без них
git log --merges
git log --no-merges
Комбинируются друг с другом. «Все багфиксы Димы за последний квартал в auth»:
git log --author="Dmitry" \
--since="3 months ago" \
--grep="^fix" \
-- src/auth/
Это типичный paragraph любого исследования. Запомнил пару ключевых фильтров - и поиск занимает секунды вместо минут листания.
10.3 Поиск по содержимому: -S и -G
Самый недооценённый инструмент Git - pickaxe (буквально «кирка»). Это поиск по тому, что появилось или исчезло в коммитах.
git log -S "<строка>" - найти коммиты, после которых
количество вхождений строки в любом файле изменилось. Иначе
говоря - коммиты, которые добавили или удалили эту строку.
git log -S "calculateTotal"
# покажет коммиты, в которых количество "calculateTotal"
# в файлах поменялось
Это не просто grep по diff'ам. -S точно знает, что
строка «появилась» или «пропала», и игнорирует случаи, когда
строка просто переехала из одной части файла в другую (там
количество вхождений не изменилось).
Типичный сценарий - нашёл странную константу или функцию, хочешь понять, кто её добавил:
git log -S "MAGIC_TIMEOUT = 8675"
# один или несколько коммитов
# дальше git show <sha> - и видишь полный контекст
-G "<regex>" - похоже, но через регулярное выражение и без
хитрости с подсчётом вхождений. Просто «коммиты, у которых diff
матчит regex». Чаще нужен -S (он точнее), -G - если ищешь
по шаблону.
git log -G "TODO\(.*\)"
Полезный довесок - флаг -p показывает diff:
git log -S "calculateTotal" -p
# увидишь, в каких коммитах добавилось/убралось,
# с полным контекстом каждой правки
git log -S уверенно находит «откуда взялась эта функция» за
одну команду в репозиториях с десятком тысяч коммитов. Знание
этого экономит часы.
10.4 git blame: построчное происхождение
git blame <file> показывает для каждой строки файла: SHA
последнего коммита, который её тронул, автора и дату.
git blame src/auth.py
# a1b2c3d (Dmitry 2026-05-15 12:34:11 +0300 1) def login(user):
# a1b2c3d (Dmitry 2026-05-15 12:34:11 +0300 2) check(user)
# e4f5a6b (Olya 2026-04-12 09:11:23 +0300 3) log_attempt(user)
# a1b2c3d (Dmitry 2026-05-15 12:34:11 +0300 4) return token
Используется на вопрос «кто и в каком коммите написал эту строку». Имя вводящее в заблуждение - blame не «обвиняет», просто атрибутирует.
Полезные опции:
git blame -L 10,30 file.py # только строки 10-30
git blame -L "/def login/,/return/" # от регулярки до регулярки
git blame -w # игнорировать whitespace в правках
git blame -C -C -C # отслеживать copy-paste между файлами
git blame --since="3 months ago" # только недавние правки
Важная тонкость: blame показывает последний коммит, тронувший строку. Если коммит просто переформатировал файл (например, прогнали через prettier), blame покажет тот форматирующий коммит для каждой строки - и оригинальная история теряется.
Решение - флаг -w (игнорировать whitespace) и опция
--ignore-rev:
git blame --ignore-rev <sha-форматера> file.py
Или в проектах есть файл .git-blame-ignore-revs со списком
«коммитов-форматеров», которые надо проигнорировать. Настроить
Git, чтобы он его подхватывал:
git config --global blame.ignoreRevsFile .git-blame-ignore-revs
GitHub автоматически уважает .git-blame-ignore-revs в корне
репозитория - в blame через веб эти коммиты пропускаются.
См. blame.
10.5 git bisect: бинарный поиск регрессии
Самый мощный инструмент расследования багов. Сценарий: «раньше
работало, теперь нет, в истории сотни коммитов». git bisect
за log(N) шагов находит первый коммит, где сломалось.
git bisect start
git bisect bad # текущий - сломанный
git bisect good v1.4 # известный рабочий - это v1.4 (или SHA)
Git переключит репозиторий на коммит посередине между good и bad. Ты проверяешь: работает? Не работает?
# ... протестировал, баг есть ...
git bisect bad
# ... протестировал, бага нет ...
git bisect good
Git делит интервал пополам и переключает дальше. За
log2(N) шагов сходится к одному коммиту: «вот этот коммит
превратил good в bad».
git bisect bad
# a1b2c3d is the first bad commit
# commit a1b2c3d
# Author: ...
# Date: ...
#
# refactor: extract calculator class
По окончании - git bisect reset, который вернёт HEAD на
исходную ветку.
Полезные команды во время bisect:
git bisect log # лог bisect-сессии
git bisect visualize # показать оставшийся интервал
git bisect skip # пропустить коммит, который не
# удаётся проверить (сборка не идёт)
git bisect reset # выйти из bisect, вернуть HEAD
Bisect работает не только для «сломалось» vs «работает». Можно
использовать любые два слова через --term-good/--term-bad,
когда «good» звучит неуместно (например, ищешь когда фича
появилась):
git bisect start --term-old=missing --term-new=present
git bisect present
git bisect missing v1.0
10.6 git bisect run: автоматический режим
Если у тебя есть скрипт, который проверяет, сломан ли проект на текущем коммите, bisect делает всю работу сам.
git bisect start
git bisect bad
git bisect good v1.4
git bisect run ./check.sh
Git будет переключать коммиты и запускать ./check.sh на
каждом. Контракт скрипта:
- exit 0 → коммит good (тест прошёл)
- exit 1..124, 126..127 → коммит bad (тест упал)
- exit 125 → пропустить коммит (как
git bisect skip)
Все остальные коды - фатальная ошибка, bisect остановится.
Пример с pytest:
git bisect run pytest tests/test_login.py::test_password_reset
Pytest возвращает 0 при успехе, 1 при провале - bisect это понимает естественно. Через 5-10 минут получишь точный SHA коммита-источника.
Когда полезно скрипт писать самому:
# check.sh
#!/usr/bin/env bash
set -e
# сборка может сломаться сама по себе - пропускаем
make build || exit 125
# запускаем конкретный сценарий
./run-app &
APP_PID=$!
sleep 2
RESULT=$(curl -s http://localhost:8000/api/health)
kill $APP_PID
if [[ "$RESULT" == "ok" ]]; then
exit 0
else
exit 1
fi
Это и есть «автоматическая регрессия за 10 минут на репозитории с десятком тысяч коммитов». Реальный пример: ядро Linux использует bisect для нахождения регрессий, и в среднем хватает 15-20 шагов на историю в сотни тысяч коммитов.
10.6.1 Копнуть глубже: когда bisect не помогает
Bisect работает на предположении, что свойство монотонно: если коммит good - все коммиты до него тоже good, если bad - все после bad. На практике это не всегда так.
Промежуточные коммиты не собираются. Если в истории есть
коммиты, которые не компилируются (а такие случаются - за это
ругают и любят атомарность из главы 7), bisect не сможет их
протестировать. Решение - git bisect skip в ручном режиме
или exit code 125 в автоматическом.
Перемежающаяся регрессия. Если баг возник, исчез, возник снова, bisect найдёт какую-то границу, но не первую. В длинной истории с шумом bisect даёт ложноположительный. Лечится точностью теста: чем меньше тест зависит от других коммитов, тем чище bisect.
Зависимость от внешнего окружения. Если коммит работает только с определённой версией зависимости, а ты bisect'ишь без переустановки зависимостей - тест может падать не из-за коммита, а из-за окружения. Включи переустановку в скрипт.
Test-driven regression. Иногда регрессию вообще не видно
на тестах того времени - её добавили позже. Тогда bisect
бесполезен; нужно вручную смотреть git log -S, искать по
авторам, искать в blame. Bisect только для «было - нет, стало -
есть» детерминированных случаев.
В целом bisect - это полпроцента ситуаций, где он применим, и в этих полпроцентах он экономит часы. Если применим, его применять надо обязательно.
10.7 Поиск через комбинации инструментов
Самые сильные сценарии расследования - это связки. Парочка примеров.
«Кто добавил эту константу - может, помнит зачем».
# 1. Найти коммит, где появилась константа
git log -S "MAX_RETRIES = 7" --oneline
# a1b2c3d: refactor: tune retry policy
# 2. Посмотреть, что вообще в этом коммите происходило
git show a1b2c3d
# 3. Если автор всё ещё в команде - спросить
«В этом файле сейчас бага - когда он сломался?».
# 1. Найти строку в blame
git blame src/api.py | grep "raise ApiError"
# 2. Посмотреть коммит, который её добавил
git show <sha>
# 3. Если это и есть источник - готово.
# Иначе - bisect или log -S по этой строке.
«Кто-то закоммитил console.log в продакшен».
git log -S "console.log" --all --since="1 month ago" -p
Покажет все коммиты за последний месяц, в которых появилось
или исчезло console.log - даже если они уже мерджились,
ребейзились, и т.д.
«В архивной ветке был полезный кусок кода, не помню где».
git log --all -S "def calculate_discount" -p
--all ищет по всем веткам и tag'ам, не только по текущей.
Полезно, когда работа была сделана в боковой ветке, не
смерженной в main.
Эти связки полезно держать в шпаргалке. Чаще всего нужны они
три: log -S, blame, bisect. Остальные - по случаю.
Уроки в sandbox
lab-10.1. Найти регрессию через bisect run
Цель - собственноручно воспроизвести «нашли регрессию за десять шагов в истории на сотню коммитов». Используем bisect run с простым тест-скриптом.
Создай репозиторий, сделай начальный коммит с простой функцией: `mkdir bisect-lab && cd bisect-lab && git init && cat > calc.sh << 'EOF' #!/usr/bin/env bash
returns 42 always
echo 42 EOF chmod +x calc.sh && git add calc.sh && git commit -m "initial calc"`.
Создай тест:
cat > test.sh << 'EOF' #!/usr/bin/env bash result=$(./calc.sh) [[ "$result" == "42" ]] EOF chmod +x test.sh && git add test.sh && git commit -m "add test".Сделай 20 "безобидных" коммитов с переименованиями переменных, добавлением комментариев и т.п. - главное, чтобы тест продолжал проходить:
for i in $(seq 1 20); do echo "# comment $i" >> calc.sh && git commit -am "chore: cosmetic $i"; done.Сломай функцию в одном коммите:
sed -i 's/echo 42/echo 43/' calc.sh && git commit -am "refactor: tune output". Теперь тест провалится.Сделай ещё 10 коммитов с косметикой, чтобы регрессия "уехала" в прошлое:
for i in $(seq 21 30); do echo "# late comment $i" >> calc.sh && git commit -am "chore: cosmetic $i"; done.Проверь, что тест сейчас падает:
./test.sh; echo $?- должно быть 1.Запусти bisect:
git bisect start && git bisect bad HEAD && git bisect good HEAD~30. Git переключит на средний коммит и подскажет, что осталось 14-15 ревизий и примерно 4 шага (log2(30) ≈ 5).Запусти автоматический поиск:
git bisect run ./test.sh. Git будет переключать коммиты, запускать тест, делить интервал. Через секунды найдёт коммит "refactor: tune output".Выйди из bisect:
git bisect reset. Проверьgit show <sha-плохого-коммита>- увидишь конкретную правку, которая сломала.Эксперимент с
log -S: запустиgit log -S "echo 4" --oneline. Должен найти коммиты, где появилось/изменилосьecho 4.... Сравни - это тот же коммит, что нашёл bisect, только без 30 шагов проверки.
sandbox с автопроверкой - открыть в песочнице
Резюме
- `git log` поддерживает богатый набор фильтров: `--author`, `--since`, `--grep`, `--no-merges`, `-- <path>`. Комбинируются - ответ на «кто-когда-где» сужается до секунд.
- Pickaxe (`git log -S`) ищет коммиты, в которых количество вхождений строки изменилось. Самый точный способ найти «когда появилась/исчезла эта строка». `-G` - то же, но через regex без подсчёта вхождений.
- `git blame` показывает построчное происхождение файла: SHA, автор, дата каждой строки. Опция `--ignore-rev` (или `.git-blame-ignore-revs`) пропускает форматирующие коммиты.
- `git bisect` - бинарный поиск регрессии. `start`/`bad`/`good`, потом отвечаешь bad/good на каждом шаге. Сходится за `log2(N)` шагов даже на десятках тысяч коммитов.
- `git bisect run <script>` автоматизирует: скрипт возвращает 0 (good), 1 (bad), 125 (пропустить). С pytest работает напрямую.
- Bisect требует монотонности (если был good, все раньше - тоже good). Не помогает при перемежающихся регрессиях, поломанных промежуточных сборках без skip, зависимости от окружения.
- Сильнее всего инструменты в связках: `log -S` для поиска источника, `blame` для понимания контекста, `bisect` когда нужна точная граница.
Контрольные вопросы
В чём разница между `git log -S "foo"` и `git log --grep "foo"`?
Показать ответ
--grep "foo"ищет в сообщениях коммитов - «коммиты, в чьём сообщении есть foo».-S "foo"ищет в содержимом изменений - «коммиты, после которых количество вхождений foo в файлах изменилось». Часто нужен именно-S, потому что сообщения коммитов могут не упоминать конкретные символы, а в diff'е они точно есть. Например, «когда добавили класс PaymentProcessor» решается черезgit log -S "class PaymentProcessor", не через grep по сообщениям.Я знаю, что регрессия появилась между v1.4 (хорошо) и сейчас (плохо). Истории 500 коммитов. Сколько шагов bisect мне понадобится?
Показать ответ
log2(500) ≈ 9. Девять проверок, каждая длится столько, сколько длится твой тест. Если тест полминуты - bisect займёт 5 минут. Если есть автоматический скрипт, который Git может сам запускать- пусть это
git bisect run, и весь процесс будет безучастный. Без bisect ты бы листал коммиты вручную, ориентируясь на сообщения, и тратил бы часы. С bisect - десять минут, конкретный SHA.
- пусть это
Файл переименован в коммите a1b2c3d. `git log file.py` показывает историю только после переименования. Как увидеть до?
Показать ответ
git log --follow file.py. Флаг--followотслеживает переименования: Git восстанавливает, как файл назывался раньше, и продолжает историю в обратную сторону. Работает только для одного пути - массово--followнельзя. Аналогично дляgit blameесть опции-C(track copies across files) и-M(track movements within a file): они помогают понять «откуда сюда переехала эта функция», когда копи-пастили или перемещали код между файлами.Как найти коммит, который удалил функцию `def discountForSeniors`?
Показать ответ
Добавь SHA этого коммита в
.git-blame-ignore-revsв корне репозитория (один SHA на строку), и настрой Git:git config blame.ignoreRevsFile .git-blame-ignore-revs. Теперьgit blameбудет «пропускать» этот коммит при поиске строки - показывать предыдущий, осмысленный. GitHub автоматически уважает этот файл в web-blame, IntelliJ/VSCode-плагины тоже поддерживают. Делай так для всех форматирующих/массовых коммитов (миграции imports, переход на новый styleguide, и т.п.)- blame резко становится полезнее.