Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
kimi-viewer.php
<?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");
-
-// Get list of available repositories
-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;
-}
-
-// Get current repository
-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 $repos ? array_values($repos)[0] : 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 readGitObject($repoPath, $hash)
-{
- $path = getObjectPath($repoPath, $hash);
- if (!$path || !file_exists($path)) {
- return false;
- }
-
- $content = file_get_contents($path);
- if ($content === false) {
- return false;
- }
-
- $decompressed = @gzuncompress($content);
- if ($decompressed === false) {
- return false;
- }
-
- $nullPos = strpos($decompressed, "\0");
- if ($nullPos === false) {
- return false;
- }
-
- $header = substr($decompressed, 0, $nullPos);
- $parts = explode(" ", $header);
-
- return [
- "type" => $parts[0] ?? "unknown",
- "size" => $parts[1] ?? 0,
- "content" => substr($decompressed, $nullPos + 1),
- ];
-}
-
-function parseTree($content)
-{
- $entries = [];
- $offset = 0;
-
- while ($offset < strlen($content)) {
- $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 > strlen($content)) {
- break;
- }
- $hash = bin2hex(substr($content, $offset, 20));
- $offset += 20;
-
- $entries[] = [
- "mode" => $mode,
- "name" => $name,
- "hash" => $hash,
- "type" => $mode === "040000" || $mode === "40000" ? "tree" : "blob",
- ];
- }
-
- 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 $content;
- }
-
- if (strpos($content, "ref: ") === 0) {
- $ref = substr($content, 5);
- $refFile = $repoPath . "/" . $ref;
- if (file_exists($refFile)) {
- return trim(file_get_contents($refFile));
- }
- }
-
- 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;
-}
-
-$action = $_GET["action"] ?? "home";
-$hash = sanitizePath($_GET["hash"] ?? "");
-
-$currentRepo = getCurrentRepo();
-$repoParam = $currentRepo
- ? "&repo=" . urlencode($currentRepo["safe_name"])
- : "";
-
-if (!$currentRepo) {
- die("Error: No repositories found in " . htmlspecialchars(REPOS_PATH));
-}
-
-if (!is_dir($currentRepo["path"])) {
- die(
- "Error: Repository not found at " . htmlspecialchars($currentRepo["path"])
- );
-}
-?>
-<!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 htmlspecialchars(
- $currentRepo["name"],
- ); ?></title>
- <style>
- * {
- 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;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <header>
- <h1><?php echo SITE_TITLE; ?></h1>
- <nav class="nav">
- <a href="?action=home<?php echo $repoParam; ?>">Home</a>
- <a href="?action=repos">All Repositories</a>
- <a href="?action=refs<?php echo $repoParam; ?>">Branches & Tags</a>
- <a href="?action=log<?php echo $repoParam; ?>">Commit Log</a>
-
- <?php if ($currentRepo): ?>
- <div class="repo-selector">
- <label>Repository:</label>
- <select onchange="window.location.href='?action=home&repo=' + encodeURIComponent(this.value)">
- <?php foreach (getRepositories() 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 && $action !== "repos"): ?>
- <div style="margin-top: 15px;">
- <span class="current-repo">
- Currently viewing: <strong><?php echo htmlspecialchars(
- $currentRepo["name"],
- ); ?></strong>
- </span>
- </div>
- <?php endif; ?>
- </header>
-
- <?php switch ($action) {
- case "repos":
- echo "<h2>All Repositories</h2>";
- $repos = getRepositories();
-
- if (empty($repos)) {
- echo '<div class="empty-state">No repositories found in ' .
- htmlspecialchars(REPOS_PATH) .
- "</div>";
- } else {
- echo '<div class="repo-grid">';
- foreach ($repos as $repo) {
- echo '<a href="?action=home&repo=' .
- urlencode($repo["safe_name"]) .
- '" class="repo-card">';
- echo "<h3>" . htmlspecialchars($repo["name"]) . "</h3>";
-
- $head = getHead($repo["path"]);
- $refs = listRefs($repo["path"]);
- $branchCount = count($refs["branches"] ?? []);
- $tagCount = count($refs["tags"] ?? []);
-
- echo "<p>" .
- $branchCount .
- " branches, " .
- $tagCount .
- " tags</p>";
- if ($head) {
- echo '<p style="margin-top: 8px; font-family: monospace;">HEAD: ' .
- substr($head, 0, 7) .
- "</p>";
- }
- echo "</a>";
- }
- echo "</div>";
- }
- break;
-
- case "refs":
- echo "<h2>Branches & Tags - " .
- htmlspecialchars($currentRepo["name"]) .
- "</h2>";
- $refs = listRefs($currentRepo["path"]);
-
- if (empty($refs["branches"]) && empty($refs["tags"])) {
- echo '<div class="empty-state">No refs found</div>';
- } else {
- echo '<div class="refs-list">';
-
- if (!empty($refs["branches"])) {
- foreach ($refs["branches"] as $name => $hash) {
- echo '<div class="ref-item">';
- echo '<span class="ref-type">Branch</span>';
- echo '<a href="?action=commit&hash=' .
- $hash .
- $repoParam .
- '" class="ref-name">' .
- htmlspecialchars($name) .
- "</a>";
- echo '<code style="color: #8b949e; font-size: 0.875rem;">' .
- substr($hash, 0, 7) .
- "</code>";
- echo "</div>";
- }
- }
-
- if (!empty($refs["tags"])) {
- foreach ($refs["tags"] as $name => $hash) {
- echo '<div class="ref-item">';
- echo '<span class="ref-type tag">Tag</span>';
- echo '<a href="?action=commit&hash=' .
- $hash .
- $repoParam .
- '" class="ref-name">' .
- htmlspecialchars($name) .
- "</a>";
- echo '<code style="color: #8b949e; font-size: 0.875rem;">' .
- substr($hash, 0, 7) .
- "</code>";
- echo "</div>";
- }
- }
-
- echo "</div>";
- }
- break;
-
- case "log":
- $head = getHead($currentRepo["path"]);
- if (!$head) {
- echo '<div class="empty-state">No commits found</div>';
- break;
- }
-
- echo "<h2>Commit History - " .
- htmlspecialchars($currentRepo["name"]) .
- "</h2>";
- $log = getLog($currentRepo["path"], $head);
-
- if (empty($log)) {
- echo '<div class="empty-state">No commits found</div>';
- } else {
- echo '<ul class="commit-list">';
- foreach ($log as $commit) {
- echo '<li class="commit-item">';
- echo "<div>";
- echo '<a href="?action=commit&hash=' .
- $commit["hash"] .
- $repoParam .
- '" class="commit-hash">' .
- substr($commit["hash"], 0, 7) .
- "</a>";
- echo "</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 "</li>";
- }
- echo "</ul>";
- }
- break;
-
- case "commit":
- if (!$hash || strlen($hash) !== 40) {
- echo '<div class="empty-state">Invalid commit hash</div>';
- break;
- }
-
- $obj = readGitObject($currentRepo["path"], $hash);
- if (!$obj || $obj["type"] !== "commit") {
- echo '<div class="empty-state">Commit not found</div>';
- break;
- }
-
- $commit = parseCommit($obj["content"]);
- $commit["hash"] = $hash;
-
- echo '<div class="commit-details">';
- echo '<div class="commit-header">';
- echo '<div class="commit-title">' .
- htmlspecialchars(trim(explode("\n", $commit["message"])[0])) .
- "</div>";
- echo "</div>";
-
- echo '<div class="commit-info">';
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Commit</span>';
- echo '<span class="commit-info-value">' . $hash . "</span>";
- echo "</div>";
-
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Tree</span>';
- echo '<span class="commit-info-value"><a href="?action=tree&hash=' .
- $commit["tree"] .
- $repoParam .
- '" class="parent-link">' .
- $commit["tree"] .
- "</a></span>";
- echo "</div>";
-
- if (!empty($commit["parents"])) {
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Parent' .
- (count($commit["parents"]) > 1 ? "s" : "") .
- "</span>";
- echo '<span class="commit-info-value">';
- foreach ($commit["parents"] as $i => $parent) {
- if ($i > 0) {
- echo ", ";
- }
- echo '<a href="?action=commit&hash=' .
- $parent .
- $repoParam .
- '" class="parent-link">' .
- $parent .
- "</a>";
- }
- echo "</span>";
- echo "</div>";
- }
-
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Author</span>';
- echo '<span class="commit-info-value">' .
- htmlspecialchars($commit["author"]) .
- "</span>";
- echo "</div>";
-
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Committer</span>';
- echo '<span class="commit-info-value">' .
- htmlspecialchars($commit["committer"]) .
- "</span>";
- echo "</div>";
- echo "</div>";
-
- echo "</div>";
-
- echo "<h3>Files at this commit</h3>";
- echo '<div class="file-list">';
- echo '<a href="?action=tree&hash=' .
- $commit["tree"] .
- $repoParam .
- '" class="file-item">';
- echo '<span class="file-mode">040000</span>';
- echo '<span class="file-name"><span class="dir-icon">[dir]</span> View Root Tree</span>';
- echo "</a>";
- echo "</div>";
- break;
-
- case "tree":
- if (!$hash || strlen($hash) !== 40) {
- echo '<div class="empty-state">Invalid tree hash</div>';
- break;
- }
-
- $obj = readGitObject($currentRepo["path"], $hash);
- if (!$obj || $obj["type"] !== "tree") {
- echo '<div class="empty-state">Tree not found</div>';
- break;
- }
-
- $entries = parseTree($obj["content"]);
-
- echo '<div class="breadcrumb">';
- echo '<a href="?action=home' .
- $repoParam .
- '">' .
- htmlspecialchars($currentRepo["name"]) .
- "</a>";
- echo "<span>/</span>";
- echo '<a href="?action=tree&hash=' .
- $hash .
- $repoParam .
- '">Tree ' .
- substr($hash, 0, 7) .
- "</a>";
- echo "</div>";
-
- echo "<h2>Tree " . substr($hash, 0, 7) . "</h2>";
-
- 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]";
- $action = $entry["type"] === "tree" ? "tree" : "blob";
-
- echo '<a href="?action=' .
- $action .
- "&hash=" .
- $entry["hash"] .
- $repoParam .
- '" 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>";
- }
- break;
-
- case "blob":
- if (!$hash || strlen($hash) !== 40) {
- echo '<div class="empty-state">Invalid blob hash</div>';
- break;
- }
-
- $obj = readGitObject($currentRepo["path"], $hash);
- if (!$obj || $obj["type"] !== "blob") {
- echo '<div class="empty-state">File not found</div>';
- break;
- }
-
- echo '<div class="breadcrumb">';
- echo '<a href="?action=home' .
- $repoParam .
- '">' .
- htmlspecialchars($currentRepo["name"]) .
- "</a>";
- echo "<span>/</span>";
- echo "<span>Blob " . substr($hash, 0, 7) . "</span>";
- echo "</div>";
-
- echo "<h2>File " . substr($hash, 0, 7) . "</h2>";
-
- echo '<div class="blob-content">';
- echo '<div class="blob-header">' .
- strlen($obj["content"]) .
- " bytes</div>";
- echo '<div class="blob-code">' .
- htmlspecialchars($obj["content"]) .
- "</div>";
- echo "</div>";
- break;
-
- case "home":
- default:
- $head = getHead($currentRepo["path"]);
-
- if (!$head) {
- echo '<div class="empty-state">';
- echo "<h3>Empty repository or no commits</h3>";
- echo "<p>Repository: " .
- htmlspecialchars($currentRepo["name"]) .
- "</p>";
- echo "</div>";
- break;
- }
-
- echo "<h2>" . htmlspecialchars($currentRepo["name"]) . "</h2>";
-
- $obj = readGitObject($currentRepo["path"], $head);
-
- if ($obj && $obj["type"] === "commit") {
- $commit = parseCommit($obj["content"]);
- $commit["hash"] = $head;
-
- echo "<h3>Latest Commit</h3>";
- echo '<div class="commit-item">';
- echo "<div>";
- echo '<a href="?action=commit&hash=' .
- $head .
- $repoParam .
- '" class="commit-hash">' .
- substr($head, 0, 7) .
- "</a>";
- echo "</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>Repository Stats</h3>";
- $refs = listRefs($currentRepo["path"]);
- $branchCount = count($refs["branches"] ?? []);
- $tagCount = count($refs["tags"] ?? []);
-
- echo '<div class="commit-details">';
- echo '<div class="commit-info">';
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Branches</span>';
- echo '<span class="commit-info-value"><a href="?action=refs' .
- $repoParam .
- '" class="parent-link">' .
- $branchCount .
- "</a></span>";
- echo "</div>";
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">Tags</span>';
- echo '<span class="commit-info-value"><a href="?action=refs' .
- $repoParam .
- '" class="parent-link">' .
- $tagCount .
- "</a></span>";
- echo "</div>";
- echo '<div class="commit-info-row">';
- echo '<span class="commit-info-label">HEAD</span>';
- echo '<span class="commit-info-value">' .
- substr($head, 0, 7) .
- "</span>";
- echo "</div>";
- echo "</div>";
- echo "</div>";
-
- echo "<h3>Quick Actions</h3>";
- echo '<div class="file-list">';
- echo '<a href="?action=log' . $repoParam . '" class="file-item">';
- echo '<span class="file-mode">&gt;&gt;</span>';
- echo '<span class="file-name">View Full Commit Log</span>';
- echo "</a>";
- echo '<a href="?action=tree&hash=' .
- $commit["tree"] .
- $repoParam .
- '" class="file-item">';
- echo '<span class="file-mode">&gt;&gt;</span>';
- echo '<span class="file-name">Browse Files</span>';
- echo "</a>";
- echo "</div>";
- }
- break;
- } ?>
+/**
+ * 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');
+
+// Get list of available repositories
+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;
+}
+
+// Get current repository
+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 readGitObject($repoPath, $hash) {
+ $path = getObjectPath($repoPath, $hash);
+ if (!$path || !file_exists($path)) {
+ return false;
+ }
+
+ $content = file_get_contents($path);
+ if ($content === false) {
+ return false;
+ }
+
+ $decompressed = @gzuncompress($content);
+ if ($decompressed === false) {
+ return false;
+ }
+
+ $nullPos = strpos($decompressed, "\0");
+ if ($nullPos === false) {
+ return false;
+ }
+
+ $header = substr($decompressed, 0, $nullPos);
+ $parts = explode(' ', $header);
+
+ return [
+ 'type' => $parts[0] ?? 'unknown',
+ 'size' => $parts[1] ?? 0,
+ 'content' => substr($decompressed, $nullPos + 1)
+ ];
+}
+
+function parseTree($content) {
+ $entries = [];
+ $offset = 0;
+
+ while ($offset < strlen($content)) {
+ $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 > strlen($content)) break;
+ $hash = bin2hex(substr($content, $offset, 20));
+ $offset += 20;
+
+ $entries[] = [
+ 'mode' => $mode,
+ 'name' => $name,
+ 'hash' => $hash,
+ 'type' => ($mode === '040000' || $mode === '40000') ? '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>
+ <style>
+ * {
+ 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;
+ }
+ </style>
+</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) {
+ // Default view: list all repositories
+ 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 {
+ // Repository selected - determine main branch and show files
+ $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 {
+ // Get the tree hash from the main branch commit
+ $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'];
+
+ // Check if we're viewing a specific tree or blob
+ $viewHash = $hash ?: $treeHash;
+ $viewObj = readGitObject($currentRepo['path'], $viewHash);
+
+ if ($viewObj && $viewObj['type'] === 'blob') {
+ // Viewing a file
+ 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 {
+ // Viewing tree (directory)
+ $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>';
+
+ // Show latest commit info
+ 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]';
+ $action = $entry['type'] === 'tree' ? '' : '&hash=' . $entry['hash'];
+
+ echo '<a href="?repo=' . urlencode($currentRepo['safe_name']) . $action . '" 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>

Views repos by default, shows files and directories

Author Dave Jarvis <email>
Date 2026-02-08 13:24:22 GMT-0800
Commit e4e553a154a8d4b8b62befada4ffc5b529e12e12
Parent 4504a79
Delta 862 lines added, 1138 lines removed, 276-line decrease