An ultra-lightweight, low-RAM PHP file browser for bare Git repositories
<?php$git_dir = '/srv/git';$domain = $_SERVER['HTTP_HOST'];$clone_prefix = "git@{$domain}:/srv/git/";$script_name = basename($_SERVER['PHP_SELF']);function run_git_cmd($repo_path, $args) { $safe_path = escapeshellarg($repo_path); $command = "git --git-dir={$safe_path} " . $args . " 2>&1"; unset($output); $retval = null; exec($command, $output, $retval); return ['output' => $output, 'retval' => $retval];}function is_repo_public($repo_path) { if (!is_dir($repo_path) || !file_exists($repo_path . '/HEAD')) { return false; } return file_exists($repo_path . '/public');}function is_text_file($filename) { $text_extensions = [ 'txt', 'md', 'php', 'js', 'css', 'html', 'json', 'yml', 'yaml', 'c', 'cpp', 'py', 'sh', 'gitignore', 'conf', 'xml', 'sql', 'toml', 'ini' ]; $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($ext, $text_extensions);}// ==========================================// ACTION: Stream Repository as ZIP Archive// ==========================================if (isset($_GET['repo']) && isset($_GET['action'])) { $repo_name = basename($_GET['repo']); $repo_path = $git_dir . '/' . $repo_name; if (!is_dir($repo_path) || !is_repo_public($repo_path)) { die("Access denied."); } $safe_path = escapeshellarg($repo_path); // 1. ZUM ZIP-DOWNLOAD (Braucht keinen 'path') if ($_GET['action'] === 'download') { $clean_name = preg_replace('/\.git$/', '', $repo_name); $filename = $clean_name . "-" . date('Ymd') . ".zip"; header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Pragma: no-cache'); header('Expires: 0'); passthru("git --git-dir={$safe_path} archive --format=zip HEAD"); exit; } // 2. ZUM ANZEIGEN VON RAW-DATEIEN (Braucht 'path') if ($_GET['action'] === 'raw' && isset($_GET['path'])) { $path = $_GET['path']; // Pfad-Sicherheit if (strpos($path, '..') !== false) die("Invalid path."); $safe_file = escapeshellarg("HEAD:" . $path); // Git zeigt den Inhalt der Datei $content = shell_exec("git --git-dir={$safe_path} show {$safe_file}"); // Mime-Type erkennen, um Bilder/PDFs korrekt anzuzeigen $finfo = new finfo(FILEINFO_MIME_TYPE); header('Content-Type: ' . $finfo->buffer($content)); echo $content; exit; }}// ==========================================// STAGE 2: View Specific Repository or File// ==========================================if (isset($_GET['repo'])) { $repo_name = basename($_GET['repo']); $repo_path = $git_dir . '/' . $repo_name; if (!is_dir($repo_path) || !is_repo_public($repo_path)) { die("Repository not found or private."); } $display_name = preg_replace('/\.git$/', '', $repo_name); $path = isset($_GET['path']) ? $_GET['path'] : '';if (strpos($path, '..') !== false) { die("Access denied: Invalid path sequence.");}if (strpos($path, '/') === 0) { die("Access denied: Absolute paths are not allowed.");} $desc_file = $repo_path . '/description'; $description = 'No description provided.'; if (file_exists($desc_file)) { $lines = file($desc_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!empty($lines) && strpos($lines[0], 'Unnamed repository') === false) { $description = htmlspecialchars($lines[0]); } } if (isset($_GET['action']) && $_GET['action'] === 'view') { $res = run_git_cmd($repo_path, "show HEAD:" . escapeshellarg($path)); $file_content = implode("\n", $res['output']); $is_code_view = true; } else { $is_code_view = false; $git_target = empty($path) ? "HEAD" : "HEAD:" . escapeshellarg($path); $res = run_git_cmd($repo_path, "ls-tree --name-only " . $git_target); $files = $res['output']; $readme_content = null; foreach ($files as $file) { if (strtolower($file) === 'readme' || strtolower(pathinfo($file, PATHINFO_FILENAME)) === 'readme') { $readme_path = empty($path) ? $file : $path . '/' . $file; $r_res = run_git_cmd($repo_path, "show HEAD:" . escapeshellarg($readme_path)); $readme_content = implode("\n", $r_res['output']); break; } } }?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo htmlspecialchars($display_name); ?> - Source</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="repo.css"></head><body> <div class="container"> <header style="margin-bottom: 2rem;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem;"> <div> <h1 style="margin: 0;"><?php echo htmlspecialchars($display_name); ?></h1> <p style="color: var(--text-muted); font-size: 1rem; margin-top: 0.25rem;"><?php echo $description; ?></p> </div> <a href="<?php echo htmlspecialchars($script_name) . '?repo=' . urlencode($repo_name) . '&action=download'; ?>" style="background-color: var(--primary); color: white; text-decoration: none; padding: 0.6rem 1.2rem; border-radius: 6px; font-size: 0.9rem; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;"> <i class="fa-solid fa-file-zipper"></i> Download ZIP </a> </div> </header> <div class="breadcrumb"> <a href="<?php echo htmlspecialchars($script_name); ?>"><i class="fa-solid fa-code-branch" style="margin-right: 5px;"></i> Repositories</a> / <a href="<?php echo htmlspecialchars($script_name) . '?repo=' . urlencode($repo_name); ?>"><?php echo htmlspecialchars($display_name); ?></a> <?php if (!empty($path)) { $parts = explode('/', $path); $running_path = ''; foreach ($parts as $part) { $running_path .= (empty($running_path) ? '' : '/') . $part; $is_last = ($part === end($parts)); if ($is_last && $is_code_view) { echo " / <span style='color: var(--text-main); font-weight: bold;'>" . htmlspecialchars($part) . "</span>"; } else { echo " / <a href='{$script_name}?repo=" . urlencode($repo_name) . "&path=" . urlencode($running_path) . "'>" . htmlspecialchars($part) . "</a>"; } } } ?> </div> <div class="content-box"> <?php if ($is_code_view): ?> <pre class="code-area"><?php $lines = explode("\n", htmlspecialchars($file_content)); foreach ($lines as $line) { echo "<span class='code-line'>" . $line . "</span>"; } ?></pre> <?php else: ?> <?php if (empty($files) || (count($files) == 1 && empty($files[0]))): ?> <div style="padding: 2rem; text-align: center; color: var(--text-muted);">This directory path is empty.</div> <?php else: ?> <?php foreach ($files as $file): $target_path = empty($path) ? $file : $path . '/' . $file; $t_res = run_git_cmd($repo_path, "cat-file -t HEAD:" . escapeshellarg($target_path)); $is_dir = (isset($t_res['output'][0]) && $t_res['output'][0] === 'tree'); // Determine the action based on file type if ($is_dir) { $link_url = $script_name . '?repo=' . urlencode($repo_name) . '&path=' . urlencode($target_path); } else { if (is_text_file($target_path)) { $link_url = $script_name . '?repo=' . urlencode($repo_name) . '&path=' . urlencode($target_path) . '&action=view'; } else { $link_url = $script_name . '?repo=' . urlencode($repo_name) . '&path=' . urlencode($target_path) . '&action=raw'; } }?> <div class="tree-item"> <?php if ($is_dir): ?> <i class="fa-solid fa-folder" style="margin-right: 10px; color: #3b82f6;"></i> <?php else: ?> <i class="fa-solid <?php echo is_text_file($target_path) ? 'fa-file-code' : 'fa-file-image'; ?>" style="margin-right: 10px; color: #64748b;"></i> <?php endif; ?> <a href="<?php echo htmlspecialchars($link_url); ?>"><?php echo htmlspecialchars($file); ?></a> </div><?php endforeach; ?> <?php endif; ?> <?php endif; ?> </div> <?php if (!$is_code_view && !empty($readme_content)): ?> <div class="readme-box"> <div class="readme-header"><i class="fa-solid fa-book-open" style="margin-right: 6px;"></i> README</div> <div id="readme-content" class="readme-content"><?php echo htmlspecialchars($readme_content); ?></div> </div> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> document.addEventListener("DOMContentLoaded", function() { const readmeEl = document.getElementById('readme-content'); if (readmeEl) { const rawMarkdown = readmeEl.textContent; readmeEl.innerHTML = marked.parse(rawMarkdown); } }); </script> <?php endif; ?> <br /> <div class="clone-input-wrapper" style="border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--card-bg);"> <input type="text" readonly style="font-family: var(--font-mono); font-size: 0.85rem; padding: 0.75rem 1rem; border: none; width: 100%; outline: none; color: var(--text-main); background: transparent;" value="git clone <?php echo htmlspecialchars($clone_prefix . $repo_name); ?>" onclick="this.select();"> </div> </div></body></html><?php exit;}// ==========================================// STAGE 1: Main Dashboard (Repository List)// ==========================================$repos = [];if (is_dir($git_dir)) { $iterator = new DirectoryIterator($git_dir); foreach ($iterator as $fileinfo) { if ($fileinfo->isDir() && !$fileinfo->isDot()) { $repo_path = $fileinfo->getPathname(); if (is_repo_public($repo_path)) { $desc_file = $repo_path . '/description'; $description = ''; if (file_exists($desc_file)) { $lines = file($desc_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!empty($lines) && strpos($lines[0], 'Unnamed repository') === false) { $description = htmlspecialchars($lines[0]); } } $repo_name = $fileinfo->getFilename(); $repos[] = [ 'name' => htmlspecialchars(preg_replace('/\.git$/', '', $repo_name)), 'folder' => htmlspecialchars($repo_name), 'description' => $description, 'mtime' => $fileinfo->getMTime() ]; } } } usort($repos, function($a, $b) { return $b['mtime'] - $a['mtime']; });}?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Git Repositories</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="dashboard.css"></head><body> <div class="container"> <header> <h1>Git Repositories</h1> <p class="subtitle">A selection of some of my projects. Dynamically displayed with <a href="https://troue.xyz/git.php?repo=nanoforge.git">nanoforge</a>.</p> </header> <div class="repo-list"> <?php if (empty($repos)): ?> <p style="color: var(--text-muted); text-align: center;">No public repositories discovered.</p> <?php else: ?> <?php foreach ($repos as $repo): ?> <a href="<?php echo htmlspecialchars($script_name . '?repo=' . urlencode($repo['folder'])); ?>" class="repo-card"> <h2><?php echo $repo['name']; ?></h2> <p><?php echo $repo['description']; ?></p> <!--<div class="clone-box" onclick="event.preventDefault();"> <span class="clone-label"><i class="fa-solid fa-terminal" style="font-size: 0.7rem; margin-right: 4px;"></i> Clone Address</span> <div class="clone-input-wrapper"> <input type="text" class="clone-url" readonly value="git clone <?php echo htmlspecialchars($clone_prefix . $repo['folder']); ?>" onclick="this.select();"> </div> </div>--> <div style="display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-muted); margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.75rem;"> <span style="color: var(--primary); font-weight: 600;"><i class="fa-solid fa-code" style="margin-right: 4px;"></i> View Source</span> <span><i class="fa-solid fa-clock" style="margin-right: 4px;"></i> <?php echo date('Y-m-d', $repo['mtime']); ?></span> </div> </a> <?php endforeach; ?> <?php endif; ?> </div> </div></body></html>