git / brickware / marrow.git - 2d1c595

(22 hours ago)commit 2d1c595: directory fixes

tree / marrow-static.c

marrow-static.c

raw

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
#include <stdint.h>

#define MAX_CMD_LEN 4096
#define MAX_LINE_LEN 2048
#define DEFAULT_GIT_BASE_PATH "/srv/git"

/* Repository metadata for directory index generation */
struct repo_info {
    char name[PATH_MAX];
    char full_path[PATH_MAX];
    char relative_path[PATH_MAX];
    char description[512];
    char last_commit_date[128];
    char last_commit_hash[64];
};

/* Directory entry for index pages */
struct dir_entry {
    char name[256];
    char path[PATH_MAX];
    char readme_snippet[512];
    int32_t is_directory;
};

/* Dynamic arrays for scanning results */
struct repo_list {
    struct repo_info *repos;
    size_t count;
    size_t capacity;
};

struct dir_list {
    struct dir_entry *entries;
    size_t count;
    size_t capacity;
};

static void
init_repo_list(struct repo_list *list) {
    list->repos = NULL;
    list->count = 0;
    list->capacity = 0;
}

static void
free_repo_list(struct repo_list *list) {
    if (list->repos) {
        free(list->repos);
        list->repos = NULL;
    }
    list->count = 0;
    list->capacity = 0;
}

static int32_t
add_repo_to_list(struct repo_list *list, const struct repo_info *repo) {
    if (list->count >= list->capacity) {
        size_t new_capacity = (0 == list->capacity) ? 16 : list->capacity * 2;
        struct repo_info *new_repos = realloc(list->repos, new_capacity * sizeof(struct repo_info));
        if (!new_repos) {
            return 0;
        }
        list->repos = new_repos;
        list->capacity = new_capacity;
    }
    list->repos[list->count++] = *repo;
    return 1;
}

static void
init_dir_list(struct dir_list *list) {
    list->entries = NULL;
    list->count = 0;
    list->capacity = 0;
}

static void
free_dir_list(struct dir_list *list) {
    if (list->entries) {
        free(list->entries);
        list->entries = NULL;
    }
    list->count = 0;
    list->capacity = 0;
}

static int32_t
add_dir_to_list(struct dir_list *list, const struct dir_entry *entry) {
    if (list->count >= list->capacity) {
        size_t new_capacity = (0 == list->capacity) ? 16 : list->capacity * 2;
        struct dir_entry *new_entries = realloc(list->entries, new_capacity * sizeof(struct dir_entry));
        if (!new_entries) {
            return 0;
        }
        list->entries = new_entries;
        list->capacity = new_capacity;
    }
    list->entries[list->count++] = *entry;
    return 1;
}

static int32_t
ensure_directory(const char *path) {
    struct stat st;
    if (0 == stat(path, &st)) {
        return S_ISDIR(st.st_mode) ? 1 : 0;
    }

    char tmp[PATH_MAX];
    snprintf(tmp, sizeof(tmp), "mkdir -p '%s'", path);
    return (0 == system(tmp));
}

static int32_t
is_git_repository(const char *path) {
    char git_dir[PATH_MAX];
    struct stat st;

    snprintf(git_dir, sizeof(git_dir), "%s/HEAD", path);
    return (0 == stat(git_dir, &st));
}

static void
html_escape(FILE *out, const char *str) {
    while (*str) {
        switch (*str) {
            case '<': fprintf(out, "&lt;"); break;
            case '>': fprintf(out, "&gt;"); break;
            case '&': fprintf(out, "&amp;"); break;
            case '"': fprintf(out, "&quot;"); break;
            default: fputc(*str, out); break;
        }
        str++;
    }
}

static void
generate_html_header(FILE *out, const char *repo_name, const char *suffix) {
    char title[512];
    if (suffix && suffix[0]) {
        snprintf(title, sizeof(title), "%s - %s", repo_name, suffix);
    } else {
        snprintf(title, sizeof(title), "%s", repo_name);
    }

    fprintf(out, "<!DOCTYPE html>\n");
    fprintf(out, "<html>\n<head>\n");
    fprintf(out, "<meta charset=\"utf-8\">\n");
    fprintf(out, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n");
    fprintf(out, "<title>%s</title>\n", title);
    fprintf(out, "<style>\n");
    fprintf(out, "body{font-family:monospace;max-width:900px;margin:40px auto;padding:0 20px;line-height:1.6;}\n");
    fprintf(out, "a{color:#00f;text-decoration:none;}\n");
    fprintf(out, "a:hover{text-decoration:underline;}\n");
    fprintf(out, "table{border-collapse:collapse;width:100%%;}\n");
    fprintf(out, "th,td{text-align:left;padding:8px 12px;border-bottom:1px solid #ddd;}\n");
    fprintf(out, "th{font-weight:bold;}\n");
    fprintf(out, "pre{background:#f5f5f5;padding:10px;overflow-x:auto;}\n");
    fprintf(out, "code{background:#f5f5f5;padding:2px 4px;}\n");
    fprintf(out, ".header{border-bottom:2px solid #000;margin-bottom:20px;padding-bottom:10px;}\n");
    fprintf(out, ".footer{margin-top:40px;padding-top:20px;border-top:1px solid #ddd;font-size:0.9em;color:#666;}\n");
    fprintf(out, "</style>\n");
    fprintf(out, "</head>\n<body>\n");

    /* Header with breadcrumb navigation */
    fprintf(out, "<div class=\"header\"><h1>");

    /* Count path segments in repo_name (e.g., "user/project.git" = 2 segments) */
    int32_t repo_segments = 1;  /* At least one segment */
    for (const char *p = repo_name; *p; p++) {
        if ('/' == *p) repo_segments++;
    }

    /* Root "git" link - go up through branch/commit structure AND repo path */
    fprintf(out, "<a href=\"");
    /* From branch/main/tree or commit/hash/summary (3 levels) up through repo path segments */
    int32_t git_levels = 3 + repo_segments;
    for (int32_t i = 0; i < git_levels; i++) {
        fprintf(out, "../");
    }
    fprintf(out, "\">git</a>");

    /* Parse repo path and create breadcrumb links */
    if (repo_name && repo_name[0]) {
        char path_copy[PATH_MAX];
        snprintf(path_copy, sizeof(path_copy), "%s", repo_name);

        char *token = strtok(path_copy, "/");
        int32_t current_segment = 0;

        while (token) {
            fprintf(out, " / ");

            /* Create link */
            fprintf(out, "<a href=\"");
            /* Go up 3 levels (branch/main/tree) plus segments after this one */
            int32_t levels_up = 3 + (repo_segments - current_segment - 1);
            for (int32_t i = 0; i < levels_up; i++) {
                fprintf(out, "../");
            }
            fprintf(out, "\">");
            html_escape(out, token);
            fprintf(out, "</a>");

            current_segment++;
            token = strtok(NULL, "/");
        }
    }

    if (suffix && suffix[0]) {
        fprintf(out, " - %s", suffix);
    }
    fprintf(out, "</h1></div>\n");
}

static void
generate_html_footer(FILE *out) {
    fprintf(out, "<div class=\"footer\">generated by marrow-static</div>\n");
    fprintf(out, "</body>\n</html>\n");
}

static int32_t
get_git_readme(const char *repo_path, char **readme) {
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git show HEAD:README.md 2>/dev/null || "
             "git show HEAD:README 2>/dev/null || "
             "git show HEAD:readme.md 2>/dev/null",
             repo_path);

    FILE *fp = popen(cmd, "r");
    if (!fp) {
        return 0;
    }

    /* Allocate buffer for README */
    size_t capacity = 8192;
    size_t total = 0;
    *readme = malloc(capacity);
    if (!*readme) {
        pclose(fp);
        return 0;
    }

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), fp)) {
        size_t len = strlen(buffer);
        if (total + len >= capacity) {
            capacity *= 2;
            char *new_readme = realloc(*readme, capacity);
            if (!new_readme) {
                free(*readme);
                pclose(fp);
                return 0;
            }
            *readme = new_readme;
        }
        strcpy(*readme + total, buffer);
        total += len;
    }

    int32_t status = pclose(fp);
    if (0 != status || 0 == total) {
        free(*readme);
        *readme = NULL;
        return 0;
    }

    return 1;
}

static int32_t
generate_commit_summary_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
    generate_html_header(out, repo_name, commit_hash);

    /* Get commit subject and date for display */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *subject_fp = popen(cmd, "r");
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (subject_fp) {
        char line[MAX_LINE_LEN];
        if (fgets(line, sizeof(line), subject_fp)) {
            char *subject = strtok(line, "|");
            char *date = strtok(NULL, "\n");
            if (subject) {
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(subject_fp);
    }

    /* Display commit info line */
    fprintf(out, "<p><small>");
    if (commit_date[0]) {
        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
    }
    fprintf(out, "commit %s: ", commit_hash);
    html_escape(out, commit_subject);
    fprintf(out, "</small></p>\n");

    /* Navigation */
    fprintf(out, "<p><strong>Summary</strong> | <a href=\"../history/\">History</a> | <a href=\"../tree/\">Files</a></p>\n");

    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);

    /* Get commit details */
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git show --pretty=format:'%%H|%%an|%%ae|%%ad|%%s%%n%%n%%b' --no-patch %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *fp = popen(cmd, "r");
    if (!fp) {
        return 0;
    }

    char line[MAX_LINE_LEN];
    if (fgets(line, sizeof(line), fp)) {
        (void)strtok(line, "|");  /* full_hash */
        char *author = strtok(NULL, "|");
        char *email = strtok(NULL, "|");
        char *date = strtok(NULL, "|");
        char *subject = strtok(NULL, "\n");

        if (author && date && subject) {
            fprintf(out, "<table>\n");
            fprintf(out, "<tr><td><strong>author</strong></td><td>");
            html_escape(out, author);
            if (email) {
                fprintf(out, " &lt;");
                html_escape(out, email);
                fprintf(out, "&gt;");
            }
            fprintf(out, "</td></tr>\n");
            fprintf(out, "<tr><td><strong>date</strong></td><td>%s</td></tr>\n", date);
            fprintf(out, "</table>\n");

            fprintf(out, "<h3>message</h3>\n<pre>");
            html_escape(out, subject);
            fprintf(out, "\n");

            /* Read commit body */
            while (fgets(line, sizeof(line), fp)) {
                html_escape(out, line);
            }
            fprintf(out, "</pre>\n");
        }
    }
    pclose(fp);

    /* Show diff */
    fprintf(out, "<h3>diff</h3>\n");
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git show %s 2>/dev/null",
             repo_path, commit_hash);

    fp = popen(cmd, "r");
    if (fp) {
        fprintf(out, "<pre>");
        while (fgets(line, sizeof(line), fp)) {
            html_escape(out, line);
        }
        fprintf(out, "</pre>\n");
        pclose(fp);
    }

    generate_html_footer(out);

    return 1;
}

static int32_t
generate_commit_history_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
    generate_html_header(out, repo_name, commit_hash);

    /* Get commit subject and date for display */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *subject_fp = popen(cmd, "r");
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (subject_fp) {
        char line[MAX_LINE_LEN];
        if (fgets(line, sizeof(line), subject_fp)) {
            char *subject = strtok(line, "|");
            char *date = strtok(NULL, "\n");
            if (subject) {
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(subject_fp);
    }

    /* Display commit info line */
    fprintf(out, "<p><small>");
    if (commit_date[0]) {
        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
    }
    fprintf(out, "commit %s: ", commit_hash);
    html_escape(out, commit_subject);
    fprintf(out, "</small></p>\n");

    /* Navigation */
    fprintf(out, "<p><a href=\"../summary/\">Summary</a> | <strong>History</strong> | <a href=\"../tree/\">Files</a></p>\n");

    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);

    /* Recent commits from this point */
    fprintf(out, "<h3>history</h3>\n");
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log --pretty=format:'%%h|%%an|%%ar|%%s' -n 10 %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *fp = popen(cmd, "r");
    if (fp) {
        fprintf(out, "<table>\n");
        fprintf(out, "<thead><tr><th>hash</th><th>author</th><th>date</th><th>message</th></tr></thead>\n");
        fprintf(out, "<tbody>\n");

        char line[MAX_LINE_LEN];
        while (fgets(line, sizeof(line), fp)) {
            char *hash = strtok(line, "|");
            char *author = strtok(NULL, "|");
            char *date = strtok(NULL, "|");
            char *msg = strtok(NULL, "|");

            if (hash && author && date && msg) {
                /* Remove trailing newline from message */
                size_t msg_len = strlen(msg);
                if (msg_len > 0 && '\n' == msg[msg_len - 1]) {
                    msg[msg_len - 1] = '\0';
                }

                fprintf(out, "<tr><td><a href=\"../../%s/summary/\"><code>%s</code></a></td><td>", hash, hash);
                html_escape(out, author);
                fprintf(out, "</td><td>%s</td><td>", date);
                html_escape(out, msg);
                fprintf(out, "</td></tr>\n");
            }
        }

        fprintf(out, "</tbody>\n</table>\n");
        pclose(fp);
    }

    generate_html_footer(out);

    return 1;
}

static int32_t
generate_commit_tree_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
    generate_html_header(out, repo_name, commit_hash);

    /* Get commit subject and date for display */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *subject_fp = popen(cmd, "r");
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (subject_fp) {
        char line[MAX_LINE_LEN];
        if (fgets(line, sizeof(line), subject_fp)) {
            char *subject = strtok(line, "|");
            char *date = strtok(NULL, "\n");
            if (subject) {
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(subject_fp);
    }

    /* Display commit info line */
    fprintf(out, "<p><small>");
    if (commit_date[0]) {
        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
    }
    fprintf(out, "commit %s: ", commit_hash);
    html_escape(out, commit_subject);
    fprintf(out, "</small></p>\n");

    /* Navigation */
    fprintf(out, "<p><a href=\"../summary/\">Summary</a> | <a href=\"../history/\">History</a> | <strong>Files</strong></p>\n");

    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);

    /* File tree */
    fprintf(out, "<h3>files</h3>\n");
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git ls-tree -l %s 2>/dev/null",
             repo_path, commit_hash);

    FILE *fp = popen(cmd, "r");
    if (fp) {
        fprintf(out, "<table>\n");
        fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
        fprintf(out, "<tbody>\n");

        char line[MAX_LINE_LEN];
        while (fgets(line, sizeof(line), fp)) {
            /* git ls-tree -l format: mode type hash size<tab>name */
            (void)strtok(line, " ");  /* mode */
            char *type = strtok(NULL, " ");
            (void)strtok(NULL, " ");  /* hash */
            char *size_str = strtok(NULL, "\t");
            char *name = strtok(NULL, "\n");

            if (type && name) {
                const char *type_display = "file";
                if (0 == strcmp(type, "tree")) {
                    type_display = "dir";
                }

                fprintf(out, "<tr><td>%s</td><td>", type_display);
                if (0 == strcmp(type, "tree")) {
                    fprintf(out, "<a href=\"%s/\">", name);
                    html_escape(out, name);
                    fprintf(out, "/</a>");
                } else {
                    fprintf(out, "<a href=\"%s/\">", name);
                    html_escape(out, name);
                    fprintf(out, "</a>");
                }
                fprintf(out, "</td>");

                /* Get last commit for this file */
                char commit_cmd[MAX_CMD_LEN];
                snprintf(commit_cmd, sizeof(commit_cmd),
                         "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
                         repo_path, commit_hash, name);

                FILE *commit_fp = popen(commit_cmd, "r");
                char commit_line[MAX_LINE_LEN] = {0};
                if (commit_fp && fgets(commit_line, sizeof(commit_line), commit_fp)) {
                    char *hash = strtok(commit_line, "|");
                    char *msg = strtok(NULL, "|");
                    char *date = strtok(NULL, "\n");

                    if (hash && msg) {
                        /* Truncate message if too long */
                        size_t msg_len = strlen(msg);
                        const size_t max_msg_len = 45;
                        if (msg_len > max_msg_len) {
                            msg[max_msg_len - 3] = '.';
                            msg[max_msg_len - 2] = '.';
                            msg[max_msg_len - 1] = '.';
                            msg[max_msg_len] = '\0';
                        }

                        fprintf(out, "<td>");
                        if (date) {
                            fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
                        }
                        fprintf(out, "<a href=\"../../%s/summary/\"><code>%s</code></a> ", hash, hash);
                        html_escape(out, msg);
                        fprintf(out, "</td>");
                    } else {
                        fprintf(out, "<td>-</td>");
                    }
                    if (commit_fp) pclose(commit_fp);
                } else {
                    fprintf(out, "<td>-</td>");
                    if (commit_fp) pclose(commit_fp);
                }

                fprintf(out, "<td>");
                if (size_str && 0 != strcmp(type, "tree")) {
                    fprintf(out, "%s", size_str);
                } else {
                    fprintf(out, "-");
                }
                fprintf(out, "</td>");

                fprintf(out, "</tr>\n");
            }
        }

        fprintf(out, "</tbody>\n</table>\n");
        pclose(fp);
    }

    generate_html_footer(out);

    return 1;
}

static int32_t
generate_branch_history_page(FILE *out, const char *repo_path, const char *repo_name, const char *branch_name) {
    generate_html_header(out, repo_name, branch_name);

    /* Get latest commit for display */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s 2>/dev/null",
             repo_path, branch_name);

    FILE *commit_fp = popen(cmd, "r");
    char commit_hash[64] = {0};
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (commit_fp) {
        char commit_line[MAX_LINE_LEN];
        if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
            char *hash = strtok(commit_line, "|");
            char *subject = strtok(NULL, "|");
            char *date = strtok(NULL, "\n");
            if (hash && subject) {
                snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(commit_fp);
    }

    /* Display commit info line */
    if (commit_hash[0]) {
        fprintf(out, "<p><small>");
        if (commit_date[0]) {
            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
        }
        fprintf(out, "commit %s: ", commit_hash);
        html_escape(out, commit_subject);
        fprintf(out, "</small></p>\n");
    }

    /* Navigation */
    fprintf(out, "<p><a href=\"../tree/\">Files</a> | <strong>History</strong></p>\n");

    /* Clone URL */
    fprintf(out, "<h2>clone</h2>\n");
    fprintf(out, "<pre>git clone git@host:%s</pre>\n", repo_name);

    /* Recent commits */
    fprintf(out, "<h2>recent commits</h2>\n");
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log --pretty=format:'%%h|%%an|%%ar|%%s' -n 10 %s 2>/dev/null",
             repo_path, branch_name);

    FILE *fp = popen(cmd, "r");
    if (fp) {
        fprintf(out, "<table>\n");
        fprintf(out, "<thead><tr><th>hash</th><th>author</th><th>date</th><th>message</th></tr></thead>\n");
        fprintf(out, "<tbody>\n");

        char line[MAX_LINE_LEN];
        while (fgets(line, sizeof(line), fp)) {
            char *hash = strtok(line, "|");
            char *author = strtok(NULL, "|");
            char *date = strtok(NULL, "|");
            char *msg = strtok(NULL, "|");

            if (hash && author && date && msg) {
                /* Remove trailing newline from message */
                size_t msg_len = strlen(msg);
                if (msg_len > 0 && '\n' == msg[msg_len - 1]) {
                    msg[msg_len - 1] = '\0';
                }

                fprintf(out, "<tr><td><a href=\"../../../commit/%s/summary/\"><code>%s</code></a></td><td>", hash, hash);
                html_escape(out, author);
                fprintf(out, "</td><td>%s</td><td>", date);
                html_escape(out, msg);
                fprintf(out, "</td></tr>\n");
            }
        }

        fprintf(out, "</tbody>\n</table>\n");
        pclose(fp);
    }

    generate_html_footer(out);

    return 1;
}

static int32_t
generate_branch_tree_page(FILE *out, const char *repo_path, const char *repo_name, const char *branch_name) {
    generate_html_header(out, repo_name, branch_name);

    /* Get latest commit for display */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s 2>/dev/null",
             repo_path, branch_name);

    FILE *commit_fp = popen(cmd, "r");
    char commit_hash[64] = {0};
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (commit_fp) {
        char commit_line[MAX_LINE_LEN];
        if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
            char *hash = strtok(commit_line, "|");
            char *subject = strtok(NULL, "|");
            char *date = strtok(NULL, "\n");
            if (hash && subject) {
                snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(commit_fp);
    }

    /* Display commit info line */
    if (commit_hash[0]) {
        fprintf(out, "<p><small>");
        if (commit_date[0]) {
            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
        }
        fprintf(out, "commit %s: ", commit_hash);
        html_escape(out, commit_subject);
        fprintf(out, "</small></p>\n");
    }

    /* Navigation */
    fprintf(out, "<p><strong>Files</strong> | <a href=\"../history/\">History</a></p>\n");

    /* Clone URL */
    fprintf(out, "<h2>clone</h2>\n");
    fprintf(out, "<pre>git clone git@host:%s</pre>\n", repo_name);

    /* Get README for later display */
    char *readme = NULL;
    get_git_readme(repo_path, &readme);

    /* File tree */
    fprintf(out, "<h2>files</h2>\n");
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git ls-tree -l %s 2>/dev/null",
             repo_path, branch_name);

    FILE *fp = popen(cmd, "r");
    if (fp) {
        fprintf(out, "<table>\n");
        fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
        fprintf(out, "<tbody>\n");

        char line[MAX_LINE_LEN];
        while (fgets(line, sizeof(line), fp)) {
            /* git ls-tree -l format: mode type hash size<tab>name */
            (void)strtok(line, " ");  /* mode */
            char *type = strtok(NULL, " ");
            (void)strtok(NULL, " ");  /* hash */
            char *size_str = strtok(NULL, "\t");
            char *name = strtok(NULL, "\n");

            if (type && name) {
                const char *type_display = "file";
                if (0 == strcmp(type, "tree")) {
                    type_display = "dir";
                }

                fprintf(out, "<tr><td>%s</td><td>", type_display);
                if (0 == strcmp(type, "tree")) {
                    fprintf(out, "<a href=\"%s/\">", name);
                    html_escape(out, name);
                    fprintf(out, "/</a>");
                } else {
                    fprintf(out, "<a href=\"%s/\">", name);
                    html_escape(out, name);
                    fprintf(out, "</a>");
                }
                fprintf(out, "</td>");

                /* Get last commit for this file */
                char commit_cmd[MAX_CMD_LEN];
                snprintf(commit_cmd, sizeof(commit_cmd),
                         "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
                         repo_path, branch_name, name);

                FILE *commit_fp = popen(commit_cmd, "r");
                char commit_line[MAX_LINE_LEN] = {0};
                if (commit_fp && fgets(commit_line, sizeof(commit_line), commit_fp)) {
                    char *hash = strtok(commit_line, "|");
                    char *msg = strtok(NULL, "|");
                    char *date = strtok(NULL, "\n");

                    if (hash && msg) {
                        /* Truncate message if too long */
                        size_t msg_len = strlen(msg);
                        const size_t max_msg_len = 45;
                        if (msg_len > max_msg_len) {
                            msg[max_msg_len - 3] = '.';
                            msg[max_msg_len - 2] = '.';
                            msg[max_msg_len - 1] = '.';
                            msg[max_msg_len] = '\0';
                        }

                        fprintf(out, "<td>");
                        if (date) {
                            fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
                        }
                        fprintf(out, "<a href=\"../../../commit/%s/summary/\"><code>%s</code></a> ", hash, hash);
                        html_escape(out, msg);
                        fprintf(out, "</td>");
                    } else {
                        fprintf(out, "<td>-</td>");
                    }
                    if (commit_fp) pclose(commit_fp);
                } else {
                    fprintf(out, "<td>-</td>");
                    if (commit_fp) pclose(commit_fp);
                }

                fprintf(out, "<td>");
                if (size_str && 0 != strcmp(type, "tree")) {
                    fprintf(out, "%s", size_str);
                } else {
                    fprintf(out, "-");
                }
                fprintf(out, "</td>");

                fprintf(out, "</tr>\n");
            }
        }

        fprintf(out, "</tbody>\n</table>\n");
        pclose(fp);
    }

    /* README - display after file tree */
    if (readme) {
        fprintf(out, "<h2>readme</h2>\n");
        fprintf(out, "<pre>");
        html_escape(out, readme);
        fprintf(out, "</pre>\n");
        free(readme);
    }

    generate_html_footer(out);

    return 1;
}

static int32_t
generate_tree_path_page(FILE *out, const char *repo_path, const char *repo_name,
                        const char *ref, const char *tree_path, const char *page_title) {
    generate_html_header(out, repo_name, page_title);

    /* Get latest commit for this path */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
             repo_path, ref, tree_path);

    FILE *commit_fp = popen(cmd, "r");
    char commit_hash[64] = {0};
    char commit_subject[512] = {0};
    char commit_date[128] = {0};
    if (commit_fp) {
        char commit_line[MAX_LINE_LEN];
        if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
            char *hash = strtok(commit_line, "|");
            char *subject = strtok(NULL, "|");
            char *date = strtok(NULL, "\n");
            if (hash && subject) {
                snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
            }
            if (date) {
                snprintf(commit_date, sizeof(commit_date), "%s", date);
            }
        }
        pclose(commit_fp);
    }

    /* Display commit info line */
    if (commit_hash[0]) {
        fprintf(out, "<p><small>");
        if (commit_date[0]) {
            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
        }
        fprintf(out, "commit %s: ", commit_hash);
        html_escape(out, commit_subject);
        fprintf(out, "</small></p>\n");
    }

    /* Navigation */
    fprintf(out, "<p><a href=\"../../\">Back</a></p>\n");

    /* Check if path is tree or blob */
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git cat-file -t %s:'%s' 2>/dev/null",
             repo_path, ref, tree_path);

    FILE *type_fp = popen(cmd, "r");
    char obj_type[32] = {0};
    if (type_fp) {
        if (fgets(obj_type, sizeof(obj_type), type_fp)) {
            size_t len = strlen(obj_type);
            if (len > 0 && '\n' == obj_type[len - 1]) {
                obj_type[len - 1] = '\0';
            }
        }
        pclose(type_fp);
    }

    if (0 == strcmp(obj_type, "tree")) {
        /* Directory - show file list */
        fprintf(out, "<h2>%s/</h2>\n", tree_path);
        fprintf(out, "<h3>files</h3>\n");

        snprintf(cmd, sizeof(cmd),
                 "cd '%s' && git ls-tree -l %s:'%s' 2>/dev/null",
                 repo_path, ref, tree_path);

        FILE *fp = popen(cmd, "r");
        if (fp) {
            fprintf(out, "<table>\n");
            fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
            fprintf(out, "<tbody>\n");

            /* Add parent directory link */
            fprintf(out, "<tr><td>dir</td><td><a href=\"../\">..</a></td><td>-</td><td>-</td></tr>\n");

            char line[MAX_LINE_LEN];
            while (fgets(line, sizeof(line), fp)) {
                (void)strtok(line, " ");  /* mode */
                char *type = strtok(NULL, " ");
                (void)strtok(NULL, " ");  /* hash */
                char *size_str = strtok(NULL, "\t");
                char *name = strtok(NULL, "\n");

                if (type && name) {
                    const char *type_display = "file";
                    if (0 == strcmp(type, "tree")) {
                        type_display = "dir";
                    }

                    fprintf(out, "<tr><td>%s</td><td>", type_display);
                    if (0 == strcmp(type, "tree")) {
                        fprintf(out, "<a href=\"%s/\">", name);
                        html_escape(out, name);
                        fprintf(out, "/</a>");
                    } else {
                        fprintf(out, "<a href=\"%s/\">", name);
                        html_escape(out, name);
                        fprintf(out, "</a>");
                    }
                    fprintf(out, "</td>");

                    /* Get last commit for this file */
                    char full_path[PATH_MAX];
                    snprintf(full_path, sizeof(full_path), "%s/%s", tree_path, name);

                    char commit_cmd[MAX_CMD_LEN];
                    snprintf(commit_cmd, sizeof(commit_cmd),
                             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
                             repo_path, ref, full_path);

                    FILE *c_fp = popen(commit_cmd, "r");
                    char c_line[MAX_LINE_LEN] = {0};
                    if (c_fp && fgets(c_line, sizeof(c_line), c_fp)) {
                        char *hash = strtok(c_line, "|");
                        char *msg = strtok(NULL, "|");
                        char *date = strtok(NULL, "\n");

                        if (hash && msg) {
                            size_t msg_len = strlen(msg);
                            const size_t max_msg_len = 45;
                            if (msg_len > max_msg_len) {
                                msg[max_msg_len - 3] = '.';
                                msg[max_msg_len - 2] = '.';
                                msg[max_msg_len - 1] = '.';
                                msg[max_msg_len] = '\0';
                            }

                            fprintf(out, "<td>");
                            if (date) {
                                fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
                            }
                            fprintf(out, "<a href=\"../../../commit/%s/summary/\"><code>%s</code></a> ", hash, hash);
                            html_escape(out, msg);
                            fprintf(out, "</td>");
                        } else {
                            fprintf(out, "<td>-</td>");
                        }
                        if (c_fp) pclose(c_fp);
                    } else {
                        fprintf(out, "<td>-</td>");
                        if (c_fp) pclose(c_fp);
                    }

                    fprintf(out, "<td>");
                    if (size_str && 0 != strcmp(type, "tree")) {
                        fprintf(out, "%s", size_str);
                    } else {
                        fprintf(out, "-");
                    }
                    fprintf(out, "</td>");

                    fprintf(out, "</tr>\n");
                }
            }

            fprintf(out, "</tbody>\n</table>\n");
            pclose(fp);
        }
    } else if (0 == strcmp(obj_type, "blob")) {
        /* File - show contents */
        fprintf(out, "<h2>%s</h2>\n", tree_path);
        fprintf(out, "<p><a href=\"raw.txt\">raw</a></p>\n");

        snprintf(cmd, sizeof(cmd),
                 "cd '%s' && git show %s:'%s' 2>/dev/null",
                 repo_path, ref, tree_path);

        FILE *fp = popen(cmd, "r");
        if (fp) {
            fprintf(out, "<pre>");
            char line[MAX_LINE_LEN];
            while (fgets(line, sizeof(line), fp)) {
                html_escape(out, line);
            }
            fprintf(out, "</pre>\n");
            pclose(fp);
        }
    }

    generate_html_footer(out);
    return 1;
}

static void
generate_tree_pages_recursive(const char *output_dir, const char *repo_path,
                               const char *repo_name, const char *ref,
                               const char *tree_path, const char *page_title) {
    char cmd[MAX_CMD_LEN];

    /* Get list of files/directories at this path */
    if (tree_path && tree_path[0]) {
        snprintf(cmd, sizeof(cmd),
                 "cd '%s' && git ls-tree %s:'%s' 2>/dev/null",
                 repo_path, ref, tree_path);
    } else {
        snprintf(cmd, sizeof(cmd),
                 "cd '%s' && git ls-tree %s 2>/dev/null",
                 repo_path, ref);
    }

    FILE *fp = popen(cmd, "r");
    if (!fp) return;

    char line[MAX_LINE_LEN];
    while (fgets(line, sizeof(line), fp)) {
        (void)strtok(line, " ");  /* mode */
        char *type = strtok(NULL, " ");
        (void)strtok(NULL, "\t");  /* hash, up to tab */
        char *name = strtok(NULL, "\n");

        if (!type || !name) continue;

        char full_path[PATH_MAX];
        char output_path[PATH_MAX];

        if (tree_path && tree_path[0]) {
            snprintf(full_path, sizeof(full_path), "%s/%s", tree_path, name);
        } else {
            snprintf(full_path, sizeof(full_path), "%s", name);
        }

        if (0 == strcmp(type, "tree")) {
            /* Directory - create directory page and recurse */
            snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, full_path);
            if (!ensure_directory(output_path)) continue;

            char index_path[PATH_MAX];
            snprintf(index_path, sizeof(index_path), "%s/index.html", output_path);

            FILE *out = fopen(index_path, "w");
            if (out) {
                generate_tree_path_page(out, repo_path, repo_name, ref, full_path, page_title);
                fclose(out);
            }

            /* Recurse into subdirectory */
            generate_tree_pages_recursive(output_dir, repo_path, repo_name, ref, full_path, page_title);
        } else {
            /* File - create file directory with index.html and raw */
            snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, full_path);
            if (!ensure_directory(output_path)) continue;

            /* Create index.html with HTML view */
            char index_path[PATH_MAX];
            snprintf(index_path, sizeof(index_path), "%s/index.html", output_path);

            FILE *out = fopen(index_path, "w");
            if (out) {
                generate_tree_path_page(out, repo_path, repo_name, ref, full_path, page_title);
                fclose(out);
            }

            /* Create raw.txt file with just content */
            char raw_path[PATH_MAX];
            snprintf(raw_path, sizeof(raw_path), "%s/raw.txt", output_path);

            FILE *raw_out = fopen(raw_path, "w");
            if (raw_out) {
                char cmd_raw[MAX_CMD_LEN];
                snprintf(cmd_raw, sizeof(cmd_raw),
                         "cd '%s' && git show %s:'%s' 2>/dev/null",
                         repo_path, ref, full_path);

                FILE *fp_raw = popen(cmd_raw, "r");
                if (fp_raw) {
                    char line_raw[MAX_LINE_LEN];
                    while (fgets(line_raw, sizeof(line_raw), fp_raw)) {
                        fputs(line_raw, raw_out);
                    }
                    pclose(fp_raw);
                }
                fclose(raw_out);
            }
        }
    }

    pclose(fp);
}

static int32_t
get_repo_metadata(const char *repo_path, const char *relative_path, struct repo_info *info) {
    snprintf(info->full_path, sizeof(info->full_path), "%s", repo_path);
    snprintf(info->relative_path, sizeof(info->relative_path), "%s", relative_path);
    snprintf(info->name, sizeof(info->name), "%s", relative_path);

    /* Read description file */
    char desc_path[PATH_MAX];
    snprintf(desc_path, sizeof(desc_path), "%s/description", repo_path);

    FILE *desc_fp = fopen(desc_path, "r");
    if (desc_fp) {
        if (fgets(info->description, sizeof(info->description), desc_fp)) {
            size_t len = strlen(info->description);
            if (len > 0 && '\n' == info->description[len - 1]) {
                info->description[len - 1] = '\0';
            }
            /* Skip default git description */
            if (0 == strncmp(info->description, "Unnamed repository", 18)) {
                info->description[0] = '\0';
            }
        }
        fclose(desc_fp);
    }

    if ('\0' == info->description[0]) {
        snprintf(info->description, sizeof(info->description), "Unnamed repository");
    }

    /* Get last commit info */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd),
             "cd '%s' && git log -1 --pretty=format:'%%h|%%ar' HEAD 2>/dev/null",
             repo_path);

    FILE *commit_fp = popen(cmd, "r");
    if (commit_fp) {
        char line[MAX_LINE_LEN];
        if (fgets(line, sizeof(line), commit_fp)) {
            char *hash = strtok(line, "|");
            char *date = strtok(NULL, "\n");

            if (hash) {
                snprintf(info->last_commit_hash, sizeof(info->last_commit_hash), "%s", hash);
            }
            if (date) {
                snprintf(info->last_commit_date, sizeof(info->last_commit_date), "%s", date);
            }
        }
        pclose(commit_fp);
    }

    if ('\0' == info->last_commit_hash[0]) {
        snprintf(info->last_commit_hash, sizeof(info->last_commit_hash), "none");
        snprintf(info->last_commit_date, sizeof(info->last_commit_date), "never");
    }

    return 1;
}

static int32_t
find_all_repositories(const char *git_base_path, struct repo_list *list) {
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd), "find '%s' -type d -name '*.git' 2>/dev/null", git_base_path);

    FILE *fp = popen(cmd, "r");
    if (!fp) {
        return 0;
    }

    char line[PATH_MAX];
    size_t base_len = strlen(git_base_path);

    while (fgets(line, sizeof(line), fp)) {
        /* Remove trailing newline */
        size_t len = strlen(line);
        if (len > 0 && '\n' == line[len - 1]) {
            line[len - 1] = '\0';
            len--;
        }

        if (0 == len) {
            continue;
        }

        /* Verify it's a git repository */
        if (!is_git_repository(line)) {
            continue;
        }

        /* Calculate relative path */
        const char *relative = line;
        if (0 == strncmp(line, git_base_path, base_len)) {
            relative = line + base_len;
            while ('/' == *relative) {
                relative++;
            }
        }

        /* Get metadata and add to list */
        struct repo_info info = {0};
        if (get_repo_metadata(line, relative, &info)) {
            if (!add_repo_to_list(list, &info)) {
                pclose(fp);
                return 0;
            }
        }
    }

    pclose(fp);
    return 1;
}

static int32_t
get_directory_readme_snippet(const char *dir_path, char *snippet, size_t size) {
    snippet[0] = '\0';

    /* Try README.md first, then README */
    const char *readme_names[] = {"README.md", "README", NULL};

    for (int i = 0; readme_names[i]; i++) {
        char readme_path[PATH_MAX];
        snprintf(readme_path, sizeof(readme_path), "%s/%s", dir_path, readme_names[i]);

        FILE *fp = fopen(readme_path, "r");
        if (fp) {
            if (fgets(snippet, size, fp)) {
                /* Remove trailing newline */
                size_t len = strlen(snippet);
                if (len > 0 && '\n' == snippet[len - 1]) {
                    snippet[len - 1] = '\0';
                }
                /* Skip markdown heading markers */
                char *text = snippet;
                while ('#' == *text || ' ' == *text) {
                    text++;
                }
                if (text != snippet) {
                    memmove(snippet, text, strlen(text) + 1);
                }
            }
            fclose(fp);
            return 1;
        }
    }

    return 0;
}

static int32_t
scan_directory_contents(const char *git_base_path, const char *dir_path,
                       struct dir_list *dirs, struct repo_list *repos) {
    char full_dir_path[PATH_MAX];

    /* Construct full directory path */
    if (dir_path && dir_path[0]) {
        snprintf(full_dir_path, sizeof(full_dir_path), "%s/%s", git_base_path, dir_path);
    } else {
        snprintf(full_dir_path, sizeof(full_dir_path), "%s", git_base_path);
    }

    /* List immediate subdirectories */
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, sizeof(cmd), "find '%s' -maxdepth 1 -type d ! -path '%s' 2>/dev/null",
             full_dir_path, full_dir_path);

    FILE *fp = popen(cmd, "r");
    if (!fp) {
        return 0;
    }

    char line[PATH_MAX];
    size_t base_len = strlen(git_base_path);

    while (fgets(line, sizeof(line), fp)) {
        /* Remove trailing newline */
        size_t len = strlen(line);
        if (len > 0 && '\n' == line[len - 1]) {
            line[len - 1] = '\0';
            len--;
        }

        if (0 == len) {
            continue;
        }

        /* Skip hidden directories */
        char *basename = strrchr(line, '/');
        if (basename && '.' == basename[1]) {
            continue;
        }

        /* Check if it's a git repository */
        if (is_git_repository(line)) {
            /* Calculate relative path */
            const char *relative = line;
            if (0 == strncmp(line, git_base_path, base_len)) {
                relative = line + base_len;
                while ('/' == *relative) {
                    relative++;
                }
            }

            /* Get metadata and add to repos list */
            struct repo_info info = {0};
            if (get_repo_metadata(line, relative, &info)) {
                if (!add_repo_to_list(repos, &info)) {
                    pclose(fp);
                    return 0;
                }
            }
        } else {
            /* It's a directory - add to dirs list */
            struct dir_entry entry = {0};

            if (basename) {
                snprintf(entry.name, sizeof(entry.name), "%s", basename + 1);
            } else {
                snprintf(entry.name, sizeof(entry.name), "%s", line);
            }

            snprintf(entry.path, sizeof(entry.path), "%s", line);
            entry.is_directory = 1;

            /* Try to get README snippet */
            get_directory_readme_snippet(line, entry.readme_snippet, sizeof(entry.readme_snippet));

            if (!add_dir_to_list(dirs, &entry)) {
                pclose(fp);
                return 0;
            }
        }
    }

    pclose(fp);
    return 1;
}

static void
generate_directory_index_header(FILE *out, const char *dir_path) {
    fprintf(out, "<!DOCTYPE html>\n");
    fprintf(out, "<html>\n<head>\n");
    fprintf(out, "<meta charset=\"utf-8\">\n");
    fprintf(out, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n");

    if (dir_path && dir_path[0]) {
        fprintf(out, "<title>Git Repositories - %s</title>\n", dir_path);
    } else {
        fprintf(out, "<title>Git Repositories</title>\n");
    }

    fprintf(out, "<style>\n");
    fprintf(out, "body{font-family:monospace;max-width:900px;margin:40px auto;padding:0 20px;line-height:1.6;}\n");
    fprintf(out, "a{color:#00f;text-decoration:none;}\n");
    fprintf(out, "a:hover{text-decoration:underline;}\n");
    fprintf(out, "table{border-collapse:collapse;width:100%%;}\n");
    fprintf(out, "th,td{text-align:left;padding:8px 12px;border-bottom:1px solid #ddd;}\n");
    fprintf(out, "th{font-weight:bold;}\n");
    fprintf(out, "pre{background:#f5f5f5;padding:10px;overflow-x:auto;}\n");
    fprintf(out, "code{background:#f5f5f5;padding:2px 4px;}\n");
    fprintf(out, ".header{border-bottom:2px solid #000;margin-bottom:20px;padding-bottom:10px;}\n");
    fprintf(out, ".footer{margin-top:40px;padding-top:20px;border-top:1px solid #ddd;font-size:0.9em;color:#666;}\n");
    fprintf(out, "</style>\n");
    fprintf(out, "</head>\n<body>\n");

    /* Generate breadcrumb header */
    fprintf(out, "<div class=\"header\"><h1>");

    if (dir_path && dir_path[0]) {
        /* Root link */
        fprintf(out, "<a href=\"");

        /* Calculate relative path back to root */
        /* Count directory segments, not slashes (e.g., "user" = 1, "user/team" = 2) */
        int32_t depth = 1;  /* Start at 1 for first directory */
        for (const char *p = dir_path; *p; p++) {
            if ('/' == *p) depth++;
        }
        for (int32_t i = 0; i < depth; i++) {
            fprintf(out, "../");
        }
        fprintf(out, "\">git</a>");

        /* Parse path and create breadcrumb links */
        char path_copy[PATH_MAX];
        snprintf(path_copy, sizeof(path_copy), "%s", dir_path);

        char *token = strtok(path_copy, "/");
        char accumulated_path[PATH_MAX] = {0};

        while (token) {
            fprintf(out, " / ");

            /* Build accumulated path for this segment */
            if (accumulated_path[0]) {
                strncat(accumulated_path, "/", sizeof(accumulated_path) - strlen(accumulated_path) - 1);
            }
            strncat(accumulated_path, token, sizeof(accumulated_path) - strlen(accumulated_path) - 1);

            /* Check if this is the last segment */
            char *remaining = strtok(NULL, "/");

            if (remaining) {
                /* Not last segment - make it a link */
                fprintf(out, "<a href=\"");

                /* Calculate relative path to this segment */
                int32_t remaining_depth = 1;
                for (char *p = remaining; *p; p++) {
                    if ('/' == *p) remaining_depth++;
                }
                for (int32_t i = 0; i < remaining_depth; i++) {
                    fprintf(out, "../");
                }

                fprintf(out, "\">");
                html_escape(out, token);
                fprintf(out, "</a>");

                token = remaining;
            } else {
                /* Last segment - no link */
                html_escape(out, token);
                token = NULL;
            }
        }
    } else {
        fprintf(out, "git repositories");
    }

    fprintf(out, "</h1></div>\n");
}

static int32_t
generate_directory_index(const char *output_dir, const char *git_base_path,
                        const char *relative_dir_path) {
    struct dir_list dirs;
    struct repo_list repos;

    init_dir_list(&dirs);
    init_repo_list(&repos);

    /* Scan directory contents */
    if (!scan_directory_contents(git_base_path, relative_dir_path, &dirs, &repos)) {
        free_dir_list(&dirs);
        free_repo_list(&repos);
        return 0;
    }

    /* Build output path */
    char index_path[PATH_MAX];
    if (relative_dir_path && relative_dir_path[0]) {
        snprintf(index_path, sizeof(index_path), "%s/%s/index.html",
                 output_dir, relative_dir_path);
    } else {
        snprintf(index_path, sizeof(index_path), "%s/index.html", output_dir);
    }

    /* Ensure parent directory exists */
    char dir_path[PATH_MAX];
    snprintf(dir_path, sizeof(dir_path), "%s", index_path);
    char *last_slash = strrchr(dir_path, '/');
    if (last_slash) {
        *last_slash = '\0';
        if (!ensure_directory(dir_path)) {
            free_dir_list(&dirs);
            free_repo_list(&repos);
            return 0;
        }
    }

    /* Open output file */
    FILE *out = fopen(index_path, "w");
    if (!out) {
        free_dir_list(&dirs);
        free_repo_list(&repos);
        return 0;
    }

    /* Generate header */
    generate_directory_index_header(out, relative_dir_path);

    /* Generate combined table with directories and repositories */
    int32_t show_table = (dirs.count > 0) || (repos.count > 0) || (relative_dir_path && relative_dir_path[0]);

    if (show_table) {
        fprintf(out, "<table>\n");
        fprintf(out, "<thead><tr><th>name</th><th>description</th><th>last update</th><th>clone</th></tr></thead>\n");
        fprintf(out, "<tbody>\n");

        /* Add parent directory link if not root */
        if (relative_dir_path && relative_dir_path[0]) {
            fprintf(out, "<tr><td><a href=\"../\">../</a></td><td></td><td></td><td></td></tr>\n");
        }

        /* List subdirectories */
        for (size_t i = 0; i < dirs.count; i++) {
            fprintf(out, "<tr><td><a href=\"");
            html_escape(out, dirs.entries[i].name);
            fprintf(out, "/\">");
            html_escape(out, dirs.entries[i].name);
            fprintf(out, "/</a></td><td>");
            if (dirs.entries[i].readme_snippet[0]) {
                html_escape(out, dirs.entries[i].readme_snippet);
            }
            fprintf(out, "</td><td></td><td></td></tr>\n");
        }

        /* List repositories */
        for (size_t i = 0; i < repos.count; i++) {
            /* Extract just the basename for display */
            const char *basename = strrchr(repos.repos[i].name, '/');
            if (basename) {
                basename++;
            } else {
                basename = repos.repos[i].name;
            }

            fprintf(out, "<tr><td><a href=\"");
            html_escape(out, basename);
            fprintf(out, "/\">");
            html_escape(out, basename);
            fprintf(out, "</a></td><td>");
            html_escape(out, repos.repos[i].description);
            fprintf(out, "</td><td>");
            html_escape(out, repos.repos[i].last_commit_date);
            fprintf(out, "</td><td><code>git clone git@HOST:");
            html_escape(out, repos.repos[i].relative_path);
            fprintf(out, "</code></td></tr>\n");
        }

        fprintf(out, "</tbody>\n");
        fprintf(out, "</table>\n");
    }

    /* Generate footer */
    generate_html_footer(out);

    fclose(out);
    free_dir_list(&dirs);
    free_repo_list(&repos);

    return 1;
}

static int32_t
regenerate_parent_indexes(const char *output_dir, const char *git_base_path,
                         const char *repo_relative_path) {
    char parent_path[PATH_MAX];
    char *parents[64];  /* Max directory depth */
    int32_t parent_count = 0;

    /* Extract parent directories from repo path */
    snprintf(parent_path, sizeof(parent_path), "%s", repo_relative_path);

    /* Walk up the directory tree */
    while (1) {
        char *last_slash = strrchr(parent_path, '/');
        if (!last_slash) {
            /* Reached root */
            break;
        }

        *last_slash = '\0';  /* Truncate at last slash */

        /* Store this parent path */
        if (parent_count < 64) {
            parents[parent_count] = strdup(parent_path);
            if (!parents[parent_count]) {
                /* Free previously allocated paths */
                for (int32_t i = 0; i < parent_count; i++) {
                    free(parents[i]);
                }
                return 0;
            }
            parent_count++;
        }
    }

    /* Regenerate indexes from deepest to shallowest (reverse order) */
    for (int32_t i = parent_count - 1; i >= 0; i--) {
        if (!generate_directory_index(output_dir, git_base_path, parents[i])) {
            /* Free all allocated paths */
            for (int32_t j = 0; j < parent_count; j++) {
                free(parents[j]);
            }
            return 0;
        }
        free(parents[i]);
    }

    /* Always regenerate root index */
    if (!generate_directory_index(output_dir, git_base_path, "")) {
        return 0;
    }

    return 1;
}

static int32_t
generate_repo_page(const char *output_dir, const char *repo_name, const char *git_base_path) {
    char repo_path[PATH_MAX];
    char repo_output_dir[PATH_MAX];

    snprintf(repo_path, sizeof(repo_path), "%s/%s", git_base_path, repo_name);
    snprintf(repo_output_dir, sizeof(repo_output_dir), "%s/%s", output_dir, repo_name);

    if (!ensure_directory(repo_output_dir)) {
        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", repo_output_dir);
        return 0;
    }

    /* Get default branch - use the first existing branch */
    char branch_cmd[MAX_CMD_LEN];
    snprintf(branch_cmd, sizeof(branch_cmd),
             "cd '%s' && git for-each-ref --format='%%(refname:short)' refs/heads/ 2>/dev/null | head -n 1",
             repo_path);

    FILE *branch_fp = popen(branch_cmd, "r");
    char default_branch[256] = {0};
    if (branch_fp) {
        if (fgets(default_branch, sizeof(default_branch), branch_fp)) {
            size_t len = strlen(default_branch);
            if (len > 0 && '\n' == default_branch[len - 1]) {
                default_branch[len - 1] = '\0';
            }
        }
        pclose(branch_fp);
    }

    if (0 == default_branch[0]) {
        dprintf(STDERR_FILENO, "Error: No branches found in repository\n");
        return 0;
    }

    /* Create redirect index at repo root */
    char repo_index[PATH_MAX];
    snprintf(repo_index, sizeof(repo_index), "%s/index.html", repo_output_dir);

    FILE *redirect = fopen(repo_index, "w");
    if (!redirect) {
        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", repo_index);
        return 0;
    }

    fprintf(redirect, "<!DOCTYPE html>\n");
    fprintf(redirect, "<html>\n<head>\n");
    fprintf(redirect, "<meta charset=\"utf-8\">\n");
    fprintf(redirect, "<meta http-equiv=\"refresh\" content=\"0; url=branch/%s/tree/\">\n", default_branch);
    fprintf(redirect, "<title>Redirecting to %s</title>\n", default_branch);
    fprintf(redirect, "</head>\n<body>\n");
    fprintf(redirect, "<p>Redirecting to <a href=\"branch/%s/tree/\">%s</a>...</p>\n",
            default_branch, default_branch);
    fprintf(redirect, "</body>\n</html>\n");
    fclose(redirect);

    /* Create branch directory structure */
    char branch_output_dir[PATH_MAX];
    char branch_history_dir[PATH_MAX];
    char branch_tree_dir[PATH_MAX];

    snprintf(branch_output_dir, sizeof(branch_output_dir), "%s/branch/%s", repo_output_dir, default_branch);
    snprintf(branch_history_dir, sizeof(branch_history_dir), "%s/history", branch_output_dir);
    snprintf(branch_tree_dir, sizeof(branch_tree_dir), "%s/tree", branch_output_dir);

    if (!ensure_directory(branch_history_dir)) {
        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", branch_history_dir);
        return 0;
    }

    if (!ensure_directory(branch_tree_dir)) {
        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", branch_tree_dir);
        return 0;
    }

    /* Generate branch history page */
    char branch_history_index[PATH_MAX];
    snprintf(branch_history_index, sizeof(branch_history_index), "%s/index.html", branch_history_dir);

    FILE *history_out = fopen(branch_history_index, "w");
    if (!history_out) {
        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", branch_history_index);
        return 0;
    }

    if (!generate_branch_history_page(history_out, repo_path, repo_name, default_branch)) {
        fclose(history_out);
        return 0;
    }

    fclose(history_out);

    /* Generate branch tree page */
    char branch_tree_index[PATH_MAX];
    snprintf(branch_tree_index, sizeof(branch_tree_index), "%s/index.html", branch_tree_dir);

    FILE *tree_out = fopen(branch_tree_index, "w");
    if (!tree_out) {
        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", branch_tree_index);
        return 0;
    }

    if (!generate_branch_tree_page(tree_out, repo_path, repo_name, default_branch)) {
        fclose(tree_out);
        return 0;
    }

    fclose(tree_out);

    /* Create redirect at branch root */
    char branch_redirect_path[PATH_MAX];
    snprintf(branch_redirect_path, sizeof(branch_redirect_path), "%s/index.html", branch_output_dir);
    FILE *branch_redirect = fopen(branch_redirect_path, "w");
    if (branch_redirect) {
        fprintf(branch_redirect, "<!DOCTYPE html>\n");
        fprintf(branch_redirect, "<html>\n<head>\n");
        fprintf(branch_redirect, "<meta charset=\"utf-8\">\n");
        fprintf(branch_redirect, "<meta http-equiv=\"refresh\" content=\"0; url=tree/\">\n");
        fprintf(branch_redirect, "<title>Redirecting to tree</title>\n");
        fprintf(branch_redirect, "</head>\n<body>\n");
        fprintf(branch_redirect, "<p>Redirecting to <a href=\"tree/\">tree</a>...</p>\n");
        fprintf(branch_redirect, "</body>\n</html>\n");
        fclose(branch_redirect);
    }

    /* Generate tree path pages recursively for branch */
    generate_tree_pages_recursive(branch_tree_dir, repo_path, repo_name, default_branch, "", default_branch);

    /* Generate commit pages for recent commits */
    char commit_cmd[MAX_CMD_LEN];
    snprintf(commit_cmd, sizeof(commit_cmd),
             "cd '%s' && git log --pretty=format:'%%h' -n 10 %s 2>/dev/null",
             repo_path, default_branch);

    FILE *commit_fp = popen(commit_cmd, "r");
    if (commit_fp) {
        char commit_hash[MAX_LINE_LEN];
        while (fgets(commit_hash, sizeof(commit_hash), commit_fp)) {
            /* Remove trailing newline */
            size_t len = strlen(commit_hash);
            if (len > 0 && '\n' == commit_hash[len - 1]) {
                commit_hash[len - 1] = '\0';
            }

            if (len > 0) {
                /* Create commit subdirectories */
                char commit_base_dir[PATH_MAX];
                char commit_summary_dir[PATH_MAX];
                char commit_history_dir[PATH_MAX];
                char commit_tree_dir[PATH_MAX];
                char redirect_path[PATH_MAX];

                snprintf(commit_base_dir, sizeof(commit_base_dir), "%s/commit/%s", repo_output_dir, commit_hash);
                snprintf(commit_summary_dir, sizeof(commit_summary_dir), "%s/summary", commit_base_dir);
                snprintf(commit_history_dir, sizeof(commit_history_dir), "%s/history", commit_base_dir);
                snprintf(commit_tree_dir, sizeof(commit_tree_dir), "%s/tree", commit_base_dir);

                if (!ensure_directory(commit_summary_dir) ||
                    !ensure_directory(commit_history_dir) ||
                    !ensure_directory(commit_tree_dir)) {
                    continue;
                }

                /* Create redirect at commit root */
                snprintf(redirect_path, sizeof(redirect_path), "%s/index.html", commit_base_dir);
                FILE *commit_redirect = fopen(redirect_path, "w");
                if (commit_redirect) {
                    fprintf(commit_redirect, "<!DOCTYPE html>\n");
                    fprintf(commit_redirect, "<html>\n<head>\n");
                    fprintf(commit_redirect, "<meta charset=\"utf-8\">\n");
                    fprintf(commit_redirect, "<meta http-equiv=\"refresh\" content=\"0; url=summary/\">\n");
                    fprintf(commit_redirect, "<title>Redirecting to summary</title>\n");
                    fprintf(commit_redirect, "</head>\n<body>\n");
                    fprintf(commit_redirect, "<p>Redirecting to <a href=\"summary/\">summary</a>...</p>\n");
                    fprintf(commit_redirect, "</body>\n</html>\n");
                    fclose(commit_redirect);
                }

                /* Generate summary page */
                char summary_index[PATH_MAX];
                snprintf(summary_index, sizeof(summary_index), "%s/index.html", commit_summary_dir);
                FILE *summary_out = fopen(summary_index, "w");
                if (summary_out) {
                    generate_commit_summary_page(summary_out, repo_path, repo_name, commit_hash);
                    fclose(summary_out);
                }

                /* Generate history page */
                char history_index[PATH_MAX];
                snprintf(history_index, sizeof(history_index), "%s/index.html", commit_history_dir);
                FILE *history_out = fopen(history_index, "w");
                if (history_out) {
                    generate_commit_history_page(history_out, repo_path, repo_name, commit_hash);
                    fclose(history_out);
                }

                /* Generate tree page */
                char tree_index[PATH_MAX];
                snprintf(tree_index, sizeof(tree_index), "%s/index.html", commit_tree_dir);
                FILE *tree_out = fopen(tree_index, "w");
                if (tree_out) {
                    generate_commit_tree_page(tree_out, repo_path, repo_name, commit_hash);
                    fclose(tree_out);

                    /* Generate tree path pages recursively for commit */
                    generate_tree_pages_recursive(commit_tree_dir, repo_path, repo_name, commit_hash, "", commit_hash);
                }
            }
        }
        pclose(commit_fp);
    }

    return 1;
}

static void
extract_unique_directories(struct repo_list *repos, struct dir_list *dirs) {
    /* Extract all unique parent directories from repo paths */
    for (size_t i = 0; i < repos->count; i++) {
        char path[PATH_MAX];
        snprintf(path, sizeof(path), "%s", repos->repos[i].relative_path);

        /* Walk up the directory tree */
        while (1) {
            char *last_slash = strrchr(path, '/');
            if (!last_slash) {
                break;
            }

            *last_slash = '\0';  /* Truncate at last slash */

            /* Check if this directory is already in the list */
            int32_t found = 0;
            for (size_t j = 0; j < dirs->count; j++) {
                if (0 == strcmp(dirs->entries[j].name, path)) {
                    found = 1;
                    break;
                }
            }

            if (!found) {
                /* Add this directory to the list */
                struct dir_entry entry = {0};
                snprintf(entry.name, sizeof(entry.name), "%s", path);
                snprintf(entry.path, sizeof(entry.path), "%s", path);
                entry.is_directory = 1;
                add_dir_to_list(dirs, &entry);
            }
        }
    }
}

static int32_t
generate_all_repositories(const char *output_dir, const char *git_base_path) {
    struct repo_list repos;
    struct dir_list dirs;

    init_repo_list(&repos);
    init_dir_list(&dirs);

    /* Find all repositories */
    if (!find_all_repositories(git_base_path, &repos)) {
        free_repo_list(&repos);
        free_dir_list(&dirs);
        dprintf(STDERR_FILENO, "Error: Failed to scan repositories\n");
        return 0;
    }

    dprintf(STDOUT_FILENO, "Found %zu repositories\n", repos.count);

    /* Generate pages for each repository */
    for (size_t i = 0; i < repos.count; i++) {
        dprintf(STDOUT_FILENO, "Generating %s...\n", repos.repos[i].relative_path);

        if (!generate_repo_page(output_dir, repos.repos[i].relative_path, git_base_path)) {
            dprintf(STDERR_FILENO, "Warning: Failed to generate pages for %s\n",
                    repos.repos[i].relative_path);
        }
    }

    /* Extract unique directories from all repo paths */
    extract_unique_directories(&repos, &dirs);

    /* Sort directories by depth (deepest first) to ensure proper ordering */
    /* Simple bubble sort - good enough for typical directory structures */
    for (size_t i = 0; i < dirs.count; i++) {
        for (size_t j = i + 1; j < dirs.count; j++) {
            /* Count slashes to determine depth */
            int32_t depth_i = 0, depth_j = 0;
            for (char *p = dirs.entries[i].name; *p; p++) {
                if ('/' == *p) depth_i++;
            }
            for (char *p = dirs.entries[j].name; *p; p++) {
                if ('/' == *p) depth_j++;
            }

            /* Sort deepest first */
            if (depth_j > depth_i) {
                struct dir_entry tmp = dirs.entries[i];
                dirs.entries[i] = dirs.entries[j];
                dirs.entries[j] = tmp;
            }
        }
    }

    /* Generate directory indexes from deepest to shallowest */
    for (size_t i = 0; i < dirs.count; i++) {
        if (!generate_directory_index(output_dir, git_base_path, dirs.entries[i].name)) {
            dprintf(STDERR_FILENO, "Warning: Failed to generate index for %s\n",
                    dirs.entries[i].name);
        }
    }

    /* Generate root index */
    if (!generate_directory_index(output_dir, git_base_path, "")) {
        free_repo_list(&repos);
        free_dir_list(&dirs);
        dprintf(STDERR_FILENO, "Error: Failed to generate root index\n");
        return 0;
    }

    free_repo_list(&repos);
    free_dir_list(&dirs);

    dprintf(STDOUT_FILENO, "Done!\n");
    return 1;
}

int32_t
main(int32_t argc, char *argv[]) {
    const char *git_base_path = DEFAULT_GIT_BASE_PATH;
    int32_t opt;

    while (-1 != (opt = getopt(argc, argv, "D:"))) {
        switch (opt) {
            case 'D':
                git_base_path = optarg;
                break;
            default:
                dprintf(STDERR_FILENO, "Usage: %s [-D git-dir] <output-dir> [repo]\n", argv[0]);
                dprintf(STDERR_FILENO, "Examples:\n");
                dprintf(STDERR_FILENO, "  Single repo:  %s -D /srv/git /var/www/git myproject.git\n", argv[0]);
                dprintf(STDERR_FILENO, "  All repos:    %s -D /srv/git /var/www/git\n", argv[0]);
                return EXIT_FAILURE;
        }
    }

    if (optind + 1 > argc) {
        dprintf(STDERR_FILENO, "Usage: %s [-D git-dir] <output-dir> [repo]\n", argv[0]);
        dprintf(STDERR_FILENO, "Examples:\n");
        dprintf(STDERR_FILENO, "  Single repo:  %s -D /srv/git /var/www/git myproject.git\n", argv[0]);
        dprintf(STDERR_FILENO, "  All repos:    %s -D /srv/git /var/www/git\n", argv[0]);
        return EXIT_FAILURE;
    }

    const char *output_dir = argv[optind];
    const char *repo_name = (optind + 1 < argc) ? argv[optind + 1] : NULL;

    if (!ensure_directory(output_dir)) {
        dprintf(STDERR_FILENO, "Error: Cannot create output directory %s\n", output_dir);
        return EXIT_FAILURE;
    }

    if (repo_name) {
        /* Single-repo mode */
        char repo_path[PATH_MAX];
        snprintf(repo_path, sizeof(repo_path), "%s/%s", git_base_path, repo_name);

        if (!is_git_repository(repo_path)) {
            dprintf(STDERR_FILENO, "Error: %s is not a git repository\n", repo_name);
            return EXIT_FAILURE;
        }

        if (!generate_repo_page(output_dir, repo_name, git_base_path)) {
            return EXIT_FAILURE;
        }

        if (!regenerate_parent_indexes(output_dir, git_base_path, repo_name)) {
            return EXIT_FAILURE;
        }
    } else {
        /* Multi-repo mode */
        if (!generate_all_repositories(output_dir, git_base_path)) {
            return EXIT_FAILURE;
        }
    }

    return EXIT_SUCCESS;
}