본문으로 건너뛰기

병합

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).

빨리 감기 병합

현재 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).

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).

트리 수준 병합 흐름

전체 병합 과정:

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

CLI 사용

셸
./vendor/bin/pitmaster merge feature/login

성공 시 출력:

코드
Merge successful.

충돌 시 출력:

코드
CONFLICT in: src/File.php, src/Other.php