セル単位のマージ
列単位のマージは、両側が同じ列を変更したことを検出します。セル単位のマージでは、さらに踏み込んで列内の内容をマージします。TEXTフィールドの異なる行や、JSONオブジェクトの異なるキーを2人が編集した場合も、競合せずにマージできます。
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 でmerge planに変換できます:
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がファイル内容に使うものと同じMyers差分アルゴリズムでマージします。各行を単位として扱うため、異なる行への変更は競合せずにマージできます。
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() に処理を委ねます。この処理ではbaseと各側の間でMyers差分を計算し、2つの編集スクリプトを組み合わせます。同じ行領域で編集が重なると、競合として報告します。
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 キーを異なる値に変更しました。これは競合です。マージャーはデフォルトでoursを採用し、競合を1件報告します。
キー単位の規則
| Base | Ours | Theirs | 結果 |
|---|---|---|---|
| キーに変更なし | キーに変更なし | baseを採用 | |
| キーに変更なし | キーを変更 | theirsを採用 | |
| キーを変更 | キーに変更なし | oursを採用 | |
| キーを変更(同じ値) | キーを変更(同じ値) | 採用(合意) | |
| キーを変更(異なる値) | キーを変更(異なる値) | 競合(デフォルト: ours) | |
| キーあり | キーあり | キーを削除 | 削除を採用 |
| キーあり | キーを削除 | キーあり | 削除を採用 |
| キーなし | キーを追加 | キーなし | 追加を採用 |
| キーなし | キーなし | キーを追加 | 追加を採用 |
ネストされたオブジェクトは、再帰的に比較せず、不透明な JSON 文字列として比較されます。ネストされたオブジェクトが異なる場合、サブキーではなく、そのキーの値全体を比較します。
要件
3つすべての値が有効な 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());