Dave Jarvis' Repositories

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

Renames viewer

AuthorDave Jarvis <email>
Date2026-02-08 15:21:11 GMT-0800
Commitfba840356d6d2cf5ead8fe9474ebc9f259b5efd5
Parentce9ae03
kimi-style.css
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- background: #0d1117;
- color: #c9d1d9;
- line-height: 1.6;
-}
-
-.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
-}
-
-header {
- border-bottom: 1px solid #30363d;
- padding-bottom: 20px;
- margin-bottom: 30px;
-}
-
-h1 {
- color: #f0f6fc;
- font-size: 1.8rem;
- margin-bottom: 10px;
-}
-
-h2 {
- color: #f0f6fc;
- font-size: 1.4rem;
- margin: 20px 0 15px;
- padding-bottom: 10px;
- border-bottom: 1px solid #21262d;
-}
-
-h3 {
- color: #f0f6fc;
- font-size: 1.1rem;
- margin: 15px 0 10px;
-}
-
-.nav {
- margin-top: 10px;
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.nav a {
- color: #58a6ff;
- text-decoration: none;
-}
-
-.nav a:hover {
- text-decoration: underline;
-}
-
-.repo-selector {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.repo-selector label {
- color: #8b949e;
- font-size: 0.875rem;
-}
-
-.repo-selector select {
- background: #21262d;
- color: #f0f6fc;
- border: 1px solid #30363d;
- padding: 6px 12px;
- border-radius: 6px;
- font-size: 0.875rem;
- cursor: pointer;
-}
-
-.repo-selector select:hover {
- border-color: #58a6ff;
-}
-
-.commit-list {
- list-style: none;
-}
-
-.commit-item {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- padding: 16px;
- margin-bottom: 12px;
- transition: border-color 0.2s;
-}
-
-.commit-item:hover {
- border-color: #58a6ff;
-}
-
-.commit-hash {
- font-family: 'SFMono-Regular', Consolas, monospace;
- font-size: 0.85rem;
- color: #58a6ff;
- text-decoration: none;
-}
-
-.commit-hash:hover {
- text-decoration: underline;
-}
-
-.commit-meta {
- font-size: 0.875rem;
- color: #8b949e;
- margin-top: 8px;
-}
-
-.commit-author {
- color: #f0f6fc;
- font-weight: 500;
-}
-
-.commit-date {
- color: #8b949e;
-}
-
-.commit-message {
- margin-top: 8px;
- color: #c9d1d9;
- white-space: pre-wrap;
-}
-
-.file-list {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- overflow: hidden;
-}
-
-.file-item {
- display: flex;
- align-items: center;
- padding: 12px 16px;
- border-bottom: 1px solid #21262d;
- text-decoration: none;
- color: #c9d1d9;
- transition: background 0.2s;
-}
-
-.file-item:last-child {
- border-bottom: none;
-}
-
-.file-item:hover {
- background: #1f242c;
-}
-
-.file-mode {
- font-family: monospace;
- color: #8b949e;
- width: 80px;
- font-size: 0.875rem;
-}
-
-.file-name {
- flex: 1;
- color: #58a6ff;
-}
-
-.file-item:hover .file-name {
- text-decoration: underline;
-}
-
-.breadcrumb {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- padding: 12px 16px;
- margin-bottom: 20px;
-}
-
-.breadcrumb a {
- color: #58a6ff;
- text-decoration: none;
-}
-
-.breadcrumb a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb span {
- color: #8b949e;
- margin: 0 8px;
-}
-
-.blob-content {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- overflow: hidden;
-}
-
-.blob-header {
- background: #21262d;
- padding: 12px 16px;
- border-bottom: 1px solid #30363d;
- font-size: 0.875rem;
- color: #8b949e;
-}
-
-.blob-code {
- padding: 16px;
- overflow-x: auto;
- font-family: 'SFMono-Regular', Consolas, monospace;
- font-size: 0.875rem;
- line-height: 1.6;
- white-space: pre;
-}
-
-.refs-list {
- display: grid;
- gap: 10px;
-}
-
-.ref-item {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- padding: 12px 16px;
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.ref-type {
- background: #238636;
- color: white;
- padding: 2px 8px;
- border-radius: 12px;
- font-size: 0.75rem;
- font-weight: 600;
- text-transform: uppercase;
-}
-
-.ref-type.tag {
- background: #8957e5;
-}
-
-.ref-name {
- font-weight: 600;
- color: #f0f6fc;
-}
-
-.empty-state {
- text-align: center;
- padding: 60px 20px;
- color: #8b949e;
-}
-
-.commit-details {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 6px;
- padding: 20px;
- margin-bottom: 20px;
-}
-
-.commit-header {
- margin-bottom: 20px;
-}
-
-.commit-title {
- font-size: 1.25rem;
- color: #f0f6fc;
- margin-bottom: 10px;
-}
-
-.commit-info {
- display: grid;
- gap: 8px;
- font-size: 0.875rem;
-}
-
-.commit-info-row {
- display: flex;
- gap: 10px;
-}
-
-.commit-info-label {
- color: #8b949e;
- width: 80px;
- flex-shrink: 0;
-}
-
-.commit-info-value {
- color: #c9d1d9;
- font-family: monospace;
-}
-
-.parent-link {
- color: #58a6ff;
- text-decoration: none;
-}
-
-.parent-link:hover {
- text-decoration: underline;
-}
-
-.repo-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 16px;
- margin-top: 20px;
-}
-
-.repo-card {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 8px;
- padding: 20px;
- text-decoration: none;
- color: inherit;
- transition: border-color 0.2s, transform 0.1s;
-}
-
-.repo-card:hover {
- border-color: #58a6ff;
- transform: translateY(-2px);
-}
-
-.repo-card h3 {
- color: #58a6ff;
- margin-bottom: 8px;
- font-size: 1.1rem;
-}
-
-.repo-card p {
- color: #8b949e;
- font-size: 0.875rem;
- margin: 0;
-}
-
-.current-repo {
- background: #21262d;
- border: 1px solid #58a6ff;
- padding: 8px 16px;
- border-radius: 6px;
- font-size: 0.875rem;
- color: #f0f6fc;
-}
-
-.current-repo strong {
- color: #58a6ff;
-}
-
-.dir-icon, .file-icon {
- display: inline-block;
- width: 20px;
- text-align: center;
- margin-right: 8px;
- color: #8b949e;
-}
-
-.branch-badge {
- background: #238636;
- color: white;
- padding: 2px 8px;
- border-radius: 12px;
- font-size: 0.75rem;
- font-weight: 600;
- margin-left: 10px;
-}
-
kimi-viewer.php
-<?php
-/**
- * SimpleGit - A secure multi-repository Git viewer
- * No external dependencies, no shell execution, no uploads
- */
-
-require_once 'Git.php';
-
-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');
-
-// Global Git instance
-$git = new Git(REPOS_PATH);
-
-function getRepositories() {
- global $git;
- $repos = [];
- $repoList = $git->listRepositories();
-
- foreach ($repoList as $name) {
- $path = REPOS_PATH . DIRECTORY_SEPARATOR . $name . '.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);
-}
-
-// These functions now bridge to the Git class
-function readGitObject($repoPath, $hash) {
- global $git;
- $repoName = basename($repoPath, '.git');
-
- // Use the class to get the object content
- // Note: The Git class returns raw uncompressed content
- $content = $git->getObject($repoPath . DIRECTORY_SEPARATOR . '.git', $hash);
-
- if (!$content) return false;
-
- // Helper to identify type for the existing UI logic
- if (str_starts_with($content, 'commit ')) return ['type' => 'commit', 'content' => substr($content, strpos($content, "\0") + 1)];
- if (str_starts_with($content, 'tree ')) return ['type' => 'tree', 'content' => substr($content, strpos($content, "\0") + 1)];
-
- return ['type' => 'blob', 'content' => $content];
-}
-
-function parseTree($content) {
- // Retaining your original tree parser logic as it matches Git's binary format
- $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 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>
-
new/index.php
+<?php
+require_once 'Git.php';
+
+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);
+}
+
+/**
+ * Heuristic to detect if content is a Git Tree or Blob
+ * The Git class strips headers, so we check for the binary tree entry pattern:
+ * <mode> <name>\0<20-byte-sha>
+ */
+function detectType($data) {
+ if (strlen($data) < 25) return 'blob'; // Too short to be a tree entry
+
+ // Check for standard file modes at start
+ // 40000 (dir), 100644 (file), 100755 (exec), 120000 (symlink)
+ // Git binary trees start immediately with the mode of the first entry
+ if (preg_match('/^(40000|100644|100755|120000) /', $data)) {
+ // Double check: find null byte and ensure 20 bytes follow
+ $null = strpos($data, "\0");
+ if ($null !== false && ($null + 21 <= strlen($data))) {
+ return 'tree';
+ }
+ }
+ return 'blob';
+}
+
+function parseTreeContent($data) {
+ $entries = [];
+ $pos = 0;
+ $len = strlen($data);
+
+ while ($pos < $len) {
+ $space = strpos($data, ' ', $pos);
+ $null = strpos($data, "\0", $space);
+ if ($space === false || $null === false) break;
+
+ $mode = substr($data, $pos, $space - $pos);
+ $name = substr($data, $space + 1, $null - $space - 1);
+ $entrySha = bin2hex(substr($data, $null + 1, 20));
+
+ $entries[] = [
+ 'mode' => $mode,
+ 'name' => $name,
+ 'hash' => $entrySha,
+ 'type' => ($mode === '40000' || $mode === '040000') ? 'tree' : 'blob'
+ ];
+
+ $pos = $null + 21;
+ }
+
+ 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 getMainBranch($git) {
+ $branches = [];
+ $git->eachBranch(function($name, $sha) use (&$branches) {
+ $branches[$name] = $sha;
+ });
+
+ $priority = ['main', 'master', 'trunk', 'develop'];
+ foreach ($priority as $branch) {
+ if (isset($branches[$branch])) {
+ return ['name' => $branch, 'hash' => $branches[$branch]];
+ }
+ }
+
+ if (!empty($branches)) {
+ $first = array_key_first($branches);
+ return ['name' => $first, 'hash' => $branches[$first]];
+ }
+
+ return null;
+}
+
+$action = $_GET['action'] ?? 'home';
+$hash = sanitizePath($_GET['hash'] ?? '');
+
+$currentRepo = getCurrentRepo();
+$repoParam = $currentRepo ? '&repo=' . urlencode($currentRepo['safe_name']) : '';
+
+$repositories = getRepositories();
+$git = $currentRepo ? new Git($currentRepo['path']) : null;
+
+?>
+<!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) {
+ $repoGit = new Git($repo['path']);
+ $mainBranch = getMainBranch($repoGit);
+
+ // Count refs
+ $branchCount = 0;
+ $repoGit->eachBranch(function() use (&$branchCount) { $branchCount++; });
+
+ $tagCount = 0;
+ $repoGit->eachTag(function() use (&$tagCount) { $tagCount++; });
+
+ 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>';
+ }
+
+ echo '<p>' . $branchCount . ' branches, ' . $tagCount . ' tags</p>';
+
+ // Get last commit on HEAD
+ $repoGit->history('HEAD', 1, function($commit) {
+ echo '<p style="margin-top: 8px; color: #58a6ff;">' . substr($commit->sha, 0, 7) . ' - ' . htmlspecialchars(substr(explode("\n", $commit->message)[0], 0, 50)) . '</p>';
+ });
+
+ echo '</a>';
+ }
+ echo '</div>';
+ }
+ } else {
+ // REPO VIEW LOGIC
+ $mainBranch = getMainBranch($git);
+
+ 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 {
+ // Get the commit for the header info
+ $commitInfo = null;
+ // If hash is a commit or we are at HEAD, we can get commit info.
+ // If hash is a deep blob/tree, this history call might fail or return that specific object context if it was a commit,
+ // but usually we want the HEAD or the branch tip commit for the header unless we are specifically browsing a past commit.
+ // For simplicity, we show the main branch tip or HEAD commit info above the file list.
+ $git->history($hash ?: $mainBranch['hash'], 1, function($c) use (&$commitInfo) {
+ $commitInfo = $c;
+ });
+
+ // Fallback to main branch if specific hash history failed (e.g. if hash is a blob)
+ if (!$commitInfo) {
+ $git->history($mainBranch['hash'], 1, function($c) use (&$commitInfo) {
+ $commitInfo = $c;
+ });
+ }
+
+ // Determine content to show
+ $targetHash = $hash ?: $mainBranch['hash'];
+ $viewType = 'tree';
+ $blobContent = '';
+ $treeEntries = [];
+
+ if (!$hash) {
+ // Root view: Use walk() which handles Commit -> Tree resolution automatically
+ $git->walk($targetHash, function($entry) use (&$treeEntries) {
+ $treeEntries[] = (array)$entry;
+ });
+
+ // Sort walk results
+ usort($treeEntries, function($a, $b) {
+ if ($a['isDir'] !== $b['isDir']) return $a['isDir'] ? -1 : 1;
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ // Map walk 'isDir' to type 'tree' for template compatibility
+ foreach ($treeEntries as &$e) { $e['type'] = $e['isDir'] ? 'tree' : 'blob'; }
+
+ } else {
+ // Specific Hash View: Could be Tree (Subdir) or Blob (File)
+ // Git::walk only works on Commits. For direct SHAs we must stream and parse.
+ $content = null;
+ $git->stream($targetHash, function($data) use (&$content) {
+ $content = $data;
+ });
+
+ if ($content !== null) {
+ if (detectType($content) === 'tree') {
+ $viewType = 'tree';
+ $treeEntries = parseTreeContent($content);
+ } else {
+ $viewType = 'blob';
+ $blobContent = $content;
+ }
+ } else {
+ echo '<div class="empty-state">Object not found</div>';
+ $viewType = 'error';
+ }
+ }
+
+ if ($viewType === '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($targetHash, 0, 7) . '</h2>';
+
+ echo '<div class="blob-content">';
+ echo '<div class="blob-header">' . strlen($blobContent) . ' bytes</div>';
+ echo '<div class="blob-code">' . htmlspecialchars($blobContent) . '</div>';
+ echo '</div>';
+ } elseif ($viewType === 'tree') {
+ 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 ($hash) {
+ echo '<span>/</span>';
+ echo '<span>Tree ' . substr($targetHash, 0, 7) . '</span>';
+ }
+ echo '</div>';
+
+ echo '<h2>' . htmlspecialchars($currentRepo['name']) . ' <span class="branch-badge">' . htmlspecialchars($mainBranch['name']) . '</span></h2>';
+
+ if ($commitInfo) {
+ echo '<div class="commit-item" style="margin-bottom: 20px;">';
+ echo '<div><a href="?action=commit&hash=' . $commitInfo->sha . $repoParam . '" class="commit-hash">' . substr($commitInfo->sha, 0, 7) . '</a></div>';
+ echo '<div class="commit-message">' . htmlspecialchars(trim(explode("\n", $commitInfo->message)[0])) . '</div>';
+ echo '<div class="commit-meta">';
+ echo '<span class="commit-author">' . htmlspecialchars($commitInfo->author) . '</span>';
+ echo ' committed on ';
+ echo '<span class="commit-date">' . date('Y-m-d H:i:s', $commitInfo->date) . '</span>';
+ echo '</div>';
+ echo '</div>';
+ }
+
+ echo '<h3>Files</h3>';
+
+ if (empty($treeEntries)) {
+ echo '<div class="empty-state">Empty directory</div>';
+ } else {
+ echo '<div class="file-list">';
+ // If not at root, add parent link logic could go here, but strictly sticking to style
+
+ foreach ($treeEntries as $entry) {
+ $icon = $entry['type'] === 'tree' ? '[dir]' : '[file]';
+ $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>
+
new/kimi-viewer.php
-<?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);
-}
-
-/**
- * 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>
-
Delta373 lines added, 1470 lines removed, 1097-line decrease