greenhouse

greenhouse

minimal software forge

git clone git@git.lostduk.com:greenhouse.git

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