Слияние на уровне ячеек
Слияние на уровне столбцов обнаруживает, когда обе стороны изменили один и тот же столбец. Слияние на уровне ячеек углубляется и объединяет содержимое внутри этого столбца. Изменения в разных строках поля TEXT или в разных ключах объекта JSON, внесенные двумя людьми, можно объединить без конфликтов.
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
$config = CellMergeConfig::auto();
$merge = new ThreeWayMerge($config);
$result = $merge->merge($base, $ours, $theirs);
Включение слияния на уровне ячеек
Слияние на уровне ячеек включается по желанию. Оно выполняется только в ThreeWayMerge, созданном с помощью CellMergeConfig. Без него каждый столбец, который обе стороны изменили на разные значения, становится конфликтом update_update.
Фасад Merql не принимает конфигурацию: Merql::merge(), Merql::patch() и Merql::plan() создают new ThreeWayMerge() без нее, а команда merge из CLI выполняется через Merql::merge(). Они всегда объединяют данные на уровне столбцов (#7).
Чтобы объединить снимки, сохраненные фасадом или CLI, слиянием на уровне ячеек, загрузите их и самостоятельно вызовите ThreeWayMerge. Результат — обычный MergeResult, поэтому фасад по-прежнему может его применить, а MergePlanBuilder превращает его в план слияния:
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Merql;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;
$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');
$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-1', $base, $ours, $theirs, $result);
Слияние ячеек выполняется только для столбцов, измененных обеими сторонами. Если столбец изменила только одна сторона, его значение берется без изменений.
CellMergeConfig
CellMergeConfig сопоставляет столбцы или типы столбцов с реализациями слияния ячеек. Фабрика auto() предоставляет разумные значения по умолчанию:
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;
use Merql\CellMerge\TextCellMerger;
// Auto: column types containing "text" (TEXT, MEDIUMTEXT, LONGTEXT, ...) use
// TextCellMerger, column types containing "json" use JsonCellMerger.
$config = CellMergeConfig::auto();
// Custom: assign mergers to specific columns.
$config = (new CellMergeConfig())
->forColumn('posts.content', new TextCellMerger())
->forColumn('settings', new JsonCellMerger())
->forType('json', new JsonCellMerger());
Приоритет выбора стратегии слияния:
- Полное имя столбца (
table.column). - Имя столбца без квалификатора (
column). - Совпадение с шаблоном типа столбца (без учета регистра
str_contains). - По умолчанию:
OpaqueCellMerger(всегда сообщает о конфликте, если значения различаются).
TextCellMerger
Объединяет многострочный текст с помощью алгоритма различий Майерса — того же, который git использует для содержимого файлов. Каждая строка рассматривается как единица, и изменения в разных строках объединяются без конфликтов.
use Merql\CellMerge\TextCellMerger;
$merger = new TextCellMerger();
Пример: слияние текста без конфликтов
Base: "line one\nline two\nline three"
Ours: "LINE ONE\nline two\nline three" // changed line 1
Theirs: "line one\nline two\nLINE THREE" // changed line 3
Изменены разные строки. В результате слияния получается "LINE ONE\nline two\nLINE THREE" без конфликтов.
Пример: конфликт при слиянии текста
Base: "line one\nline two"
Ours: "changed by us\nline two" // changed line 1
Theirs: "changed by them\nline two" // also changed line 1
Обе стороны изменили одну и ту же строку на разные значения. Стратегия слияния возвращает результат с конфликтом. Объединенное содержимое включает маркеры конфликта, похожие на git.
Как это работает
TextCellMerger делегирует работу ThreeWayMerge::merge() из pitmaster: он вычисляет различия Майерса между базой и каждой стороной, а затем объединяет два набора изменений. Если изменения пересекаются в одной и той же области строк, сообщается о конфликте.
JsonCellMerger
Объединяет объекты JSON, независимо сравнивая ключи верхнего уровня. Для каждого ключа применяются стандартные правила трехстороннего слияния.
use Merql\CellMerge\JsonCellMerger;
$merger = new JsonCellMerger();
Пример: слияние JSON без конфликтов
// Base
{"name": "Alice", "role": "editor", "active": true}
// Ours: changed "role"
{"name": "Alice", "role": "admin", "active": true}
// Theirs: changed "name"
{"name": "Bob", "role": "editor", "active": true}
Изменены разные ключи. Результат слияния:
{"name": "Bob", "role": "admin", "active": true}
Пример: конфликт JSON
// Base
{"color": "red"}
// Ours
{"color": "blue"}
// Theirs
{"color": "green"}
Обе стороны изменили ключ color на разные значения. Это конфликт. По умолчанию стратегия оставляет наше значение и сообщает об одном конфликте.
Правила для ключей
| База | Наша сторона | Их сторона | Результат |
|---|---|---|---|
| Ключ не изменен | Ключ не изменен | Принять базовое значение | |
| Ключ не изменен | Ключ изменен | Принять значение их стороны | |
| Ключ изменен | Ключ не изменен | Принять наше значение | |
| Ключ изменен (одинаково) | Ключ изменен (одинаково) | Принять (совпадает) | |
| Ключ изменен (по-разному) | Ключ изменен (по-разному) | Конфликт (по умолчанию: наша сторона) | |
| Ключ существует | Ключ существует | Ключ удален | Принять удаление |
| Ключ существует | Ключ удален | Ключ существует | Принять удаление |
| Ключ отсутствует | Ключ добавлен | Ключ отсутствует | Принять добавление |
| Ключ отсутствует | Ключ отсутствует | Ключ добавлен | Принять добавление |
Вложенные объекты сравниваются как непрозрачные строки JSON, без рекурсивного обхода. Если вложенный объект отличается, сравнивается все значение этого ключа, а не его подчиненные ключи.
Требования
Все три значения должны быть допустимыми объектами или массивами JSON. Если какое-либо значение не является допустимым JSON или представляет собой скаляр JSON (строку, число, логическое значение), стратегия возвращает конфликт.
OpaqueCellMerger
Стратегия слияния по умолчанию, если стратегия слияния на уровне ячеек не настроена. Она рассматривает значение столбца как непрозрачный блок данных и всегда сообщает о конфликте, если значения различаются.
use Merql\CellMerge\OpaqueCellMerger;
$merger = new OpaqueCellMerger();
$result = $merger->merge($base, $ours, $theirs);
// $result->clean is always false when $ours !== $theirs
CellMergeResult
Все стратегии слияния ячеек возвращают CellMergeResult:
$result = $merger->merge($base, $ours, $theirs);
$result->clean; // bool: true if merge resolved cleanly
$result->value; // mixed: the merged value (or ours on conflict)
$result->conflicts; // int: number of conflicts within the cell
Методы фабрики:
CellMergeResult::resolved($value); // clean merge
CellMergeResult::conflict($oursValue, $n); // n conflicts, defaults to ours
Пользовательские стратегии слияния ячеек
Реализуйте интерфейс CellMerger, чтобы добавить собственную стратегию слияния:
use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;
class YamlCellMerger implements CellMerger
{
public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult
{
// Parse YAML, merge keys, return result.
$merged = $this->mergeYaml($base, $ours, $theirs);
if ($merged['clean']) {
return CellMergeResult::resolved($merged['value']);
}
return CellMergeResult::conflict($ours);
}
}
$config = (new CellMergeConfig())
->forColumn('config_data', new YamlCellMerger());