Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
M Git.php
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
A LICENSE.md
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
A Tag.php
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
A TagRenderer.php
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
M TagsPage.php
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
  }
M repo.css
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