linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть III — Ежедневный Git

$ глава 10 · 55 минут

Расследование истории

Git хранит всю историю проекта в обходимом виде. Это значит, что на любой вопрос «кто, когда, зачем и какой ценой» есть точный ответ - нужно только знать команды.

Эта глава - про четыре главных инструмента расследования: log с фильтрами, blame, bisect, и поиск по содержимому через log -S. С ними можно отлаживать на уровне истории, а не только на уровне кода. «Когда эта функция перестала работать?» и «кто и зачем добавил этот файл?» становятся вопросами с конкретным SHA в ответе.

10.1 git log: больше, чем oneline

Самая базовая команда - git log. Без аргументов выводит коммиты с автором, датой, полным сообщением. Это годится, чтобы посмотреть «что было недавно», но для расследования нужны другие форматы.

bash
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:

bash
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:

bash
git config --global alias.lg \
  "log --graph --oneline --decorate --all"

После этого git lg - однокомандный обзор истории.

Кастомный формат через --pretty:

bash
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 поддерживает фильтры. Самые частые:

bash
# По автору
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»:

bash
git log --author="Dmitry" \
        --since="3 months ago" \
        --grep="^fix" \
        -- src/auth/

Это типичный paragraph любого исследования. Запомнил пару ключевых фильтров - и поиск занимает секунды вместо минут листания.

10.3 Поиск по содержимому: -S и -G

Самый недооценённый инструмент Git - pickaxe (буквально «кирка»). Это поиск по тому, что появилось или исчезло в коммитах.

git log -S "<строка>" - найти коммиты, после которых количество вхождений строки в любом файле изменилось. Иначе говоря - коммиты, которые добавили или удалили эту строку.

bash
git log -S "calculateTotal"
# покажет коммиты, в которых количество "calculateTotal"
# в файлах поменялось

Это не просто grep по diff'ам. -S точно знает, что строка «появилась» или «пропала», и игнорирует случаи, когда строка просто переехала из одной части файла в другую (там количество вхождений не изменилось).

Типичный сценарий - нашёл странную константу или функцию, хочешь понять, кто её добавил:

bash
git log -S "MAGIC_TIMEOUT = 8675"
# один или несколько коммитов
# дальше git show <sha> - и видишь полный контекст

-G "<regex>" - похоже, но через регулярное выражение и без хитрости с подсчётом вхождений. Просто «коммиты, у которых diff матчит regex». Чаще нужен -S (он точнее), -G - если ищешь по шаблону.

bash
git log -G "TODO\(.*\)"

Полезный довесок - флаг -p показывает diff:

bash
git log -S "calculateTotal" -p
# увидишь, в каких коммитах добавилось/убралось,
# с полным контекстом каждой правки

git log -S уверенно находит «откуда взялась эта функция» за одну команду в репозиториях с десятком тысяч коммитов. Знание этого экономит часы.

10.4 git blame: построчное происхождение

git blame <file> показывает для каждой строки файла: SHA последнего коммита, который её тронул, автора и дату.

bash
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 не «обвиняет», просто атрибутирует.

Полезные опции:

bash
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:

bash
git blame --ignore-rev <sha-форматера> file.py

Или в проектах есть файл .git-blame-ignore-revs со списком «коммитов-форматеров», которые надо проигнорировать. Настроить Git, чтобы он его подхватывал:

bash
git config --global blame.ignoreRevsFile .git-blame-ignore-revs

GitHub автоматически уважает .git-blame-ignore-revs в корне репозитория - в blame через веб эти коммиты пропускаются.

См. blame.

10.5 git bisect: бинарный поиск регрессии

Самый мощный инструмент расследования багов. Сценарий: «раньше работало, теперь нет, в истории сотни коммитов». git bisect за log(N) шагов находит первый коммит, где сломалось.

bash
git bisect start
git bisect bad                # текущий - сломанный
git bisect good v1.4          # известный рабочий - это v1.4 (или SHA)

Git переключит репозиторий на коммит посередине между good и bad. Ты проверяешь: работает? Не работает?

bash
# ... протестировал, баг есть ...
git bisect bad
# ... протестировал, бага нет ...
git bisect good

Git делит интервал пополам и переключает дальше. За log2(N) шагов сходится к одному коммиту: «вот этот коммит превратил good в bad».

bash
git bisect bad
# a1b2c3d is the first bad commit
# commit a1b2c3d
# Author: ...
# Date:   ...
#
#     refactor: extract calculator class

По окончании - git bisect reset, который вернёт HEAD на исходную ветку.

Полезные команды во время bisect:

bash
git bisect log              # лог bisect-сессии
git bisect visualize        # показать оставшийся интервал
git bisect skip             # пропустить коммит, который не
                            # удаётся проверить (сборка не идёт)
git bisect reset            # выйти из bisect, вернуть HEAD

Bisect работает не только для «сломалось» vs «работает». Можно использовать любые два слова через --term-good/--term-bad, когда «good» звучит неуместно (например, ищешь когда фича появилась):

bash
git bisect start --term-old=missing --term-new=present
git bisect present
git bisect missing v1.0

10.6 git bisect run: автоматический режим

Если у тебя есть скрипт, который проверяет, сломан ли проект на текущем коммите, bisect делает всю работу сам.

bash
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:

bash
git bisect run pytest tests/test_login.py::test_password_reset

Pytest возвращает 0 при успехе, 1 при провале - bisect это понимает естественно. Через 5-10 минут получишь точный SHA коммита-источника.

Когда полезно скрипт писать самому:

bash
# 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 Поиск через комбинации инструментов

Самые сильные сценарии расследования - это связки. Парочка примеров.

«Кто добавил эту константу - может, помнит зачем».

bash
# 1. Найти коммит, где появилась константа
git log -S "MAX_RETRIES = 7" --oneline
# a1b2c3d: refactor: tune retry policy
# 2. Посмотреть, что вообще в этом коммите происходило
git show a1b2c3d
# 3. Если автор всё ещё в команде - спросить

«В этом файле сейчас бага - когда он сломался?».

bash
# 1. Найти строку в blame
git blame src/api.py | grep "raise ApiError"
# 2. Посмотреть коммит, который её добавил
git show <sha>
# 3. Если это и есть источник - готово.
#    Иначе - bisect или log -S по этой строке.

«Кто-то закоммитил console.log в продакшен».

bash
git log -S "console.log" --all --since="1 month ago" -p

Покажет все коммиты за последний месяц, в которых появилось или исчезло console.log - даже если они уже мерджились, ребейзились, и т.д.

«В архивной ветке был полезный кусок кода, не помню где».

bash
git log --all -S "def calculate_discount" -p

--all ищет по всем веткам и tag'ам, не только по текущей. Полезно, когда работа была сделана в боковой ветке, не смерженной в main.

Эти связки полезно держать в шпаргалке. Чаще всего нужны они три: log -S, blame, bisect. Остальные - по случаю.

Уроки в sandbox

lab-10.1. Найти регрессию через bisect run

Цель - собственноручно воспроизвести «нашли регрессию за десять шагов в истории на сотню коммитов». Используем bisect run с простым тест-скриптом.

  1. Создай репозиторий, сделай начальный коммит с простой функцией: `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"`.

  2. Создай тест: 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".

  3. Сделай 20 "безобидных" коммитов с переименованиями переменных, добавлением комментариев и т.п. - главное, чтобы тест продолжал проходить: for i in $(seq 1 20); do echo "# comment $i" >> calc.sh && git commit -am "chore: cosmetic $i"; done.

  4. Сломай функцию в одном коммите: sed -i 's/echo 42/echo 43/' calc.sh && git commit -am "refactor: tune output". Теперь тест провалится.

  5. Сделай ещё 10 коммитов с косметикой, чтобы регрессия "уехала" в прошлое: for i in $(seq 21 30); do echo "# late comment $i" >> calc.sh && git commit -am "chore: cosmetic $i"; done.

  6. Проверь, что тест сейчас падает: ./test.sh; echo $? - должно быть 1.

  7. Запусти bisect: git bisect start && git bisect bad HEAD && git bisect good HEAD~30. Git переключит на средний коммит и подскажет, что осталось 14-15 ревизий и примерно 4 шага (log2(30) ≈ 5).

  8. Запусти автоматический поиск: git bisect run ./test.sh. Git будет переключать коммиты, запускать тест, делить интервал. Через секунды найдёт коммит "refactor: tune output".

  9. Выйди из bisect: git bisect reset. Проверь git show <sha-плохого-коммита> - увидишь конкретную правку, которая сломала.

  10. Эксперимент с 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` когда нужна точная граница.

Контрольные вопросы

  1. В чём разница между `git log -S "foo"` и `git log --grep "foo"`?

    Показать ответ

    --grep "foo" ищет в сообщениях коммитов - «коммиты, в чьём сообщении есть foo». -S "foo" ищет в содержимом изменений - «коммиты, после которых количество вхождений foo в файлах изменилось». Часто нужен именно -S, потому что сообщения коммитов могут не упоминать конкретные символы, а в diff'е они точно есть. Например, «когда добавили класс PaymentProcessor» решается через git log -S "class PaymentProcessor", не через grep по сообщениям.

  2. Я знаю, что регрессия появилась между v1.4 (хорошо) и сейчас (плохо). Истории 500 коммитов. Сколько шагов bisect мне понадобится?

    Показать ответ

    log2(500) ≈ 9. Девять проверок, каждая длится столько, сколько длится твой тест. Если тест полминуты - bisect займёт 5 минут. Если есть автоматический скрипт, который Git может сам запускать

    • пусть это git bisect run, и весь процесс будет безучастный. Без bisect ты бы листал коммиты вручную, ориентируясь на сообщения, и тратил бы часы. С bisect - десять минут, конкретный SHA.
  3. Файл переименован в коммите 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): они помогают понять «откуда сюда переехала эта функция», когда копи-пастили или перемещали код между файлами.

  4. Как найти коммит, который удалил функцию `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 резко становится полезнее.
← Предыдущая09-undoСледующая →11-branching-strategies
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки