| | <?php |
| | abstract class BasePage implements Page { |
| | - protected $repositories; |
| | - protected $title; |
| | + protected $repositories; |
| | + protected $title; |
| | |
| | - public function __construct(array $repositories) { |
| | - $this->repositories = $repositories; |
| | - } |
| | + public function __construct(array $repositories) { |
| | + $this->repositories = $repositories; |
| | + } |
| | |
| | - // Shared Layout Logic |
| | - protected function renderLayout($contentCallback, $currentRepo = null) { |
| | - ?> |
| | - <!DOCTYPE html> |
| | - <html lang="en"> |
| | - <head> |
| | - <meta charset="UTF-8"> |
| | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | - <title><?php echo Config::SITE_TITLE . ($this->title ? ' - ' . htmlspecialchars($this->title) : ''); ?></title> |
| | - <link rel="stylesheet" href="repo.css"> |
| | - <style> |
| | - .commit-list { margin-top: 20px; } |
| | - .commit-row { display: flex; padding: 10px 0; border-bottom: 1px solid #eee; gap: 15px; align-items: baseline; } |
| | - .commit-row:last-child { border-bottom: none; } |
| | - .commit-row .sha { font-family: monospace; color: #0366d6; text-decoration: none; } |
| | - .commit-row .message { flex: 1; font-weight: 500; } |
| | - .commit-row .meta { font-size: 0.85em; color: #666; white-space: nowrap; } |
| | - </style> |
| | - </head> |
| | - <body> |
| | - <div class="container"> |
| | - <header> |
| | - <h1><?php echo Config::SITE_TITLE; ?></h1> |
| | - <nav class="nav"> |
| | - <a href="?">Home</a> |
| | - <?php if ($currentRepo): |
| | - $safeName = urlencode($currentRepo['safe_name']); ?> |
| | - <a href="?repo=<?php echo $safeName; ?>">Files</a> |
| | - <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a> |
| | - <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a> |
| | - <?php endif; ?> |
| | + // Shared Layout Logic |
| | + protected function renderLayout($contentCallback, $currentRepo = null) { |
| | + ?> |
| | + <!DOCTYPE html> |
| | + <html lang="en"> |
| | + <head> |
| | + <meta charset="UTF-8"> |
| | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | + <title><?php echo Config::SITE_TITLE . ($this->title ? ' - ' . htmlspecialchars($this->title) : ''); ?></title> |
| | + <link rel="stylesheet" href="repo.css"> |
| | + <style> |
| | + .commit-list { margin-top: 20px; } |
| | + .commit-row { display: flex; padding: 10px 0; border-bottom: 1px solid #eee; gap: 15px; align-items: baseline; } |
| | + .commit-row:last-child { border-bottom: none; } |
| | + .commit-row .sha { font-family: monospace; color: #0366d6; text-decoration: none; } |
| | + .commit-row .message { flex: 1; font-weight: 500; } |
| | + .commit-row .meta { font-size: 0.85em; color: #666; white-space: nowrap; } |
| | + </style> |
| | + </head> |
| | + <body> |
| | + <div class="container"> |
| | + <header> |
| | + <h1><?php echo Config::SITE_TITLE; ?></h1> |
| | + <nav class="nav"> |
| | + <a href="?">Home</a> |
| | + <?php if ($currentRepo): |
| | + $safeName = urlencode($currentRepo['safe_name']); ?> |
| | + <a href="?repo=<?php echo $safeName; ?>">Files</a> |
| | + <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a> |
| | + <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a> |
| | + <?php endif; ?> |
| | |
| | - <?php if ($currentRepo): ?> |
| | - <div class="repo-selector"> |
| | - <label>Repository:</label> |
| | - <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)"> |
| | - <option value="">Select repository...</option> |
| | - <?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 endforeach; ?> |
| | - </select> |
| | - </div> |
| | - <?php endif; ?> |
| | - </nav> |
| | + <?php if ($currentRepo): ?> |
| | + <div class="repo-selector"> |
| | + <label>Repository:</label> |
| | + <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)"> |
| | + <option value="">Select repository...</option> |
| | + <?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 endforeach; ?> |
| | + </select> |
| | + </div> |
| | + <?php endif; ?> |
| | + </nav> |
| | |
| | - <?php if ($currentRepo): ?> |
| | - <div style="margin-top: 15px;"> |
| | - <span class="current-repo">Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong></span> |
| | - </div> |
| | - <?php endif; ?> |
| | - </header> |
| | + <?php if ($currentRepo): ?> |
| | + <div style="margin-top: 15px;"> |
| | + <span class="current-repo">Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong></span> |
| | + </div> |
| | + <?php endif; ?> |
| | + </header> |
| | |
| | - <?php call_user_func($contentCallback); ?> |
| | + <?php call_user_func($contentCallback); ?> |
| | |
| | - </div> |
| | - </body> |
| | - </html> |
| | - <?php |
| | - } |
| | + </div> |
| | + </body> |
| | + </html> |
| | + <?php |
| | + } |
| | |
| | - protected function time_elapsed_string($timestamp) { |
| | - if (!$timestamp) return 'never'; |
| | - $diff = time() - $timestamp; |
| | - if ($diff < 5) return 'just now'; |
| | - $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); |
| | - return $num . ' ' . $text . (($num > 1) ? 's' : '') . ' ago'; |
| | - } |
| | - return 'just now'; |
| | + protected function time_elapsed_string($timestamp) { |
| | + if (!$timestamp) return 'never'; |
| | + $diff = time() - $timestamp; |
| | + if ($diff < 5) return 'just now'; |
| | + $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); |
| | + return $num . ' ' . $text . (($num > 1) ? 's' : '') . ' ago'; |
| | } |
| | + return 'just now'; |
| | + } |
| | } |
| | |
| | class HomePage extends BasePage { |
| | - public function render() { |
| | - $this->renderLayout(function() { |
| | - echo '<h2>Repositories</h2>'; |
| | - if (empty($this->repositories)) { |
| | - echo '<div class="empty-state">No repositories found in ' . htmlspecialchars(Config::getReposPath()) . '</div>'; |
| | - return; |
| | - } |
| | - echo '<div class="repo-grid">'; |
| | - foreach ($this->repositories as $repo) { |
| | - $this->renderRepoCard($repo); |
| | - } |
| | - echo '</div>'; |
| | - }); |
| | - } |
| | + public function render() { |
| | + $this->renderLayout(function() { |
| | + echo '<h2>Repositories</h2>'; |
| | + if (empty($this->repositories)) { |
| | + echo '<div class="empty-state">No repositories found in ' . htmlspecialchars(Config::getReposPath()) . '</div>'; |
| | + return; |
| | + } |
| | + echo '<div class="repo-grid">'; |
| | + foreach ($this->repositories as $repo) { |
| | + $this->renderRepoCard($repo); |
| | + } |
| | + echo '</div>'; |
| | + }); |
| | + } |
| | |
| | - private function renderRepoCard($repo) { |
| | - $git = new Git($repo['path']); |
| | - $main = $git->getMainBranch(); |
| | + private function renderRepoCard($repo) { |
| | + $git = new Git($repo['path']); |
| | + $main = $git->getMainBranch(); |
| | |
| | - $stats = ['branches' => 0, 'tags' => 0]; |
| | - $git->eachBranch(function() use (&$stats) { $stats['branches']++; }); |
| | - $git->eachTag(function() use (&$stats) { $stats['tags']++; }); |
| | + $stats = ['branches' => 0, 'tags' => 0]; |
| | + $git->eachBranch(function() use (&$stats) { $stats['branches']++; }); |
| | + $git->eachTag(function() use (&$stats) { $stats['tags']++; }); |
| | |
| | - echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">'; |
| | - echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>'; |
| | - if ($main) echo '<p>Branch: ' . htmlspecialchars($main['name']) . '</p>'; |
| | - echo '<p>' . $stats['branches'] . ' branches, ' . $stats['tags'] . ' tags</p>'; |
| | + echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">'; |
| | + echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>'; |
| | + if ($main) echo '<p>Branch: ' . htmlspecialchars($main['name']) . '</p>'; |
| | + echo '<p>' . $stats['branches'] . ' branches, ' . $stats['tags'] . ' tags</p>'; |
| | |
| | - // Fixed: Only attempt to fetch history if a main branch exists |
| | - if ($main) { |
| | - $git->history('HEAD', 1, function($c) { |
| | - echo '<p style="margin-top: 8px; color: #58a6ff;">' . $this->time_elapsed_string($c->date) . '</p>'; |
| | - }); |
| | - } |
| | - echo '</a>'; |
| | + // Fixed: Only attempt to fetch history if a main branch exists |
| | + if ($main) { |
| | + $git->history('HEAD', 1, function($c) { |
| | + echo '<p style="margin-top: 8px; color: #58a6ff;">' . $this->time_elapsed_string($c->date) . '</p>'; |
| | + }); |
| | } |
| | + echo '</a>'; |
| | + } |
| | } |
| | |
| | class CommitsPage extends BasePage { |
| | - private $currentRepo; |
| | - private $git; |
| | - private $hash; |
| | + private $currentRepo; |
| | + private $git; |
| | + private $hash; |
| | |
| | - public function __construct($allRepos, $currentRepo, $git, $hash) { |
| | - parent::__construct($allRepos); |
| | - $this->currentRepo = $currentRepo; |
| | - $this->git = $git; |
| | - $this->hash = $hash; |
| | - $this->title = $currentRepo['name']; |
| | - } |
| | + public function __construct($allRepos, $currentRepo, $git, $hash) { |
| | + parent::__construct($allRepos); |
| | + $this->currentRepo = $currentRepo; |
| | + $this->git = $git; |
| | + $this->hash = $hash; |
| | + $this->title = $currentRepo['name']; |
| | + } |
| | |
| | - public function render() { |
| | - $this->renderLayout(function() { |
| | - $main = $this->git->getMainBranch(); |
| | - if (!$main) { |
| | - echo '<div class="empty-state"><h3>No branches</h3><p>Empty repository.</p></div>'; |
| | - return; |
| | - } |
| | + public function render() { |
| | + $this->renderLayout(function() { |
| | + $main = $this->git->getMainBranch(); |
| | + if (!$main) { |
| | + echo '<div class="empty-state"><h3>No branches</h3><p>Empty repository.</p></div>'; |
| | + return; |
| | + } |
| | |
| | - $this->renderBreadcrumbs(); |
| | - echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; |
| | - echo '<div class="commit-list">'; |
| | + $this->renderBreadcrumbs(); |
| | + echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; |
| | + echo '<div class="commit-list">'; |
| | |
| | - $start = $this->hash ?: $main['hash']; |
| | - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); |
| | + $start = $this->hash ?: $main['hash']; |
| | + $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); |
| | |
| | - $this->git->history($start, 100, function($commit) use ($repoParam) { |
| | - $msg = htmlspecialchars(explode("\n", $commit->message)[0]); |
| | - echo '<div class="commit-row">'; |
| | - echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . '" class="sha">' . substr($commit->sha, 0, 7) . '</a>'; |
| | - echo '<span class="message">' . $msg . '</span>'; |
| | - echo '<span class="meta">' . htmlspecialchars($commit->author) . ' • ' . date('Y-m-d', $commit->date) . '</span>'; |
| | - echo '</div>'; |
| | - }); |
| | - echo '</div>'; |
| | - }, $this->currentRepo); |
| | - } |
| | + $this->git->history($start, 100, function($commit) use ($repoParam) { |
| | + $msg = htmlspecialchars(explode("\n", $commit->message)[0]); |
| | + echo '<div class="commit-row">'; |
| | + echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . '" class="sha">' . substr($commit->sha, 0, 7) . '</a>'; |
| | + echo '<span class="message">' . $msg . '</span>'; |
| | + echo '<span class="meta">' . htmlspecialchars($commit->author) . ' • ' . date('Y-m-d', $commit->date) . '</span>'; |
| | + echo '</div>'; |
| | + }); |
| | + echo '</div>'; |
| | + }, $this->currentRepo); |
| | + } |
| | |
| | - private function renderBreadcrumbs() { |
| | - echo '<div class="breadcrumb">'; |
| | - echo '<a href="?">Repositories</a><span>/</span>'; |
| | - echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a><span>/</span>'; |
| | - echo '<span>Commits</span></div>'; |
| | - } |
| | + private function renderBreadcrumbs() { |
| | + echo '<div class="breadcrumb">'; |
| | + echo '<a href="?">Repositories</a><span>/</span>'; |
| | + echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a><span>/</span>'; |
| | + echo '<span>Commits</span></div>'; |
| | + } |
| | } |
| | |
| | class FilePage extends BasePage { |
| | - private $currentRepo; |
| | - private $git; |
| | - private $hash; |
| | + private $currentRepo; |
| | + private $git; |
| | + private $hash; |
| | |
| | - public function __construct($allRepos, $currentRepo, $git, $hash) { |
| | - parent::__construct($allRepos); |
| | - $this->currentRepo = $currentRepo; |
| | - $this->git = $git; |
| | - $this->hash = $hash; |
| | - $this->title = $currentRepo['name']; |
| | - } |
| | + public function __construct($allRepos, $currentRepo, $git, $hash) { |
| | + parent::__construct($allRepos); |
| | + $this->currentRepo = $currentRepo; |
| | + $this->git = $git; |
| | + $this->hash = $hash; |
| | + $this->title = $currentRepo['name']; |
| | + } |
| | |
| | - public function render() { |
| | - $this->renderLayout(function() { |
| | - $main = $this->git->getMainBranch(); |
| | - if (!$main) { |
| | - echo '<div class="empty-state"><h3>No branches</h3></div>'; |
| | - return; |
| | - } |
| | + public function render() { |
| | + $this->renderLayout(function() { |
| | + $main = $this->git->getMainBranch(); |
| | + if (!$main) { |
| | + echo '<div class="empty-state"><h3>No branches</h3></div>'; |
| | + return; |
| | + } |
| | |
| | - $target = $this->hash ?: $main['hash']; |
| | - $entries = []; |
| | - $this->git->walk($target, function($e) use (&$entries) { |
| | - $entries[] = (array)$e; |
| | - }); |
| | + $target = $this->hash ?: $main['hash']; |
| | + $entries = []; |
| | + $this->git->walk($target, function($e) use (&$entries) { |
| | + $entries[] = (array)$e; |
| | + }); |
| | |
| | - if (!empty($entries)) { |
| | - $this->renderTree($main, $target, $entries); |
| | - } else { |
| | - $this->renderBlob($target); |
| | - } |
| | - }, $this->currentRepo); |
| | - } |
| | + if (!empty($entries)) { |
| | + $this->renderTree($main, $target, $entries); |
| | + } else { |
| | + $this->renderBlob($target); |
| | + } |
| | + }, $this->currentRepo); |
| | + } |
| | |
| | - private function renderTree($main, $targetHash, $entries) { |
| | - $this->renderBreadcrumbs($targetHash, 'Tree'); |
| | - echo '<h2>' . htmlspecialchars($this->currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; |
| | + private function renderTree($main, $targetHash, $entries) { |
| | + $this->renderBreadcrumbs($targetHash, 'Tree'); |
| | + echo '<h2>' . htmlspecialchars($this->currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; |
| | |
| | - usort($entries, function($a, $b) { |
| | - return ($b['isDir'] <=> $a['isDir']) ?: strcasecmp($a['name'], $b['name']); |
| | - }); |
| | + usort($entries, function($a, $b) { |
| | + return ($b['isDir'] <=> $a['isDir']) ?: strcasecmp($a['name'], $b['name']); |
| | + }); |
| | |
| | - echo '<div class="file-list">'; |
| | - foreach ($entries as $e) { |
| | - $icon = $e['isDir'] ? '[dir]' : '[file]'; |
| | - $url = '?repo=' . urlencode($this->currentRepo['safe_name']) . '&hash=' . $e['sha']; |
| | - echo '<a href="' . $url . '" class="file-item">'; |
| | - echo '<span class="file-mode">' . $e['mode'] . '</span>'; |
| | - echo '<span class="file-name"><span class="' . ($e['isDir'] ? 'dir' : 'file') . '-icon">' . $icon . '</span> ' . htmlspecialchars($e['name']) . '</span>'; |
| | - echo '</a>'; |
| | - } |
| | - echo '</div>'; |
| | + echo '<div class="file-list">'; |
| | + foreach ($entries as $e) { |
| | + $icon = $e['isDir'] ? '[dir]' : '[file]'; |
| | + $url = '?repo=' . urlencode($this->currentRepo['safe_name']) . '&hash=' . $e['sha']; |
| | + echo '<a href="' . $url . '" class="file-item">'; |
| | + echo '<span class="file-mode">' . $e['mode'] . '</span>'; |
| | + echo '<span class="file-name"><span class="' . ($e['isDir'] ? 'dir' : 'file') . '-icon">' . $icon . '</span> ' . htmlspecialchars($e['name']) . '</span>'; |
| | + echo '</a>'; |
| | } |
| | + echo '</div>'; |
| | + } |
| | |
| | - private function renderBlob($targetHash) { |
| | - $content = ''; |
| | - $this->git->stream($targetHash, function($d) use (&$content) { $content = $d; }); |
| | + private function renderBlob($targetHash) { |
| | + $content = ''; |
| | + $this->git->stream($targetHash, function($d) use (&$content) { $content = $d; }); |
| | |
| | - $this->renderBreadcrumbs($targetHash, 'File'); |
| | - echo '<h2>' . substr($targetHash, 0, 7) . '</h2>'; |
| | - echo '<div class="blob-content"><div class="blob-code">' . htmlspecialchars($content) . '</div></div>'; |
| | - } |
| | + $this->renderBreadcrumbs($targetHash, 'File'); |
| | + echo '<h2>' . substr($targetHash, 0, 7) . '</h2>'; |
| | + echo '<div class="blob-content"><div class="blob-code">' . htmlspecialchars($content) . '</div></div>'; |
| | + } |
| | |
| | - private function renderBreadcrumbs($hash, $type) { |
| | - echo '<div class="breadcrumb">'; |
| | - echo '<a href="?">Repositories</a><span>/</span>'; |
| | - echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>'; |
| | - if ($this->hash) echo '<span>/</span><span>' . $type . ' ' . substr($hash, 0, 7) . '</span>'; |
| | - echo '</div>'; |
| | - } |
| | + private function renderBreadcrumbs($hash, $type) { |
| | + echo '<div class="breadcrumb">'; |
| | + echo '<a href="?">Repositories</a><span>/</span>'; |
| | + echo '<a href="?repo=' . urlencode($this->currentRepo['safe_name']) . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>'; |
| | + if ($this->hash) echo '<span>/</span><span>' . $type . ' ' . substr($hash, 0, 7) . '</span>'; |
| | + echo '</div>'; |
| | + } |
| | } |
| | |