(25 hours ago)commit 2cd3640: rough version of marrow-static
tree / marrow-static.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
#include <stdint.h>
#define MAX_CMD_LEN 4096
#define MAX_LINE_LEN 2048
#define DEFAULT_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 *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");
fprintf(out, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n");
fprintf(out, "<title>%s</title>\n", title);
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");
/* 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
generate_html_footer(FILE *out) {
fprintf(out, "<div class=\"footer\">generated by marrow-static</div>\n");
fprintf(out, "</body>\n</html>\n");
}
static int32_t
get_git_readme(const char *repo_path, char **readme) {
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd),
"cd '%s' && git show HEAD:README.md 2>/dev/null || "
"git show HEAD:README 2>/dev/null || "
"git show HEAD:readme.md 2>/dev/null",
repo_path);
FILE *fp = popen(cmd, "r");
if (!fp) {
return 0;
}
/* Allocate buffer for README */
size_t capacity = 8192;
size_t total = 0;
*readme = malloc(capacity);
if (!*readme) {
pclose(fp);
return 0;
}
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp)) {
size_t len = strlen(buffer);
if (total + len >= capacity) {
capacity *= 2;
char *new_readme = realloc(*readme, capacity);
if (!new_readme) {
free(*readme);
pclose(fp);
return 0;
}
*readme = new_readme;
}
strcpy(*readme + total, buffer);
total += len;
}
int32_t status = pclose(fp);
if (0 != status || 0 == total) {
free(*readme);
*readme = NULL;
return 0;
}
return 1;
}
static int32_t
generate_commit_summary_page(FILE *out, const char *repo_path, const char *repo_name, const char *commit_hash) {
generate_html_header(out, repo_name, commit_hash);
/* Get commit subject and date for display */
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd),
"cd '%s' && git log -1 --pretty=format:'%%s|%%ar' %s 2>/dev/null",
repo_path, commit_hash);
FILE *subject_fp = popen(cmd, "r");
char commit_subject[512] = {0};
char commit_date[128] = {0};
if (subject_fp) {
char line[MAX_LINE_LEN];
if (fgets(line, sizeof(line), subject_fp)) {
char *subject = strtok(line, "|");
char *date = strtok(NULL, "\n");
if (subject) {
snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
}
if (date) {
snprintf(commit_date, sizeof(commit_date), "%s", date);
}
}
pclose(subject_fp);
}
/* Display commit info line */
fprintf(out, "<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);
/* Get commit details */
snprintf(cmd, sizeof(cmd),
"cd '%s' && git show --pretty=format:'%%H|%%an|%%ae|%%ad|%%s%%n%%n%%b' --no-patch %s 2>/dev/null",
repo_path, commit_hash);
FILE *fp = popen(cmd, "r");
if (!fp) {
return 0;
}
char line[MAX_LINE_LEN];
if (fgets(line, sizeof(line), fp)) {
(void)strtok(line, "|"); /* full_hash */
char *author = strtok(NULL, "|");
char *email = strtok(NULL, "|");
char *date = strtok(NULL, "|");
char *subject = strtok(NULL, "\n");
if (author && date && subject) {
fprintf(out, "<table>\n");
fprintf(out, "<tr><td><strong>author</strong></td><td>");
html_escape(out, author);
if (email) {
fprintf(out, " <");
html_escape(out, email);
fprintf(out, ">");
}
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);
}
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);
fp = popen(cmd, "r");
if (fp) {
fprintf(out, "<pre>");
while (fgets(line, sizeof(line), fp)) {
html_escape(out, line);
}
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);
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><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, "|");
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");
}
}
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);
}
}
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> | <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_branch_history_page(FILE *out, const char *repo_path, const char *repo_name, const char *branch_name) {
generate_html_header(out, repo_name, branch_name);
/* Get latest commit for display */
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd),
"cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s 2>/dev/null",
repo_path, branch_name);
FILE *commit_fp = popen(cmd, "r");
char commit_hash[64] = {0};
char commit_subject[512] = {0};
char commit_date[128] = {0};
if (commit_fp) {
char commit_line[MAX_LINE_LEN];
if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
char *hash = strtok(commit_line, "|");
char *subject = strtok(NULL, "|");
char *date = strtok(NULL, "\n");
if (hash && subject) {
snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
}
if (date) {
snprintf(commit_date, sizeof(commit_date), "%s", date);
}
}
pclose(commit_fp);
}
/* Display commit info line */
if (commit_hash[0]) {
fprintf(out, "<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=\"../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);
/* Recent commits */
fprintf(out, "<h2>recent commits</h2>\n");
snprintf(cmd, sizeof(cmd),
"cd '%s' && git log --pretty=format:'%%h|%%an|%%ar|%%s' -n 10 %s 2>/dev/null",
repo_path, branch_name);
FILE *fp = popen(cmd, "r");
if (fp) {
fprintf(out, "<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, "|");
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=\"../../../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);
fprintf(out, "</td></tr>\n");
}
}
fprintf(out, "</tbody>\n</table>\n");
pclose(fp);
}
generate_html_footer(out);
return 1;
}
static int32_t
generate_branch_tree_page(FILE *out, const char *repo_path, const char *repo_name, const char *branch_name) {
generate_html_header(out, repo_name, branch_name);
/* Get latest commit for display */
char cmd[MAX_CMD_LEN];
snprintf(cmd, sizeof(cmd),
"cd '%s' && git log -1 --pretty=format:'%%h|%%s|%%ar' %s 2>/dev/null",
repo_path, branch_name);
FILE *commit_fp = popen(cmd, "r");
char commit_hash[64] = {0};
char commit_subject[512] = {0};
char commit_date[128] = {0};
if (commit_fp) {
char commit_line[MAX_LINE_LEN];
if (fgets(commit_line, sizeof(commit_line), commit_fp)) {
char *hash = strtok(commit_line, "|");
char *subject = strtok(NULL, "|");
char *date = strtok(NULL, "\n");
if (hash && subject) {
snprintf(commit_hash, sizeof(commit_hash), "%s", hash);
snprintf(commit_subject, sizeof(commit_subject), "%s", subject);
}
if (date) {
snprintf(commit_date, sizeof(commit_date), "%s", date);
}
}
pclose(commit_fp);
}
/* Display commit info line */
if (commit_hash[0]) {
fprintf(out, "<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[]) {
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[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);
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, git_base_path)) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}