
# Слияние

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](https://github.com/inline0/pitmaster/issues/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, создает новый коммит слияния с неизменившимся деревом вместо сообщения «Уже актуально» ([#6](https://github.com/inline0/pitmaster/issues/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](https://github.com/inline0/pitmaster/issues/20)).

## Процесс слияния на уровне деревьев

Полный процесс слияния:

1. `MergeBase::findAll()` находит общих предков
2. Базовое, наше и их деревья сравниваются путь за путем
3. Для файлов, измененных только в одной ветви, принимается это изменение
4. Для файлов, измененных в обеих ветвях:
   - Если изменения одинаковы, принимается любое из них (они совпадают)
   - Если изменения различаются, содержимое обрабатывается с помощью `ThreeWayMerge::merge()`
5. Если слияние прошло без конфликтов, создается объединенное дерево и коммит слияния
6. При наличии конфликтов возвращается `MergeResult` с `clean = false` и путями конфликтующих файлов

## Использование CLI

```bash
./vendor/bin/pitmaster merge feature/login
```

Вывод при успехе:
```
Merge successful.
```

Вывод при конфликте:
```
CONFLICT in: src/File.php, src/Other.php
```
