マージ
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()を呼び出してマージ前の状態に戻します。
マージ前にローカルの変更をコミットするかstashしてください。マージでは結果から作業ツリーを再構築し、追跡対象ファイルの未コミット変更を上書きします(#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がすでに含んでいるブランチをマージすると、「すでに最新です」と報告されず、ツリーに変更のない新しいマージコミットが作成されます(#6)。
三者マージ
ブランチの履歴が分岐している場合、Pitmasterは3ウェイマージを実行します:
- マージベース(最も近い共通祖先)を見つける
- ベースから自分側、ベースから相手側への差分をツリーレベルで計算する
- 両方のブランチで変更された各ファイルについて、コンテンツレベルのマージを実行する
- 2つの親を持つマージコミットを作成する
Before: After merge:
C---D (feature) C---D
/ / \
A---B---E---F (main) A---B---E---F---G (main)
マージベース
マージベースは2つのコミットの最新の共通祖先です。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クラスは2つのバージョンをマーカーで囲みます:
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
ベース、自分側、相手側の3つの文字列に、任意のラベルと競合スタイルを指定できます。競合スタイルは既定で'merge'、ベース部分を含める場合は'diff3'です。
マージ戦略
再帰的マージ
既定の戦略($repo->merge($branch))で、マージベースが複数ある交差マージの履歴を処理します。CLIでは、-s ortと-s recursiveも指定できます。
Oursマージ
HEADのツリーを変更せずにマージコミットを記録します。変更を取り込まずにブランチの履歴を維持する場合に便利です。
$result = $repo->merge('feature/old', 'ours');
Octopusマージ
2つ以上のブランチを1つのマージコミットにまとめます。いずれかのブランチで競合が発生すると、マージ状態を書き込まず、コミットも作成せずに、未解決の結果を返します。
$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