
# 병합

Pitmaster은 `git merge`처럼 3방향 병합으로 브랜치를 병합합니다. 빨리 감기 가능성을 감지하고, 병합 기준을 계산하며, 트리 수준 및 콘텐츠 수준 병합을 수행하고, 파일 충돌 시 표준 충돌 마커를 생성합니다.

## 기본 병합

```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에 이미 포함된 브랜치를 병합하면 "Already up to date"를 보고하는 대신 트리가 변경되지 않은 새 병합 커밋이 생성됩니다([#6](https://github.com/inline0/pitmaster/issues/6)).

## 3방향 병합

브랜치가 서로 분기된 경우 Pitmaster은 3방향 병합을 수행합니다:

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

## 콘텐츠 수준 3방향 병합

`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'`을 사용합니다.

## 병합 전략

### Recursive 병합

기본 전략(`$repo->merge($branch)`)으로, 병합 기준이 여러 개인 교차 병합 이력을 처리합니다. CLI은 이를 위해 `-s ort` 및 `-s recursive`도 받습니다.

### Ours 병합

HEAD의 트리는 변경하지 않은 채 병합 커밋을 기록합니다. 변경 사항을 포함하지 않고 브랜치 이력을 유지할 때 유용합니다.

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

### Octopus 병합

두 개 이상의 브랜치를 하나의 병합 커밋으로 병합합니다. 브랜치에서 충돌이 발생하면 병합 상태를 기록하거나 커밋을 만들지 않고 미완료 결과를 반환합니다.

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

`Pitmaster\Merge`의 `RecursiveMerge`, `OursMerge`, `OctopusMerge` 클래스는 콘텐츠 수준 헬퍼이며, `Repository`가 위 전략을 직접 구현합니다.

## Rerere(기록된 해결 방법 재사용)

Pitmaster에는 git의 `rr-cache` 레이아웃에 해결 방법을 저장하는 독립형 `Rerere` 클래스가 포함되어 있습니다:

```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()`은 `.git/config`에서 문자 그대로 `rerere.enabled = true` 텍스트를 찾으며, `git config rerere.enabled true`이 기록하는 `[rerere]` 섹션은 인식하지 못합니다([#20](https://github.com/inline0/pitmaster/issues/20)).

## 트리 수준 병합 흐름

전체 병합 과정:

1. `MergeBase::findAll()`이 공통 조상을 찾습니다.
2. 기준, 현재 브랜치 및 병합 대상 브랜치의 트리를 경로별로 비교합니다.
3. 한 브랜치에서만 변경된 파일은 해당 변경 사항을 적용합니다.
4. 두 브랜치에서 모두 변경된 파일의 경우:
   - 두 브랜치가 같은 방식으로 변경했다면 두 변경 사항이 일치하므로 어느 쪽이든 적용합니다.
   - 변경 사항이 다르면 콘텐츠에 `ThreeWayMerge::merge()`을 실행합니다.
5. 병합이 모두 성공하면 병합된 트리를 구성하고 병합 커밋을 만듭니다.
6. 충돌이 있으면 `clean = false` 및 충돌 경로가 포함된 `MergeResult`를 반환합니다.

## CLI 사용

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

성공 시 출력:
```
Merge successful.
```

충돌 시 출력:
```
CONFLICT in: src/File.php, src/Other.php
```
