Пятница, сервер Rust, полтора десятка плагинов на Oxide. Выходит обновление одного из них — того самого, что отвечает за киты. Кладёте новую версию, плагин при загрузке дописывает в ваш конфиг новые ключи и заодно нормализует файл под себя: меняет порядок ключей, переформатирует, приводит секции к своему виду. Сервер поднимается, в консоли чисто, все довольны.

Через сутки в дискорде: «почему кит на кулдауне сутки вместо часа», «почему из ящиков сыпется не то», «у меня баланс обнулился». Где-то среди дописанного новая опция получила дефолтное значение, а старая, которую вы правили полгода назад, тихо перестала читаться.

Бэкап конфига у вас есть. Открываете два файла в текстовом diff — и он красный целиком. Не потому что всё изменилось, а потому что порядок ключей другой и отступы поехали. Отключаете пробелы — легче не стало. Дальше 1500 строк глазами в поисках трёх значений, которые реально отличаются.

Вот об этом статья. Точнее — о том, почему обычный diff на JSON врёт, и что я по этому поводу сделал в своём редакторе.

Почему текстовый diff врёт на JSON

Текстовый diff честно показывает разницу в тексте. Проблема в том, что текст и данные — не одно и то же, а JSON вы правите как данные.

Что ломает сравнение:

  • Порядок ключей. Сериализатор поменял местами name и id — данные идентичны, diff красный.

  • Форматирование. Один файл на два пробела, второй на четыре, третий минифицирован. Diff: «изменено 100% файла».

  • Сдвиг блока. Добавили элемент в начало массива — всё, что ниже, «изменилось», потому что уехало на несколько строк.

  • Служебный мусор. updatedAt, version, lastModified меняются при каждом экспорте и шумят в каждом сравнении.

  • null против отсутствия ключа. Для текста это просто две разные строки, и по красно-зелёной простыне не понять, что именно перед вами.

В сумме вы отфильтровываете шум вручную. Каждый раз. На конфиге, где реальных изменений — три штуки.

Если коротко: diff ничего не знает про JSON. Ему нужно об этом рассказать.

Что я сделал

Я пишу CraftHub — кроссплатформенный редактор, который показывает JSON как таблицу: колонки типизированы, строки правятся в гриде или в текстовом режиме с подсветкой. Изначально он делался под игровые конфиги: друзья возились с Unity, и время уходило не на разработку, а на ручное ковыряние JSON.

В новой версии в нём появилось сравнение — две штуки, которые закрывают историю выше целиком.

Кнопка Changes: что я наредактировал

Первое и самое частое: открыл конфиг, поправил десяток значений, отвлёкся, вернулся — и уже не помнишь, что именно менял. Раньше вариант был один: сохранить и посмотреть в git. Если файл вообще под git — а конфиги сервера, будем честны, чаще всего нет.

Кнопка Changes показывает документ в памяти против его последней сохранённой версии на диске.

Окно изменений
Окно изменений

Работает и в табличном режиме, и в текстовом: дифф берётся из документа, а не из того редактора, который сейчас на экране. Правили таблицу — увидите изменения в JSON, ровно в том виде, в каком оно ляжет в файл.

Внутри:

  • side-by-side и unified — переключаются одним кликом;

  • подсветка на уровне слов — видно не «строка изменилась», а какой именно фрагмент внутри строки;

  • счётчики добавленного, удалённого и изменённого;

  • F7 / F8 — прыжки по изменениям, без скролла через две тысячи строк;

  • миниатюра справа — полоса со всеми изменениями по всему файлу, клик — переход;

  • свёрнутые куски — неизменённые участки схлопываются в «▾ показать ещё N строк».

Отдельная вкладка — структурное сравнение. Не строки, а пути JSON деревом: что добавлено, что удалено, что изменилось, где поменялся тип. Когда нужно не «прочитать дифф», а ответить на вопрос «какие поля вообще затронуты» — это быстрее в разы. Именно эта вкладка отвечает на вопрос из истории с китами: какая опция появилась, какая исчезла, у какой поменялся тип с числа на строку.

JSON Comparer: до шести документов сразу

Второе окно — самостоятельный сравниватель, он вообще не связан с открытыми вкладками.

Сравниватель json
Сравниватель json
  • от двух до шести панелей; в каждую можно вставить текст, открыть файл или перетащить его мышью;

  • первая панель — эталон, все остальные сравниваются с ней, каждая пара получает свою вкладку с результатом;

  • панели переименовываются и переставляются: перетащил нужную влево — она стала эталоном;

  • любую панель можно заполнить из активной вкладки редактора — её текущим или последним сохранённым содержимым.

Шесть панелей — это не «потому что можно». Это ровно наш случай: бэкап до обновления, текущий конфиг, дефолтный конфиг из поставки плагина, и заодно конфиг с тестового сервера. Кидаете бэкап в первую панель — и сразу видите три сравнения против него.

Опции: что считать отличием

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

  • игнорировать порядок ключей — снимает добрую половину ложных срабатываний;

  • игнорировать порядок элементов массива — когда массив по смыслу множество, а не список;

  • не учитывать регистр строк;

  • игнорировать null и пустые значения;

  • список игнорируемых путей — вписали $.updatedAt и $.meta.version, и они больше не мозолят глаза.

Включаете первый пункт — и та самая красная простыня из начала статьи сжимается до пяти строк, которые действительно изменились. Текстовый diff так не умеет в принципе: он не знает, что перед ним JSON.

Экспорт: чтобы приложить к тикету

  • скопировать как unified patch или сохранить в .patch;

  • сохранить как JSON Patch (RFC 6902) — готовый машиночитаемый набор операций;

  • сохранить отчёт в Markdown или HTML — кинуть в issue разработчику плагина, в PR или коллеге, у которого CraftHub не стоит.

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

Дифф перед сохранением

Мелочь, которая внезапно оказалась одной из самых удобных: перед записью на диск редактор показывает, что именно сейчас изменится, и ждёт подтверждения. Один раз это спасает от «а, я же не в том файле правил». Кого раздражает — галочка «больше не показывать» на месте.

Это не только про Rust

Rust — просто самый наглядный пример: десятки JSON-конфигов, регулярные обновления плагинов, вайпы, два-три сервера, которые «одинаковые» ровно до момента, когда перестают быть одинаковыми.

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

  • Unity: экспорт из редактора, база предметов, баланс, локализация — JSON, который правят несколько человек и который надо сверять между ветками и билдами;

  • бэкенд: конфиги окружений, которые «должны отличаться только парой значений», а по факту разошлись за полгода;

  • API: два ответа сервиса до и после релиза, где надо понять, что поменялось в контракте, а не в порядке полей.

Если у вас есть свой случай, где diff врёт особенно нагло, — напишите в issue. Мне интересно, какие ещё правила сравнения имеет смысл добавить.

Что дальше

Следующая большая штука, которую я сейчас проектирую, — вычисляемые поля прямо в таблице: формулы в стиле Excel (=@[price] * @[qty]), протягивание вниз за уголок, пересчёт по графу зависимостей. При этом сами формулы не будут засорять ваш JSON: они лягут в отдельный файл рядом, а в документ попадёт только вычисленное значение.

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

Попробовать

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

Если инструмент оказался полезным — звёздочка на GitHub реально помогает ему расти. А если что-то раздражает, тормозит или работает не так, как вы ожидали, — заводите issue, разберусь.