#include #include #include #include #include #include #include #include #include #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, "<"); break; case '>': fprintf(out, ">"); break; case '&': fprintf(out, "&"); break; case '"': fprintf(out, """); 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, "\n"); fprintf(out, "\n\n"); fprintf(out, "\n"); fprintf(out, "\n"); fprintf(out, "%s\n", title); fprintf(out, "\n"); fprintf(out, "\n\n"); /* Header with breadcrumb navigation */ fprintf(out, "

"); /* Count slashes in repo_name to determine directory depth */ int32_t repo_depth = 0; for (const char *p = repo_name; *p; p++) { if ('/' == *p) repo_depth++; } /* Root "git" link - go up through branch/commit structure AND repo path */ fprintf(out, "git"); /* 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, "/"); while (token) { fprintf(out, " / "); /* Calculate how many more segments remain */ char *remaining = strtok(NULL, "/"); int32_t remaining_segments = 0; if (remaining) { remaining_segments = 1; for (char *p = remaining; *p; p++) { if ('/' == *p) remaining_segments++; } } /* Create link */ fprintf(out, ""); html_escape(out, token); fprintf(out, ""); token = remaining; } } if (suffix && suffix[0]) { fprintf(out, " - %s", suffix); } fprintf(out, "

\n"); } static void generate_html_footer(FILE *out) { fprintf(out, "
generated by marrow-static
\n"); fprintf(out, "\n\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); /* Navigation */ fprintf(out, "

Summary | History | Files

\n"); fprintf(out, "

commit %s

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\n", date); fprintf(out, "
author"); html_escape(out, author); if (email) { fprintf(out, " <"); html_escape(out, email); fprintf(out, ">"); } fprintf(out, "
date%s
\n"); fprintf(out, "

message

\n
");
            html_escape(out, subject);
            fprintf(out, "\n");

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

diff

\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, "
");
        while (fgets(line, sizeof(line), fp)) {
            html_escape(out, line);
        }
        fprintf(out, "
\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); /* Navigation */ fprintf(out, "

Summary | History | Files

\n"); fprintf(out, "

commit %s

\n", commit_hash); /* Recent commits from this point */ fprintf(out, "

history

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\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, "\n"); } } fprintf(out, "\n
hashauthordatemessage
%s", hash, hash); html_escape(out, author); fprintf(out, "%s", date); html_escape(out, msg); fprintf(out, "
\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); /* Navigation */ fprintf(out, "

Summary | History | Files

\n"); fprintf(out, "

commit %s

\n", commit_hash); /* File tree */ fprintf(out, "

files

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); char line[MAX_LINE_LEN]; while (fgets(line, sizeof(line), fp)) { /* git ls-tree -l format: mode type hash sizename */ (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, ""); /* 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, ""); } else { fprintf(out, ""); } if (commit_fp) pclose(commit_fp); } else { fprintf(out, ""); if (commit_fp) pclose(commit_fp); } fprintf(out, ""); fprintf(out, "\n"); } } fprintf(out, "\n
typenamecommitsize
%s", type_display); if (0 == strcmp(type, "tree")) { fprintf(out, "", name); html_escape(out, name); fprintf(out, "/"); } else { fprintf(out, "", name); html_escape(out, name); fprintf(out, ""); } fprintf(out, ""); if (date) { fprintf(out, "(%s)", date); } fprintf(out, "%s ", hash, hash); html_escape(out, msg); fprintf(out, "--"); if (size_str && 0 != strcmp(type, "tree")) { fprintf(out, "%s", size_str); } else { fprintf(out, "-"); } fprintf(out, "
\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); } /* Navigation */ fprintf(out, "

Files | History

\n"); /* Clone URL */ fprintf(out, "

clone

\n"); fprintf(out, "
git clone git@host:%s
\n", repo_name); /* Recent commits */ fprintf(out, "

recent commits

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\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, "\n"); } } fprintf(out, "\n
hashauthordatemessage
%s", hash, hash); html_escape(out, author); fprintf(out, "%s", date); html_escape(out, msg); fprintf(out, "
\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); } /* Navigation */ fprintf(out, "

Files | History

\n"); /* Clone URL */ fprintf(out, "

clone

\n"); fprintf(out, "
git clone git@host:%s
\n", repo_name); /* Get README for later display */ char *readme = NULL; get_git_readme(repo_path, &readme); /* File tree */ fprintf(out, "

files

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); char line[MAX_LINE_LEN]; while (fgets(line, sizeof(line), fp)) { /* git ls-tree -l format: mode type hash sizename */ (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, ""); /* 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, ""); } else { fprintf(out, ""); } if (commit_fp) pclose(commit_fp); } else { fprintf(out, ""); if (commit_fp) pclose(commit_fp); } fprintf(out, ""); fprintf(out, "\n"); } } fprintf(out, "\n
typenamecommitsize
%s", type_display); if (0 == strcmp(type, "tree")) { fprintf(out, "", name); html_escape(out, name); fprintf(out, "/"); } else { fprintf(out, "", name); html_escape(out, name); fprintf(out, ""); } fprintf(out, ""); if (date) { fprintf(out, "(%s)", date); } fprintf(out, "%s ", hash, hash); html_escape(out, msg); fprintf(out, "--"); if (size_str && 0 != strcmp(type, "tree")) { fprintf(out, "%s", size_str); } else { fprintf(out, "-"); } fprintf(out, "
\n"); pclose(fp); } /* README - display after file tree */ if (readme) { fprintf(out, "

readme

\n"); fprintf(out, "
");
        html_escape(out, readme);
        fprintf(out, "
\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, "

"); if (commit_date[0]) { fprintf(out, "(%s)", commit_date); } fprintf(out, "commit %s: ", commit_hash); html_escape(out, commit_subject); fprintf(out, "

\n"); } /* Navigation */ fprintf(out, "

Back

\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, "

%s/

\n", tree_path); fprintf(out, "

files

\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, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); /* Add parent directory link */ fprintf(out, "\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, ""); /* 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, ""); } else { fprintf(out, ""); } if (c_fp) pclose(c_fp); } else { fprintf(out, ""); if (c_fp) pclose(c_fp); } fprintf(out, ""); fprintf(out, "\n"); } } fprintf(out, "\n
typenamecommitsize
dir..--
%s", type_display); if (0 == strcmp(type, "tree")) { fprintf(out, "", name); html_escape(out, name); fprintf(out, "/"); } else { fprintf(out, "", name); html_escape(out, name); fprintf(out, ""); } fprintf(out, ""); if (date) { fprintf(out, "(%s)", date); } fprintf(out, "%s ", hash, hash); html_escape(out, msg); fprintf(out, "--"); if (size_str && 0 != strcmp(type, "tree")) { fprintf(out, "%s", size_str); } else { fprintf(out, "-"); } fprintf(out, "
\n"); pclose(fp); } } else if (0 == strcmp(obj_type, "blob")) { /* File - show contents */ fprintf(out, "

%s

\n", tree_path); fprintf(out, "

raw

\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, "
");
            char line[MAX_LINE_LEN];
            while (fgets(line, sizeof(line), fp)) {
                html_escape(out, line);
            }
            fprintf(out, "
\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, "\n"); fprintf(out, "\n\n"); fprintf(out, "\n"); fprintf(out, "\n"); if (dir_path && dir_path[0]) { fprintf(out, "Git Repositories - %s\n", dir_path); } else { fprintf(out, "Git Repositories\n"); } fprintf(out, "\n"); fprintf(out, "\n\n"); /* Generate breadcrumb header */ fprintf(out, "

"); if (dir_path && dir_path[0]) { /* Root link */ fprintf(out, "git"); /* 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, ""); html_escape(out, token); fprintf(out, ""); token = remaining; } else { /* Last segment - no link */ html_escape(out, token); token = NULL; } } } else { fprintf(out, "git repositories"); } fprintf(out, "

\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 subdirectories table if any exist OR if we need parent link */ int32_t show_dirs_table = (dirs.count > 0) || (relative_dir_path && relative_dir_path[0]); if (show_dirs_table) { fprintf(out, "

directories

\n"); fprintf(out, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); /* Add parent directory link if not root */ if (relative_dir_path && relative_dir_path[0]) { fprintf(out, "\n"); } /* List subdirectories */ for (size_t i = 0; i < dirs.count; i++) { fprintf(out, "\n"); } fprintf(out, "\n"); fprintf(out, "
namedescription
../parent directory
"); html_escape(out, dirs.entries[i].name); fprintf(out, "/"); if (dirs.entries[i].readme_snippet[0]) { html_escape(out, dirs.entries[i].readme_snippet); } fprintf(out, "
\n\n"); } /* Generate repositories table if any exist */ if (repos.count > 0) { fprintf(out, "

repositories

\n"); fprintf(out, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); 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, "\n"); } fprintf(out, "\n"); fprintf(out, "
namedescriptionlast updateclone
"); html_escape(out, basename); fprintf(out, ""); html_escape(out, repos.repos[i].description); fprintf(out, ""); html_escape(out, repos.repos[i].last_commit_date); fprintf(out, "git clone git@HOST:"); html_escape(out, repos.repos[i].relative_path); fprintf(out, "
\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, "\n"); fprintf(redirect, "\n\n"); fprintf(redirect, "\n"); fprintf(redirect, "\n", default_branch); fprintf(redirect, "Redirecting to %s\n", default_branch); fprintf(redirect, "\n\n"); fprintf(redirect, "

Redirecting to %s...

\n", default_branch, default_branch); fprintf(redirect, "\n\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, "\n"); fprintf(branch_redirect, "\n\n"); fprintf(branch_redirect, "\n"); fprintf(branch_redirect, "\n"); fprintf(branch_redirect, "Redirecting to tree\n"); fprintf(branch_redirect, "\n\n"); fprintf(branch_redirect, "

Redirecting to tree...

\n"); fprintf(branch_redirect, "\n\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, "\n"); fprintf(commit_redirect, "\n\n"); fprintf(commit_redirect, "\n"); fprintf(commit_redirect, "\n"); fprintf(commit_redirect, "Redirecting to summary\n"); fprintf(commit_redirect, "\n\n"); fprintf(commit_redirect, "

Redirecting to summary...

\n"); fprintf(commit_redirect, "\n\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] [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] [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; }