跳转到正文

合并

Pitmaster 使用三方合并来合并分支,类似于 git merge。它会检测快进机会、计算合并基、执行树级和内容级合并,并在文件发生冲突时生成标准冲突标记。

基本合并

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 已包含的分支时,会创建一个树未变化的新合并提交,而不是报告“已经是最新状态”(问题 #6)。

三方合并

当分支出现分歧时,Pitmaster 会执行三方合并:

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

内容级三方合并

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' 来包含基准部分)。

合并策略

递归合并

默认策略($repo->merge($branch)),用于处理具有多个合并基的交叉合并历史。CLI 还接受 -s ort 和 -s recursive 作为其参数。

本方合并

创建一个保持 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 包含一个独立的 Rerere 类,用于按照 git 的 rr-cache 布局存储解决方案:

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