Vai al contenuto

Merge a livello di cella

Il merge a livello di colonna rileva quando entrambi i lati hanno modificato la stessa colonna. Quello a livello di cella scende più in profondità e unisce i contenuti all'interno della colonna. Due persone che modificano righe diverse di un campo TEXT, o chiavi diverse di un oggetto JSON, possono unire le modifiche senza conflitti.

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

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

Abilitazione del merge a livello di cella

Il merge a livello di cella è facoltativo. Viene eseguito solo con un ThreeWayMerge costruito con un CellMergeConfig. Senza quest'ultimo, ogni colonna modificata da entrambi i lati con valori diversi genera un conflitto update_update.

La facciata Merql non accetta una configurazione: Merql::merge(), Merql::patch() e Merql::plan() costruiscono new ThreeWayMerge() senza configurazione, e il comando merge di CLI passa per Merql::merge(). Eseguono sempre il merge a livello di colonna (#7).

Per eseguire un merge a livello di cella sugli snapshot salvati dalla facciata o da CLI, caricateli e chiamate direttamente ThreeWayMerge. Il risultato è un normale MergeResult, quindi la facciata può comunque applicarlo e MergePlanBuilder lo trasforma in un piano di merge:

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

I merger di cella vengono eseguiti solo per le colonne modificate da entrambi i lati. Se una colonna è stata modificata da un solo lato, il suo valore viene mantenuto così com'è.

CellMergeConfig

CellMergeConfig associa colonne o tipi di colonna alle implementazioni dei merger di cella. La factory auto() fornisce valori predefiniti ragionevoli:

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à di risoluzione dei merger:

  1. Nome di colonna qualificato (table.column).
  2. Nome di colonna non qualificato (column).
  3. Corrispondenza del tipo di colonna (str_contains senza distinzione tra maiuscole e minuscole).
  4. Predefinito: OpaqueCellMerger (se i valori differiscono, segnala sempre un conflitto).

TextCellMerger

Unisce testi su più righe usando il diff di Myers, lo stesso algoritmo usato da git per i contenuti dei file. Ogni riga viene trattata come un'unità e le modifiche a righe diverse vengono unite senza conflitti.

PHP
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();

Esempio: merge di testo senza conflitti

Codice
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

Sono state modificate righe diverse. Il merge produce "LINE ONE\nline two\nLINE THREE" senza conflitti.

Esempio: conflitto nel testo

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

Entrambi i lati hanno modificato la stessa riga assegnandole valori diversi. Il merger restituisce un risultato con conflitto. Il contenuto unito include marcatori di conflitto simili a quelli di git.

Come funziona

TextCellMerger delega a ThreeWayMerge::merge() di pitmaster, che calcola un diff di Myers tra la base e ciascun lato, poi combina i due script di modifica. Se le modifiche si sovrappongono nella stessa regione di righe, segnala un conflitto.

JsonCellMerger

Unisce gli oggetti JSON confrontando separatamente le chiavi di primo livello. Ogni chiave viene unita con le normali regole a tre vie.

PHP
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();

Esempio: merge JSON senza conflitti

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}

Sono state modificate chiavi diverse. Il risultato del merge è:

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

Esempio: conflitto JSON

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

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

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

Entrambi i lati hanno modificato la chiave color assegnandole valori diversi. Si tratta di un conflitto. Per impostazione predefinita, il merger mantiene ours e segnala un conflitto.

Regole a livello di chiave

Base Ours Theirs Risultato
Chiave invariata Chiave invariata Accetta la base
Chiave invariata Chiave modificata Accetta theirs
Chiave modificata Chiave invariata Accetta ours
Chiave modificata (uguale) Chiave modificata (uguale) Accetta (concordano)
Chiave modificata (diversa) Chiave modificata (diversa) Conflitto (predefinito: ours)
Chiave presente Chiave presente Chiave rimossa Accetta la rimozione
Chiave presente Chiave rimossa Chiave presente Accetta la rimozione
Chiave assente Chiave aggiunta Chiave assente Accetta l'aggiunta
Chiave assente Chiave assente Chiave aggiunta Accetta l'aggiunta

Gli oggetti annidati vengono confrontati come stringhe JSON opache, non in modo ricorsivo. Se un oggetto annidato è diverso, viene confrontato l'intero valore della chiave, non le sue sottochiavi.

Requisiti

Tutti e tre i valori devono essere oggetti o array JSON validi. Se un valore non è JSON valido o è uno scalare JSON (stringa, numero, booleano), il merger ripiega su un conflitto.

OpaqueCellMerger

Il merger predefinito quando non è configurata alcuna strategia a livello di cella. Tratta il valore della colonna come un blocco opaco e segnala sempre un conflitto quando entrambi i lati lo modificano.

PHP
use Merql\CellMerge\OpaqueCellMerger;

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

CellMergeResult

Tutti i merger di cella restituiscono un 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

Metodi factory:

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

Merger di cella personalizzati

Implementate l'interfaccia CellMerger per aggiungere una strategia di merge personalizzata:

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