跳转到正文

单元格级合并

列级合并可检测双方是否都修改了同一列。单元格级合并会进一步深入,合并该列内部的内容。两个人编辑 TEXT 字段中的不同文本行,或编辑 JSON 对象中的不同键时,都可以顺利合并。

PHP
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;

$config = CellMergeConfig::auto();
$merge = new ThreeWayMerge($config);
$result = $merge->merge($base, $ours, $theirs);

启用单元格级合并

单元格级合并需显式启用。它只会在使用 CellMergeConfig 构造的 ThreeWayMerge 中运行。若未配置,双方都改成不同值的每一列都会产生 update_update 冲突。

Merql 门面不接受配置:Merql::merge()、Merql::patch() 和 Merql::plan() 无需配置即可构造 new ThreeWayMerge(),而 CLI 的 merge 命令会通过 Merql::merge() 执行。它们始终在列级别合并(#7)。

若要使用单元格级合并来合并由门面或 CLI 保存的快照,请自行加载快照并调用 ThreeWayMerge。结果是普通的 MergeResult,因此门面仍可应用它;MergePlanBuilder 会将其转换为合并计划:

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

使用 Myers 差异算法合并多行文本,与 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 会调用 pitmaster 的 ThreeWayMerge::merge(),后者会计算基线与双方状态之间的 Myers 差异,然后合并两组编辑脚本。如果编辑在同一行区域重叠,就会报告冲突。

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