Merge
Pitmaster esegue il merge dei branch con un merge a tre vie, come git merge. Rileva le opportunità di fast-forward, calcola le basi di merge, esegue merge a livello di albero e di contenuto e genera i marcatori di conflitto standard quando i file sono in conflitto.
Merge di base
$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 è un oggetto valore in sola lettura:
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)
Quando il merge si interrompe a causa di conflitti, lo stato del conflitto viene scritto nell'indice e nell'albero di lavoro. Risolvete i file, add(), quindi chiamate $repo->mergeContinue() oppure chiamate $repo->mergeAbort() per ripristinare lo stato precedente al merge.
Prima del merge, eseguite il commit o mettete da parte le modifiche locali: il merge ricostruisce l'albero di lavoro a partire dal risultato e sovrascrive le modifiche non confermate nei file tracciati (#2).
Merge fast-forward
Quando l'HEAD corrente è un antenato del branch di destinazione, Pitmaster esegue un merge fast-forward: sposta semplicemente HEAD sul commit di destinazione senza creare un commit di merge.
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
Il caso inverso non è ancora rilevato: un merge di un branch già contenuto in HEAD crea un nuovo commit di merge con un albero invariato, invece di segnalare "Already up to date" (#6).
Merge a tre vie
Quando i branch sono divergenti, Pitmaster esegue un merge a tre vie:
- Trovare la base di merge (l'antenato comune più prossimo)
- Confrontare la base con il nostro branch e con quello altrui a livello di albero
- Per ogni file modificato in entrambi i branch, eseguire il merge a livello di contenuto
- Creare un commit di merge con due genitori
Before: After merge:
C---D (feature) C---D
/ / \
A---B---E---F (main) A---B---E---F---G (main)
Base di merge
La base di merge è l'antenato comune più recente di due commit. Pitmaster la trova percorrendo il grafo dei commit. Se ci sono più basi di merge (cronologia incrociata), merge() le unisce prima e usa il risultato come base.
$base = $repo->mergeBase(
$repo->resolve('main'),
$repo->resolve('feature/login'),
);
if ($base !== null) {
echo "Common ancestor: {$base->hex}\n";
}
Per accedere direttamente all'algoritmo di 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);
Verifica del merge dei branch
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
}
Marcatori di conflitto
Quando un file viene modificato in entrambi i branch e le modifiche si sovrappongono, Pitmaster genera i marcatori di conflitto standard. merge() etichetta i due lati con HEAD e con il nome del branch risultante dal merge:
<<<<<<< HEAD
return $this->value + 1;
=======
return $this->value * 2;
>>>>>>> feature
La classe ConflictMarker racchiude due versioni tra marcatori:
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');
Merge a tre vie a livello di contenuto
La classe ThreeWayMerge unisce il contenuto dei blob (non i file):
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
Accetta tre stringhe (la versione di base, la nostra versione e quella altrui), oltre a etichette facoltative e a uno stile di conflitto ('merge' per impostazione predefinita, oppure 'diff3' per includere la sezione di base).
Strategie di merge
Merge ricorsivo
La strategia predefinita ($repo->merge($branch)) gestisce le cronologie di merge incrociate con più basi di merge. Anche CLI accetta -s ort e -s recursive.
Merge ours
Registra un commit di merge che lascia invariato l'albero di HEAD. Utile per mantenere la cronologia dei branch senza incorporare modifiche.
$result = $repo->merge('feature/old', 'ours');
Merge octopus
Unisce due o più branch in un unico commit di merge. Se un branch qualsiasi presenta conflitti, restituisce un risultato non pulito senza scrivere lo stato del merge né creare un commit.
$result = $repo->mergeOctopus(['feature/a', 'feature/b', 'feature/c']);
Le classi RecursiveMerge, OursMerge e OctopusMerge in Pitmaster\Merge sono helper a livello di contenuto; Repository implementa autonomamente le strategie sopra descritte.
Rerere (riutilizzo della risoluzione registrata)
Pitmaster include una classe Rerere autonoma che memorizza le risoluzioni nel formato rr-cache di 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() non la chiama: le risoluzioni vengono registrate e riprodotte solo quando lo fa il vostro codice. isEnabled() cerca il testo letterale rerere.enabled = true in .git/config e non riconosce la sezione [rerere] scritta da git config rerere.enabled true (#20).
Flusso di merge a livello di albero
Il processo completo di merge:
MergeBase::findAll()individua gli antenati comuni- Gli alberi base, nostro e altrui vengono confrontati percorso per percorso
- Per i file modificati in un solo branch: si applica quella modifica
- Per i file modificati in entrambi i branch:
- Se sono stati modificati nello stesso modo: si applica una delle due modifiche (sono identiche)
- Se le modifiche differiscono: si esegue
ThreeWayMerge::merge()sul contenuto
- Se tutti i merge vanno a buon fine: si crea l'albero risultante e un commit di merge
- Se ci sono conflitti: si restituisce
MergeResultconclean = falsee i percorsi in conflitto
Utilizzo di CLI
./vendor/bin/pitmaster merge feature/login
Output in caso di successo:
Merge successful.
Output in caso di conflitto:
CONFLICT in: src/File.php, src/Other.php