Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Refactors code into smaller units

AuthorDave Jarvis <email>
Date2026-01-13 22:35:54 GMT-0800
Commit80e1f75a2fe1cc0171208ee147463fade939cd94
Parent86cfc60
config.php
+<?php
+// Configuration constants
+define('REPOS_PATH', '/home/autonoma/repos');
+define('SITE_TITLE', 'Dave Jarvis' Repositories');
+define('CACHE_DIR', __DIR__ . '/cache');
+define('CACHE_EXPIRY', 3600); // 1 hour
+
+// Create cache directory if it doesn't exist
+if (!is_dir(CACHE_DIR)) {
+ mkdir(CACHE_DIR, 0777, true);
+}
+
+// Error reporting
+error_reporting(E_ALL);
+ini_set('display_errors', 1);
+
includes/git_functions.php
+<?php
+/**
+ * Git-related functions for repository operations
+ */
+
+/**
+ * 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 = REPOS_PATH . '/' . basename($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')) {
+ $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 = REPOS_PATH . '/' . basename($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);
+}
+
+/**
+ * Main Git execution function (routes to cached version).
+ */
+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 = REPOS_PATH . '/' . basename($repo);
+ $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git');
+
+ if ($isBare) {
+ $cmd = "git --git-dir=" . escapeshellarg($repoPath) . " cat-file blob " . escapeshellarg($hash);
+ } else {
+ $cmd = "cd " . escapeshellarg($repoPath) . " && git cat-file blob " . escapeshellarg($hash);
+ }
+
+ $descriptors = [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w']
+ ];
+
+ $process = proc_open($cmd, $descriptors, $pipes);
+ if (is_resource($process)) {
+ fclose($pipes[0]);
+ $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 '';
+}
+
includes/helpers.php
+<?php
+/**
+ * Helper utility functions
+ */
+
+function isImageFile($filename) {
+ $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+ return in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico']);
+}
+
+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 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';
+ }
+ }
+}
+
includes/repo_functions.php
+<?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) {
+ $output = execGitCached($repo, "log $branch -$limit --format='%H|%an|%ae|%at|%s'");
+ $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 = '') {
+ if ($ref === 'HEAD') {
+ $defaultBranch = execGitCached($repo, "symbolic-ref HEAD");
+ if (!empty($defaultBranch) && $defaultBranch !== null) {
+ $ref = trim(str_replace('refs/heads/', '', $defaultBranch));
+ } else {
+ $branches = execGitCached($repo, "branch");
+ if (!empty($branches)) {
+ $branchList = array_filter(array_map('trim', explode("\n", $branches)));
+ if (!empty($branchList)) {
+ $ref = trim(str_replace('* ', '', $branchList[0]));
+ }
+ }
+ }
+ }
+
+ $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)) {
+ $items[] = [
+ 'mode' => $matches[1],
+ 'type' => $matches[2],
+ 'hash' => $matches[3],
+ 'name' => $matches[4]
+ ];
+ }
+ }
+
+ 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");
+}
+
index.php
<?php
-error_reporting(E_ALL);
-ini_set('display_errors', 1);
-
-define('REPOS_PATH', '/home/autonoma/repos');
-define('SITE_TITLE', 'Dave Jarvis’ Repositories');
-
-// --- CACHING SETUP ---
-define('CACHE_DIR', __DIR__ . '/cache');
-define('CACHE_EXPIRY', 3600); // 1 hour in seconds (you requested at least an hour)
-
-if (!is_dir(CACHE_DIR)) {
- mkdir(CACHE_DIR, 0777, true);
-}
-// --- END CACHING SETUP ---
-
-/**
- * Executes a Git command and caches the output.
- * @param string $repo The repository name.
- * @param string $command The Git command arguments.
- * @return string The command output.
- */
-function execGitCached($repo, $command) {
- // 1. Generate a unique key for the cache file based on repo and command
- $cache_key = md5($repo . '|' . $command);
- $cache_file = CACHE_DIR . '/' . $cache_key . '.cache';
-
- // 2. Check if the cache file exists and is still valid
- if (file_exists($cache_file) && (time() - filemtime($cache_file) < CACHE_EXPIRY)) {
- return file_get_contents($cache_file);
- }
-
- // 3. If no valid cache, execute the command (using the original execGit logic)
- $repoPath = REPOS_PATH . '/' . basename($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')) {
- // Cannot execute command without a .git directory for non-bare repos
- $output = '';
- } else {
- $cmd = "cd " . escapeshellarg($repoPath) . " && git " . $command . " 2>&1";
- }
- }
-
- $output = isset($cmd) ? shell_exec($cmd) : '';
-
- // 4. Save the new output to the cache file
- if (!empty($output)) {
- file_put_contents($cache_file, $output);
- }
-
- return $output;
-}
-
-
-// The original execGit is kept (renamed to _execGitRaw) for functions that shouldn't be cached,
-// like getBlobBinary (for raw downloads) or if needed for debugging, but we'll revert to
-// the original name to be safe if a function wasn't updated. However, the 'blob' and 'commit'
-// actions use unique hashes, so caching them is less effective unless done at the 'getBlob' level.
-// Let's replace 'execGit' with 'execGitRaw' and update the functions.
-function execGitRaw($repo, $command) {
- $repoPath = REPOS_PATH . '/' . basename($repo);
- // Check if it's a bare repo or regular 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);
-}
-
-// Map the new function name to the old one in all call sites.
-function execGit($repo, $command) {
- // For simplicity, we'll route all non-binary calls to the cached version.
- // The functions using it below are the high-frequency ones we need to cache.
- return execGitCached($repo, $command);
-}
-
-
-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;
- // Check for both bare repos and regular repos
- if (is_dir($path) && (is_dir($path . '/.git') || is_file($path . '/HEAD'))) {
- $repos[] = $dir;
- }
- }
-
- // Load custom order from order.txt if it exists
- $orderFile = __DIR__ . '/order.txt';
- if (file_exists($orderFile)) {
- $order = array_filter(array_map('trim', file($orderFile)));
- // Create an array with order positions
- $orderMap = array_flip($order);
- // Sort repos: ordered first, then alphabetically for the rest
- 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) {
-
- // Both not in order.txt, sort alphabetically
- return strcmp($a, $b);
- }
-
- return $aPos - $bPos;
- });
- } else {
- // No order file, just sort alphabetically
- sort($repos);
- }
-
- return $repos;
-}
-
-function getRepoInfo($repo) {
- $info = [];
- $repoPath = REPOS_PATH . '/' . basename($repo);
-
- // Get description (This uses file system access, so no execGit needed)
- $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';
- }
-
- // Get last commit (Uses execGitCached)
- // Note: The Git command for 'log -1' is cheap and frequently changing, but caching it
- // for a short time (e.g., 5 mins) or 1 hour as requested, still helps with high traffic.
- $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]
- ];
- }
- }
-
- // Get branch count (Uses execGitCached)
- $branches = execGitCached($repo, "branch -a");
- $info['branches'] = empty($branches) ? 0 : count(array_filter(explode("\n", $branches)));
-
- // Get tag count (Uses execGitCached)
- $tags = execGitCached($repo, "tag");
- $info['tags'] = empty($tags) ? 0 : count(array_filter(explode("\n", $tags)));
-
- return $info;
-}
-
-function getBranches($repo) {
- // Uses execGitCached
- $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) {
- // Uses execGitCached (key includes branch and limit)
- $output = execGitCached($repo, "log $branch -$limit --format='%H|%an|%ae|%at|%s'");
- $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) {
- // Commit details are unique by hash, but since we call it once per commit view,
- // caching it for 1 hour is still useful if the same commit page is reloaded often.
- $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 = '') {
- // This is the most complex one to cache because of HEAD resolution.
- // The key generated by execGitCached will use the initial $ref and $path.
-
- // For bare repos, we need to resolve HEAD to an actual branch
- if ($ref === 'HEAD') {
- // Resolve symbolic-ref using a raw (uncached) or short-term cached call if necessary
- // Sticking to execGitCached here for simplicity and to match the prompt's request for
- // general process reduction, assuming the resolution of HEAD won't change often.
- $defaultBranch = execGitCached($repo, "symbolic-ref HEAD");
- if (!empty($defaultBranch) && $defaultBranch !== null) {
- $ref = trim(str_replace('refs/heads/', '', $defaultBranch));
- } else {
- // Fallback to master or main
- $branches = execGitCached($repo, "branch");
- if (!empty($branches)) {
- $branchList = array_filter(array_map('trim', explode("\n", $branches)));
- if (!empty($branchList)) {
- $ref = trim(str_replace('* ', '', $branchList[0]));
- }
- }
- }
- }
-
- $fullPath = $path ? $ref . ':' . $path : $ref;
- $cmd = "ls-tree " . escapeshellarg($fullPath);
- // Uses execGitCached (key includes resolved ref and path)
- $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)) {
- $items[] = [
- 'mode' => $matches[1],
- 'type' => $matches[2],
- 'hash' => $matches[3],
- 'name' => $matches[4]
-
- ];
- }
- }
-
- 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) {
- // Blob content is immutable by hash, so caching it is highly effective.
- return execGitCached($repo, "cat-file blob $hash");
-}
-
-function getBlobBinary($repo, $hash) {
- // The proc_open version is needed for binary data. This is a complex call,
- // so we'll implement a cache check *before* calling the raw proc_open logic.
-
- // 1. Check Cache
- $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);
- }
-
- // 2. Execute raw command if no cache found
- $repoPath = REPOS_PATH . '/' . basename($repo);
- $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git');
-
- if ($isBare) {
- $cmd = "git --git-dir=" . escapeshellarg($repoPath) . " cat-file blob " . escapeshellarg($hash);
- } else {
- $cmd = "cd " . escapeshellarg($repoPath) . " && git cat-file blob " . escapeshellarg($hash);
- }
-
- $descriptors = [
- 0 => ['pipe', 'r'],
- 1 => ['pipe', 'w'],
- 2 => ['pipe', 'w']
- ];
- $process = proc_open($cmd, $descriptors, $pipes);
- if (is_resource($process)) {
- fclose($pipes[0]);
- $output = stream_get_contents($pipes[1]);
- fclose($pipes[1]);
- fclose($pipes[2]);
- proc_close($process);
-
- // 3. Save to cache
- if (!empty($output)) {
- file_put_contents($cache_file, $output);
- }
-
- return $output;
- }
- return '';
-}
-
-function isImageFile($filename) {
- $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
- return in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico']);
-}
-
-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 formatDate($timestamp) {
- return date('M j, Y H:i', intval($timestamp));
-}
-
-function timeAgo($timestamp) {
- $diff = time() - intval($timestamp);
-
- // If the difference is less than a minute
- if ($diff < 60) {
- return 'just now';
- }
-
- // Array of time units to iterate through, from largest to smallest
- $units = [
- 31536000 => 'year', // Approx 365 days
- 2592000 => 'month', // Approx 30 days
- 604800 => 'week', // 7 days
- 86400 => 'day', // 24 hours
- 3600 => 'hour', // 60 minutes
- 60 => 'minute', // 60 seconds
- ];
+// Load configuration
+require_once __DIR__ . '/config.php';
- foreach ($units as $seconds => $unit_name) {
- if ($diff >= $seconds) {
- $value = floor($diff / $seconds);
-
- // Single line of code for pluralization
- $plural = ($value > 1) ? 's' : '';
-
- return $value . ' ' . $unit_name . $plural . ' ago';
- }
- }
-}
+// Load function libraries
+require_once __DIR__ . '/includes/git_functions.php';
+require_once __DIR__ . '/includes/repo_functions.php';
+require_once __DIR__ . '/includes/helpers.php';
-// Routing
+// Get request parameters
$action = $_GET['action'] ?? 'list';
$repo = $_GET['repo'] ?? '';
$ref = $_GET['ref'] ?? 'HEAD';
$path = $_GET['path'] ?? '';
$hash = $_GET['hash'] ?? '';
-// Theme selection logic: sets the CSS file based on the 'theme' URL parameter
+// Theme selection
$current_theme = $_GET['theme'] ?? 'dark';
$css_file = ($current_theme === 'dark') ? 'dark.css' : 'light.css';
-// Handle raw download
+// Handle raw file download
if ($action === 'raw' && !empty($repo) && !empty($hash)) {
$name = $_GET['name'] ?? 'file';
$content = getBlobBinary($repo, $hash);
-
+
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . strlen($content));
echo $content;
-exit;
+ exit;
}
-?>
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title><?php echo $action === 'list' ? SITE_TITLE : htmlspecialchars($repo) . ' - ' . SITE_TITLE; ?></title>
- <link rel="stylesheet" href="<?php echo $css_file; ?>">
- <style>
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 20px; /* Assuming some padding */
- }
- .theme-toggle a {
- padding: 5px 10px;
- border: 1px solid currentColor; /* Use a color visible on both themes */
- border-radius: 4px;
- text-decoration: none;
- /* Force contrast color: dark text on light, light text on dark */
- 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="?<?php echo http_build_query(array_merge($_GET, ['theme' => $current_theme, 'action' => 'list', 'repo' => '', 'ref' => '', 'path' => '', 'hash' => ''])); ?>">πŸ“š <?php echo SITE_TITLE; ?></a></h1>
-
- <div class="theme-toggle">
- <?php
- // Build base query, preserving the current state but replacing 'theme'
- $query_params = $_GET;
- unset($query_params['theme']);
-
- // Get the link for the *opposite* 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 if ($action === 'list'): ?>
- <?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 elseif ($action === 'repo'): ?>
- <div class="breadcrumb">
- <a href="?theme=<?php echo $current_theme; ?>">Repositories</a> <span>/</span> <strong><?php echo htmlspecialchars($repo); ?></strong>
- </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'): ?>
- <?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>
- </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>
- </li>
- <?php endforeach; ?>
- </ul>
- </div>
-
- <?php elseif ($view === 'branches'): ?>
- <?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 else: ?>
- <?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 endif; ?>
-
- <?php elseif ($action === 'commit'): ?>
- <?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']); ?> &lt;<?php echo htmlspecialchars($commit['email']); ?>&gt;</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>
-
- <?php elseif ($action === 'blob'):
- $name = $_GET['name'] ?? 'file';
- $isImage = isImageFile($name);
+// Set page title
+$page_title = $action === 'list' ? SITE_TITLE : htmlspecialchars($repo) . ' - ' . SITE_TITLE;
- // Check file size before loading
- $repoPath = REPOS_PATH . '/' . basename($repo);
- $isBare = is_file($repoPath . '/HEAD') && !is_dir($repoPath . '/.git');
+// Include header
+include __DIR__ . '/views/header.php';
- if ($isBare) {
- $sizeCmd = "git --git-dir=" . escapeshellarg($repoPath) . " cat-file -s " . escapeshellarg($hash);
- } else {
- $sizeCmd = "cd " . escapeshellarg($repoPath) . " && git cat-file -s " . escapeshellarg($hash);
- }
+// Route to appropriate view
+switch ($action) {
+ case 'list':
+ include __DIR__ . '/views/list.php';
+ break;
- $fileSize = intval(trim(shell_exec($sizeCmd)));
- $maxDisplaySize = 10 * 1024 * 1024; // 10MB limit for display
- $tooLarge = $fileSize > $maxDisplaySize;
- ?>
- <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>
+ case 'repo':
+ include __DIR__ . '/views/repo.php';
+ break;
- <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>
+ case 'commit':
+ include __DIR__ . '/views/commit.php';
+ break;
- <?php if ($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);
- // Check if content is empty OR if it is binary/not valid UTF-8
- 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 endif; ?>
- </div>
-</body>
-</html>
+ case 'blob':
+ include __DIR__ . '/views/blob.php';
+ break;
+
+ default:
+ include __DIR__ . '/views/list.php';
+ break;
+}
+
+// Include footer
+include __DIR__ . '/views/footer.php';
views/blob.php
+<?php
+$name = $_GET['name'] ?? 'file';
+$isImage = isImageFile($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
+$tooLarge = $fileSize > $maxDisplaySize;
+?>
+<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 ($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>
+
views/commit.php
+<?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']); ?> &lt;<?php echo htmlspecialchars($commit['email']); ?>&gt;</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>
+
views/footer.php
+<!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="<?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="?<?php echo http_build_query(array_merge($_GET, ['theme' => $current_theme, 'action' => 'list', 'repo' => '', 'ref' => '', 'path' => '', 'hash' => ''])); ?>">πŸ“š <?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">
+
views/header.php
+<!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="<?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="?<?php echo http_build_query(array_merge($_GET, ['theme' => $current_theme, 'action' => 'list', 'repo' => '', 'ref' => '', 'path' => '', 'hash' => ''])); ?>">πŸ“š <?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">
+
views/list.php
+<?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; ?>
+
views/repo.php
+<div class="breadcrumb">
+ <a href="?theme=<?php echo $current_theme; ?>">Repositories</a> <span>/</span> <strong><?php echo htmlspecialchars($repo); ?></strong>
+</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';
+ }
+?>
+
views/repo_branches.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>
+
views/repo_commits.php
+<?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>
+
views/repo_tree.php
+<?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>
+ </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>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+</div>
+
Delta776 lines added, 725 lines removed, 51-line increase