
# 差异

Pitmaster 以 git 的统一格式计算差异。差异引擎支持多种算法、统一差异格式、二进制文件检测和树级递归比较。

对于模式为 `100644` 的已修改文本文件，`format()` 生成的字节与 `git diff` 相同。当前其他情况的格式有所不同（[问题 #3](https://github.com/inline0/pitmaster/issues/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` 行。

## 差异操作

### 未暂存的更改（工作树与索引比较）

```php
$diffs = $repo->diff();

foreach ($diffs as $diff) {
    echo $diff->format();
}
```

将索引中的每个文件与其在工作树中的当前状态进行比较。检测修改、删除和二进制文件。

### 已暂存的更改（索引与 HEAD 比较）

```php
$diffs = $repo->diffStaged();

foreach ($diffs as $diff) {
    echo $diff->format();
}
```

将当前索引与 HEAD 提交的树进行比较。显示下一次提交将包含的内容。内容相似的已删除和新增文件会作为一个重命名条目报告。首次提交前没有 HEAD，因此结果为空（[问题 #5](https://github.com/inline0/pitmaster/issues/5)）。

目前，重命名配对也会匹配互不相关的单行文件，因为末尾的空行会被计为共享内容（[问题 #4](https://github.com/inline0/pitmaster/issues/4)）。

### 树与树之间的差异

```php
$a = $repo->readObject($repo->resolve('HEAD~1')->hex)->tree;
$b = $repo->head()->tree;
$diffs = $repo->diffTree($a, $b);
```

递归比较两棵树。传入树 ID：提交 ID 不会被解析为对应的树，因此会得到空结果。同一个 `TreeDiff` 引擎为 `diffStaged()`、`show()` 和 `logPath()` 提供支持。

### 按路径筛选

```php
$diffs = $repo->diff('src/specific-file.php');
```

路径规范是一个精确的文件路径。`diffStaged()` 接受相同参数，但目前会忽略它，因此始终返回所有已暂存文件（[问题 #5](https://github.com/inline0/pitmaster/issues/5)）。

### 选择算法

```php
$diffs = $repo->diff(null, 'patience');
$diffs = $repo->diffStaged(null, 'histogram');
$diffs = $repo->diffTree($a, $b, 'minimal');
```

有效名称为 `myers`（默认）、`patience`、`histogram` 和 `minimal`。其他名称都会抛出 `InvalidArgumentException`。

## DiffResult 结构

每个 `DiffResult` 表示单个文件的差异。

```php
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'
        }
    }
}
```

### 格式化输出

```php
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()
     {
```

### 检查更改

```php
if ($diff->hasChanges()) {
    // File was modified (or is binary with changes)
}
```

## Hunk 结构

每个 `Hunk` 表示一个包含上下文行的连续更改区域。

```php
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 的默认算法相同。生成最小编辑脚本。

```php
use Pitmaster\Diff\MyersDiff;

$hunks = MyersDiff::diff($oldContent, $newContent);
```

`diff()` 方法接受两个字符串并返回一个 `Hunk` 对象数组。它按换行符拆分输入，计算最短编辑脚本，并将更改分组为带有 3 行上下文的 hunk（与 git 的默认值一致）。

### Patience 差异

为代码生成结构更清晰的差异。先匹配唯一行，再填补它们之间的部分。

```php
use Pitmaster\Diff\PatienceDiff;

$hunks = PatienceDiff::diff($oldContent, $newContent);
```

### Histogram 差异

Patience 差异的一种变体，更擅长处理重复行。JGit 和 Eclipse 会使用它。

```php
use Pitmaster\Diff\HistogramDiff;

$hunks = HistogramDiff::diff($oldContent, $newContent);
```

### 最小差异

生成尽可能小的差异（更改行数最少），代价是需要更多计算。

```php
use Pitmaster\Diff\MinimalDiff;

$hunks = MinimalDiff::diff($oldContent, $newContent);
```

## 二进制文件检测

Pitmaster 会将内容视为二进制数据，条件是其前 8192 字节中包含 NUL 字节（git 会检查前 8000 字节）。

```php
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 颜色。

```php
use Pitmaster\Diff\ColorDiff;

$colorized = ColorDiff::colorize($diff->format());
echo $colorized;
```

- 新增行（`+`）显示为绿色
- 删除行（`-`）显示为红色
- Hunk 标头（`@@`）显示为青色
- 文件标头（`diff --git`、`index`、`---`、`+++`）显示为粗体

`pitmaster diff` 始终会为输出添加颜色，即使输出被管道重定向也是如此。`pitmaster show` 仅在 stdout 是终端时才会添加颜色。

## 树差异（内部）

`TreeDiff` 类会递归比较两个树对象，并为每个已更改文件生成一个 `DiffResult`。

```php
use Pitmaster\Diff\TreeDiff;

$treeDiff = new TreeDiff($repo->objectDatabase());
$diffs = $treeDiff->diff($oldTreeId, $newTreeId);
```

树差异会处理：
- 新增文件（仅存在于新树中）
- 已删除文件（仅存在于旧树中）
- 已修改文件（路径相同、blob 哈希不同）
- 递归比较子目录
- 配对重命名的已删除和新增文件（哈希完全匹配，或至少有 50% 的共享行）

哈希未变化的条目会被跳过，因此仅更改模式不会产生结果。

## 单词差异

用于检测行内的单词级更改。

```php
use Pitmaster\Diff\WordDiff;

$result = WordDiff::diff('the quick fox', 'the slow fox');
// 'the [-quick-]{+slow+} fox'
```
