Dave Jarvis' Repositories

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

Fully compares tag to previous tag

AuthorDave Jarvis <email>
Date2026-02-15 23:41:29 GMT-0800
Commit1bf329e1fc93bb7ec3e4338bf35972aea27553e8
Parent830ae85
Router.php
require_once __DIR__ . '/pages/TagsPage.php';
require_once __DIR__ . '/pages/ClonePage.php';
+require_once __DIR__ . '/pages/ComparePage.php';
class Router {
$nextPart = $parts[0] ?? '';
- $uiActions = ['tree', 'blob', 'raw', 'commits', 'commit', 'tags'];
+ $uiActions = [
+ 'tree', 'blob', 'raw', 'commits', 'commit', 'tags', 'compare'
+ ];
if( str_ends_with( $repoName, '.git' ) &&
if( $isRealRepo ) {
$subPath = implode( '/', $parts );
- $repoPath = $this->repos[$repoName]['path'] ?? $this->repos[$realName]['path'];
+ $repoPath = $this->repos[$repoName]['path'] ??
+ $this->repos[$realName]['path'];
+
$this->git->setRepository( $repoPath );
}
- $hash = '';
- $path = '';
+ $hash = '';
+ $path = '';
+ $baseHash = '';
if( in_array( $action, ['tree', 'blob', 'raw', 'commits'] ) ) {
$hash = array_shift( $parts ) ?? 'HEAD';
$path = implode( '/', $parts );
}
elseif( $action === 'commit' ) {
$hash = array_shift( $parts );
+ }
+ elseif( $action === 'compare' ) {
+ $hash = array_shift( $parts );
+ $baseHash = array_shift( $parts );
}
- $_GET['repo'] = $repoName;
+ $_GET['repo'] = $repoName;
$_GET['action'] = $action;
- $_GET['hash'] = $hash;
- $_GET['name'] = $path;
+ $_GET['hash'] = $hash;
+ $_GET['name'] = $path;
switch( $action ) {
case 'tree':
case 'blob':
- return new FilePage( $this->repos, $currRepo, $this->git, $hash, $path );
+ return new FilePage(
+ $this->repos, $currRepo, $this->git, $hash, $path
+ );
case 'raw':
case 'tags':
return new TagsPage( $this->repos, $currRepo, $this->git );
+
+ case 'compare':
+ return new ComparePage(
+ $this->repos, $currRepo, $this->git, $hash, $baseHash
+ );
default:
git/GitDiff.php
}
- public function compare( string $commitHash ) {
- $commitData = $this->git->read( $commitHash );
-
- $parentHash = preg_match(
- '/^parent ([0-9a-f]{40})/m',
- $commitData,
- $matches
- ) ? $matches[1] : '';
-
- $newTree = $this->getTreeHash( $commitHash );
- $oldTree = $parentHash ? $this->getTreeHash( $parentHash ) : null;
-
- return $this->diffTrees( $oldTree, $newTree );
- }
-
- private function getTreeHash( $commitSha ) {
- $data = $this->git->read( $commitSha );
-
- return preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches )
- ? $matches[1]
- : null;
- }
-
- private function diffTrees( $oldTreeSha, $newTreeSha, $path = '' ) {
- $changes = [];
-
- 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] ?? null;
- $new = $newEntries[$name] ?? null;
- $currentPath = $path ? "$path/$name" : $name;
-
- if( !$old ) {
- $changes = $new['is_dir']
- ? array_merge(
- $changes,
- $this->diffTrees( null, $new['sha'], $currentPath )
- )
- : array_merge(
- $changes,
- [$this->createChange( 'A', $currentPath, null, $new['sha'] )]
- );
- } elseif( !$new ) {
- $changes = $old['is_dir']
- ? array_merge(
- $changes,
- $this->diffTrees( $old['sha'], null, $currentPath )
- )
- : array_merge(
- $changes,
- [$this->createChange( 'D', $currentPath, $old['sha'], null )]
- );
- } elseif( $old['sha'] !== $new['sha'] ) {
- $changes = ($old['is_dir'] && $new['is_dir'])
- ? array_merge(
- $changes,
- $this->diffTrees( $old['sha'], $new['sha'], $currentPath )
- )
- : (($old['is_dir'] || $new['is_dir'])
- ? $changes
- : array_merge(
- $changes,
- [$this->createChange(
- 'M',
- $currentPath,
- $old['sha'],
- $new['sha']
- )]
- ));
- }
- }
- }
-
- return $changes;
- }
-
- private function parseTree( $sha ) {
- $data = $this->git->read( $sha );
- $entries = [];
- $len = strlen( $data );
- $pos = 0;
-
- while( $pos < $len ) {
- $space = strpos( $data, ' ', $pos );
- $null = strpos( $data, "\0", $space );
-
- if( $space === false || $null === false ) {
- break;
- }
-
- $mode = substr( $data, $pos, $space - $pos );
- $name = substr( $data, $space + 1, $null - $space - 1 );
- $hash = bin2hex( substr( $data, $null + 1, 20 ) );
-
- $entries[$name] = [
- 'mode' => $mode,
- 'sha' => $hash,
- 'is_dir' => $mode === '40000' || $mode === '040000'
- ];
-
- $pos = $null + 21;
- }
-
- return $entries;
- }
-
- private function createChange( $type, $path, $oldSha, $newSha ) {
- $oldSize = $oldSha ? $this->git->getObjectSize( $oldSha ) : 0;
- $newSize = $newSha ? $this->git->getObjectSize( $newSha ) : 0;
- $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 =
- ($newSha && (new VirtualDiffFile( $path, $newContent ))->isBinary()) ||
- (!$newSha && $oldSha &&
- (new VirtualDiffFile( $path, $oldContent ))->isBinary());
-
- $result = [
- 'type' => $type,
- 'path' => $path,
- 'is_binary' => $isBinary,
- 'hunks' => $isBinary
- ? null
- : $this->calculateDiff( $oldContent, $newContent )
- ];
- }
-
- return $result;
- }
-
- private function calculateDiff( $old, $new ) {
- $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 );
-
- $start = 0;
-
- while(
- $start < $m &&
- $start < $n &&
- $oldLines[$start] === $newLines[$start]
- ) {
- $start++;
- }
-
- $end = 0;
-
- while(
- $m - $end > $start &&
- $n - $end > $start &&
- $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]
- ) {
- $end++;
- }
-
- $oldSlice = array_slice( $oldLines, $start, $m - $start - $end );
- $newSlice = array_slice( $newLines, $start, $n - $start - $end );
-
- $result = null;
-
- if( (count( $oldSlice ) * count( $newSlice )) > 500000 ) {
- $result = [['t' => 'gap']];
- } else {
- $ops = $this->computeLCS( $oldSlice, $newSlice );
-
- $groupedOps = [];
- $bufferDel = [];
- $bufferAdd = [];
-
- foreach( $ops as $op ) {
- if( $op['t'] === ' ' ) {
- foreach( $bufferDel as $o ) { $groupedOps[] = $o; }
- foreach( $bufferAdd as $o ) { $groupedOps[] = $o; }
-
- $bufferDel = [];
- $bufferAdd = [];
- $groupedOps[] = $op;
- } elseif( $op['t'] === '-' ) {
- $bufferDel[] = $op;
- } elseif( $op['t'] === '+' ) {
- $bufferAdd[] = $op;
- }
- }
-
- foreach( $bufferDel as $o ) { $groupedOps[] = $o; }
- foreach( $bufferAdd as $o ) { $groupedOps[] = $o; }
-
- $ops = $groupedOps;
- $stream = [];
-
- for( $i = 0; $i < $start; $i++ ) {
- $stream[] = [
- 't' => ' ',
- 'l' => $oldLines[$i],
- 'no' => $i + 1,
- 'nn' => $i + 1
- ];
- }
-
- $currO = $start + 1;
- $currN = $start + 1;
-
- foreach( $ops as $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++
- ];
- }
- }
-
- for( $i = $m - $end; $i < $m; $i++ ) {
- $stream[] = [
- 't' => ' ',
- 'l' => $oldLines[$i],
- 'no' => $currO++,
- 'nn' => $currN++
- ];
- }
-
- $finalLines = [];
- $lastVisibleIndex = -1;
- $streamLen = count( $stream );
- $contextLines = 3;
-
- for( $i = 0; $i < $streamLen; $i++ ) {
- $show = false;
-
- if( $stream[$i]['t'] !== ' ' ) {
- $show = true;
- } else {
- for( $j = 1; $j <= $contextLines; $j++ ) {
- if( ($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ' ) {
- $show = true;
- break;
- }
- }
-
- 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 ) {
- $finalLines[] = ['t' => 'gap'];
- }
-
- $finalLines[] = $stream[$i];
- $lastVisibleIndex = $i;
- }
- }
-
- $result = $finalLines;
- }
-
- return $result;
- }
-
- private function computeLCS( $old, $new ) {
- $m = count( $old );
- $n = count( $new );
- $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) );
-
- for( $i = 1; $i <= $m; $i++ ) {
- for( $j = 1; $j <= $n; $j++ ) {
- $c[$i][$j] = ($old[$i - 1] === $new[$j - 1])
- ? $c[$i - 1][$j - 1] + 1
- : max( $c[$i][$j - 1], $c[$i - 1][$j] );
- }
- }
-
- $diff = [];
- $i = $m;
- $j = $n;
-
- while( $i > 0 || $j > 0 ) {
- if( $i > 0 && $j > 0 && $old[$i - 1] === $new[$j - 1] ) {
- array_unshift( $diff, ['t' => ' ', 'l' => $old[$i - 1]] );
- $i--;
- $j--;
- } elseif( $j > 0 && ($i === 0 || $c[$i][$j - 1] >= $c[$i - 1][$j]) ) {
- array_unshift( $diff, ['t' => '+', 'l' => $new[$j - 1]] );
- $j--;
- } elseif( $i > 0 && ($j === 0 || $c[$i][$j - 1] < $c[$i - 1][$j]) ) {
- array_unshift( $diff, ['t' => '-', 'l' => $old[$i - 1]] );
- $i--;
- }
- }
-
- return $diff;
- }
-}
-
-class VirtualDiffFile extends File {
- public function __construct( string $name, string $content ) {
- parent::__construct(
- $name,
- '',
- '100644',
- 0,
- strlen( $content ),
- $content
- );
+ public function diff( string $oldSha, string $newSha ) {
+ $oldTree = $oldSha ? $this->getTreeHash( $oldSha ) : null;
+ $newTree = $newSha ? $this->getTreeHash( $newSha ) : null;
+
+ return $this->diffTrees( $oldTree, $newTree );
+ }
+
+ public function compare( string $commitHash ) {
+ $commitData = $this->git->read( $commitHash );
+
+ $parentHash = preg_match(
+ '/^parent ([0-9a-f]{40})/m',
+ $commitData,
+ $matches
+ ) ? $matches[1] : '';
+
+ $newTree = $this->getTreeHash( $commitHash );
+ $oldTree = $parentHash ? $this->getTreeHash( $parentHash ) : null;
+
+ return $this->diffTrees( $oldTree, $newTree );
+ }
+
+ private function getTreeHash( $commitSha ) {
+ $data = $this->git->read( $commitSha );
+
+ return preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches )
+ ? $matches[1]
+ : null;
+ }
+
+ private function diffTrees( $oldTreeSha, $newTreeSha, $path = '' ) {
+ $changes = [];
+
+ 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] ?? null;
+ $new = $newEntries[$name] ?? null;
+ $currentPath = $path ? "$path/$name" : $name;
+
+ if( !$old ) {
+ $changes = $new['is_dir']
+ ? array_merge(
+ $changes,
+ $this->diffTrees( null, $new['sha'], $currentPath )
+ )
+ : array_merge(
+ $changes,
+ [$this->createChange( 'A', $currentPath, null, $new['sha'] )]
+ );
+ } elseif( !$new ) {
+ $changes = $old['is_dir']
+ ? array_merge(
+ $changes,
+ $this->diffTrees( $old['sha'], null, $currentPath )
+ )
+ : array_merge(
+ $changes,
+ [$this->createChange( 'D', $currentPath, $old['sha'], null )]
+ );
+ } elseif( $old['sha'] !== $new['sha'] ) {
+ $changes = ($old['is_dir'] && $new['is_dir'])
+ ? array_merge(
+ $changes,
+ $this->diffTrees( $old['sha'], $new['sha'], $currentPath )
+ )
+ : (($old['is_dir'] || $new['is_dir'])
+ ? $changes
+ : array_merge(
+ $changes,
+ [$this->createChange(
+ 'M',
+ $currentPath,
+ $old['sha'],
+ $new['sha']
+ )]
+ ));
+ }
+ }
+ }
+
+ return $changes;
+ }
+
+ private function parseTree( $sha ) {
+ $data = $this->git->read( $sha );
+ $entries = [];
+ $len = strlen( $data );
+ $pos = 0;
+
+ while( $pos < $len ) {
+ $space = strpos( $data, ' ', $pos );
+ $null = strpos( $data, "\0", $space );
+
+ if( $space === false || $null === false ) {
+ break;
+ }
+
+ $mode = substr( $data, $pos, $space - $pos );
+ $name = substr( $data, $space + 1, $null - $space - 1 );
+ $hash = bin2hex( substr( $data, $null + 1, 20 ) );
+
+ $entries[$name] = [
+ 'mode' => $mode,
+ 'sha' => $hash,
+ 'is_dir' => $mode === '40000' || $mode === '040000'
+ ];
+
+ $pos = $null + 21;
+ }
+
+ return $entries;
+ }
+
+ private function createChange( $type, $path, $oldSha, $newSha ) {
+ $oldSize = $oldSha ? $this->git->getObjectSize( $oldSha ) : 0;
+ $newSize = $newSha ? $this->git->getObjectSize( $newSha ) : 0;
+ $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 =
+ ($newSha && (new VirtualDiffFile( $path, $newContent ))->isBinary()) ||
+ (!$newSha && $oldSha &&
+ (new VirtualDiffFile( $path, $oldContent ))->isBinary());
+
+ $result = [
+ 'type' => $type,
+ 'path' => $path,
+ 'is_binary' => $isBinary,
+ 'hunks' => $isBinary
+ ? null
+ : $this->calculateDiff( $oldContent, $newContent )
+ ];
+ }
+
+ return $result;
+ }
+
+ private function calculateDiff( $old, $new ) {
+ $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 );
+
+ $start = 0;
+
+ while(
+ $start < $m &&
+ $start < $n &&
+ $oldLines[$start] === $newLines[$start]
+ ) {
+ $start++;
+ }
+
+ $end = 0;
+
+ while(
+ $m - $end > $start &&
+ $n - $end > $start &&
+ $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]
+ ) {
+ $end++;
+ }
+
+ $oldSlice = array_slice( $oldLines, $start, $m - $start - $end );
+ $newSlice = array_slice( $newLines, $start, $n - $start - $end );
+
+ $result = null;
+
+ if( (count( $oldSlice ) * count( $newSlice )) > 500000 ) {
+ $result = [['t' => 'gap']];
+ } else {
+ $ops = $this->computeLCS( $oldSlice, $newSlice );
+
+ $groupedOps = [];
+ $bufferDel = [];
+ $bufferAdd = [];
+
+ foreach( $ops as $op ) {
+ if( $op['t'] === ' ' ) {
+ foreach( $bufferDel as $o ) { $groupedOps[] = $o; }
+ foreach( $bufferAdd as $o ) { $groupedOps[] = $o; }
+
+ $bufferDel = [];
+ $bufferAdd = [];
+ $groupedOps[] = $op;
+ } elseif( $op['t'] === '-' ) {
+ $bufferDel[] = $op;
+ } elseif( $op['t'] === '+' ) {
+ $bufferAdd[] = $op;
+ }
+ }
+
+ foreach( $bufferDel as $o ) { $groupedOps[] = $o; }
+ foreach( $bufferAdd as $o ) { $groupedOps[] = $o; }
+
+ $ops = $groupedOps;
+ $stream = [];
+
+ for( $i = 0; $i < $start; $i++ ) {
+ $stream[] = [
+ 't' => ' ',
+ 'l' => $oldLines[$i],
+ 'no' => $i + 1,
+ 'nn' => $i + 1
+ ];
+ }
+
+ $currO = $start + 1;
+ $currN = $start + 1;
+
+ foreach( $ops as $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++
+ ];
+ }
+ }
+
+ for( $i = $m - $end; $i < $m; $i++ ) {
+ $stream[] = [
+ 't' => ' ',
+ 'l' => $oldLines[$i],
+ 'no' => $currO++,
+ 'nn' => $currN++
+ ];
+ }
+
+ $finalLines = [];
+ $lastVisibleIndex = -1;
+ $streamLen = count( $stream );
+ $contextLines = 3;
+
+ for( $i = 0; $i < $streamLen; $i++ ) {
+ $show = false;
+
+ if( $stream[$i]['t'] !== ' ' ) {
+ $show = true;
+ } else {
+ for( $j = 1; $j <= $contextLines; $j++ ) {
+ if( ($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ' ) {
+ $show = true;
+ break;
+ }
+ }
+
+ 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 ) {
+ $finalLines[] = ['t' => 'gap'];
+ }
+
+ $finalLines[] = $stream[$i];
+ $lastVisibleIndex = $i;
+ }
+ }
+
+ $result = $finalLines;
+ }
+
+ return $result;
+ }
+
+ private function computeLCS( $old, $new ) {
+ $m = count( $old );
+ $n = count( $new );
+ $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) );
+
+ for( $i = 1; $i <= $m; $i++ ) {
+ for( $j = 1; $j <= $n; $j++ ) {
+ $c[$i][$j] = ($old[$i - 1] === $new[$j - 1])
+ ? $c[$i - 1][$j - 1] + 1
+ : max( $c[$i][$j - 1], $c[$i - 1][$j] );
+ }
+ }
+
+ $diff = [];
+ $i = $m;
+ $j = $n;
+
+ while( $i > 0 || $j > 0 ) {
+ if( $i > 0 && $j > 0 && $old[$i - 1] === $new[$j - 1] ) {
+ array_unshift( $diff, ['t' => ' ', 'l' => $old[$i - 1]] );
+ $i--;
+ $j--;
+ } elseif( $j > 0 && ($i === 0 || $c[$i][$j - 1] >= $c[$i - 1][$j]) ) {
+ array_unshift( $diff, ['t' => '+', 'l' => $new[$j - 1]] );
+ $j--;
+ } elseif( $i > 0 && ($j === 0 || $c[$i][$j - 1] < $c[$i - 1][$j]) ) {
+ array_unshift( $diff, ['t' => '-', 'l' => $old[$i - 1]] );
+ $i--;
+ }
+ }
+
+ return $diff;
}
}
pages/ComparePage.php
+<?php
+require_once __DIR__ . '/BasePage.php';
+require_once __DIR__ . '/../git/GitDiff.php';
+
+class ComparePage extends BasePage {
+ private $currentRepo;
+ private $git;
+ private $newSha;
+ private $oldSha;
+
+ public function __construct(
+ array $repositories,
+ array $currentRepo,
+ Git $git,
+ string $newSha,
+ string $oldSha
+ ) {
+ parent::__construct( $repositories );
+ $this->currentRepo = $currentRepo;
+ $this->git = $git;
+ $this->newSha = $newSha;
+ $this->oldSha = $oldSha;
+ $this->title = $currentRepo['name'] . ' - Compare';
+ }
+
+ public function render() {
+ $this->renderLayout( function() {
+ $shortNew = substr( $this->newSha, 0, 7 );
+ $shortOld = substr( $this->oldSha, 0, 7 );
+
+ $this->renderBreadcrumbs(
+ $this->currentRepo,
+ ["Compare $shortNew...$shortOld"]
+ );
+
+ $differ = new GitDiff( $this->git );
+ $changes = $differ->diff( $this->oldSha, $this->newSha );
+
+ if( empty( $changes ) ) {
+ echo '<div class="empty-state"><h3>No changes</h3>' .
+ '<p>No differences.</p></div>';
+ return;
+ }
+
+ foreach( $changes as $change ) {
+ $this->renderDiffFile( $change );
+ }
+ }, $this->currentRepo );
+ }
+
+ private function renderDiffFile( array $change ) {
+ $typeMap = [
+ 'A' => 'added',
+ 'D' => 'deleted',
+ 'M' => 'modified'
+ ];
+
+ $statusClass = $typeMap[$change['type']] ?? 'modified';
+ $path = htmlspecialchars( $change['path'] );
+
+ echo '<div class="diff-file">';
+ echo '<div class="diff-header ' . $statusClass . '">';
+ echo '<span class="diff-status-icon">' . $change['type'] . '</span> ';
+ echo $path;
+ echo '</div>';
+
+ if( $change['is_binary'] ) {
+ echo '<div class="diff-content binary">Binary file</div>';
+ } elseif( !empty( $change['hunks'] ) ) {
+ echo '<div class="diff-content">';
+ echo '<table class="diff-table"><tbody>';
+
+ foreach( $change['hunks'] as $line ) {
+ $this->renderDiffLine( $line );
+ }
+
+ echo '</tbody></table>';
+ echo '</div>';
+ }
+
+ echo '</div>';
+ }
+
+ private function renderDiffLine( array $line ) {
+ if( isset( $line['t'] ) && $line['t'] === 'gap' ) {
+ echo '<tr class="diff-gap"><td colspan="3">...</td></tr>';
+ return;
+ }
+
+ $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-code"><pre>' .
+ htmlspecialchars( $line['l'] ?? '' ) . '</pre></td>';
+ echo '</tr>';
+ }
+}
pages/TagsPage.php
'<p>No tags found.</p></div></td></tr>';
} else {
- foreach( $tags as $tag ) {
- $tag->render( $renderer );
+ $count = count( $tags );
+
+ for( $i = 0; $i < $count; $i++ ) {
+ $tag = $tags[$i];
+ $prevTag = $tags[$i + 1] ?? null;
+
+ $renderer->renderTagItem(
+ $tag->getName(),
+ $tag->getSha(),
+ $tag->getTargetSha(),
+ $prevTag ? $prevTag->getTargetSha() : null,
+ $tag->getTimestamp(),
+ $tag->getMessage(),
+ $tag->getAuthorName()
+ );
}
}
render/HtmlTagRenderer.php
+<?php
+require_once __DIR__ . '/TagRenderer.php';
+
+class HtmlTagRenderer implements TagRenderer {
+ private string $repoSafeName;
+
+ public function __construct( string $repoSafeName ) {
+ $this->repoSafeName = $repoSafeName;
+ }
+
+ public function renderTagItem(
+ string $name,
+ string $sha,
+ string $targetSha,
+ ?string $prevTargetSha,
+ int $timestamp,
+ string $message,
+ string $author
+ ): void {
+ $filesUrl = (new UrlBuilder())
+ ->withRepo( $this->repoSafeName )
+ ->withAction( 'tree' )
+ ->withHash( $name )
+ ->build();
+
+ $commitUrl = (new UrlBuilder())
+ ->withRepo( $this->repoSafeName )
+ ->withAction( 'commit' )
+ ->withHash( $targetSha )
+ ->build();
+
+ if( $prevTargetSha ) {
+ $diffUrl = (new UrlBuilder())
+ ->withRepo( $this->repoSafeName )
+ ->withAction( 'compare' )
+ ->withHash( $targetSha )
+ ->withName( $prevTargetSha )
+ ->build();
+ } else {
+ $diffUrl = $commitUrl;
+ }
+
+ echo '<tr>';
+ echo '<td class="tag-name">';
+ echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' .
+ htmlspecialchars( $name ) . '</a>';
+ echo '</td>';
+ echo '<td class="tag-message">';
+
+ echo ($message !== '') ? htmlspecialchars( strtok( $message, "\n" ) ) :
+ '<span style="color: #484f58; font-style: italic;">No description</span>';
+
+ echo '</td>';
+ echo '<td class="tag-author">' . htmlspecialchars( $author ) . '</td>';
+ echo '<td class="tag-time">';
+ $this->renderTime( $timestamp );
+ echo '</td>';
+ echo '<td class="tag-hash">';
+ echo '<a href="' . $diffUrl . '" class="commit-hash">' .
+ substr( $sha, 0, 7 ) . '</a>';
+ echo '</td>';
+ echo '</tr>';
+ }
+
+ public function renderTime( int $timestamp ): void {
+ if( !$timestamp ) {
+ echo 'never';
+ return;
+ }
+
+ $diff = time() - $timestamp;
+
+ if( $diff < 5 ) {
+ echo 'just now';
+ return;
+ }
+
+ $tokens = [
+ 31536000 => 'year',
+ 2592000 => 'month',
+ 604800 => 'week',
+ 86400 => 'day',
+ 3600 => 'hour',
+ 60 => 'minute',
+ 1 => 'second'
+ ];
+
+ foreach( $tokens as $unit => $text ) {
+ if( $diff < $unit ) {
+ continue;
+ }
+
+ $num = floor( $diff / $unit );
+
+ echo $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago';
+ return;
+ }
+ }
+}
render/TagRenderer.php
<?php
-require_once __DIR__ . '/../UrlBuilder.php';
-
interface TagRenderer {
public function renderTagItem(
string $name,
string $sha,
string $targetSha,
+ ?string $prevTargetSha,
int $timestamp,
string $message,
string $author
): void;
public function renderTime( int $timestamp ): void;
-}
-
-class HtmlTagRenderer implements TagRenderer {
- private string $repoSafeName;
-
- public function __construct( string $repoSafeName ) {
- $this->repoSafeName = $repoSafeName;
- }
-
- public function renderTagItem(
- string $name,
- string $sha,
- string $targetSha,
- int $timestamp,
- string $message,
- string $author
- ): void {
- $filesUrl = (new UrlBuilder())
- ->withRepo( $this->repoSafeName )
- ->withAction( 'tree' )
- ->withHash( $name )
- ->build();
-
- $commitUrl = (new UrlBuilder())
- ->withRepo( $this->repoSafeName )
- ->withAction( 'commit' )
- ->withHash( $targetSha )
- ->build();
-
- echo '<tr>';
- echo '<td class="tag-name">';
- echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' .
- htmlspecialchars( $name ) . '</a>';
- echo '</td>';
- echo '<td class="tag-message">';
-
- echo ($message !== '') ? htmlspecialchars( strtok( $message, "\n" ) ) :
- '<span style="color: #484f58; font-style: italic;">No description</span>';
-
- echo '</td>';
- echo '<td class="tag-author">' . htmlspecialchars( $author ) . '</td>';
- echo '<td class="tag-time">';
- $this->renderTime( $timestamp );
- echo '</td>';
- echo '<td class="tag-hash">';
- echo '<a href="' . $commitUrl . '" class="commit-hash">' .
- substr( $sha, 0, 7 ) . '</a>';
- echo '</td>';
- echo '</tr>';
- }
-
- public function renderTime( int $timestamp ): void {
- if( !$timestamp ) {
- echo 'never';
- return;
- }
-
- $diff = time() - $timestamp;
-
- if( $diff < 5 ) {
- echo 'just now';
- return;
- }
-
- $tokens = [
- 31536000 => 'year',
- 2592000 => 'month',
- 604800 => 'week',
- 86400 => 'day',
- 3600 => 'hour',
- 60 => 'minute',
- 1 => 'second'
- ];
-
- foreach( $tokens as $unit => $text ) {
- if( $diff < $unit ) {
- continue;
- }
-
- $num = floor( $diff / $unit );
-
- echo $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago';
- return;
- }
- }
}
Delta586 lines added, 446 lines removed, 140-line increase