
# Diff

Pitmaster calcula diffs no formato unificado de git. O mecanismo de diff oferece suporte a vários algoritmos, formatação de diff unificado, detecção de arquivos binários e comparação recursiva no nível da árvore.

Para um arquivo de texto modificado com modo `100644`, `format()` produz os mesmos bytes que `git diff`. Atualmente, os outros casos são formatados de forma diferente ([#3](https://github.com/inline0/pitmaster/issues/3)):

- Arquivos novos e excluídos não têm linha `new file mode` / `deleted file mode`, linha `index` nem `/dev/null`: os dois lados recebem os rótulos `a/<path>` e `b/<path>`, e o intervalo do hunk do lado vazio é escrito como `-1,0` ou `+1,0`, enquanto git escreve `-0,0` ou `+0,0`.
- A linha `index` sempre termina em `100644`, seja qual for o modo do arquivo.
- Alterações apenas no modo (por exemplo, `chmod +x`) não geram resultado.
- Arquivos binários exibem `Binary files differ` sem uma linha `index`, enquanto git exibe `Binary files a/<path> and b/<path> differ`.
- Renomeações são mostradas como uma única entrada `diff --git a/<old> b/<new>` com diff de conteúdo, sem linhas `similarity index`, `rename from` ou `rename to`.

## Operações de diff

### Alterações não preparadas (árvore de trabalho vs índice)

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

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

Compara cada arquivo do índice com seu estado atual na árvore de trabalho. Detecta modificações, exclusões e arquivos binários.

### Alterações preparadas (índice vs HEAD)

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

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

Compara o índice atual com a árvore do commit HEAD. Mostra o que será incluído no próximo commit. Arquivos excluídos e adicionados com conteúdo semelhante são relatados como uma única entrada renomeada. Antes do primeiro commit, não há HEAD e o resultado é vazio ([#5](https://github.com/inline0/pitmaster/issues/5)).

Atualmente, o pareamento de renomeações também corresponde a arquivos de uma linha sem relação entre si, porque a linha vazia final conta como conteúdo compartilhado ([#4](https://github.com/inline0/pitmaster/issues/4)).

### Diff entre árvores

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

Compara duas árvores recursivamente. Passe IDs de árvore: IDs de commit não são convertidos em suas árvores e retornam um resultado vazio. O mesmo mecanismo `TreeDiff` dá suporte a `diffStaged()`, `show()` e `logPath()`.

### Filtrar por caminho

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

A pathspec é um caminho exato de arquivo. `diffStaged()` aceita o mesmo argumento, mas por enquanto o ignora, então sempre retorna todos os arquivos preparados ([#5](https://github.com/inline0/pitmaster/issues/5)).

### Escolher um algoritmo

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

Os nomes válidos são `myers` (padrão), `patience`, `histogram` e `minimal`. Qualquer outro nome lança `InvalidArgumentException`.

## Estrutura de DiffResult

Cada `DiffResult` representa o diff de um único arquivo.

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

### Saída formatada

```php
echo $diff->format();
```

Gera saída de diff unificado. Para um arquivo de texto modificado, corresponde a `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()
     {
```

### Verificar alterações

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

## Estrutura de hunk

Cada `Hunk` representa uma região contínua de alterações com linhas de contexto.

```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 @@"
```

## Algoritmos de diff

Pitmaster implementa quatro algoritmos de diff. O padrão é Myers.

### Diff de Myers

O algoritmo O(ND) padrão, igual ao padrão de git. Produz scripts de edição mínimos.

```php
use Pitmaster\Diff\MyersDiff;

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

O método `diff()` recebe duas strings e retorna um array de objetos `Hunk`. Divide a entrada em novas linhas, calcula o script de edição mais curto e agrupa as alterações em hunks com 3 linhas de contexto (igual ao padrão de git).

### Diff de Patience

Gera diffs estruturais melhores para código. Primeiro corresponde linhas únicas e, em seguida, preenche os intervalos entre elas.

```php
use Pitmaster\Diff\PatienceDiff;

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

### Diff de histograma

Uma variante do diff de patience que lida melhor com linhas repetidas. Usada por JGit e Eclipse.

```php
use Pitmaster\Diff\HistogramDiff;

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

### Diff mínimo

Gera o menor diff possível (com o menor número de linhas alteradas), ao custo de mais processamento.

```php
use Pitmaster\Diff\MinimalDiff;

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

## Detecção de binários

Pitmaster trata o conteúdo como binário quando ele contém um byte NUL em seus primeiros 8192 bytes (git verifica os primeiros 8000).

```php
if (MyersDiff::isBinary($content)) {
    // Binary file, skip line-level diff
}
```

Arquivos binários produzem um `DiffResult` com `$binary = true` e um array `$hunks` vazio. A saída formatada é:

```
diff --git a/image.png b/image.png
Binary files differ
```

## Saída colorida

O CLI usa `ColorDiff` para saída de terminal com cores ANSI.

```php
use Pitmaster\Diff\ColorDiff;

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

- Linhas adicionadas (`+`) aparecem em verde
- Linhas removidas (`-`) aparecem em vermelho
- Cabeçalhos de hunk (`@@`) aparecem em ciano
- Cabeçalhos de arquivo (`diff --git`, `index`, `---`, `+++`) aparecem em negrito

`pitmaster diff` sempre aplica cores, inclusive quando sua saída é redirecionada por pipe. `pitmaster show` aplica cores somente quando stdout é um terminal.

## Diff de árvore (interno)

A classe `TreeDiff` compara dois objetos de árvore recursivamente, produzindo um `DiffResult` para cada arquivo alterado.

```php
use Pitmaster\Diff\TreeDiff;

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

O diff de árvore trata de:
- Arquivos adicionados (presentes apenas na nova árvore)
- Arquivos excluídos (presentes apenas na árvore antiga)
- Arquivos modificados (mesmo caminho, hash de blob diferente)
- Comparação recursiva de subdiretórios
- Associação de renomeações entre arquivos excluídos e adicionados (hash exato ou pelo menos 50% de linhas em comum)

Entradas cujo hash não mudou são ignoradas, portanto uma alteração apenas no modo não gera um resultado.

## Diff de palavras

Para alterações em linha no nível das palavras.

```php
use Pitmaster\Diff\WordDiff;

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