greenhouse

greenhouse

forge suite

git clone [email protected]:greenhouse.git

greenhouse / petal / petal.c1171 lines · 28325B · raw

1/* imports */
2#include <ctype.h>
3#include <dirent.h>
4#include <limits.h>
5#include <stdarg.h>
6#include <stdio.h>
7#include <stdlib.h>
8#include <string.h>
9#include <time.h>
10#include <sys/stat.h>
11#include <git2.h>
12
13/* macros */
14#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))
15#define HTML      "text/html; charset=utf-8"
16#define HOUR      3600L
17#define DAY       (24 * HOUR)
18
19/* enums */
20typedef struct {
21	const char *name;
22	const char *url;
23} Service;
24
25/* function declarations */
26static int bprintf(char *buf, size_t len, const char *fmt, ...);
27static int commitdiff(git_diff **diff, git_commit *commit);
28static void commitpage(const char *id);
29static const char *envor(const char *name, const char *fallback);
30static void filepage(const char *path, const git_tree_entry *e);
31static int findref(char *spec, char **rest);
32static int headcommit(git_commit **commit, git_repository *r);
33static void httpheader(int status, const char *type);
34static void indexpage(void);
35static void indexrow(const char *name);
36static int isrepo(const char *name);
37static int loadref(git_reference *r);
38static void logpage(void);
39static void logrow(const git_oid *oid);
40static const char *modestr(git_filemode_t mode);
41static void notfound(void);
42static int openrepo(git_repository **r, const char *name);
43static void pagebegin(int status, const char *pagetitle);
44static void pageend(void);
45static void printage(const git_time *t);
46static void printdiff(git_diff *diff);
47static void printname(const char *name);
48static void printpath(const char *path);
49static void printstat(git_diff *diff);
50static void printtime(const git_time *t, const char *fmt);
51static void printurl(const char *page, const char *r, const char *path);
52static void rawpage(const char *path);
53static void readdesc(const char *name, char *buf, size_t len);
54static void readfile(const char *name, const char *file, char *buf,
55                     size_t len);
56static void refrows(const char *glob);
57static void refspage(void);
58static void repoheader(const char *tab);
59static void route(char *path);
60static void treepage(const char *path);
61static void treerow(const git_tree_entry *e, const char *path);
62static void treetable(const git_tree *tree, const char *path);
63static void urlencode(const char *s);
64static int validname(const char *s);
65static void walk(const char *rel, int depth);
66static void walkent(const char *dir, const char *rel, const char *e,
67                    int depth);
68static void xmlencode(const char *s);
69static void xmlencoden(const char *s, size_t len);
70
71/* variables */
72static const char refshead[] =
73	"<thead><tr><th>Name</th><th>Last commit</th>"
74	"<th class=\"sm\">Author</th></tr></thead>\n<tbody>\n";
75static const char *base;
76static const char *reponame;
77static git_repository *repo;
78static char ref[PATH_MAX];
79static git_oid tip;
80static long ofs;
81
82#include "config.h"
83
84/* function implementations */
85int
86bprintf(char *buf, size_t len, const char *fmt, ...)
87{
88	va_list ap;
89	int n;
90
91	va_start(ap, fmt);
92	n = vsnprintf(buf, len, fmt, ap);
93	va_end(ap);
94	if (n < 0 || (size_t)n >= len)
95		return -1;
96	return 0;
97}
98
99int
100commitdiff(git_diff **diff, git_commit *commit)
101{
102	git_commit *parent = NULL;
103	git_tree *tree = NULL, *ptree = NULL;
104	git_diff_options opts;
105	git_diff_find_options fopts;
106	int err;
107
108	*diff = NULL;
109	git_diff_options_init(&opts, GIT_DIFF_OPTIONS_VERSION);
110	git_diff_find_options_init(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION);
111	opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH;
112	opts.flags |= GIT_DIFF_IGNORE_SUBMODULES;
113	fopts.flags |= GIT_DIFF_FIND_RENAMES;
114
115	if ((err = git_commit_tree(&tree, commit)) < 0)
116		goto end;
117	if (git_commit_parentcount(commit)) {
118		if ((err = git_commit_parent(&parent, commit, 0)) < 0)
119			goto end;
120		if ((err = git_commit_tree(&ptree, parent)) < 0)
121			goto end;
122	}
123	if ((err = git_diff_tree_to_tree(diff, repo, ptree, tree, &opts)) < 0)
124		goto end;
125	err = git_diff_find_similar(*diff, &fopts);
126end:
127	if (err < 0) {
128		git_diff_free(*diff);
129		*diff = NULL;
130	}
131	git_tree_free(ptree);
132	git_tree_free(tree);
133	git_commit_free(parent);
134	return err;
135}
136
137void
138commitpage(const char *id)
139{
140	char spec[128];
141	git_object *obj;
142	git_commit *c;
143	git_diff *diff;
144	const git_signature *a;
145	const char *body, *pid;
146	unsigned int i;
147
148	if (bprintf(spec, sizeof(spec), "%s^{commit}", id) < 0
149	|| git_revparse_single(&obj, repo, spec) < 0) {
150		notfound();
151		return;
152	}
153	c = (git_commit *)obj;
154	a = git_commit_author(c);
155
156	pagebegin(200, git_commit_summary(c));
157	repoheader("log");
158	fputs("<h2 class=\"subject\">", stdout);
159	xmlencode(git_commit_summary(c));
160	fputs("</h2>\n", stdout);
161	if ((body = git_commit_body(c))) {
162		fputs("<pre class=\"body\">", stdout);
163		xmlencode(body);
164		fputs("</pre>\n", stdout);
165	}
166	printf("<table class=\"meta\">\n<tr><td>commit</td><td>%s</td></tr>\n",
167		git_oid_tostr_s(git_commit_id(c)));
168	for (i = 0; i < git_commit_parentcount(c); i++) {
169		pid = git_oid_tostr_s(git_commit_parent_id(c, i));
170		fputs("<tr><td>parent</td><td><a href=\"", stdout);
171		printurl("commit", pid, NULL);
172		printf("\">%s</a></td></tr>\n", pid);
173	}
174	fputs("<tr><td>author</td><td>", stdout);
175	xmlencode(a->name);
176	fputs(" &lt;", stdout);
177	xmlencode(a->email);
178	fputs("&gt;</td></tr>\n<tr><td>date</td><td>", stdout);
179	printtime(&a->when, "%Y-%m-%d %H:%M");
180	printf(" %c%02d%02d</td></tr>\n</table>\n", a->when.sign,
181		abs(a->when.offset) / 60, abs(a->when.offset) % 60);
182	if (commitdiff(&diff, c) >= 0) {
183		printstat(diff);
184		printdiff(diff);
185	}
186	pageend();
187	git_diff_free(diff);
188	git_commit_free(c);
189}
190
191const char *
192envor(const char *name, const char *fallback)
193{
194	const char *v;
195
196	v = getenv(name);
197	return v && *v ? v : fallback;
198}
199
200void
201filepage(const char *path, const git_tree_entry *e)
202{
203	git_blob *blob;
204	const char *s, *nl;
205	size_t len, i, l, n, lines;
206	int binary;
207
208	if (git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0) {
209		notfound();
210		return;
211	}
212	s = git_blob_rawcontent(blob);
213	len = (size_t)git_blob_rawsize(blob);
214	binary = git_blob_is_binary(blob);
215	for (i = 0, lines = 0; i < len; i++)
216		if (s[i] == '\n')
217			lines++;
218	if (len && s[len - 1] != '\n')
219		lines++;
220
221	pagebegin(200, reponame);
222	repoheader("tree");
223	printpath(path);
224	fputs("<span class=\"info\">", stdout);
225	if (!binary)
226		printf("%zu lines &middot; ", lines);
227	printf("%zuB &middot; <a href=\"", len);
228	printurl("raw", ref, path);
229	fputs("\">raw</a></span></p>\n", stdout);
230	if (binary) {
231		fputs("<p>Binary file.</p>\n", stdout);
232	} else {
233		fputs("<pre class=\"blob\">", stdout);
234		for (i = 0, n = 1; i < len; i += l + 1, n++) {
235			nl = memchr(s + i, '\n', len - i);
236			l = nl ? (size_t)(nl - s) - i : len - i;
237			printf("<a id=\"l%zu\" href=\"#l%zu\">%4zu</a> ",
238				n, n, n);
239			xmlencoden(s + i, l);
240			putchar('\n');
241		}
242		fputs("</pre>\n", stdout);
243	}
244	pageend();
245	git_blob_free(blob);
246}
247
248int
249findref(char *spec, char **rest)
250{
251	git_reference *r;
252	char *cut = NULL, *p;
253
254	for (;;) {
255		if (git_reference_dwim(&r, repo, spec) >= 0
256		&& loadref(r) >= 0) {
257			*rest = cut ? cut + 1 : spec + strlen(spec);
258			return 0;
259		}
260		if (cut)
261			*cut = '/';
262		p = cut ? cut : spec + strlen(spec);
263		while (p > spec && *--p != '/')
264			;
265		if (p == spec)
266			return -1;
267		*p = '\0';
268		cut = p;
269	}
270}
271
272int
273headcommit(git_commit **commit, git_repository *r)
274{
275	git_object *obj;
276
277	if (git_revparse_single(&obj, r, "HEAD^{commit}") < 0)
278		return -1;
279	*commit = (git_commit *)obj;
280	return 0;
281}
282
283void
284httpheader(int status, const char *type)
285{
286	if (status == 404)
287		fputs("Status: 404 Not Found\r\n", stdout);
288	printf("Content-Type: %s\r\n", type);
289	fputs("X-Content-Type-Options: nosniff\r\n\r\n", stdout);
290}
291
292void
293indexpage(void)
294{
295	pagebegin(200, title);
296	fputs("<main>\n<table class=\"repos\">\n<thead><tr><th>Name</th>"
297		"<th class=\"sm\">Description</th><th class=\"sm\">Owner</th>"
298		"<th class=\"r\">Updated</th></tr></thead>\n<tbody>\n",
299		stdout);
300	walk("", 0);
301	fputs("</tbody>\n</table>\n", stdout);
302	pageend();
303}
304
305void
306indexrow(const char *name)
307{
308	char desc[256], owner[256];
309	git_repository *r = NULL;
310	git_commit *commit = NULL;
311
312	readdesc(name, desc, sizeof(desc));
313	readfile(name, "owner", owner, sizeof(owner));
314	fputs("<tr><td class=\"name\"><a href=\"", stdout);
315	xmlencode(base);
316	putchar('/');
317	urlencode(name);
318	fputs("\">", stdout);
319	printname(name);
320	fputs("</a></td><td class=\"desc sm\">", stdout);
321	xmlencode(desc);
322	fputs("</td><td class=\"owner sm\">", stdout);
323	xmlencode(owner);
324	fputs("</td>", stdout);
325	if (openrepo(&r, name) >= 0 && headcommit(&commit, r) >= 0)
326		printage(&git_commit_committer(commit)->when);
327	else
328		fputs("<td class=\"r age a4\">&mdash;</td>", stdout);
329	fputs("</tr>\n", stdout);
330	git_commit_free(commit);
331	git_repository_free(r);
332}
333
334int
335isrepo(const char *name)
336{
337	char path[PATH_MAX];
338	struct stat st;
339
340	if (!validname(name))
341		return 0;
342	if (bprintf(path, sizeof(path), "%s/%s.git", root, name) < 0)
343		return 0;
344	if (stat(path, &st) < 0)
345		return 0;
346	return S_ISDIR(st.st_mode);
347}
348
349int
350loadref(git_reference *r)
351{
352	git_object *obj;
353	int err;
354
355	if ((err = git_reference_peel(&obj, r, GIT_OBJECT_COMMIT)) >= 0) {
356		git_oid_cpy(&tip, git_object_id(obj));
357		err = bprintf(ref, sizeof(ref), "%s",
358			git_reference_shorthand(r));
359		git_object_free(obj);
360	}
361	git_reference_free(r);
362	return err;
363}
364
365void
366logpage(void)
367{
368	git_revwalk *w = NULL;
369	git_oid oid;
370	long i;
371	int n = 0, more = 0;
372
373	pagebegin(200, reponame);
374	repoheader("log");
375	fputs("<table class=\"log\">\n<thead><tr><th>Date</th>"
376		"<th>Message</th><th class=\"sm\">Author</th>"
377		"<th class=\"r sm\">Files</th><th class=\"r\">Lines</th>"
378		"</tr></thead>\n<tbody>\n", stdout);
379	if (*ref && git_revwalk_new(&w, repo) >= 0
380	&& git_revwalk_push(w, &tip) >= 0) {
381		git_revwalk_sorting(w, GIT_SORT_TIME);
382		for (i = 0; i < ofs && git_revwalk_next(&oid, w) >= 0; i++)
383			;
384		for (; n < loglimit && git_revwalk_next(&oid, w) >= 0; n++)
385			logrow(&oid);
386		more = git_revwalk_next(&oid, w) >= 0;
387	}
388	fputs("</tbody>\n</table>\n", stdout);
389	if (!n) {
390		fputs(*ref ? "<p>No commits.</p>\n"
391			: "<p>Empty repository.</p>\n", stdout);
392	} else if (ofs || more) {
393		fputs("<nav class=\"pager\">", stdout);
394		if (ofs) {
395			fputs("<a href=\"", stdout);
396			printurl("log", ref, NULL);
397			if (ofs > loglimit)
398				printf("?ofs=%ld", ofs - loglimit);
399			fputs("\">&larr; newer</a>", stdout);
400		} else {
401			fputs("<span>&larr; newer</span>", stdout);
402		}
403		printf("<span>%ld&ndash;%ld</span>", ofs + 1, ofs + n);
404		if (more) {
405			fputs("<a href=\"", stdout);
406			printurl("log", ref, NULL);
407			printf("?ofs=%ld\">older &rarr;</a>", ofs + n);
408		} else {
409			fputs("<span>older &rarr;</span>", stdout);
410		}
411		fputs("</nav>\n", stdout);
412	}
413	git_revwalk_free(w);
414	pageend();
415}
416
417void
418logrow(const git_oid *oid)
419{
420	git_commit *commit;
421	git_diff *diff;
422	git_diff_stats *stats = NULL;
423	const git_signature *author;
424
425	if (git_commit_lookup(&commit, repo, oid) < 0)
426		return;
427	author = git_commit_author(commit);
428	fputs("<tr><td class=\"date\" title=\"", stdout);
429	printtime(&author->when, "%Y-%m-%d %H:%M");
430	fputs("\">", stdout);
431	printtime(&author->when, "%Y-%m-%d");
432	fputs("</td><td class=\"msg\"><a href=\"", stdout);
433	printurl("commit", git_oid_tostr_s(oid), NULL);
434	fputs("\">", stdout);
435	xmlencode(git_commit_summary(commit));
436	fputs("</a></td><td class=\"sm\">", stdout);
437	xmlencode(author->name);
438	if (commitdiff(&diff, commit) >= 0)
439		git_diff_get_stats(&stats, diff);
440	if (stats)
441		printf("</td><td class=\"r num sm\">%zu</td>"
442			"<td class=\"r num\"><span class=\"del\">-%zu</span>/"
443			"<span class=\"ins\">+%zu</span></td></tr>\n",
444			git_diff_stats_files_changed(stats),
445			git_diff_stats_deletions(stats),
446			git_diff_stats_insertions(stats));
447	else
448		fputs("</td><td class=\"sm\"></td><td></td></tr>\n", stdout);
449	git_diff_stats_free(stats);
450	git_diff_free(diff);
451	git_commit_free(commit);
452}
453
454const char *
455modestr(git_filemode_t mode)
456{
457	switch (mode) {
458	case GIT_FILEMODE_TREE:            return "d---------";
459	case GIT_FILEMODE_BLOB_EXECUTABLE: return "-rwxr-xr-x";
460	case GIT_FILEMODE_LINK:            return "lrwxrwxrwx";
461	case GIT_FILEMODE_COMMIT:          return "m---------";
462	default:                           return "-rw-r--r--";
463	}
464}
465
466void
467notfound(void)
468{
469	pagebegin(404, "Not found");
470	fputs("<main>\n<p>Not found.</p>\n", stdout);
471	pageend();
472}
473
474int
475openrepo(git_repository **r, const char *name)
476{
477	char path[PATH_MAX];
478
479	if (bprintf(path, sizeof(path), "%s/%s.git", root, name) < 0)
480		return -1;
481	return git_repository_open_ext(r, path,
482		GIT_REPOSITORY_OPEN_NO_SEARCH, NULL);
483}
484
485void
486pagebegin(int status, const char *pagetitle)
487{
488	size_t i;
489
490	httpheader(status, HTML);
491	fputs("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n"
492		"<meta name=\"viewport\" "
493		"content=\"width=device-width, initial-scale=1\">\n<title>",
494		stdout);
495	xmlencode(pagetitle);
496	fputs("</title>\n<link rel=\"stylesheet\" href=\"", stdout);
497	xmlencode(css);
498	fputs("\">\n<link rel=\"icon\" href=\"", stdout);
499	xmlencode(icon);
500	fputs("\">\n</head>\n<body>\n<div id=\"top\"><a class=\"brand\" "
501		"href=\"", stdout);
502	xmlencode(base);
503	fputs("/\"><img src=\"", stdout);
504	xmlencode(icon);
505	fputs("\" alt=\"\"> ", stdout);
506	xmlencode(title);
507	fputs("</a><nav>", stdout);
508	for (i = 0; i < LENGTH(services); i++) {
509		if (!strcmp(services[i].name, service)) {
510			fputs("<a class=\"on\" href=\"", stdout);
511			xmlencode(base);
512			putchar('/');
513		} else {
514			fputs("<a href=\"", stdout);
515			xmlencode(services[i].url);
516		}
517		fputs("\">", stdout);
518		xmlencode(services[i].name);
519		fputs("</a>", stdout);
520	}
521	fputs("</nav></div>\n", stdout);
522}
523
524void
525pageend(void)
526{
527	fputs("</main>\n</body>\n</html>\n", stdout);
528}
529
530void
531printage(const git_time *t)
532{
533	static const char *units[] = {
534		"minute", "hour", "day", "week", "month", "year"
535	};
536	static const long secs[] = {
537		60, HOUR, DAY, 7 * DAY, 30 * DAY, 365 * DAY
538	};
539	static const long upto[] = {
540		HOUR, DAY, 14 * DAY, 60 * DAY, 730 * DAY, LONG_MAX
541	};
542	static const long fade[] = { 7 * DAY, 30 * DAY, 180 * DAY, 730 * DAY };
543	long d, n;
544	size_t i, level;
545
546	if ((d = (long)(time(NULL) - (time_t)t->time)) < 0)
547		d = 0;
548	for (i = 0; d >= upto[i]; i++)
549		;
550	if ((n = d / secs[i]) < 1)
551		n = 1;
552	for (level = 0; level < LENGTH(fade) && d >= fade[level]; level++)
553		;
554	printf("<td class=\"r age a%zu\" title=\"", level);
555	printtime(t, "%Y-%m-%d %H:%M");
556	printf("\">%ld %s%s</td>", n, units[i], n == 1 ? "" : "s");
557}
558
559void
560printdiff(git_diff *diff)
561{
562	git_patch *patch;
563	const git_diff_delta *delta;
564	const git_diff_hunk *hunk;
565	const git_diff_line *line;
566	size_t i, h, l, nhunks, nlines, len;
567
568	for (i = 0; i < git_diff_num_deltas(diff); i++) {
569		if (git_patch_from_diff(&patch, diff, i) < 0)
570			continue;
571		delta = git_patch_get_delta(patch);
572		printf("<h3 id=\"f%zu\">", i);
573		if (strcmp(delta->old_file.path, delta->new_file.path)) {
574			xmlencode(delta->old_file.path);
575			fputs(" &rarr; ", stdout);
576		}
577		xmlencode(delta->new_file.path);
578		fputs("</h3>\n<pre class=\"diff\">", stdout);
579		if (delta->flags & GIT_DIFF_FLAG_BINARY)
580			fputs("<span>Binary files differ.</span>", stdout);
581		nhunks = git_patch_num_hunks(patch);
582		for (h = 0; h < nhunks; h++) {
583			if (git_patch_get_hunk(&hunk, &nlines, patch, h) < 0)
584				break;
585			len = hunk->header_len;
586			while (len && hunk->header[len - 1] == '\n')
587				len--;
588			fputs("<span class=\"h\">", stdout);
589			xmlencoden(hunk->header, len);
590			fputs("</span>", stdout);
591			for (l = 0; l < nlines; l++) {
592				if (git_patch_get_line_in_hunk(&line, patch,
593				    h, l) < 0)
594					break;
595				if (line->origin == GIT_DIFF_LINE_ADDITION)
596					fputs("<span class=\"i\">+", stdout);
597				else if (line->origin ==
598				    GIT_DIFF_LINE_DELETION)
599					fputs("<span class=\"d\">-", stdout);
600				else if (line->origin == GIT_DIFF_LINE_CONTEXT)
601					fputs("<span> ", stdout);
602				else
603					continue;
604				len = line->content_len;
605				if (len && line->content[len - 1] == '\n')
606					len--;
607				xmlencoden(line->content, len);
608				fputs("</span>", stdout);
609			}
610		}
611		fputs("</pre>\n", stdout);
612		git_patch_free(patch);
613	}
614}
615
616void
617printname(const char *name)
618{
619	const char *p;
620
621	if ((p = strrchr(name, '/'))) {
622		fputs("<span class=\"dir\">", stdout);
623		xmlencoden(name, p - name + 1);
624		fputs("</span>", stdout);
625		name = p + 1;
626	}
627	xmlencode(name);
628}
629
630void
631printpath(const char *path)
632{
633	char prefix[PATH_MAX];
634	const char *p, *q, *s;
635
636	fputs("<p class=\"path\">", stdout);
637	if ((s = strrchr(reponame, '/')))
638		s++;
639	else
640		s = reponame;
641	if (!*path) {
642		xmlencode(s);
643		return;
644	}
645	fputs("<a href=\"", stdout);
646	printurl("tree", ref, NULL);
647	fputs("\">", stdout);
648	xmlencode(s);
649	fputs("</a>", stdout);
650	for (p = path; (q = strchr(p, '/')); p = q + 1) {
651		if (bprintf(prefix, sizeof(prefix), "%.*s",
652		    (int)(q - path), path) < 0)
653			return;
654		fputs(" / <a href=\"", stdout);
655		printurl("tree", ref, prefix);
656		fputs("\">", stdout);
657		xmlencoden(p, q - p);
658		fputs("</a>", stdout);
659	}
660	fputs(" / ", stdout);
661	xmlencode(p);
662}
663
664void
665printstat(git_diff *diff)
666{
667	git_patch *patch;
668	size_t i, j, n, add, del, max = 0, tadd = 0, tdel = 0;
669	size_t *st;
670
671	n = git_diff_num_deltas(diff);
672	if (!(st = calloc(n * 2 + 1, sizeof(*st))))
673		return;
674	for (i = 0; i < n; i++) {
675		if (git_patch_from_diff(&patch, diff, i) < 0)
676			continue;
677		git_patch_line_stats(NULL, &st[i * 2], &st[i * 2 + 1], patch);
678		git_patch_free(patch);
679		if (st[i * 2] + st[i * 2 + 1] > max)
680			max = st[i * 2] + st[i * 2 + 1];
681	}
682	fputs("<table class=\"stat\">\n", stdout);
683	for (i = 0; i < n; i++) {
684		add = st[i * 2];
685		del = st[i * 2 + 1];
686		tadd += add;
687		tdel += del;
688		printf("<tr><td><a href=\"#f%zu\">", i);
689		xmlencode(git_diff_get_delta(diff, i)->new_file.path);
690		printf("</a></td><td class=\"r num\">%zu</td>"
691			"<td class=\"bar\"><span class=\"ins\">", add + del);
692		if (max > (size_t)barwidth) {
693			add = (add * barwidth + max - 1) / max;
694			del = (del * barwidth + max - 1) / max;
695		}
696		for (j = 0; j < add; j++)
697			putchar('+');
698		fputs("</span><span class=\"del\">", stdout);
699		for (j = 0; j < del; j++)
700			putchar('-');
701		fputs("</span></td></tr>\n", stdout);
702	}
703	printf("</table>\n<p class=\"total\">%zu file%s changed, "
704		"<span class=\"del\">-%zu</span>/"
705		"<span class=\"ins\">+%zu</span></p>\n",
706		n, n == 1 ? "" : "s", tdel, tadd);
707	free(st);
708}
709
710void
711printtime(const git_time *t, const char *fmt)
712{
713	struct tm tm;
714	time_t secs;
715	char buf[32];
716
717	secs = (time_t)t->time + (time_t)t->offset * 60;
718	if (!gmtime_r(&secs, &tm))
719		return;
720	if (strftime(buf, sizeof(buf), fmt, &tm))
721		fputs(buf, stdout);
722}
723
724void
725printurl(const char *page, const char *r, const char *path)
726{
727	xmlencode(base);
728	putchar('/');
729	urlencode(reponame);
730	printf("/%s", page);
731	if (r && *r) {
732		putchar('/');
733		urlencode(r);
734	}
735	if (path && *path) {
736		putchar('/');
737		urlencode(path);
738	}
739}
740
741void
742rawpage(const char *path)
743{
744	git_commit *c = NULL;
745	git_tree *tree = NULL;
746	git_tree_entry *e = NULL;
747	git_blob *blob = NULL;
748
749	if (git_commit_lookup(&c, repo, &tip) < 0
750	|| git_commit_tree(&tree, c) < 0
751	|| git_tree_entry_bypath(&e, tree, path) < 0
752	|| git_tree_entry_type(e) != GIT_OBJECT_BLOB
753	|| git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0) {
754		notfound();
755		goto end;
756	}
757	httpheader(200, git_blob_is_binary(blob) ? "application/octet-stream"
758		: "text/plain; charset=utf-8");
759	fwrite(git_blob_rawcontent(blob), 1, (size_t)git_blob_rawsize(blob),
760		stdout);
761end:
762	git_blob_free(blob);
763	git_tree_entry_free(e);
764	git_tree_free(tree);
765	git_commit_free(c);
766}
767
768void
769readdesc(const char *name, char *buf, size_t len)
770{
771	readfile(name, "description", buf, len);
772	if (!strncmp(buf, "Unnamed repository", 18))
773		buf[0] = '\0';
774}
775
776void
777readfile(const char *name, const char *file, char *buf, size_t len)
778{
779	char path[PATH_MAX];
780	FILE *fp;
781
782	buf[0] = '\0';
783	if (bprintf(path, sizeof(path), "%s/%s.git/%s", root, name, file) < 0)
784		return;
785	if (!(fp = fopen(path, "r")))
786		return;
787	if (fgets(buf, (int)len, fp))
788		buf[strcspn(buf, "\r\n")] = '\0';
789	fclose(fp);
790}
791
792void
793refrows(const char *glob)
794{
795	git_reference_iterator *it;
796	git_reference *r;
797	git_object *obj;
798	const git_signature *author;
799
800	if (git_reference_iterator_glob_new(&it, repo, glob) < 0)
801		return;
802	while (git_reference_next(&r, it) >= 0) {
803		fputs("<tr><td class=\"name\"><a href=\"", stdout);
804		printurl("log", git_reference_shorthand(r), NULL);
805		fputs("\">", stdout);
806		xmlencode(git_reference_shorthand(r));
807		fputs("</a></td><td class=\"date\">", stdout);
808		if (git_reference_peel(&obj, r, GIT_OBJECT_COMMIT) >= 0) {
809			author = git_commit_author((git_commit *)obj);
810			printtime(&author->when, "%Y-%m-%d %H:%M");
811			fputs("</td><td class=\"sm\">", stdout);
812			xmlencode(author->name);
813			git_object_free(obj);
814		} else {
815			fputs("</td><td class=\"sm\">", stdout);
816		}
817		fputs("</td></tr>\n", stdout);
818		git_reference_free(r);
819	}
820	git_reference_iterator_free(it);
821}
822
823void
824refspage(void)
825{
826	pagebegin(200, reponame);
827	repoheader("refs");
828	printf("<h2>Branches</h2>\n<table class=\"refs\">\n%s", refshead);
829	refrows("refs/heads/*");
830	printf("</tbody>\n</table>\n<h2>Tags</h2>\n"
831		"<table class=\"refs\">\n%s", refshead);
832	refrows("refs/tags/*");
833	fputs("</tbody>\n</table>\n", stdout);
834	pageend();
835}
836
837void
838repoheader(const char *tab)
839{
840	static const char *tabs[] = { "log", "tree", "refs" };
841	char desc[256];
842	size_t i;
843
844	readdesc(reponame, desc, sizeof(desc));
845	fputs("<header>\n<h1>", stdout);
846	printname(reponame);
847	fputs("</h1>\n", stdout);
848	if (*desc) {
849		fputs("<p class=\"desc\">", stdout);
850		xmlencode(desc);
851		fputs("</p>\n", stdout);
852	}
853	fputs("<p class=\"clone\"><span>git clone</span> ", stdout);
854	xmlencode(clone);
855	xmlencode(reponame);
856	fputs(".git</p>\n<nav>", stdout);
857	for (i = 0; i < LENGTH(tabs); i++) {
858		printf("<a%s href=\"", strcmp(tab, tabs[i]) ? ""
859			: " class=\"on\"");
860		printurl(tabs[i], i < 2 ? ref : NULL, NULL);
861		printf("\">%s</a>", tabs[i]);
862	}
863	if (*ref) {
864		fputs("<span class=\"ref\">ref <a href=\"", stdout);
865		printurl("refs", NULL, NULL);
866		fputs("\">", stdout);
867		xmlencode(ref);
868		fputs("</a></span>", stdout);
869	}
870	fputs("</nav>\n</header>\n<main>\n", stdout);
871}
872
873void
874route(char *path)
875{
876	git_reference *r;
877	char *rest = NULL, *page, *arg, *cut;
878
879	while (*path == '/')
880		path++;
881	for (cut = path + strlen(path); cut > path && cut[-1] == '/'; cut--)
882		cut[-1] = '\0';
883	if (!*path) {
884		indexpage();
885		return;
886	}
887	while (!isrepo(path)) {
888		if (!(cut = strrchr(path, '/'))) {
889			notfound();
890			return;
891		}
892		if (rest)
893			rest[-1] = '/';
894		*cut = '\0';
895		rest = cut + 1;
896	}
897	if (openrepo(&repo, path) < 0) {
898		notfound();
899		return;
900	}
901	reponame = path;
902	if (git_repository_head(&r, repo) >= 0)
903		loadref(r);
904
905	page = rest ? rest : "log";
906	if ((arg = strchr(page, '/')))
907		*arg++ = '\0';
908	else
909		arg = "";
910	if (!strcmp(page, "log")) {
911		if (*arg && (findref(arg, &rest) < 0 || *rest))
912			notfound();
913		else
914			logpage();
915	} else if (!strcmp(page, "tree")) {
916		if (*arg && findref(arg, &rest) < 0)
917			notfound();
918		else
919			treepage(*arg ? rest : "");
920	} else if (!strcmp(page, "raw")) {
921		if (findref(arg, &rest) < 0 || !*rest)
922			notfound();
923		else
924			rawpage(rest);
925	} else if (!strcmp(page, "commit") && *arg) {
926		commitpage(arg);
927	} else if (!strcmp(page, "refs") && !*arg) {
928		refspage();
929	} else {
930		notfound();
931	}
932	git_repository_free(repo);
933}
934
935void
936treepage(const char *path)
937{
938	git_commit *c = NULL;
939	git_tree *root = NULL, *tree = NULL;
940	git_tree_entry *e = NULL;
941
942	if (*ref && (git_commit_lookup(&c, repo, &tip) < 0
943	|| git_commit_tree(&root, c) < 0))
944		goto fail;
945	if (*path) {
946		if (!root || git_tree_entry_bypath(&e, root, path) < 0)
947			goto fail;
948		if (git_tree_entry_type(e) == GIT_OBJECT_BLOB) {
949			filepage(path, e);
950			goto end;
951		}
952		if (git_tree_entry_type(e) != GIT_OBJECT_TREE
953		|| git_tree_lookup(&tree, repo, git_tree_entry_id(e)) < 0)
954			goto fail;
955	}
956	pagebegin(200, reponame);
957	repoheader("tree");
958	printpath(path);
959	fputs("</p>\n", stdout);
960	if (root)
961		treetable(tree ? tree : root, path);
962	else
963		fputs("<p>Empty repository.</p>\n", stdout);
964	pageend();
965	goto end;
966fail:
967	notfound();
968end:
969	git_tree_entry_free(e);
970	git_tree_free(tree);
971	git_tree_free(root);
972	git_commit_free(c);
973}
974
975void
976treerow(const git_tree_entry *e, const char *path)
977{
978	char child[PATH_MAX];
979	const char *name;
980	git_object_t type;
981	git_blob *blob;
982
983	name = git_tree_entry_name(e);
984	type = git_tree_entry_type(e);
985	if (bprintf(child, sizeof(child), "%s%s%s", path, *path ? "/" : "",
986	    name) < 0)
987		return;
988	printf("<tr><td class=\"mode sm\">%s</td><td class=\"%s\">",
989		modestr(git_tree_entry_filemode(e)),
990		type == GIT_OBJECT_TREE ? "dirent" : "name");
991	if (type == GIT_OBJECT_COMMIT) {
992		xmlencode(name);
993	} else {
994		fputs("<a href=\"", stdout);
995		printurl("tree", ref, child);
996		fputs("\">", stdout);
997		xmlencode(name);
998		fputs(type == GIT_OBJECT_TREE ? "/</a>" : "</a>", stdout);
999	}
1000	fputs("</td><td class=\"r num\">", stdout);
1001	if (type == GIT_OBJECT_BLOB
1002	&& git_blob_lookup(&blob, repo, git_tree_entry_id(e)) >= 0) {
1003		printf("%lluB", (unsigned long long)git_blob_rawsize(blob));
1004		git_blob_free(blob);
1005	}
1006	fputs("</td></tr>\n", stdout);
1007}
1008
1009void
1010treetable(const git_tree *tree, const char *path)
1011{
1012	char parent[PATH_MAX];
1013	const git_tree_entry *e;
1014	const char *p;
1015	size_t i, n;
1016	int pass, isdir;
1017
1018	fputs("<table class=\"tree\">\n<thead><tr><th class=\"sm\">Mode</th>"
1019		"<th>Name</th><th class=\"r\">Size</th></tr></thead>\n"
1020		"<tbody>\n", stdout);
1021	if (*path) {
1022		p = strrchr(path, '/');
1023		if (bprintf(parent, sizeof(parent), "%.*s",
1024		    p ? (int)(p - path) : 0, path) < 0)
1025			return;
1026		fputs("<tr><td class=\"mode sm\">d---------</td>"
1027			"<td class=\"dirent\"><a href=\"", stdout);
1028		printurl("tree", ref, parent);
1029		fputs("\">../</a></td><td></td></tr>\n", stdout);
1030	}
1031	n = git_tree_entrycount(tree);
1032	for (pass = 0; pass < 2; pass++) {
1033		for (i = 0; i < n; i++) {
1034			e = git_tree_entry_byindex(tree, i);
1035			isdir = git_tree_entry_type(e) == GIT_OBJECT_TREE;
1036			if (isdir == pass)
1037				continue;
1038			treerow(e, path);
1039		}
1040	}
1041	fputs("</tbody>\n</table>\n", stdout);
1042}
1043
1044void
1045urlencode(const char *s)
1046{
1047	for (; *s; s++) {
1048		if (isalnum((unsigned char)*s) || strchr("-._~/", *s))
1049			putchar(*s);
1050		else
1051			printf("%%%02X", (unsigned char)*s);
1052	}
1053}
1054
1055int
1056validname(const char *s)
1057{
1058	unsigned char c;
1059	int start = 1;
1060
1061	if (!*s)
1062		return 0;
1063	for (; *s; s++) {
1064		c = *s;
1065		if (c == '/') {
1066			if (start)
1067				return 0;
1068			start = 1;
1069			continue;
1070		}
1071		if (start && !(isalnum(c) || c == '_'))
1072			return 0;
1073		if (!(isalnum(c) || c == '.' || c == '_' || c == '-'))
1074			return 0;
1075		start = 0;
1076	}
1077	return !start;
1078}
1079
1080void
1081walk(const char *rel, int depth)
1082{
1083	char dir[PATH_MAX];
1084	struct dirent **ents;
1085	int i, n;
1086
1087	if (depth > maxdepth)
1088		return;
1089	if (bprintf(dir, sizeof(dir), "%s/%s", root, rel) < 0)
1090		return;
1091	if ((n = scandir(dir, &ents, NULL, alphasort)) < 0)
1092		return;
1093	for (i = 0; i < n; i++) {
1094		walkent(dir, rel, ents[i]->d_name, depth);
1095		free(ents[i]);
1096	}
1097	free(ents);
1098}
1099
1100void
1101walkent(const char *dir, const char *rel, const char *e, int depth)
1102{
1103	char path[PATH_MAX], name[PATH_MAX];
1104	struct stat st;
1105	size_t len;
1106
1107	if (e[0] == '.')
1108		return;
1109	if (bprintf(path, sizeof(path), "%s/%s", dir, e) < 0)
1110		return;
1111	if (lstat(path, &st) < 0 || !S_ISDIR(st.st_mode))
1112		return;
1113	if (bprintf(name, sizeof(name), "%s%s%s", rel, *rel ? "/" : "", e) < 0)
1114		return;
1115	len = strlen(e);
1116	if (len <= 4 || strcmp(e + len - 4, ".git")) {
1117		walk(name, depth + 1);
1118		return;
1119	}
1120	name[strlen(name) - 4] = '\0';
1121	if (validname(name))
1122		indexrow(name);
1123}
1124
1125void
1126xmlencode(const char *s)
1127{
1128	if (s)
1129		xmlencoden(s, strlen(s));
1130}
1131
1132void
1133xmlencoden(const char *s, size_t len)
1134{
1135	for (; len; s++, len--) {
1136		switch (*s) {
1137		case '<':  fputs("&lt;", stdout);   break;
1138		case '>':  fputs("&gt;", stdout);   break;
1139		case '&':  fputs("&amp;", stdout);  break;
1140		case '"':  fputs("&quot;", stdout); break;
1141		case '\'': fputs("&#39;", stdout);  break;
1142		default:   putchar(*s);
1143		}
1144	}
1145}
1146
1147/* main */
1148int
1149main(void)
1150{
1151	char path[PATH_MAX];
1152	const char *q;
1153
1154	root = envor("PETAL_ROOT", root);
1155	css = envor("PETAL_CSS", css);
1156	icon = envor("PETAL_ICON", icon);
1157	clone = envor("PETAL_CLONE", clone);
1158	title = envor("PETAL_TITLE", title);
1159	base = envor("SCRIPT_NAME", "");
1160	q = envor("QUERY_STRING", "");
1161	if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)
1162		ofs = 0;
1163
1164	git_libgit2_init();
1165	if (bprintf(path, sizeof(path), "%s", envor("PATH_INFO", "")) < 0)
1166		notfound();
1167	else
1168		route(path);
1169	git_libgit2_shutdown();
1170	return 0;
1171}