#include #include #include #include #include #include #include #include #include #define MAX_CMD_LEN 4096 #define MAX_LINE_LEN 2048 #define GIT_BASE_PATH "/srv/git" static int32_t ensure_directory(const char *path) { struct stat st; if (0 == stat(path, &st)) { return S_ISDIR(st.st_mode) ? 1 : 0; } char tmp[PATH_MAX]; snprintf(tmp, sizeof(tmp), "mkdir -p '%s'", path); return (0 == system(tmp)); } static int32_t is_git_repository(const char *path) { char git_dir[PATH_MAX]; struct stat st; snprintf(git_dir, sizeof(git_dir), "%s/HEAD", path); return (0 == stat(git_dir, &st)); } static void html_escape(FILE *out, const char *str) { while (*str) { switch (*str) { case '<': fprintf(out, "<"); break; case '>': fprintf(out, ">"); break; case '&': fprintf(out, "&"); break; case '"': fprintf(out, """); break; default: fputc(*str, out); break; } str++; } } static void generate_html_header(FILE *out, const char *title) { fprintf(out, "\n"); fprintf(out, "\n\n"); fprintf(out, "\n"); fprintf(out, "\n"); fprintf(out, "%s\n", title); fprintf(out, "\n"); fprintf(out, "\n\n"); fprintf(out, "

%s

\n", title); } static void generate_html_footer(FILE *out) { fprintf(out, "
generated by marrow-static
\n"); fprintf(out, "\n\n"); } static int32_t get_git_readme(const char *repo_path, char **readme) { char cmd[MAX_CMD_LEN]; snprintf(cmd, sizeof(cmd), "cd '%s' && git show HEAD:README.md 2>/dev/null || " "git show HEAD:README 2>/dev/null || " "git show HEAD:readme.md 2>/dev/null", repo_path); FILE *fp = popen(cmd, "r"); if (!fp) { return 0; } /* Allocate buffer for README */ size_t capacity = 8192; size_t total = 0; *readme = malloc(capacity); if (!*readme) { pclose(fp); return 0; } char buffer[1024]; while (fgets(buffer, sizeof(buffer), fp)) { size_t len = strlen(buffer); if (total + len >= capacity) { capacity *= 2; char *new_readme = realloc(*readme, capacity); if (!new_readme) { free(*readme); pclose(fp); return 0; } *readme = new_readme; } strcpy(*readme + total, buffer); total += len; } int32_t status = pclose(fp); if (0 != status || 0 == total) { free(*readme); *readme = NULL; return 0; } return 1; } static 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); } size_t line_len = next - line; /* Code blocks */ if (line_len >= 3 && 0 == strncmp(line, "```", 3)) { if (in_code) { fprintf(out, "\n"); in_code = 0; } else { fprintf(out, "
");
                in_code = 1;
            }
            line = (*next ? next + 1 : next);
            continue;
        }

        if (in_code) {
            html_escape(out, line);
            if (*next) {
                fprintf(out, "\n");
            }
            line = (*next ? next + 1 : next);
            continue;
        }

        /* Headers */
        if ('#' == *line && line_len > 0) {
            int32_t level = 0;
            const char *p = line;
            while (p < next && '#' == *p && level < 6) {
                level++;
                p++;
            }
            if (level > 0) {
                /* Skip space after # */
                if (p < next && ' ' == *p) {
                    p++;
                }
                fprintf(out, "", 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, "\n", level + 1);
                line = (*next ? next + 1 : next);
                continue;
            }
        }

        /* Empty lines */
        if (0 == line_len) {
            line = (*next ? next + 1 : next);
            continue;
        }

        /* Regular paragraph */
        fprintf(out, "

"); for (size_t i = 0; i < line_len; i++) { html_escape(out, (char[]){line[i], '\0'}); } fprintf(out, "

\n"); line = (*next ? next + 1 : next); } if (in_code) { fprintf(out, "
\n"); } } 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]; 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); if (!ensure_directory(repo_output_dir)) { dprintf(STDERR_FILENO, "Error: Cannot create directory %s\n", repo_output_dir); return 0; } FILE *out = fopen(repo_index, "w"); if (!out) { dprintf(STDERR_FILENO, "Error: Cannot create %s\n", repo_index); return 0; } generate_html_header(out, repo_name); /* Clone URL */ fprintf(out, "

clone

\n"); fprintf(out, "
git clone git@host:%s
\n", repo_name); /* README */ char *readme = NULL; if (get_git_readme(repo_path, &readme)) { fprintf(out, "

readme

\n"); render_markdown_simple(out, readme); free(readme); } /* Recent commits */ fprintf(out, "

recent commits

\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); FILE *fp = popen(cmd, "r"); if (fp) { fprintf(out, "\n"); fprintf(out, "\n"); fprintf(out, "\n"); char line[MAX_LINE_LEN]; while (fgets(line, sizeof(line), fp)) { char *hash = strtok(line, "|"); char *author = strtok(NULL, "|"); char *date = strtok(NULL, "|"); char *msg = strtok(NULL, "|"); if (hash && author && date && msg) { /* Remove trailing newline from message */ size_t msg_len = strlen(msg); if (msg_len > 0 && '\n' == msg[msg_len - 1]) { msg[msg_len - 1] = '\0'; } fprintf(out, "\n"); } } fprintf(out, "\n
hashauthordatemessage
%s", hash); html_escape(out, author); fprintf(out, "%s", date); html_escape(out, msg); fprintf(out, "
\n"); pclose(fp); } generate_html_footer(out); fclose(out); return 1; } int32_t main(int32_t argc, char *argv[]) { if (argc < 3) { dprintf(STDERR_FILENO, "Usage: %s \n", argv[0]); dprintf(STDERR_FILENO, "Example: %s /var/www/git myproject.git\n", argv[0]); return EXIT_FAILURE; } const char *output_dir = argv[1]; const char *repo_name = argv[2]; 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 (!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)) { return EXIT_FAILURE; } return EXIT_SUCCESS; }