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)
На большом файле выводить 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
История строки 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 по предку и эволюция функции через 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
Второй приём мощнее - git log -L. Он не блеймит срез, а показывает всю эволюцию диапазона строк или функции, коммит за коммитом, с дифами на каждом шаге:
Код: Выделить всё
$ git log -L 41,44:src/auth.py
$ git log -L ':verify_token:src/auth.py'
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
-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
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>
Код: Выделить всё
$ 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>
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
Код: Выделить всё
$ git push origin refs/notes/commits
$ git fetch origin 'refs/notes/*:refs/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 отвечает на вопрос "какой контекст я упускаю", а не "кого наказать". Раскапывай решения.