병합
Pitmaster은 git merge처럼 3방향 병합으로 브랜치를 병합합니다. 빨리 감기 가능성을 감지하고, 병합 기준을 계산하며, 트리 수준 및 콘텐츠 수준 병합을 수행하고, 파일 충돌 시 표준 충돌 마커를 생성합니다.
기본 병합
$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 클래스는 읽기 전용 값 객체입니다:
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)
$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방향 병합을 수행합니다:
- 병합 기준(가장 가까운 공통 조상)을 찾습니다.
- 트리 수준에서 기준과 현재 브랜치, 기준과 병합 대상 브랜치를 각각 비교합니다.
- 두 브랜치에서 모두 변경된 각 파일에 대해 콘텐츠 수준 병합을 수행합니다.
- 부모가 두 개인 병합 커밋을 만듭니다.
Before: After merge:
C---D (feature) C---D
/ / \
A---B---E---F (main) A---B---E---F---G (main)
병합 기준
병합 기준은 두 커밋의 가장 최근 공통 조상입니다. Pitmaster은 커밋 그래프를 따라가며 이를 찾습니다. 병합 기준이 여러 개인 경우(교차 이력) merge()이 먼저 이를 병합하고 그 결과를 기준으로 사용합니다.
$base = $repo->mergeBase(
$repo->resolve('main'),
$repo->resolve('feature/login'),
);
if ($base !== null) {
echo "Common ancestor: {$base->hex}\n";
}
MergeBase 알고리즘에 직접 접근하려면 다음을 참고하세요:
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);
브랜치 병합 확인
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 클래스는 두 버전을 마커로 감쌉니다:
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 콘텐츠를 병합합니다:
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의 트리는 변경하지 않은 채 병합 커밋을 기록합니다. 변경 사항을 포함하지 않고 브랜치 이력을 유지할 때 유용합니다.
$result = $repo->merge('feature/old', 'ours');
Octopus 병합
두 개 이상의 브랜치를 하나의 병합 커밋으로 병합합니다. 브랜치에서 충돌이 발생하면 병합 상태를 기록하거나 커밋을 만들지 않고 미완료 결과를 반환합니다.
$result = $repo->mergeOctopus(['feature/a', 'feature/b', 'feature/c']);
Pitmaster\Merge의 RecursiveMerge, OursMerge, OctopusMerge 클래스는 콘텐츠 수준 헬퍼이며, Repository가 위 전략을 직접 구현합니다.
Rerere(기록된 해결 방법 재사용)
Pitmaster에는 git의 rr-cache 레이아웃에 해결 방법을 저장하는 독립형 Rerere 클래스가 포함되어 있습니다:
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).
트리 수준 병합 흐름
전체 병합 과정:
MergeBase::findAll()이 공통 조상을 찾습니다.- 기준, 현재 브랜치 및 병합 대상 브랜치의 트리를 경로별로 비교합니다.
- 한 브랜치에서만 변경된 파일은 해당 변경 사항을 적용합니다.
- 두 브랜치에서 모두 변경된 파일의 경우:
- 두 브랜치가 같은 방식으로 변경했다면 두 변경 사항이 일치하므로 어느 쪽이든 적용합니다.
- 변경 사항이 다르면 콘텐츠에
ThreeWayMerge::merge()을 실행합니다.
- 병합이 모두 성공하면 병합된 트리를 구성하고 병합 커밋을 만듭니다.
- 충돌이 있으면
clean = false및 충돌 경로가 포함된MergeResult를 반환합니다.
CLI 사용
./vendor/bin/pitmaster merge feature/login
성공 시 출력:
Merge successful.
충돌 시 출력:
CONFLICT in: src/File.php, src/Other.php