| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-10 19:12:35 GMT-0800 |
| Commit | c3267c94011e7e5a2678e4ac60f390376ce2ddb5 |
| Parent | eefd18e |
| <?php | ||
| require_once 'File.php'; | ||
| +require_once 'Tag.php'; | ||
| require_once 'GitRefs.php'; | ||
| require_once 'GitPacks.php'; | ||
| public function eachTag( callable $callback ): void { | ||
| - $this->refs->scanRefs( 'refs/tags', $callback ); | ||
| + $this->refs->scanRefs( 'refs/tags', function($name, $sha) use ($callback) { | ||
| + $data = $this->read($sha); | ||
| + | ||
| + $targetSha = $sha; | ||
| + $timestamp = 0; | ||
| + $message = ''; | ||
| + $author = ''; | ||
| + | ||
| + // Determine if Annotated Tag or Lightweight Tag | ||
| + if (strncmp($data, 'object ', 7) === 0) { | ||
| + // Annotated Tag | ||
| + if (preg_match('/^object ([0-9a-f]{40})$/m', $data, $m)) { | ||
| + $targetSha = $m[1]; | ||
| + } | ||
| + if (preg_match('/^tagger (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) { | ||
| + $author = trim($m[1]); | ||
| + $timestamp = (int)$m[2]; | ||
| + } | ||
| + | ||
| + $pos = strpos($data, "\n\n"); | ||
| + if ($pos !== false) { | ||
| + $message = trim(substr($data, $pos + 2)); | ||
| + } | ||
| + } else { | ||
| + // Lightweight Tag (points directly to commit) | ||
| + // We parse the commit data to get date/author | ||
| + if (preg_match('/^author (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) { | ||
| + $author = trim($m[1]); | ||
| + $timestamp = (int)$m[2]; | ||
| + } | ||
| + | ||
| + $pos = strpos($data, "\n\n"); | ||
| + if ($pos !== false) { | ||
| + $message = trim(substr($data, $pos + 2)); | ||
| + } | ||
| + } | ||
| + | ||
| + $callback(new Tag( | ||
| + $name, | ||
| + $sha, | ||
| + $targetSha, | ||
| + $timestamp, | ||
| + $message, | ||
| + $author | ||
| + )); | ||
| + }); | ||
| } | ||
| +<?php | ||
| +require_once 'TagRenderer.php'; | ||
| + | ||
| +class Tag { | ||
| + private string $name; | ||
| + private string $sha; | ||
| + private string $targetSha; | ||
| + private int $timestamp; | ||
| + private string $message; | ||
| + private string $author; | ||
| + | ||
| + public function __construct( | ||
| + string $name, | ||
| + string $sha, | ||
| + string $targetSha, | ||
| + int $timestamp, | ||
| + string $message, | ||
| + string $author | ||
| + ) { | ||
| + $this->name = $name; | ||
| + $this->sha = $sha; | ||
| + $this->targetSha = $targetSha; | ||
| + $this->timestamp = $timestamp; | ||
| + $this->message = $message; | ||
| + $this->author = $author; | ||
| + } | ||
| + | ||
| + public function compare(Tag $other): int { | ||
| + return $other->timestamp <=> $this->timestamp; | ||
| + } | ||
| + | ||
| + public function render(TagRenderer $renderer): void { | ||
| + $renderer->renderTagItem( | ||
| + $this->name, | ||
| + $this->sha, | ||
| + $this->targetSha, | ||
| + $this->timestamp, | ||
| + $this->message, | ||
| + $this->author | ||
| + ); | ||
| + } | ||
| +} | ||
| +<?php | ||
| +interface TagRenderer { | ||
| + public function renderTagItem( | ||
| + string $name, | ||
| + string $sha, | ||
| + string $targetSha, | ||
| + 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 { | ||
| + $repoParam = '&repo=' . urlencode($this->repoSafeName); | ||
| + $filesUrl = '?hash=' . $targetSha . $repoParam; | ||
| + $commitUrl = '?action=commits&hash=' . $targetSha . $repoParam; | ||
| + | ||
| + echo '<div class="ref-item tag-item">'; | ||
| + | ||
| + // Header: Name and Date | ||
| + echo '<div class="ref-header">'; | ||
| + echo '<a href="' . $filesUrl . '" class="ref-name">'; | ||
| + echo '<i class="fas fa-tag"></i> ' . htmlspecialchars($name); | ||
| + echo '</a>'; | ||
| + | ||
| + if ($timestamp > 0) { | ||
| + echo '<span class="ref-date">'; | ||
| + $this->renderTime($timestamp); | ||
| + echo '</span>'; | ||
| + } | ||
| + echo '</div>'; | ||
| + | ||
| + // Body: Message | ||
| + if ($message !== '') { | ||
| + // Get first line only for brevity in list view | ||
| + $firstLine = strtok($message, "\n"); | ||
| + echo '<div class="ref-message">' . htmlspecialchars($firstLine) . '</div>'; | ||
| + } | ||
| + | ||
| + // Footer: SHA and Author | ||
| + echo '<div class="ref-meta">'; | ||
| + echo '<a href="' . $commitUrl . '" class="commit-hash">' . substr($sha, 0, 7) . '</a>'; | ||
| + | ||
| + if ($author !== '') { | ||
| + echo '<span class="ref-author"> by ' . htmlspecialchars($author) . '</span>'; | ||
| + } | ||
| + echo '</div>'; | ||
| + | ||
| + echo '</div>'; | ||
| + } | ||
| + | ||
| + 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; | ||
| + } | ||
| + | ||
| + echo 'just now'; | ||
| + } | ||
| +} | ||
| <?php | ||
| +require_once 'TagRenderer.php'; | ||
| + | ||
| class TagsPage extends BasePage { | ||
| private $currentRepo; | ||
| $tags = []; | ||
| - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - | ||
| - // 1. Collect tags and parse dates | ||
| - $this->git->eachTag(function($name, $sha) use (&$tags) { | ||
| - // Read the object to peel tags and get dates | ||
| - $data = $this->git->read($sha); | ||
| - $targetSha = $sha; | ||
| - $timestamp = 0; | ||
| - | ||
| - // Check if Annotated Tag (starts with 'object <sha>') | ||
| - if (strncmp($data, 'object ', 7) === 0) { | ||
| - // Extract target SHA | ||
| - if (preg_match('/^object ([0-9a-f]{40})$/m', $data, $matches)) { | ||
| - $targetSha = $matches[1]; | ||
| - } | ||
| - // Extract Tagger Date | ||
| - if (preg_match('/^tagger .* (\d+) [+\-]\d{4}$/m', $data, $matches)) { | ||
| - $timestamp = (int)$matches[1]; | ||
| - } | ||
| - } | ||
| - // Lightweight Tag (Commit object) | ||
| - else { | ||
| - // Extract Author Date | ||
| - if (preg_match('/^author .* (\d+) [+\-]\d{4}$/m', $data, $matches)) { | ||
| - $timestamp = (int)$matches[1]; | ||
| - } | ||
| - } | ||
| - | ||
| - $tags[] = [ | ||
| - 'name' => $name, | ||
| - 'sha' => $sha, | ||
| - 'targetSha' => $targetSha, | ||
| - 'timestamp' => $timestamp | ||
| - ]; | ||
| + $this->git->eachTag(function(Tag $tag) use (&$tags) { | ||
| + $tags[] = $tag; | ||
| }); | ||
| - // 2. Sort by date descending (newest first) | ||
| - usort($tags, function($a, $b) { | ||
| - return $b['timestamp'] <=> $a['timestamp']; | ||
| + usort($tags, function(Tag $a, Tag $b) { | ||
| + return $a->compare($b); | ||
| }); | ||
| - // 3. Render | ||
| + $renderer = new HtmlTagRenderer($this->currentRepo['safe_name']); | ||
| + | ||
| if (empty($tags)) { | ||
| echo '<div class="empty-state"><p>No tags found.</p></div>'; | ||
| } else { | ||
| foreach ($tags as $tag) { | ||
| - $dateStr = $tag['timestamp'] ? date('Y-M-d', $tag['timestamp']) : ''; | ||
| - $filesUrl = '?hash=' . $tag['targetSha'] . $repoParam; | ||
| - $commitUrl = '?action=commit&hash=' . $tag['targetSha'] . $repoParam; | ||
| - | ||
| - echo '<div class="ref-item">'; | ||
| - // "Tag" label removed here | ||
| - echo '<a href="' . $filesUrl . '" class="ref-name">' . htmlspecialchars($tag['name']) . '</a>'; | ||
| - | ||
| - if ($dateStr) { | ||
| - echo '<span class="commit-date" style="margin-left: auto; margin-right: 15px;">' . $dateStr . '</span>'; | ||
| - } | ||
| - | ||
| - // We display the Tag SHA, but link to the Commit SHA | ||
| - echo '<a href="' . $commitUrl . '" class="commit-hash" ' . (!$dateStr ? 'style="margin-left: auto;"' : '') . '>' . substr($tag['sha'], 0, 7) . '</a>'; | ||
| - echo '</div>'; | ||
| + $tag->render($renderer); | ||
| } | ||
| } | ||
| Delta | 198 lines added, 54 lines removed, 144-line increase |
|---|