Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
new/File.php
+<?php
+require_once 'MediaTypeSniffer.php';
+require_once 'FileRenderer.php';
+
+class File {
+ private string $name;
+ private string $sha;
+ private string $mode;
+ private int $timestamp;
+ private bool $isDir;
+
+ public function __construct(string $name, string $sha, string $mode, int $timestamp = 0) {
+ $this->name = $name;
+ $this->sha = $sha;
+ $this->mode = $mode;
+ $this->timestamp = $timestamp;
+ $this->isDir = ($mode === '40000' || $mode === '040000');
+ }
+
+ public function render(FileRenderer $renderer): void {
+ $renderer->render(
+ $this->name,
+ $this->sha,
+ $this->mode,
+ $this->getTimeElapsed(),
+ $this->isDir,
+ $this->getMediaType()
+ );
+ }
+
+ private function getMediaType(): string {
+ return MediaTypeSniffer::getMediaType('', $this->name);
+ }
+
+ private function getTimeElapsed(): string {
+ if (!$this->timestamp) return '';
+
+ $diff = time() - $this->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';
+ }
+}
+
new/FileRenderer.php
+<?php
+interface FileRenderer {
+ public function render(
+ string $name,
+ string $sha,
+ string $mode,
+ string $time,
+ bool $isDir,
+ string $mediaType
+ ): void;
+}
+
+class HtmlFileRenderer implements FileRenderer {
+ private string $repoSafeName;
+
+ public function __construct(string $repoSafeName) {
+ $this->repoSafeName = $repoSafeName;
+ }
+
+ public function render(
+ string $name,
+ string $sha,
+ string $mode,
+ string $time,
+ bool $isDir,
+ string $mediaType
+ ): void {
+ $icon = $isDir ? '[dir]' : '[file]';
+
+ // Simple presentation logic for icons
+ if (!$isDir && str_starts_with($mediaType, 'image/')) {
+ $icon = '[img]';
+ }
+
+ $url = '?repo=' . urlencode($this->repoSafeName) . '&hash=' . $sha;
+
+ echo '<a href="' . $url . '" class="file-item">';
+ echo '<span class="file-mode">' . $mode . '</span>';
+ echo '<span class="file-name">';
+ echo '<span class="' . ($isDir ? 'dir' : 'file') . '-icon">' . $icon . '</span> ';
+ echo htmlspecialchars($name);
+ echo '</span>';
+
+ if ($time) {
+ echo '<span class="file-date" style="color: #8b949e; font-size: 0.8em; margin-left: auto;">' . $time . '</span>';
+ }
+
+ echo '</a>';
+ }
+}
+
new/MediaTypeSniffer.php
+<?php
+class MediaTypeSniffer {
+ private const BUFFER = 12;
+ private const ANY = -1;
+ private const EOS = -2;
+
+ private const FORMATS = [
+ // Images
+ [0x3C, 0x73, 0x76, 0x67, 0x20] => 'image/svg+xml',
+ [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] => 'image/png',
+ [0xFF, 0xD8, 0xFF, 0xE0] => 'image/jpeg',
+ [0xFF, 0xD8, 0xFF, 0xEE] => 'image/jpeg',
+ [0xFF, 0xD8, 0xFF, 0xE1, self::ANY, self::ANY, 0x45, 0x78, 0x69, 0x66, 0x00] =>
+ 'image/jpeg',
+ [0x47, 0x49, 0x46, 0x38] => 'image/gif',
+ [0x42, 0x4D] => 'image/bmp',
+ [0x49, 0x49, 0x2A, 0x00] => 'image/tiff',
+ [0x4D, 0x4D, 0x00, 0x2A] => 'image/tiff',
+ [0x52, 0x49, 0x46, 0x46, self::ANY, self::ANY, self::ANY, self::ANY,
+ 0x57, 0x45, 0x42, 0x50] => 'image/webp',
+ [0x38, 0x42, 0x50, 0x53, 0x00, 0x01] => 'image/vnd.adobe.photoshop',
+ [0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] => 'video/x-mng',
+ [0x23, 0x64, 0x65, 0x66] => 'image/x-xbitmap',
+ [0x21, 0x20, 0x58, 0x50, 0x4D, 0x32] => 'image/x-xpixmap',
+
+ // Documents/Text
+ [0x3C, 0x21] => 'text/html',
+ [0x3C, 0x68, 0x74, 0x6D, 0x6C] => 'text/html',
+ [0x3C, 0x68, 0x65, 0x61, 0x64] => 'text/html',
+ [0x3C, 0x62, 0x6F, 0x64, 0x79] => 'text/html',
+ [0x3C, 0x48, 0x54, 0x4D, 0x4C] => 'text/html',
+ [0x3C, 0x48, 0x45, 0x41, 0x44] => 'text/html',
+ [0x3C, 0x42, 0x4F, 0x44, 0x59] => 'text/html',
+ [0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20] => 'text/xml',
+ [0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x3f, 0x00, 0x78] => 'text/xml',
+ [0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00] => 'text/xml',
+ [0x25, 0x50, 0x44, 0x46, 0x2D] => 'application/pdf',
+ [0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D] =>
+ 'application/postscript',
+ [0x25, 0x21, 0x50, 0x53] => 'application/postscript',
+
+ // Audio/Video
+ [0xFF, 0xFB, self::ANY] => 'audio/mpeg',
+ [0x49, 0x44, 0x33] => 'audio/mpeg',
+ [0x2E, 0x73, 0x6E, 0x64] => 'audio/basic',
+ [0x64, 0x6E, 0x73, 0x2E] => 'audio/basic',
+ [0x52, 0x49, 0x46, 0x46, self::ANY, self::ANY, self::ANY, self::ANY,
+ 0x57, 0x41, 0x56, 0x45] => 'audio/wav',
+
+ // Archives/Binaries
+ [0x50, 0x4B, 0x03, 0x04] => 'application/zip',
+ [0x50, 0x4B, 0x05, 0x06] => 'application/zip',
+ [0x50, 0x4B, 0x07, 0x08] => 'application/zip',
+ [0x1F, 0x8B, 0x08] => 'application/gzip',
+ [0x42, 0x5A, 0x68] => 'application/x-bzip2',
+ [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00] => 'application/x-xz',
+ [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07] => 'application/vnd.rar',
+ [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C] => 'application/x-7z-compressed',
+
+ // Executables/System
+ [0x41, 0x43, self::ANY, self::ANY, self::ANY, self::ANY, 0x00, 0x00,
+ 0x00, 0x00, 0x00] => 'application/acad',
+ [0xCA, 0xFE, 0xBA, 0xBE] => 'application/java-vm',
+ [0xAC, 0xED] => 'application/x-java-serialized-object',
+ [0x4D, 0x5A] => 'application/x-msdownload',
+ [0x7F, 0x45, 0x4C, 0x46] => 'application/x-elf',
+ [0xCE, 0xFA, 0xED, 0xFE] => 'application/x-mach-binary',
+ [0xCF, 0xFA, 0xED, 0xFE] => 'application/x-mach-binary',
+ [0xFE, 0xED, 0xFA, 0xCE] => 'application/x-mach-binary',
+ [0xFE, 0xED, 0xFA, 0xCF] => 'application/x-mach-binary',
+ ];
+
+ private const EXTENSION_MAP = [
+ 'txt' => 'text/plain', 'html' => 'text/html', 'htm' => 'text/html',
+ 'css' => 'text/css', 'js' => 'application/javascript',
+ 'json' => 'application/json', 'xml' => 'application/xml',
+ 'pdf' => 'application/pdf', 'zip' => 'application/zip',
+ 'jar' => 'application/java-archive', 'war' => 'application/java-archive',
+ 'ear' => 'application/java-archive', 'class' => 'application/java-vm',
+ 'gz' => 'application/gzip', 'bz2' => 'application/x-bzip2',
+ 'xz' => 'application/x-xz', 'tar' => 'application/x-tar',
+ 'rar' => 'application/vnd.rar', '7z' => 'application/x-7z-compressed',
+ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
+ 'gif' => 'image/gif', 'svg' => 'image/svg+xml', 'webp' => 'image/webp',
+ 'bmp' => 'image/bmp', 'tiff' => 'image/tiff', 'tif' => 'image/tiff',
+ 'ico' => 'image/x-icon', 'mp4' => 'video/mp4', 'avi' => 'video/x-msvideo',
+ 'mov' => 'video/quicktime', 'wmv' => 'video/x-ms-wmv',
+ 'flv' => 'video/x-flv', 'webm' => 'video/webm', 'mp3' => 'audio/mpeg',
+ 'wav' => 'audio/wav', 'ogg' => 'audio/ogg', 'flac' => 'audio/flac',
+ 'aac' => 'audio/aac', 'php' => 'application/x-php',
+ 'py' => 'text/x-python', 'java' => 'text/x-java', 'c' => 'text/x-c',
+ 'cpp' => 'text/x-c++', 'h' => 'text/x-c', 'hpp' => 'text/x-c++',
+ 'cs' => 'text/x-csharp', 'go' => 'text/x-go', 'rs' => 'text/x-rust',
+ 'rb' => 'text/x-ruby', 'pl' => 'text/x-perl', 'sh' => 'application/x-sh',
+ 'bat' => 'application/x-bat', 'ps1' => 'application/x-powershell',
+ 'md' => 'text/markdown', 'yaml' => 'text/yaml', 'yml' => 'text/yaml',
+ 'toml' => 'application/toml', 'ini' => 'text/plain', 'cfg' => 'text/plain',
+ 'conf' => 'text/plain',
+ ];
+
+ /**
+ * Sniffs the media type based on magic bytes (the first few bytes)
+ * of the data. This internal method is the primary detection.
+ *
+ * @param string $data The raw binary data (a string of bytes).
+ * @return string The determined media type (MIME type).
+ */
+ private static function sniff( $data ): string {
+ $mediaType = 'application/octet-stream';
+
+ if( !empty( $data ) ) {
+ $dataLength = strlen( $data );
+ $maxScan = min( $dataLength, self::BUFFER );
+ $sourceBytes = [];
+
+ for( $i = 0; $i < $maxScan; $i++ ) {
+ $sourceBytes[$i] = ord( $data[$i] ) & 0xFF;
+ }
+
+ foreach( self::FORMATS as $pattern => $type ) {
+ $patternLength = count( $pattern );
+
+ if( $patternLength > $dataLength ) {
+ continue;
+ }
+
+ $matches = true;
+
+ for( $i = 0; $i < $patternLength; $i++ ) {
+ $patternByte = $pattern[$i];
+ $sourceByte = $sourceBytes[$i];
+
+ if( $patternByte !== self::ANY && $patternByte !== $sourceByte ) {
+ $matches = false;
+ break;
+ }
+ }
+
+ if( $matches ) {
+ $mediaType = $type;
+ break;
+ }
+ }
+ }
+
+ return $mediaType;
+ }
+
+ /**
+ * Determines the media type based purely on the file extension.
+ *
+ * @param string $filePath The path to the file.
+ * @return string The determined media type (MIME type).
+ */
+ private static function getMediaTypeByExtension( $filePath ): string {
+ $extension = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) );
+
+ return self::EXTENSION_MAP[$extension] ?? 'application/octet-stream';
+ }
+
+ /**
+ * Public method to get the media type, prioritizing byte analysis and
+ * falling back to extension.
+ *
+ * @param string $data The raw binary data (file content).
+ * @param string $filePath The file path (used for extension fallback).
+ * @return string The determined media type (MIME type).
+ */
+ public static function getMediaType( $data, $filePath = '' ): string {
+ $sniffed = self::sniff( $data );
+
+ return ($sniffed === 'application/octet-stream' && !empty( $filePath )
+ ? self::getMediaTypeByExtension( $filePath )
+ : $sniffed;
+ }
+}
+?>
new/Views.php
<?php
+require_once 'File.php';
+require_once 'FileRenderer.php';
+
abstract class BasePage implements Page {
protected $repositories;
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 '<div class="file-list">';
+
+ // Initialize Renderer
+ $renderer = new HtmlFileRenderer($this->currentRepo['safe_name']);
+
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>';
+ // Pass 0 for timestamp as standard git tree walk doesn't provide it cheaply
+ $file = new File(
+ $e['name'],
+ $e['sha'],
+ $e['mode'],
+ 0
+ );
+
+ $file->render($renderer);
}
+
echo '</div>';
}

Uses file renderer

Author Dave Jarvis <email>
Date 2026-02-08 19:06:55 GMT-0800
Commit b7482d4ab7d84cc4ae8e7719c4185f7c84959452
Parent 8b1325a
Delta 304 lines added, 7 lines removed, 297-line increase