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
$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:
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.
Before: After fast-forward:
A---B (main) A---B---C---D (main)
\
C---D (feature)
$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:
- Buscar la base de fusión (antecesor común más cercano)
- Comparar los árboles de la base con el nuestro y con el de ellos
- Para cada archivo modificado en ambas ramas, realizar una fusión a nivel de contenido
- Crear un commit de fusión con dos padres
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.
$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:
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
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:
<<<<<<< HEAD
return $this->value + 1;
=======
return $this->value * 2;
>>>>>>> feature
La clase ConflictMarker envuelve dos versiones con marcadores:
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):
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.
$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.
$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:
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:
MergeBase::findAll()localiza los antecesores comunes- Se comparan las rutas de los árboles base, nuestro y de ellos
- Para los archivos modificados en una sola rama: se aplica ese cambio
- 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
- Si todas las fusiones se completan sin conflictos: se crea el árbol fusionado y un commit de fusión
- Si hay conflictos: se devuelve
MergeResultconclean = falsey las rutas en conflicto
Uso de CLI
./vendor/bin/pitmaster merge feature/login
Salida si la operación tiene éxito:
Merge successful.
Salida en caso de conflicto:
CONFLICT in: src/File.php, src/Other.php