git blame, mailmap и археология кода

Рейтинг: 45.3% · 9 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

git blame, mailmap и археология кода

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода (вы здесь)
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Представь: прод лежит, в логах стектрейс упирается в одну строку, и эта строка делает что-то странное. Очевидный костыль? Защита от древнего бага, который все забыли? Ты открываешь файл и видишь голый код без контекста. Кто это написал? Когда? Зачем? Вот ровно для этого нужна археология кода - набор инструментов Git, которые превращают мёртвый файл в историю принятых решений. Сегодня разбираем git blame по-взрослому: не "кто виноват", а "какой контекст я упускаю". Плюс mailmap, чтобы один человек под пятью email не разваливал статистику, и git notes, чтобы навешивать смысл поверх готовой истории.

git blame: кто изменил строку в git и когда

Базовая идея проста. git blame файл проходит по каждой строке и говорит: вот этот коммит последним её трогал, вот автор, вот дата. Не "кто создал строку вообще", а "кто внёс её в текущем виде". Это важная оговорка, к ней вернёмся.

Код: Выделить всё

$ git blame src/auth.py
a3f9c1d2 (Marina Volkova 2025-11-03 14:22:10 +0300  41) def verify_token(raw):
a3f9c1d2 (Marina Volkova 2025-11-03 14:22:10 +0300  42)     if not raw:
7b2e8a01 (Pavel Orlov    2024-06-18 09:05:44 +0300  43)         return None  # legacy clients send empty
a3f9c1d2 (Marina Volkova 2025-11-03 14:22:10 +0300  44)     payload = decode(raw, LEEWAY)
Читаем слева направо: короткий хеш коммита, автор, дата с временем, номер строки в текущем файле, сам код. Строка 43 выбивается - её последним трогал Pavel в 2024, а соседние Marina в 2025. И тут же подсказка в комментарии: пустой токен от старых клиентов. Это и есть тот контекст, ради которого мы пришли.

На большом файле выводить blame целиком бессмысленно. Сужаем диапазон через -L:

Код: Выделить всё

$ git blame -L 41,44 src/auth.py
$ git blame -L 41,+4 src/auth.py
$ git blame -L '/def verify_token/,+10' src/auth.py
Первая форма - строки с 41 по 44. Вторая - от 41-й и ещё 4 строки вниз (плюс-нотация удобнее, когда не хочешь считать). Третья - от строки, совпадающей с регуляркой, и 10 строк дальше: так ты блеймишь функцию, не зная её точных номеров строк. Регулярка ищется как обычный basic regex.

История строки git через blame - это не только "последний коммит". Получив хеш a3f9c1d2, ты сразу идёшь дальше: git show a3f9c1d2 покажет весь коммит целиком - сообщение, diff, соседние изменения. Часто именно в сообщении коммита (или в номере PR/задачи в нём) лежит ответ "зачем". Связка blame -> show -> PR на хостинге - базовый рефлекс археолога. Сначала blame даёт хеш, show даёт сообщение и общий diff, а ссылка на pull request в сообщении даёт обсуждение ревью, где решение и аргументировалось.

Изображение

Главная ловушка blame и спасение через -w, -M, -C

Вот реальная боль. Кто-то прогнал автоформаттер по всему файлу, или переименовал переменную через sed, или переместил функцию в другое место. Формально все строки теперь "изменены" этим человеком. И git blame файл честно укажет на него - хотя по смыслу строки писал кто-то совсем другой годы назад. Ты копаешь контекст и упираешься в коммит "reformat with black", который ничего не объясняет. Тупик.

У Git есть три флага против этого, и профи их включают почти всегда.

-w - игнорировать изменения, которые состоят только из пробелов. Сдвинули отступ, поменяли табы на пробелы, переформатировали отступы - blame проигнорирует такой коммит и покажет того, кто менял содержательно.

-M - детект перемещённых и скопированных строк внутри того же файла. Кто-то взял блок и переставил его выше по файлу - без -M это будет "перемещавший", с -M Git найдёт исходное место и покажет настоящего автора. Порог по умолчанию - около 20 буквенно-цифровых символов, чтобы не цеплять случайные совпадения вроде return None.

-C - детект строк, приехавших из других файлов. Это сильнее и тоньше, чем -M, и работает по уровням, потому что обход дороже:
  • один -C: ищет источник среди файлов, которые менялись в том же коммите (расщепили большой файл на модули - типичный случай). Быстро.
  • два -C -C: ищет среди всех файлов родительского коммита, ловит копирование из существующего кода.
  • три -C -C -C: ищет копии по всем файлам в любом коммите. Дорого, но самый дотошный режим.
Боевая комбинация археолога выглядит так:

Код: Выделить всё

$ git blame -w -M -C -C -L 41,60 src/auth.py
Игнорируем пробелы, ловим перемещения внутри файла и копии из других файлов родителя. Сравни выхлоп без флагов и с ними - нередко половина строк меняет "автора" с того, кто двигал код, на того, кто его реально придумал. Цена - blame с -C -C считается заметно дольше на больших файлах, так что L-диапазон тут не роскошь, а необходимость.

Движение во времени: blame по предку и эволюция функции через git log -L

Обычный blame показывает срез "как сейчас". Но строку могли переписать несколько раз, и тебе нужна предыдущая редакция. Тут два мощных приёма.

Первый - блеймить не текущее состояние, а конкретный коммит или его предка. git blame принимает ревизию:

Код: Выделить всё

$ git blame a3f9c1d2 -- src/auth.py
$ git blame a3f9c1d2^ -- src/auth.py
$ git blame v2.3.0 -- src/auth.py
Запись commit^ означает "родитель этого коммита", то есть состояние до изменения. Логика типичной раскопки такая: blame показал хеш, ты блеймишь этого предка (хеш^), видишь, кто и что было там, опять берёшь хеш, опять блеймишь предка - и так слой за слоем разматываешь историю строки git вглубь, пока не дойдёшь до первоисточника. Двойные дефисы перед именем файла - не украшение: они отделяют ревизию от пути, иначе Git может запутаться, когда имя файла совпадает с именем ветки.

Второй приём мощнее - git log -L. Он не блеймит срез, а показывает всю эволюцию диапазона строк или функции, коммит за коммитом, с дифами на каждом шаге:

Код: Выделить всё

$ git log -L 41,44:src/auth.py
$ git log -L ':verify_token:src/auth.py'
Первая форма следит за строками 41-44. Вторая - за функцией verify_token: Git сам находит её границы по языку и показывает каждое изменение тела функции через всю историю, даже когда номера строк уезжали. Это лучший способ ответить "как эта функция дошла до жизни такой". В Git 2.54 (конец 2025) log -L переписали так, что его вывод теперь идёт через стандартный diff-конвейер - и впервые стали работать опции форматирования патчей и pickaxe-поиск вместе с -L. Раньше -L был на своём отдельном пути и многое игнорировал.

Pickaxe: когда строка появилась или исчезла (-S и -G)

Blame отвечает "кто трогал строку, которая есть сейчас". А если строки уже нет? Удалили вызов, выпилили фичу-флаг, и ты хочешь найти коммит, где это произошло. Blame тут бесполезен - строки в текущем файле нет. Нужен pickaxe, "кирка".

-S ищет коммиты, где поменялось число вхождений заданной строки. То есть где строку добавили или удалили:

Код: Выделить всё

$ git log -S 'LEEWAY' --oneline -- src/auth.py
e4d1a90 introduce clock skew leeway for jwt
b71c2f5 drop LEEWAY, switch to strict exp check
Видишь оба события - и появление, и исчезновение константы. Дальше git show по любому из хешей даёт полный контекст. Это самый быстрый способ ответить "когда вообще появилась эта штука" и "кто и почему её выпилил".

-G - брат -S, но принимает регулярку и показывает коммиты, где любая строка дифа совпала с шаблоном, даже если общее число вхождений не изменилось (например, строку отредактировали, а не добавили/удалили). -G шире и шумнее, -S точнее для "появилось/исчезло". Добавь --pickaxe-regex к -S, если хочешь, чтобы и -S трактовал аргумент как регулярку. Не путай pickaxe с git grep: grep ищет по рабочему дереву или одному снимку, pickaxe ищет по дифам всей истории - это разные инструменты.

git log --follow: blame через переименование файла

Ещё одна типичная стена. Файл переименовали, а git log по нему обрывается на коммите переименования - дальше пустота, будто история началась там. На самом деле она есть, просто log по умолчанию не идёт через смену имени. Лечится флагом --follow:

Код: Выделить всё

$ git log --follow --oneline -- src/auth.py
$ git log --follow -p -- src/auth.py
--follow заставляет log перейти границу переименования и продолжить историю под старым именем. Работает только с одним файлом за раз - это ограничение по дизайну. Хорошая новость: blame с -C и log -L переименования отслеживают и сами, потому что под капотом гоняют ту же эвристику обнаружения переименований, что и обычный diff с rename detection.

git mailmap: нормализация авторов под разными email

Реальность любой команды: человек коммитил с рабочей почты, потом с личной, потом с ноута, где забыл настроить git config, и подписался "marina" вместо "Marina Volkova". В итоге shortlog показывает пятерых "разных" людей вместо одного, blame путается в авторстве, статистика врёт. Решение - git mailmap.

Создаёшь в корне репозитория файл .mailmap (он коммитится в репо, общий для всех) и описываешь правила сопоставления:

Код: Выделить всё

# каноничное_имя <каноничный_email> <старый_email>
Marina Volkova <marina@cyberlake.ru> <marina@gmail.com>
Marina Volkova <marina@cyberlake.ru> marina <m.volkova@old-corp.ru>
Pavel Orlov <pavel@cyberlake.ru> <orlov@laptop.local>
Первая строка: коммиты с marina@gmail.com показывать как Marina Volkova с рабочей почтой. Вторая: и автора с именем marina и почтой m.volkova@old-corp.ru тоже свести к канону. Синтаксис гибкий: можно матчить по email, по паре имя+email, схлопывать имена. После этого git mailmap прозрачно применяется в blame, shortlog и log:

Код: Выделить всё

$ git shortlog -sne
   142  Marina Volkova <marina@cyberlake.ru>
    89  Pavel Orlov <pavel@cyberlake.ru>
$ git log --use-mailmap
$ git check-mailmap "marina" "<marina@gmail.com>"
Marina Volkova <marina@cyberlake.ru>
shortlog и blame подхватывают .mailmap автоматически. У log есть флаг --use-mailmap (и опция log.mailmap=true в конфиге, чтобы включить везде). check-mailmap проверяет конкретное правило, не гадая. Ключевое: .mailmap не переписывает историю - реальные коммиты не меняются, это слой отображения поверх них. Поэтому добавить mailmap безопасно и обратимо, в отличие от переписывания истории. Это первый пункт в любом аудите репозитория: завести .mailmap и навести порядок в авторах.

git notes: заметки поверх истории и где они лежат

Иногда нужно добавить к старому коммиту информацию, которой там не было: ссылку на инцидент, результат ревью, пометку "этот коммит причина регрессии". Менять сообщение коммита нельзя - это меняет хеш и переписывает всю историю после него. Для этого есть git notes - заметки, которые живут рядом с коммитами, не трогая их.

Код: Выделить всё

$ git notes add -m "Причина инцидента INC-481, откат в e4d1a90" a3f9c1d2
$ git log -1 a3f9c1d2
commit a3f9c1d2...
Author: Marina Volkova ...

    refresh token validation

Notes:
    Причина инцидента INC-481, откат в e4d1a90
Заметка показывается в log/show отдельным блоком Notes, но хеш коммита a3f9c1d2 не изменился ни на бит. Где это хранится: notes - это обычные объекты Git, на которые указывает ссылка refs/notes/commits (дефолтное пространство имён). Можно держать несколько раздельных наборов заметок через --ref, например refs/notes/review для ревью и refs/notes/ci для результатов сборки. Важная грабля: notes не передаются автоматически при push и fetch. refs/notes/commits надо гонять явно:

Код: Выделить всё

$ git push origin refs/notes/commits
$ git fetch origin 'refs/notes/*:refs/notes/*'
Из-за этого notes хороши для автоматики (бот вешает результаты CI на коммит) и хуже для ручной командной коммуникации - там обсуждение лучше живёт в PR. Но как археологический слой, который не ломает хеши, notes незаменимы.

Этика blame: инструмент контекста, а не суда

Слово blame ("винить") - историческая неудача нейминга, и она задаёт неверный тон. Хорошие команды используют blame, чтобы понять почему код такой, а не чтобы найти стрелочника. Строка, которая выглядит глупо в 2026, могла быть единственно верной в 2023 при тех ограничениях. Прежде чем писать "кто это вообще придумал" - прочитай сообщение коммита и PR: в девяти случаях из десяти там разумная причина, а ты просто не знал контекста. Именно поэтому в некоторых хостингах кнопку переименовали в "annotate". Технически это тот же blame, но психологически здоровее. Раскапывай решения, а не людей.

Мини-лаба: раскопай свою строку за 8 шагов

Повтори руками в любом своём репозитории с историей:
  • Выбери подозрительный файл и сделай git blame -L 1,20 path - посмотри авторов и даты верхних строк.
  • Прогони тот же диапазон с git blame -w -M -C -C -L 1,20 path и сравни: где "автор" сменился? Это и были перемещения/форматирование.
  • Возьми хеш одной строки и сделай git show <хеш> - прочитай сообщение коммита, найди контекст.
  • Блейми предка: git blame <хеш>^ -- path. Кто был автором до этого изменения?
  • Проследи функцию: git log -L ':имя_функции:path' и пройди её эволюцию сверху вниз.
  • Найди появление/исчезновение строки: git log -S 'какая_то_константа' --oneline -- path.
  • Если файл переименовывали - git log --follow --oneline -- path, убедись, что история не обрывается.
  • Создай .mailmap с одной строкой про себя (старый и новый email) и сверь git shortlog -sne до и после.
Контрольные вопросы
  • Чем -M отличается от -C в git blame и почему -C иногда указывают дважды?
  • Файл переформатировали автоформаттером, и blame показывает на того, кто прогнал формат. Какие два флага вернут реальных авторов?
  • Строки больше нет в файле - её удалили полгода назад. Каким инструментом найти коммит удаления и почему blame тут не поможет?
  • Меняет ли .mailmap хеши коммитов? А git notes add? Где физически хранятся notes?
Итог

Археология кода - это не одна команда, а связка. git blame с -w -M -C -C ищет настоящего автора строки, а не того, кто двигал файл. git log -L разматывает эволюцию функции, pickaxe -S/-G ловит появление и исчезновение кода, --follow перешагивает переименования. git mailmap наводит порядок в авторах, не трогая историю, а git notes навешивает смысл поверх готовых коммитов через refs/notes. И главное правило: blame отвечает на вопрос "какой контекст я упускаю", а не "кого наказать". Раскапывай решения.
👍4 ❤️2 🔥 😄 🤔1
Аватара пользователя
timbod
Сообщения: 1
Зарегистрирован: 31 май 2026, 16:30

Re: git blame, mailmap и археология кода

Сообщение timbod »

Лет десять блеймил без -M -C и каждый раз упирался в коммит reformat with black. Спасибо, теперь понятно почему
👍2 ❤️1 🔥 😄 🤔1
Аватара пользователя
bebela
Сообщения: 1
Зарегистрирован: 01 июн 2026, 04:01

Re: git blame, mailmap и археология кода

Сообщение bebela »

Вопрос: а можно blame -C -C -C на весь монорепо запустить или он там умрёт по времени? Чувствую что умрёт
👍3 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
git bisect: бинарный поиск регрессии
Следующая глава →
Теги, релизы, archive и bundle

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git blame: кто изменил строкуgit bisect: найти коммит с багом

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость