| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-08 22:25:30 GMT-0800 |
| Commit | f9d2c669fb26ec1bcfa6fba47617c9fd26d76a8f |
| Parent | f4271bc |
| Delta | 0 lines added, 745 lines removed, 745-line decrease |
|---|
| -<?php | ||
| -/** | ||
| - * Git-related functions for repository operations | ||
| - */ | ||
| -function getRepoPath($repo) { | ||
| - return REPOS_PATH . '/' . basename($repo); | ||
| -} | ||
| - | ||
| -/** | ||
| - * Executes a Git command and caches the output. | ||
| - */ | ||
| -function execGitCached($repo, $command) { | ||
| - $cache_key = md5($repo . '|' . $command); | ||
| - $cache_file = CACHE_DIR . '/' . $cache_key . '.cache'; | ||
| - | ||
| - if (file_exists($cache_file) && (time() - filemtime($cache_file) < CACHE_EXPIRY)) { | ||
| - return file_get_contents($cache_file); | ||
| - } | ||
| - | ||
| - $repoPath = getRepoPath($repo); | ||
| - $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git'); | ||
| - | ||
| - // Note: $command should be built using escapeshellarg() by the caller | ||
| - if ($isBare) { | ||
| - $cmd = "git --git-dir=" . escapeshellarg($repoPath) . " " . $command . " 2>&1"; | ||
| - } else { | ||
| - if (!is_dir($repoPath . '/.git')) { | ||
| - $output = ''; | ||
| - } else { | ||
| - $cmd = "cd " . escapeshellarg($repoPath) . " && git " . $command . " 2>&1"; | ||
| - } | ||
| - } | ||
| - | ||
| - $output = isset($cmd) ? shell_exec($cmd) : ''; | ||
| - | ||
| - if (!empty($output)) { | ||
| - file_put_contents($cache_file, $output); | ||
| - } | ||
| - | ||
| - return $output; | ||
| -} | ||
| - | ||
| -/** | ||
| - * Executes a raw Git command without caching. | ||
| - */ | ||
| -function execGitRaw($repo, $command) { | ||
| - $repoPath = getRepoPath($repo); | ||
| - $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git'); | ||
| - | ||
| - if ($isBare) { | ||
| - $cmd = "git --git-dir=" . escapeshellarg($repoPath) . " " . $command . " 2>&1"; | ||
| - } else { | ||
| - if (!is_dir($repoPath . '/.git')) { | ||
| - return ''; | ||
| - } | ||
| - $cmd = "cd " . escapeshellarg($repoPath) . " && git " . $command . " 2>&1"; | ||
| - } | ||
| - | ||
| - return shell_exec($cmd); | ||
| -} | ||
| - | ||
| -function execGit($repo, $command) { | ||
| - return execGitCached($repo, $command); | ||
| -} | ||
| - | ||
| -/** | ||
| - * Gets blob content as binary data (with caching). | ||
| - */ | ||
| -function getBlobBinary($repo, $hash) { | ||
| - $cache_key = md5($repo . '|blob_binary|' . $hash); | ||
| - $cache_file = CACHE_DIR . '/' . $cache_key . '.cache'; | ||
| - | ||
| - if (file_exists($cache_file) && (time() - filemtime($cache_file) < CACHE_EXPIRY)) { | ||
| - return file_get_contents($cache_file); | ||
| - } | ||
| - | ||
| - $repoPath = getRepoPath($repo); | ||
| - $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git'); | ||
| - | ||
| - // Strictly escape the hash | ||
| - $safeHash = escapeshellarg($hash); | ||
| - | ||
| - if ($isBare) { | ||
| - $cmd = "git --git-dir=" . escapeshellarg($repoPath) . " cat-file blob " . $safeHash; | ||
| - } else { | ||
| - $cmd = "cd " . escapeshellarg($repoPath) . " && git cat-file blob " . $safeHash; | ||
| - } | ||
| - | ||
| - $descriptors = [ | ||
| - 1 => ['pipe', 'w'], | ||
| - 2 => ['pipe', 'w'] | ||
| - ]; | ||
| - | ||
| - $process = proc_open($cmd, $descriptors, $pipes); | ||
| - if (is_resource($process)) { | ||
| - $output = stream_get_contents($pipes[1]); | ||
| - fclose($pipes[1]); | ||
| - fclose($pipes[2]); | ||
| - proc_close($process); | ||
| - | ||
| - if (!empty($output)) { | ||
| - file_put_contents($cache_file, $output); | ||
| - } | ||
| - | ||
| - return $output; | ||
| - } | ||
| - return ''; | ||
| -} | ||
| - | ||
| -<?php | ||
| -function isImageFile($filename) { | ||
| - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); | ||
| - return in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico']); | ||
| -} | ||
| - | ||
| -function isVideoFile($filename) { | ||
| - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); | ||
| - return in_array($ext, ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv', 'm4v']); | ||
| -} | ||
| - | ||
| -function getImageMimeType($filename) { | ||
| - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); | ||
| - $mimeTypes = [ | ||
| - 'png' => 'image/png', | ||
| - 'jpg' => 'image/jpeg', | ||
| - 'jpeg' => 'image/jpeg', | ||
| - 'gif' => 'image/gif', | ||
| - 'svg' => 'image/svg+xml', | ||
| - 'webp' => 'image/webp', | ||
| - 'bmp' => 'image/bmp', | ||
| - 'ico' => 'image/x-icon' | ||
| - ]; | ||
| - return $mimeTypes[$ext] ?? 'application/octet-stream'; | ||
| -} | ||
| - | ||
| -function getVideoMimeType($filename) { | ||
| - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); | ||
| - $mimeTypes = [ | ||
| - 'mp4' => 'video/mp4', | ||
| - 'webm' => 'video/webm', | ||
| - 'ogg' => 'video/ogg', | ||
| - 'mov' => 'video/quicktime', | ||
| - 'avi' => 'video/x-msvideo', | ||
| - 'mkv' => 'video/x-matroska', | ||
| - 'm4v' => 'video/x-m4v' | ||
| - ]; | ||
| - return $mimeTypes[$ext] ?? 'application/octet-stream'; | ||
| -} | ||
| - | ||
| -function formatDate($timestamp) { | ||
| - return date('M j, Y H:i', intval($timestamp)); | ||
| -} | ||
| - | ||
| -function timeAgo($timestamp) { | ||
| - $diff = time() - intval($timestamp); | ||
| - | ||
| - if ($diff < 60) { | ||
| - return 'just now'; | ||
| - } | ||
| - | ||
| - $units = [ | ||
| - 31536000 => 'year', | ||
| - 2592000 => 'month', | ||
| - 604800 => 'week', | ||
| - 86400 => 'day', | ||
| - 3600 => 'hour', | ||
| - 60 => 'minute', | ||
| - ]; | ||
| - | ||
| - foreach ($units as $seconds => $unit_name) { | ||
| - if ($diff >= $seconds) { | ||
| - $value = floor($diff / $seconds); | ||
| - $plural = ($value > 1) ? 's' : ''; | ||
| - return $value . ' ' . $unit_name . $plural . ' ago'; | ||
| - } | ||
| - } | ||
| -} | ||
| - | ||
| -<?php | ||
| -/** | ||
| - * Repository management functions | ||
| - */ | ||
| - | ||
| -function getRepositories() { | ||
| - $repos = []; | ||
| - if (!is_dir(REPOS_PATH)) { | ||
| - return $repos; | ||
| - } | ||
| - | ||
| - $dirs = scandir(REPOS_PATH); | ||
| - foreach ($dirs as $dir) { | ||
| - if ($dir === '.' || $dir === '..') continue; | ||
| - $path = REPOS_PATH . '/' . $dir; | ||
| - if (is_dir($path) && (is_dir($path . '/.git') || is_file($path . '/HEAD'))) { | ||
| - $repos[] = $dir; | ||
| - } | ||
| - } | ||
| - | ||
| - $orderFile = __DIR__ . '/../order.txt'; | ||
| - if (file_exists($orderFile)) { | ||
| - $lines = array_filter(array_map('trim', file($orderFile))); | ||
| - $blacklist = []; | ||
| - $order = []; | ||
| - | ||
| - // Parse order.txt for blacklisted (starting with -) and ordered repos | ||
| - foreach ($lines as $line) { | ||
| - if (substr($line, 0, 1) === '-') { | ||
| - // Blacklisted repo - add without the - prefix | ||
| - $blacklist[] = substr($line, 1); | ||
| - } else { | ||
| - // Normal ordered repo | ||
| - $order[] = $line; | ||
| - } | ||
| - } | ||
| - | ||
| - // Filter out blacklisted repos | ||
| - $repos = array_filter($repos, function($repo) use ($blacklist) { | ||
| - return !in_array($repo, $blacklist); | ||
| - }); | ||
| - | ||
| - // Create order map and sort | ||
| - $orderMap = array_flip($order); | ||
| - | ||
| - usort($repos, function($a, $b) use ($orderMap) { | ||
| - $aPos = isset($orderMap[$a]) ? $orderMap[$a] : PHP_INT_MAX; | ||
| - $bPos = isset($orderMap[$b]) ? $orderMap[$b] : PHP_INT_MAX; | ||
| - | ||
| - if ($aPos === PHP_INT_MAX && $bPos === PHP_INT_MAX) { | ||
| - return strcmp($a, $b); | ||
| - } | ||
| - | ||
| - return $aPos - $bPos; | ||
| - }); | ||
| - } else { | ||
| - sort($repos); | ||
| - } | ||
| - | ||
| - return $repos; | ||
| -} | ||
| - | ||
| -function getRepoInfo($repo) { | ||
| - $info = []; | ||
| - $repoPath = REPOS_PATH . '/' . basename($repo); | ||
| - | ||
| - $descFile = is_dir($repoPath . '/.git') ? $repoPath . '/.git/description' : $repoPath . '/description'; | ||
| - $info['description'] = file_exists($descFile) ? trim(file_get_contents($descFile)) : 'No description'; | ||
| - if ($info['description'] === 'Unnamed repository; edit this file \'description\' to name the repository.') { | ||
| - $info['description'] = 'No description'; | ||
| - } | ||
| - | ||
| - $log = execGitCached($repo, "log -1 --format='%H|%an|%ae|%at|%s'"); | ||
| - if (!empty($log) && $log !== null) { | ||
| - $parts = explode('|', trim($log)); | ||
| - if (count($parts) >= 5) { | ||
| - $info['last_commit'] = [ | ||
| - 'hash' => $parts[0], | ||
| - 'author' => $parts[1], | ||
| - 'email' => $parts[2], | ||
| - 'date' => $parts[3], | ||
| - 'message' => $parts[4] | ||
| - ]; | ||
| - } | ||
| - } | ||
| - | ||
| - $branches = execGitCached($repo, "branch -a"); | ||
| - $info['branches'] = empty($branches) ? 0 : count(array_filter(explode("\n", $branches))); | ||
| - | ||
| - $tags = execGitCached($repo, "tag"); | ||
| - $info['tags'] = empty($tags) ? 0 : count(array_filter(explode("\n", $tags))); | ||
| - | ||
| - return $info; | ||
| -} | ||
| - | ||
| -function getBranches($repo) { | ||
| - $output = execGitCached($repo, "branch -a --format='%(refname:short)|%(committerdate:unix)|%(subject)'"); | ||
| - $branches = []; | ||
| - if (empty($output) || $output === null) { | ||
| - return $branches; | ||
| - } | ||
| - foreach (explode("\n", trim($output)) as $line) { | ||
| - if (empty($line)) continue; | ||
| - $parts = explode('|', $line, 3); | ||
| - if (count($parts) >= 3) { | ||
| - $branches[] = [ | ||
| - 'name' => $parts[0], | ||
| - 'date' => $parts[1], | ||
| - 'message' => $parts[2] | ||
| - ]; | ||
| - } | ||
| - } | ||
| - return $branches; | ||
| -} | ||
| - | ||
| -function getCommits($repo, $branch = 'HEAD', $limit = 30) { | ||
| - $cmd = "log " . escapeshellarg($branch) . " -$limit --format='%H|%an|%ae|%at|%s'"; | ||
| - $output = execGitCached($repo, $cmd); | ||
| - $commits = []; | ||
| - if (empty($output) || $output === null) { | ||
| - return $commits; | ||
| - } | ||
| - foreach (explode("\n", trim($output)) as $line) { | ||
| - if (empty($line)) continue; | ||
| - $parts = explode('|', $line, 5); | ||
| - if (count($parts) >= 5) { | ||
| - $commits[] = [ | ||
| - 'hash' => $parts[0], | ||
| - 'author' => $parts[1], | ||
| - 'email' => $parts[2], | ||
| - 'date' => $parts[3], | ||
| - 'message' => $parts[4] | ||
| - ]; | ||
| - } | ||
| - } | ||
| - return $commits; | ||
| -} | ||
| - | ||
| -function getCommitDetails($repo, $hash) { | ||
| - $info = execGitCached($repo, "show --format='%H|%an|%ae|%at|%s|%b' --stat $hash"); | ||
| - $diff = execGitCached($repo, "show --format='' $hash"); | ||
| - $lines = explode("\n", $info); | ||
| - $header = array_shift($lines); | ||
| - $parts = explode('|', $header, 6); | ||
| - return [ | ||
| - 'hash' => $parts[0] ?? '', | ||
| - 'author' => $parts[1] ?? '', | ||
| - 'email' => $parts[2] ?? '', | ||
| - 'date' => $parts[3] ?? '', | ||
| - 'message' => $parts[4] ?? '', | ||
| - 'body' => $parts[5] ?? '', | ||
| - 'stat' => implode("\n", $lines), | ||
| - 'diff' => $diff | ||
| - ]; | ||
| -} | ||
| - | ||
| -function getTree($repo, $ref = 'HEAD', $path = '') { | ||
| - $fullPath = $path ? $ref . ':' . $path : $ref; | ||
| - $cmd = "ls-tree " . escapeshellarg($fullPath); | ||
| - $output = execGitCached($repo, $cmd); | ||
| - $items = []; | ||
| - if (empty($output) || $output === null) { | ||
| - return $items; | ||
| - } | ||
| - | ||
| - foreach (explode("\n", trim($output)) as $line) { | ||
| - if (empty($line)) continue; | ||
| - if (preg_match('/^(\d+)\s+(blob|tree)\s+([a-f0-9]+)\s+(.+)$/', $line, $matches)) { | ||
| - $item = [ | ||
| - 'mode' => $matches[1], | ||
| - 'type' => $matches[2], | ||
| - 'hash' => $matches[3], | ||
| - 'name' => $matches[4] | ||
| - ]; | ||
| - | ||
| - // Get size for blobs | ||
| - if ($item['type'] === 'blob') { | ||
| - $sizeOutput = execGitRaw($repo, "cat-file -s " . escapeshellarg($item['hash'])); | ||
| - $item['size'] = (int)trim($sizeOutput); | ||
| - } | ||
| - | ||
| - // Get last modification time for this file | ||
| - $filePath = $path ? $path . '/' . $item['name'] : $item['name']; | ||
| - $mtimeOutput = execGitRaw($repo, "log -1 --format=%ct " . escapeshellarg($ref) . " -- " . escapeshellarg($filePath)); | ||
| - if (!empty($mtimeOutput)) { | ||
| - $item['mtime'] = (int)trim($mtimeOutput); | ||
| - } | ||
| - | ||
| - $items[] = $item; | ||
| - } | ||
| - } | ||
| - | ||
| - usort($items, function($a, $b) { | ||
| - if ($a['type'] !== $b['type']) { | ||
| - return $a['type'] === 'tree' ? -1 : 1; | ||
| - } | ||
| - return strcmp($a['name'], $b['name']); | ||
| - }); | ||
| - return $items; | ||
| -} | ||
| - | ||
| -function getBlob($repo, $hash) { | ||
| - return execGitCached($repo, "cat-file blob $hash"); | ||
| -} | ||
| - | ||
| -<?php | ||
| -$name = $_GET['name'] ?? 'file'; | ||
| -$isImage = isImageFile($name); | ||
| -$isVideo = isVideoFile($name); | ||
| - | ||
| -// Check file size before loading | ||
| -$repoPath = REPOS_PATH . '/' . basename($repo); | ||
| -$isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git'); | ||
| - | ||
| -if ($isBare) { | ||
| - $sizeCmd = "git --git-dir=" . escapeshellarg($repoPath) . " cat-file -s " . escapeshellarg($hash); | ||
| -} else { | ||
| - $sizeCmd = "cd " . escapeshellarg($repoPath) . " && git cat-file -s " . escapeshellarg($hash); | ||
| -} | ||
| - | ||
| -$fileSize = intval(trim(shell_exec($sizeCmd))); | ||
| -$maxDisplaySize = 10 * 1024 * 1024; // 10MB limit for display | ||
| -$maxVideoSize = 100 * 1024 * 1024; // 100MB limit for video playback | ||
| -$tooLarge = $fileSize > $maxDisplaySize; | ||
| -$videoTooLarge = $isVideo && $fileSize > $maxVideoSize; | ||
| -?> | ||
| -<div class="breadcrumb"> | ||
| - <a href="?theme=<?php echo $current_theme; ?>">Repositories</a> <span>/</span> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&theme=<?php echo $current_theme; ?>"><?php echo htmlspecialchars($repo); ?></a> <span>/</span> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&view=tree&theme=<?php echo $current_theme; ?>">Files</a> <span>/</span> | ||
| - <strong><?php echo htmlspecialchars($name); ?></strong> | ||
| -</div> | ||
| - | ||
| -<div class="card"> | ||
| - <div class="card-header"> | ||
| - <?php echo htmlspecialchars($name); ?> | ||
| - <span style="color: #666; font-size: 0.9em; margin-left: 10px;"> | ||
| - (<?php echo number_format($fileSize / 1024, 2); ?> KB) | ||
| - </span> | ||
| - </div> | ||
| - <div class="file-actions"> | ||
| - <a href="?action=raw&repo=<?php echo urlencode($repo); ?>&hash=<?php echo urlencode($hash); ?>&name=<?php echo urlencode($name); ?>" class="btn" download> | ||
| - π₯ Download | ||
| - </a> | ||
| - </div> | ||
| - | ||
| - <?php if ($videoTooLarge): ?> | ||
| - <div class="empty-state"> | ||
| - <div class="empty-state-icon">β οΈ</div> | ||
| - <p>Video file is too large to play in browser (<?php echo number_format($fileSize / 1024 / 1024, 2); ?> MB)</p> | ||
| - <p>Please download it to view the contents.</p> | ||
| - </div> | ||
| - <?php elseif ($isVideo): ?> | ||
| - <div class="video-preview"> | ||
| - <video controls style="max-width: 100%; height: auto;"> | ||
| - <source src="data:<?php echo getVideoMimeType($name); ?>;base64,<?php echo base64_encode(getBlobBinary($repo, $hash)); ?>" type="<?php echo getVideoMimeType($name); ?>"> | ||
| - Your browser does not support the video tag. | ||
| - </video> | ||
| - </div> | ||
| - <?php elseif ($tooLarge): ?> | ||
| - <div class="empty-state"> | ||
| - <div class="empty-state-icon">β οΈ</div> | ||
| - <p>File is too large to display (<?php echo number_format($fileSize / 1024 / 1024, 2); ?> MB)</p> | ||
| - <p>Please download it to view the contents.</p> | ||
| - </div> | ||
| - <?php elseif ($isImage): ?> | ||
| - <div class="image-preview"> | ||
| - <img src="data:<?php echo getImageMimeType($name); ?>;base64,<?php echo base64_encode(getBlobBinary($repo, $hash)); ?>" alt="<?php echo htmlspecialchars($name); ?>"> | ||
| - </div> | ||
| - <?php else: | ||
| - $content = getBlob($repo, $hash); | ||
| - if (empty($content) || mb_detect_encoding($content, 'UTF-8', true) === false): | ||
| - ?> | ||
| - <div class="empty-state"> | ||
| - <div class="empty-state-icon">π¦</div> | ||
| - <p>This appears to be a binary file.</p> | ||
| - <p>Please download it to view the contents.</p> | ||
| - </div> | ||
| - <?php else: ?> | ||
| - <div class="code-block"> | ||
| - <pre><?php echo htmlspecialchars($content); ?></pre> | ||
| - </div> | ||
| - <?php endif; ?> | ||
| - <?php endif; ?> | ||
| -</div> | ||
| -<?php $commit = getCommitDetails($repo, $hash); ?> | ||
| -<div class="breadcrumb"> | ||
| - <a href="?theme=<?php echo $current_theme; ?>">Repositories</a> <span>/</span> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&theme=<?php echo $current_theme; ?>"><?php echo htmlspecialchars($repo); ?></a> <span>/</span> | ||
| - <strong>Commit <?php echo substr($hash, 0, 7); ?></strong> | ||
| -</div> | ||
| - | ||
| -<div class="card"> | ||
| - <div class="commit-detail"> | ||
| - <div class="commit-title"><?php echo htmlspecialchars($commit['message']); ?></div> | ||
| - <?php if (!empty($commit['body'])): ?> | ||
| - <p style="color: #586069; margin-bottom: 1.5rem;"><?php echo nl2br(htmlspecialchars($commit['body'])); ?></p> | ||
| - <?php endif; ?> | ||
| - | ||
| - <div class="commit-info"> | ||
| - <div class="commit-info-row"> | ||
| - <div class="commit-info-label">Author:</div> | ||
| - <div><?php echo htmlspecialchars($commit['author']); ?> <<?php echo htmlspecialchars($commit['email']); ?>></div> | ||
| - </div> | ||
| - <div class="commit-info-row"> | ||
| - <div class="commit-info-label">Date:</div> | ||
| - <div><?php echo formatDate($commit['date']); ?> (<?php echo timeAgo($commit['date']); ?>)</div> | ||
| - </div> | ||
| - <div class="commit-info-row"> | ||
| - <div class="commit-info-label">Commit:</div> | ||
| - <div style="font-family: monospace;"><?php echo htmlspecialchars($commit['hash']); ?></div> | ||
| - </div> | ||
| - </div> | ||
| - | ||
| - <?php if (!empty($commit['stat'])): ?> | ||
| - <div class="code-block" style="margin-bottom: 1.5rem;"> | ||
| - <pre><?php echo htmlspecialchars($commit['stat']); ?></pre> | ||
| - </div> | ||
| - <?php endif; ?> | ||
| - | ||
| - <?php if (!empty($commit['diff'])): ?> | ||
| - <div class="code-block"> | ||
| - <div class="diff"> | ||
| - <?php foreach (explode("\n", $commit['diff']) as $line): ?> | ||
| - <?php | ||
| - $class = ''; | ||
| - if (substr($line, 0, 1) === '+' && substr($line, 0, 3) !== '+++') { | ||
| - $class = 'diff-add'; | ||
| - } elseif (substr($line, 0, 1) === '-' && substr($line, 0, 3) !== '---') { | ||
| - $class = 'diff-del'; | ||
| - } elseif (substr($line, 0, 2) === '@@' || substr($line, 0, 4) === 'diff') { | ||
| - $class = 'diff-header'; | ||
| - } | ||
| - ?> | ||
| - <div class="diff-line <?php echo $class; ?>"><?php echo htmlspecialchars($line); ?></div> | ||
| - <?php endforeach; ?> | ||
| - </div> | ||
| - </div> | ||
| - <?php endif; ?> | ||
| - </div> | ||
| -</div> | ||
| - | ||
| -</div> | ||
| -</body> | ||
| -</html> | ||
| - | ||
| -<!DOCTYPE html> | ||
| -<html lang="en"> | ||
| -<head> | ||
| - <meta charset="UTF-8"> | ||
| - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| - <title><?php echo $page_title ?? SITE_TITLE; ?></title> | ||
| - <link rel="stylesheet" href="main.css"> | ||
| - <link rel="stylesheet" href="<?php echo $css_file; ?>"> | ||
| - <style> | ||
| - .header { | ||
| - display: flex; | ||
| - justify-content: space-between; | ||
| - align-items: center; | ||
| - padding: 10px 20px; | ||
| - } | ||
| - .theme-toggle a { | ||
| - padding: 5px 10px; | ||
| - border: 1px solid currentColor; | ||
| - border-radius: 4px; | ||
| - text-decoration: none; | ||
| - color: <?php echo ($current_theme === 'light') ? '#000000' : '#FFFFFF'; ?>; | ||
| - background-color: <?php echo ($current_theme === 'light') ? '#F0F0F0' : '#333333'; ?>; | ||
| - } | ||
| - .theme-toggle a:hover { | ||
| - opacity: 0.8; | ||
| - } | ||
| - </style> | ||
| -</head> | ||
| -<body> | ||
| - <div class="header"> | ||
| - <h1><a href="?action=list&theme=<?php echo $current_theme; ?>">π <?php echo SITE_TITLE; ?></a></h1> | ||
| - <div class="theme-toggle"> | ||
| - <?php | ||
| - $query_params = $_GET; | ||
| - unset($query_params['theme']); | ||
| - $opposite_theme = ($current_theme === 'light') ? 'dark' : 'light'; | ||
| - $toggle_link = '?' . http_build_query(array_merge($query_params, ['theme' => $opposite_theme])); | ||
| - ?> | ||
| - <a href="<?php echo htmlspecialchars($toggle_link); ?>" class="btn-theme" title="Switch to <?php echo $opposite_theme; ?> Mode"> | ||
| - <?php if ($current_theme === 'light'): ?> | ||
| - π Dark | ||
| - <?php else: ?> | ||
| - βοΈLight | ||
| - <?php endif; ?> | ||
| - </a> | ||
| - </div> | ||
| - </div> | ||
| - <div class="container"> | ||
| -<?php $repos = getRepositories(); ?> | ||
| -<?php if (empty($repos)): ?> | ||
| - <div class="card"> | ||
| - <div class="empty-state"> | ||
| - <div class="empty-state-icon">π</div> | ||
| - <p>No repositories found in <?php echo htmlspecialchars(REPOS_PATH); ?></p> | ||
| - </div> | ||
| - </div> | ||
| -<?php else: ?> | ||
| - <ul class="repo-list"> | ||
| - <?php foreach ($repos as $r): ?> | ||
| - <?php $info = getRepoInfo($r); ?> | ||
| - <li class="repo-item"> | ||
| - <div class="repo-name"> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($r); ?>&theme=<?php echo $current_theme; ?>"><?php echo htmlspecialchars($r); ?></a> | ||
| - </div> | ||
| - <div class="repo-desc"><?php echo htmlspecialchars($info['description']); ?></div> | ||
| - <div class="repo-meta"> | ||
| - <?php if (isset($info['last_commit'])): ?> | ||
| - <span>π <?php echo timeAgo($info['last_commit']['date']); ?></span> | ||
| - <?php endif; ?> | ||
| - </div> | ||
| - </li> | ||
| - <?php endforeach; ?> | ||
| - </ul> | ||
| -<?php endif; ?> | ||
| - | ||
| -<?php | ||
| -$current_theme = $current_theme ?? ($_GET['theme'] ?? 'dark'); | ||
| -$repo = $repo ?? ($_GET['repo'] ?? ''); | ||
| -$ref = $ref ?? ($_GET['ref'] ?? 'HEAD'); | ||
| -$path = $path ?? ($_GET['path'] ?? ''); | ||
| -?> | ||
| -<div class="breadcrumb"> | ||
| - <a href="?action=list&theme=<?php echo $current_theme; ?>">Repositories</a> | ||
| -</div> | ||
| - | ||
| -<?php | ||
| - $base_repo_link = "?action=repo&repo=" . urlencode($repo) . "&theme=" . $current_theme; | ||
| - $view = $_GET['view'] ?? ''; | ||
| -?> | ||
| -<div class="nav-tabs"> | ||
| - <a href="<?php echo $base_repo_link; ?>" class="nav-tab <?php echo empty($view) ? 'active' : ''; ?>">π Commits</a> | ||
| - <a href="<?php echo $base_repo_link; ?>&view=tree" class="nav-tab <?php echo $view === 'tree' ? 'active' : ''; ?>">π Files</a> | ||
| - <a href="<?php echo $base_repo_link; ?>&view=branches" class="nav-tab <?php echo $view === 'branches' ? 'active' : ''; ?>">πΏ Branches</a> | ||
| -</div> | ||
| - | ||
| -<?php | ||
| - if ($view === 'tree') { | ||
| - include __DIR__ . '/repo_tree.php'; | ||
| - } elseif ($view === 'branches') { | ||
| - include __DIR__ . '/repo_branches.php'; | ||
| - } else { | ||
| - include __DIR__ . '/repo_commits.php'; | ||
| - } | ||
| -?> | ||
| - | ||
| -<?php $branches = getBranches($repo); ?> | ||
| -<div class="card"> | ||
| - <ul class="commit-list"> | ||
| - <?php foreach ($branches as $branch): ?> | ||
| - <li class="commit-item"> | ||
| - <div class="commit-message"> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&ref=<?php echo urlencode($branch['name']); ?>&theme=<?php echo $current_theme; ?>"> | ||
| - πΏ <?php echo htmlspecialchars($branch['name']); ?> | ||
| - </a> | ||
| - <div class="commit-meta"> | ||
| - <?php echo htmlspecialchars($branch['message']); ?> β’ | ||
| - <?php echo timeAgo($branch['date']); ?> | ||
| - </div> | ||
| - </div> | ||
| - </li> | ||
| - <?php endforeach; ?> | ||
| - </ul> | ||
| -</div> | ||
| - | ||
| -<?php $commits = getCommits($repo, $ref); ?> | ||
| -<div class="card"> | ||
| - <ul class="commit-list"> | ||
| - <?php foreach ($commits as $commit): ?> | ||
| - <li class="commit-item"> | ||
| - <div class="commit-message"> | ||
| - <a href="?action=commit&repo=<?php echo urlencode($repo); ?>&hash=<?php echo urlencode($commit['hash']); ?>&theme=<?php echo $current_theme; ?>"> | ||
| - <?php echo htmlspecialchars($commit['message']); ?> | ||
| - </a> | ||
| - <div class="commit-meta"> | ||
| - <?php echo htmlspecialchars($commit['author']); ?> committed <?php echo timeAgo($commit['date']); ?> | ||
| - </div> | ||
| - </div> | ||
| - <div class="commit-hash"> | ||
| - <a href="?action=commit&repo=<?php echo urlencode($repo); ?>&hash=<?php echo urlencode($commit['hash']); ?>&theme=<?php echo $current_theme; ?>"> | ||
| - <?php echo substr($commit['hash'], 0, 7); ?> | ||
| - </a> | ||
| - </div> | ||
| - </li> | ||
| - <?php endforeach; ?> | ||
| - </ul> | ||
| -</div> | ||
| - | ||
| -<?php $items = getTree($repo, $ref, $path); ?> | ||
| -<div class="card"> | ||
| - <div class="card-header"> | ||
| - <?php if (empty($path)): ?> | ||
| - Root directory | ||
| - <?php else: ?> | ||
| - <?php echo htmlspecialchars($path); ?> | ||
| - <?php endif; ?> | ||
| - </div> | ||
| - <ul class="file-tree"> | ||
| - <?php if (!empty($path)): ?> | ||
| - <li class="file-item"> | ||
| - <span class="file-icon">π</span> | ||
| - <div class="file-name"> | ||
| - <?php | ||
| - $parentPath = dirname($path); | ||
| - if ($parentPath === '.' || $parentPath === '') { | ||
| - $parentPath = ''; | ||
| - } | ||
| - ?> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&view=tree&ref=<?php echo urlencode($ref); ?>&path=<?php echo urlencode($parentPath); ?>&theme=<?php echo $current_theme; ?>">../</a> | ||
| - </div> | ||
| - <div class="file-meta"> | ||
| - <span class="file-size">-</span> | ||
| - <span class="file-date">-</span> | ||
| - </div> | ||
| - </li> | ||
| - <?php endif; ?> | ||
| - <?php foreach ($items as $item): ?> | ||
| - <li class="file-item"> | ||
| - <span class="file-icon"><?php echo $item['type'] === 'tree' ? 'π' : 'π'; ?></span> | ||
| - <div class="file-name"> | ||
| - <?php if ($item['type'] === 'tree'): ?> | ||
| - <a href="?action=repo&repo=<?php echo urlencode($repo); ?>&view=tree&ref=<?php echo urlencode($ref); ?>&path=<?php echo urlencode($path . ($path ? '/' : '') . $item['name']); ?>&theme=<?php echo $current_theme; ?>"> | ||
| - <?php echo htmlspecialchars($item['name']); ?>/ | ||
| - </a> | ||
| - <?php else: ?> | ||
| - <a href="?action=blob&repo=<?php echo urlencode($repo); ?>&hash=<?php echo urlencode($item['hash']); ?>&name=<?php echo urlencode($item['name']); ?>&theme=<?php echo $current_theme; ?>"> | ||
| - <?php echo htmlspecialchars($item['name']); ?> | ||
| - </a> | ||
| - <?php endif; ?> | ||
| - </div> | ||
| - <div class="file-meta"> | ||
| - <span class="file-size"> | ||
| - <?php | ||
| - if ($item['type'] === 'blob' && isset($item['size'])) { | ||
| - $size = $item['size']; | ||
| - if ($size < 1024) { | ||
| - echo $size . ' B'; | ||
| - } elseif ($size < 1024 * 1024) { | ||
| - echo round($size / 1024, 1) . ' KB'; | ||
| - } else { | ||
| - echo round($size / (1024 * 1024), 1) . ' MB'; | ||
| - } | ||
| - } else { | ||
| - echo '-'; | ||
| - } | ||
| - ?> | ||
| - </span> | ||
| - <span class="file-date"> | ||
| - <?php | ||
| - if (isset($item['mtime'])) { | ||
| - echo date('Y-m-d H:i', $item['mtime']); | ||
| - } else { | ||
| - echo '-'; | ||
| - } | ||
| - ?> | ||
| - </span> | ||
| - </div> | ||
| - </li> | ||
| - <?php endforeach; ?> | ||
| - </ul> | ||
| -</div> | ||
| - | ||