| | $parentHash = ''; |
| | |
| | - // Only diff against the first parent for now |
| | if (preg_match('/^parent ([0-9a-f]{40})/m', $commitData, $matches)) { |
| | $parentHash = $matches[1]; |
 |
| | $isBinary = false; |
| | |
| | - // Check New Content |
| | if ($newSha) { |
| | $f = new VirtualDiffFile($path, $newContent); |
| | if ($f->isBinary()) $isBinary = true; |
| | } |
| | - // Check Old Content |
| | if (!$isBinary && $oldSha) { |
| | $f = new VirtualDiffFile($path, $oldContent); |
 |
| | |
| | private function calculateDiff($old, $new) { |
| | + // Normalize line endings to avoid "entire file changed" on CRLF vs LF |
| | + $old = str_replace("\r\n", "\n", $old); |
| | + $new = str_replace("\r\n", "\n", $new); |
| | + |
| | $oldLines = explode("\n", $old); |
| | $newLines = explode("\n", $new); |
| | |
| | $m = count($oldLines); |
| | $n = count($newLines); |
| | |
| | + // LCS Algorithm |
| | $start = 0; |
| | while ($start < $m && $start < $n && $oldLines[$start] === $newLines[$start]) { |
 |
| | $ops = $this->computeLCS($oldSlice, $newSlice); |
| | |
| | - $finalDiff = []; |
| | + // Convert Ops to Hunks with Context |
| | + $hunks = []; |
| | + $contextLines = 3; |
| | + |
| | + // Add full context if file is small, otherwise use hunks |
| | + if (count($ops) === 0) { |
| | + // Identical? Return empty or single hunk of text? |
| | + // If createChange was called, SHAs differed. If content same, maybe just whitespace/normalization? |
| | + // Return empty implies no visual diff. |
| | + return []; |
| | + } |
| | + |
| | + // Flatten ops into a stream of changes with line numbers |
| | + $stream = []; |
| | |
| | + // Prefix context |
| | for ($i = 0; $i < $start; $i++) { |
| | - $finalDiff[] = ['t' => ' ', 'l' => $oldLines[$i]]; |
| | + $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $i + 1, 'nn' => $i + 1]; |
| | } |
| | + |
| | + $currO = $start + 1; |
| | + $currN = $start + 1; |
| | |
| | foreach ($ops as $op) { |
| | - $finalDiff[] = $op; |
| | + if ($op['t'] === ' ') { |
| | + $stream[] = ['t' => ' ', 'l' => $op['l'], 'no' => $currO++, 'nn' => $currN++]; |
| | + } elseif ($op['t'] === '-') { |
| | + $stream[] = ['t' => '-', 'l' => $op['l'], 'no' => $currO++, 'nn' => null]; |
| | + } elseif ($op['t'] === '+') { |
| | + $stream[] = ['t' => '+', 'l' => $op['l'], 'no' => null, 'nn' => $currN++]; |
| | + } |
| | } |
| | |
| | + // Suffix context |
| | for ($i = $m - $end; $i < $m; $i++) { |
| | - $finalDiff[] = ['t' => ' ', 'l' => $oldLines[$i]]; |
| | + $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $currO++, 'nn' => $currN++]; |
| | } |
| | |
| | - return $finalDiff; |
| | + // Filter stream to create hunks |
| | + $finalLines = []; |
| | + $lastVisibleIndex = -1; |
| | + $streamLen = count($stream); |
| | + |
| | + for ($i = 0; $i < $streamLen; $i++) { |
| | + $show = false; |
| | + |
| | + // Is this line a change? |
| | + if ($stream[$i]['t'] !== ' ') { |
| | + $show = true; |
| | + } else { |
| | + // Check proximity to a change |
| | + // Look ahead |
| | + for ($j = 1; $j <= $contextLines; $j++) { |
| | + if (($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ') { |
| | + $show = true; |
| | + break; |
| | + } |
| | + } |
| | + // Look behind |
| | + if (!$show) { |
| | + for ($j = 1; $j <= $contextLines; $j++) { |
| | + if (($i - $j) >= 0 && $stream[$i - $j]['t'] !== ' ') { |
| | + $show = true; |
| | + break; |
| | + } |
| | + } |
| | + } |
| | + } |
| | + |
| | + if ($show) { |
| | + if ($lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1) { |
| | + // Gap detected |
| | + $finalLines[] = ['t' => 'gap']; |
| | + } |
| | + $finalLines[] = $stream[$i]; |
| | + $lastVisibleIndex = $i; |
| | + } |
| | + } |
| | + |
| | + return $finalLines; |
| | } |
| | |
 |
| | } |
| | |
| | -/** |
| | - * Helper Class to check binary types on in-memory content. |
| | - * Fixes recursion crash by storing name locally. |
| | - */ |
| | class VirtualDiffFile extends File { |
| | - private $content; |
| | - private $vName; // Local storage for name since parent::$name is private |
| | + private $content; |
| | + private $vName; |
| | |
| | - public function __construct($name, $content) { |
| | - parent::__construct($name, '', '100644', 0, strlen($content)); |
| | - $this->vName = $name; |
| | - $this->content = $content; |
| | - } |
| | + public function __construct($name, $content) { |
| | + parent::__construct($name, '', '100644', 0, strlen($content)); |
| | + $this->vName = $name; |
| | + $this->content = $content; |
| | + } |
| | |
| | - public function isBinary(): bool { |
| | - // Use local $this->vName to avoid accessing private parent::$name |
| | - $buffer = substr($this->content, 0, 12); |
| | - return MediaTypeSniffer::isBinary($buffer, $this->vName); |
| | - } |
| | + public function isBinary(): bool { |
| | + $buffer = substr($this->content, 0, 12); |
| | + return MediaTypeSniffer::isBinary($buffer, $this->vName); |
| | + } |
| | } |
| | |