greenhouse

greenhouse

forge suite

git clone [email protected]:greenhouse.git

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