Saltar al contenido

Fusión

Pitmaster realiza fusiones de ramas mediante una fusión de tres vías, como git merge. Detecta oportunidades de avance rápido, calcula las bases de fusión, realiza fusiones a nivel de árbol y de contenido, y genera marcadores de conflicto estándar cuando los archivos entran en conflicto.

Fusión básica

PHP
$result = $repo->merge('feature/login');

if ($result->clean) {
    echo "Merged successfully: {$result->commitId->hex}\n";
} else {
    echo "Conflicts:\n";
    foreach ($result->conflictPaths as $path) {
        echo "  {$path}\n";
    }
}

MergeResult

La clase MergeResult es un objeto de valor de solo lectura:

PHP
use Pitmaster\Merge\MergeResult;

$result->clean;           // bool: true if merge completed without conflicts
$result->commitId;        // ?ObjectId: the merge commit (null if conflicts)
$result->conflictPaths;   // array: list of file paths with conflicts
$result->mergedContents;  // array: path => content with conflict markers (if conflicts)

Cuando la fusión se detiene por conflictos, el estado del conflicto se escribe en el índice y el árbol de trabajo. Resuelve los archivos, add() y llama a $repo->mergeContinue(), o llama a $repo->mergeAbort() para restaurar el estado anterior a la fusión.

Antes de fusionar, confirma o guarda en el stash los cambios locales: la fusión reconstruye el árbol de trabajo a partir del resultado y sobrescribe los cambios sin confirmar en los archivos bajo seguimiento (#2).

Fusiones de avance rápido

Cuando el HEAD actual es antecesor de la rama de destino, Pitmaster realiza una fusión de avance rápido: simplemente mueve HEAD al commit de destino sin crear un commit de fusión.

Código
Before:         After fast-forward:

A---B (main)    A---B---C---D (main)
     \
      C---D (feature)
PHP
$result = $repo->merge('feature');
// $result->clean === true
// $result->commitId points to D
// No merge commit created

Aún no se detecta el caso inverso: fusionar una rama que HEAD ya contiene crea un nuevo commit de fusión con un árbol sin cambios, en lugar de indicar «Already up to date» (#6).

Fusión de tres vías

Cuando las ramas han divergido, Pitmaster realiza una fusión de tres vías:

  1. Buscar la base de fusión (antecesor común más cercano)
  2. Comparar los árboles de la base con el nuestro y con el de ellos
  3. Para cada archivo modificado en ambas ramas, realizar una fusión a nivel de contenido
  4. Crear un commit de fusión con dos padres
Código
Before:              After merge:

      C---D (feature)         C---D
     /                       /     \
A---B---E---F (main)   A---B---E---F---G (main)

Base de fusión

La base de fusión es el antecesor común más reciente de dos commits. Pitmaster la encuentra recorriendo el grafo de commits. Si hay varias bases de fusión (historial cruzado), merge() las fusiona primero y usa el resultado como base.

PHP
$base = $repo->mergeBase(
    $repo->resolve('main'),
    $repo->resolve('feature/login'),
);

if ($base !== null) {
    echo "Common ancestor: {$base->hex}\n";
}

Para acceder directamente al algoritmo MergeBase:

PHP
use Pitmaster\Merge\MergeBase;

$finder = new MergeBase($repo->objectDatabase());
$base = $finder->find($commitA, $commitB);

// All merge bases (criss-cross histories can have more than one)
$bases = $finder->findAll($commitA, $commitB);

// Check ancestry
$isAncestor = $finder->isAncestor($oldCommit, $newCommit);

Comprobación de fusión de ramas

PHP
if ($repo->isBranchMerged('feature/done')) {
    // All commits in feature/done are reachable from default branch
    $repo->deleteBranch('feature/done');
}

// Check against a specific target
if ($repo->isBranchMerged('feature/done', 'develop')) {
    // Merged into develop
}

Marcadores de conflicto

Cuando un archivo se modifica en ambas ramas y los cambios se solapan, Pitmaster genera marcadores de conflicto estándar. merge() etiqueta los lados como HEAD y con el nombre de la rama fusionada:

Código
<<<<<<< HEAD
    return $this->value + 1;
=======
    return $this->value * 2;
>>>>>>> feature

La clase ConflictMarker envuelve dos versiones con marcadores:

PHP
use Pitmaster\Merge\ConflictMarker;

$marked = ConflictMarker::mark($oursContent, $theirsContent, 'HEAD', 'feature');

// diff3 style, with the base section
$marked = ConflictMarker::mark($oursContent, $theirsContent, 'HEAD', 'feature', $baseContent, 'base');

Fusión de tres vías a nivel de contenido

La clase ThreeWayMerge fusiona el contenido de blobs (no archivos):

PHP
use Pitmaster\Merge\ThreeWayMerge;

$result = ThreeWayMerge::merge($baseContent, $oursContent, $theirsContent, 'HEAD', 'feature');

$result['content'];   // merged text, with conflict markers if needed
$result['clean'];     // true when nothing conflicted
$result['conflicts']; // number of conflict regions

Toma tres cadenas (la versión base, nuestra versión y la versión de ellos), además de etiquetas opcionales y un estilo de conflicto ('merge' de forma predeterminada o 'diff3' para incluir la sección base).

Estrategias de fusión

Fusión recursiva

La estrategia predeterminada ($repo->merge($branch)) gestiona historiales de fusiones cruzadas con varias bases de fusión. CLI también acepta -s ort y -s recursive para esta estrategia.

Fusión nuestra

Registra un commit de fusión que mantiene sin cambios el árbol de HEAD. Es útil para conservar el historial de ramas sin incorporar cambios.

PHP
$result = $repo->merge('feature/old', 'ours');

Fusión octopus

Fusiona dos o más ramas en un único commit de fusión. Si hay conflictos en alguna rama, devuelve un resultado no limpio sin escribir el estado de fusión ni crear un commit.

PHP
$result = $repo->mergeOctopus(['feature/a', 'feature/b', 'feature/c']);

Las clases RecursiveMerge, OursMerge y OctopusMerge de Pitmaster\Merge son auxiliares a nivel de contenido; Repository implementa por sí misma las estrategias anteriores.

Rerere (reutilizar resolución registrada)

Pitmaster incluye una clase independiente Rerere que almacena resoluciones en la estructura rr-cache de git:

PHP
use Pitmaster\Merge\Rerere;

$rerere = new Rerere($repo->gitDir());

if ($rerere->isEnabled()) {                     // rerere.enabled = true in .git/config
    $rerere->record($conflicted, $resolved);    // remember a resolution
    $resolved = $rerere->resolve($conflicted);  // replay it, or null
}

merge() no la llama: las resoluciones solo se registran y se vuelven a aplicar cuando el código lo indica. isEnabled() busca el texto literal rerere.enabled = true en .git/config y no reconoce la sección [rerere] que escribe git config rerere.enabled true (#20).

Flujo de fusión a nivel de árbol

El proceso completo de fusión:

  1. MergeBase::findAll() localiza los antecesores comunes
  2. Se comparan las rutas de los árboles base, nuestro y de ellos
  3. Para los archivos modificados en una sola rama: se aplica ese cambio
  4. Para los archivos modificados en ambas ramas:
    • Si ambos se modificaron del mismo modo: se aplica cualquiera (coinciden)
    • Si los cambios difieren: se ejecuta ThreeWayMerge::merge() sobre el contenido
  5. Si todas las fusiones se completan sin conflictos: se crea el árbol fusionado y un commit de fusión
  6. Si hay conflictos: se devuelve MergeResult con clean = false y las rutas en conflicto

Uso de CLI

Shell
./vendor/bin/pitmaster merge feature/login

Salida si la operación tiene éxito:

Código
Merge successful.

Salida en caso de conflicto:

Código
CONFLICT in: src/File.php, src/Other.php