(22 hours ago)commit f64bdd6: marrow-static (directories)
| author | Tanner Stenson <tanner@brickware.sh> |
| date | Wed Jan 7 23:01:12 2026 -0500 |
marrow-static (directories)
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;