117 lines
4.3 KiB
PHP
117 lines
4.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Diff Service — generates unified diffs between two text contents.
|
|
*
|
|
* Uses SebastianBergmann\Diff if available, falls back to simple line diff.
|
|
*
|
|
* @author Ball Studios <https://git.balls.studio>
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace Pterodactyl\Addons\AdvancedAdmin\Services\Revisions;
|
|
|
|
class DiffService
|
|
{
|
|
/**
|
|
* Generate a unified diff between old and new content.
|
|
* Returns array of lines with type: 'context' | 'added' | 'removed' | 'header'
|
|
*/
|
|
public function diff(string $oldContent, string $newContent, string $fileName = 'file'): array
|
|
{
|
|
if (class_exists(\SebastianBergmann\Diff\Differ::class)) {
|
|
return $this->diffViaSebastian($oldContent, $newContent, $fileName);
|
|
}
|
|
return $this->diffNative($oldContent, $newContent);
|
|
}
|
|
|
|
/**
|
|
* Compute a structured diff using SebastianBergmann\Diff.
|
|
*/
|
|
private function diffViaSebastian(string $old, string $new, string $fileName): array
|
|
{
|
|
$differ = new \SebastianBergmann\Diff\Differ(
|
|
new \SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder("--- a/{$fileName}\n+++ b/{$fileName}\n")
|
|
);
|
|
$rawDiff = $differ->diff($old, $new);
|
|
return $this->parseUnifiedDiff($rawDiff);
|
|
}
|
|
|
|
/**
|
|
* Simple pure-PHP line-level diff (fallback).
|
|
*/
|
|
private function diffNative(string $old, string $new): array
|
|
{
|
|
$oldLines = explode("\n", $old);
|
|
$newLines = explode("\n", $new);
|
|
$result = [];
|
|
|
|
// LCS-based diff
|
|
$lcs = $this->lcs($oldLines, $newLines);
|
|
$i = 0; $j = 0; $k = 0;
|
|
|
|
while ($i < count($oldLines) || $j < count($newLines)) {
|
|
if ($k < count($lcs) && $i < count($oldLines) && $oldLines[$i] === $lcs[$k] && $j < count($newLines) && $newLines[$j] === $lcs[$k]) {
|
|
$result[] = ['type' => 'context', 'content' => $oldLines[$i], 'line_old' => $i + 1, 'line_new' => $j + 1];
|
|
$i++; $j++; $k++;
|
|
} elseif ($j < count($newLines) && ($k >= count($lcs) || $newLines[$j] !== $lcs[$k])) {
|
|
$result[] = ['type' => 'added', 'content' => $newLines[$j], 'line_new' => $j + 1];
|
|
$j++;
|
|
} else {
|
|
$result[] = ['type' => 'removed', 'content' => $oldLines[$i], 'line_old' => $i + 1];
|
|
$i++;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
private function parseUnifiedDiff(string $diff): array
|
|
{
|
|
$lines = explode("\n", $diff);
|
|
$result = [];
|
|
$oldLine = 0; $newLine = 0;
|
|
|
|
foreach ($lines as $line) {
|
|
if (str_starts_with($line, '---') || str_starts_with($line, '+++')) {
|
|
$result[] = ['type' => 'header', 'content' => $line];
|
|
} elseif (str_starts_with($line, '@@')) {
|
|
// Parse @@ -old +new @@
|
|
preg_match('/@@ -(\d+).*\+(\d+)/', $line, $m);
|
|
$oldLine = (int) ($m[1] ?? 0);
|
|
$newLine = (int) ($m[2] ?? 0);
|
|
$result[] = ['type' => 'header', 'content' => $line];
|
|
} elseif (str_starts_with($line, '-')) {
|
|
$result[] = ['type' => 'removed', 'content' => substr($line, 1), 'line_old' => $oldLine++];
|
|
} elseif (str_starts_with($line, '+')) {
|
|
$result[] = ['type' => 'added', 'content' => substr($line, 1), 'line_new' => $newLine++];
|
|
} elseif (str_starts_with($line, ' ')) {
|
|
$result[] = ['type' => 'context', 'content' => substr($line, 1), 'line_old' => $oldLine++, 'line_new' => $newLine++];
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Longest Common Subsequence (for native diff).
|
|
*/
|
|
private function lcs(array $a, array $b): array
|
|
{
|
|
$m = count($a); $n = count($b);
|
|
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
|
|
for ($i = 1; $i <= $m; $i++) {
|
|
for ($j = 1; $j <= $n; $j++) {
|
|
$dp[$i][$j] = $a[$i-1] === $b[$j-1]
|
|
? $dp[$i-1][$j-1] + 1
|
|
: max($dp[$i-1][$j], $dp[$i][$j-1]);
|
|
}
|
|
}
|
|
$result = []; $i = $m; $j = $n;
|
|
while ($i > 0 && $j > 0) {
|
|
if ($a[$i-1] === $b[$j-1]) { array_unshift($result, $a[$i-1]); $i--; $j--; }
|
|
elseif ($dp[$i-1][$j] > $dp[$i][$j-1]) { $i--; }
|
|
else { $j--; }
|
|
}
|
|
return $result;
|
|
}
|
|
}
|