greenhouse / petal / petal.c1165 lines · 28321B · 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(" <", stdout);
174 xmlencode(a->email);
175 fputs("></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 · ", lines);
238 printf("%zuB · <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("\">← newer</a>", stdout);
421 }
422 printf("<span>%ld–%ld</span>", ofs + 1, ofs + n);
423 if (more) {
424 fputs("<a href=\"", stdout);
425 printurl("log", ref, NULL);
426 printf("?ofs=%ld\">older →</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\">—</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(" → ", 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("<", stdout); break;
1137 case '>': fputs(">", stdout); break;
1138 case '&': fputs("&", stdout); break;
1139 case '"': fputs(""", stdout); break;
1140 case '\'': fputs("'", stdout); break;
1141 default: putchar(*s);
1142 }
1143 }
1144}
1145
1146/* main */
1147int
1148main(void)
1149{
1150 char path[1024];
1151 const char *q;
1152
1153 base = envor("SCRIPT_NAME", "");
1154 q = envor("QUERY_STRING", "");
1155 if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)
1156 ofs = 0;
1157
1158 git_libgit2_init();
1159 if (bprintf(path, sizeof(path), "%s", envor("PATH_INFO", "")) < 0)
1160 notfound();
1161 else
1162 route(path);
1163 git_libgit2_shutdown();
1164 return 0;
1165}