Dave Jarvis' Repositories

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

Migrates HTML code into templates

Author Dave Jarvis <email>
Date 2026-03-01 23:56:29 GMT-0800
Commit be19c99ee25c6912d0c8d299bc87ff6651597722
Parent 09cc2d6
pages/BasePage.php
<?php
require_once __DIR__ . '/Page.php';
+require_once __DIR__ . '/Template.php';
require_once __DIR__ . '/../model/UrlBuilder.php';
$this->title = $title;
}
-
- protected function renderLayout(
- $contentCallback,
- array $currentRepo = []
- ) {
- $siteTitle = Config::SITE_TITLE;
- $pageTitle = $this->title === ''
- ? ''
- : ' - ' . htmlspecialchars( $this->title );
-
- ?>
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title><?php echo $siteTitle . $pageTitle; ?></title>
- <link rel="stylesheet"
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
- <link rel="stylesheet" href="/styles/repo.css">
-</head>
-<body>
-<div class="container">
- <header>
- <h1><?php echo Config::SITE_TITLE; ?></h1>
-
- <?php if( $currentRepo ) { ?>
- <input type="checkbox" id="clone-toggle" class="clone-checkbox">
- <?php } ?>
-
- <nav class="nav">
- <?php if( $currentRepo ) { ?>
- <a href="<?php echo (new UrlBuilder())->build(); ?>">Home</a>
- <?php $safeName = $currentRepo['safe_name']; ?>
- <a href="<?php echo (new UrlBuilder())
- ->withRepo( $safeName )
- ->withAction( 'tree' )
- ->build(); ?>">Files</a>
- <a href="<?php echo (new UrlBuilder())
- ->withRepo( $safeName )
- ->withAction( 'commits' )
- ->build(); ?>">Commits</a>
- <a href="<?php echo (new UrlBuilder())
- ->withRepo( $safeName )
- ->withAction( 'tags' )
- ->build(); ?>">Tags</a>
-
- <label for="clone-toggle" class="clone-link">Clone</label>
- <?php } ?>
-
- <?php if( $currentRepo ) { ?>
- <div class="repo-selector">
- <label>Repository:</label>
- <select onchange="<?php echo (new UrlBuilder())
- ->withSwitcher( 'this.value' )
- ->build(); ?>">
- <?php foreach( $this->repositories as $r ) { ?>
- <option
- value="<?php echo htmlspecialchars( $r['safe_name'] ); ?>"
- <?php
- echo $r['safe_name'] === $currentRepo['safe_name']
- ? 'selected'
- : '';
- ?>>
- <?php echo htmlspecialchars( $r['name'] ); ?>
- </option>
- <?php } ?>
- </select>
- </div>
- <?php } ?>
- </nav>
- <?php if( $currentRepo ) { ?>
- <div class="clone-region">
- <div class="clone-wrapper">
- <?php
- $cloneCmd = 'git clone https://repo.autonoma.ca/repo/' .
- $currentRepo['safe_name'] . '.git';
- ?>
- <span class="clone-sizer"><?php echo htmlspecialchars( $cloneCmd ); ?></span>
- <input type="text" class="clone-input" readonly
- value="<?php echo htmlspecialchars( $cloneCmd ); ?>">
- </div>
- </div>
- <?php } ?>
- </header>
+ protected function getBaseTemplateVars( array $currentRepo = [] ): array {
+ $suffix = $this->title === '' ? '' : ' - ' . \htmlspecialchars( $this->title );
+ $vars = [
+ 'site_title' => Config::SITE_TITLE,
+ 'page_title' => $suffix,
+ 'has_repo' => !empty( $currentRepo ),
+ 'repositories' => $this->repositories,
+ 'current_repo' => $currentRepo,
+ 'url' => [
+ 'home' => (new UrlBuilder())->build()
+ ]
+ ];
- <?php call_user_func( $contentCallback ); ?>
+ if( !empty( $currentRepo ) ) {
+ $safeName = $currentRepo['safe_name'];
+ $vars['url']['files'] = (new UrlBuilder())->withRepo( $safeName )->withAction( 'tree' )->build();
+ $vars['url']['commits'] = (new UrlBuilder())->withRepo( $safeName )->withAction( 'commits' )->build();
+ $vars['url']['tags'] = (new UrlBuilder())->withRepo( $safeName )->withAction( 'tags' )->build();
+ $vars['url']['switcher'] = (new UrlBuilder())->withSwitcher( 'this.value' )->build();
+ $vars['clone_cmd'] = 'git clone ' . (new UrlBuilder())->withRepo( $safeName . '.git' )->build();
+ }
-</div>
-</body>
-</html>
- <?php
+ return $vars;
}
-
- protected function renderBreadcrumbs( array $repo, array $trail = [] ) {
- $repoUrl = (new UrlBuilder())->withRepo( $repo['safe_name'] )->build();
- $parts = array_merge(
+ protected function buildBreadcrumbs( array $repo, array $trail = [] ): array {
+ $parts = [
[
- '<a href="' . $repoUrl . '" class="repo-breadcrumb">' .
- htmlspecialchars( $repo['name'] ) . '</a>'
- ],
- $trail
- );
+ 'url' => (new UrlBuilder())->withRepo( $repo['safe_name'] )->build(),
+ 'name' => $repo['name']
+ ]
+ ];
- echo '<div class="breadcrumb">' . implode( ' / ', $parts ) . '</div>';
+ return \array_merge( $parts, $trail );
}
}
pages/ClonePage.php
echo "Not Found";
}
-
- exit;
}
pages/CommitsPage.php
private const PER_PAGE = 100;
- private array $currentRepo;
- private Git $git;
- private string $hash;
+ private $currentRepo;
+ private $git;
+ private $hash;
public function __construct(
public function render(): void {
- $this->renderLayout( function() {
- $main = $this->git->getMainBranch();
-
- if( !$main ) {
- echo '<div class="empty-state"><h3>No branches</h3>' .
- '<p>Empty repository.</p></div>';
- } else {
- $this->renderBreadcrumbs( $this->currentRepo, ['Commits'] );
-
- echo '<h2>Commit History <span class="branch-badge">' .
- htmlspecialchars( $main['name'] ) . '</span></h2>';
- echo '<div class="commit-list">';
-
- $start = $this->hash !== '' ? $this->hash : $main['hash'];
- $commits = [];
+ $main = $this->git->getMainBranch();
+ $tpl = new Template( 'commits.tpl' );
- $this->git->history(
- $start,
- self::PER_PAGE,
- function( Commit $commit ) use( &$commits ) {
- $commits[] = $commit;
- }
- );
+ foreach( $this->getBaseTemplateVars( $this->currentRepo ) as $k => $v ) {
+ $tpl->assign( $k, $v );
+ }
- $nav = $this->buildPagination( $main['hash'], count( $commits ) );
+ if( !$main ) {
+ $tpl->assign( 'has_branches', false );
+ } else {
+ $trail = [
+ [
+ 'url' => '',
+ 'name' => 'Commits'
+ ]
+ ];
- $this->renderPagination( $nav );
+ $tpl->assign( 'has_branches', true );
+ $tpl->assign( 'main_name', $main['name'] );
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $trail ) );
- $renderer = new HtmlCommitRenderer(
- $this->currentRepo['safe_name']
- );
+ $start = $this->hash !== '' ? $this->hash : $main['hash'];
+ $commits = [];
- foreach( $commits as $commit ) {
- $commit->render( $renderer );
+ $this->git->history(
+ $start,
+ self::PER_PAGE,
+ function( Commit $commit ) use( &$commits ) {
+ $commits[] = $commit;
}
-
- echo '</div>';
- $this->renderPagination( $nav );
- }
- }, $this->currentRepo );
- }
-
- private function renderPagination( array $nav ): void {
- $pages = $nav['pages'];
- $current = $nav['current'];
- $hasNext = $nav['hasNext'];
- $hasPrev = $current > 1;
- $total = count( $pages );
-
- if( $hasPrev || $hasNext ) {
- echo '<div class="pagination">';
+ );
- if( $hasPrev ) {
- echo '<a href="' . $this->pageUrl( $pages[0] ) .
- '" class="page-link page-nav" aria-label="first">' .
- $this->svgArrow( 'first' ) . '</a>';
+ $nav = $this->buildPagination( $main['hash'], \count( $commits ) );
+ $commitsHtml = '';
+ $renderer = new HtmlCommitRenderer( $this->currentRepo['safe_name'] );
- echo '<a href="' . $this->pageUrl( $pages[$current - 2] ) .
- '" class="page-link page-nav" aria-label="back">' .
- $this->svgArrow( 'back' ) . '</a>';
- } else {
- echo '<span class="page-link page-nav page-nav-hidden" ' .
- 'aria-hidden="true">' . $this->svgArrow( 'first' ) .
- '</span>';
+ \ob_start();
- echo '<span class="page-link page-nav page-nav-hidden" ' .
- 'aria-hidden="true">' . $this->svgArrow( 'back' ) .
- '</span>';
+ foreach( $commits as $commit ) {
+ $commit->render( $renderer );
}
- $this->renderPageNumbers( $pages, $current );
+ $commitsHtml = \ob_get_clean();
- if( $hasNext ) {
- echo '<a href="' . $this->pageUrl( $pages[$current] ) .
- '" class="page-link page-nav" aria-label="next">' .
- $this->svgArrow( 'next' ) . '</a>';
+ $tpl->assign( 'commits_html', $commitsHtml );
- echo '<a href="' . $this->pageUrl( $pages[$total - 1] ) .
- '" class="page-link page-nav" aria-label="last">' .
- $this->svgArrow( 'last' ) . '</a>';
- } else {
- echo '<span class="page-link page-nav page-nav-hidden" ' .
- 'aria-hidden="true">' . $this->svgArrow( 'next' ) .
- '</span>';
+ $pagesData = [];
- echo '<span class="page-link page-nav page-nav-hidden" ' .
- 'aria-hidden="true">' . $this->svgArrow( 'last' ) .
- '</span>';
+ foreach( $nav['pages'] as $idx => $c ) {
+ $pagesData[] = [
+ 'url' => $this->pageUrl( $c ),
+ 'num' => $idx + 1
+ ];
}
-
- echo '</div>';
- }
- }
-
- private function svgArrow( string $type ): string {
- $icons = [
- 'back' => '<path d="M14 17 L9 12 L14 7" />',
- 'next' => '<path d="M10 17 L15 12 L10 7" />',
- 'first' => '<path d="M13 17 L6 12 L13 7 M19 17 L12 12 L19 7" />',
- 'last' => '<path d="M11 17 L18 12 L11 7 M5 17 L12 12 L5 7" />',
- ];
-
- $inner = $icons[$type] ?? '';
-
- return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" ' .
- 'fill="none" stroke="currentColor" stroke-width="2" ' .
- 'stroke-linecap="round" stroke-linejoin="round" ' .
- 'aria-label="' . $type . '" role="img">' .
- '<title>' . $type . '</title>' . $inner . '</svg>';
- }
- private function renderPageNumbers( array $pages, int $current ): void {
- $total = count( $pages );
- $start = max( 1, $current - 4 );
- $end = min( $total, $start + 9 );
- $actual = 1;
+ $tpl->assign( 'nav', [
+ 'has_prev' => $nav['current'] > 1,
+ 'has_next' => $nav['hasNext'],
+ 'current' => $nav['current'],
+ 'total' => \count( $nav['pages'] ),
+ 'pages' => $pagesData,
+ 'first_url' => $this->pageUrl( $nav['pages'][0] ),
+ 'back_url' => $nav['current'] > 1 ? $this->pageUrl( $nav['pages'][$nav['current'] - 2] ) : '',
+ 'next_url' => $nav['hasNext'] ? $this->pageUrl( $nav['pages'][$nav['current']] ) : '',
+ 'last_url' => $nav['hasNext'] ? $this->pageUrl( $nav['pages'][\count( $nav['pages'] ) - 1] ) : ''
+ ] );
- if( $end === $total ) {
- $start = max( 1, $end - 9 );
- }
+ $total = \count( $nav['pages'] );
+ $current = $nav['current'];
+ $start = \max( 1, $current - 4 );
+ $end = \min( $total, $start + 9 );
- while( $actual <= $total ) {
- if( $actual >= $start && $actual <= $end ) {
- if( $actual === $current ) {
- echo '<span class="page-badge">' . $actual . '</span>';
- } else {
- echo '<a href="' . $this->pageUrl( $pages[$actual - 1] ) .
- '" class="page-link">' . $actual . '</a>';
- }
+ if( $end === $total ) {
+ $start = \max( 1, $end - 9 );
}
- $actual++;
+ $tpl->assign( 'nav_start', $start );
+ $tpl->assign( 'nav_end', $end );
}
+
+ echo $tpl->render( 'commits.tpl' );
}
if( $commit->isSha( $target ) ) {
- $currentPage = count( $pageCommits );
+ $currentPage = \count( $pageCommits );
$found = true;
}
- if( $found && count( $pageCommits ) > $currentPage + 10 ) {
+ if( $found && \count( $pageCommits ) > $currentPage + 10 ) {
$hitLimit = true;
$continue = false;
'current' => $currentPage,
'hasAll' => !$hitLimit,
- 'hasNext' => $count === self::PER_PAGE &&
- isset( $pageCommits[$currentPage] )
+ 'hasNext' => $count === self::PER_PAGE && isset( $pageCommits[$currentPage] )
];
}
pages/ComparePage.php
string $oldSha
) {
- $title = $currentRepo['name'] . ' - Compare';
- parent::__construct( $repositories, $title );
+ parent::__construct( $repositories, $currentRepo['name'] . ' - Compare' );
$this->currentRepo = $currentRepo;
$this->git = $git;
$this->newSha = $newSha;
$this->oldSha = $oldSha;
- }
-
- 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>';
- } else {
- foreach( $changes as $change ) {
- $this->renderDiffFile( $change );
- }
- }
- }, $this->currentRepo );
}
- private function renderDiffFile( array $change ) {
- $typeMap = [
- 'A' => 'added',
- 'D' => 'deleted',
- 'M' => 'modified'
+ public function render(): void {
+ $shortNew = \substr( $this->newSha, 0, 7 );
+ $shortOld = \substr( $this->oldSha, 0, 7 );
+ $trail = [
+ [
+ 'url' => '',
+ 'name' => "Compare $shortNew...$shortOld"
+ ]
];
-
- $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 );
- }
+ $tpl = new Template( 'compare.tpl' );
- echo '</tbody></table>';
- echo '</div>';
+ foreach( $this->getBaseTemplateVars( $this->currentRepo ) as $k => $v ) {
+ $tpl->assign( $k, $v );
}
- echo '</div>';
- }
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $trail ) );
- private function renderDiffLine( array $line ) {
- if( $line['t'] === 'gap' ) {
- echo '<tr class="diff-gap"><td colspan="3">...</td></tr>';
- } else {
- $class = match( $line['t'] ) {
- '+' => 'diff-add',
- '-' => 'diff-del',
- default => ''
- };
+ $differ = new GitDiff( $this->git );
+ $changes = $differ->diff( $this->oldSha, $this->newSha );
- 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>';
- }
+ $tpl->assign( 'empty_changes', empty( $changes ) );
+ $tpl->assign( 'changes', $changes );
+
+ echo $tpl->render( 'compare.tpl' );
}
}
pages/DiffPage.php
string $hash
) {
- parent::__construct( $repositories, substr( $hash, 0, 7 ) );
+ parent::__construct( $repositories, \substr( $hash, 0, 7 ) );
$this->currentRepo = $currentRepo;
$this->git = $git;
$this->hash = $hash;
}
-
- public function render() {
- $this->renderLayout( function() {
- $commitData = $this->git->read( $this->hash );
- $diffEngine = new GitDiff( $this->git );
- $lines = explode( "\n", $commitData );
- $msg = '';
- $isMsg = false;
- $headers = [];
-
- foreach( $lines as $line ) {
- if( $line === '' ) {
- $isMsg = true;
- } elseif( $isMsg ) {
- $msg .= $line . "\n";
- } else {
- if( preg_match( '/^(\w+) (.*)$/', $line, $m ) ) {
- $headers[$m[1]] = $m[2];
- }
- }
- }
-
- $commitsUrl = (new UrlBuilder())
- ->withRepo( $this->currentRepo['safe_name'] )
- ->withAction( 'commits' )
- ->build();
-
- $this->renderBreadcrumbs( $this->currentRepo, [
- '<a href="' . $commitsUrl . '">Commits</a>',
- substr( $this->hash, 0, 7 )
- ] );
-
- echo '<div style="display:flex;flex-direction:column">';
-
- $this->renderCommitHeader( $headers, $msg );
-
- echo '<div class="diff-container" style="order:3">';
-
- $added = 0;
- $deleted = 0;
- $empty = true;
- foreach( $diffEngine->compare( $this->hash ) as $change ) {
- $empty = false;
+ public function render(): void {
+ $commitData = $this->git->read( $this->hash );
+ $diffEngine = new GitDiff( $this->git );
+ $lines = \explode( "\n", $commitData );
+ $msg = '';
+ $isMsg = false;
+ $headers = [];
- foreach( $change['hunks'] as $hunkLine ) {
- if( $hunkLine['t'] === '+' ) {
- $added++;
- } elseif( $hunkLine['t'] === '-' ) {
- $deleted++;
- }
+ foreach( $lines as $line ) {
+ if( $line === '' ) {
+ $isMsg = true;
+ } elseif( $isMsg ) {
+ $msg .= $line . "\n";
+ } else {
+ if( \preg_match( '/^(\w+) (.*)$/', $line, $m ) ) {
+ $headers[$m[1]] = $m[2];
}
-
- $this->renderFileDiff( $change );
- }
-
- if( $empty ) {
- echo '<div class="empty-state">'
- . '<p>No changes detected.</p></div>';
}
+ }
- echo '</div>';
+ $commitsUrl = (new UrlBuilder())->withRepo( $this->currentRepo['safe_name'] )->withAction( 'commits' )->build();
+ $trail = [
+ [
+ 'url' => $commitsUrl,
+ 'name' => 'Commits'
+ ],
+ [
+ 'url' => '',
+ 'name' => \substr( $this->hash, 0, 7 )
+ ]
+ ];
+ $tpl = new Template( 'diff.tpl' );
- $this->renderDelta( $added, $deleted );
+ foreach( $this->getBaseTemplateVars( $this->currentRepo ) as $k => $v ) {
+ $tpl->assign( $k, $v );
+ }
- echo '</div>';
- }, $this->currentRepo );
- }
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $trail ) );
+ $tpl->assign( 'hash', $this->hash );
+ $tpl->assign( 'msg', \trim( $msg ) );
- private function renderCommitHeader(
- array $headers,
- string $msg
- ) {
$authorRaw = $headers['author'] ?? 'Unknown';
- $authorName = preg_replace(
- '/<[^>]+>/', '<email>', $authorRaw
- );
- $authorName = htmlspecialchars( $authorName );
+ $authorName = \preg_replace( '/<[^>]+>/', '<email>', $authorRaw );
$commitDate = '';
- if( preg_match(
- '/^(.*?) <.*?> (\d+) ([-+]\d{4})$/',
- $authorRaw,
- $m
- ) ) {
- $authorName = htmlspecialchars( $m[1] )
- . ' &lt;email&gt;';
+ if( \preg_match( '/^(.*?) <.*?> (\d+) ([-+]\d{4})$/', $authorRaw, $m ) ) {
+ $authorName = $m[1] . ' <email>';
$timestamp = (int)$m[2];
- $pattern = '/([-+])(\d{2})(\d{2})/';
- $tzString = preg_replace(
- $pattern, '$1$2:$3', $m[3]
- );
+ $tzString = \preg_replace( '/([-+])(\d{2})(\d{2})/', '$1$2:$3', $m[3] );
$dt = new DateTime( '@' . $timestamp );
$dt->setTimezone( new DateTimeZone( $tzString ) );
$commitDate = $dt->format( 'Y-m-d H:i:s \G\M\TO' );
}
-
- echo '<div class="commit-details" style="order:1">';
- echo '<div class="commit-header">';
- echo '<h1 class="commit-title">'
- . htmlspecialchars( trim( $msg ) ) . '</h1>';
-
- echo '<table class="commit-info-table"><tbody>';
-
- echo '<tr>'
- . '<th class="commit-info-label">Author</th>'
- . '<td class="commit-info-value">'
- . $authorName . '</td></tr>';
- if( $commitDate !== '' ) {
- echo '<tr>'
- . '<th class="commit-info-label">Date</th>'
- . '<td class="commit-info-value">'
- . $commitDate . '</td></tr>';
- }
+ $tpl->assign( 'author', $authorName );
+ $tpl->assign( 'date', $commitDate );
- echo '<tr>'
- . '<th class="commit-info-label">Commit</th>'
- . '<td class="commit-info-value">'
- . $this->hash . '</td></tr>';
+ $hasParent = isset( $headers['parent'] );
+ $parentUrl = '';
+ $parentSha = '';
- if( isset( $headers['parent'] ) ) {
- $url = (new UrlBuilder())
+ if( $hasParent ) {
+ $parentSha = \substr( $headers['parent'], 0, 7 );
+ $parentUrl = (new UrlBuilder())
->withRepo( $this->currentRepo['safe_name'] )
->withAction( 'commit' )
->withHash( $headers['parent'] )
->build();
-
- echo '<tr>'
- . '<th class="commit-info-label">Parent</th>'
- . '<td class="commit-info-value">';
- echo '<a href="' . $url . '" class="parent-link">'
- . substr( $headers['parent'], 0, 7 ) . '</a>';
- echo '</td></tr>';
}
- echo '</tbody></table></div></div>';
- }
+ $tpl->assign( 'has_parent', $hasParent );
+ $tpl->assign( 'parent_url', $parentUrl );
+ $tpl->assign( 'parent_sha', $parentSha );
- private function renderDelta( int $added, int $deleted ) {
- $pluralize = function( int $count ): string {
- return $count . ' line' . ($count !== 1 ? 's' : '');
- };
+ $added = 0;
+ $deleted = 0;
+ $changes = [];
- $diffNet = $added - $deleted;
- $deltaMsg = $pluralize( $added ) . ' added, '
- . $pluralize( $deleted ) . ' removed';
+ foreach( $diffEngine->compare( $this->hash ) as $change ) {
+ foreach( $change['hunks'] as $hunkLine ) {
+ if( $hunkLine['t'] === '+' ) {
+ $added++;
+ } elseif( $hunkLine['t'] === '-' ) {
+ $deleted++;
+ }
+ }
- if( $diffNet !== 0 ) {
- $direction = $diffNet > 0 ? 'increase' : 'decrease';
- $deltaMsg .= ', ' . abs( $diffNet )
- . "-line $direction";
+ $changes[] = $change;
}
-
- echo '<div class="diff-delta" style="order:2">';
-
- echo '<table class="commit-info-table"><tbody>';
- echo '<tr>'
- . '<th class="commit-info-label">Delta</th>'
- . '<td class="commit-info-value">'
- . htmlspecialchars( $deltaMsg ) . '</td></tr>';
- echo '</tbody></table>';
- echo '</div>';
- }
+ $tpl->assign( 'empty_changes', empty( $changes ) );
+ $tpl->assign( 'changes', $changes );
- private function renderFileDiff( array $change ) {
- $statusIcon = 'fa-file';
- $statusClass = '';
+ $diffNet = $added - $deleted;
+ $deltaMsg = $added . ' line' . ($added !== 1 ? 's' : '') . ' added, ' .
+ $deleted . ' line' . ($deleted !== 1 ? 's' : '') . ' removed';
- if( $change['type'] === 'A' ) {
- $statusIcon = 'fa-plus-circle';
- $statusClass = 'status-add';
- } elseif( $change['type'] === 'D' ) {
- $statusIcon = 'fa-minus-circle';
- $statusClass = 'status-del';
- } elseif( $change['type'] === 'M' ) {
- $statusIcon = 'fa-pencil-alt';
- $statusClass = 'status-mod';
+ if( $diffNet !== 0 ) {
+ $direction = $diffNet > 0 ? 'increase' : 'decrease';
+ $deltaMsg .= ', ' . \abs( $diffNet ) . "-line $direction";
}
-
- echo '<div class="diff-file">';
- echo '<div class="diff-header">';
- echo '<span class="diff-status ' . $statusClass . '">' .
- '<i class="fa ' . $statusIcon . '"></i></span>';
- echo '<span class="diff-path">' .
- htmlspecialchars( $change['path'] ) . '</span>';
- echo '</div>';
-
- if( $change['is_binary'] ) {
- echo '<div class="diff-binary">Binary files differ</div>';
- } else {
- echo '<div class="diff-content">';
- echo '<table><tbody>';
-
- foreach( $change['hunks'] as $line ) {
- if( isset( $line['t'] ) && $line['t'] === 'gap' ) {
- echo '<tr class="diff-gap"><td colspan="3">';
- echo '<img src="/images/diff-gap.svg" class="diff-gap-icon" />';
- echo '</td></tr>';
- } else {
- $class = 'diff-ctx';
- $char = ' ';
-
- if( $line['t'] === '+' ) {
- $class = 'diff-add';
- $char = '+';
- } elseif( $line['t'] === '-' ) {
- $class = 'diff-del';
- $char = '-';
- }
-
- echo '<tr class="' . $class . '">';
- echo '<td class="diff-num" data-num="' . $line['no'] . '"></td>';
- echo '<td class="diff-num" data-num="' . $line['nn'] . '"></td>';
- echo '<td class="diff-code"><span class="diff-marker">' .
- $char . '</span>' . htmlspecialchars( $line['l'] ) . '</td>';
- echo '</tr>';
- }
- }
- echo '</tbody></table>';
- echo '</div>';
- }
+ $tpl->assign( 'delta_msg', $deltaMsg );
- echo '</div>';
+ echo $tpl->render( 'diff.tpl' );
}
}
pages/FilePage.php
}
- public function render() {
- $this->renderLayout( function() {
- $main = $this->git->getMainBranch();
+ public function render(): void {
+ $main = $this->git->getMainBranch();
+ $tpl = new Template( 'file.tpl' );
- if( !$main ) {
- echo '<div class="empty-state"><h3>No branches</h3></div>';
- } else {
- $target = $this->hash;
- $entries = [];
+ foreach( $this->getBaseTemplateVars( $this->currentRepo ) as $k => $v ) {
+ $tpl->assign( $k, $v );
+ }
- $this->git->walk( $target, function( $file ) use ( &$entries ) {
- $entries[] = $file;
- }, $this->path );
+ if( !$main ) {
+ $tpl->assign( 'has_branches', false );
+ } else {
+ $tpl->assign( 'has_branches', true );
- if( !empty( $entries ) && !$this->isExactFileMatch( $entries ) ) {
- $this->renderTree( $main, $target, $entries );
- } else {
- $this->renderBlob( $target );
+ $target = $this->hash;
+ $entries = [];
+
+ $this->git->walk( $target, function( $file ) use ( &$entries ) {
+ $entries[] = $file;
+ }, $this->path );
+
+ $isExact = \count( $entries ) === 1 && $entries[0]->isName( \basename( $this->path ) ) && !$entries[0]->isDir;
+
+ if( !empty( $entries ) && !$isExact ) {
+ $tpl->assign( 'mode', 'tree' );
+ $tpl->assign( 'target_hash', $target );
+ $tpl->assign( 'repo_name', $this->currentRepo['name'] );
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $this->buildTrail( $target, 'Tree', $this->path ) ) );
+
+ \usort( $entries, function( $a, $b ) {
+ return $a->compare( $b );
+ } );
+
+ $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], $this->path, $target );
+
+ \ob_start();
+
+ foreach( $entries as $file ) {
+ $file->renderListEntry( $renderer );
}
- }
- }, $this->currentRepo );
- }
- private function isExactFileMatch( $entries ) {
- return count( $entries ) === 1 &&
- $entries[0]->isName( basename( $this->path ) ) &&
- !$entries[0]->isDir;
- }
+ $tpl->assign( 'entries_html', \ob_get_clean() );
+ } else {
+ $tpl->assign( 'mode', 'blob' );
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $this->buildTrail( $target, 'File', $this->path ) ) );
- private function renderTree( $main, $targetHash, $entries ) {
- $this->emitBreadcrumbs( $targetHash, 'Tree', $this->path );
+ $filename = $this->path;
+ $file = $this->git->readFile( $target, $filename );
+ $size = $this->git->getObjectSize( $target, $filename );
- echo '<h2>' . htmlspecialchars( $this->currentRepo['name'] ) .
- ' <span class="branch-badge">' .
- htmlspecialchars( $targetHash ) . '</span></h2>';
+ if( $size === 0 && !$file ) {
+ $tpl->assign( 'file_found', false );
+ } else {
+ $tpl->assign( 'file_found', true );
- usort( $entries, function( $a, $b ) {
- return $a->compare( $b );
- } );
+ $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], \dirname( $filename ), $target );
+ $rawUrl = (new UrlBuilder())->withRepo( $this->currentRepo['safe_name'] )->withAction( 'raw' )->withHash( $target )->withName( $filename )->build();
- echo '<table class="file-list-table">';
- echo '<thead><tr><th></th><th>Name</th>' .
- '<th class="file-mode-cell">Mode</th>' .
- '<th class="file-size-cell">Size</th></tr></thead>';
- echo '<tbody>';
+ \ob_start();
- $renderer = new HtmlFileRenderer(
- $this->currentRepo['safe_name'],
- $this->path,
- $targetHash
- );
+ $handledMedia = $file->renderMedia( $renderer, $rawUrl );
+ $mediaHtml = \ob_get_clean();
- foreach( $entries as $file ) {
- $file->renderListEntry( $renderer );
- }
+ $tpl->assign( 'handled_media', $handledMedia );
+ $tpl->assign( 'media_html', $mediaHtml );
- echo '</tbody></table>';
- }
+ if( !$handledMedia ) {
+ if( $file->isText() ) {
+ $tpl->assign( 'is_text', true );
- private function renderBlob( $targetHash ) {
- $filename = $this->path;
- $file = $this->git->readFile( $targetHash, $filename );
- $size = $this->git->getObjectSize( $targetHash, $filename );
+ if( $size > self::MAX_DISPLAY_SIZE ) {
+ $tpl->assign( 'is_large', true );
- $renderer = new HtmlFileRenderer(
- $this->currentRepo['safe_name'],
- dirname( $filename ),
- $targetHash
- );
+ \ob_start();
- $this->emitBreadcrumbs( $targetHash, 'File', $filename );
+ $file->renderSize( $renderer );
- if( $size === 0 && !$file ) {
- echo '<div class="empty-state">File not found.</div>';
- } else {
- $rawUrl = (new UrlBuilder())
- ->withRepo( $this->currentRepo['safe_name'] )
- ->withAction( 'raw' )
- ->withHash( $targetHash )
- ->withName( $filename )
- ->build();
+ $tpl->assign( 'size_str', \ob_get_clean() );
+ $tpl->assign( 'download_url', $rawUrl );
+ } else {
+ $tpl->assign( 'is_large', false );
- if( !$file->renderMedia( $renderer, $rawUrl ) ) {
- if( $file->isText() ) {
- if( $size > self::MAX_DISPLAY_SIZE ) {
- ob_start();
- $file->renderSize( $renderer );
- $sizeStr = ob_get_clean();
- $this->renderDownloadState(
- $targetHash,
- "File is too large to display ($sizeStr)."
- );
- } else {
- $content = '';
- $this->git->stream(
- $targetHash,
- function( $d ) use ( &$content ) {
- $content .= $d;
- },
- $filename
- );
+ $content = '';
- echo '<div class="blob-content"><pre class="blob-code">' .
- ($size > self::MAX_HIGHLIGHT_SIZE
- ? htmlspecialchars( $content )
- : $file->highlight( $renderer, $content )) .
- '</pre></div>';
+ $this->git->stream( $target, function( $d ) use ( &$content ) {
+ $content .= $d;
+ }, $filename );
+
+ $highlighted = $size > self::MAX_HIGHLIGHT_SIZE ? \htmlspecialchars( $content ) : $file->highlight( $renderer, $content );
+
+ $tpl->assign( 'blob_content', $highlighted );
+ }
+ } else {
+ $tpl->assign( 'is_text', false );
+ $tpl->assign( 'download_url', $rawUrl );
+ }
}
- } else {
- $this->renderDownloadState(
- $targetHash,
- "This is a binary file."
- );
}
}
}
- }
-
- private function renderDownloadState( $hash, $reason ) {
- $url = (new UrlBuilder())
- ->withRepo( $this->currentRepo['safe_name'] )
- ->withAction( 'raw' )
- ->withHash( $hash )
- ->withName( $this->path )
- ->build();
- echo '<div class="empty-state download-state">';
- echo '<p>' . htmlspecialchars( $reason ) . '</p>';
- echo '<a href="' . $url . '" class="btn-download">Download</a>';
- echo '</div>';
+ echo $tpl->render( 'file.tpl' );
}
- private function emitBreadcrumbs( $hash, $type, $path ) {
+ private function buildTrail( string $hash, string $type, string $path ): array {
$trail = [];
- if( $path ) {
- $parts = explode( '/', trim( $path, '/' ) );
+ if( $path !== '' ) {
+ $parts = \explode( '/', \trim( $path, '/' ) );
$acc = '';
foreach( $parts as $idx => $part ) {
$acc .= ($idx === 0 ? '' : '/') . $part;
- if( $idx === count( $parts ) - 1 ) {
- $trail[] = htmlspecialchars( $part );
+ if( $idx === \count( $parts ) - 1 ) {
+ $trail[] = [
+ 'url' => '',
+ 'name' => $part
+ ];
} else {
$url = (new UrlBuilder())
->withRepo( $this->currentRepo['safe_name'] )
->withAction( 'tree' )
->withHash( $hash )
->withName( $acc )
->build();
- $trail[] = '<a href="' . $url . '">' .
- htmlspecialchars( $part ) . '</a>';
+ $trail[] = [
+ 'url' => $url,
+ 'name' => $part
+ ];
}
}
- } elseif( $hash ) {
- $trail[] = $type . ' ' . substr( $hash, 0, 7 );
+ } elseif( $hash !== '' ) {
+ $trail[] = [
+ 'url' => '',
+ 'name' => $type . ' ' . \substr( $hash, 0, 7 )
+ ];
}
- $this->renderBreadcrumbs( $this->currentRepo, $trail );
+ return $trail;
}
}
pages/HomePage.php
class HomePage extends BasePage {
- private array $repositories;
- private Git $git;
+ private $repositories;
+ private $git;
public function __construct( array $repositories, Git $git ) {
parent::__construct( $repositories );
$this->repositories = $repositories;
$this->git = $git;
}
public function render(): void {
- $this->renderLayout( function() {
- if( empty( $this->repositories ) ) {
- echo '<div class="empty-state">No repositories found.</div>';
- } else {
- echo '<div class="repo-grid">';
-
- foreach( $this->repositories as $repo ) {
- $this->renderRepoCard( $repo );
- }
-
- echo '</div>';
- }
- } );
- }
-
- private function renderRepoCard( array $repo ): void {
- $this->git->setRepository( $repo['path'] );
+ $reposData = [];
- $stats = [ 'branches' => 0, 'tags' => 0 ];
+ foreach( $this->repositories as $repo ) {
+ $this->git->setRepository( $repo['path'] );
- $this->git->eachBranch( function() use( &$stats ) {
- $stats['branches']++;
- } );
+ $branches = 0;
+ $tags = 0;
- $this->git->eachTag( function() use( &$stats ) {
- $stats['tags']++;
- } );
+ $this->git->eachBranch( function() use( &$branches ) {
+ $branches++;
+ } );
- $url = (new UrlBuilder())->withRepo( $repo['safe_name'] )->build();
+ $this->git->eachTag( function() use( &$tags ) {
+ $tags++;
+ } );
- echo '<a href="' . $url . '" class="repo-card">';
- echo '<h3>' . htmlspecialchars( $repo['name'] ) . '</h3>';
- echo '<p class="repo-meta">';
- echo $stats['branches'] .
- ($stats['branches'] === 1 ? ' branch, ' : ' branches, ') .
- $stats['tags'] .
- ($stats['tags'] === 1 ? ' tag' : ' tags');
+ $descPath = $repo['path'] . '/description';
+ $desc = \file_exists( $descPath ) ? \trim( \file_get_contents( $descPath ) ) : '';
+ $timeHtml = '';
- if( $this->git->getMainBranch() ) {
- echo ', ';
+ if( $this->git->getMainBranch() ) {
+ $this->git->history( 'HEAD', 1, function( Commit $c ) use( &$timeHtml, $repo ) {
+ $renderer = new HtmlCommitRenderer( $repo['safe_name'] );
- $this->git->history( 'HEAD', 1, function( Commit $c ) use( $repo ) {
- $renderer = new HtmlCommitRenderer( $repo['safe_name'] );
+ \ob_start();
+ $c->renderTime( $renderer );
+ $timeHtml = \ob_get_clean();
+ } );
+ }
- $c->renderTime( $renderer );
- } );
+ $reposData[] = [
+ 'name' => $repo['name'],
+ 'url' => (new UrlBuilder())->withRepo( $repo['safe_name'] )->build(),
+ 'branches' => $branches,
+ 'tags' => $tags,
+ 'desc' => $desc,
+ 'time_html' => $timeHtml
+ ];
}
-
- echo '</p>';
-
- $descPath = $repo['path'] . '/description';
- if( file_exists( $descPath ) ) {
- $description = trim( file_get_contents( $descPath ) );
+ $tpl = new Template( 'home.tpl' );
- if( $description !== '' ) {
- echo '<p style="margin-top: 1.5em;">' .
- htmlspecialchars( $description ) . '</p>';
- }
+ foreach( $this->getBaseTemplateVars() as $k => $v ) {
+ $tpl->assign( $k, $v );
}
- echo '</a>';
+ $tpl->assign( 'repos', $reposData );
+
+ echo $tpl->render( 'home.tpl' );
}
}
pages/Page.php
<?php
interface Page {
- public function render();
+ public function render(): void;
}
pages/RawPage.php
class RawPage implements Page {
- private Git $git;
- private string $hash;
- private string $repo;
- private string $name;
+ private $git;
+ private $hash;
+ private $repo;
+ private $name;
public function __construct(
public function render(): void {
if( $this->name === '' ) {
- header( "Location: /{$this->repo}/" );
+ \header( "Location: /{$this->repo}/" );
} else {
$file = $this->git->readFile( $this->hash, $this->name );
- while( ob_get_level() > 0 ) {
- if( !ob_end_clean() ) {
+ while( \ob_get_level() > 0 ) {
+ if( !\ob_end_clean() ) {
break;
}
);
}
-
- exit;
}
}
pages/TagsPage.php
class TagsPage extends BasePage {
- private array $currentRepo;
- private Git $git;
+ private $currentRepo;
+ private $git;
public function __construct(
public function render(): void {
- $this->renderLayout( function() {
- $this->renderBreadcrumbs( $this->currentRepo, ['Tags'] );
- echo '<h2>Tags</h2>';
+ $tpl = new Template( 'tags.tpl' );
+ $trail = [
+ [
+ 'url' => '',
+ 'name' => 'Tags'
+ ]
+ ];
- $tags = [];
+ foreach( $this->getBaseTemplateVars( $this->currentRepo ) as $k => $v ) {
+ $tpl->assign( $k, $v );
+ }
- $this->git->eachTag( function( Tag $tag ) use ( &$tags ) {
- $tags[] = $tag;
- } );
+ $tpl->assign( 'breadcrumbs', $this->buildBreadcrumbs( $this->currentRepo, $trail ) );
- usort( $tags, function( Tag $a, Tag $b ) {
- return $a->compare( $b );
- } );
+ $tags = [];
- if( empty( $tags ) ) {
- echo '<p>No tags found.</p>';
- } else {
- $renderer = new HtmlTagRenderer(
- $this->currentRepo['safe_name']
- );
+ $this->git->eachTag( function( Tag $tag ) use ( &$tags ) {
+ $tags[] = $tag;
+ } );
- echo '<table class="tag-table">';
- echo '<thead>';
- echo '<tr>';
- echo '<th>Name</th><th>Message</th><th>Author</th>';
- echo '<th class="tag-age-header">Age</th>';
- echo '<th class="tag-commit-header">Commit</th>';
- echo '</tr>';
- echo '</thead>';
- echo '<tbody>';
+ \usort( $tags, function( Tag $a, Tag $b ) {
+ return $a->compare( $b );
+ } );
- $count = count( $tags );
+ $tpl->assign( 'has_tags', !empty( $tags ) );
- for( $i = 0; $i < $count; $i++ ) {
- $tags[$i]->render(
- $renderer,
- isset( $tags[$i + 1] ) ? $tags[$i + 1] : new MissingTag()
- );
- }
+ if( !empty( $tags ) ) {
+ $renderer = new HtmlTagRenderer( $this->currentRepo['safe_name'] );
- echo '</tbody>';
- echo '</table>';
+ \ob_start();
+
+ $count = \count( $tags );
+
+ for( $i = 0; $i < $count; $i++ ) {
+ $tags[$i]->render(
+ $renderer,
+ isset( $tags[$i + 1] ) ? $tags[$i + 1] : new MissingTag()
+ );
}
- }, $this->currentRepo );
+
+ $tpl->assign( 'tags_html', \ob_get_clean() );
+ }
+
+ echo $tpl->render( 'tags.tpl' );
}
}
Delta 365 lines added, 698 lines removed, 333-line decrease