#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_MARKDOWN #include "extras/markdown.h" #endif #ifndef PATH_MAX #define PATH_MAX 4096 #endif #define COMMIT_LIMIT 100 #define CACHE_VERSION "2" #define SEP '\x1f' struct repo { char *branch; char *cloneurl; char *head; char *description; char *last_author; char *last_date; char *last_subject; char *name; char *owner; char *outdir; char *path; char *rootlink_deep; char *rootlink_repo; int changed; }; struct tree_node { struct tree_node **children; char *name; char *path; size_t cap; size_t nchildren; int is_dir; }; static const char *progname; static int raw_mode; static void err(int, const char *, ...); static void errx(int, const char *, ...); static void warn(const char *, ...); static void append_char(char **, size_t *, size_t *, char); static void append_str(char **, size_t *, size_t *, const char *); static void add_repo(struct repo **, size_t *, size_t *, const char *); static void discover_repos(const char *, struct repo **, size_t *, size_t *); static void ensure_dir(const char *); static void ensure_parent_dir(const char *); static void clear_html_files(const char *); static void free_repos(struct repo *, size_t); static char *join3(const char *, const char *, const char *); static int load_repo_meta(struct repo *); static void make_commit_page(const struct repo *, const char *, const char *, const char *, const char *, const char *); static void make_files_page(const struct repo *, const char *); static char *repo_gitdir_path(const struct repo *); static char *read_git_meta_file(const struct repo *, const char *); static void make_index(const char *, struct repo *, size_t); static void make_readme_page(const struct repo *, const char *, int); static void make_repo_page(const struct repo *); static char *file_page_name(const char *); static int has_suffix_ci(const char *, const char *); static void render_tree(FILE *, const struct repo *, struct tree_node *, int); static int repo_cmp(const void *, const void *); static size_t read_blob_size(const struct repo *, const char *); static char *read_cache_head(const char *); static char *read_cmd_output(const char *, ...); static char *read_file_head(const struct repo *, const char *); static char *read_readme(const struct repo *, int *); static char *repo_basename(const char *); static int repo_changed(struct repo *); static int repo_has_git(const char *); static char *split_field(char **); static int stat_is_dir(const char *); static char *str_dup(const char *); static char *trim_newline(char *); static size_t text_loc(const char *); static int text_content_p(const char *, size_t, size_t); static void format_size(size_t, char *, size_t); static void write_raw_blob(const struct repo *, const char *); static struct tree_node *tree_add_path(struct tree_node *, const char *); static int tree_cmp(const void *, const void *); static void tree_free(struct tree_node *); static struct tree_node *tree_new(const char *, const char *, int); static struct tree_node *tree_root_from_files(const char *); static void usage(void); static void write_cache_head(const char *, const char *); static void write_html_escaped(FILE *, const char *); static void write_html_escaped_len(FILE *, const char *, size_t); static void write_diff_html(FILE *, const char *); static char *write_shell_quoted(const char *); int main(int argc, char *argv[]) { char *argv0; const char *input, *output; const char *raw; struct repo *repos; size_t cap, i, nrepos; int ch, multi; argv0 = argv[0]; while ((ch = getopt(argc, argv, "")) != -1) { switch (ch) { default: usage(); } } argc -= optind; argv += optind; progname = argv0; raw = getenv("RAW"); raw_mode = 0; if (raw != NULL && strcmp(raw, "1") == 0) raw_mode = 1; if (argc == 0) { input = "repos"; output = "out"; } else if (argc == 2) { input = argv[0]; output = argv[1]; } else usage(); repos = NULL; cap = 0; nrepos = 0; discover_repos(input, &repos, &nrepos, &cap); if (nrepos == 0) errx(1, "no git repositories found in %s", input); ensure_dir(output); multi = nrepos > 1; for (i = 0; i < nrepos; i++) { if (multi) { repos[i].outdir = join3(output, "/", repos[i].name); if (repos[i].outdir == NULL) err(1, "malloc"); } else { repos[i].outdir = str_dup(output); if (repos[i].outdir == NULL) err(1, "malloc"); } if (multi) { repos[i].rootlink_repo = str_dup("../index.html"); repos[i].rootlink_deep = str_dup("../../index.html"); } else { repos[i].rootlink_repo = str_dup("index.html"); repos[i].rootlink_deep = str_dup("../index.html"); } if (repos[i].rootlink_repo == NULL || repos[i].rootlink_deep == NULL) err(1, "malloc"); if (!load_repo_meta(&repos[i])) { fprintf(stderr, "%s: skipping %s (no commits)\n", progname, repos[i].path); continue; } repos[i].changed = repo_changed(&repos[i]); if (repos[i].changed) make_repo_page(&repos[i]); } if (multi) { qsort(repos, nrepos, sizeof(struct repo), repo_cmp); make_index(output, repos, nrepos); } free_repos(repos, nrepos); return 0; } static void usage(void) { fprintf(stderr, "usage: %s [repos out]\n", progname); exit(1); } static void warn(const char *fmt, ...) { va_list ap; int saved; saved = errno; if (progname != NULL && *progname != '\0') fprintf(stderr, "%s: ", progname); va_start(ap, fmt); if (fmt != NULL && *fmt != '\0') vfprintf(stderr, fmt, ap); else fputs("warning", stderr); va_end(ap); fprintf(stderr, ": %s\n", strerror(saved)); } static void errx(int eval, const char *fmt, ...) { va_list ap; if (progname != NULL && *progname != '\0') fprintf(stderr, "%s: ", progname); va_start(ap, fmt); if (fmt != NULL && *fmt != '\0') vfprintf(stderr, fmt, ap); else fputs("error", stderr); va_end(ap); fputc('\n', stderr); exit(eval); } static void err(int eval, const char *fmt, ...) { va_list ap; int saved; saved = errno; if (progname != NULL && *progname != '\0') fprintf(stderr, "%s: ", progname); va_start(ap, fmt); if (fmt != NULL && *fmt != '\0') vfprintf(stderr, fmt, ap); else fputs("error", stderr); va_end(ap); fprintf(stderr, ": %s\n", strerror(saved)); exit(eval); } static char * str_dup(const char *s) { char *out; size_t len; len = strlen(s); out = malloc(len + 1); if (out == NULL) return NULL; memcpy(out, s, len + 1); return out; } static char * trim_newline(char *s) { size_t len; len = strlen(s); while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r')) { s[len - 1] = '\0'; len--; } return s; } static size_t text_loc(const char *s) { size_t loc; if (*s == '\0') return 0; loc = 0; for (; *s != '\0'; s++) { if (*s == '\n') loc++; } return loc + 1; } static int text_content_p(const char *s, size_t nbytes, size_t blob_size) { size_t i; unsigned char c; if (nbytes < blob_size) return 0; for (i = 0; i < nbytes; i++) { c = (unsigned char)s[i]; if (c == '\n' || c == '\r' || c == '\t') continue; if (c >= 0x20 || c >= 0x80) continue; return 0; } return 1; } static void format_size(size_t nbytes, char *buf, size_t buflen) { const char *units[] = { "bytes", "kb", "mb", "gb", "tb", "pb" }; double v; size_t i, nunits; nunits = sizeof(units) / sizeof(units[0]); if (nbytes < 1024) { snprintf(buf, buflen, "%lu bytes", (unsigned long)nbytes); return; } v = (double)nbytes; i = 0; while (v >= 1024.0 && i + 1 < nunits) { v /= 1024.0; i++; } if (v >= 100.0) snprintf(buf, buflen, "%.0f %s", v, units[i]); else if (v >= 10.0) snprintf(buf, buflen, "%.1f %s", v, units[i]); else snprintf(buf, buflen, "%.2f %s", v, units[i]); } static int stat_is_dir(const char *path) { struct stat st; if (stat(path, &st) == -1) return 0; return S_ISDIR(st.st_mode); } static char * join3(const char *a, const char *b, const char *c) { char *out; size_t la, lb, lc; la = strlen(a); lb = strlen(b); lc = strlen(c); out = malloc(la + lb + lc + 1); if (out == NULL) return NULL; memcpy(out, a, la); memcpy(out + la, b, lb); memcpy(out + la + lb, c, lc); out[la + lb + lc] = '\0'; return out; } static int repo_has_git(const char *path) { char *dotgit, *head, *objects, *out, *packed_refs, *qpath, *refs; struct stat st; int ok; dotgit = join3(path, "/", ".git"); if (dotgit == NULL) err(1, "malloc"); ok = stat(dotgit, &st) == 0 && (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)); free(dotgit); if (ok) return 1; /* Bare repository layout fallback: HEAD + objects + (refs or packed-refs). */ head = join3(path, "/", "HEAD"); objects = join3(path, "/", "objects"); refs = join3(path, "/", "refs"); packed_refs = join3(path, "/", "packed-refs"); if (head == NULL || objects == NULL || refs == NULL || packed_refs == NULL) err(1, "malloc"); ok = stat(head, &st) == 0 && S_ISREG(st.st_mode); ok = ok && stat(objects, &st) == 0 && S_ISDIR(st.st_mode); ok = ok && ((stat(refs, &st) == 0 && S_ISDIR(st.st_mode)) || (stat(packed_refs, &st) == 0 && S_ISREG(st.st_mode))); free(head); free(objects); free(refs); free(packed_refs); if (ok) return 1; qpath = write_shell_quoted(path); if (qpath == NULL) err(1, "malloc"); out = read_cmd_output("git -c safe.directory='*' -C %s rev-parse --git-dir 2>/dev/null", qpath); free(qpath); if (out == NULL) return 0; trim_newline(out); ok = out[0] != '\0'; free(out); return ok; } static char * repo_basename(const char *path) { const char *p; p = strrchr(path, '/'); if (p == NULL) return str_dup(path); if (*(p + 1) == '\0') { for (; p > path && *p == '/'; p--) continue; for (; p > path && *(p - 1) != '/'; p--) continue; return str_dup(p); } return str_dup(p + 1); } static void append_char(char **buf, size_t *len, size_t *cap, char c) { char *tmp; if (*len + 2 > *cap) { if (*cap == 0) *cap = 128; else *cap *= 2; tmp = realloc(*buf, *cap); if (tmp == NULL) err(1, "realloc"); *buf = tmp; } (*buf)[*len] = c; (*len)++; (*buf)[*len] = '\0'; } static void append_str(char **buf, size_t *len, size_t *cap, const char *s) { while (*s != '\0') { append_char(buf, len, cap, *s); s++; } } static char * write_shell_quoted(const char *s) { char *out; size_t cap, len; out = NULL; cap = 0; len = 0; append_char(&out, &len, &cap, '\''); while (*s != '\0') { if (*s == '\'') append_str(&out, &len, &cap, "'\\''"); else append_char(&out, &len, &cap, *s); s++; } append_char(&out, &len, &cap, '\''); return out; } static char * read_cmd_output(const char *fmt, ...) { va_list ap; char line[4096], *cmd, *out; size_t cap, len; FILE *fp; int n; cmd = NULL; n = 0; va_start(ap, fmt); n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (n < 0) return NULL; cmd = malloc((size_t)n + 1); if (cmd == NULL) err(1, "malloc"); va_start(ap, fmt); vsnprintf(cmd, (size_t)n + 1, fmt, ap); va_end(ap); fp = popen(cmd, "r"); free(cmd); if (fp == NULL) return NULL; out = NULL; cap = 0; len = 0; while (fgets(line, sizeof(line), fp) != NULL) append_str(&out, &len, &cap, line); if (pclose(fp) == -1) { free(out); return NULL; } if (out == NULL) { out = str_dup(""); if (out == NULL) err(1, "malloc"); } return out; } static void add_repo(struct repo **repos, size_t *nrepos, size_t *cap, const char *path) { struct repo *tmp; char *name; name = repo_basename(path); if (name == NULL) err(1, "malloc"); if (*nrepos + 1 > *cap) { if (*cap == 0) *cap = 8; else *cap *= 2; tmp = realloc(*repos, *cap * sizeof(struct repo)); if (tmp == NULL) err(1, "realloc"); *repos = tmp; } (*repos)[*nrepos].branch = NULL; (*repos)[*nrepos].cloneurl = NULL; (*repos)[*nrepos].head = NULL; (*repos)[*nrepos].description = NULL; (*repos)[*nrepos].last_author = NULL; (*repos)[*nrepos].last_date = NULL; (*repos)[*nrepos].last_subject = NULL; (*repos)[*nrepos].name = name; (*repos)[*nrepos].owner = NULL; (*repos)[*nrepos].outdir = NULL; (*repos)[*nrepos].path = str_dup(path); (*repos)[*nrepos].rootlink_deep = NULL; (*repos)[*nrepos].rootlink_repo = NULL; if ((*repos)[*nrepos].path == NULL) err(1, "malloc"); (*repos)[*nrepos].changed = 1; (*nrepos)++; } static void discover_repos(const char *input, struct repo **repos, size_t *nrepos, size_t *cap) { struct dirent *de; char *candidate; DIR *dp; if (repo_has_git(input)) { add_repo(repos, nrepos, cap, input); return; } if (!stat_is_dir(input)) errx(1, "%s is not a directory", input); dp = opendir(input); if (dp == NULL) err(1, "opendir %s", input); while ((de = readdir(dp)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; candidate = join3(input, "/", de->d_name); if (candidate == NULL) err(1, "malloc"); if (repo_has_git(candidate)) add_repo(repos, nrepos, cap, candidate); free(candidate); } if (closedir(dp) == -1) err(1, "closedir %s", input); } static int repo_cmp(const void *a, const void *b) { const struct repo *ra, *rb; int c; ra = a; rb = b; if (ra->last_date != NULL && rb->last_date != NULL) { c = strcmp(rb->last_date, ra->last_date); if (c != 0) return c; } else if (ra->last_date == NULL && rb->last_date != NULL) { return 1; } else if (ra->last_date != NULL && rb->last_date == NULL) { return -1; } return strcmp(ra->name, rb->name); } static void ensure_dir(const char *path) { char buf[PATH_MAX]; char *p; size_t len; len = strlen(path); if (len == 0) errx(1, "empty path"); if (len >= sizeof(buf)) errx(1, "path too long: %s", path); memcpy(buf, path, len + 1); for (p = buf + 1; *p != '\0'; p++) { if (*p != '/') continue; *p = '\0'; if (mkdir(buf, 0755) == -1 && errno != EEXIST) err(1, "mkdir %s", buf); *p = '/'; } if (mkdir(buf, 0755) == -1 && errno != EEXIST) err(1, "mkdir %s", buf); } static void ensure_parent_dir(const char *path) { char *copy; char *p; copy = str_dup(path); if (copy == NULL) err(1, "malloc"); p = strrchr(copy, '/'); if (p != NULL) { *p = '\0'; if (*copy != '\0') ensure_dir(copy); } free(copy); } static void clear_html_files(const char *dir) { struct dirent *de; char *path; DIR *dp; size_t len; dp = opendir(dir); if (dp == NULL) return; while ((de = readdir(dp)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; len = strlen(de->d_name); if (len < 6 || strcmp(de->d_name + len - 5, ".html") != 0) continue; path = join3(dir, "/", de->d_name); if (path == NULL) err(1, "malloc"); if (unlink(path) == -1 && errno != ENOENT) warn("unlink %s", path); free(path); } if (closedir(dp) == -1) warn("closedir %s", dir); } static char * read_cache_head(const char *outdir) { char buf[256], *path; FILE *fp; path = join3(outdir, "/", ".sgit-cache"); if (path == NULL) err(1, "malloc"); fp = fopen(path, "r"); free(path); if (fp == NULL) return NULL; if (fgets(buf, sizeof(buf), fp) == NULL) { fclose(fp); return NULL; } if (fclose(fp) == EOF) warn("fclose cache"); trim_newline(buf); return str_dup(buf); } static void write_cache_head(const char *outdir, const char *head) { char *path; FILE *fp; path = join3(outdir, "/", ".sgit-cache"); if (path == NULL) err(1, "malloc"); fp = fopen(path, "w"); if (fp == NULL) err(1, "fopen %s", path); fprintf(fp, CACHE_VERSION ":%s\n", head); if (fclose(fp) == EOF) err(1, "fclose %s", path); free(path); } static int load_repo_meta(struct repo *r) { char *fmt, *out, *p; char *author, *branch, *date, *head, *qpath, *subject; qpath = write_shell_quoted(r->path); if (qpath == NULL) err(1, "malloc"); /* Use latest commit from any ref so bare repos without HEAD still work. */ fmt = "%H%x1f%ad%x1f%an%x1f%s"; out = read_cmd_output("git -c safe.directory='*' -C %s log -1 --all --date=iso --pretty=format:%s 2>/dev/null", qpath, fmt); if (out == NULL) errx(1, "failed to get last commit for %s", r->path); trim_newline(out); if (out[0] == '\0') { free(out); free(qpath); return 0; } p = out; if ((head = split_field(&p)) == NULL || (date = split_field(&p)) == NULL || (author = split_field(&p)) == NULL || (subject = split_field(&p)) == NULL) { free(out); free(qpath); return 0; } r->head = str_dup(head); if (r->head == NULL) err(1, "malloc"); branch = read_cmd_output("git -c safe.directory='*' -C %s rev-parse --abbrev-ref HEAD 2>/dev/null", qpath); if (branch == NULL) errx(1, "failed to get branch for %s", r->path); trim_newline(branch); if (branch[0] == '\0' || strcmp(branch, "HEAD") == 0) { free(branch); branch = str_dup("(no HEAD)"); if (branch == NULL) err(1, "malloc"); } r->branch = branch; r->last_date = str_dup(date); if (r->last_date == NULL) err(1, "malloc"); r->last_author = str_dup(author); if (r->last_author == NULL) err(1, "malloc"); r->last_subject = str_dup(subject); if (r->last_subject == NULL) err(1, "malloc"); r->owner = read_git_meta_file(r, "owner"); if (r->owner == NULL) err(1, "malloc"); r->cloneurl = read_git_meta_file(r, "cloneurl"); if (r->cloneurl == NULL) err(1, "malloc"); r->description = read_git_meta_file(r, "description"); if (r->description == NULL) err(1, "malloc"); free(out); free(qpath); return 1; } static char * split_field(char **s) { char *field, *p; if (*s == NULL) return NULL; field = *s; p = strchr(field, SEP); if (p == NULL) { *s = NULL; return field; } *p = '\0'; *s = p + 1; return field; } static int repo_changed(struct repo *r) { char *cached, *expect; int changed; ensure_dir(r->outdir); cached = read_cache_head(r->outdir); if (cached == NULL) return 1; expect = join3(CACHE_VERSION ":", "", r->head); if (expect == NULL) err(1, "malloc"); changed = strcmp(cached, expect) != 0; free(cached); free(expect); return changed; } static void write_html_escaped(FILE *fp, const char *s) { write_html_escaped_len(fp, s, strlen(s)); } static void write_html_escaped_len(FILE *fp, const char *s, size_t len) { size_t i; for (i = 0; i < len; i++) { switch (*s) { case '&': fputs("&", fp); break; case '<': fputs("<", fp); break; case '>': fputs(">", fp); break; case '\"': fputs(""", fp); break; default: fputc((unsigned char)*s, fp); break; } s++; } } static void write_diff_html(FILE *fp, const char *patch) { const char *line, *nl; size_t len; line = patch; while (*line != '\0') { nl = strchr(line, '\n'); if (nl != NULL) len = (size_t)(nl - line); else len = strlen(line); if (len > 0 && line[0] == '+' && strncmp(line, "+++", 3) != 0) fputs("", fp); else if (len > 0 && line[0] == '-' && strncmp(line, "---", 3) != 0) fputs("", fp); else fputs("", fp); write_html_escaped_len(fp, line, len); fputs("", fp); if (nl != NULL) fputc('\n', fp); if (nl == NULL) break; line = nl + 1; } } static void make_commit_page(const struct repo *r, const char *hash, const char *short_hash, const char *date, const char *author, const char *subject) { char *dir, *html, *patch, *qhash, *qpath, *tmp; FILE *fp; dir = join3(r->outdir, "/", "commits"); if (dir == NULL) err(1, "malloc"); ensure_dir(dir); tmp = join3(dir, "/", hash); if (tmp == NULL) err(1, "malloc"); html = join3(tmp, "", ".html"); if (html == NULL) err(1, "malloc"); free(tmp); qpath = write_shell_quoted(r->path); qhash = write_shell_quoted(hash); if (qpath == NULL || qhash == NULL) err(1, "malloc"); patch = read_cmd_output("git -c safe.directory='*' -C %s show --date=short --pretty=format: " "--patch --stat %s 2>/dev/null", qpath, qhash); if (patch == NULL) patch = str_dup(""); if (patch == NULL) err(1, "malloc"); fp = fopen(html, "w"); if (fp == NULL) err(1, "fopen %s", html); fputs("\n\n\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("", fp); write_html_escaped(fp, short_hash); fputs(" - ", fp); write_html_escaped(fp, r->name); fputs("\n", fp); fputs("\n\n

", fp); write_html_escaped(fp, short_hash); fputs("

\n

summary | " "files | " "history | ", fp); write_html_escaped(fp, r->name); fputs("

\n

", fp); fputs("commit : ", fp); write_html_escaped(fp, hash); fputs("
author : ", fp); write_html_escaped(fp, author); fputs("
date : ", fp); write_html_escaped(fp, date); fputs("
subject : ", fp); write_html_escaped(fp, subject); fputs("

\n", fp); fputs("

Diff

\n
", fp);
  write_diff_html(fp, patch);
  fputs("
\n\n\n", fp); if (fclose(fp) == EOF) err(1, "fclose %s", html); free(dir); free(html); free(patch); free(qhash); free(qpath); } static char * read_readme(const struct repo *r, int *is_md) { const char *names[] = { "README.md", "README", "README.txt", "readme.md", "readme", NULL }; char *out, *qhead, *qname, *qpath; size_t i; *is_md = 0; qpath = write_shell_quoted(r->path); qhead = write_shell_quoted(r->head); if (qpath == NULL) err(1, "malloc"); if (qhead == NULL) err(1, "malloc"); for (i = 0; names[i] != NULL; i++) { qname = write_shell_quoted(names[i]); if (qname == NULL) err(1, "malloc"); out = read_cmd_output("git -c safe.directory='*' -C %s show %s:%s 2>/dev/null", qpath, qhead, qname); free(qname); if (out == NULL) continue; if (out[0] != '\0') { *is_md = has_suffix_ci(names[i], ".md"); free(qhead); free(qpath); return out; } free(out); } free(qhead); free(qpath); return str_dup(""); } static int has_suffix_ci(const char *s, const char *suffix) { size_t i, ls, lsf; ls = strlen(s); lsf = strlen(suffix); if (lsf > ls) return 0; for (i = 0; i < lsf; i++) { if (tolower((unsigned char)s[ls - lsf + i]) != tolower((unsigned char)suffix[i])) return 0; } return 1; } static char * repo_gitdir_path(const struct repo *r) { char *gitdir, *qpath, *tmp; qpath = write_shell_quoted(r->path); if (qpath == NULL) err(1, "malloc"); gitdir = read_cmd_output("git -c safe.directory='*' -C %s rev-parse --git-dir 2>/dev/null", qpath); free(qpath); if (gitdir == NULL) return NULL; trim_newline(gitdir); if (gitdir[0] == '\0') { free(gitdir); return NULL; } if (gitdir[0] == '/') return gitdir; tmp = join3(r->path, "/", gitdir); free(gitdir); if (tmp == NULL) err(1, "malloc"); return tmp; } static char * read_git_meta_file(const struct repo *r, const char *name) { char *file, *gitdir, *out; FILE *fp; size_t n; out = NULL; n = 0; gitdir = repo_gitdir_path(r); if (gitdir == NULL) return str_dup(""); file = join3(gitdir, "/", name); if (file == NULL) err(1, "malloc"); free(gitdir); fp = fopen(file, "r"); free(file); if (fp == NULL) return str_dup(""); if (getline(&out, &n, fp) == -1) { if (fclose(fp) == EOF) warn("fclose metadata"); free(out); return str_dup(""); } if (fclose(fp) == EOF) warn("fclose metadata"); trim_newline(out); return out; } static void make_readme_page(const struct repo *r, const char *readme, int readme_md) { char *html; FILE *fp; html = join3(r->outdir, "/", "index.html"); if (html == NULL) err(1, "malloc"); fp = fopen(html, "w"); if (fp == NULL) err(1, "fopen %s", html); fputs("\n\n\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("", fp); write_html_escaped(fp, r->name); fputs(" summary\n", fp); fputs("\n\n", fp); if (strcmp(r->rootlink_repo, "index.html") != 0) fputs("

index

\n", fp); fputs("

", fp); write_html_escaped(fp, r->name); fputs("

\n

summary | files", fp); fputs(" | history

\n

", fp); fputs("branch : ", fp); write_html_escaped(fp, r->branch); fputs("
head : ", fp); write_html_escaped(fp, r->head); fputs("
owner : ", fp); if (r->owner[0] == '\0') fputs("unknown", fp); else write_html_escaped(fp, r->owner); if (r->cloneurl[0] != '\0') { fputs("
clone : git clone ", fp); write_html_escaped(fp, r->cloneurl); } fputs("

\n", fp); if (readme[0] != '\0') { fputs("

readme

\n", fp); if (readme_md) { #ifdef USE_MARKDOWN markdown_render(fp, readme, write_html_escaped_len); #else fputs("
", fp);
          write_html_escaped(fp, readme);
          fputs("
\n", fp); #endif } else { fputs("
", fp);
          write_html_escaped(fp, readme);
          fputs("
\n", fp); } } fputs("\n\n", fp); if (fclose(fp) == EOF) err(1, "fclose %s", html); free(html); } static char * file_page_name(const char *path) { const unsigned char *p; char *out; size_t cap, len; static const char hex[] = "0123456789abcdef"; out = NULL; cap = 0; len = 0; for (p = (const unsigned char *)path; *p != '\0'; p++) { if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '.' || *p == '_' || *p == '-') { append_char(&out, &len, &cap, (char)*p); continue; } /* * Avoid percent-encoding in URL path segments. Some servers decode * %2f into '/' before path lookup, which breaks nested file links. */ append_char(&out, &len, &cap, '_'); append_char(&out, &len, &cap, hex[*p >> 4]); append_char(&out, &len, &cap, hex[*p & 0x0f]); } append_str(&out, &len, &cap, ".html"); if (out == NULL) err(1, "malloc"); return out; } static char * read_file_head(const struct repo *r, const char *path) { char *qpath, *qspec, *spec, *out; spec = join3(r->head, ":", path); if (spec == NULL) err(1, "malloc"); qpath = write_shell_quoted(r->path); qspec = write_shell_quoted(spec); free(spec); if (qpath == NULL || qspec == NULL) err(1, "malloc"); out = read_cmd_output("git -c safe.directory='*' -C %s show %s 2>/dev/null", qpath, qspec); if (out == NULL) out = str_dup(""); if (out == NULL) err(1, "malloc"); free(qpath); free(qspec); return out; } static size_t read_blob_size(const struct repo *r, const char *path) { char *end, *out, *qpath, *qspec, *spec; unsigned long n; spec = join3(r->head, ":", path); if (spec == NULL) err(1, "malloc"); qpath = write_shell_quoted(r->path); qspec = write_shell_quoted(spec); free(spec); if (qpath == NULL || qspec == NULL) err(1, "malloc"); out = read_cmd_output("git -c safe.directory='*' -C %s cat-file -s %s 2>/dev/null", qpath, qspec); free(qpath); free(qspec); if (out == NULL) return 0; trim_newline(out); errno = 0; n = strtoul(out, &end, 10); if (errno != 0 || end == NULL || *end != '\0') { free(out); return 0; } free(out); return (size_t)n; } static void write_raw_blob(const struct repo *r, const char *path) { char *cmd, *dst, *qdst, *qpath, *qspec, *spec; int n, rc; dst = join3(r->outdir, "/raw/", path); if (dst == NULL) err(1, "malloc"); ensure_parent_dir(dst); qpath = write_shell_quoted(r->path); spec = join3(r->head, ":", path); if (qpath == NULL || spec == NULL) err(1, "malloc"); qspec = write_shell_quoted(spec); qdst = write_shell_quoted(dst); free(spec); if (qspec == NULL || qdst == NULL) err(1, "malloc"); n = snprintf(NULL, 0, "git -c safe.directory='*' -C %s cat-file blob %s > %s 2>/dev/null", qpath, qspec, qdst); if (n < 0) errx(1, "snprintf"); cmd = malloc((size_t)n + 1); if (cmd == NULL) err(1, "malloc"); snprintf(cmd, (size_t)n + 1, "git -c safe.directory='*' -C %s cat-file blob %s > %s 2>/dev/null", qpath, qspec, qdst); rc = system(cmd); if (rc != 0) warn("write raw blob %s", path); free(cmd); free(dst); free(qdst); free(qpath); free(qspec); } static struct tree_node * tree_new(const char *name, const char *path, int is_dir) { struct tree_node *n; n = calloc(1, sizeof(*n)); if (n == NULL) err(1, "calloc"); n->name = str_dup(name); if (n->name == NULL) err(1, "malloc"); n->path = str_dup(path); if (n->path == NULL) err(1, "malloc"); n->is_dir = is_dir; return n; } static int tree_cmp(const void *a, const void *b) { const struct tree_node *na, *nb; const struct tree_node *const *pa, *const *pb; pa = a; pb = b; na = *pa; nb = *pb; if (na->is_dir != nb->is_dir) return nb->is_dir - na->is_dir; return strcmp(na->name, nb->name); } static struct tree_node * tree_add_path(struct tree_node *root, const char *path) { char *copy, *curpath, *part, *slash; struct tree_node *cur, *next; size_t i; copy = str_dup(path); curpath = str_dup(""); if (copy == NULL || curpath == NULL) err(1, "malloc"); cur = root; part = copy; while (part != NULL && *part != '\0') { int is_dir; slash = strchr(part, '/'); is_dir = slash != NULL; if (slash != NULL) *slash = '\0'; if (curpath[0] == '\0') next = tree_new(part, part, is_dir); else { char *tmp; tmp = join3(curpath, "/", part); if (tmp == NULL) err(1, "malloc"); next = tree_new(part, tmp, is_dir); free(tmp); } for (i = 0; i < cur->nchildren; i++) { if (strcmp(cur->children[i]->name, next->name) == 0 && cur->children[i]->is_dir == next->is_dir) break; } if (i == cur->nchildren) { struct tree_node **tmp_children; if (cur->nchildren + 1 > cur->cap) { if (cur->cap == 0) cur->cap = 8; else cur->cap *= 2; tmp_children = realloc(cur->children, cur->cap * sizeof(*cur->children)); if (tmp_children == NULL) err(1, "realloc"); cur->children = tmp_children; } cur->children[cur->nchildren++] = next; cur = next; } else { tree_free(next); cur = cur->children[i]; } free(curpath); curpath = str_dup(cur->path); if (curpath == NULL) err(1, "malloc"); part = slash == NULL ? NULL : slash + 1; } free(curpath); free(copy); return root; } static struct tree_node * tree_root_from_files(const char *files) { char *copy, *line; struct tree_node *root; root = tree_new("", "", 1); copy = str_dup(files); if (copy == NULL) err(1, "malloc"); line = strtok(copy, "\n"); while (line != NULL) { tree_add_path(root, line); line = strtok(NULL, "\n"); } free(copy); return root; } static void tree_free(struct tree_node *n) { size_t i; if (n == NULL) return; for (i = 0; i < n->nchildren; i++) tree_free(n->children[i]); free(n->children); free(n->name); free(n->path); free(n); } static void render_tree(FILE *fp, const struct repo *r, struct tree_node *n, int depth) { char *blob, *html, *name; char *content; char hsize[64]; FILE *bfp; size_t blob_size, i, loc, nbytes; if (!n->is_dir) { name = file_page_name(n->path); if (raw_mode) write_raw_blob(r, n->path); blob_size = read_blob_size(r, n->path); content = read_file_head(r, n->path); nbytes = strlen(content); if (!text_content_p(content, nbytes, blob_size)) { if (raw_mode) { fputs("
  • path); fputs("\">", fp); write_html_escaped(fp, n->path); fputs("
  • \n", fp); } else { fputs("
  • ", fp); write_html_escaped(fp, n->path); fputs("
  • \n", fp); } free(content); free(name); return; } fputs("
  • ", fp); write_html_escaped(fp, n->path); fputs("
  • \n", fp); blob = join3(r->outdir, "/file/", name); if (blob == NULL) err(1, "malloc"); html = blob; loc = text_loc(content); bfp = fopen(html, "w"); if (bfp == NULL) err(1, "fopen %s", html); fputs("\n\n\n", bfp); fputs("\n", bfp); fputs("\n", bfp); fputs("\n", bfp); fputs("", bfp); write_html_escaped(bfp, n->path); fputs(" - ", bfp); write_html_escaped(bfp, r->name); fputs("\n", bfp); fputs("\n\n

    summary | " "files | " "history

    \n

    ", bfp); write_html_escaped(bfp, n->path); fputs("

    \n

    ", bfp); fputs("file : ", bfp); write_html_escaped(bfp, n->path); fputs("
    loc : ", bfp); fprintf(bfp, "%lu", (unsigned long)loc); fputs("
    size : ", bfp); format_size(blob_size, hsize, sizeof(hsize)); fprintf(bfp, "%lu bytes (%s)", (unsigned long)blob_size, hsize); if (raw_mode) { fputs("
    raw : path); fputs("\">raw", bfp); } fputs("

    \n
    ", bfp);
          write_html_escaped(bfp, content);
          fputs("
    \n\n\n", bfp); if (fclose(bfp) == EOF) err(1, "fclose %s", html); free(content); free(name); free(html); return; } if (depth == 0) fputs("
      \n", fp); else { fputs("
    • ", fp); write_html_escaped(fp, n->name); fputs("/\n
        \n", fp); } qsort(n->children, n->nchildren, sizeof(*n->children), tree_cmp); for (i = 0; i < n->nchildren; i++) render_tree(fp, r, n->children[i], depth + 1); if (depth == 0) fputs("
      \n", fp); else fputs("
    \n\n", fp); } static void make_files_page(const struct repo *r, const char *files) { struct tree_node *root; char *dir, *html; FILE *fp; root = tree_root_from_files(files); dir = join3(r->outdir, "/", "file"); if (dir == NULL) err(1, "malloc"); ensure_dir(dir); clear_html_files(dir); free(dir); html = join3(r->outdir, "/", "files.html"); if (html == NULL) err(1, "malloc"); fp = fopen(html, "w"); if (fp == NULL) err(1, "fopen %s", html); fputs("\n\n\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("", fp); write_html_escaped(fp, r->name); fputs(" files\n", fp); fputs("\n\n", fp); if (strcmp(r->rootlink_repo, "index.html") != 0) fputs("

    index

    \n", fp); fputs("

    ", fp); write_html_escaped(fp, r->name); fputs("

    \n

    summary | files", fp); fputs(" | history

    \n

    Files

    \n", fp); render_tree(fp, r, root, 0); fputs("\n\n", fp); if (fclose(fp) == EOF) err(1, "fclose %s", html); free(html); tree_free(root); } static void make_repo_page(const struct repo *r) { char *commits, *files, *full_hash, *history, *line, *qhead, *qpath, *readme; char *short_hash, *author, *date, *subject; FILE *fp; int readme_md; qpath = write_shell_quoted(r->path); if (qpath == NULL) err(1, "malloc"); commits = read_cmd_output("git -c safe.directory='*' -C %s log -n %d --date=short " "--pretty=format:%%H%%x1f%%h%%x1f%%ad%%x1f%%an%%x1f%%s 2>/dev/null", qpath, COMMIT_LIMIT); if (commits == NULL) errx(1, "failed to read commits for %s", r->path); qhead = write_shell_quoted(r->head); if (qhead == NULL) err(1, "malloc"); files = read_cmd_output("git -c safe.directory='*' -C %s ls-tree -r --name-only %s 2>/dev/null", qpath, qhead); if (files == NULL) files = str_dup(""); if (files == NULL) err(1, "malloc"); free(qhead); free(qpath); readme = read_readme(r, &readme_md); if (readme == NULL) err(1, "malloc"); make_readme_page(r, readme, readme_md); make_files_page(r, files); history = join3(r->outdir, "/", "history.html"); if (history == NULL) err(1, "malloc"); fp = fopen(history, "w"); if (fp == NULL) err(1, "fopen %s", history); fputs("\n\n\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("", fp); write_html_escaped(fp, r->name); fputs(" history\n\n\n", fp); if (strcmp(r->rootlink_repo, "index.html") != 0) fputs("

    index

    \n", fp); fputs("

    ", fp); write_html_escaped(fp, r->name); fputs("

    \n

    summary | files", fp); fputs(" | history

    \n", fp); fputs("

    History

    \n", fp); fputs("\n\n\n", fp); line = strtok(commits, "\n"); while (line != NULL) { full_hash = split_field(&line); short_hash = split_field(&line); date = split_field(&line); author = split_field(&line); subject = line == NULL ? "" : line; if (full_hash != NULL && short_hash != NULL && date != NULL && author != NULL && subject != NULL) { make_commit_page(r, full_hash, short_hash, date, author, subject); fputs("\n", fp); } line = strtok(NULL, "\n"); } fputs("\n
    hashdateauthorsubject
    ", fp); write_html_escaped(fp, short_hash); fputs("", fp); write_html_escaped(fp, date); fputs("", fp); write_html_escaped(fp, author); fputs("", fp); write_html_escaped(fp, subject); fputs("
    \n\n\n", fp); if (fclose(fp) == EOF) err(1, "fclose %s", history); write_cache_head(r->outdir, r->head); free(history); free(commits); free(files); free(readme); } static void make_index(const char *outdir, struct repo *repos, size_t nrepos) { FILE *fp; char *path; size_t i; path = join3(outdir, "/", "index.html"); if (path == NULL) err(1, "malloc"); fp = fopen(path, "w"); if (fp == NULL) err(1, "fopen %s", path); fputs("\n\n\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("\n", fp); fputs("git index\n", fp); fputs("\n\n

    Repositories

    \n", fp); fputs("\n\n\n", fp); for (i = 0; i < nrepos; i++) { if (repos[i].head == NULL) continue; fputs("\n", fp); } fputs("\n
    repoownerbranchdescriptionlast commit
    ", fp); write_html_escaped(fp, repos[i].name); fputs("", fp); if (repos[i].owner[0] == '\0') fputs("unknown", fp); else write_html_escaped(fp, repos[i].owner); fputs("", fp); write_html_escaped(fp, repos[i].branch); fputs("", fp); write_html_escaped(fp, repos[i].description); fputs("", fp); write_html_escaped(fp, repos[i].last_date); fputs("
    \n\n\n", fp); if (fclose(fp) == EOF) err(1, "fclose %s", path); free(path); } static void free_repos(struct repo *repos, size_t nrepos) { size_t i; for (i = 0; i < nrepos; i++) { free(repos[i].branch); free(repos[i].cloneurl); free(repos[i].head); free(repos[i].description); free(repos[i].last_author); free(repos[i].last_date); free(repos[i].last_subject); free(repos[i].name); free(repos[i].owner); free(repos[i].outdir); free(repos[i].path); free(repos[i].rootlink_deep); free(repos[i].rootlink_repo); } free(repos); }