Страничный diff по строкам отлично читается в коде, но превращается в мучение, когда меняется пара слов в длинном предложении или внутри одной строки меняется один идентификатор. Строка целиком помечается удалённой, новая строка целиком помечается добавленной, и глазам приходится вручную сверять символ за символом, где именно различие. Git давно умеет решать эту задачу на уровне слов, просто эти опции почему-то остаются в тени большинства руководств. Разбираем по-настоящему рабочие сценарии: от редактуры документации и юридических текстов до охоты за переименованной переменной в коде и машинного разбора через формат porcelain.

Когда строчный diff бессилен и нужен пословный разбор

Классический вывод git diff работает гранулами в одну строку. Для документации, конфигов в формате YAML, README, текстов лицензий и коммит-сообщений это слишком грубо. Человек, правивший пунктуацию в абзаце, получает кроссворд из красных и зелёных строк, где реально изменилось два слова и запятая. Похожая беда в коде с длинными строками: SQL-запросы в одну строку, URL, строковые литералы, форматные строки.

Опция --word-diff меняет единицу сравнения со строки на слово. По умолчанию "слово" в понимании Git это последовательность непробельных символов, а границы проходят по пробельным. Вывод при этом по умолчанию остаётся текстовым и выглядит непривычно:

git diff --word-diff README.md
diff --git a/README.md b/README.md
index 3f2a1b4..9c77e20 100644
--- a/README.md
+++ b/README.md
@@ -3,1 +3,1 @@
Сервис принимает заказы [-и отправляет-]{+и складывает+} уведомления {+в очередь. Клиент получает+} ...

Разбор строки с изменением построчно: минусы в квадратных скобках обозначают удалённый фрагмент, плюсы в фигурных - добавленный. Строка "@@ -3,1 +3,1 @@" сообщает, что в блоке участвовала одна строка исходного файла и одна строка результата. Заметно, что контекст сохраняется весь: фраза не перепечатывается дважды, как в обычном diff, а показывается один раз с вкраплениями изменений. Читать такой вывод глазами сильно легче, особенно в редактуре.

У --word-diff есть и режим с цветом без скобок:

git diff --word-diff=color

Здесь удалённые слова красятся в красный, добавленные в зелёный, и никакие скобки не мусорят текст. Есть ещё промежуточные значения plain, совпадающее с поведением по умолчанию, и none, который подавляет сам word diff.

Формат porcelain для скриптов и автоматической обработки

Текст со скобками приятен человеку, но для скрипта это разбор регулярками с болью. Для машинной обработки есть --word-diff=porcelain:

git diff --word-diff=porcelain -- README.md
 Сервис
 принимает
 заказы
-и
 отправляет
+и
+складывает
 уведомления
~

Формат строчный и однозначный: пробел в начале означает неизменившееся слово, минус - удалённое, плюс - добавленное, а тильда помечает разрыв строки, вокруг которого Git не знает, к чему отнести перевод строки. Разбор построчно: первые три строки со словами "Сервис", "принимает", "заказы" начинаются с пробела, значит они общая часть. Затем идёт "-и" и " отправляет": слово "и" удалено, а "отправляет" совпало. Потом "+и", "+складывает" - два добавленных слова. Тильда в конце блока закрывает абзац.

Тонкость: в porcelain перевод строки как часть контента сливается с предыдущим словом, на что и намекает лишняя пустая строка в выводе. Парсер стоит писать жадным по строкам, а не по словам. Полный блок при этом сохраняет и заголовки hunk "@@", их удобно использовать как разделители секций.

Отдельного упоминания заслуживает связка пословного просмотра с уже подготовленными к коммиту изменениями. До того как патч уйдёт в индекс, смотрим "git diff --word-diff", а после "git add" аналогичную проверку делаем через "git diff --cached --word-diff=color". Это дисциплинирует: опечатки в документации ловятся до попадания в историю, а не во время чужого ревью. Многие разработчики замечали, что именно staged-просмотр пословно экономит больше всего нервов, потому что исправление ещё ничего не стоит.

Ещё одна практическая связка - "git show --word-diff коммит". Когда нужно разобрать чужую правку в документации, пословный просмотр одиночного коммита показывает заменённые термины без перепечатывания целых абзацев. Для поиска момента, когда фраза изменилась, сочетайте "git log -S'старый термин' --word-diff=color": флаг -S отфильтрует коммиты, где количество вхождений строки изменилось, а пословная раскраска покажет контекст каждой правки.

Типовой скриптовой приём: подсчёт, сколько слов реально поменялось в документации после какого-то рефакторинга CI-генерации.

git diff --word-diff=porcelain docs/ | grep -c '^+'

Это приблизительная метрика "объёма правки", гораздо честнее, чем количество строк, потому что перенос одной фразы в конец абзаца перестаёт считаться правкой сорока строк.

Тонкая настройка границ слов через --word-diff-regex

Пробел как единственный разделитель - слабое приближение. В коде "tokens" разделены точкой, скобкой, оператором присваивания, а не пробелом. Для этого есть --word-diff-regex, куда передаётся регулярное выражение, определяющее слово. Всё, что под него попадает, считается словом, всё остальное - неизменяемый контекст.

Найти места, где поменялся именно идентификатор, а не пробельное оформление:

git diff HEAD~3 --word-diff=porcelain --word-diff-regex='[A-Za-z_][A-Za-z0-9_]*' -- src/parser.c

Шаги: fork, время, сводка.

  1. Регулярка '[A-Za-z_][A-Za-z0-9_]*' выделяет только идентификаторы в стиле C;
  2. Поиск по HEAD~3 охватывает три последних коммита;
  3. Путь src/parser.c сужает область через двойной дефис;
  4. Формат porcelain отдаёт итог машиночитаемым.

Специальное значение regex '.' означает "посимвольный diff": каждое отличие разбивается до одного символа. Это нужно редко, но при разборе опечатки в одной букве незаменимо.

Подводный камень первый: регулярка жадно матчит, и если сделать паттерн слишком широким (например, '.*'), весь вывод превратится в одно гигантское "слово" и вся опция теряет смысл. Подводный камень второй: экранирование в shell. Одинарные кавычки почти всегда правильный выбор, иначе cash-переменные и обратные слеши схлопнутся до того, как их увидит Git.

Цветные слова без скобок через --color-words

Часто нужен компромисс: человекочитаемость color-режима и произвольная граница слов, как у --word-diff-regex. Это умеет --color-words: он эквивалентен --word-diff=color плюс --word-diff-regex с заданным паттерном, но короче в наборе.

git diff --color-words='[[:alnum:]_]+|[^[:space:]]' -- config.yaml

Паттерн '[[:alnum:]_]+|[^[:space:]]' делает две вещи: длинные последовательности букв, цифр и подчёркивания считаются словами, а каждый прочий непробельный символ (скобки, двоеточия, запятые) - отдельным словом. В результате подсвечиваются даже смены знаков препинания, что для юридических текстов и договоров в Git-репозитории критично: потерянная запятая в договоре это деньги.

Для просмотра истории по словам удобно подключать те же опции к git log:

git log -p --word-diff=color --since=2.weeks -- docs/

Здесь "-p" включает вывод патчей у каждого коммита, "--since=2.weeks" ограничивает интервал, а путь docs/ срежет шум от правок кода. Приём полезен перед релизом: быстро понять, что реально поменялось в пользовательской документации за две недели, не читая все подряд коммиты.

Ещё блок полезных алиасов в ~/.gitconfig, который закрывает обе задачи - ревью текстов и охоту за идентификаторами в коде:

[alias]
    wd = diff --word-diff=color
    wdp = diff --word-diff=porcelain
    wid = diff --word-diff=porcelain --word-diff-regex=[A-Za-z_][A-Za-z0-9_]*
    wlog = log -p --word-diff=color

Теперь "git wd docs/" показывает редакторскую правку по словам, "git wdp | wc -l" отдаёт машиночитаемый поток, а "git wid HEAD~5 -- src/" выуживает переименования переменных в любом C-подобном файле. В квадратных скобках паттерна спецсимволы вроде дефиса стоит ставить в начало или конец класса, иначе диапазоны истолкуются неожиданно.

Сравнение веток и предварительная оценка масштаба через --stat

До детального пословного разбора на этапе ревью полезно сначала прикинуть масштаб. Связка --stat и диапазона веток даёт сухую статистику, сколько файлов и строк затронуто:

git diff --stat main..feature/redesign-docs
 docs/install.md | 12 ++++++------
 docs/api.md    | 34 +++++++++++++++++++++++-----------
 2 files changed, 24 insertions(+), 22 deletions(-)

Разбор построчно: первая и вторая строка показывают файл, число затронутых строк и графическую шкалу плюсов/минусов; последняя строка - итог по коммиту, сколько файлов, сколько вставок и удалений. Шкала пропорционально сжимается, если правок много, поэтому число в начале надёжнее плюсиков.

Дальше решаем, что меняли по существу:

git range-diff main..feature/redesign-docs

range-diff полезен, когда ветка перебазировалась: он сопоставляет коммиты попарно и показывает, какие из них действительно изменились, а какие просто переехали на новую базу. Связка "--stat + range-diff + word-diff" покрывает три уровня детализации: масштаб, состав коммитов, содержание правок.

Отдельный сценарий из практики: редактура общего глоссария в ветке feature, где меняются термины внутри длинных определений. Сравнение "main..feature" с --word-diff=color показывает именно заменённые термины, а не переписанные строки целиком. На ревью такой diff читается минут за пять вместо получаса глазных сверок.

Внешние diff-инструменты, типичные ошибки и устойчивый workflow

Когда встроенного терминального вывода не хватает, Git умеет передавать сравнение внешнему движку через difftool. Настройка живёт в ~/.gitconfig:

[diff]
    tool = vimdiff
[difftool "vimdiff"]
    cmd = vimdiff "$LOCAL" "$REMOTE"
[difftool]
    prompt = false

Блок [diff] назначает инструмент по умолчанию, блок [difftool "vimdiff"] описывает команду запуска, prompt = false снимает назойливое подтверждение на каждый файл. После чего:

git difftool main..feature -- docs/

Четвёртая распространённая ошибка: путаница между двумя и тремя точками. В git diff записи "main feature" и "main..feature" равнозначны: обе сравнивают вершины двух веток напрямую. Чтобы увидеть только изменения ветки от точки её расхождения с main, нужны три точки: "main...feature". Кроме того, легко перепутать порядок аргументов и получить зеркальный diff, где добавленное и удалённое поменялись местами. Пятая: ожидание пословной точности от опции --minimal или --patience; эти флаги влияют на алгоритм строчного Myer's diff и на границы слов не действуют, их с --word-diff совмещать бессмысленно. Шестая тонкость касается файлов без перевода строки в конце: Git честно помечает такие замены маркером "No newline at end of file", и в porcelain-парсере эту служебную строку нужно игнорировать, иначе счётчики слов начнут врать.

Типичные ошибки новичков в этой области предсказуемы. Первая: пытаться распарсить обычный --word-diff (не porcelain) в скрипте и ломаться о скобки и цветовые escape-последовательности; профилактика - всегда брать porcelain или --porcelain у plumbing-команд. Вторая: забыть двойной дефис перед путём, и Git интерпретирует путь как ревизию, выдавая "ambiguous argument". Третья: регулярка для --word-diff-regex, съедающая пробелы, после чего diff теряет якоря и выглядит случайным набором плюсов и минусов.

Рабочий рекомендуемый workflow для документационной правки сводится к короткой схеме. Сначала "git diff --stat main..feature" - увидеть масштаб. Затем "git log --oneline main..feature" - карта коммитов. Дальше "git diff main..feature --word-diff=color --word-diff-regex" с паттерном под конкретный тип контента - суть правок. И наконец "git difftool" для пар подозрительных файлов, где хочется глазами сверить детали. При такой последовательности шум от форматирования исчезает, ревью фокусируется на смысловых изменениях, а история остаётся чистой и предсказуемой, что и требуется от зрелого инструментария.

Финальный совет от практикующих: держите пару готовых алиасов в ~/.gitconfig для пословного diff текстов и для кода с идентификаторами. Пять секунд настройки экономят минуты на каждом ревью, а текстовая правка перестаёт пугать красно-зелёной стеной. Пословная точность это, по сути, уважение ко времени ревьюера, и Git всё для этого уже умеет из коробки.