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(" <", stdout);
175 xmlencode(a->email);
176 fputs("></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 · ", lines);
239 printf("%zuB · <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("\">← newer</a>", stdout);
422 }
423 printf("<span>%ld–%ld</span>", ofs + 1, ofs + n);
424 if (more) {
425 fputs("<a href=\"", stdout);
426 printurl("log", ref, NULL);
427 printf("?ofs=%ld\">older →</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\">—</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(" → ", 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("<", stdout); break;
1138 case '>': fputs(">", stdout); break;
1139 case '&': fputs("&", stdout); break;
1140 case '"': fputs(""", stdout); break;
1141 case '\'': fputs("'", 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}