feat(*): init
6 files changed, -0/+1570
.gitignore
@@ -0,0 +1,4 @@+.dev/+petal/petal+petal/*.o+petal/config.h
petal/Makefile
@@ -0,0 +1,34 @@+include config.mk++SRC = petal.c+OBJ = ${SRC:.c=.o}++all: petal++.c.o:+ ${CC} -c ${CFLAGS} $<++${OBJ}: config.h config.mk++config.h:+ cp config.def.h $@++petal: ${OBJ}+ ${CC} -o $@ ${OBJ} ${LDFLAGS}++clean:+ rm -f petal ${OBJ}++install: all+ mkdir -p ${DESTDIR}${PREFIX}/bin+ cp -f petal ${DESTDIR}${PREFIX}/bin+ chmod 755 ${DESTDIR}${PREFIX}/bin/petal+ mkdir -p ${DESTDIR}${PREFIX}/share/petal+ cp -f style.css ${DESTDIR}${PREFIX}/share/petal+ chmod 644 ${DESTDIR}${PREFIX}/share/petal/style.css++uninstall:+ rm -f ${DESTDIR}${PREFIX}/bin/petal\+ ${DESTDIR}${PREFIX}/share/petal/style.css++.PHONY: all clean install uninstall
petal/config.def.h
@@ -0,0 +1,14 @@+static const char *root = "/srv/git";+static const char *css = "/style.css";+static const char *clone = "[email protected]:";+static const char *title = "greenhouse";+static const char *mark = "❀";+static const char *service = "git";+static const int loglimit = 100;+static const int maxdepth = 8;+static const int barwidth = 40;++static const Service services[] = {+ { "git", "https://git.lostduk.com/" },+ { "lists", "https://lists.lostduk.com/" },+};
petal/config.mk
@@ -0,0 +1,13 @@+PREFIX = /usr/local++GIT2INC = /usr/local/include+GIT2LIB = /usr/local/lib++INCS = -I${GIT2INC}+LIBS = -L${GIT2LIB} -lgit2++CPPFLAGS = -D_XOPEN_SOURCE=700+CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os ${INCS} ${CPPFLAGS}+LDFLAGS = ${LIBS}++CC = cc
petal/petal.c
@@ -0,0 +1,1170 @@+/* imports */+#include <ctype.h>+#include <dirent.h>+#include <limits.h>+#include <stdarg.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <time.h>+#include <sys/stat.h>+#include <git2.h>++/* macros */+#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))+#define HTML "text/html; charset=utf-8"+#define HOUR 3600L+#define DAY (24 * HOUR)++/* enums */+typedef struct {+ const char *name;+ const char *url;+} Service;++/* function declarations */+static int bprintf(char *buf, size_t len, const char *fmt, ...);+static int commitdiff(git_diff **diff, git_commit *commit);+static void commitpage(const char *id);+static const char *envor(const char *name, const char *fallback);+static void filepage(const char *path, const git_tree_entry *e);+static int findref(char *spec, char **rest);+static int headcommit(git_commit **commit, git_repository *r);+static void httpheader(int status, const char *type);+static void indexpage(void);+static void indexrow(const char *name);+static int isrepo(const char *name);+static int loadref(git_reference *r);+static void logpage(void);+static void logrow(const git_oid *oid);+static const char *modestr(git_filemode_t mode);+static void notfound(void);+static int openrepo(git_repository **r, const char *name);+static void pagebegin(int status, const char *pagetitle);+static void pageend(void);+static void printage(const git_time *t);+static void printdiff(git_diff *diff);+static void printname(const char *name);+static void printpath(const char *path);+static void printstat(git_diff *diff);+static void printtime(const git_time *t, const char *fmt);+static void printurl(const char *page, const char *r, const char *path);+static void rawpage(const char *path);+static void readdesc(const char *name, char *buf, size_t len);+static void readfile(const char *name, const char *file, char *buf,+ size_t len);+static void refrows(const char *glob);+static void refspage(void);+static void repoheader(const char *tab);+static void route(char *path);+static void treepage(const char *path);+static void treerow(const git_tree_entry *e, const char *path);+static void treetable(const git_tree *tree, const char *path);+static void urlencode(const char *s);+static int validname(const char *s);+static void walk(const char *rel, int depth);+static void walkent(const char *dir, const char *rel, const char *e,+ int depth);+static void xmlencode(const char *s);+static void xmlencoden(const char *s, size_t len);++/* variables */+static const char refshead[] =+ "<thead><tr><th>Name</th><th>Last commit</th>"+ "<th class=\"sm\">Author</th></tr></thead>\n<tbody>\n";+static const char *base;+static const char *reponame;+static git_repository *repo;+static char ref[PATH_MAX];+static git_oid tip;+static long ofs;++#include "config.h"++/* function implementations */+int+bprintf(char *buf, size_t len, const char *fmt, ...)+{+ va_list ap;+ int n;++ va_start(ap, fmt);+ n = vsnprintf(buf, len, fmt, ap);+ va_end(ap);+ if (n < 0 || (size_t)n >= len)+ return -1;+ return 0;+}++int+commitdiff(git_diff **diff, git_commit *commit)+{+ git_commit *parent = NULL;+ git_tree *tree = NULL, *ptree = NULL;+ git_diff_options opts;+ git_diff_find_options fopts;+ int err;++ *diff = NULL;+ git_diff_options_init(&opts, GIT_DIFF_OPTIONS_VERSION);+ git_diff_find_options_init(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION);+ opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH;+ opts.flags |= GIT_DIFF_IGNORE_SUBMODULES;+ fopts.flags |= GIT_DIFF_FIND_RENAMES;++ if ((err = git_commit_tree(&tree, commit)) < 0)+ goto end;+ if (git_commit_parentcount(commit)) {+ if ((err = git_commit_parent(&parent, commit, 0)) < 0)+ goto end;+ if ((err = git_commit_tree(&ptree, parent)) < 0)+ goto end;+ }+ if ((err = git_diff_tree_to_tree(diff, repo, ptree, tree, &opts)) < 0)+ goto end;+ err = git_diff_find_similar(*diff, &fopts);+end:+ if (err < 0) {+ git_diff_free(*diff);+ *diff = NULL;+ }+ git_tree_free(ptree);+ git_tree_free(tree);+ git_commit_free(parent);+ return err;+}++void+commitpage(const char *id)+{+ char spec[128];+ git_object *obj;+ git_commit *c;+ git_diff *diff;+ const git_signature *a;+ const char *body, *pid;+ unsigned int i;++ if (bprintf(spec, sizeof(spec), "%s^{commit}", id) < 0+ || git_revparse_single(&obj, repo, spec) < 0) {+ notfound();+ return;+ }+ c = (git_commit *)obj;+ a = git_commit_author(c);++ pagebegin(200, git_commit_summary(c));+ repoheader("log");+ fputs("<h2 class=\"subject\">", stdout);+ xmlencode(git_commit_summary(c));+ fputs("</h2>\n", stdout);+ if ((body = git_commit_body(c))) {+ fputs("<pre class=\"body\">", stdout);+ xmlencode(body);+ fputs("</pre>\n", stdout);+ }+ printf("<table class=\"meta\">\n<tr><td>commit</td><td>%s</td></tr>\n",+ git_oid_tostr_s(git_commit_id(c)));+ for (i = 0; i < git_commit_parentcount(c); i++) {+ pid = git_oid_tostr_s(git_commit_parent_id(c, i));+ fputs("<tr><td>parent</td><td><a href=\"", stdout);+ printurl("commit", pid, NULL);+ printf("\">%s</a></td></tr>\n", pid);+ }+ fputs("<tr><td>author</td><td>", stdout);+ xmlencode(a->name);+ fputs(" <", stdout);+ xmlencode(a->email);+ fputs("></td></tr>\n<tr><td>date</td><td>", stdout);+ printtime(&a->when, "%Y-%m-%d %H:%M");+ printf(" %c%02d%02d</td></tr>\n</table>\n", a->when.sign,+ abs(a->when.offset) / 60, abs(a->when.offset) % 60);+ if (commitdiff(&diff, c) >= 0) {+ printstat(diff);+ printdiff(diff);+ }+ pageend();+ git_diff_free(diff);+ git_commit_free(c);+}++const char *+envor(const char *name, const char *fallback)+{+ const char *v;++ v = getenv(name);+ return v && *v ? v : fallback;+}++void+filepage(const char *path, const git_tree_entry *e)+{+ git_blob *blob;+ const char *s, *nl;+ size_t len, i, l, n, lines;+ int binary;++ if (git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0) {+ notfound();+ return;+ }+ s = git_blob_rawcontent(blob);+ len = (size_t)git_blob_rawsize(blob);+ binary = git_blob_is_binary(blob);+ for (i = 0, lines = 0; i < len; i++)+ if (s[i] == '\n')+ lines++;+ if (len && s[len - 1] != '\n')+ lines++;++ pagebegin(200, reponame);+ repoheader("tree");+ printpath(path);+ fputs("<span class=\"info\">", stdout);+ if (!binary)+ printf("%zu lines · ", lines);+ printf("%zuB · <a href=\"", len);+ printurl("raw", ref, path);+ fputs("\">raw</a></span></p>\n", stdout);+ if (binary) {+ fputs("<p>Binary file.</p>\n", stdout);+ } else {+ fputs("<pre class=\"blob\">", stdout);+ for (i = 0, n = 1; i < len; i += l + 1, n++) {+ nl = memchr(s + i, '\n', len - i);+ l = nl ? (size_t)(nl - s) - i : len - i;+ printf("<a id=\"l%zu\" href=\"#l%zu\">%4zu</a> ",+ n, n, n);+ xmlencoden(s + i, l);+ putchar('\n');+ }+ fputs("</pre>\n", stdout);+ }+ pageend();+ git_blob_free(blob);+}++int+findref(char *spec, char **rest)+{+ git_reference *r;+ char *cut = NULL, *p;++ for (;;) {+ if (git_reference_dwim(&r, repo, spec) >= 0+ && loadref(r) >= 0) {+ *rest = cut ? cut + 1 : spec + strlen(spec);+ return 0;+ }+ if (cut)+ *cut = '/';+ p = cut ? cut : spec + strlen(spec);+ while (p > spec && *--p != '/')+ ;+ if (p == spec)+ return -1;+ *p = '\0';+ cut = p;+ }+}++int+headcommit(git_commit **commit, git_repository *r)+{+ git_object *obj;++ if (git_revparse_single(&obj, r, "HEAD^{commit}") < 0)+ return -1;+ *commit = (git_commit *)obj;+ return 0;+}++void+httpheader(int status, const char *type)+{+ if (status == 404)+ fputs("Status: 404 Not Found\r\n", stdout);+ printf("Content-Type: %s\r\n", type);+ fputs("X-Content-Type-Options: nosniff\r\n\r\n", stdout);+}++void+indexpage(void)+{+ pagebegin(200, title);+ fputs("<main>\n<table class=\"repos\">\n<thead><tr><th>Name</th>"+ "<th class=\"sm\">Description</th><th class=\"sm\">Owner</th>"+ "<th class=\"r\">Updated</th></tr></thead>\n<tbody>\n",+ stdout);+ walk("", 0);+ fputs("</tbody>\n</table>\n", stdout);+ pageend();+}++void+indexrow(const char *name)+{+ char desc[256], owner[256];+ git_repository *r = NULL;+ git_commit *commit = NULL;++ readdesc(name, desc, sizeof(desc));+ readfile(name, "owner", owner, sizeof(owner));+ fputs("<tr><td class=\"name\"><a href=\"", stdout);+ xmlencode(base);+ putchar('/');+ urlencode(name);+ fputs("\">", stdout);+ printname(name);+ fputs("</a></td><td class=\"desc sm\">", stdout);+ xmlencode(desc);+ fputs("</td><td class=\"owner sm\">", stdout);+ xmlencode(owner);+ fputs("</td>", stdout);+ if (openrepo(&r, name) >= 0 && headcommit(&commit, r) >= 0)+ printage(&git_commit_committer(commit)->when);+ else+ fputs("<td class=\"r age a4\">—</td>", stdout);+ fputs("</tr>\n", stdout);+ git_commit_free(commit);+ git_repository_free(r);+}++int+isrepo(const char *name)+{+ char path[PATH_MAX];+ struct stat st;++ if (!validname(name))+ return 0;+ if (bprintf(path, sizeof(path), "%s/%s.git", root, name) < 0)+ return 0;+ if (stat(path, &st) < 0)+ return 0;+ return S_ISDIR(st.st_mode);+}++int+loadref(git_reference *r)+{+ git_object *obj;+ int err;++ if ((err = git_reference_peel(&obj, r, GIT_OBJECT_COMMIT)) >= 0) {+ git_oid_cpy(&tip, git_object_id(obj));+ err = bprintf(ref, sizeof(ref), "%s",+ git_reference_shorthand(r));+ git_object_free(obj);+ }+ git_reference_free(r);+ return err;+}++void+logpage(void)+{+ git_revwalk *w = NULL;+ git_oid oid;+ long i;+ int n = 0, more = 0;++ pagebegin(200, reponame);+ repoheader("log");+ fputs("<table class=\"log\">\n<thead><tr><th>Date</th>"+ "<th>Message</th><th class=\"sm\">Author</th>"+ "<th class=\"r sm\">Files</th><th class=\"r\">+</th>"+ "<th class=\"r\">−</th></tr></thead>\n<tbody>\n",+ stdout);+ if (*ref && git_revwalk_new(&w, repo) >= 0+ && git_revwalk_push(w, &tip) >= 0) {+ git_revwalk_sorting(w, GIT_SORT_TIME);+ for (i = 0; i < ofs && git_revwalk_next(&oid, w) >= 0; i++)+ ;+ for (; n < loglimit && git_revwalk_next(&oid, w) >= 0; n++)+ logrow(&oid);+ more = git_revwalk_next(&oid, w) >= 0;+ }+ fputs("</tbody>\n</table>\n", stdout);+ if (!n) {+ fputs(*ref ? "<p>No commits.</p>\n"+ : "<p>Empty repository.</p>\n", stdout);+ } else if (ofs || more) {+ fputs("<nav class=\"pager\">", stdout);+ if (ofs) {+ fputs("<a href=\"", stdout);+ printurl("log", ref, NULL);+ if (ofs > loglimit)+ printf("?ofs=%ld", ofs - loglimit);+ fputs("\">← newer</a>", stdout);+ } else {+ fputs("<span>← newer</span>", stdout);+ }+ printf("<span>%ld–%ld</span>", ofs + 1, ofs + n);+ if (more) {+ fputs("<a href=\"", stdout);+ printurl("log", ref, NULL);+ printf("?ofs=%ld\">older →</a>", ofs + n);+ } else {+ fputs("<span>older →</span>", stdout);+ }+ fputs("</nav>\n", stdout);+ }+ git_revwalk_free(w);+ pageend();+}++void+logrow(const git_oid *oid)+{+ git_commit *commit;+ git_diff *diff;+ git_diff_stats *stats = NULL;+ const git_signature *author;++ if (git_commit_lookup(&commit, repo, oid) < 0)+ return;+ author = git_commit_author(commit);+ fputs("<tr><td class=\"date\" title=\"", stdout);+ printtime(&author->when, "%Y-%m-%d %H:%M");+ fputs("\">", stdout);+ printtime(&author->when, "%Y-%m-%d");+ fputs("</td><td class=\"msg\"><a href=\"", stdout);+ printurl("commit", git_oid_tostr_s(oid), NULL);+ fputs("\">", stdout);+ xmlencode(git_commit_summary(commit));+ fputs("</a></td><td class=\"sm\">", stdout);+ xmlencode(author->name);+ if (commitdiff(&diff, commit) >= 0)+ git_diff_get_stats(&stats, diff);+ if (stats)+ printf("</td><td class=\"r num sm\">%zu</td>"+ "<td class=\"r num ins\">+%zu</td>"+ "<td class=\"r num del\">−%zu</td></tr>\n",+ git_diff_stats_files_changed(stats),+ git_diff_stats_insertions(stats),+ git_diff_stats_deletions(stats));+ else+ fputs("</td><td class=\"sm\"></td><td></td><td></td></tr>\n",+ stdout);+ git_diff_stats_free(stats);+ git_diff_free(diff);+ git_commit_free(commit);+}++const char *+modestr(git_filemode_t mode)+{+ switch (mode) {+ case GIT_FILEMODE_TREE: return "d---------";+ case GIT_FILEMODE_BLOB_EXECUTABLE: return "-rwxr-xr-x";+ case GIT_FILEMODE_LINK: return "lrwxrwxrwx";+ case GIT_FILEMODE_COMMIT: return "m---------";+ default: return "-rw-r--r--";+ }+}++void+notfound(void)+{+ pagebegin(404, "Not found");+ fputs("<main>\n<p>Not found.</p>\n", stdout);+ pageend();+}++int+openrepo(git_repository **r, const char *name)+{+ char path[PATH_MAX];++ if (bprintf(path, sizeof(path), "%s/%s.git", root, name) < 0)+ return -1;+ return git_repository_open_ext(r, path,+ GIT_REPOSITORY_OPEN_NO_SEARCH, NULL);+}++void+pagebegin(int status, const char *pagetitle)+{+ size_t i;++ httpheader(status, HTML);+ fputs("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n"+ "<meta name=\"viewport\" "+ "content=\"width=device-width, initial-scale=1\">\n<title>",+ stdout);+ xmlencode(pagetitle);+ fputs("</title>\n<link rel=\"stylesheet\" href=\"", stdout);+ xmlencode(css);+ fputs("\">\n</head>\n<body>\n<div id=\"top\"><a class=\"brand\" "+ "href=\"", stdout);+ xmlencode(base);+ fputs("/\"><b>", stdout);+ xmlencode(mark);+ fputs("</b> ", stdout);+ xmlencode(title);+ fputs("</a><nav>", stdout);+ for (i = 0; i < LENGTH(services); i++) {+ if (!strcmp(services[i].name, service)) {+ fputs("<a class=\"on\" href=\"", stdout);+ xmlencode(base);+ putchar('/');+ } else {+ fputs("<a href=\"", stdout);+ xmlencode(services[i].url);+ }+ fputs("\">", stdout);+ xmlencode(services[i].name);+ fputs("</a>", stdout);+ }+ fputs("</nav></div>\n", stdout);+}++void+pageend(void)+{+ fputs("</main>\n</body>\n</html>\n", stdout);+}++void+printage(const git_time *t)+{+ static const char *units[] = {+ "minute", "hour", "day", "week", "month", "year"+ };+ static const long secs[] = {+ 60, HOUR, DAY, 7 * DAY, 30 * DAY, 365 * DAY+ };+ static const long upto[] = {+ HOUR, DAY, 14 * DAY, 60 * DAY, 730 * DAY, LONG_MAX+ };+ static const long fade[] = { 7 * DAY, 30 * DAY, 180 * DAY, 730 * DAY };+ long d, n;+ size_t i, level;++ if ((d = (long)(time(NULL) - (time_t)t->time)) < 0)+ d = 0;+ for (i = 0; d >= upto[i]; i++)+ ;+ if ((n = d / secs[i]) < 1)+ n = 1;+ for (level = 0; level < LENGTH(fade) && d >= fade[level]; level++)+ ;+ printf("<td class=\"r age a%zu\" title=\"", level);+ printtime(t, "%Y-%m-%d %H:%M");+ printf("\">%ld %s%s</td>", n, units[i], n == 1 ? "" : "s");+}++void+printdiff(git_diff *diff)+{+ git_patch *patch;+ const git_diff_delta *delta;+ const git_diff_hunk *hunk;+ const git_diff_line *line;+ size_t i, h, l, nhunks, nlines, len;++ for (i = 0; i < git_diff_num_deltas(diff); i++) {+ if (git_patch_from_diff(&patch, diff, i) < 0)+ continue;+ delta = git_patch_get_delta(patch);+ printf("<h3 id=\"f%zu\">", i);+ if (strcmp(delta->old_file.path, delta->new_file.path)) {+ xmlencode(delta->old_file.path);+ fputs(" → ", stdout);+ }+ xmlencode(delta->new_file.path);+ fputs("</h3>\n<pre class=\"diff\">", stdout);+ if (delta->flags & GIT_DIFF_FLAG_BINARY)+ fputs("<span>Binary files differ.</span>", stdout);+ nhunks = git_patch_num_hunks(patch);+ for (h = 0; h < nhunks; h++) {+ if (git_patch_get_hunk(&hunk, &nlines, patch, h) < 0)+ break;+ len = hunk->header_len;+ while (len && hunk->header[len - 1] == '\n')+ len--;+ fputs("<span class=\"h\">", stdout);+ xmlencoden(hunk->header, len);+ fputs("</span>", stdout);+ for (l = 0; l < nlines; l++) {+ if (git_patch_get_line_in_hunk(&line, patch,+ h, l) < 0)+ break;+ if (line->origin == GIT_DIFF_LINE_ADDITION)+ fputs("<span class=\"i\">+", stdout);+ else if (line->origin ==+ GIT_DIFF_LINE_DELETION)+ fputs("<span class=\"d\">-", stdout);+ else if (line->origin == GIT_DIFF_LINE_CONTEXT)+ fputs("<span> ", stdout);+ else+ continue;+ len = line->content_len;+ if (len && line->content[len - 1] == '\n')+ len--;+ xmlencoden(line->content, len);+ fputs("</span>", stdout);+ }+ }+ fputs("</pre>\n", stdout);+ git_patch_free(patch);+ }+}++void+printname(const char *name)+{+ const char *p;++ if ((p = strrchr(name, '/'))) {+ fputs("<span class=\"dir\">", stdout);+ xmlencoden(name, p - name + 1);+ fputs("</span>", stdout);+ name = p + 1;+ }+ xmlencode(name);+}++void+printpath(const char *path)+{+ char prefix[PATH_MAX];+ const char *p, *q, *s;++ fputs("<p class=\"path\">", stdout);+ if ((s = strrchr(reponame, '/')))+ s++;+ else+ s = reponame;+ if (!*path) {+ xmlencode(s);+ return;+ }+ fputs("<a href=\"", stdout);+ printurl("tree", ref, NULL);+ fputs("\">", stdout);+ xmlencode(s);+ fputs("</a>", stdout);+ for (p = path; (q = strchr(p, '/')); p = q + 1) {+ if (bprintf(prefix, sizeof(prefix), "%.*s",+ (int)(q - path), path) < 0)+ return;+ fputs(" / <a href=\"", stdout);+ printurl("tree", ref, prefix);+ fputs("\">", stdout);+ xmlencoden(p, q - p);+ fputs("</a>", stdout);+ }+ fputs(" / ", stdout);+ xmlencode(p);+}++void+printstat(git_diff *diff)+{+ git_patch *patch;+ size_t i, j, n, add, del, max = 0, tadd = 0, tdel = 0;+ size_t *st;++ n = git_diff_num_deltas(diff);+ if (!(st = calloc(n * 2 + 1, sizeof(*st))))+ return;+ for (i = 0; i < n; i++) {+ if (git_patch_from_diff(&patch, diff, i) < 0)+ continue;+ git_patch_line_stats(NULL, &st[i * 2], &st[i * 2 + 1], patch);+ git_patch_free(patch);+ if (st[i * 2] + st[i * 2 + 1] > max)+ max = st[i * 2] + st[i * 2 + 1];+ }+ fputs("<table class=\"stat\">\n", stdout);+ for (i = 0; i < n; i++) {+ add = st[i * 2];+ del = st[i * 2 + 1];+ tadd += add;+ tdel += del;+ printf("<tr><td><a href=\"#f%zu\">", i);+ xmlencode(git_diff_get_delta(diff, i)->new_file.path);+ printf("</a></td><td class=\"r num\">%zu</td>"+ "<td class=\"bar\"><span class=\"ins\">", add + del);+ if (max > (size_t)barwidth) {+ add = (add * barwidth + max - 1) / max;+ del = (del * barwidth + max - 1) / max;+ }+ for (j = 0; j < add; j++)+ putchar('+');+ fputs("</span><span class=\"del\">", stdout);+ for (j = 0; j < del; j++)+ putchar('-');+ fputs("</span></td></tr>\n", stdout);+ }+ printf("</table>\n<p class=\"total\">%zu file%s changed, "+ "<span class=\"ins\">+%zu</span> "+ "<span class=\"del\">−%zu</span></p>\n",+ n, n == 1 ? "" : "s", tadd, tdel);+ free(st);+}++void+printtime(const git_time *t, const char *fmt)+{+ struct tm tm;+ time_t secs;+ char buf[32];++ secs = (time_t)t->time + (time_t)t->offset * 60;+ if (!gmtime_r(&secs, &tm))+ return;+ if (strftime(buf, sizeof(buf), fmt, &tm))+ fputs(buf, stdout);+}++void+printurl(const char *page, const char *r, const char *path)+{+ xmlencode(base);+ putchar('/');+ urlencode(reponame);+ printf("/%s", page);+ if (r && *r) {+ putchar('/');+ urlencode(r);+ }+ if (path && *path) {+ putchar('/');+ urlencode(path);+ }+}++void+rawpage(const char *path)+{+ git_commit *c = NULL;+ git_tree *tree = NULL;+ git_tree_entry *e = NULL;+ git_blob *blob = NULL;++ if (git_commit_lookup(&c, repo, &tip) < 0+ || git_commit_tree(&tree, c) < 0+ || git_tree_entry_bypath(&e, tree, path) < 0+ || git_tree_entry_type(e) != GIT_OBJECT_BLOB+ || git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0) {+ notfound();+ goto end;+ }+ httpheader(200, git_blob_is_binary(blob) ? "application/octet-stream"+ : "text/plain; charset=utf-8");+ fwrite(git_blob_rawcontent(blob), 1, (size_t)git_blob_rawsize(blob),+ stdout);+end:+ git_blob_free(blob);+ git_tree_entry_free(e);+ git_tree_free(tree);+ git_commit_free(c);+}++void+readdesc(const char *name, char *buf, size_t len)+{+ readfile(name, "description", buf, len);+ if (!strncmp(buf, "Unnamed repository", 18))+ buf[0] = '\0';+}++void+readfile(const char *name, const char *file, char *buf, size_t len)+{+ char path[PATH_MAX];+ FILE *fp;++ buf[0] = '\0';+ if (bprintf(path, sizeof(path), "%s/%s.git/%s", root, name, file) < 0)+ return;+ if (!(fp = fopen(path, "r")))+ return;+ if (fgets(buf, (int)len, fp))+ buf[strcspn(buf, "\r\n")] = '\0';+ fclose(fp);+}++void+refrows(const char *glob)+{+ git_reference_iterator *it;+ git_reference *r;+ git_object *obj;+ const git_signature *author;++ if (git_reference_iterator_glob_new(&it, repo, glob) < 0)+ return;+ while (git_reference_next(&r, it) >= 0) {+ fputs("<tr><td class=\"name\"><a href=\"", stdout);+ printurl("log", git_reference_shorthand(r), NULL);+ fputs("\">", stdout);+ xmlencode(git_reference_shorthand(r));+ fputs("</a></td><td class=\"date\">", stdout);+ if (git_reference_peel(&obj, r, GIT_OBJECT_COMMIT) >= 0) {+ author = git_commit_author((git_commit *)obj);+ printtime(&author->when, "%Y-%m-%d %H:%M");+ fputs("</td><td class=\"sm\">", stdout);+ xmlencode(author->name);+ git_object_free(obj);+ } else {+ fputs("</td><td class=\"sm\">", stdout);+ }+ fputs("</td></tr>\n", stdout);+ git_reference_free(r);+ }+ git_reference_iterator_free(it);+}++void+refspage(void)+{+ pagebegin(200, reponame);+ repoheader("refs");+ printf("<h2>Branches</h2>\n<table class=\"refs\">\n%s", refshead);+ refrows("refs/heads/*");+ printf("</tbody>\n</table>\n<h2>Tags</h2>\n"+ "<table class=\"refs\">\n%s", refshead);+ refrows("refs/tags/*");+ fputs("</tbody>\n</table>\n", stdout);+ pageend();+}++void+repoheader(const char *tab)+{+ static const char *tabs[] = { "log", "tree", "refs" };+ char desc[256];+ size_t i;++ readdesc(reponame, desc, sizeof(desc));+ fputs("<header>\n<h1>", stdout);+ printname(reponame);+ fputs("</h1>\n", stdout);+ if (*desc) {+ fputs("<p class=\"desc\">", stdout);+ xmlencode(desc);+ fputs("</p>\n", stdout);+ }+ fputs("<p class=\"clone\"><span>git clone</span> ", stdout);+ xmlencode(clone);+ xmlencode(reponame);+ fputs(".git</p>\n<nav>", stdout);+ for (i = 0; i < LENGTH(tabs); i++) {+ printf("<a%s href=\"", strcmp(tab, tabs[i]) ? ""+ : " class=\"on\"");+ printurl(tabs[i], i < 2 ? ref : NULL, NULL);+ printf("\">%s</a>", tabs[i]);+ }+ if (*ref) {+ fputs("<span class=\"ref\">ref <a href=\"", stdout);+ printurl("refs", NULL, NULL);+ fputs("\">", stdout);+ xmlencode(ref);+ fputs("</a></span>", stdout);+ }+ fputs("</nav>\n</header>\n<main>\n", stdout);+}++void+route(char *path)+{+ git_reference *r;+ char *rest = NULL, *page, *arg, *cut;++ while (*path == '/')+ path++;+ for (cut = path + strlen(path); cut > path && cut[-1] == '/'; cut--)+ cut[-1] = '\0';+ if (!*path) {+ indexpage();+ return;+ }+ while (!isrepo(path)) {+ if (!(cut = strrchr(path, '/'))) {+ notfound();+ return;+ }+ if (rest)+ rest[-1] = '/';+ *cut = '\0';+ rest = cut + 1;+ }+ if (openrepo(&repo, path) < 0) {+ notfound();+ return;+ }+ reponame = path;+ if (git_repository_head(&r, repo) >= 0)+ loadref(r);++ page = rest ? rest : "log";+ if ((arg = strchr(page, '/')))+ *arg++ = '\0';+ else+ arg = "";+ if (!strcmp(page, "log")) {+ if (*arg && (findref(arg, &rest) < 0 || *rest))+ notfound();+ else+ logpage();+ } else if (!strcmp(page, "tree")) {+ if (*arg && findref(arg, &rest) < 0)+ notfound();+ else+ treepage(*arg ? rest : "");+ } else if (!strcmp(page, "raw")) {+ if (findref(arg, &rest) < 0 || !*rest)+ notfound();+ else+ rawpage(rest);+ } else if (!strcmp(page, "commit") && *arg) {+ commitpage(arg);+ } else if (!strcmp(page, "refs") && !*arg) {+ refspage();+ } else {+ notfound();+ }+ git_repository_free(repo);+}++void+treepage(const char *path)+{+ git_commit *c = NULL;+ git_tree *root = NULL, *tree = NULL;+ git_tree_entry *e = NULL;++ if (*ref && (git_commit_lookup(&c, repo, &tip) < 0+ || git_commit_tree(&root, c) < 0))+ goto fail;+ if (*path) {+ if (!root || git_tree_entry_bypath(&e, root, path) < 0)+ goto fail;+ if (git_tree_entry_type(e) == GIT_OBJECT_BLOB) {+ filepage(path, e);+ goto end;+ }+ if (git_tree_entry_type(e) != GIT_OBJECT_TREE+ || git_tree_lookup(&tree, repo, git_tree_entry_id(e)) < 0)+ goto fail;+ }+ pagebegin(200, reponame);+ repoheader("tree");+ printpath(path);+ fputs("</p>\n", stdout);+ if (root)+ treetable(tree ? tree : root, path);+ else+ fputs("<p>Empty repository.</p>\n", stdout);+ pageend();+ goto end;+fail:+ notfound();+end:+ git_tree_entry_free(e);+ git_tree_free(tree);+ git_tree_free(root);+ git_commit_free(c);+}++void+treerow(const git_tree_entry *e, const char *path)+{+ char child[PATH_MAX];+ const char *name;+ git_object_t type;+ git_blob *blob;++ name = git_tree_entry_name(e);+ type = git_tree_entry_type(e);+ if (bprintf(child, sizeof(child), "%s%s%s", path, *path ? "/" : "",+ name) < 0)+ return;+ printf("<tr><td class=\"mode sm\">%s</td><td class=\"%s\">",+ modestr(git_tree_entry_filemode(e)),+ type == GIT_OBJECT_TREE ? "dirent" : "name");+ if (type == GIT_OBJECT_COMMIT) {+ xmlencode(name);+ } else {+ fputs("<a href=\"", stdout);+ printurl("tree", ref, child);+ fputs("\">", stdout);+ xmlencode(name);+ fputs(type == GIT_OBJECT_TREE ? "/</a>" : "</a>", stdout);+ }+ fputs("</td><td class=\"r num\">", stdout);+ if (type == GIT_OBJECT_BLOB+ && git_blob_lookup(&blob, repo, git_tree_entry_id(e)) >= 0) {+ printf("%lluB", (unsigned long long)git_blob_rawsize(blob));+ git_blob_free(blob);+ }+ fputs("</td></tr>\n", stdout);+}++void+treetable(const git_tree *tree, const char *path)+{+ char parent[PATH_MAX];+ const git_tree_entry *e;+ const char *p;+ size_t i, n;+ int pass, isdir;++ fputs("<table class=\"tree\">\n<thead><tr><th class=\"sm\">Mode</th>"+ "<th>Name</th><th class=\"r\">Size</th></tr></thead>\n"+ "<tbody>\n", stdout);+ if (*path) {+ p = strrchr(path, '/');+ if (bprintf(parent, sizeof(parent), "%.*s",+ p ? (int)(p - path) : 0, path) < 0)+ return;+ fputs("<tr><td class=\"mode sm\">d---------</td>"+ "<td class=\"dirent\"><a href=\"", stdout);+ printurl("tree", ref, parent);+ fputs("\">../</a></td><td></td></tr>\n", stdout);+ }+ n = git_tree_entrycount(tree);+ for (pass = 0; pass < 2; pass++) {+ for (i = 0; i < n; i++) {+ e = git_tree_entry_byindex(tree, i);+ isdir = git_tree_entry_type(e) == GIT_OBJECT_TREE;+ if (isdir == pass)+ continue;+ treerow(e, path);+ }+ }+ fputs("</tbody>\n</table>\n", stdout);+}++void+urlencode(const char *s)+{+ for (; *s; s++) {+ if (isalnum((unsigned char)*s) || strchr("-._~/", *s))+ putchar(*s);+ else+ printf("%%%02X", (unsigned char)*s);+ }+}++int+validname(const char *s)+{+ unsigned char c;+ int start = 1;++ if (!*s)+ return 0;+ for (; *s; s++) {+ c = *s;+ if (c == '/') {+ if (start)+ return 0;+ start = 1;+ continue;+ }+ if (start && !(isalnum(c) || c == '_'))+ return 0;+ if (!(isalnum(c) || c == '.' || c == '_' || c == '-'))+ return 0;+ start = 0;+ }+ return !start;+}++void+walk(const char *rel, int depth)+{+ char dir[PATH_MAX];+ struct dirent **ents;+ int i, n;++ if (depth > maxdepth)+ return;+ if (bprintf(dir, sizeof(dir), "%s/%s", root, rel) < 0)+ return;+ if ((n = scandir(dir, &ents, NULL, alphasort)) < 0)+ return;+ for (i = 0; i < n; i++) {+ walkent(dir, rel, ents[i]->d_name, depth);+ free(ents[i]);+ }+ free(ents);+}++void+walkent(const char *dir, const char *rel, const char *e, int depth)+{+ char path[PATH_MAX], name[PATH_MAX];+ struct stat st;+ size_t len;++ if (e[0] == '.')+ return;+ if (bprintf(path, sizeof(path), "%s/%s", dir, e) < 0)+ return;+ if (lstat(path, &st) < 0 || !S_ISDIR(st.st_mode))+ return;+ if (bprintf(name, sizeof(name), "%s%s%s", rel, *rel ? "/" : "", e) < 0)+ return;+ len = strlen(e);+ if (len <= 4 || strcmp(e + len - 4, ".git")) {+ walk(name, depth + 1);+ return;+ }+ name[strlen(name) - 4] = '\0';+ if (validname(name))+ indexrow(name);+}++void+xmlencode(const char *s)+{+ if (s)+ xmlencoden(s, strlen(s));+}++void+xmlencoden(const char *s, size_t len)+{+ for (; len; s++, len--) {+ switch (*s) {+ case '<': fputs("<", stdout); break;+ case '>': fputs(">", stdout); break;+ case '&': fputs("&", stdout); break;+ case '"': fputs(""", stdout); break;+ case '\'': fputs("'", stdout); break;+ default: putchar(*s);+ }+ }+}++/* main */+int+main(void)+{+ char path[PATH_MAX];+ const char *q;++ root = envor("PETAL_ROOT", root);+ css = envor("PETAL_CSS", css);+ clone = envor("PETAL_CLONE", clone);+ title = envor("PETAL_TITLE", title);+ base = envor("SCRIPT_NAME", "");+ q = envor("QUERY_STRING", "");+ if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)+ ofs = 0;++ git_libgit2_init();+ if (bprintf(path, sizeof(path), "%s", envor("PATH_INFO", "")) < 0)+ notfound();+ else+ route(path);+ git_libgit2_shutdown();+ return 0;+}
petal/style.css
@@ -0,0 +1,335 @@+:root {+ --bg: #0d0f0d;+ --fg: #c8cec3;+ --hi: #eef2ea;+ --dim: #6c7569;+ --faint: #1c201c;+ --leaf: #9fd47c;+ --petal: #f08fae;+ --a0: #9fd47c;+ --a1: #b9cfa8;+ --a2: #8d9689;+ --a3: #626a5f;+ --a4: #424840;+}++@media (prefers-color-scheme: light) {+ :root {+ --bg: #fafaf7;+ --fg: #2a2e28;+ --hi: #000;+ --dim: #7a8277;+ --faint: #ecefe8;+ --leaf: #2f6d1a;+ --petal: #b0345c;+ --a0: #2f6d1a;+ --a1: #577a47;+ --a2: #7a8277;+ --a3: #a2a89f;+ --a4: #c4c8c1;+ }+}++body {+ max-width: 980px;+ margin: 0 auto;+ padding: 1.25rem 1.5rem 3rem;+ background: var(--bg);+ color: var(--fg);+ font: 14px/1.6 ui-monospace, "JetBrains Mono", "DejaVu Sans Mono", monospace;+}++a {+ color: var(--hi);+ text-decoration: none;+}++a:hover {+ color: var(--leaf);+}++#top {+ display: flex;+ gap: 2ch;+ padding-bottom: 0.75rem;+ border-bottom: 1px dashed var(--dim);+}++#top .brand b {+ color: var(--petal);+ font-weight: normal;+}++#top nav, header nav {+ display: flex;+ gap: 1ch;+}++#top nav a, header nav a {+ padding: 0 1ch;+ color: var(--dim);+}++#top nav a.on, header nav a.on {+ background: var(--leaf);+ color: var(--bg);+}++header {+ padding: 2.25rem 0 0;+}++h1 {+ margin: 0;+ font-size: 1.75rem;+ line-height: 1.2;+ letter-spacing: -0.02em;+ color: var(--hi);+}++.desc {+ color: var(--dim);+}++header .desc {+ margin: 0.35rem 0 0;+}++.clone {+ display: inline-block;+ margin: 1.1rem 0 0;+ padding: 0.4rem 1.5ch 0.4rem 1.25ch;+ background: var(--faint);+ border-left: 2px solid var(--leaf);+}++.clone span {+ color: var(--dim);+ user-select: none;+}++.clone span::before {+ content: "$ ";+ color: var(--leaf);+}++header nav {+ margin: 1.25rem 0 0;+}++header nav a {+ margin-left: -1ch;+ margin-right: 1ch;+}++main {+ padding-top: 1.5rem;+}++table {+ width: 100%;+ border-collapse: collapse;+}++th {+ padding: 0 2ch 0.4rem 0;+ text-align: left;+ font-weight: normal;+ color: var(--dim);+}++td {+ padding: 0.1rem 2ch 0.1rem 0;+ white-space: nowrap;+ vertical-align: top;+}++th:last-child, td:last-child {+ padding-right: 0;+}++tbody tr:hover td {+ background: var(--faint);+}++.r {+ text-align: right;+}++.desc, .msg {+ width: 100%;+ white-space: normal;+}++.msg a {+ color: var(--fg);+}++.dir, .owner, .date, .num {+ color: var(--dim);+}++.ins { color: var(--leaf); }+.del { color: var(--petal); }+.a0 { color: var(--a0); }+.a1 { color: var(--a1); }+.a2 { color: var(--a2); }+.a3 { color: var(--a3); }+.a4 { color: var(--a4); }++header nav .ref {+ margin-left: auto;+ color: var(--dim);+}++header nav .ref a {+ margin: 0;+ padding: 0;+ color: var(--hi);+}++h2 {+ margin: 1.75rem 0 0.5rem;+ font-size: 1em;+ color: var(--dim);+}++h2:first-child {+ margin-top: 0;+}++h2:not(.subject)::before {+ content: "## ";+}++.pager {+ display: flex;+ gap: 3ch;+ margin-top: 1.25rem;+ color: var(--dim);+}++.pager a {+ color: var(--leaf);+}++.path {+ margin: 0 0 1rem;+ color: var(--dim);+}++.path .info {+ float: right;+}++.mode, .num {+ color: var(--dim);+}++.dirent a {+ color: var(--leaf);+}++pre {+ margin: 0;+ font: inherit;+ overflow-x: auto;+}++.blob a {+ color: var(--a4);+ margin-right: 1ch;+ user-select: none;+}++.blob a:hover, .blob a:target {+ color: var(--leaf);+}++.subject {+ margin: 0 0 1rem;+ font-size: 1.15rem;+ color: var(--hi);+}++.body {+ margin: 0 0 1.25rem;+ color: var(--fg);+ white-space: pre-wrap;+}++.meta {+ width: auto;+ margin-bottom: 1.5rem;+}++.meta td:first-child {+ color: var(--dim);+}++.stat {+ width: auto;+}++.stat td {+ padding-top: 0;+ padding-bottom: 0;+}++.bar {+ letter-spacing: -0.05em;+}++.total {+ margin: 0.5rem 0 2rem;+ color: var(--dim);+}++h3 {+ margin: 1.75rem 0 0;+ padding: 0.3rem 1ch;+ font-size: 1em;+ font-weight: normal;+ color: var(--hi);+ background: var(--faint);+}++.diff {+ padding: 0.5rem 0;+}++.diff span {+ display: block;+}++.diff .h {+ color: var(--dim);+}++.diff .i {+ color: var(--leaf);+ background: color-mix(in srgb, var(--leaf) 8%, transparent);+}++.diff .d {+ color: var(--petal);+ background: color-mix(in srgb, var(--petal) 8%, transparent);+}++@media (max-width: 640px) {+ .sm {+ display: none;+ }+}++.diff span {+ min-height: 1.6em;+}++.tree .name, .tree .dirent {+ width: 100%;+}++.refs .name {+ width: 100%;+}