Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Eliminates file reference

AuthorDave Jarvis <email>
Date2026-02-22 12:02:56 GMT-0800
Commit6ccb6b9ae6f7555e31cbdfcf7305b0ebbf160dfc
Parent65f8e9e
git/GitDiff.php
<?php
-require_once __DIR__ . '/../File.php';
-
-class GitDiff {
- private const MAX_DIFF_SIZE = 262144;
-
- private Git $git;
-
- public function __construct( Git $git ) {
- $this->git = $git;
- }
-
- public function diff( string $oldSha, string $newSha ): Generator {
- $oldTree = $oldSha === '' ? '' : $this->getTreeHash( $oldSha );
- $newTree = $newSha === '' ? '' : $this->getTreeHash( $newSha );
-
- yield from $this->diffTrees( $oldTree, $newTree );
- }
-
- public function compare( string $commitHash ): Generator {
- $commitData = $this->git->read( $commitHash );
- $parentHash = '';
-
- if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $m ) ) {
- $parentHash = $m[1];
- }
-
- $newTree = $this->getTreeHash( $commitHash );
- $oldTree = $parentHash === ''
- ? ''
- : $this->getTreeHash( $parentHash );
-
- yield from $this->diffTrees( $oldTree, $newTree );
- }
-
- private function getTreeHash( string $commitSha ): string {
- $data = $this->git->read( $commitSha );
- $result = '';
-
- if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
- $result = $matches[1];
- }
-
- return $result;
- }
-
- private function diffTrees(
- string $oldTreeSha,
- string $newTreeSha,
- string $path = ''
- ): Generator {
- if( $oldTreeSha !== $newTreeSha ) {
- $oldEntries = $oldTreeSha === ''
- ? []
- : $this->parseTree( $oldTreeSha );
- $newEntries = $newTreeSha === ''
- ? []
- : $this->parseTree( $newTreeSha );
- $allNames = array_unique(
- array_merge(
- array_keys( $oldEntries ),
- array_keys( $newEntries )
- )
- );
-
- sort( $allNames );
-
- static $emptyFile = new File( '', '', '0', 0, 0 );
-
- foreach( $allNames as $name ) {
- $old = $oldEntries[$name] ?? [];
- $new = $newEntries[$name] ?? [];
- $hasOld = !empty( $old );
- $hasNew = !empty( $new );
- $currentPath = $path === '' ? $name : "$path/$name";
- $oldFile = $hasOld ? $old['file'] : $emptyFile;
- $newFile = $hasNew ? $new['file'] : $emptyFile;
- $oldSha = $hasOld ? $old['sha'] : '';
- $newSha = $hasNew ? $new['sha'] : '';
-
- if( !$hasOld && $hasNew ) {
- if( $newFile->isDir() ) {
- yield from $this->diffTrees( '', $newSha, $currentPath );
- } else {
- yield $this->createChange(
- 'A',
- $currentPath,
- '',
- $newSha,
- $oldFile,
- $newFile
- );
- }
- } elseif( !$hasNew && $hasOld ) {
- if( $oldFile->isDir() ) {
- yield from $this->diffTrees( $oldSha, '', $currentPath );
- } else {
- yield $this->createChange(
- 'D',
- $currentPath,
- $oldSha,
- '',
- $oldFile,
- $newFile
- );
- }
- } elseif( $hasOld && $hasNew && $oldSha !== $newSha ) {
- if( $oldFile->isDir() && $newFile->isDir() ) {
- yield from $this->diffTrees(
- $oldSha,
- $newSha,
- $currentPath
- );
- } elseif( !$oldFile->isDir() && !$newFile->isDir() ) {
- yield $this->createChange(
- 'M',
- $currentPath,
- $oldSha,
- $newSha,
- $oldFile,
- $newFile
- );
- }
- }
- }
- }
- }
-
- private function parseTree( string $sha ): array {
- $data = $this->git->read( $sha );
- $entries = [];
-
- $this->git->parseTreeData(
- $data,
- function( string $name, string $hash, string $mode ) use (
- &$entries
- ) {
- $dir = $mode === '40000' || $mode === '040000';
- $isSub = $mode === '160000';
-
- $entries[$name] = [
- 'file' => new File(
- $name,
- $hash,
- $mode,
- 0,
- $dir || $isSub ? 0 : $this->git->getObjectSize( $hash ),
- $dir || $isSub ? '' : $this->git->peek( $hash )
- ),
- 'sha' => $hash
- ];
- }
- );
-
- return $entries;
- }
-
- private function createChange(
- string $type,
- string $path,
- string $oldSha,
- string $newSha,
- File $oldFile,
- File $newFile
- ): array {
- $oldSize = $oldSha === ''
- ? 0
- : $this->git->getObjectSize( $oldSha );
- $newSize = $newSha === ''
- ? 0
- : $this->git->getObjectSize( $newSha );
- $result = [];
-
- if( $oldSize > self::MAX_DIFF_SIZE
- || $newSize > self::MAX_DIFF_SIZE
- ) {
- $result = [
- 'type' => $type,
- 'path' => $path,
- 'is_binary' => true,
- 'hunks' => []
- ];
- } else {
- $oldContent = $oldSha === '' ? '' : $this->git->read( $oldSha );
- $newContent = $newSha === '' ? '' : $this->git->read( $newSha );
- $isBinary = $oldFile->isBinary() || $newFile->isBinary();
-
- $result = [
- 'type' => $type,
- 'path' => $path,
- 'is_binary' => $isBinary,
- 'hunks' => $isBinary
- ? []
- : $this->calculateDiff( $oldContent, $newContent )
- ];
- }
-
- return $result;
- }
-
- private function calculateDiff( string $old, string $new ): array {
- $oldLines = explode( "\n", str_replace( "\r\n", "\n", $old ) );
- $newLines = explode( "\n", str_replace( "\r\n", "\n", $new ) );
- $m = count( $oldLines );
- $n = count( $newLines );
- $start = 0;
- $end = 0;
-
- while( $start < $m && $start < $n &&
- $oldLines[$start] === $newLines[$start] ) {
- $start++;
- }
-
- while( $m - $end > $start && $n - $end > $start &&
- $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end] ) {
- $end++;
- }
-
- $stream = $this->buildFullStream(
- $oldLines,
- $newLines,
- $m,
- $n,
- $start,
- $end
- );
-
- return $this->formatDiffOutput( $stream );
- }
-
- private function buildFullStream(
- array $oldLines,
- array $newLines,
- int $m,
- int $n,
- int $start,
- int $end
- ): array {
- $context = 2;
- $limit = 100000;
- $stream = [];
- $pStart = max( 0, $start - $context );
-
- for( $i = $pStart; $i < $start; $i++ ) {
- $stream[] = [
- 't' => ' ',
- 'l' => $oldLines[$i],
- 'no' => $i + 1,
- 'nn' => $i + 1
- ];
- }
-
- $oldSlice = array_slice( $oldLines, $start, $m - $start - $end );
- $newSlice = array_slice( $newLines, $start, $n - $start - $end );
- $mid = count( $oldSlice ) * count( $newSlice ) > $limit
- ? $this->buildFallbackDiff( $oldSlice, $newSlice, $start )
- : $this->buildDiffStream(
- $this->computeLCS( $oldSlice, $newSlice ),
- $start
- );
-
- foreach( $mid as $line ) {
- $stream[] = $line;
- }
-
- $sLimit = min( $end, $context );
-
- for( $i = 0; $i < $sLimit; $i++ ) {
- $idxO = $m - $end + $i;
- $idxN = $n - $end + $i;
-
- $stream[] = [
- 't' => ' ',
- 'l' => $oldLines[$idxO],
- 'no' => $idxO + 1,
- 'nn' => $idxN + 1
- ];
- }
-
- return $stream;
- }
-
- private function formatDiffOutput( array $stream ): array {
- $n = count( $stream );
- $keep = array_fill( 0, $n, false );
- $context = 2;
-
- for( $i = 0; $i < $n; $i++ ) {
- if( $stream[$i]['t'] !== ' ' ) {
- $low = max( 0, $i - $context );
- $high = min( $n - 1, $i + $context );
-
- for( $j = $low; $j <= $high; $j++ ) {
- $keep[$j] = true;
- }
- }
- }
-
- $result = [];
- $buffer = [];
-
- for( $i = 0; $i < $n; $i++ ) {
- if( $keep[$i] ) {
- $this->flushBuffer( $result, $buffer );
-
- $result[] = $stream[$i];
- } else {
- $buffer[] = $stream[$i];
- }
- }
-
- $this->flushBuffer( $result, $buffer );
-
- return $result;
- }
-
- private function flushBuffer( array &$result, array &$buffer ): void {
- $cnt = count( $buffer );
-
- if( $cnt > 0 ) {
- if( $cnt > 5 ) {
- $result[] = [ 't' => 'gap' ];
- } else {
- foreach( $buffer as $bufLine ) {
- $result[] = $bufLine;
- }
- }
-
- $buffer = [];
- }
- }
-
- private function buildFallbackDiff(
- array $old,
- array $new,
- int $offset
- ): array {
- $stream = [];
- $currO = $offset + 1;
- $currN = $offset + 1;
-
- foreach( $old as $line ) {
- $stream[] = [
- 't' => '-',
- 'l' => $line,
- 'no' => $currO++,
- 'nn' => null
- ];
- }
-
- foreach( $new as $line ) {
- $stream[] = [
- 't' => '+',
- 'l' => $line,
- 'no' => null,
- 'nn' => $currN++
- ];
- }
-
- return $stream;
- }
-
- private function buildDiffStream( array $ops, int $start ): array {
- $stream = [];
- $currO = $start + 1;
- $currN = $start + 1;
-
- foreach( $ops as $op ) {
- $stream[] = [
- 't' => $op['t'],
- 'l' => $op['l'],
- 'no' => $op['t'] === '+' ? null : $currO++,
- 'nn' => $op['t'] === '-' ? null : $currN++
+class GitDiff {
+ private const MAX_DIFF_SIZE = 262144;
+
+ private Git $git;
+
+ public function __construct( Git $git ) {
+ $this->git = $git;
+ }
+
+ public function diff( string $oldSha, string $newSha ): Generator {
+ $oldTree = $oldSha === '' ? '' : $this->getTreeHash( $oldSha );
+ $newTree = $newSha === '' ? '' : $this->getTreeHash( $newSha );
+
+ yield from $this->diffTrees( $oldTree, $newTree );
+ }
+
+ public function compare( string $commitHash ): Generator {
+ $commitData = $this->git->read( $commitHash );
+ $parentHash = '';
+
+ if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $m ) ) {
+ $parentHash = $m[1];
+ }
+
+ $newTree = $this->getTreeHash( $commitHash );
+ $oldTree = $parentHash === '' ? '' : $this->getTreeHash( $parentHash );
+
+ yield from $this->diffTrees( $oldTree, $newTree );
+ }
+
+ private function getTreeHash( string $commitSha ): string {
+ $data = $this->git->read( $commitSha );
+ $result = '';
+
+ if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
+ $result = $matches[1];
+ }
+
+ return $result;
+ }
+
+ private function diffTrees(
+ string $oldTreeSha,
+ string $newTreeSha,
+ string $path = ''
+ ): Generator {
+ if( $oldTreeSha !== $newTreeSha ) {
+ $oldEntries = $oldTreeSha === '' ? [] : $this->parseTree( $oldTreeSha );
+ $newEntries = $newTreeSha === '' ? [] : $this->parseTree( $newTreeSha );
+ $allNames = array_unique(
+ array_merge( array_keys( $oldEntries ), array_keys( $newEntries ) )
+ );
+
+ sort( $allNames );
+
+ foreach( $allNames as $name ) {
+ $old = $oldEntries[$name] ?? [];
+ $new = $newEntries[$name] ?? [];
+ $oldSha = $old['sha'] ?? '';
+ $newSha = $new['sha'] ?? '';
+ $oldDir = $old['is_dir'] ?? false;
+ $newDir = $new['is_dir'] ?? false;
+ $curPath = $path === '' ? $name : "$path/$name";
+
+ if( $oldSha === '' && $newSha !== '' ) {
+ if( $newDir ) {
+ yield from $this->diffTrees( '', $newSha, $curPath );
+ } else {
+ yield $this->createChange( 'A', $curPath, $old, $new );
+ }
+ } elseif( $newSha === '' && $oldSha !== '' ) {
+ if( $oldDir ) {
+ yield from $this->diffTrees( $oldSha, '', $curPath );
+ } else {
+ yield $this->createChange( 'D', $curPath, $old, $new );
+ }
+ } elseif( $oldSha !== '' && $newSha !== '' && $oldSha !== $newSha ) {
+ if( $oldDir && $newDir ) {
+ yield from $this->diffTrees( $oldSha, $newSha, $curPath );
+ } elseif( !$oldDir && !$newDir ) {
+ yield $this->createChange( 'M', $curPath, $old, $new );
+ }
+ }
+ }
+ }
+ }
+
+ private function parseTree( string $sha ): array {
+ $data = $this->git->read( $sha );
+ $entries = [];
+
+ $this->git->parseTreeData(
+ $data,
+ function( string $name, string $hash, string $mode ) use ( &$entries ) {
+ $isDir = $mode === '40000' || $mode === '040000' || $mode === '160000';
+
+ $entries[$name] = [
+ 'sha' => $hash,
+ 'is_dir' => $isDir,
+ 'size' => $isDir ? 0 : $this->git->getObjectSize( $hash ),
+ 'peek' => $isDir ? '' : $this->git->peek( $hash )
+ ];
+ }
+ );
+
+ return $entries;
+ }
+
+ private function createChange(
+ string $type,
+ string $path,
+ array $old,
+ array $new
+ ): array {
+ $oldSha = $old['sha'] ?? '';
+ $newSha = $new['sha'] ?? '';
+ $oldSize = $old['size'] ?? 0;
+ $newSize = $new['size'] ?? 0;
+ $oldPeek = $old['peek'] ?? '';
+ $newPeek = $new['peek'] ?? '';
+ $isBin = str_contains( $oldPeek, "\0" ) || str_contains( $newPeek, "\0" );
+ $tooBig = $oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE;
+ $result = [];
+
+ if( $tooBig || $isBin ) {
+ $result = [
+ 'type' => $type,
+ 'path' => $path,
+ 'is_binary' => true,
+ 'hunks' => []
+ ];
+ } else {
+ $oldContent = $oldSha === '' ? '' : $this->git->read( $oldSha );
+ $newContent = $newSha === '' ? '' : $this->git->read( $newSha );
+
+ $result = [
+ 'type' => $type,
+ 'path' => $path,
+ 'is_binary' => false,
+ 'hunks' => $this->calculateDiff( $oldContent, $newContent )
+ ];
+ }
+
+ return $result;
+ }
+
+ private function calculateDiff( string $old, string $new ): array {
+ $oldLines = explode( "\n", str_replace( "\r\n", "\n", $old ) );
+ $newLines = explode( "\n", str_replace( "\r\n", "\n", $new ) );
+ $m = count( $oldLines );
+ $n = count( $newLines );
+ $start = 0;
+ $end = 0;
+
+ while( $start < $m && $start < $n &&
+ $oldLines[$start] === $newLines[$start]
+ ) {
+ $start++;
+ }
+
+ while( $m - $end > $start && $n - $end > $start &&
+ $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]
+ ) {
+ $end++;
+ }
+
+ $stream = $this->buildFullStream(
+ $oldLines,
+ $newLines,
+ $m,
+ $n,
+ $start,
+ $end
+ );
+
+ return $this->formatDiffOutput( $stream );
+ }
+
+ private function buildFullStream(
+ array $oldLines,
+ array $newLines,
+ int $m,
+ int $n,
+ int $start,
+ int $end
+ ): array {
+ $context = 2;
+ $limit = 100000;
+ $stream = [];
+ $pStart = max( 0, $start - $context );
+
+ for( $i = $pStart; $i < $start; $i++ ) {
+ $stream[] = [
+ 't' => ' ',
+ 'l' => $oldLines[$i],
+ 'no' => (string)($i + 1),
+ 'nn' => (string)($i + 1)
+ ];
+ }
+
+ $oldSlice = array_slice( $oldLines, $start, $m - $start - $end );
+ $newSlice = array_slice( $newLines, $start, $n - $start - $end );
+ $mid = count( $oldSlice ) * count( $newSlice ) > $limit
+ ? $this->buildFallbackDiff( $oldSlice, $newSlice, $start )
+ : $this->buildDiffStream(
+ $this->computeLCS( $oldSlice, $newSlice ),
+ $start
+ );
+
+ foreach( $mid as $line ) {
+ $stream[] = $line;
+ }
+
+ $sLimit = min( $end, $context );
+
+ for( $i = 0; $i < $sLimit; $i++ ) {
+ $idxO = $m - $end + $i;
+ $idxN = $n - $end + $i;
+
+ $stream[] = [
+ 't' => ' ',
+ 'l' => $oldLines[$idxO],
+ 'no' => (string)($idxO + 1),
+ 'nn' => (string)($idxN + 1)
+ ];
+ }
+
+ return $stream;
+ }
+
+ private function formatDiffOutput( array $stream ): array {
+ $n = count( $stream );
+ $keep = array_fill( 0, $n, false );
+ $context = 2;
+
+ for( $i = 0; $i < $n; $i++ ) {
+ if( $stream[$i]['t'] !== ' ' ) {
+ $low = max( 0, $i - $context );
+ $high = min( $n - 1, $i + $context );
+
+ for( $j = $low; $j <= $high; $j++ ) {
+ $keep[$j] = true;
+ }
+ }
+ }
+
+ $result = [];
+ $buffer = [];
+
+ for( $i = 0; $i < $n; $i++ ) {
+ if( $keep[$i] ) {
+ $this->flushBuffer( $result, $buffer );
+
+ $result[] = $stream[$i];
+ } else {
+ $buffer[] = $stream[$i];
+ }
+ }
+
+ $this->flushBuffer( $result, $buffer );
+
+ return $result;
+ }
+
+ private function flushBuffer( array &$result, array &$buffer ): void {
+ $cnt = count( $buffer );
+
+ if( $cnt > 0 ) {
+ if( $cnt > 5 ) {
+ $result[] = [
+ 't' => 'gap',
+ 'l' => '',
+ 'no' => '',
+ 'nn' => ''
+ ];
+ } else {
+ foreach( $buffer as $bufLine ) {
+ $result[] = $bufLine;
+ }
+ }
+
+ $buffer = [];
+ }
+ }
+
+ private function buildFallbackDiff(
+ array $old,
+ array $new,
+ int $offset
+ ): array {
+ $stream = [];
+ $currO = $offset + 1;
+ $currN = $offset + 1;
+
+ foreach( $old as $line ) {
+ $stream[] = [
+ 't' => '-',
+ 'l' => $line,
+ 'no' => (string)$currO++,
+ 'nn' => ''
+ ];
+ }
+
+ foreach( $new as $line ) {
+ $stream[] = [
+ 't' => '+',
+ 'l' => $line,
+ 'no' => '',
+ 'nn' => (string)$currN++
+ ];
+ }
+
+ return $stream;
+ }
+
+ private function buildDiffStream( array $ops, int $start ): array {
+ $stream = [];
+ $currO = $start + 1;
+ $currN = $start + 1;
+
+ foreach( $ops as $op ) {
+ $stream[] = [
+ 't' => $op['t'],
+ 'l' => $op['l'],
+ 'no' => $op['t'] === '+' ? '' : (string)$currO++,
+ 'nn' => $op['t'] === '-' ? '' : (string)$currN++
];
}
pages/BasePage.php
<?php
-require_once __DIR__ . '/../File.php';
require_once __DIR__ . '/../UrlBuilder.php';
require_once __DIR__ . '/Page.php';
) {
$siteTitle = Config::SITE_TITLE;
- $pageTitle = $this->title
- ? ' - ' . htmlspecialchars( $this->title )
- : '';
+ $pageTitle = $this->title === ''
+ ? ''
+ : ' - ' . htmlspecialchars( $this->title );
?>
pages/ComparePage.php
private function renderDiffLine( array $line ) {
- if( isset( $line['t'] ) && $line['t'] === 'gap' ) {
+ if( $line['t'] === 'gap' ) {
echo '<tr class="diff-gap"><td colspan="3">...</td></tr>';
} else {
$class = match( $line['t'] ) {
'+' => 'diff-add',
'-' => 'diff-del',
default => ''
};
echo '<tr class="' . $class . '">';
- echo '<td class="diff-line-num">' . ($line['no'] ?? '') . '</td>';
- echo '<td class="diff-line-num">' . ($line['nn'] ?? '') . '</td>';
+ echo '<td class="diff-line-num">' . $line['no'] . '</td>';
+ echo '<td class="diff-line-num">' . $line['nn'] . '</td>';
echo '<td class="diff-code"><pre>' .
- htmlspecialchars( $line['l'] ?? '' ) . '</pre></td>';
+ htmlspecialchars( $line['l'] ) . '</pre></td>';
echo '</tr>';
}
Delta333 lines added, 380 lines removed, 47-line decrease