DevToys Web Pro iconDevToys Web ProБлог
Переведено с помощью LocalePack logoLocalePack
Оцените нас:
Попробуйте расширение для браузера:
← Назад к блогу

Сравнение текстов (diff): файлы, конфиги и код как настоящий разработчик

13 мин чтения

Diff-инструмент отвечает на один вопрос: что изменилось? Ревью пулл-реквеста, сравнение двух конфигов, поиск регрессии, аудит изменений деплой-скрипта — diff является одной из самых частых операций в рабочем дне разработчика. Это руководство объясняет, как работают diff-алгоритмы, как читать их вывод, когда использовать каждый уровень детализации и практические примеры. Используйте Сравнение текстов по ходу чтения.

Как работает diff: алгоритм Майерса

Стандартный алгоритм diff, используемый в git diff, GNU diff и большинстве современных инструментов — это алгоритм Майерса, опубликованный Юджином Майерсом в 1986 году. Он решает задачу наибольшей общей подпоследовательности (LCS): найти максимальное множество строк, присутствующих в обоих текстах в одинаковом порядке, затем отметить всё остальное как добавленное или удалённое.

Рассмотрим две версии конфигурационного файла:

# Версия A                    # Версия B
host: localhost               host: db.prod.example.com
port: 5432                    port: 5432
database: myapp_dev           database: myapp_prod
user: admin                   user: app_user
password: secret              password: ••••••••
                              ssl: true
                              pool_size: 10

Алгоритм Майерса находит наибольшую последовательность общих строк (port: 5432 и структурные элементы), затем сообщает минимальный набор изменений для преобразования A в B. Цель всегда — кратчайший сценарий правок: минимум вставок и удалений.

Чтение вывода diff

Unified diff (унифицированный формат)

Unified diff — это то, что выводит git diff. Именно этот формат вы видите в пулл-реквестах. Каждая изменённая секция (называемая hunk) показывает несколько строк контекста вокруг изменения:

--- a/config.yml
+++ b/config.yml
@@ -1,5 +1,7 @@
-host: localhost
+host: db.prod.example.com
 port: 5432
-database: myapp_dev
+database: myapp_prod
-user: admin
+user: app_user
-password: secret
+password: ••••••••
+ssl: true
+pool_size: 10

Как читать:

  • --- — оригинальный файл, +++ — новый файл
  • @@ -1,5 +1,7 @@: оригинал начинается со строки 1, показывает 5 строк; новая версия начинается со строки 1, показывает 7 строк
  • Строки с - — удалены
  • Строки с + — добавлены
  • Строки без префикса — контекст (не изменились)

Вид «бок о бок» (side-by-side)

Side-by-side показывает оригинал и изменённый текст в двух колонках. Удобнее для прозы и конфигов, когда нужно видеть старое и новое значение рядом. Инструмент Сравнение текстов рендерит diff именно так — лучше для понимания, что изменилось с чего на что.

Детализация diff: по строкам, словам, символам

Одни и те же два текста можно сравнивать с разной детализацией. Каждый уровень подходит для своих задач:

ДетализацияЕдиница сравненияЛучше всего для
По строкамЦелые строкиКод, конфиги, логи — всё структурированное построчно
По словамТокены, разделённые пробеламиДокументация, длинные строки с мелкими правками
По символамОтдельные символыПоиск опечаток, односимвольные изменения в ID и флагах

Построчный diff может вводить в заблуждение, когда в длинной строке меняется одно слово — вся строка показывается как удалённая и добавленная заново. Пословное или посимвольное сравнение сразу показывает, что изменилось:

# Построчный diff — тяжело заметить изменение:
- timeout: 30000
+ timeout: 300000

# Пословный/посимвольный diff — сразу очевидно:
  timeout: [30000  300000]

Сравнение YAML: частый источник путаницы

YAML-сравнение заслуживает отдельного внимания, поскольку неявная типизация YAML создаёт сюрпризы. Два YAML-файла могут быть семантически идентичны, но текстуально различаться, и наоборот.

Пробелы и отступы

YAML использует отступы для структуры, а не фигурные скобки. Diff, показывающий только изменения пробелов, может на самом деле представлять структурное изменение:

# Версия A               # Версия B
servers:                 servers:
  - name: web            - name: web
    port: 80               port: 80
  - name: api            - name: api
    port: 8080               port: 8080
                         #   ↑ Лишний отступ: теперь вложено в 'api'

Изменения типов значений

Неявная типизация YAML означает, что строка "true" и булево значение true похожи в текстовом diff, но ведут себя по-разному в коде. Diff-инструмент показывает текстовую разницу, но не предупреждает о семантических изменениях типов. Подробнее в статье Подводные камни неявной типизации YAML.

Раскрытие якорей и псевдонимов

# Версия A — использует якорь
defaults: &defaults
  timeout: 30
  retries: 3

service_a:
  <<: *defaults
  port: 8080

# Версия B — раскрыто встроенно
service_a:
  timeout: 30
  retries: 3
  port: 8080

Эти два файла семантически эквивалентны, но текстовый diff покажет их как полностью разные. При сравнении YAML решите: нужен текстовый или семантический diff (парсинг обоих файлов и сравнение структур данных).

Практические кейсы

Аудит конфигурационных файлов

Перед деплоем в продакшн сравните конфиги staging и production, чтобы найти environment-специфичные значения, которые могли случайно остаться захардкоженными:

# В командной строке
diff staging.env production.env

# Или через git
git diff staging..production -- config/

Что искать: хосты БД, указывающие на dev, включённые debug-флаги, различающиеся rate-limit значения между окружениями.

Ревью пулл-реквестов

При ревью PR именно unified diff показывают GitHub, GitLab и Bitbucket. Советы для эффективного чтения:

  • Начинайте со списка файлов. Понимание того, что изменилось, даёт контекст до чтения строк.
  • Читайте строки контекста. Неизменённые строки вокруг hunk показывают, в какой функции или блоке находится изменение.
  • Обнаружение переименований. Git определяет переименования — файл, показанный как удалённый и добавленный с высокой схожестью, скорее всего перемещён, а не переписан.
  • Игнорируйте пробелы. git diff -w убирает изменения только в пробелах, которые засоряют diff без изменения логики.

Отладка регрессий

Когда функция работала на прошлой неделе, но сломалась сегодня, git bisect + diff сужает круг поиска:

# Найти коммит, который сломал
git bisect start
git bisect bad HEAD
git bisect good v1.2.0

# После того как bisect найдёт коммит-виновник:
git show <commit-hash>
# Показывает diff именно того, что изменилось в этом коммите

Сравнение API-ответов

Вставьте два JSON-ответа в Сравнение текстов, чтобы увидеть, что изменила новая версия API. Сначала отформатируйте оба через JSON Formatter — это нормализует порядок ключей и отступы, и diff покажет только реальные изменения данных, а не форматирование.

# Нормализация JSON для чистого diff
cat response_v1.json | jq --sort-keys . > v1_sorted.json
cat response_v2.json | jq --sort-keys . > v2_sorted.json
diff v1_sorted.json v2_sorted.json

Ревью миграций баз данных

Перед запуском миграции в продакшн сравните сгенерированный SQL с предыдущей миграцией:

diff migrations/0042_add_users.sql migrations/0043_add_roles.sql

Что проверять: неожиданные операторы DROP, изменения типов колонок в больших таблицах, изменения индексов, которые заблокируют таблицу.

Полезные опции git diff

КомандаЧто делает
git diffНезафиксированные изменения в рабочей директории
git diff --stagedStaged-изменения (что попадёт в коммит)
git diff HEAD~1Изменения с последнего коммита
git diff main..featureВсе изменения в feature, которых нет в main
git diff -wИгнорировать все изменения пробелов
git diff --word-diffПословный diff вместо построчного
git diff --statСводка: файлы, вставки, удаления
git diff -- path/to/fileDiff только конкретного файла

Программный diff в Node.js

Для построения diff-интерфейсов в приложениях стандартная библиотека — diff:

npm install diff
import { diffLines, diffWords, diffChars, createPatch } from 'diff';

const oldText = 'host: localhost
port: 5432
';
const newText = 'host: db.prod.example.com
port: 5432
';

// Построчный diff
const lineDiff = diffLines(oldText, newText);
for (const part of lineDiff) {
  const symbol = part.added ? '+' : part.removed ? '-' : ' ';
  process.stdout.write(symbol + part.value);
}
// - host: localhost
// + host: db.prod.example.com
//   port: 5432

// Пословный diff
const wordDiff = diffWords(
  'The quick brown fox',
  'The fast brown fox'
);
// [unchanged] The
// [removed]   quick
// [added]     fast
// [unchanged] brown fox

// Генерация unified patch
const patch = createPatch('config.yml', oldText, newText);
console.log(patch);

Когда текстового diff недостаточно

Текстовый diff сравнивает байты и ничего не знает о структуре или семантике. В некоторых случаях нужно больше:

  • Семантический diff JSON/YAML: Парсинг обоих файлов и сравнение структур данных. Инструменты вроде jsondiffpatch или dyff делают это и игнорируют форматирование.
  • Diff SQL-схем: Инструменты вроде pgdiff сравнивают схемы баз данных, а не файлы миграций, улавливая структурные изменения даже при переупорядочении SQL.
  • Бинарные файлы: Текстовый diff бесполезен для бинарных форматов. Используйте специализированные инструменты — exiftool для метаданных изображений, специальные diff-инструменты для PDF или Word.
  • Минифицированный код: Всегда форматируйте перед diff. Минифицированный JS-файл покажется одной большой изменённой строкой. Сначала используйте Форматтер JavaScript.

Сравнивайте любые два текста прямо в браузере с помощью Сравнения текстов — YAML, JSON, конфиги, код или обычный текст. Данные не покидают ваш компьютер.