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