Перейти до вмісту

Злиття

Pitmaster виконує злиття гілок за допомогою тристороннього злиття, як git merge. Він виявляє можливості перемотування вперед, обчислює бази злиття, виконує злиття на рівні дерева та вмісту й створює стандартні маркери конфліктів для файлів із конфліктами.

Базове злиття

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

Клас MergeResult — це об’єкт-значення лише для читання:

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)

Якщо злиття зупиняється через конфлікти, їхній стан записується до індексу й робочого дерева. Вирішіть конфлікти у файлах, виконайте add() і викличте $repo->mergeContinue() або викличте $repo->mergeAbort(), щоб відновити стан до злиття.

Перш ніж виконувати злиття, зафіксуйте локальні зміни або сховайте їх: злиття перебудовує робоче дерево за результатом і перезаписує незбережені зміни у відстежуваних файлах (#2).

Злиття з перемотуванням уперед

Якщо поточний HEAD є предком цільової гілки, Pitmaster виконує злиття з перемотуванням уперед: просто переміщує HEAD до цільового коміту, не створюючи коміту злиття.

Код
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

Зворотний випадок поки що не виявляється: злиття гілки, яка вже міститься в HEAD, створює новий коміт злиття з незміненим деревом замість повідомлення «Already up to date» (#6).

Тристороннє злиття

Якщо гілки розійшлися, Pitmaster виконує тристороннє злиття:

  1. Знаходить базу злиття (найближчого спільного предка)
  2. Порівнює дерево від бази до нашої гілки та від бази до їхньої гілки
  3. Для кожного файлу, зміненого в обох гілках, виконує злиття на рівні вмісту
  4. Створює коміт злиття з двома батьківськими комітами
Код
Before:              After merge:

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

База злиття

База злиття — це найновіший спільний предок двох комітів. Pitmaster знаходить її, проходячи граф комітів. Якщо баз злиття кілька (історія перехресних злиттів), merge() спочатку зливає їх, а потім використовує результат як базу.

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

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

Для безпосереднього доступу до алгоритму 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);

Перевірка злиття гілок

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
}

Маркери конфліктів

Якщо файл змінено в обох гілках і ці зміни перетинаються, Pitmaster створює стандартні маркери конфліктів. merge() позначає сторони як HEAD та назву гілки злиття:

Код
<<<<<<< HEAD
    return $this->value + 1;
=======
    return $this->value * 2;
>>>>>>> feature

Клас ConflictMarker обрамляє дві версії маркерами:

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

Тристороннє злиття на рівні вмісту

Клас ThreeWayMerge зливає вміст blob-об’єктів (не файлів):

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

Він приймає три рядки (базову версію, нашу версію та їхню версію), необов’язкові мітки й стиль конфлікту (за замовчуванням 'merge' або 'diff3', щоб включити базовий розділ).

Стратегії злиття

Рекурсивне злиття

Стратегія за замовчуванням ($repo->merge($branch)) обробляє історії перехресних злиттів із кількома базами злиття. CLI також приймає для неї -s ort і -s recursive.

Злиття «наша версія»

Записує коміт злиття, залишаючи дерево HEAD незмінним. Корисно для збереження історії гілки без включення змін.

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

Злиття «восьминіг»

Зливає дві або більше гілок в один коміт злиття. Якщо в будь-якій гілці є конфлікти, повертає неузгоджений результат, не записуючи стан злиття й не створюючи коміт.

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

Класи RecursiveMerge, OursMerge і OctopusMerge у Pitmaster\Merge — це допоміжні засоби для злиття на рівні вмісту; Repository самостійно реалізує наведені вище стратегії.

Rerere (повторне використання записаного вирішення)

Pitmaster містить окремий клас Rerere, який зберігає вирішення у форматі rr-cache від 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() не викликає його: вирішення записуються й відтворюються лише тоді, коли це робить ваш код. isEnabled() шукає буквальний текст rerere.enabled = true у .git/config і не розпізнає розділ [rerere], який записує git config rerere.enabled true (#20).

Процес злиття на рівні дерева

Повний процес злиття:

  1. MergeBase::findAll() знаходить спільних предків
  2. Дерева бази, нашої та їхньої гілок порівнюються за шляхами
  3. Для файлів, змінених лише в одній гілці: застосовується ця зміна
  4. Для файлів, змінених в обох гілках:
    • Якщо обидві гілки змінили файл однаково: застосовується будь-яка з цих змін (вони збігаються)
    • Якщо зміни відрізняються: до вмісту застосовується ThreeWayMerge::merge()
  5. Якщо всі злиття завершилися без конфліктів: створюється об’єднане дерево й коміт злиття
  6. Якщо є конфлікти: повертається MergeResult з clean = false і шляхами файлів із конфліктами

Використання CLI

Командна оболонка
./vendor/bin/pitmaster merge feature/login

Вивід у разі успіху:

Код
Merge successful.

Вивід за наявності конфлікту:

Код
CONFLICT in: src/File.php, src/Other.php