Aller au contenu

Fusion

Pitmaster fusionne les branches à l’aide d’une fusion à trois voies, comme git merge. Il détecte les possibilités d’avance rapide, calcule les bases de fusion, effectue des fusions au niveau des arbres et du contenu, et génère les marqueurs de conflit standard lorsque des fichiers entrent en conflit.

Fusion de base

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 classe MergeResult est un objet valeur en lecture seule :

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)

Lorsque la fusion s’arrête en raison de conflits, l’état du conflit est écrit dans l’index et l’arbre de travail. Résolvez les conflits dans les fichiers, add()-les, puis appelez $repo->mergeContinue(), ou appelez $repo->mergeAbort() pour rétablir l’état précédant la fusion.

Avant de fusionner, validez ou mettez de côté les modifications locales : la fusion reconstruit l’arbre de travail à partir du résultat et écrase les modifications non validées des fichiers suivis (#2).

Fusions en avance rapide

Lorsque le HEAD actuel est un ancêtre de la branche cible, Pitmaster effectue une fusion en avance rapide : il déplace simplement HEAD vers le commit cible, sans créer de commit de fusion.

Code
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

Le cas inverse n’est pas encore détecté : fusionner une branche déjà intégrée à HEAD crée un nouveau commit de fusion avec un arbre inchangé, au lieu d’indiquer « Already up to date » (#6).

Fusion à trois voies

Lorsque les branches ont divergé, Pitmaster effectue une fusion à trois voies :

  1. Trouver la base de fusion (le plus proche ancêtre commun)
  2. Comparer l’arbre de la base à celui de notre branche et à celui de leur branche
  3. Pour chaque fichier modifié dans les deux branches, effectuer une fusion au niveau du contenu
  4. Créer un commit de fusion avec deux parents
Code
Before:              After merge:

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

Base de fusion

La base de fusion est le plus récent ancêtre commun de deux commits. Pitmaster la trouve en parcourant le graphe des commits. Lorsqu’il existe plusieurs bases de fusion (historique croisé), merge() les fusionne d’abord et utilise le résultat comme base.

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

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

Pour accéder directement à l’algorithme 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);

Vérification de fusion des branches

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
}

Marqueurs de conflit

Lorsqu’un fichier est modifié dans les deux branches et que les changements se chevauchent, Pitmaster génère des marqueurs de conflit standard. merge() étiquette les côtés HEAD et avec le nom de la branche fusionnée :

Code
<<<<<<< HEAD
    return $this->value + 1;
=======
    return $this->value * 2;
>>>>>>> feature

La classe ConflictMarker encadre deux versions avec des marqueurs :

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

Fusion à trois voies au niveau du contenu

La classe ThreeWayMerge fusionne le contenu des blobs (et non les fichiers) :

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

Elle prend trois chaînes (la version de base, notre version et leur version), ainsi que des étiquettes facultatives et un style de conflit ('merge' par défaut, ou 'diff3' pour inclure la section de base).

Stratégies de fusion

Fusion récursive

La stratégie par défaut ($repo->merge($branch)) gère les historiques de fusion croisés avec plusieurs bases de fusion. CLI accepte aussi -s ort et -s recursive pour cette stratégie.

Fusion Ours

Enregistre un commit de fusion qui conserve l’arbre de HEAD tel quel. Pratique pour préserver l’historique des branches sans intégrer les changements.

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

Fusion Octopus

Fusionne deux branches ou plus dans un seul commit de fusion. Si une branche entre en conflit, renvoie un résultat non propre sans enregistrer l’état de la fusion ni créer de commit.

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

Les classes RecursiveMerge, OursMerge et OctopusMerge de Pitmaster\Merge sont des outils d’aide au niveau du contenu ; Repository implémente lui-même les stratégies ci-dessus.

Rerere (réutiliser une résolution enregistrée)

Pitmaster inclut une classe Rerere autonome qui stocke les résolutions selon l’organisation 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() ne l’appelle pas : les résolutions sont enregistrées et rejouées uniquement lorsque votre code le fait. isEnabled() recherche le texte littéral rerere.enabled = true dans .git/config et ne reconnaît pas la section [rerere] écrite par git config rerere.enabled true (#20).

Flux de fusion au niveau des arbres

Processus complet de fusion :

  1. MergeBase::findAll() localise les ancêtres communs
  2. Les arbres de base, de notre branche et de leur branche sont comparés chemin par chemin
  3. Pour les fichiers modifiés dans une seule branche : appliquer cette modification
  4. Pour les fichiers modifiés dans les deux branches :
    • Si les modifications sont identiques : appliquer l’une ou l’autre (elles concordent)
    • Si elles diffèrent : lancer ThreeWayMerge::merge() sur le contenu
  5. Si toutes les fusions sont propres : créer l’arbre fusionné et un commit de fusion
  6. En cas de conflit : renvoyer MergeResult avec clean = false et les chemins en conflit

Utilisation de CLI

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

Sortie en cas de réussite :

Code
Merge successful.

Sortie en cas de conflit :

Code
CONFLICT in: src/File.php, src/Other.php