git / brickware / marrow.git - 2cd3640

(25 hours ago)commit 2cd3640: rough version of marrow-static

Summary | History | Files

commit 2cd3640

authorTanner Stenson <tanner@brickware.sh>
dateWed Jan 7 20:02:08 2026 -0500

message

rough version of marrow-static

diff

commit 2cd36406f165f10b13dd95168dbf947c1c7333c9
Author: Tanner Stenson <tanner@brickware.sh>
Date:   Wed Jan 7 20:02:08 2026 -0500

    rough version of marrow-static

diff --git a/marrow-static.c b/marrow-static.c
index e472dad..b3007e5 100644
--- a/marrow-static.c
+++ b/marrow-static.c
@@ -10,7 +10,7 @@
 
 #define MAX_CMD_LEN 4096
 #define MAX_LINE_LEN 2048
-#define GIT_BASE_PATH "/srv/git"
+#define DEFAULT_GIT_BASE_PATH "/srv/git"
 
 static int32_t
 ensure_directory(const char *path) {
@@ -48,7 +48,14 @@ html_escape(FILE *out, const char *str) {
 }
 
 static void
-generate_html_header(FILE *out, const char *title) {
+generate_html_header(FILE *out, const char *repo_name, const char *suffix) {
+    char title[512];
+    if (suffix && suffix[0]) {
+        snprintf(title, sizeof(title), "%s - %s", repo_name, suffix);
+    } else {
+        snprintf(title, sizeof(title), "%s", repo_name);
+    }
+
     fprintf(out, "<!DOCTYPE html>\n");
     fprintf(out, "<html>\n<head>\n");
     fprintf(out, "<meta charset=\"utf-8\">\n");
@@ -67,7 +74,13 @@ generate_html_header(FILE *out, const char *title) {
     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>%s</h1></div>\n", title);
+
+    /* Header with repo name as link */
+    fprintf(out, "<div class=\"header\"><h1><a href=\"../../../\">%s</a>", repo_name);
+    if (suffix && suffix[0]) {
+        fprintf(out, " - %s", suffix);
+    }
+    fprintf(out, "</h1></div>\n");
 }
 
 static void
@@ -126,127 +139,394 @@ get_git_readme(const char *repo_path, char **readme) {
     return 1;
 }
 
-static void
-render_markdown_simple(FILE *out, const char *markdown) {
-    const char *line = markdown;
-    int32_t in_code = 0;
-
-    while (*line) {
-        const char *next = strchr(line, '\n');
-        if (!next) {
-            next = line + strlen(line);
+static int32_t
+generate_commit_summary_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
+    generate_html_header(out, repo_name, commit_hash);
+
+    /* Get commit subject and date for display */
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
+             repo_path, commit_hash);
+
+    FILE *subject_fp = popen(cmd, "r");
+    char commit_subject[512] = {0};
+    char commit_date[128] = {0};
+    if (subject_fp) {
+        char line[MAX_LINE_LEN];
+        if (fgets(line, sizeof(line), subject_fp)) {
+            char *subject = strtok(line, "|");
+            char *date = strtok(NULL, "\n");
+            if (subject) {
+                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
+            }
+            if (date) {
+                snprintf(commit_date, sizeof(commit_date), "%s", date);
+            }
         }
+        pclose(subject_fp);
+    }
+
+    /* Display commit info line */
+    fprintf(out, "<p><small>");
+    if (commit_date[0]) {
+        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
+    }
+    fprintf(out, "commit %s: ", commit_hash);
+    html_escape(out, commit_subject);
+    fprintf(out, "</small></p>\n");
+
+    /* Navigation */
+    fprintf(out, "<p><strong>Summary</strong> | <a href=\"../history/\">History</a> | <a href=\"../tree/\">Files</a></p>\n");
+
+    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);
 
-        size_t line_len = next - line;
+    /* 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;
+    }
 
-        /* Code blocks */
-        if (line_len >= 3 && 0 == strncmp(line, "```", 3)) {
-            if (in_code) {
-                fprintf(out, "</pre>\n");
-                in_code = 0;
-            } else {
-                fprintf(out, "<pre>");
-                in_code = 1;
+    char line[MAX_LINE_LEN];
+    if (fgets(line, sizeof(line), fp)) {
+        (void)strtok(line, "|");  /* full_hash */
+        char *author = strtok(NULL, "|");
+        char *email = strtok(NULL, "|");
+        char *date = strtok(NULL, "|");
+        char *subject = strtok(NULL, "\n");
+
+        if (author && date && subject) {
+            fprintf(out, "<table>\n");
+            fprintf(out, "<tr><td><strong>author</strong></td><td>");
+            html_escape(out, author);
+            if (email) {
+                fprintf(out, " &lt;");
+                html_escape(out, email);
+                fprintf(out, "&gt;");
+            }
+            fprintf(out, "</td></tr>\n");
+            fprintf(out, "<tr><td><strong>date</strong></td><td>%s</td></tr>\n", date);
+            fprintf(out, "</table>\n");
+
+            fprintf(out, "<h3>message</h3>\n<pre>");
+            html_escape(out, subject);
+            fprintf(out, "\n");
+
+            /* Read commit body */
+            while (fgets(line, sizeof(line), fp)) {
+                html_escape(out, line);
             }
-            line = (*next ? next + 1 : next);
-            continue;
+            fprintf(out, "</pre>\n");
         }
+    }
+    pclose(fp);
+
+    /* Show diff */
+    fprintf(out, "<h3>diff</h3>\n");
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git show %s 2>/dev/null",
+             repo_path, commit_hash);
 
-        if (in_code) {
+    fp = popen(cmd, "r");
+    if (fp) {
+        fprintf(out, "<pre>");
+        while (fgets(line, sizeof(line), fp)) {
             html_escape(out, line);
-            if (*next) {
-                fprintf(out, "\n");
-            }
-            line = (*next ? next + 1 : next);
-            continue;
         }
+        fprintf(out, "</pre>\n");
+        pclose(fp);
+    }
+
+    generate_html_footer(out);
+
+    return 1;
+}
+
+static int32_t
+generate_commit_history_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
+    generate_html_header(out, repo_name, commit_hash);
+
+    /* Get commit subject and date for display */
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
+             repo_path, commit_hash);
 
-        /* Headers */
-        if ('#' == *line && line_len > 0) {
-            int32_t level = 0;
-            const char *p = line;
-            while (p < next && '#' == *p && level < 6) {
-                level++;
-                p++;
+    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 (level > 0) {
-                /* Skip space after # */
-                if (p < next && ' ' == *p) {
-                    p++;
-                }
-                fprintf(out, "<h%d>", level + 1);
-                size_t text_len = next - p;
-                for (size_t i = 0; i < text_len; i++) {
-                    html_escape(out, (char[]){p[i], '\0'});
-                }
-                fprintf(out, "</h%d>\n", level + 1);
-                line = (*next ? next + 1 : next);
-                continue;
+            if (date) {
+                snprintf(commit_date, sizeof(commit_date), "%s", date);
             }
         }
+        pclose(subject_fp);
+    }
+
+    /* Display commit info line */
+    fprintf(out, "<p><small>");
+    if (commit_date[0]) {
+        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
+    }
+    fprintf(out, "commit %s: ", commit_hash);
+    html_escape(out, commit_subject);
+    fprintf(out, "</small></p>\n");
+
+    /* Navigation */
+    fprintf(out, "<p><a href=\"../summary/\">Summary</a> | <strong>History</strong> | <a href=\"../tree/\">Files</a></p>\n");
+
+    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);
+
+    /* Recent commits from this point */
+    fprintf(out, "<h3>history</h3>\n");
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log --pretty=format:'%%h|%%an|%%ar|%%s' -n 10 %s 2>/dev/null",
+             repo_path, commit_hash);
+
+    FILE *fp = popen(cmd, "r");
+    if (fp) {
+        fprintf(out, "<table>\n");
+        fprintf(out, "<thead><tr><th>hash</th><th>author</th><th>date</th><th>message</th></tr></thead>\n");
+        fprintf(out, "<tbody>\n");
+
+        char line[MAX_LINE_LEN];
+        while (fgets(line, sizeof(line), fp)) {
+            char *hash = strtok(line, "|");
+            char *author = strtok(NULL, "|");
+            char *date = strtok(NULL, "|");
+            char *msg = strtok(NULL, "|");
 
-        /* Empty lines */
-        if (0 == line_len) {
-            line = (*next ? next + 1 : next);
-            continue;
+            if (hash && author && date && msg) {
+                /* Remove trailing newline from message */
+                size_t msg_len = strlen(msg);
+                if (msg_len > 0 && '\n' == msg[msg_len - 1]) {
+                    msg[msg_len - 1] = '\0';
+                }
+
+                fprintf(out, "<tr><td><a href=\"../../%s/summary/\"><code>%s</code></a></td><td>", hash, hash);
+                html_escape(out, author);
+                fprintf(out, "</td><td>%s</td><td>", date);
+                html_escape(out, msg);
+                fprintf(out, "</td></tr>\n");
+            }
         }
 
-        /* Regular paragraph */
-        fprintf(out, "<p>");
-        for (size_t i = 0; i < line_len; i++) {
-            html_escape(out, (char[]){line[i], '\0'});
+        fprintf(out, "</tbody>\n</table>\n");
+        pclose(fp);
+    }
+
+    generate_html_footer(out);
+
+    return 1;
+}
+
+static int32_t
+generate_commit_tree_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
+    generate_html_header(out, repo_name, commit_hash);
+
+    /* Get commit subject and date for display */
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
+             repo_path, commit_hash);
+
+    FILE *subject_fp = popen(cmd, "r");
+    char commit_subject[512] = {0};
+    char commit_date[128] = {0};
+    if (subject_fp) {
+        char line[MAX_LINE_LEN];
+        if (fgets(line, sizeof(line), subject_fp)) {
+            char *subject = strtok(line, "|");
+            char *date = strtok(NULL, "\n");
+            if (subject) {
+                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
+            }
+            if (date) {
+                snprintf(commit_date, sizeof(commit_date), "%s", date);
+            }
         }
-        fprintf(out, "</p>\n");
+        pclose(subject_fp);
+    }
 
-        line = (*next ? next + 1 : next);
+    /* Display commit info line */
+    fprintf(out, "<p><small>");
+    if (commit_date[0]) {
+        fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
     }
+    fprintf(out, "commit %s: ", commit_hash);
+    html_escape(out, commit_subject);
+    fprintf(out, "</small></p>\n");
 
-    if (in_code) {
-        fprintf(out, "</pre>\n");
+    /* Navigation */
+    fprintf(out, "<p><a href=\"../summary/\">Summary</a> | <a href=\"../history/\">History</a> | <strong>Files</strong></p>\n");
+
+    fprintf(out, "<h2>commit %s</h2>\n", commit_hash);
+
+    /* File tree */
+    fprintf(out, "<h3>files</h3>\n");
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git ls-tree -l %s 2>/dev/null",
+             repo_path, commit_hash);
+
+    FILE *fp = popen(cmd, "r");
+    if (fp) {
+        fprintf(out, "<table>\n");
+        fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
+        fprintf(out, "<tbody>\n");
+
+        char line[MAX_LINE_LEN];
+        while (fgets(line, sizeof(line), fp)) {
+            /* git ls-tree -l format: mode type hash size<tab>name */
+            (void)strtok(line, " ");  /* mode */
+            char *type = strtok(NULL, " ");
+            (void)strtok(NULL, " ");  /* hash */
+            char *size_str = strtok(NULL, "\t");
+            char *name = strtok(NULL, "\n");
+
+            if (type && name) {
+                const char *type_display = "file";
+                if (0 == strcmp(type, "tree")) {
+                    type_display = "dir";
+                }
+
+                fprintf(out, "<tr><td>%s</td><td>", type_display);
+                if (0 == strcmp(type, "tree")) {
+                    fprintf(out, "<a href=\"%s/\">", name);
+                    html_escape(out, name);
+                    fprintf(out, "/</a>");
+                } else {
+                    fprintf(out, "<a href=\"%s/\">", name);
+                    html_escape(out, name);
+                    fprintf(out, "</a>");
+                }
+                fprintf(out, "</td>");
+
+                /* Get last commit for this file */
+                char commit_cmd[MAX_CMD_LEN];
+                snprintf(commit_cmd, sizeof(commit_cmd),
+                         "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
+                         repo_path, commit_hash, name);
+
+                FILE *commit_fp = popen(commit_cmd, "r");
+                char commit_line[MAX_LINE_LEN] = {0};
+                if (commit_fp && fgets(commit_line, sizeof(commit_line), commit_fp)) {
+                    char *hash = strtok(commit_line, "|");
+                    char *msg = strtok(NULL, "|");
+                    char *date = strtok(NULL, "\n");
+
+                    if (hash && msg) {
+                        /* Truncate message if too long */
+                        size_t msg_len = strlen(msg);
+                        const size_t max_msg_len = 45;
+                        if (msg_len > max_msg_len) {
+                            msg[max_msg_len - 3] = '.';
+                            msg[max_msg_len - 2] = '.';
+                            msg[max_msg_len - 1] = '.';
+                            msg[max_msg_len] = '\0';
+                        }
+
+                        fprintf(out, "<td>");
+                        if (date) {
+                            fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
+                        }
+                        fprintf(out, "<a href=\"../../%s/summary/\"><code>%s</code></a> ", hash, hash);
+                        html_escape(out, msg);
+                        fprintf(out, "</td>");
+                    } else {
+                        fprintf(out, "<td>-</td>");
+                    }
+                    if (commit_fp) pclose(commit_fp);
+                } else {
+                    fprintf(out, "<td>-</td>");
+                    if (commit_fp) pclose(commit_fp);
+                }
+
+                fprintf(out, "<td>");
+                if (size_str && 0 != strcmp(type, "tree")) {
+                    fprintf(out, "%s", size_str);
+                } else {
+                    fprintf(out, "-");
+                }
+                fprintf(out, "</td>");
+
+                fprintf(out, "</tr>\n");
+            }
+        }
+
+        fprintf(out, "</tbody>\n</table>\n");
+        pclose(fp);
     }
+
+    generate_html_footer(out);
+
+    return 1;
 }
 
 static int32_t
-generate_repo_page(const char *output_dir, const char *repo_name) {
-    char repo_path[PATH_MAX];
-    char repo_output_dir[PATH_MAX];
-    char repo_index[PATH_MAX];
+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);
 
-    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);
-    snprintf(repo_index, sizeof(repo_index), "%s/index.html", repo_output_dir);
+    /* 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);
 
-    if (!ensure_directory(repo_output_dir)) {
-        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", repo_output_dir);
-        return 0;
+    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);
     }
 
-    FILE *out = fopen(repo_index, "w");
-    if (!out) {
-        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", repo_index);
-        return 0;
+    /* Display commit info line */
+    if (commit_hash[0]) {
+        fprintf(out, "<p><small>");
+        if (commit_date[0]) {
+            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
+        }
+        fprintf(out, "commit %s: ", commit_hash);
+        html_escape(out, commit_subject);
+        fprintf(out, "</small></p>\n");
     }
 
-    generate_html_header(out, repo_name);
+    /* Navigation */
+    fprintf(out, "<p><a href=\"../tree/\">Files</a> | <strong>History</strong></p>\n");
 
     /* Clone URL */
     fprintf(out, "<h2>clone</h2>\n");
     fprintf(out, "<pre>git clone git@host:%s</pre>\n", repo_name);
 
-    /* README */
-    char *readme = NULL;
-    if (get_git_readme(repo_path, &readme)) {
-        fprintf(out, "<h2>readme</h2>\n");
-        render_markdown_simple(out, readme);
-        free(readme);
-    }
-
     /* Recent commits */
     fprintf(out, "<h2>recent commits</h2>\n");
-    char cmd[MAX_CMD_LEN];
     snprintf(cmd, sizeof(cmd),
-             "cd '%s' && git log --pretty=format:'%%h|%%an|%%ar|%%s' -n 10 HEAD 2>/dev/null",
-             repo_path);
+             "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) {
@@ -268,7 +548,7 @@ generate_repo_page(const char *output_dir, const char *repo_name) {
                     msg[msg_len - 1] = '\0';
                 }
 
-                fprintf(out, "<tr><td><code>%s</code></td><td>", hash);
+                fprintf(out, "<tr><td><a href=\"../../../commit/%s/summary/\"><code>%s</code></a></td><td>", hash, hash);
                 html_escape(out, author);
                 fprintf(out, "</td><td>%s</td><td>", date);
                 html_escape(out, msg);
@@ -281,21 +561,698 @@ generate_repo_page(const char *output_dir, const char *repo_name) {
     }
 
     generate_html_footer(out);
-    fclose(out);
+
+    return 1;
+}
+
+static int32_t
+generate_branch_tree_page(FILE *out, const char *repo_path, const char *repo_name, const char *branch_name) {
+    generate_html_header(out, repo_name, branch_name);
+
+    /* Get latest commit for display */
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s 2>/dev/null",
+             repo_path, branch_name);
+
+    FILE *commit_fp = popen(cmd, "r");
+    char commit_hash[64] = {0};
+    char commit_subject[512] = {0};
+    char commit_date[128] = {0};
+    if (commit_fp) {
+        char commit_line[MAX_LINE_LEN];
+        if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
+            char *hash = strtok(commit_line, "|");
+            char *subject = strtok(NULL, "|");
+            char *date = strtok(NULL, "\n");
+            if (hash && subject) {
+                snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
+                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
+            }
+            if (date) {
+                snprintf(commit_date, sizeof(commit_date), "%s", date);
+            }
+        }
+        pclose(commit_fp);
+    }
+
+    /* Display commit info line */
+    if (commit_hash[0]) {
+        fprintf(out, "<p><small>");
+        if (commit_date[0]) {
+            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
+        }
+        fprintf(out, "commit %s: ", commit_hash);
+        html_escape(out, commit_subject);
+        fprintf(out, "</small></p>\n");
+    }
+
+    /* Navigation */
+    fprintf(out, "<p><strong>Files</strong> | <a href=\"../history/\">History</a></p>\n");
+
+    /* Clone URL */
+    fprintf(out, "<h2>clone</h2>\n");
+    fprintf(out, "<pre>git clone git@host:%s</pre>\n", repo_name);
+
+    /* Get README for later display */
+    char *readme = NULL;
+    get_git_readme(repo_path, &readme);
+
+    /* File tree */
+    fprintf(out, "<h2>files</h2>\n");
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git ls-tree -l %s 2>/dev/null",
+             repo_path, branch_name);
+
+    FILE *fp = popen(cmd, "r");
+    if (fp) {
+        fprintf(out, "<table>\n");
+        fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
+        fprintf(out, "<tbody>\n");
+
+        char line[MAX_LINE_LEN];
+        while (fgets(line, sizeof(line), fp)) {
+            /* git ls-tree -l format: mode type hash size<tab>name */
+            (void)strtok(line, " ");  /* mode */
+            char *type = strtok(NULL, " ");
+            (void)strtok(NULL, " ");  /* hash */
+            char *size_str = strtok(NULL, "\t");
+            char *name = strtok(NULL, "\n");
+
+            if (type && name) {
+                const char *type_display = "file";
+                if (0 == strcmp(type, "tree")) {
+                    type_display = "dir";
+                }
+
+                fprintf(out, "<tr><td>%s</td><td>", type_display);
+                if (0 == strcmp(type, "tree")) {
+                    fprintf(out, "<a href=\"%s/\">", name);
+                    html_escape(out, name);
+                    fprintf(out, "/</a>");
+                } else {
+                    fprintf(out, "<a href=\"%s/\">", name);
+                    html_escape(out, name);
+                    fprintf(out, "</a>");
+                }
+                fprintf(out, "</td>");
+
+                /* Get last commit for this file */
+                char commit_cmd[MAX_CMD_LEN];
+                snprintf(commit_cmd, sizeof(commit_cmd),
+                         "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
+                         repo_path, branch_name, name);
+
+                FILE *commit_fp = popen(commit_cmd, "r");
+                char commit_line[MAX_LINE_LEN] = {0};
+                if (commit_fp && fgets(commit_line, sizeof(commit_line), commit_fp)) {
+                    char *hash = strtok(commit_line, "|");
+                    char *msg = strtok(NULL, "|");
+                    char *date = strtok(NULL, "\n");
+
+                    if (hash && msg) {
+                        /* Truncate message if too long */
+                        size_t msg_len = strlen(msg);
+                        const size_t max_msg_len = 45;
+                        if (msg_len > max_msg_len) {
+                            msg[max_msg_len - 3] = '.';
+                            msg[max_msg_len - 2] = '.';
+                            msg[max_msg_len - 1] = '.';
+                            msg[max_msg_len] = '\0';
+                        }
+
+                        fprintf(out, "<td>");
+                        if (date) {
+                            fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
+                        }
+                        fprintf(out, "<a href=\"../../../commit/%s/summary/\"><code>%s</code></a> ", hash, hash);
+                        html_escape(out, msg);
+                        fprintf(out, "</td>");
+                    } else {
+                        fprintf(out, "<td>-</td>");
+                    }
+                    if (commit_fp) pclose(commit_fp);
+                } else {
+                    fprintf(out, "<td>-</td>");
+                    if (commit_fp) pclose(commit_fp);
+                }
+
+                fprintf(out, "<td>");
+                if (size_str && 0 != strcmp(type, "tree")) {
+                    fprintf(out, "%s", size_str);
+                } else {
+                    fprintf(out, "-");
+                }
+                fprintf(out, "</td>");
+
+                fprintf(out, "</tr>\n");
+            }
+        }
+
+        fprintf(out, "</tbody>\n</table>\n");
+        pclose(fp);
+    }
+
+    /* README - display after file tree */
+    if (readme) {
+        fprintf(out, "<h2>readme</h2>\n");
+        fprintf(out, "<pre>");
+        html_escape(out, readme);
+        fprintf(out, "</pre>\n");
+        free(readme);
+    }
+
+    generate_html_footer(out);
+
+    return 1;
+}
+
+static int32_t
+generate_tree_path_page(FILE *out, const char *repo_path, const char *repo_name,
+                        const char *ref, const char *tree_path, const char *page_title) {
+    generate_html_header(out, repo_name, page_title);
+
+    /* Get latest commit for this path */
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
+             repo_path, ref, tree_path);
+
+    FILE *commit_fp = popen(cmd, "r");
+    char commit_hash[64] = {0};
+    char commit_subject[512] = {0};
+    char commit_date[128] = {0};
+    if (commit_fp) {
+        char commit_line[MAX_LINE_LEN];
+        if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
+            char *hash = strtok(commit_line, "|");
+            char *subject = strtok(NULL, "|");
+            char *date = strtok(NULL, "\n");
+            if (hash && subject) {
+                snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
+                snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
+            }
+            if (date) {
+                snprintf(commit_date, sizeof(commit_date), "%s", date);
+            }
+        }
+        pclose(commit_fp);
+    }
+
+    /* Display commit info line */
+    if (commit_hash[0]) {
+        fprintf(out, "<p><small>");
+        if (commit_date[0]) {
+            fprintf(out, "<span style=\"float:right\">(%s)</span>", commit_date);
+        }
+        fprintf(out, "commit %s: ", commit_hash);
+        html_escape(out, commit_subject);
+        fprintf(out, "</small></p>\n");
+    }
+
+    /* Navigation */
+    fprintf(out, "<p><a href=\"../../\">Back</a></p>\n");
+
+    /* Check if path is tree or blob */
+    snprintf(cmd, sizeof(cmd),
+             "cd '%s' && git cat-file -t %s:'%s' 2>/dev/null",
+             repo_path, ref, tree_path);
+
+    FILE *type_fp = popen(cmd, "r");
+    char obj_type[32] = {0};
+    if (type_fp) {
+        if (fgets(obj_type, sizeof(obj_type), type_fp)) {
+            size_t len = strlen(obj_type);
+            if (len > 0 && '\n' == obj_type[len - 1]) {
+                obj_type[len - 1] = '\0';
+            }
+        }
+        pclose(type_fp);
+    }
+
+    if (0 == strcmp(obj_type, "tree")) {
+        /* Directory - show file list */
+        fprintf(out, "<h2>%s/</h2>\n", tree_path);
+        fprintf(out, "<h3>files</h3>\n");
+
+        snprintf(cmd, sizeof(cmd),
+                 "cd '%s' && git ls-tree -l %s:'%s' 2>/dev/null",
+                 repo_path, ref, tree_path);
+
+        FILE *fp = popen(cmd, "r");
+        if (fp) {
+            fprintf(out, "<table>\n");
+            fprintf(out, "<thead><tr><th>type</th><th>name</th><th>commit</th><th>size</th></tr></thead>\n");
+            fprintf(out, "<tbody>\n");
+
+            /* Add parent directory link */
+            fprintf(out, "<tr><td>dir</td><td><a href=\"../\">..</a></td><td>-</td><td>-</td></tr>\n");
+
+            char line[MAX_LINE_LEN];
+            while (fgets(line, sizeof(line), fp)) {
+                (void)strtok(line, " ");  /* mode */
+                char *type = strtok(NULL, " ");
+                (void)strtok(NULL, " ");  /* hash */
+                char *size_str = strtok(NULL, "\t");
+                char *name = strtok(NULL, "\n");
+
+                if (type && name) {
+                    const char *type_display = "file";
+                    if (0 == strcmp(type, "tree")) {
+                        type_display = "dir";
+                    }
+
+                    fprintf(out, "<tr><td>%s</td><td>", type_display);
+                    if (0 == strcmp(type, "tree")) {
+                        fprintf(out, "<a href=\"%s/\">", name);
+                        html_escape(out, name);
+                        fprintf(out, "/</a>");
+                    } else {
+                        fprintf(out, "<a href=\"%s/\">", name);
+                        html_escape(out, name);
+                        fprintf(out, "</a>");
+                    }
+                    fprintf(out, "</td>");
+
+                    /* Get last commit for this file */
+                    char full_path[PATH_MAX];
+                    snprintf(full_path, sizeof(full_path), "%s/%s", tree_path, name);
+
+                    char commit_cmd[MAX_CMD_LEN];
+                    snprintf(commit_cmd, sizeof(commit_cmd),
+                             "cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s -- '%s' 2>/dev/null",
+                             repo_path, ref, full_path);
+
+                    FILE *c_fp = popen(commit_cmd, "r");
+                    char c_line[MAX_LINE_LEN] = {0};
+                    if (c_fp && fgets(c_line, sizeof(c_line), c_fp)) {
+                        char *hash = strtok(c_line, "|");
+                        char *msg = strtok(NULL, "|");
+                        char *date = strtok(NULL, "\n");
+
+                        if (hash && msg) {
+                            size_t msg_len = strlen(msg);
+                            const size_t max_msg_len = 45;
+                            if (msg_len > max_msg_len) {
+                                msg[max_msg_len - 3] = '.';
+                                msg[max_msg_len - 2] = '.';
+                                msg[max_msg_len - 1] = '.';
+                                msg[max_msg_len] = '\0';
+                            }
+
+                            fprintf(out, "<td>");
+                            if (date) {
+                                fprintf(out, "<span style=\"float:right\">(%s)</span>", date);
+                            }
+                            fprintf(out, "<a href=\"../../../commit/%s/summary/\"><code>%s</code></a> ", hash, hash);
+                            html_escape(out, msg);
+                            fprintf(out, "</td>");
+                        } else {
+                            fprintf(out, "<td>-</td>");
+                        }
+                        if (c_fp) pclose(c_fp);
+                    } else {
+                        fprintf(out, "<td>-</td>");
+                        if (c_fp) pclose(c_fp);
+                    }
+
+                    fprintf(out, "<td>");
+                    if (size_str && 0 != strcmp(type, "tree")) {
+                        fprintf(out, "%s", size_str);
+                    } else {
+                        fprintf(out, "-");
+                    }
+                    fprintf(out, "</td>");
+
+                    fprintf(out, "</tr>\n");
+                }
+            }
+
+            fprintf(out, "</tbody>\n</table>\n");
+            pclose(fp);
+        }
+    } else if (0 == strcmp(obj_type, "blob")) {
+        /* File - show contents */
+        fprintf(out, "<h2>%s</h2>\n", tree_path);
+        fprintf(out, "<p><a href=\"raw.txt\">raw</a></p>\n");
+
+        snprintf(cmd, sizeof(cmd),
+                 "cd '%s' && git show %s:'%s' 2>/dev/null",
+                 repo_path, ref, tree_path);
+
+        FILE *fp = popen(cmd, "r");
+        if (fp) {
+            fprintf(out, "<pre>");
+            char line[MAX_LINE_LEN];
+            while (fgets(line, sizeof(line), fp)) {
+                html_escape(out, line);
+            }
+            fprintf(out, "</pre>\n");
+            pclose(fp);
+        }
+    }
+
+    generate_html_footer(out);
+    return 1;
+}
+
+static void
+generate_tree_pages_recursive(const char *output_dir, const char *repo_path,
+                               const char *repo_name, const char *ref,
+                               const char *tree_path, const char *page_title) {
+    char cmd[MAX_CMD_LEN];
+
+    /* Get list of files/directories at this path */
+    if (tree_path && tree_path[0]) {
+        snprintf(cmd, sizeof(cmd),
+                 "cd '%s' && git ls-tree %s:'%s' 2>/dev/null",
+                 repo_path, ref, tree_path);
+    } else {
+        snprintf(cmd, sizeof(cmd),
+                 "cd '%s' && git ls-tree %s 2>/dev/null",
+                 repo_path, ref);
+    }
+
+    FILE *fp = popen(cmd, "r");
+    if (!fp) return;
+
+    char line[MAX_LINE_LEN];
+    while (fgets(line, sizeof(line), fp)) {
+        (void)strtok(line, " ");  /* mode */
+        char *type = strtok(NULL, " ");
+        (void)strtok(NULL, "\t");  /* hash, up to tab */
+        char *name = strtok(NULL, "\n");
+
+        if (!type || !name) continue;
+
+        char full_path[PATH_MAX];
+        char output_path[PATH_MAX];
+
+        if (tree_path && tree_path[0]) {
+            snprintf(full_path, sizeof(full_path), "%s/%s", tree_path, name);
+        } else {
+            snprintf(full_path, sizeof(full_path), "%s", name);
+        }
+
+        if (0 == strcmp(type, "tree")) {
+            /* Directory - create directory page and recurse */
+            snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, full_path);
+            if (!ensure_directory(output_path)) continue;
+
+            char index_path[PATH_MAX];
+            snprintf(index_path, sizeof(index_path), "%s/index.html", output_path);
+
+            FILE *out = fopen(index_path, "w");
+            if (out) {
+                generate_tree_path_page(out, repo_path, repo_name, ref, full_path, page_title);
+                fclose(out);
+            }
+
+            /* Recurse into subdirectory */
+            generate_tree_pages_recursive(output_dir, repo_path, repo_name, ref, full_path, page_title);
+        } else {
+            /* File - create file directory with index.html and raw */
+            snprintf(output_path, sizeof(output_path), "%s/%s", output_dir, full_path);
+            if (!ensure_directory(output_path)) continue;
+
+            /* Create index.html with HTML view */
+            char index_path[PATH_MAX];
+            snprintf(index_path, sizeof(index_path), "%s/index.html", output_path);
+
+            FILE *out = fopen(index_path, "w");
+            if (out) {
+                generate_tree_path_page(out, repo_path, repo_name, ref, full_path, page_title);
+                fclose(out);
+            }
+
+            /* Create raw.txt file with just content */
+            char raw_path[PATH_MAX];
+            snprintf(raw_path, sizeof(raw_path), "%s/raw.txt", output_path);
+
+            FILE *raw_out = fopen(raw_path, "w");
+            if (raw_out) {
+                char cmd_raw[MAX_CMD_LEN];
+                snprintf(cmd_raw, sizeof(cmd_raw),
+                         "cd '%s' && git show %s:'%s' 2>/dev/null",
+                         repo_path, ref, full_path);
+
+                FILE *fp_raw = popen(cmd_raw, "r");
+                if (fp_raw) {
+                    char line_raw[MAX_LINE_LEN];
+                    while (fgets(line_raw, sizeof(line_raw), fp_raw)) {
+                        fputs(line_raw, raw_out);
+                    }
+                    pclose(fp_raw);
+                }
+                fclose(raw_out);
+            }
+        }
+    }
+
+    pclose(fp);
+}
+
+static int32_t
+generate_repo_page(const char *output_dir, const char *repo_name, const char *git_base_path) {
+    char repo_path[PATH_MAX];
+    char repo_output_dir[PATH_MAX];
+
+    snprintf(repo_path, sizeof(repo_path), "%s/%s", git_base_path, repo_name);
+    snprintf(repo_output_dir, sizeof(repo_output_dir), "%s/%s", output_dir, repo_name);
+
+    if (!ensure_directory(repo_output_dir)) {
+        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", repo_output_dir);
+        return 0;
+    }
+
+    /* Get default branch - use the first existing branch */
+    char branch_cmd[MAX_CMD_LEN];
+    snprintf(branch_cmd, sizeof(branch_cmd),
+             "cd '%s' && git for-each-ref --format='%%(refname:short)' refs/heads/ 2>/dev/null | head -n 1",
+             repo_path);
+
+    FILE *branch_fp = popen(branch_cmd, "r");
+    char default_branch[256] = {0};
+    if (branch_fp) {
+        if (fgets(default_branch, sizeof(default_branch), branch_fp)) {
+            size_t len = strlen(default_branch);
+            if (len > 0 && '\n' == default_branch[len - 1]) {
+                default_branch[len - 1] = '\0';
+            }
+        }
+        pclose(branch_fp);
+    }
+
+    if (0 == default_branch[0]) {
+        dprintf(STDERR_FILENO, "Error: No branches found in repository\n");
+        return 0;
+    }
+
+    /* Create redirect index at repo root */
+    char repo_index[PATH_MAX];
+    snprintf(repo_index, sizeof(repo_index), "%s/index.html", repo_output_dir);
+
+    FILE *redirect = fopen(repo_index, "w");
+    if (!redirect) {
+        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", repo_index);
+        return 0;
+    }
+
+    fprintf(redirect, "<!DOCTYPE html>\n");
+    fprintf(redirect, "<html>\n<head>\n");
+    fprintf(redirect, "<meta charset=\"utf-8\">\n");
+    fprintf(redirect, "<meta http-equiv=\"refresh\" content=\"0; url=branch/%s/tree/\">\n", default_branch);
+    fprintf(redirect, "<title>Redirecting to %s</title>\n", default_branch);
+    fprintf(redirect, "</head>\n<body>\n");
+    fprintf(redirect, "<p>Redirecting to <a href=\"branch/%s/tree/\">%s</a>...</p>\n",
+            default_branch, default_branch);
+    fprintf(redirect, "</body>\n</html>\n");
+    fclose(redirect);
+
+    /* Create branch directory structure */
+    char branch_output_dir[PATH_MAX];
+    char branch_history_dir[PATH_MAX];
+    char branch_tree_dir[PATH_MAX];
+
+    snprintf(branch_output_dir, sizeof(branch_output_dir), "%s/branch/%s", repo_output_dir, default_branch);
+    snprintf(branch_history_dir, sizeof(branch_history_dir), "%s/history", branch_output_dir);
+    snprintf(branch_tree_dir, sizeof(branch_tree_dir), "%s/tree", branch_output_dir);
+
+    if (!ensure_directory(branch_history_dir)) {
+        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", branch_history_dir);
+        return 0;
+    }
+
+    if (!ensure_directory(branch_tree_dir)) {
+        dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", branch_tree_dir);
+        return 0;
+    }
+
+    /* Generate branch history page */
+    char branch_history_index[PATH_MAX];
+    snprintf(branch_history_index, sizeof(branch_history_index), "%s/index.html", branch_history_dir);
+
+    FILE *history_out = fopen(branch_history_index, "w");
+    if (!history_out) {
+        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", branch_history_index);
+        return 0;
+    }
+
+    if (!generate_branch_history_page(history_out, repo_path, repo_name, default_branch)) {
+        fclose(history_out);
+        return 0;
+    }
+
+    fclose(history_out);
+
+    /* Generate branch tree page */
+    char branch_tree_index[PATH_MAX];
+    snprintf(branch_tree_index, sizeof(branch_tree_index), "%s/index.html", branch_tree_dir);
+
+    FILE *tree_out = fopen(branch_tree_index, "w");
+    if (!tree_out) {
+        dprintf(STDERR_FILENO, "Error: Cannot create %s\n", branch_tree_index);
+        return 0;
+    }
+
+    if (!generate_branch_tree_page(tree_out, repo_path, repo_name, default_branch)) {
+        fclose(tree_out);
+        return 0;
+    }
+
+    fclose(tree_out);
+
+    /* Create redirect at branch root */
+    char branch_redirect_path[PATH_MAX];
+    snprintf(branch_redirect_path, sizeof(branch_redirect_path), "%s/index.html", branch_output_dir);
+    FILE *branch_redirect = fopen(branch_redirect_path, "w");
+    if (branch_redirect) {
+        fprintf(branch_redirect, "<!DOCTYPE html>\n");
+        fprintf(branch_redirect, "<html>\n<head>\n");
+        fprintf(branch_redirect, "<meta charset=\"utf-8\">\n");
+        fprintf(branch_redirect, "<meta http-equiv=\"refresh\" content=\"0; url=tree/\">\n");
+        fprintf(branch_redirect, "<title>Redirecting to tree</title>\n");
+        fprintf(branch_redirect, "</head>\n<body>\n");
+        fprintf(branch_redirect, "<p>Redirecting to <a href=\"tree/\">tree</a>...</p>\n");
+        fprintf(branch_redirect, "</body>\n</html>\n");
+        fclose(branch_redirect);
+    }
+
+    /* Generate tree path pages recursively for branch */
+    generate_tree_pages_recursive(branch_tree_dir, repo_path, repo_name, default_branch, "", default_branch);
+
+    /* Generate commit pages for recent commits */
+    char commit_cmd[MAX_CMD_LEN];
+    snprintf(commit_cmd, sizeof(commit_cmd),
+             "cd '%s' && git log --pretty=format:'%%h' -n 10 %s 2>/dev/null",
+             repo_path, default_branch);
+
+    FILE *commit_fp = popen(commit_cmd, "r");
+    if (commit_fp) {
+        char commit_hash[MAX_LINE_LEN];
+        while (fgets(commit_hash, sizeof(commit_hash), commit_fp)) {
+            /* Remove trailing newline */
+            size_t len = strlen(commit_hash);
+            if (len > 0 && '\n' == commit_hash[len - 1]) {
+                commit_hash[len - 1] = '\0';
+            }
+
+            if (len > 0) {
+                /* Create commit subdirectories */
+                char commit_base_dir[PATH_MAX];
+                char commit_summary_dir[PATH_MAX];
+                char commit_history_dir[PATH_MAX];
+                char commit_tree_dir[PATH_MAX];
+                char redirect_path[PATH_MAX];
+
+                snprintf(commit_base_dir, sizeof(commit_base_dir), "%s/commit/%s", repo_output_dir, commit_hash);
+                snprintf(commit_summary_dir, sizeof(commit_summary_dir), "%s/summary", commit_base_dir);
+                snprintf(commit_history_dir, sizeof(commit_history_dir), "%s/history", commit_base_dir);
+                snprintf(commit_tree_dir, sizeof(commit_tree_dir), "%s/tree", commit_base_dir);
+
+                if (!ensure_directory(commit_summary_dir) ||
+                    !ensure_directory(commit_history_dir) ||
+                    !ensure_directory(commit_tree_dir)) {
+                    continue;
+                }
+
+                /* Create redirect at commit root */
+                snprintf(redirect_path, sizeof(redirect_path), "%s/index.html", commit_base_dir);
+                FILE *commit_redirect = fopen(redirect_path, "w");
+                if (commit_redirect) {
+                    fprintf(commit_redirect, "<!DOCTYPE html>\n");
+                    fprintf(commit_redirect, "<html>\n<head>\n");
+                    fprintf(commit_redirect, "<meta charset=\"utf-8\">\n");
+                    fprintf(commit_redirect, "<meta http-equiv=\"refresh\" content=\"0; url=summary/\">\n");
+                    fprintf(commit_redirect, "<title>Redirecting to summary</title>\n");
+                    fprintf(commit_redirect, "</head>\n<body>\n");
+                    fprintf(commit_redirect, "<p>Redirecting to <a href=\"summary/\">summary</a>...</p>\n");
+                    fprintf(commit_redirect, "</body>\n</html>\n");
+                    fclose(commit_redirect);
+                }
+
+                /* Generate summary page */
+                char summary_index[PATH_MAX];
+                snprintf(summary_index, sizeof(summary_index), "%s/index.html", commit_summary_dir);
+                FILE *summary_out = fopen(summary_index, "w");
+                if (summary_out) {
+                    generate_commit_summary_page(summary_out, repo_path, repo_name, commit_hash);
+                    fclose(summary_out);
+                }
+
+                /* Generate history page */
+                char history_index[PATH_MAX];
+                snprintf(history_index, sizeof(history_index), "%s/index.html", commit_history_dir);
+                FILE *history_out = fopen(history_index, "w");
+                if (history_out) {
+                    generate_commit_history_page(history_out, repo_path, repo_name, commit_hash);
+                    fclose(history_out);
+                }
+
+                /* Generate tree page */
+                char tree_index[PATH_MAX];
+                snprintf(tree_index, sizeof(tree_index), "%s/index.html", commit_tree_dir);
+                FILE *tree_out = fopen(tree_index, "w");
+                if (tree_out) {
+                    generate_commit_tree_page(tree_out, repo_path, repo_name, commit_hash);
+                    fclose(tree_out);
+
+                    /* Generate tree path pages recursively for commit */
+                    generate_tree_pages_recursive(commit_tree_dir, repo_path, repo_name, commit_hash, "", commit_hash);
+                }
+            }
+        }
+        pclose(commit_fp);
+    }
 
     return 1;
 }
 
 int32_t
 main(int32_t argc, char *argv[]) {
-    if (argc < 3) {
-        dprintf(STDERR_FILENO, "Usage: %s <output-dir> <repo>\n", argv[0]);
-        dprintf(STDERR_FILENO, "Example: %s /var/www/git myproject.git\n", argv[0]);
+    const char *git_base_path = DEFAULT_GIT_BASE_PATH;
+    int32_t opt;
+
+    while (-1 != (opt = getopt(argc, argv, "D:"))) {
+        switch (opt) {
+            case 'D':
+                git_base_path = optarg;
+                break;
+            default:
+                dprintf(STDERR_FILENO, "Usage: %s [-D git-dir] <output-dir> <repo>\n", argv[0]);
+                dprintf(STDERR_FILENO, "Example: %s -D /srv/git /var/www/git myproject.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]);
         return EXIT_FAILURE;
     }
 
-    const char *output_dir = argv[1];
-    const char *repo_name = argv[2];
+    const char *output_dir = argv[optind];
+    const char *repo_name = argv[optind + 1];
 
     if (!ensure_directory(output_dir)) {
         dprintf(STDERR_FILENO, "Error: Cannot create output directory %s\n", output_dir);
@@ -303,14 +1260,14 @@ main(int32_t argc, char *argv[]) {
     }
 
     char repo_path[PATH_MAX];
-    snprintf(repo_path, sizeof(repo_path), "%s/%s", GIT_BASE_PATH, repo_name);
+    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)) {
+    if (!generate_repo_page(output_dir, repo_name, git_base_path)) {
         return EXIT_FAILURE;
     }