单元格级合并
列级合并可检测双方是否都修改了同一列。单元格级合并会进一步深入,合并该列内部的内容。两个人编辑 TEXT 字段中的不同文本行,或编辑 JSON 对象中的不同键时,都可以顺利合并。
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 会将其转换为合并计划:
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
使用 Myers 差异算法合并多行文本,与 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 会调用 pitmaster 的 ThreeWayMerge::merge(),后者会计算基线与双方状态之间的 Myers 差异,然后合并两组编辑脚本。如果编辑在同一行区域重叠,就会报告冲突。
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());