git / brickware / marrow.git - f64bdd6

(22 hours ago)commit f64bdd6: marrow-static (directories)

Summary | History | Files

commit f64bdd6

authorTanner Stenson <tanner@brickware.sh>
dateWed Jan 7 23:01:12 2026 -0500

message

marrow-static (directories)

diff

commit f64bdd6a90ee029cc624f80fd8f0cbf26971e064
Author: Tanner Stenson <tanner@brickware.sh>
Date:   Wed Jan 7 23:01:12 2026 -0500

    marrow-static (directories)

diff --git a/marrow-static.c b/marrow-static.c
index b3007e5..72d52aa 100644
--- a/marrow-static.c
+++ b/marrow-static.c
@@ -12,6 +12,101 @@
 #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;
@@ -1012,6 +1107,456 @@ generate_tree_pages_recursive(const char *output_dir, const char *repo_path,
     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");
+
+    fprintf(out, "<div class=\"header\"><h1>");
+    if (dir_path && dir_path[0]) {
+        fprintf(out, "git / ");
+        html_escape(out, dir_path);
+    } 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 subdirectories table if any exist */
+    if (dirs.count > 0) {
+        fprintf(out, "<h2>directories</h2>\n");
+        fprintf(out, "<table>\n");
+        fprintf(out, "<thead><tr><th>name</th><th>description</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>parent directory</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></tr>\n");
+        }
+
+        fprintf(out, "</tbody>\n");
+        fprintf(out, "</table>\n\n");
+    }
+
+    /* Generate repositories table if any exist */
+    if (repos.count > 0) {
+        fprintf(out, "<h2>repositories</h2>\n");
+        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");
+
+        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];
@@ -1228,6 +1773,119 @@ generate_repo_page(const char *output_dir, const char *repo_name, const char *gi
     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;
@@ -1239,36 +1897,52 @@ main(int32_t argc, char *argv[]) {
                 git_base_path = optarg;
                 break;
             default:
-                dprintf(STDERR_FILENO, "Usage: %s [-D git-dir] <output-dir> <repo>\n", argv[0]);
-                dprintf(STDERR_FILENO, "Example: %s -D /srv/git /var/www/git myproject.git\n", argv[0]);
+                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 + 2 > argc) {
-        dprintf(STDERR_FILENO, "Usage: %s [-D git-dir] <output-dir> <repo>\n", argv[0]);
-        dprintf(STDERR_FILENO, "Example: %s -D /srv/git /var/www/git myproject.git\n", argv[0]);
+    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 = argv[optind + 1];
+    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;
     }
 
-    char repo_path[PATH_MAX];
-    snprintf(repo_path, sizeof(repo_path), "%s/%s", git_base_path, repo_name);
+    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 (!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 (!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;