| 1 | 1 | <?php |
| 2 | 2 | require_once 'File.php'; |
| 3 | require_once 'Tag.php'; |
|
| 3 | 4 | require_once 'GitRefs.php'; |
| 4 | 5 | require_once 'GitPacks.php'; |
| ... | ||
| 39 | 40 | |
| 40 | 41 | public function eachTag( callable $callback ): void { |
| 41 | $this->refs->scanRefs( 'refs/tags', $callback ); |
|
| 42 | $this->refs->scanRefs( 'refs/tags', function($name, $sha) use ($callback) { |
|
| 43 | $data = $this->read($sha); |
|
| 44 | ||
| 45 | $targetSha = $sha; |
|
| 46 | $timestamp = 0; |
|
| 47 | $message = ''; |
|
| 48 | $author = ''; |
|
| 49 | ||
| 50 | // Determine if Annotated Tag or Lightweight Tag |
|
| 51 | if (strncmp($data, 'object ', 7) === 0) { |
|
| 52 | // Annotated Tag |
|
| 53 | if (preg_match('/^object ([0-9a-f]{40})$/m', $data, $m)) { |
|
| 54 | $targetSha = $m[1]; |
|
| 55 | } |
|
| 56 | if (preg_match('/^tagger (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) { |
|
| 57 | $author = trim($m[1]); |
|
| 58 | $timestamp = (int)$m[2]; |
|
| 59 | } |
|
| 60 | ||
| 61 | $pos = strpos($data, "\n\n"); |
|
| 62 | if ($pos !== false) { |
|
| 63 | $message = trim(substr($data, $pos + 2)); |
|
| 64 | } |
|
| 65 | } else { |
|
| 66 | // Lightweight Tag (points directly to commit) |
|
| 67 | // We parse the commit data to get date/author |
|
| 68 | if (preg_match('/^author (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) { |
|
| 69 | $author = trim($m[1]); |
|
| 70 | $timestamp = (int)$m[2]; |
|
| 71 | } |
|
| 72 | ||
| 73 | $pos = strpos($data, "\n\n"); |
|
| 74 | if ($pos !== false) { |
|
| 75 | $message = trim(substr($data, $pos + 2)); |
|
| 76 | } |
|
| 77 | } |
|
| 78 | ||
| 79 | $callback(new Tag( |
|
| 80 | $name, |
|
| 81 | $sha, |
|
| 82 | $targetSha, |
|
| 83 | $timestamp, |
|
| 84 | $message, |
|
| 85 | $author |
|
| 86 | )); |
|
| 87 | }); |
|
| 42 | 88 | } |
| 43 | 89 | |
| 1 | MIT License |
|
| 2 | ||
| 3 | Copyright (c) 2026 White Magic Software, Ltd. |
|
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy |
|
| 6 | of this software and associated documentation files (the "Software"), to deal |
|
| 7 | in the Software without restriction, including without limitation the rights |
|
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
| 9 | copies of the Software, and to permit persons to whom the Software is |
|
| 10 | furnished to do so, subject to the following conditions: |
|
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all |
|
| 13 | copies or substantial portions of the Software. |
|
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
| 21 | SOFTWARE. |
|
| 1 | 22 |
| 1 | <?php |
|
| 2 | require_once 'TagRenderer.php'; |
|
| 3 | ||
| 4 | class Tag { |
|
| 5 | private string $name; |
|
| 6 | private string $sha; |
|
| 7 | private string $targetSha; |
|
| 8 | private int $timestamp; |
|
| 9 | private string $message; |
|
| 10 | private string $author; |
|
| 11 | ||
| 12 | public function __construct( |
|
| 13 | string $name, |
|
| 14 | string $sha, |
|
| 15 | string $targetSha, |
|
| 16 | int $timestamp, |
|
| 17 | string $message, |
|
| 18 | string $author |
|
| 19 | ) { |
|
| 20 | $this->name = $name; |
|
| 21 | $this->sha = $sha; |
|
| 22 | $this->targetSha = $targetSha; |
|
| 23 | $this->timestamp = $timestamp; |
|
| 24 | $this->message = $message; |
|
| 25 | $this->author = $author; |
|
| 26 | } |
|
| 27 | ||
| 28 | public function compare(Tag $other): int { |
|
| 29 | return $other->timestamp <=> $this->timestamp; |
|
| 30 | } |
|
| 31 | ||
| 32 | public function render(TagRenderer $renderer): void { |
|
| 33 | $renderer->renderTagItem( |
|
| 34 | $this->name, |
|
| 35 | $this->sha, |
|
| 36 | $this->targetSha, |
|
| 37 | $this->timestamp, |
|
| 38 | $this->message, |
|
| 39 | $this->author |
|
| 40 | ); |
|
| 41 | } |
|
| 42 | } |
|
| 1 | 43 |
| 1 | <?php |
|
| 2 | interface TagRenderer { |
|
| 3 | public function renderTagItem( |
|
| 4 | string $name, |
|
| 5 | string $sha, |
|
| 6 | string $targetSha, |
|
| 7 | int $timestamp, |
|
| 8 | string $message, |
|
| 9 | string $author |
|
| 10 | ): void; |
|
| 11 | ||
| 12 | public function renderTime(int $timestamp): void; |
|
| 13 | } |
|
| 14 | ||
| 15 | class HtmlTagRenderer implements TagRenderer { |
|
| 16 | private string $repoSafeName; |
|
| 17 | ||
| 18 | public function __construct(string $repoSafeName) { |
|
| 19 | $this->repoSafeName = $repoSafeName; |
|
| 20 | } |
|
| 21 | ||
| 22 | public function renderTagItem( |
|
| 23 | string $name, |
|
| 24 | string $sha, |
|
| 25 | string $targetSha, |
|
| 26 | int $timestamp, |
|
| 27 | string $message, |
|
| 28 | string $author |
|
| 29 | ): void { |
|
| 30 | $repoParam = '&repo=' . urlencode($this->repoSafeName); |
|
| 31 | $filesUrl = '?hash=' . $targetSha . $repoParam; |
|
| 32 | $commitUrl = '?action=commits&hash=' . $targetSha . $repoParam; |
|
| 33 | ||
| 34 | echo '<tr>'; |
|
| 35 | ||
| 36 | // 1. Name |
|
| 37 | echo '<td class="tag-name">'; |
|
| 38 | echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' . htmlspecialchars($name) . '</a>'; |
|
| 39 | echo '</td>'; |
|
| 40 | ||
| 41 | // 2. Message |
|
| 42 | echo '<td class="tag-message">'; |
|
| 43 | echo ($message !== '') ? htmlspecialchars(strtok($message, "\n")) : '<span style="color: #484f58; font-style: italic;">No description</span>'; |
|
| 44 | echo '</td>'; |
|
| 45 | ||
| 46 | // 3. Author |
|
| 47 | echo '<td class="tag-author">' . htmlspecialchars($author) . '</td>'; |
|
| 48 | ||
| 49 | // 4. Timestamp |
|
| 50 | echo '<td class="tag-time">'; |
|
| 51 | $this->renderTime($timestamp); |
|
| 52 | echo '</td>'; |
|
| 53 | ||
| 54 | // 5. Hash |
|
| 55 | echo '<td class="tag-hash">'; |
|
| 56 | echo '<a href="' . $commitUrl . '" class="commit-hash">' . substr($sha, 0, 7) . '</a>'; |
|
| 57 | echo '</td>'; |
|
| 58 | ||
| 59 | echo '</tr>'; |
|
| 60 | } |
|
| 61 | ||
| 62 | public function renderTime(int $timestamp): void { |
|
| 63 | if (!$timestamp) { echo 'never'; return; } |
|
| 64 | $diff = time() - $timestamp; |
|
| 65 | if ($diff < 5) { echo 'just now'; return; } |
|
| 66 | ||
| 67 | $tokens = [ |
|
| 68 | 31536000 => 'year', |
|
| 69 | 2592000 => 'month', |
|
| 70 | 604800 => 'week', |
|
| 71 | 86400 => 'day', |
|
| 72 | 3600 => 'hour', |
|
| 73 | 60 => 'minute', |
|
| 74 | 1 => 'second' |
|
| 75 | ]; |
|
| 76 | ||
| 77 | foreach ($tokens as $unit => $text) { |
|
| 78 | if ($diff < $unit) continue; |
|
| 79 | $num = floor($diff / $unit); |
|
| 80 | echo $num . ' ' . $text . (($num > 1) ? 's' : '') . ' ago'; |
|
| 81 | return; |
|
| 82 | } |
|
| 83 | } |
|
| 84 | } |
|
| 1 | 85 |
| 1 | 1 | <?php |
| 2 | require_once 'TagRenderer.php'; |
|
| 3 | ||
| 2 | 4 | class TagsPage extends BasePage { |
| 3 | 5 | private $currentRepo; |
| ... | ||
| 16 | 18 | |
| 17 | 19 | echo '<h2>Tags</h2>'; |
| 18 | echo '<div class="refs-list">'; |
|
| 20 | echo '<table class="tag-table">'; |
|
| 21 | echo '<thead>'; |
|
| 22 | echo '<tr>'; |
|
| 23 | echo '<th>Name</th>'; |
|
| 24 | echo '<th>Message</th>'; |
|
| 25 | echo '<th>Author</th>'; |
|
| 26 | echo '<th>Age</th>'; |
|
| 27 | echo '<th style="text-align: right;">Commit</th>'; |
|
| 28 | echo '</tr>'; |
|
| 29 | echo '</thead>'; |
|
| 30 | echo '<tbody>'; |
|
| 19 | 31 | |
| 20 | 32 | $tags = []; |
| 21 | $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); |
|
| 22 | ||
| 23 | // 1. Collect tags and parse dates |
|
| 24 | $this->git->eachTag(function($name, $sha) use (&$tags) { |
|
| 25 | // Read the object to peel tags and get dates |
|
| 26 | $data = $this->git->read($sha); |
|
| 27 | $targetSha = $sha; |
|
| 28 | $timestamp = 0; |
|
| 29 | ||
| 30 | // Check if Annotated Tag (starts with 'object <sha>') |
|
| 31 | if (strncmp($data, 'object ', 7) === 0) { |
|
| 32 | // Extract target SHA |
|
| 33 | if (preg_match('/^object ([0-9a-f]{40})$/m', $data, $matches)) { |
|
| 34 | $targetSha = $matches[1]; |
|
| 35 | } |
|
| 36 | // Extract Tagger Date |
|
| 37 | if (preg_match('/^tagger .* (\d+) [+\-]\d{4}$/m', $data, $matches)) { |
|
| 38 | $timestamp = (int)$matches[1]; |
|
| 39 | } |
|
| 40 | } |
|
| 41 | // Lightweight Tag (Commit object) |
|
| 42 | else { |
|
| 43 | // Extract Author Date |
|
| 44 | if (preg_match('/^author .* (\d+) [+\-]\d{4}$/m', $data, $matches)) { |
|
| 45 | $timestamp = (int)$matches[1]; |
|
| 46 | } |
|
| 47 | } |
|
| 48 | ||
| 49 | $tags[] = [ |
|
| 50 | 'name' => $name, |
|
| 51 | 'sha' => $sha, |
|
| 52 | 'targetSha' => $targetSha, |
|
| 53 | 'timestamp' => $timestamp |
|
| 54 | ]; |
|
| 33 | $this->git->eachTag(function(Tag $tag) use (&$tags) { |
|
| 34 | $tags[] = $tag; |
|
| 55 | 35 | }); |
| 56 | 36 | |
| 57 | // 2. Sort by date descending (newest first) |
|
| 58 | usort($tags, function($a, $b) { |
|
| 59 | return $b['timestamp'] <=> $a['timestamp']; |
|
| 37 | usort($tags, function(Tag $a, Tag $b) { |
|
| 38 | return $a->compare($b); |
|
| 60 | 39 | }); |
| 61 | 40 | |
| 62 | // 3. Render |
|
| 41 | $renderer = new HtmlTagRenderer($this->currentRepo['safe_name']); |
|
| 42 | ||
| 63 | 43 | if (empty($tags)) { |
| 64 | echo '<div class="empty-state"><p>No tags found.</p></div>'; |
|
| 44 | echo '<tr><td colspan="5"><div class="empty-state"><p>No tags found.</p></div></td></tr>'; |
|
| 65 | 45 | } else { |
| 66 | 46 | foreach ($tags as $tag) { |
| 67 | $dateStr = $tag['timestamp'] ? date('Y-M-d', $tag['timestamp']) : ''; |
|
| 68 | $filesUrl = '?hash=' . $tag['targetSha'] . $repoParam; |
|
| 69 | $commitUrl = '?action=commit&hash=' . $tag['targetSha'] . $repoParam; |
|
| 70 | ||
| 71 | echo '<div class="ref-item">'; |
|
| 72 | // "Tag" label removed here |
|
| 73 | echo '<a href="' . $filesUrl . '" class="ref-name">' . htmlspecialchars($tag['name']) . '</a>'; |
|
| 74 | ||
| 75 | if ($dateStr) { |
|
| 76 | echo '<span class="commit-date" style="margin-left: auto; margin-right: 15px;">' . $dateStr . '</span>'; |
|
| 77 | } |
|
| 78 | ||
| 79 | // We display the Tag SHA, but link to the Commit SHA |
|
| 80 | echo '<a href="' . $commitUrl . '" class="commit-hash" ' . (!$dateStr ? 'style="margin-left: auto;"' : '') . '>' . substr($tag['sha'], 0, 7) . '</a>'; |
|
| 81 | echo '</div>'; |
|
| 47 | $tag->render($renderer); |
|
| 82 | 48 | } |
| 83 | 49 | } |
| 84 | 50 | |
| 85 | echo '</div>'; |
|
| 51 | echo '</tbody>'; |
|
| 52 | echo '</table>'; |
|
| 86 | 53 | }, $this->currentRepo); |
| 87 | 54 | } |
| 476 | 476 | |
| 477 | 477 | |
| 478 | /* --- GIT DIFF STYLES (Protanopia Dark) --- */ |
|
| 479 | ||
| 480 | 478 | .diff-container { |
| 481 | 479 | display: flex; |
| ... | ||
| 595 | 593 | .status-del { color: #d29922; } |
| 596 | 594 | .status-mod { color: #a371f7; } |
| 595 | ||
| 596 | .tag-table { |
|
| 597 | width: 100%; |
|
| 598 | border-collapse: collapse; |
|
| 599 | margin-top: 10px; |
|
| 600 | } |
|
| 601 | ||
| 602 | .tag-table th { |
|
| 603 | text-align: left; |
|
| 604 | padding: 10px 16px; |
|
| 605 | border-bottom: 2px solid #30363d; |
|
| 606 | color: #8b949e; |
|
| 607 | font-size: 0.875rem; |
|
| 608 | font-weight: 600; |
|
| 609 | } |
|
| 610 | ||
| 611 | .tag-table td { |
|
| 612 | padding: 12px 16px; |
|
| 613 | border-bottom: 1px solid #21262d; |
|
| 614 | vertical-align: middle; |
|
| 615 | color: #c9d1d9; |
|
| 616 | font-size: 0.9rem; |
|
| 617 | } |
|
| 618 | ||
| 619 | .tag-table tr:hover td { |
|
| 620 | background: #161b22; |
|
| 621 | } |
|
| 622 | ||
| 623 | .tag-table .tag-name a { |
|
| 624 | color: #58a6ff; |
|
| 625 | text-decoration: none; |
|
| 626 | font-family: 'SFMono-Regular', Consolas, monospace; |
|
| 627 | } |
|
| 628 | ||
| 629 | .tag-table .tag-message { |
|
| 630 | font-weight: 500; |
|
| 631 | } |
|
| 632 | ||
| 633 | .tag-table .tag-author { |
|
| 634 | color: #c9d1d9; |
|
| 635 | } |
|
| 636 | ||
| 637 | .tag-table .tag-time { |
|
| 638 | color: #8b949e; |
|
| 639 | white-space: nowrap; |
|
| 640 | } |
|
| 641 | ||
| 642 | .tag-table .tag-hash { |
|
| 643 | text-align: right; |
|
| 644 | } |
|
| 645 | ||
| 646 | .tag-table .commit-hash { |
|
| 647 | font-family: 'SFMono-Regular', Consolas, monospace; |
|
| 648 | color: #58a6ff; |
|
| 649 | text-decoration: none; |
|
| 650 | } |
|
| 597 | 651 | |
| 652 | .tag-table .commit-hash:hover { |
|
| 653 | text-decoration: underline; |
|
| 654 | } |
|
| 598 | 655 | |