Przejdź do treści

Scalanie

Pitmaster scala gałęzie metodą trójstronną, podobnie jak git merge. Wykrywa możliwość przewinięcia do przodu, oblicza bazy scalania, scala drzewa i zawartość oraz generuje standardowe znaczniki konfliktu, gdy pliki są w konflikcie.

Podstawowe scalanie

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

Klasa MergeResult to obiekt wartości tylko do odczytu:

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)

Gdy scalanie zatrzyma się z powodu konfliktów, ich stan jest zapisywany w indeksie i drzewie roboczym. Rozwiąż konflikty w plikach, dodaj je do indeksu za pomocą add() i wywołaj $repo->mergeContinue() albo wywołaj $repo->mergeAbort(), aby przywrócić stan sprzed scalania.

Przed scalaniem zatwierdź lub odłóż lokalne zmiany na stos: scalanie odtwarza drzewo robocze na podstawie wyniku i nadpisuje niezatwierdzone zmiany w śledzonych plikach (#2).

Scalanie z przewinięciem do przodu

Jeśli bieżący HEAD jest przodkiem docelowej gałęzi, Pitmaster wykonuje scalanie przez przewinięcie do przodu: po prostu przesuwa HEAD do docelowego commita bez tworzenia commita scalającego.

Kod
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

Odwrotny przypadek nie jest jeszcze wykrywany: scalenie gałęzi, którą HEAD już zawiera, tworzy nowy commit scalający z niezmienionym drzewem, zamiast wyświetlić komunikat „Already up to date” (#6).

Scalenie trójstronne

Gdy gałęzie rozeszły się, Pitmaster wykonuje scalanie trójstronne:

  1. Znajduje bazę scalania (najniższego wspólnego przodka)
  2. Porównuje bazę z naszą gałęzią i bazę z ich gałęzią na poziomie drzewa
  3. Dla każdego pliku zmienionego w obu gałęziach wykonuje scalanie zawartości
  4. Tworzy commit scalający z dwoma rodzicami
Kod
Before:              After merge:

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

Baza scalania

Baza scalania to najbliższy wspólny przodek dwóch commitów. Pitmaster znajduje ją, przechodząc przez graf commitów. Jeśli istnieje kilka baz scalania (historia typu criss-cross), merge() najpierw je scala i używa wyniku jako bazy.

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

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

Aby uzyskać bezpośredni dostęp do algorytmu 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);

Sprawdzanie scalania gałęzi

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
}

Znaczniki konfliktu

Gdy plik został zmodyfikowany w obu gałęziach, a zmiany na siebie nachodzą, Pitmaster generuje standardowe znaczniki konfliktu. merge() oznacza strony jako HEAD i nazwą scalanej gałęzi:

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

Klasa ConflictMarker otacza dwie wersje znacznikami:

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

Trójstronne scalanie zawartości

Klasa ThreeWayMerge scala zawartość blobów (nie plików):

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

Przyjmuje trzy ciągi znaków (wersję bazową, naszą wersję i ich wersję), opcjonalne etykiety oraz styl konfliktu (domyślnie 'merge' lub 'diff3', aby uwzględnić sekcję bazową).

Strategie scalania

Scalanie rekurencyjne

Strategia domyślna ($repo->merge($branch)), obsługuje historie scalania typu criss-cross z wieloma bazami scalania. CLI przyjmuje dla niej także -s ort i -s recursive.

Scalanie naszej wersji

Zapisuje commit scalający, który pozostawia drzewo HEAD bez zmian. Przydatne do zachowania historii gałęzi bez uwzględniania zmian.

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

Scalanie typu octopus

Scala dwie lub więcej gałęzi w jeden commit scalający. Jeśli wystąpi konflikt w którejkolwiek gałęzi, zwraca wynik z konfliktami, nie zapisując stanu scalania ani nie tworząc commita.

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

Klasy RecursiveMerge, OursMerge i OctopusMerge w Pitmaster\Merge to pomocnicze narzędzia do scalania zawartości; Repository samodzielnie implementuje powyższe strategie.

Rerere (ponowne użycie zapisanego rozwiązania)

Pitmaster zawiera samodzielną klasę Rerere, która przechowuje rozwiązania w układzie rr-cache używanym przez 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() jej nie wywołuje: rozwiązania są zapisywane i odtwarzane tylko wtedy, gdy zrobi to Twój kod. isEnabled() szuka dosłownego tekstu rerere.enabled = true w .git/config i nie rozpoznaje sekcji [rerere], którą zapisuje git config rerere.enabled true (#20).

Przebieg scalania na poziomie drzewa

Pełny proces scalania:

  1. MergeBase::findAll() znajduje wspólnych przodków
  2. Drzewa bazy, naszej gałęzi i ich gałęzi są porównywane ścieżka po ścieżce
  3. W przypadku plików zmienionych tylko w jednej gałęzi: przyjmuje tę zmianę
  4. W przypadku plików zmienionych w obu gałęziach:
    • Jeśli obie zmiany są takie same: przyjmuje dowolną z nich (są zgodne)
    • Jeśli zmiany się różnią: uruchamia ThreeWayMerge::merge() na zawartości
  5. Jeśli wszystkie scalania przebiegną bez konfliktów: buduje scalone drzewo i tworzy commit scalający
  6. Jeśli wystąpią konflikty: zwraca MergeResult wraz z clean = false i ścieżkami konfliktów

Użycie CLI

Powłoka
./vendor/bin/pitmaster merge feature/login

Dane wyjściowe po pomyślnym zakończeniu:

Kod
Merge successful.

Wynik w przypadku konfliktu:

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