
# Слияние на уровне ячеек

Слияние на уровне столбцов обнаруживает, когда обе стороны изменили один и тот же столбец. Слияние на уровне ячеек углубляется и объединяет содержимое внутри этого столбца. Изменения в разных строках поля TEXT или в разных ключах объекта JSON, внесенные двумя людьми, можно объединить без конфликтов.

```php
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](https://github.com/inline0/merql/issues/7)).

Чтобы объединить снимки, сохраненные фасадом или CLI, слиянием на уровне ячеек, загрузите их и самостоятельно вызовите `ThreeWayMerge`. Результат — обычный `MergeResult`, поэтому фасад по-прежнему может его применить, а `MergePlanBuilder` превращает его в [план слияния](https://merql.inline0.dev/ru/docs/merge/plans):

```php
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()` предоставляет разумные значения по умолчанию:

```php
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());
```

Приоритет выбора стратегии слияния:

1. Полное имя столбца (`table.column`).
2. Имя столбца без квалификатора (`column`).
3. Совпадение с шаблоном типа столбца (без учета регистра `str_contains`).
4. По умолчанию: `OpaqueCellMerger` (всегда сообщает о конфликте, если значения различаются).

## TextCellMerger

Объединяет многострочный текст с помощью алгоритма различий Майерса — того же, который git использует для содержимого файлов. Каждая строка рассматривается как единица, и изменения в разных строках объединяются без конфликтов.

```php
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, независимо сравнивая ключи верхнего уровня. Для каждого ключа применяются стандартные правила трехстороннего слияния.

```php
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();
```

### Пример: слияние JSON без конфликтов

```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}
```

Изменены разные ключи. Результат слияния:

```json
{"name": "Bob", "role": "admin", "active": true}
```

### Пример: конфликт JSON

```json
// Base
{"color": "red"}

// Ours
{"color": "blue"}

// Theirs
{"color": "green"}
```

Обе стороны изменили ключ `color` на разные значения. Это конфликт. По умолчанию стратегия оставляет наше значение и сообщает об одном конфликте.

### Правила для ключей

| База | Наша сторона | Их сторона | Результат |
|---|---|---|---|
| Ключ не изменен | Ключ не изменен | Принять базовое значение |
| Ключ не изменен | Ключ изменен | Принять значение их стороны |
| Ключ изменен | Ключ не изменен | Принять наше значение |
| Ключ изменен (одинаково) | Ключ изменен (одинаково) | Принять (совпадает) |
| Ключ изменен (по-разному) | Ключ изменен (по-разному) | **Конфликт** (по умолчанию: наша сторона) |
| Ключ существует | Ключ существует | Ключ удален | Принять удаление |
| Ключ существует | Ключ удален | Ключ существует | Принять удаление |
| Ключ отсутствует | Ключ добавлен | Ключ отсутствует | Принять добавление |
| Ключ отсутствует | Ключ отсутствует | Ключ добавлен | Принять добавление |

Вложенные объекты сравниваются как непрозрачные строки JSON, без рекурсивного обхода. Если вложенный объект отличается, сравнивается все значение этого ключа, а не его подчиненные ключи.

### Требования

Все три значения должны быть допустимыми объектами или массивами JSON. Если какое-либо значение не является допустимым JSON или представляет собой скаляр JSON (строку, число, логическое значение), стратегия возвращает конфликт.

## OpaqueCellMerger

Стратегия слияния по умолчанию, если стратегия слияния на уровне ячеек не настроена. Она рассматривает значение столбца как непрозрачный блок данных и всегда сообщает о конфликте, если значения различаются.

```php
use Merql\CellMerge\OpaqueCellMerger;

$merger = new OpaqueCellMerger();
$result = $merger->merge($base, $ours, $theirs);
// $result->clean is always false when $ours !== $theirs
```

## CellMergeResult

Все стратегии слияния ячеек возвращают `CellMergeResult`:

```php
$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
```

Методы фабрики:

```php
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // n conflicts, defaults to ours
```

## Пользовательские стратегии слияния ячеек

Реализуйте интерфейс `CellMerger`, чтобы добавить собственную стратегию слияния:

```php
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());
```
