Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
M BasePage.php
33 33
            <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a>
34 34
            <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a>
35
            <a href="?action=tags&repo=<?php echo $safeName; ?>">Tags</a>
35 36
          <?php endif; ?>
36 37
37 38
          <?php if ($currentRepo): ?>
38 39
          <div class="repo-selector">
39 40
            <label>Repository:</label>
40 41
            <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)">
41
              <option value="">Select repository...</option>
42 42
              <?php foreach ($this->repositories as $r): ?>
43 43
              <option value="<?php echo htmlspecialchars($r['safe_name']); ?>"
M Git.php
6 6
class Git {
7 7
  private const CHUNK_SIZE = 128;
8
  private const MAX_READ_SIZE = 1048576;
8 9
9 10
  private string $repoPath;
...
52 53
53 54
  public function read( string $sha ): string {
54
    $loosePath = $this->getLoosePath( $sha );
55
56
    if( file_exists( $loosePath ) ) {
57
      $rawContent = file_get_contents( $loosePath );
58
      $inflated   = $rawContent ? @gzuncompress( $rawContent ) : false;
55
    $size = $this->getObjectSize( $sha );
59 56
60
      return $inflated ? explode( "\0", $inflated, 2 )[1] : '';
57
    if( $size > self::MAX_READ_SIZE ) {
58
      return '';
61 59
    }
62 60
63
    return $this->packs->read( $sha ) ?? '';
61
    $content = '';
62
63
    $this->slurp( $sha, function( $chunk ) use ( &$content ) {
64
      $content .= $chunk;
65
    } );
66
67
    return $content;
64 68
  }
65 69
66 70
  public function stream( string $sha, callable $callback ): void {
67
    $data = $this->read( $sha );
71
    $this->slurp( $sha, $callback );
72
  }
68 73
69
    if( $data !== '' ) {
74
  private function slurp( string $sha, callable $callback ): void {
75
    $loosePath = $this->getLoosePath( $sha );
76
77
    if( file_exists( $loosePath ) ) {
78
      $fileHandle = @fopen( $loosePath, 'rb' );
79
80
      if( !$fileHandle ) return;
81
82
      $inflator    = inflate_init( ZLIB_ENCODING_DEFLATE );
83
      $buffer      = '';
84
      $headerFound = false;
85
86
      while( !feof( $fileHandle ) ) {
87
        $chunk         = fread( $fileHandle, 16384 );
88
        $inflatedChunk = @inflate_add( $inflator, $chunk );
89
90
        if( $inflatedChunk === false ) break;
91
92
        if( !$headerFound ) {
93
          $buffer .= $inflatedChunk;
94
          $nullPos = strpos( $buffer, "\0" );
95
96
          if( $nullPos !== false ) {
97
            $body = substr( $buffer, $nullPos + 1 );
98
99
            if( $body !== '' ) {
100
              $callback( $body );
101
            }
102
103
            $headerFound = true;
104
            $buffer      = '';
105
          }
106
        } else {
107
          $callback( $inflatedChunk );
108
        }
109
      }
110
111
      fclose( $fileHandle );
112
      return;
113
    }
114
115
    $data = $this->packs->read( $sha );
116
117
    if( $data !== null && $data !== '' ) {
70 118
      $callback( $data );
71 119
    }
M GitDiff.php
4 4
class GitDiff {
5 5
  private $git;
6
  private const MAX_DIFF_SIZE = 1048576;
6 7
7 8
  public function __construct(Git $git) {
...
99 100
100 101
  private function createChange($type, $path, $oldSha, $newSha) {
102
    // Check file sizes before reading content to prevent OOM
103
    $oldSize = $oldSha ? $this->git->getObjectSize($oldSha) : 0;
104
    $newSize = $newSha ? $this->git->getObjectSize($newSha) : 0;
105
106
    // If file is too large, skip diffing and treat as binary
107
    if ($oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE) {
108
      return [
109
        'type' => $type,
110
        'path' => $path,
111
        'is_binary' => true,
112
        'hunks' => []
113
      ];
114
    }
115
101 116
    $oldContent = $oldSha ? $this->git->read($oldSha) : '';
102 117
    $newContent = $newSha ? $this->git->read($newSha) : '';
...
137 152
    $n = count($newLines);
138 153
139
    // LCS Algorithm
154
    // LCS Algorithm Optimization: Trim matching start/end
140 155
    $start = 0;
141 156
    while ($start < $m && $start < $n && $oldLines[$start] === $newLines[$start]) {
...
150 165
    $oldSlice = array_slice($oldLines, $start, $m - $start - $end);
151 166
    $newSlice = array_slice($newLines, $start, $n - $start - $end);
167
168
    $cntOld = count($oldSlice);
169
    $cntNew = count($newSlice);
170
171
    if (($cntOld * $cntNew) > 500000) {
172
        return [['t' => 'gap']];
173
    }
152 174
153 175
    $ops = $this->computeLCS($oldSlice, $newSlice);
154 176
155
    // Grouping Optimization: Reorder interleaved +/- to be - then +
156 177
    $groupedOps = [];
157 178
    $bufferDel = [];
M GitPacks.php
364 364
365 365
  private function skipSize( string $data, int &$position ): void {
366
    while( ord( $data[$position++] ) & 128 ) {
367
      // Empty loop body
366
    $length = strlen( $data );
367
368
    while( $position < $length && (ord( $data[$position++] ) & 128) ) {
369
      // Loop continues while MSB is 1
368 370
    }
369 371
  }
M HomePage.php
24 24
25 25
  private function renderRepoCard($repo) {
26
    // REUSE: Re-target the single Git instance
27 26
    $this->git->setRepository($repo['path']);
28 27
...
35 34
    echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">';
36 35
    echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>';
37
    if ($main) echo '<p>Branch: ' . htmlspecialchars($main['name']) . '</p>';
38
    echo '<p>' . $stats['branches'] . ' branches, ' . $stats['tags'] . ' tags</p>';
36
37
    echo '<p class="repo-meta">';
38
39
    $branchLabel = $stats['branches'] === 1 ? 'branch' : 'branches';
40
    $tagLabel = $stats['tags'] === 1 ? 'tag' : 'tags';
41
42
    echo $stats['branches'] . ' ' . $branchLabel . ', ' . $stats['tags'] . ' ' . $tagLabel;
39 43
40 44
    if ($main) {
45
      echo ', ';
41 46
      $this->git->history('HEAD', 1, function($c) use ($repo) {
42 47
        $renderer = new HtmlFileRenderer($repo['safe_name']);
43
        echo '<p class="repo-card-time">';
44 48
        $renderer->renderTime($c->date);
45
        echo '</p>';
46 49
      });
50
    }
51
    echo '</p>';
52
53
    $descPath = $repo['path'] . '/description';
54
    if (file_exists($descPath)) {
55
      $description = trim(file_get_contents($descPath));
56
      if ($description !== '') {
57
        echo '<p style="margin-top: 1.5em;">' . htmlspecialchars($description) . '</p>';
58
      }
47 59
    }
48 60
M MediaTypeSniffer.php
53 53
54 54
  private const EXTENSION_MAP = [
55
    'json'      => [self::CAT_TEXT, 'application/json'],
56
    'xml'       => [self::CAT_TEXT, 'application/xml'],
55
    // Documentation / markup
57 56
    'md'        => [self::CAT_TEXT, 'text/markdown'],
58 57
    'rmd'       => [self::CAT_TEXT, 'text/r-markdown'],
59 58
    'txt'       => [self::CAT_TEXT, 'text/plain'],
60
    'yaml'      => [self::CAT_TEXT, 'text/yaml'],
61
    'yml'       => [self::CAT_TEXT, 'text/yaml'],
62
    'gradle'    => [self::CAT_TEXT, 'text/plain'],
63
    'gitignore' => [self::CAT_TEXT, 'text/plain'],
64 59
    'tex'       => [self::CAT_TEXT, 'application/x-tex'],
65 60
    'lyx'       => [self::CAT_TEXT, 'application/x-lyx'],
66
    'bat'       => [self::CAT_TEXT, 'application/x-msdos-program'],
67
    'ts'        => [self::CAT_TEXT, 'application/typescript'],
68
    'log'       => [self::CAT_TEXT, 'text/plain'],
69
    'ini'       => [self::CAT_TEXT, 'text/plain'],
70
    'conf'      => [self::CAT_TEXT, 'text/plain'],
71
    'zip'       => [self::CAT_ARCHIVE, 'application/zip'],
72
    'jpg'       => [self::CAT_IMAGE, 'image/jpeg'],
73
    'jpeg'      => [self::CAT_IMAGE, 'image/jpeg'],
74
    'png'       => [self::CAT_IMAGE, 'image/png'],
75
    'gif'       => [self::CAT_IMAGE, 'image/gif'],
76
    'svg'       => [self::CAT_IMAGE, 'image/svg+xml'],
77
    'webp'      => [self::CAT_IMAGE, 'image/webp'],
78
    'mp4'       => [self::CAT_VIDEO, 'video/mp4'],
79
    'mp3'       => [self::CAT_AUDIO, 'audio/mpeg'],
80
81
    // Data formats
61
    'rst'       => [self::CAT_TEXT, 'text/x-rst'],
62
    'asciidoc'  => [self::CAT_TEXT, 'text/asciidoc'],
63
    'adoc'      => [self::CAT_TEXT, 'text/asciidoc'],
64
    'org'       => [self::CAT_TEXT, 'text/org'],
65
    'latex'     => [self::CAT_TEXT, 'application/x-tex'],
82 66
    'csv'       => [self::CAT_TEXT, 'text/csv'],
83 67
    'tsv'       => [self::CAT_TEXT, 'text/tab-separated-values'],
84 68
    'psv'       => [self::CAT_TEXT, 'text/plain'],
85
    'ndjson'    => [self::CAT_TEXT, 'application/x-ndjson'],
86 69
87
    // Config formats
70
    'json'      => [self::CAT_TEXT, 'application/json'],
71
    'xml'       => [self::CAT_TEXT, 'application/xml'],
72
    'gitignore' => [self::CAT_TEXT, 'text/plain'],
73
    'ts'        => [self::CAT_TEXT, 'application/typescript'],
74
    'log'       => [self::CAT_TEXT, 'text/plain'],
75
    'ndjson'    => [self::CAT_TEXT, 'application/x-ndjson'],
76
    'conf'      => [self::CAT_TEXT, 'text/plain'],
77
    'ini'       => [self::CAT_TEXT, 'text/plain'],
78
    'yaml'      => [self::CAT_TEXT, 'text/yaml'],
79
    'yml'       => [self::CAT_TEXT, 'text/yaml'],
88 80
    'toml'      => [self::CAT_TEXT, 'application/toml'],
89 81
    'env'       => [self::CAT_TEXT, 'text/plain'],
90 82
    'cfg'       => [self::CAT_TEXT, 'text/plain'],
91 83
    'properties'=> [self::CAT_TEXT, 'text/plain'],
92 84
    'dotenv'    => [self::CAT_TEXT, 'text/plain'],
93
94
    // Documentation / markup
95
    'rst'       => [self::CAT_TEXT, 'text/x-rst'],
96
    'asciidoc'  => [self::CAT_TEXT, 'text/asciidoc'],
97
    'adoc'      => [self::CAT_TEXT, 'text/asciidoc'],
98
    'org'       => [self::CAT_TEXT, 'text/org'],
99
    'latex'     => [self::CAT_TEXT, 'application/x-tex'],
100 85
101 86
    // Programming languages
87
    'gradle'    => [self::CAT_TEXT, 'text/plain'],
102 88
    'php'       => [self::CAT_TEXT, 'application/x-php'],
103 89
    'sql'       => [self::CAT_TEXT, 'application/sql'],
104 90
    'html'      => [self::CAT_TEXT, 'text/html'],
91
    'xhtml'     => [self::CAT_TEXT, 'text/xhtml'],
105 92
    'css'       => [self::CAT_TEXT, 'text/css'],
106 93
    'js'        => [self::CAT_TEXT, 'application/javascript'],
...
131 118
    'zsh'       => [self::CAT_TEXT, 'application/x-sh'],
132 119
    'fish'      => [self::CAT_TEXT, 'text/plain'],
133
    'ps1'       => [self::CAT_TEXT, 'application/x-powershell'],
134
135
    // Build / DevOps
136
    'dockerfile'=> [self::CAT_TEXT, 'text/plain'],
137
    'makefile'  => [self::CAT_TEXT, 'text/x-makefile'],
138
    'cmake'     => [self::CAT_TEXT, 'text/x-cmake'],
139
    'gitmodules'=> [self::CAT_TEXT, 'text/plain'],
140
    'editorconfig'=> [self::CAT_TEXT, 'text/plain'],
141
142
    // Dependency / package files
143
    'lock'      => [self::CAT_TEXT, 'text/plain'],
144
    'pipfile'   => [self::CAT_TEXT, 'text/plain'],
145
    'pipfile.lock' => [self::CAT_TEXT, 'application/json'],
146
    'requirements.txt' => [self::CAT_TEXT, 'text/plain'],
147
148
    // Misc text
149
    'license'   => [self::CAT_TEXT, 'text/plain'],
150
    'readme'    => [self::CAT_TEXT, 'text/plain'],
151
    'todo'      => [self::CAT_TEXT, 'text/plain'],
152
    'manifest'  => [self::CAT_TEXT, 'text/plain'],
120
    'bat'       => [self::CAT_TEXT, 'application/x-msdos-program'],
121
    'ps1'       => [self::CAT_TEXT, 'application/x-powershell']
153 122
  ];
154 123
M RepositoryList.php
40 40
41 41
    $lines = file($orderFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
42
    $order = array_flip(array_map('trim', $lines));
42
    $order = [];
43
    $exclude = [];
44
45
    foreach ($lines as $line) {
46
      $line = trim($line);
47
      if ($line === '') continue;
48
49
      if ($line[0] === '-') {
50
        $exclude[substr($line, 1)] = true;
51
      } else {
52
        $order[$line] = count($order);
53
      }
54
    }
55
56
    foreach ($repos as $key => $repo) {
57
      if (isset($exclude[$repo['safe_name']])) {
58
        unset($repos[$key]);
59
      }
60
    }
43 61
44 62
    uasort($repos, function($a, $b) use ($order) {
M Router.php
5 5
require_once 'GitDiff.php';
6 6
require_once 'DiffPage.php';
7
require_once 'TagsPage.php';
7 8
8 9
class Router {
...
50 51
    if ($action === 'commits') {
51 52
      return new CommitsPage($this->repositories, $currentRepo, $this->git, $hash);
53
    }
54
55
    if ($action === 'tags') {
56
      return new TagsPage($this->repositories, $currentRepo, $this->git);
52 57
    }
53 58
A TagsPage.php
1
<?php
2
class TagsPage extends BasePage {
3
  private $currentRepo;
4
  private $git;
5
6
  public function __construct(array $repositories, array $currentRepo, Git $git) {
7
    parent::__construct($repositories);
8
    $this->currentRepo = $currentRepo;
9
    $this->git = $git;
10
    $this->title = $currentRepo['name'] . ' - Tags';
11
  }
12
13
  public function render() {
14
    $this->renderLayout(function() {
15
      $this->renderBreadcrumbs();
16
17
      echo '<h2>Tags</h2>';
18
      echo '<div class="refs-list">';
19
20
      $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
        ];
55
      });
56
57
      // 2. Sort by date descending (newest first)
58
      usort($tags, function($a, $b) {
59
          return $b['timestamp'] <=> $a['timestamp'];
60
      });
61
62
      // 3. Render
63
      if (empty($tags)) {
64
        echo '<div class="empty-state"><p>No tags found.</p></div>';
65
      } else {
66
        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>';
82
        }
83
      }
84
85
      echo '</div>';
86
    }, $this->currentRepo);
87
  }
88
89
  private function renderBreadcrumbs() {
90
    $repoUrl = '?repo=' . urlencode($this->currentRepo['safe_name']);
91
92
    $crumbs = [
93
      '<a href="?">Repositories</a>',
94
      '<a href="' . $repoUrl . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>',
95
      'Tags'
96
    ];
97
98
    echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>';
99
  }
100
}
1 101