nanoforge

An ultra-lightweight, low-RAM PHP file browser for bare Git repositories

Download ZIP
<?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>