Merge
Pitmaster performs branch merges with a three-way merge, like git merge. It detects fast-forward opportunities, computes merge bases, performs tree-level and content-level merges, and generates standard conflict markers when files conflict.
Basic merge
$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
The MergeResult class is a readonly value object:
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)
When the merge stops on conflicts, the conflict state is written to the index and working tree. Resolve the files, add() them, and call $repo->mergeContinue(), or call $repo->mergeAbort() to restore the pre-merge state.
Before merging, commit or stash local changes: the merge rebuilds the working tree from the result and overwrites uncommitted changes in tracked files (#2).
Fast-forward merges
When the current HEAD is an ancestor of the target branch, Pitmaster performs a fast-forward merge: it simply moves HEAD to the target commit without creating a merge commit.
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
The reverse case is not detected yet: merging a branch that HEAD already contains creates a new merge commit with an unchanged tree instead of reporting "Already up to date" (#6).
Three-way merge
When branches have diverged, Pitmaster performs a three-way merge:
- Find the merge base (lowest common ancestor)
- Diff base-to-ours and base-to-theirs at the tree level
- For each file changed in both branches, perform content-level merge
- Create a merge commit with two parents
Before: After merge:
C---D (feature) C---D
/ / \
A---B---E---F (main) A---B---E---F---G (main)
Merge base
The merge base is the most recent common ancestor of two commits. Pitmaster finds it by walking the commit graph. When there are several merge bases (criss-cross history), merge() merges them first and uses the result as the base.
$base = $repo->mergeBase(
$repo->resolve('main'),
$repo->resolve('feature/login'),
);
if ($base !== null) {
echo "Common ancestor: {$base->hex}\n";
}
For direct access to the MergeBase algorithm:
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);
Branch merge check
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
}
Conflict markers
When a file is modified in both branches and the changes overlap, Pitmaster generates standard conflict markers. merge() labels the sides HEAD and the merged branch name:
<<<<<<< HEAD
return $this->value + 1;
=======
return $this->value * 2;
>>>>>>> feature
The ConflictMarker class wraps two versions in markers:
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');
Content-level three-way merge
The ThreeWayMerge class merges blob content (not files):
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
It takes three strings (the base version, our version, and their version) plus optional labels, and a conflict style ('merge' by default, or 'diff3' to include the base section).
Merge strategies
Recursive merge
The default strategy ($repo->merge($branch)), handles criss-cross merge histories with multiple merge bases. The CLI also accepts -s ort and -s recursive for it.
Ours merge
Records a merge commit that keeps HEAD's tree unchanged. Useful for maintaining branch history without incorporating changes.
$result = $repo->merge('feature/old', 'ours');
Octopus merge
Merges two or more branches into a single merge commit. If any branch conflicts, it returns an unclean result without writing merge state or creating a commit.
$result = $repo->mergeOctopus(['feature/a', 'feature/b', 'feature/c']);
The RecursiveMerge, OursMerge, and OctopusMerge classes in Pitmaster\Merge are content-level helpers; Repository implements the strategies above itself.
Rerere (reuse recorded resolution)
Pitmaster includes a standalone Rerere class that stores resolutions in git's rr-cache layout:
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() does not call it: resolutions are recorded and replayed only when your code does so. isEnabled() looks for the literal text rerere.enabled = true in .git/config and does not recognise the [rerere] section that git config rerere.enabled true writes (#20).
Tree-level merge flow
The full merge process:
MergeBase::findAll()locates the common ancestors- The base, ours, and theirs trees are compared path by path
- For files changed only in one branch: take that change
- For files changed in both branches:
- If both changed the same way: take either (they agree)
- If changes differ: run
ThreeWayMerge::merge()on the content
- If all merges are clean: build the merged tree and create a merge commit
- If any conflicts: return
MergeResultwithclean = falseand the conflict paths
CLI usage
./vendor/bin/pitmaster merge feature/login
Output on success:
Merge successful.
Output on conflict:
CONFLICT in: src/File.php, src/Other.php