差异
Pitmaster 以 git 的统一格式计算差异。差异引擎支持多种算法、统一差异格式、二进制文件检测和树级递归比较。
对于模式为 100644 的已修改文本文件,format() 生成的字节与 git diff 相同。当前其他情况的格式有所不同(问题 #3):
- 新建和已删除文件没有
new file mode/deleted file mode行,也没有index行和/dev/null:两侧都标记为a/<path>和b/<path>;空侧的 hunk 范围写作-1,0或+1,0,而 git 会写作-0,0或+0,0。 index行始终以100644结尾,与文件模式无关。- 仅模式变更(例如
chmod +x)不会产生结果。 - 二进制文件会输出
Binary files differ,但没有index行;而 git 会输出Binary files a/<path> and b/<path> differ。 - 重命名会显示为一个
diff --git a/<old> b/<new>条目,并附带内容差异,不包含similarity index、rename from或rename to行。
差异操作
未暂存的更改(工作树与索引比较)
$diffs = $repo->diff();
foreach ($diffs as $diff) {
echo $diff->format();
}
将索引中的每个文件与其在工作树中的当前状态进行比较。检测修改、删除和二进制文件。
已暂存的更改(索引与 HEAD 比较)
$diffs = $repo->diffStaged();
foreach ($diffs as $diff) {
echo $diff->format();
}
将当前索引与 HEAD 提交的树进行比较。显示下一次提交将包含的内容。内容相似的已删除和新增文件会作为一个重命名条目报告。首次提交前没有 HEAD,因此结果为空(问题 #5)。
目前,重命名配对也会匹配互不相关的单行文件,因为末尾的空行会被计为共享内容(问题 #4)。
树与树之间的差异
$a = $repo->readObject($repo->resolve('HEAD~1')->hex)->tree;
$b = $repo->head()->tree;
$diffs = $repo->diffTree($a, $b);
递归比较两棵树。传入树 ID:提交 ID 不会被解析为对应的树,因此会得到空结果。同一个 TreeDiff 引擎为 diffStaged()、show() 和 logPath() 提供支持。
按路径筛选
$diffs = $repo->diff('src/specific-file.php');
路径规范是一个精确的文件路径。diffStaged() 接受相同参数,但目前会忽略它,因此始终返回所有已暂存文件(问题 #5)。
选择算法
$diffs = $repo->diff(null, 'patience');
$diffs = $repo->diffStaged(null, 'histogram');
$diffs = $repo->diffTree($a, $b, 'minimal');
有效名称为 myers(默认)、patience、histogram 和 minimal。其他名称都会抛出 InvalidArgumentException。
DiffResult 结构
每个 DiffResult 表示单个文件的差异。
use Pitmaster\Diff\DiffResult;
foreach ($diffs as $diff) {
echo $diff->oldPath; // Path in the old tree
echo $diff->newPath; // Path in the new tree
echo $diff->binary; // true for binary files
echo $diff->oldHash; // Hex hash of the old blob (null for added files)
echo $diff->newHash; // Hex hash of the new blob (null for deleted files)
foreach ($diff->hunks as $hunk) {
echo $hunk->header(); // @@ -1,5 +1,7 @@
foreach ($hunk->lines as $line) {
echo $line . "\n"; // ' context', '+added', '-removed'
}
}
}
格式化输出
echo $diff->format();
生成统一差异输出。对于已修改的文本文件,结果与 git diff 一致:
diff --git a/src/File.php b/src/File.php
index abc1234..def5678 100644
--- a/src/File.php
+++ b/src/File.php
@@ -10,6 +10,8 @@ class File
private string $name;
+ private int $count;
+
public function __construct()
{
检查更改
if ($diff->hasChanges()) {
// File was modified (or is binary with changes)
}
Hunk 结构
每个 Hunk 表示一个包含上下文行的连续更改区域。
use Pitmaster\Diff\Hunk;
$hunk->oldStart; // Starting line in old file
$hunk->oldCount; // Number of lines from old file
$hunk->newStart; // Starting line in new file
$hunk->newCount; // Number of lines in new file
$hunk->lines; // Array of diff lines (prefixed with ' ', '+', or '-')
$hunk->header(); // "@@ -10,6 +10,8 @@"
差异算法
Pitmaster 实现了四种差异算法。默认算法是 Myers。
Myers 差异
标准的 O(ND) 算法,与 git 的默认算法相同。生成最小编辑脚本。
use Pitmaster\Diff\MyersDiff;
$hunks = MyersDiff::diff($oldContent, $newContent);
diff() 方法接受两个字符串并返回一个 Hunk 对象数组。它按换行符拆分输入,计算最短编辑脚本,并将更改分组为带有 3 行上下文的 hunk(与 git 的默认值一致)。
Patience 差异
为代码生成结构更清晰的差异。先匹配唯一行,再填补它们之间的部分。
use Pitmaster\Diff\PatienceDiff;
$hunks = PatienceDiff::diff($oldContent, $newContent);
Histogram 差异
Patience 差异的一种变体,更擅长处理重复行。JGit 和 Eclipse 会使用它。
use Pitmaster\Diff\HistogramDiff;
$hunks = HistogramDiff::diff($oldContent, $newContent);
最小差异
生成尽可能小的差异(更改行数最少),代价是需要更多计算。
use Pitmaster\Diff\MinimalDiff;
$hunks = MinimalDiff::diff($oldContent, $newContent);
二进制文件检测
Pitmaster 会将内容视为二进制数据,条件是其前 8192 字节中包含 NUL 字节(git 会检查前 8000 字节)。
if (MyersDiff::isBinary($content)) {
// Binary file, skip line-level diff
}
二进制文件会生成一个 DiffResult,其中包含 $binary = true 和一个空的 $hunks 数组。格式化输出为:
diff --git a/image.png b/image.png
Binary files differ
彩色输出
CLI 使用 ColorDiff 为终端输出添加 ANSI 颜色。
use Pitmaster\Diff\ColorDiff;
$colorized = ColorDiff::colorize($diff->format());
echo $colorized;
- 新增行(
+)显示为绿色 - 删除行(
-)显示为红色 - Hunk 标头(
@@)显示为青色 - 文件标头(
diff --git、index、---、+++)显示为粗体
pitmaster diff 始终会为输出添加颜色,即使输出被管道重定向也是如此。pitmaster show 仅在 stdout 是终端时才会添加颜色。
树差异(内部)
TreeDiff 类会递归比较两个树对象,并为每个已更改文件生成一个 DiffResult。
use Pitmaster\Diff\TreeDiff;
$treeDiff = new TreeDiff($repo->objectDatabase());
$diffs = $treeDiff->diff($oldTreeId, $newTreeId);
树差异会处理:
- 新增文件(仅存在于新树中)
- 已删除文件(仅存在于旧树中)
- 已修改文件(路径相同、blob 哈希不同)
- 递归比较子目录
- 配对重命名的已删除和新增文件(哈希完全匹配,或至少有 50% 的共享行)
哈希未变化的条目会被跳过,因此仅更改模式不会产生结果。
单词差异
用于检测行内的单词级更改。
use Pitmaster\Diff\WordDiff;
$result = WordDiff::diff('the quick fox', 'the slow fox');
// 'the [-quick-]{+slow+} fox'