Слияние
Pitmaster выполняет слияние ветвей методом трехстороннего слияния, как git merge. Он обнаруживает возможность перемотки вперед, вычисляет базы слияния, выполняет слияние на уровне деревьев и содержимого, а также добавляет стандартные маркеры конфликтов, если файлы конфликтуют.
Базовое слияние
$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
Класс MergeResult — это объект-значение только для чтения:
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)
Если слияние останавливается из-за конфликтов, состояние конфликта записывается в индекс и рабочее дерево. Разрешите конфликты в файлах, добавьте их в индекс с помощью add() и вызовите $repo->mergeContinue() либо вызовите $repo->mergeAbort(), чтобы восстановить состояние до слияния.
Перед слиянием сохраните локальные изменения в коммите или в хранилище: слияние заново формирует рабочее дерево по результату и перезаписывает незакоммиченные изменения в отслеживаемых файлах (#2).
Слияние перемоткой вперед
Если текущий HEAD является предком целевой ветви, Pitmaster выполняет слияние перемоткой вперед: просто перемещает HEAD на целевой коммит, не создавая коммит слияния.
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
Обратный случай пока не обнаруживается: слияние ветви, которая уже содержится в HEAD, создает новый коммит слияния с неизменившимся деревом вместо сообщения «Уже актуально» (#6).
Слияние трёх ветвей
Если ветви разошлись, Pitmaster выполняет трехстороннее слияние:
- Найти базу слияния (ближайшего общего предка)
- Сравнить базу с нашей и их версиями на уровне дерева
- Для каждого файла, измененного в обеих ветвях, выполнить слияние на уровне содержимого
- Создать коммит слияния с двумя родителями
Before: After merge:
C---D (feature) C---D
/ / \
A---B---E---F (main) A---B---E---F---G (main)
База слияния
База слияния — это самый недавний общий предок двух коммитов. Pitmaster находит ее, обходя граф коммитов. Если баз слияния несколько (история с перекрестными слияниями), merge() сначала объединяет их, а затем использует результат как базу.
$base = $repo->mergeBase(
$repo->resolve('main'),
$repo->resolve('feature/login'),
);
if ($base !== null) {
echo "Common ancestor: {$base->hex}\n";
}
Для прямого доступа к алгоритму 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);
Проверка слияния ветвей
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
}
Маркеры конфликтов
Если файл изменен в обеих ветвях и изменения пересекаются, Pitmaster добавляет стандартные маркеры конфликтов. merge() помечает стороны как HEAD и указывает имя объединенной ветви:
<<<<<<< HEAD
return $this->value + 1;
=======
return $this->value * 2;
>>>>>>> feature
Класс ConflictMarker обрамляет две версии маркерами:
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');
Трехстороннее слияние на уровне содержимого
Класс ThreeWayMerge объединяет содержимое blob-объектов (не файлов):
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
Он принимает три строки (базовую версию, нашу версию и их версию), необязательные метки и стиль конфликтов (по умолчанию 'merge' или 'diff3', чтобы включить базовый раздел).
Стратегии слияния
Рекурсивное слияние
Стратегия по умолчанию ($repo->merge($branch)) обрабатывает истории перекрестных слияний с несколькими базами слияния. Класс CLI также принимает для нее -s ort и -s recursive.
Слияние «наша версия»
Создает коммит слияния, сохраняя дерево HEAD без изменений. Полезно для сохранения истории ветвей без включения изменений.
$result = $repo->merge('feature/old', 'ours');
Осьминожье слияние
Объединяет две или более ветвей в один коммит слияния. Если в любой ветви есть конфликты, возвращает результат с конфликтами, не записывая состояние слияния и не создавая коммит.
$result = $repo->mergeOctopus(['feature/a', 'feature/b', 'feature/c']);
Классы RecursiveMerge, OursMerge и OctopusMerge в Pitmaster\Merge помогают выполнять слияние на уровне содержимого; Repository самостоятельно реализует описанные выше стратегии.
Rerere (повторное использование сохраненного решения)
Pitmaster включает отдельный класс Rerere, который хранит решения в формате rr-cache, используемом в 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() не вызывает его: решения записываются и применяются повторно, только если ваш код делает это сам. isEnabled() ищет буквальный текст rerere.enabled = true в .git/config и не распознает раздел [rerere], который записывает git config rerere.enabled true (#20).
Процесс слияния на уровне деревьев
Полный процесс слияния:
MergeBase::findAll()находит общих предков- Базовое, наше и их деревья сравниваются путь за путем
- Для файлов, измененных только в одной ветви, принимается это изменение
- Для файлов, измененных в обеих ветвях:
- Если изменения одинаковы, принимается любое из них (они совпадают)
- Если изменения различаются, содержимое обрабатывается с помощью
ThreeWayMerge::merge()
- Если слияние прошло без конфликтов, создается объединенное дерево и коммит слияния
- При наличии конфликтов возвращается
MergeResultсclean = falseи путями конфликтующих файлов
Использование CLI
./vendor/bin/pitmaster merge feature/login
Вывод при успехе:
Merge successful.
Вывод при конфликте:
CONFLICT in: src/File.php, src/Other.php