Dave Jarvis' Repositories

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

Fixes repository

AuthorDave Jarvis <email>
Date2026-02-08 14:45:06 GMT-0800
Commit7f699c94d3b420b1067f05f5620f3c046858d252
Parent3eaf036
kimi-viewer.php
/**
* SimpleGit - A secure multi-repository Git viewer
- * 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);
-}
-
-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);
- }
-
- $compressed = stream_get_contents($pack);
- $deltaData = @uncompressGitData($compressed);
- $baseObj = readPackObject($packFile, $offset - $baseOffset);
- fclose($pack);
-
- return [
- 'type' => $baseObj['type'],
- 'content' => applyGitDelta($baseObj['content'], $deltaData)
- ];
- }
-
- // Standard Objects (Commit, Tree, Blob)
- $compressed = stream_get_contents($pack);
- fclose($pack);
-
- // Use a wrapper to handle the raw stream decompression
- $uncompressed = @uncompressGitData($compressed);
-
- if ($uncompressed === false) return false;
-
- $types = ['', 'commit', 'tree', 'blob', 'tag'];
- return [
- 'type' => $types[$type] ?? 'unknown',
- 'content' => $uncompressed
- ];
-}
-
-/**
- * Helper to handle decompression when trailing data exists in the buffer
- */
-function uncompressGitData($data) {
- return @zlib_decode($data);
-}
-
-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>
+ * 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>
+</body>
+</html>
+
Delta374 lines added, 649 lines removed, 275-line decrease