Dave Jarvis' Repositories

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

Reverts some changes

Author Dave Jarvis <email>
Date 2026-02-08 14:46:24 GMT-0800
Commit 10dff4175b719049c4f99a0ba6a4a05a366af289
Parent 7f699c9
kimi-viewer.php
/**
* SimpleGit - A secure multi-repository Git viewer
- * Fixed: Stream-based decompression and robust HEAD resolution
- */
-
-function getHomeDirectory() {
- if (!empty($_SERVER['HOME'])) return $_SERVER['HOME'];
- if (!empty(getenv('HOME'))) return getenv('HOME');
- if (function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
- $userInfo = posix_getpwuid(posix_getuid());
- if (!empty($userInfo['dir'])) return $userInfo['dir'];
- }
- return '';
-}
-
-define('REPOS_PATH', getHomeDirectory() . '/repos');
-define('SITE_TITLE', "Dave Jarvis' Repositories");
-
-ini_set('display_errors', 0);
-ini_set('log_errors', 1);
-ini_set('error_log', __DIR__ . '/error.log');
-
-function getRepositories() {
- $repos = [];
- if (!is_dir(REPOS_PATH)) return $repos;
- foreach (glob(REPOS_PATH . '/*.git') as $path) {
- if (is_dir($path)) {
- $name = basename($path, '.git');
- $repos[$name] = [
- 'path' => $path,
- 'name' => urldecode($name),
- 'safe_name' => $name
- ];
- }
- }
- uasort($repos, function($a, $b) { return strcasecmp($a['name'], $b['name']); });
- return $repos;
-}
-
-function getCurrentRepo() {
- $repos = getRepositories();
- $requested = $_GET['repo'] ?? '';
- $decodedRequested = urldecode($requested);
- foreach ($repos as $repo) {
- if ($repo['safe_name'] === $requested || $repo['name'] === $decodedRequested) return $repo;
- }
- return null;
-}
-
-function sanitizePath($path) {
- $path = str_replace(['..', '\\', "\0"], ['', '/', ''], $path);
- return preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '', $path);
-}
-
-function getObjectPath($repoPath, $hash) {
- if (!preg_match('/^[a-f0-9]{40}$/', $hash)) return false;
- return $repoPath . '/objects/' . substr($hash, 0, 2) . '/' . substr($hash, 2);
-}
-
-function uncompressGitData($handle) {
- $inflator = inflate_init(ZLIB_ENCODING_ANY);
- $output = '';
- while (!feof($handle)) {
- $chunk = fread($handle, 8192);
- if ($chunk === false) break;
- $output .= inflate_add($inflator, $chunk, PHP_ZLIB_FINISH_FLUSH);
- if (inflate_get_status($inflator) === ZLIB_STREAM_END) break;
- }
- return $output;
-}
-
-function readPackObject($packFile, $offset) {
- $pack = fopen($packFile, 'rb');
- if (!$pack) return false;
-
- fseek($pack, $offset);
- $byte = ord(fread($pack, 1));
- $type = ($byte >> 4) & 0x07;
- $size = $byte & 0x0f;
- $shift = 4;
-
- while ($byte & 0x80) {
- $byte = ord(fread($pack, 1));
- $size |= ($byte & 0x7f) << $shift;
- $shift += 7;
- }
-
- if ($type === 6) { // Offset Delta
- $byte = ord(fread($pack, 1));
- $baseOffset = $byte & 0x7f;
- while ($byte & 0x80) {
- $byte = ord(fread($pack, 1));
- $baseOffset = (($baseOffset + 1) << 7) | ($byte & 0x7f);
- }
- $deltaData = uncompressGitData($pack);
- $baseObj = readPackObject($packFile, $offset - $baseOffset);
- fclose($pack);
- return [
- 'type' => $baseObj['type'],
- 'content' => applyGitDelta($baseObj['content'], $deltaData)
- ];
- }
-
- $uncompressed = uncompressGitData($pack);
- fclose($pack);
-
- $types = ['', 'commit', 'tree', 'blob', 'tag'];
- return [
- 'type' => $types[$type] ?? 'unknown',
- 'content' => $uncompressed
- ];
-}
-
-function readFromPackIndex($idxFile, $hash) {
- $packFile = str_replace('.idx', '.pack', $idxFile);
- if (!file_exists($packFile)) return false;
- $idx = fopen($idxFile, 'rb');
- if (!$idx) return false;
-
- $magic = fread($idx, 4);
- if ($magic === "\377tOc") {
- $version = unpack('N', fread($idx, 4))[1];
- if ($version !== 2) { fclose($idx); return false; }
- } else { fseek($idx, 0); }
-
- fseek($idx, 256 * 4 - 4);
- $numObjects = unpack('N', fread($idx, 4))[1];
- $targetHash = hex2bin($hash);
- $left = 0; $right = $numObjects - 1; $foundOffset = -1;
-
- while ($left <= $right) {
- $mid = (int)(($left + $right) / 2);
- fseek($idx, 256 * 4 + $mid * 20);
- $midHash = fread($idx, 20);
- $cmp = strcmp($midHash, $targetHash);
- if ($cmp === 0) {
- fseek($idx, 256 * 4 + $numObjects * 20 + $mid * 4);
- $offset = unpack('N', fread($idx, 4))[1];
- if ($offset & 0x80000000) {
- fseek($idx, 256 * 4 + $numObjects * 24 + ($offset & 0x7fffffff) * 8);
- $offset = unpack('J', fread($idx, 8))[1];
- }
- $foundOffset = $offset;
- break;
- } elseif ($cmp < 0) { $left = $mid + 1; } else { $right = $mid - 1; }
- }
- fclose($idx);
- return $foundOffset >= 0 ? readPackObject($packFile, $foundOffset) : false;
-}
-
-function readPackedObject($repoPath, $hash) {
- $packDir = $repoPath . '/objects/pack';
- if (!is_dir($packDir)) return false;
- foreach (glob($packDir . '/*.idx') as $idxFile) {
- $result = readFromPackIndex($idxFile, $hash);
- if ($result !== false) return $result;
- }
- return false;
-}
-
-function applyGitDelta($base, $delta) {
- $pos = 0;
- $readVarInt = function() use (&$delta, &$pos) {
- $res = 0; $shift = 0;
- do { $b = ord($delta[$pos++]); $res |= ($b & 0x7f) << $shift; $shift += 7; } while ($b & 0x80);
- return $res;
- };
- $readVarInt(); // baseSize
- $readVarInt(); // targetSize
- $res = '';
- while ($pos < strlen($delta)) {
- $opcode = ord($delta[$pos++]);
- if ($opcode & 0x80) {
- $off = 0; $sz = 0;
- if ($opcode & 0x01) $off |= ord($delta[$pos++]);
- if ($opcode & 0x02) $off |= ord($delta[$pos++]) << 8;
- if ($opcode & 0x04) $off |= ord($delta[$pos++]) << 16;
- if ($opcode & 0x08) $off |= ord($delta[$pos++]) << 24;
- if ($opcode & 0x10) $sz |= ord($delta[$pos++]);
- if ($opcode & 0x20) $sz |= ord($delta[$pos++]) << 8;
- if ($opcode & 0x40) $sz |= ord($delta[$pos++]) << 16;
- if ($sz === 0) $sz = 0x10000;
- $res .= substr($base, $off, $sz);
- } else {
- $res .= substr($delta, $pos, $opcode);
- $pos += $opcode;
- }
- }
- return $res;
-}
-
-function readGitObject($repoPath, $hash) {
- $path = getObjectPath($repoPath, $hash);
- if ($path && file_exists($path)) {
- $content = @file_get_contents($path);
- if ($content !== false) {
- $decompressed = @gzuncompress($content);
- if ($decompressed !== false) {
- $nullPos = strpos($decompressed, "\0");
- $header = substr($decompressed, 0, $nullPos);
- $parts = explode(' ', $header, 2);
- return ['type' => $parts[0] ?? 'unknown', 'content' => substr($decompressed, $nullPos + 1)];
- }
- }
- }
- return readPackedObject($repoPath, $hash);
-}
-
-function parseTree($content) {
- $entries = []; $offset = 0; $len = strlen($content);
- while ($offset < $len) {
- $spacePos = strpos($content, ' ', $offset);
- if ($spacePos === false) break;
- $mode = substr($content, $offset, $spacePos - $offset);
- $offset = $spacePos + 1;
- $nullPos = strpos($content, "\0", $offset);
- if ($nullPos === false) break;
- $name = substr($content, $offset, $nullPos - $offset);
- $offset = $nullPos + 1;
- $hash = bin2hex(substr($content, $offset, 20));
- $offset += 20;
- $entries[] = ['mode' => $mode, 'name' => $name, 'hash' => $hash, 'type' => in_array($mode, ['040000', '40000']) ? 'tree' : 'blob'];
- }
- usort($entries, function($a, $b) { return $a['type'] === $b['type'] ? strcasecmp($a['name'], $b['name']) : ($a['type'] === 'tree' ? -1 : 1); });
- return $entries;
-}
-
-function parseCommit($content) {
- $lines = explode("\n", $content);
- $commit = ['tree' => '', 'parents' => [], 'author' => '', 'committer' => '', 'message' => ''];
- $inMsg = false; $msg = [];
- foreach ($lines as $line) {
- if ($inMsg) { $msg[] = $line; }
- elseif ($line === '') { $inMsg = true; }
- elseif (strpos($line, 'tree ') === 0) { $commit['tree'] = substr($line, 5); }
- elseif (strpos($line, 'parent ') === 0) { $commit['parents'][] = substr($line, 7); }
- elseif (strpos($line, 'author ') === 0) { $commit['author'] = substr($line, 7); }
- elseif (strpos($line, 'committer ') === 0) { $commit['committer'] = substr($line, 10); }
- }
- $commit['message'] = implode("\n", $msg);
- return $commit;
-}
-
-function getHead($repoPath) {
- $headFile = $repoPath . '/HEAD';
- if (!file_exists($headFile)) return false;
- $content = trim(file_get_contents($headFile));
- if (preg_match('/^[a-f0-9]{40}$/', $content)) return ['type' => 'detached', 'hash' => $content];
- if (strpos($content, 'ref: ') === 0) {
- $ref = substr($content, 5);
- $refFile = $repoPath . '/' . $ref;
- if (file_exists($refFile)) return ['type' => 'ref', 'hash' => trim(file_get_contents($refFile)), 'ref' => $ref];
- }
- return false;
-}
-
-function listRefs($repoPath) {
- $refs = ['branches' => [], 'tags' => []];
- foreach (['heads' => 'branches', 'tags' => 'tags'] as $dir => $key) {
- $path = $repoPath . '/refs/' . $dir;
- if (is_dir($path)) {
- foreach (glob($path . '/*') as $file) {
- if (is_file($file)) $refs[$key][basename($file)] = trim(file_get_contents($file));
- }
- }
- }
- return $refs;
-}
-
-function formatDate($line) {
- return preg_match('/(\d+)\s+([\+\-]\d{4})$/', $line, $m) ? date('Y-m-d H:i', $m[1]) : 'Unknown';
-}
-
-function getAuthor($line) {
- return preg_match('/^([^<]+)/', $line, $m) ? trim($m[1]) : $line;
-}
-
-function getMainBranch($repoPath) {
- $refs = listRefs($repoPath);
- foreach (['main', 'master', 'develop'] as $b) if (isset($refs['branches'][$b])) return ['name' => $b, 'hash' => $refs['branches'][$b]];
- if (!empty($refs['branches'])) {
- $name = array_key_first($refs['branches']);
- return ['name' => $name, 'hash' => $refs['branches'][$name]];
- }
- return null;
-}
-
-$action = $_GET['action'] ?? 'home';
-$hash = sanitizePath($_GET['hash'] ?? '');
-$currentRepo = getCurrentRepo();
-$repositories = getRepositories();
-?>
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <title><?php echo SITE_TITLE . ($currentRepo ? ' - ' . htmlspecialchars($currentRepo['name']) : ''); ?></title>
- <link rel="stylesheet" href="kimi-style.css">
- <style>
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.5; margin: 0; }
- .container { max-width: 1012px; margin: 0 auto; padding: 40px 16px; }
- a { color: #58a6ff; text-decoration: none; }
- a:hover { text-decoration: underline; }
- header { border-bottom: 1px solid #30363d; padding-bottom: 20px; margin-bottom: 20px; }
- .nav { display: flex; gap: 15px; align-items: center; margin-top: 10px; }
- .repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
- .repo-card { border: 1px solid #30363d; padding: 16px; border-radius: 6px; }
- .file-list { border: 1px solid #30363d; border-radius: 6px; }
- .file-item { display: flex; padding: 8px 16px; border-bottom: 1px solid #30363d; }
- .file-item:last-child { border-bottom: none; }
- .blob-code { background: #161b22; padding: 16px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; font-family: monospace; }
- .empty-state { padding: 40px; text-align: center; color: #8b949e; border: 1px dashed #30363d; border-radius: 6px; }
- </style>
-</head>
-<body>
- <div class="container">
- <header>
- <h1><?php echo SITE_TITLE; ?></h1>
- <nav class="nav">
- <a href="?">Home</a>
- <?php if ($currentRepo): ?>
- <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)">
- <option value="">Switch Repo...</option>
- <?php foreach ($repositories as $r): ?>
- <option value="<?php echo htmlspecialchars($r['safe_name']); ?>" <?php echo $r['safe_name'] === $currentRepo['safe_name'] ? 'selected' : ''; ?>><?php echo htmlspecialchars($r['name']); ?></option>
- <?php endforeach; ?>
- </select>
- <?php endif; ?>
- </nav>
- </header>
-
- <?php if (!$currentRepo): ?>
- <div class="repo-grid">
- <?php foreach ($repositories as $repo): ?>
- <div class="repo-card">
- <h3><a href="?repo=<?php echo urlencode($repo['safe_name']); ?>"><?php echo htmlspecialchars($repo['name']); ?></a></h3>
- </div>
- <?php endforeach; ?>
- </div>
- <?php else:
- $head = getHead($currentRepo['path']);
- $main = getMainBranch($currentRepo['path']);
- $targetHash = $hash ?: ($head['hash'] ?? ($main['hash'] ?? null));
-
- if (!$targetHash) {
- echo '<div class="empty-state">Repository is empty or HEAD is missing.</div>';
- } else {
- $obj = readGitObject($currentRepo['path'], $targetHash);
- if (!$obj) {
- echo '<div class="empty-state">Error reading object: ' . htmlspecialchars($targetHash) . '</div>';
- } elseif ($obj['type'] === 'commit') {
- $commit = parseCommit($obj['content']);
- $treeObj = readGitObject($currentRepo['path'], $commit['tree']);
- $entries = parseTree($treeObj['content']);
- echo "<h2>Files in " . htmlspecialchars($currentRepo['name']) . "</h2>";
- echo '<div class="file-list">';
- foreach ($entries as $e) {
- echo '<div class="file-item"><a href="?repo='.urlencode($currentRepo['safe_name']).'&hash='.$e['hash'].'">'.($e['type'] === 'tree' ? '📁 ' : '📄 ') . htmlspecialchars($e['name']).'</a></div>';
- }
- echo '</div>';
- } elseif ($obj['type'] === 'tree') {
- $entries = parseTree($obj['content']);
- echo '<div class="file-list">';
- foreach ($entries as $e) {
- echo '<div class="file-item"><a href="?repo='.urlencode($currentRepo['safe_name']).'&hash='.$e['hash'].'">'.($e['type'] === 'tree' ? '📁 ' : '📄 ') . htmlspecialchars($e['name']).'</a></div>';
- }
- echo '</div>';
- } elseif ($obj['type'] === 'blob') {
- echo '<div class="blob-code">' . htmlspecialchars($obj['content']) . '</div>';
- }
- }
- endif; ?>
- </div>
+ * No external dependencies, no shell execution, no uploads
+ */
+
+function getHomeDirectory() {
+ if (!empty($_SERVER['HOME'])) {
+ return $_SERVER['HOME'];
+ }
+
+ if (!empty(getenv('HOME'))) {
+ return getenv('HOME');
+ }
+
+ if (function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
+ $userInfo = posix_getpwuid(posix_getuid());
+
+ if (!empty($userInfo['dir'])) {
+ return $userInfo['dir'];
+ }
+ }
+
+ return '';
+}
+
+define('REPOS_PATH', getHomeDirectory() . '/repos');
+define('SITE_TITLE', "Dave Jarvis' Repositories");
+
+ini_set('display_errors', 0);
+ini_set('log_errors', 1);
+ini_set('error_log', __DIR__ . '/error.log');
+
+function getRepositories() {
+ $repos = [];
+ if (!is_dir(REPOS_PATH)) {
+ return $repos;
+ }
+
+ foreach (glob(REPOS_PATH . '/*.git') as $path) {
+ if (is_dir($path)) {
+ $name = basename($path, '.git');
+ $displayName = urldecode($name);
+ $repos[$name] = [
+ 'path' => $path,
+ 'name' => $displayName,
+ 'safe_name' => $name
+ ];
+ }
+ }
+
+ uasort($repos, function($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ return $repos;
+}
+
+function getCurrentRepo() {
+ $repos = getRepositories();
+ $requested = $_GET['repo'] ?? '';
+ $decodedRequested = urldecode($requested);
+
+ foreach ($repos as $key => $repo) {
+ if ($repo['safe_name'] === $requested || $repo['name'] === $decodedRequested) {
+ return $repo;
+ }
+ }
+
+ return null;
+}
+
+function sanitizePath($path) {
+ $path = str_replace(['..', '\\', "\0"], ['', '/', ''], $path);
+ return preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '', $path);
+}
+
+function getObjectPath($repoPath, $hash) {
+ if (!preg_match('/^[a-f0-9]{40}$/', $hash)) {
+ return false;
+ }
+ $dir = substr($hash, 0, 2);
+ $file = substr($hash, 2);
+ return $repoPath . '/objects/' . $dir . '/' . $file;
+}
+
+function readPackedObject($repoPath, $hash) {
+ $packDir = $repoPath . '/objects/pack';
+ if (!is_dir($packDir)) {
+ return false;
+ }
+
+ foreach (glob($packDir . '/*.idx') as $idxFile) {
+ $result = readFromPackIndex($idxFile, $hash);
+ if ($result !== false) {
+ return $result;
+ }
+ }
+
+ return false;
+}
+
+function readFromPackIndex($idxFile, $hash) {
+ $packFile = str_replace('.idx', '.pack', $idxFile);
+ if (!file_exists($packFile)) {
+ return false;
+ }
+
+ $idx = fopen($idxFile, 'rb');
+ if (!$idx) return false;
+
+ $magic = fread($idx, 4);
+ $version = 0;
+
+ if ($magic === "\377tOc") {
+ $versionData = fread($idx, 4);
+ $version = unpack('N', $versionData)[1];
+ if ($version !== 2) {
+ fclose($idx);
+ return false;
+ }
+ } else {
+ fseek($idx, 0);
+ }
+
+ fseek($idx, 256 * 4 - 4);
+ $numObjects = unpack('N', fread($idx, 4))[1];
+
+ fseek($idx, 256 * 4);
+ $targetHash = hex2bin($hash);
+ $left = 0;
+ $right = $numObjects - 1;
+ $foundOffset = -1;
+
+ while ($left <= $right) {
+ $mid = (int)(($left + $right) / 2);
+ fseek($idx, 256 * 4 + $mid * 20);
+ $midHash = fread($idx, 20);
+
+ $cmp = strcmp($midHash, $targetHash);
+ if ($cmp === 0) {
+ fseek($idx, 256 * 4 + $numObjects * 20 + $mid * 4);
+ $offset = unpack('N', fread($idx, 4))[1];
+
+ if ($offset & 0x80000000) {
+ fseek($idx, 256 * 4 + $numObjects * 24 + ($offset & 0x7fffffff) * 8);
+ $offset = unpack('J', fread($idx, 8))[1];
+ }
+
+ $foundOffset = $offset;
+ break;
+ } elseif ($cmp < 0) {
+ $left = $mid + 1;
+ } else {
+ $right = $mid - 1;
+ }
+ }
+
+ fclose($idx);
+
+ if ($foundOffset < 0) {
+ return false;
+ }
+
+ return readPackObject($packFile, $foundOffset);
+}
+
+/**
+ * FIXED: Uses stream-aware decompression to avoid trailing data errors
+ */
+function uncompressGitData($handle) {
+ $inflator = inflate_init(ZLIB_ENCODING_ANY);
+ $output = '';
+ while (!feof($handle)) {
+ $chunk = fread($handle, 8192);
+ if ($chunk === false) break;
+ $output .= inflate_add($inflator, $chunk, PHP_ZLIB_FINISH_FLUSH);
+ if (inflate_get_status($inflator) === ZLIB_STREAM_END) break;
+ }
+ return $output;
+}
+
+function readPackObject($packFile, $offset) {
+ $pack = fopen($packFile, 'rb');
+ if (!$pack) return false;
+
+ fseek($pack, $offset);
+ $byte = ord(fread($pack, 1));
+ $type = ($byte >> 4) & 0x07;
+ $size = $byte & 0x0f;
+ $shift = 4;
+
+ while ($byte & 0x80) {
+ $byte = ord(fread($pack, 1));
+ $size |= ($byte & 0x7f) << $shift;
+ $shift += 7;
+ }
+
+ // Handle Offset Deltas (Type 6)
+ if ($type === 6) {
+ $byte = ord(fread($pack, 1));
+ $baseOffset = $byte & 0x7f;
+ while ($byte & 0x80) {
+ $byte = ord(fread($pack, 1));
+ $baseOffset = (($baseOffset + 1) << 7) | ($byte & 0x7f);
+ }
+ $deltaData = uncompressGitData($pack);
+ $baseObj = readPackObject($packFile, $offset - $baseOffset);
+ fclose($pack);
+ return [
+ 'type' => $baseObj['type'],
+ 'content' => applyGitDelta($baseObj['content'], $deltaData)
+ ];
+ }
+
+ // Standard Objects (Commit, Tree, Blob)
+ $uncompressed = uncompressGitData($pack);
+ fclose($pack);
+
+ $types = ['', 'commit', 'tree', 'blob', 'tag'];
+ return [
+ 'type' => $types[$type] ?? 'unknown',
+ 'content' => $uncompressed
+ ];
+}
+
+function applyGitDelta($base, $delta) {
+ $pos = 0;
+ $readVarInt = function() use (&$delta, &$pos) {
+ $res = 0; $shift = 0;
+ do {
+ $b = ord($delta[$pos++]);
+ $res |= ($b & 0x7f) << $shift;
+ $shift += 7;
+ } while ($b & 0x80);
+ return $res;
+ };
+
+ $baseSize = $readVarInt();
+ $targetSize = $readVarInt();
+ $res = '';
+
+ while ($pos < strlen($delta)) {
+ $opcode = ord($delta[$pos++]);
+ if ($opcode & 0x80) { // Copy from base
+ $off = 0; $sz = 0;
+ if ($opcode & 0x01) $off |= ord($delta[$pos++]);
+ if ($opcode & 0x02) $off |= ord($delta[$pos++] ) << 8;
+ if ($opcode & 0x04) $off |= ord($delta[$pos++] ) << 16;
+ if ($opcode & 0x08) $off |= ord($delta[$pos++] ) << 24;
+ if ($opcode & 0x10) $sz |= ord($delta[$pos++]);
+ if ($opcode & 0x20) $sz |= ord($delta[$pos++] ) << 8;
+ if ($opcode & 0x40) $sz |= ord($delta[$pos++] ) << 16;
+ if ($sz === 0) $sz = 0x10000;
+ $res .= substr($base, $off, $sz);
+ } else { // Insert new data
+ $res .= substr($delta, $pos, $opcode);
+ $pos += $opcode;
+ }
+ }
+ return $res;
+}
+
+function readGitObject($repoPath, $hash) {
+ $path = getObjectPath($repoPath, $hash);
+ if ($path && file_exists($path)) {
+ $content = @file_get_contents($path);
+ if ($content !== false) {
+ $decompressed = @gzuncompress($content);
+ if ($decompressed !== false) {
+ $nullPos = strpos($decompressed, "\0");
+ if ($nullPos !== false) {
+ $header = substr($decompressed, 0, $nullPos);
+ $parts = explode(' ', $header, 2);
+ return [
+ 'type' => $parts[0] ?? 'unknown',
+ 'size' => $parts[1] ?? 0,
+ 'content' => substr($decompressed, $nullPos + 1)
+ ];
+ }
+ }
+ }
+ }
+
+ return readPackedObject($repoPath, $hash);
+}
+
+function parseTree($content) {
+ $entries = [];
+ $offset = 0;
+ $len = strlen($content);
+
+ while ($offset < $len) {
+ $spacePos = strpos($content, ' ', $offset);
+ if ($spacePos === false) break;
+
+ $mode = substr($content, $offset, $spacePos - $offset);
+ $offset = $spacePos + 1;
+
+ $nullPos = strpos($content, "\0", $offset);
+ if ($nullPos === false) break;
+
+ $name = substr($content, $offset, $nullPos - $offset);
+ $offset = $nullPos + 1;
+
+ if ($offset + 20 > $len) break;
+ $hash = bin2hex(substr($content, $offset, 20));
+ $offset += 20;
+
+ $isTree = in_array($mode, ['040000', '40000', '160000']);
+
+ $entries[] = [
+ 'mode' => $mode,
+ 'name' => $name,
+ 'hash' => $hash,
+ 'type' => $isTree ? 'tree' : 'blob'
+ ];
+ }
+
+ usort($entries, function($a, $b) {
+ if ($a['type'] !== $b['type']) {
+ return $a['type'] === 'tree' ? -1 : 1;
+ }
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ return $entries;
+}
+
+function parseCommit($content) {
+ $lines = explode("\n", $content);
+ $commit = [
+ 'tree' => '',
+ 'parents' => [],
+ 'author' => '',
+ 'committer' => '',
+ 'message' => ''
+ ];
+
+ $inMessage = false;
+ $messageLines = [];
+
+ foreach ($lines as $line) {
+ if ($inMessage) {
+ $messageLines[] = $line;
+ } elseif ($line === '') {
+ $inMessage = true;
+ } elseif (strpos($line, 'tree ') === 0) {
+ $commit['tree'] = substr($line, 5);
+ } elseif (strpos($line, 'parent ') === 0) {
+ $commit['parents'][] = substr($line, 7);
+ } elseif (strpos($line, 'author ') === 0) {
+ $commit['author'] = substr($line, 7);
+ } elseif (strpos($line, 'committer ') === 0) {
+ $commit['committer'] = substr($line, 10);
+ }
+ }
+
+ $commit['message'] = implode("\n", $messageLines);
+ return $commit;
+}
+
+function getHead($repoPath) {
+ $headFile = $repoPath . '/HEAD';
+ if (!file_exists($headFile)) {
+ return false;
+ }
+
+ $content = trim(file_get_contents($headFile));
+
+ if (preg_match('/^[a-f0-9]{40}$/', $content)) {
+ return ['type' => 'detached', 'hash' => $content, 'ref' => null];
+ }
+
+ if (strpos($content, 'ref: ') === 0) {
+ $ref = substr($content, 5);
+ $refFile = $repoPath . '/' . $ref;
+ if (file_exists($refFile)) {
+ $hash = trim(file_get_contents($refFile));
+ return ['type' => 'ref', 'hash' => $hash, 'ref' => $ref];
+ }
+ }
+
+ return false;
+}
+
+function listRefs($repoPath) {
+ $refs = [];
+
+ $branchesDir = $repoPath . '/refs/heads';
+ if (is_dir($branchesDir)) {
+ foreach (glob($branchesDir . '/*') as $file) {
+ if (is_file($file)) {
+ $name = basename($file);
+ $hash = trim(file_get_contents($file));
+ $refs['branches'][$name] = $hash;
+ }
+ }
+ }
+
+ $tagsDir = $repoPath . '/refs/tags';
+ if (is_dir($tagsDir)) {
+ foreach (glob($tagsDir . '/*') as $file) {
+ if (is_file($file)) {
+ $name = basename($file);
+ $content = file_get_contents($file);
+ $refs['tags'][$name] = trim($content);
+ }
+ }
+ }
+
+ return $refs;
+}
+
+function formatDate($line) {
+ if (preg_match('/(\d+)\s+([\+\-]\d{4})$/', $line, $matches)) {
+ $timestamp = $matches[1];
+ return date('Y-m-d H:i:s', $timestamp);
+ }
+ return 'Unknown';
+}
+
+function getAuthor($line) {
+ if (preg_match('/^([^<]+)/', $line, $matches)) {
+ return trim($matches[1]);
+ }
+ return $line;
+}
+
+function getLog($repoPath, $commitHash, $max = 20) {
+ $log = [];
+ $seen = [];
+ $queue = [$commitHash];
+
+ while (!empty($queue) && count($log) < $max) {
+ $hash = array_shift($queue);
+ if (isset($seen[$hash])) continue;
+ $seen[$hash] = true;
+
+ $obj = readGitObject($repoPath, $hash);
+ if (!$obj || $obj['type'] !== 'commit') continue;
+
+ $commit = parseCommit($obj['content']);
+ $commit['hash'] = $hash;
+ $log[] = $commit;
+
+ foreach ($commit['parents'] as $parent) {
+ $queue[] = $parent;
+ }
+ }
+
+ return $log;
+}
+
+function getMainBranch($repoPath) {
+ $refs = listRefs($repoPath);
+
+ $priority = ['main', 'master', 'trunk', 'develop'];
+ foreach ($priority as $branch) {
+ if (isset($refs['branches'][$branch])) {
+ return ['name' => $branch, 'hash' => $refs['branches'][$branch]];
+ }
+ }
+
+ if (!empty($refs['branches'])) {
+ $first = array_key_first($refs['branches']);
+ return ['name' => $first, 'hash' => $refs['branches'][$first]];
+ }
+
+ return null;
+}
+
+$action = $_GET['action'] ?? 'home';
+$hash = sanitizePath($_GET['hash'] ?? '');
+
+$currentRepo = getCurrentRepo();
+$repoParam = $currentRepo ? '&repo=' . urlencode($currentRepo['safe_name']) : '';
+
+$repositories = getRepositories();
+
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title><?php echo SITE_TITLE; ?><?php echo $currentRepo ? ' - ' . htmlspecialchars($currentRepo['name']) : ''; ?></title>
+ <link rel="stylesheet" href="kimi-style.css">
+</head>
+<body>
+ <div class="container">
+ <header>
+ <h1><?php echo SITE_TITLE; ?></h1>
+ <nav class="nav">
+ <a href="?">Home</a>
+ <?php if ($currentRepo): ?>
+ <a href="?repo=<?php echo urlencode($currentRepo['safe_name']); ?>">Files</a>
+ <a href="?action=commits<?php echo $repoParam; ?>">Commits</a>
+ <a href="?action=refs<?php echo $repoParam; ?>">Branches</a>
+ <?php endif; ?>
+ <?php if ($currentRepo): ?>
+ <div class="repo-selector">
+ <label>Repository:</label>
+ <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)">
+ <option value="">Select repository...</option>
+ <?php foreach ($repositories as $repo): ?>
+ <option value="<?php echo htmlspecialchars($repo['safe_name']); ?>" <?php echo $repo['safe_name'] === $currentRepo['safe_name'] ? 'selected' : ''; ?>>
+ <?php echo htmlspecialchars($repo['name']); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ <?php endif; ?>
+ </nav>
+
+ <?php if ($currentRepo): ?>
+ <div style="margin-top: 15px;">
+ <span class="current-repo">
+ Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong>
+ </span>
+ </div>
+ <?php endif; ?>
+ </header>
+
+ <?php
+ if (!$currentRepo) {
+ echo '<h2>Repositories</h2>';
+
+ if (empty($repositories)) {
+ echo '<div class="empty-state">No repositories found in ' . htmlspecialchars(REPOS_PATH) . '</div>';
+ } else {
+ echo '<div class="repo-grid">';
+ foreach ($repositories as $repo) {
+ $mainBranch = getMainBranch($repo['path']);
+ $head = getHead($repo['path']);
+
+ echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">';
+ echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>';
+
+ if ($mainBranch) {
+ echo '<p>Branch: ' . htmlspecialchars($mainBranch['name']) . '</p>';
+ }
+
+ $refs = listRefs($repo['path']);
+ $branchCount = count($refs['branches'] ?? []);
+ $tagCount = count($refs['tags'] ?? []);
+
+ echo '<p>' . $branchCount . ' branches, ' . $tagCount . ' tags</p>';
+
+ if ($head) {
+ $obj = readGitObject($repo['path'], $head['hash']);
+ if ($obj && $obj['type'] === 'commit') {
+ $commit = parseCommit($obj['content']);
+ echo '<p style="margin-top: 8px; color: #58a6ff;">' . substr($head['hash'], 0, 7) . ' - ' . htmlspecialchars(substr(trim(explode("\n", $commit['message'])[0]), 0, 50)) . '</p>';
+ }
+ }
+ echo '</a>';
+ }
+ echo '</div>';
+ }
+ } else {
+ $mainBranch = getMainBranch($currentRepo['path']);
+
+ if (!$mainBranch) {
+ echo '<div class="empty-state">';
+ echo '<h3>No branches found</h3>';
+ echo '<p>This repository appears to be empty.</p>';
+ echo '</div>';
+ } else {
+ $obj = readGitObject($currentRepo['path'], $mainBranch['hash']);
+
+ if (!$obj || $obj['type'] !== 'commit') {
+ echo '<div class="empty-state">Error reading commit</div>';
+ } else {
+ $commit = parseCommit($obj['content']);
+ $treeHash = $commit['tree'];
+
+ $viewHash = $hash ?: $treeHash;
+ $viewObj = readGitObject($currentRepo['path'], $viewHash);
+
+ if ($viewObj && $viewObj['type'] === 'blob') {
+ echo '<div class="breadcrumb">';
+ echo '<a href="?">Repositories</a>';
+ echo '<span>/</span>';
+ echo '<a href="?repo=' . urlencode($currentRepo['safe_name']) . '">' . htmlspecialchars($currentRepo['name']) . '</a>';
+ echo '<span>/</span>';
+ echo '<span>File</span>';
+ echo '</div>';
+
+ echo '<h2>' . substr($viewHash, 0, 7) . '</h2>';
+
+ echo '<div class="blob-content">';
+ echo '<div class="blob-header">' . strlen($viewObj['content']) . ' bytes</div>';
+ echo '<div class="blob-code">' . htmlspecialchars($viewObj['content']) . '</div>';
+ echo '</div>';
+ } else {
+ $treeObj = readGitObject($currentRepo['path'], $viewHash);
+
+ if (!$treeObj || $treeObj['type'] !== 'tree') {
+ echo '<div class="empty-state">Directory not found</div>';
+ } else {
+ $entries = parseTree($treeObj['content']);
+
+ echo '<div class="breadcrumb">';
+ echo '<a href="?">Repositories</a>';
+ echo '<span>/</span>';
+ echo '<a href="?repo=' . urlencode($currentRepo['safe_name']) . '">' . htmlspecialchars($currentRepo['name']) . '</a>';
+ if ($viewHash !== $treeHash) {
+ echo '<span>/</span>';
+ echo '<span>Tree ' . substr($viewHash, 0, 7) . '</span>';
+ }
+ echo '</div>';
+
+ echo '<h2>' . htmlspecialchars($currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($mainBranch['name']) . '</span></h2>';
+
+ echo '<div class="commit-item" style="margin-bottom: 20px;">';
+ echo '<div><a href="?action=commit&hash=' . $mainBranch['hash'] . $repoParam . '" class="commit-hash">' . substr($mainBranch['hash'], 0, 7) . '</a></div>';
+ echo '<div class="commit-message">' . htmlspecialchars(trim($commit['message'])) . '</div>';
+ echo '<div class="commit-meta">';
+ echo '<span class="commit-author">' . htmlspecialchars(getAuthor($commit['author'])) . '</span>';
+ echo ' committed on ';
+ echo '<span class="commit-date">' . formatDate($commit['committer']) . '</span>';
+ echo '</div>';
+ echo '</div>';
+
+ echo '<h3>Files</h3>';
+
+ if (empty($entries)) {
+ echo '<div class="empty-state">Empty directory</div>';
+ } else {
+ echo '<div class="file-list">';
+ foreach ($entries as $entry) {
+ $icon = $entry['type'] === 'tree' ? '[dir]' : '[file]';
+ // CRITICAL FIX: Always include hash so directories work
+ $url = '?repo=' . urlencode($currentRepo['safe_name']) . '&hash=' . $entry['hash'];
+
+ echo '<a href="' . $url . '" class="file-item">';
+ echo '<span class="file-mode">' . $entry['mode'] . '</span>';
+ echo '<span class="file-name"><span class="' . ($entry['type'] === 'tree' ? 'dir-icon' : 'file-icon') . '">' . $icon . '</span> ' . htmlspecialchars($entry['name']) . '</span>';
+ echo '</a>';
+ }
+ echo '</div>';
+ }
+ }
+ }
+ }
+ }
+ }
+ ?>
+</div>
</body>
</html>
Delta 647 lines added, 371 lines removed, 276-line increase