Zum Inhalt springen

Merge auf Zellenebene

Beim Merge auf Spaltenebene wird erkannt, wenn beide Seiten dieselbe Spalte geändert haben. Das Merge auf Zellenebene geht tiefer und führt die Inhalte innerhalb dieser Spalte zusammen. Zwei Personen können unterschiedliche Zeilen eines TEXT-Felds oder unterschiedliche Schlüssel desselben JSON-Objekts konfliktfrei bearbeiten.

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

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

Merge auf Zellenebene aktivieren

Das Merge auf Zellenebene ist optional. Es wird nur in einem ThreeWayMerge ausgeführt, das mit einem CellMergeConfig erstellt wurde. Ohne dieses Element führt jede Spalte, die beide Seiten auf unterschiedliche Werte geändert haben, zu einem update_update-Konflikt.

Die Fassade Merql nimmt keine Konfiguration entgegen: Merql::merge(), Merql::patch() und Merql::plan() erstellen new ThreeWayMerge() ohne eine solche, und der Befehl merge von CLI läuft über Merql::merge(). Sie führen immer nur auf Spaltenebene zusammen (#7).

Um Snapshots, die mit der Fassade oder CLI erstellt wurden, mit einem Merge auf Zellenebene zusammenzuführen, lade sie und rufe selbst ThreeWayMerge auf. Das Ergebnis ist ein gewöhnliches MergeResult. Die Fassade kann es also weiterhin anwenden, und MergePlanBuilder wandelt es in einen Merge-Plan um:

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

Zellen-Merger werden nur für Spalten ausgeführt, die beide Seiten geändert haben. Hat nur eine Seite eine Spalte geändert, wird ihr Wert unverändert übernommen.

CellMergeConfig

CellMergeConfig ordnet Spalten oder Spaltentypen passende Zellen-Merger-Implementierungen zu. Die Factory auto() stellt sinnvolle Standardwerte bereit:

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

Priorität bei der Auswahl des Mergers:

  1. Vollständiger Spaltenname (table.column).
  2. Spaltenname ohne Qualifizierer (column).
  3. Abgleich mit Spaltentyp (ohne Beachtung der Groß- und Kleinschreibung: str_contains).
  4. Standard: OpaqueCellMerger (meldet bei unterschiedlichen Werten immer einen Konflikt).

TextCellMerger

Führt mehrzeiligen Text mit dem Myers-Diff zusammen, demselben Algorithmus, den git für Datei-Inhalte verwendet. Jede Zeile gilt als Einheit; Änderungen an unterschiedlichen Zeilen lassen sich konfliktfrei zusammenführen.

PHP
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();

Beispiel: konfliktfreies Text-Merge

Code
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

Es wurden unterschiedliche Zeilen bearbeitet. Das Merge-Ergebnis ist "LINE ONE\nline two\nLINE THREE" ohne Konflikt.

Beispiel: Textkonflikt

Code
Base:    "line one\nline two"
Ours:    "changed by us\nline two"     // changed line 1
Theirs:  "changed by them\nline two"   // also changed line 1

Beide Seiten haben dieselbe Zeile auf unterschiedliche Werte geändert. Der Merger gibt ein Konfliktergebnis zurück. Der zusammengeführte Inhalt enthält konfliktähnliche Markierungen wie bei git.

Funktionsweise

TextCellMerger delegiert die Aufgabe an ThreeWayMerge::merge() von pitmaster. Dieser berechnet einen Myers-Diff zwischen Base und jeder Seite und führt anschließend die beiden Bearbeitungsskripte zusammen. Überschneiden sich Änderungen im selben Zeilenbereich, wird ein Konflikt gemeldet.

JsonCellMerger

Führt JSON-Objekte zusammen, indem die Schlüssel der obersten Ebene einzeln verglichen werden. Für jeden Schlüssel gelten die üblichen Drei-Wege-Regeln.

PHP
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();

Beispiel: konfliktfreies JSON-Merge

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}

Die Schlüssel wurden unterschiedlich geändert. Das Merge-Ergebnis lautet:

JSON
{"name": "Bob", "role": "admin", "active": true}

Beispiel: JSON-Konflikt

JSON
// Base
{"color": "red"}

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

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

Beide Seiten haben den Schlüssel color auf unterschiedliche Werte geändert. Das ist ein Konflikt. Standardmäßig übernimmt der Merger Ours und meldet einen Konflikt.

Regeln auf Schlüsselebene

Basis Ours Theirs Ergebnis
Schlüssel unverändert Schlüssel unverändert Basis übernehmen
Schlüssel unverändert Schlüssel geändert Theirs übernehmen
Schlüssel geändert Schlüssel unverändert Ours übernehmen
Schlüssel geändert (gleich) Schlüssel geändert (gleich) Übernehmen (übereinstimmend)
Schlüssel geändert (unterschiedlich) Schlüssel geändert (unterschiedlich) Konflikt (Standard: Ours)
Schlüssel vorhanden Schlüssel vorhanden Schlüssel entfernt Entfernung übernehmen
Schlüssel vorhanden Schlüssel entfernt Schlüssel vorhanden Entfernung übernehmen
Schlüssel fehlt Schlüssel hinzugefügt Schlüssel fehlt Hinzufügung übernehmen
Schlüssel fehlt Schlüssel fehlt Schlüssel hinzugefügt Hinzufügung übernehmen

Verschachtelte Objekte werden als undurchsichtige JSON-Zeichenfolgen verglichen, nicht rekursiv. Unterscheidet sich ein verschachteltes Objekt, wird der gesamte Wert dieses Schlüssels verglichen, nicht seine Unterschlüssel.

Voraussetzungen

Alle drei Werte müssen gültige JSON-Objekte oder Arrays sein. Ist ein Wert kein gültiges JSON oder ein skalarer JSON-Wert (String, Zahl, Boolean), fällt der Merger auf einen Konflikt zurück.

OpaqueCellMerger

Der Standard-Merger, wenn keine Strategie für die Zellenebene konfiguriert ist. Er behandelt den Spaltenwert als undurchsichtigen Block und meldet immer einen Konflikt, wenn sich die Werte beider Seiten unterscheiden.

PHP
use Merql\CellMerge\OpaqueCellMerger;

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

CellMergeResult

Alle Zellen-Merger geben ein CellMergeResult zurück:

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

Factory-Methoden:

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

Benutzerdefinierte Zellen-Merger

Implementiere die Schnittstelle CellMerger, um eine eigene Merge-Strategie hinzuzufügen:

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