stagit.c (34116B)
1 #include <sys/stat.h>
2 #include <sys/types.h>
3
4 #include <err.h>
5 #include <errno.h>
6 #include <libgen.h>
7 #include <limits.h>
8 #include <stdint.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <time.h>
13 #include <unistd.h>
14
15 #include <git2.h>
16
17 #include "compat.h"
18
19 struct deltainfo {
20 git_patch *patch;
21
22 size_t addcount;
23 size_t delcount;
24 };
25
26 struct commitinfo {
27 const git_oid *id;
28
29 char oid[GIT_OID_HEXSZ + 1];
30 char parentoid[GIT_OID_HEXSZ + 1];
31
32 const git_signature *author;
33 const git_signature *committer;
34 const char *summary;
35 const char *msg;
36
37 git_diff *diff;
38 git_commit *commit;
39 git_commit *parent;
40 git_tree *commit_tree;
41 git_tree *parent_tree;
42
43 size_t addcount;
44 size_t delcount;
45 size_t filecount;
46
47 struct deltainfo **deltas;
48 size_t ndeltas;
49 };
50
51 /* reference and associated data for sorting */
52 struct referenceinfo {
53 struct git_reference *ref;
54 struct commitinfo *ci;
55 };
56
57 static git_repository *repo;
58
59 static const char *relpath = "";
60 static const char *repodir;
61
62 static char *name = "";
63 static char *strippedname = "";
64 static char description[255];
65 static char cloneurl[1024];
66 static char baseurl[255] = "https://git.shimmy1996.com";
67 static char *submodules;
68 static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:.LICENSE",
69 "HEAD:LICENSE.md", "HEAD:.LICENSE.md",
70 "HEAD:LICENSE.org", "HEAD:.LICENSE.org",
71 "HEAD:COPYING", "HEAD:.COPYING" };
72 static char *license;
73 static char *readmefiles[] = { "HEAD:README", "HEAD:.README",
74 "HEAD:README.md", "HEAD:.README.md",
75 "HEAD:README.org", "HEAD:.README.org" };
76 static char *readme;
77 static long long nlogcommits = 100; /* < 0 indicates not used */
78
79 /* cache */
80 static git_oid lastoid;
81 static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
82 static FILE *rcachefp, *wcachefp;
83 static const char *cachefile;
84
85 void
86 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
87 {
88 int r;
89
90 r = snprintf(buf, bufsiz, "%s%s%s",
91 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
92 if (r < 0 || (size_t)r >= bufsiz)
93 errx(1, "path truncated: '%s%s%s'",
94 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
95 }
96
97 void
98 deltainfo_free(struct deltainfo *di)
99 {
100 if (!di)
101 return;
102 git_patch_free(di->patch);
103 memset(di, 0, sizeof(*di));
104 free(di);
105 }
106
107 int
108 commitinfo_getstats(struct commitinfo *ci)
109 {
110 struct deltainfo *di;
111 git_diff_options opts;
112 git_diff_find_options fopts;
113 const git_diff_delta *delta;
114 const git_diff_hunk *hunk;
115 const git_diff_line *line;
116 git_patch *patch = NULL;
117 size_t ndeltas, nhunks, nhunklines;
118 size_t i, j, k;
119
120 if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
121 goto err;
122 if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
123 if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
124 ci->parent = NULL;
125 ci->parent_tree = NULL;
126 }
127 }
128
129 git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
130 opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
131 GIT_DIFF_IGNORE_SUBMODULES |
132 GIT_DIFF_INCLUDE_TYPECHANGE;
133 if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
134 goto err;
135
136 if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
137 goto err;
138 /* find renames and copies, exact matches (no heuristic) for renames. */
139 fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
140 GIT_DIFF_FIND_EXACT_MATCH_ONLY;
141 if (git_diff_find_similar(ci->diff, &fopts))
142 goto err;
143
144 ndeltas = git_diff_num_deltas(ci->diff);
145 if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
146 err(1, "calloc");
147
148 for (i = 0; i < ndeltas; i++) {
149 if (git_patch_from_diff(&patch, ci->diff, i))
150 goto err;
151
152 if (!(di = calloc(1, sizeof(struct deltainfo))))
153 err(1, "calloc");
154 di->patch = patch;
155 ci->deltas[i] = di;
156
157 delta = git_patch_get_delta(patch);
158
159 /* skip stats for binary data */
160 if (delta->flags & GIT_DIFF_FLAG_BINARY)
161 continue;
162
163 nhunks = git_patch_num_hunks(patch);
164 for (j = 0; j < nhunks; j++) {
165 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
166 break;
167 for (k = 0; ; k++) {
168 if (git_patch_get_line_in_hunk(&line, patch, j, k))
169 break;
170 if (line->old_lineno == -1) {
171 di->addcount++;
172 ci->addcount++;
173 } else if (line->new_lineno == -1) {
174 di->delcount++;
175 ci->delcount++;
176 }
177 }
178 }
179 }
180 ci->ndeltas = i;
181 ci->filecount = i;
182
183 return 0;
184
185 err:
186 git_diff_free(ci->diff);
187 ci->diff = NULL;
188 git_tree_free(ci->commit_tree);
189 ci->commit_tree = NULL;
190 git_tree_free(ci->parent_tree);
191 ci->parent_tree = NULL;
192 git_commit_free(ci->parent);
193 ci->parent = NULL;
194
195 if (ci->deltas)
196 for (i = 0; i < ci->ndeltas; i++)
197 deltainfo_free(ci->deltas[i]);
198 free(ci->deltas);
199 ci->deltas = NULL;
200 ci->ndeltas = 0;
201 ci->addcount = 0;
202 ci->delcount = 0;
203 ci->filecount = 0;
204
205 return -1;
206 }
207
208 void
209 commitinfo_free(struct commitinfo *ci)
210 {
211 size_t i;
212
213 if (!ci)
214 return;
215 if (ci->deltas)
216 for (i = 0; i < ci->ndeltas; i++)
217 deltainfo_free(ci->deltas[i]);
218
219 free(ci->deltas);
220 git_diff_free(ci->diff);
221 git_tree_free(ci->commit_tree);
222 git_tree_free(ci->parent_tree);
223 git_commit_free(ci->commit);
224 git_commit_free(ci->parent);
225 memset(ci, 0, sizeof(*ci));
226 free(ci);
227 }
228
229 struct commitinfo *
230 commitinfo_getbyoid(const git_oid *id)
231 {
232 struct commitinfo *ci;
233
234 if (!(ci = calloc(1, sizeof(struct commitinfo))))
235 err(1, "calloc");
236
237 if (git_commit_lookup(&(ci->commit), repo, id))
238 goto err;
239 ci->id = id;
240
241 git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
242 git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
243
244 ci->author = git_commit_author(ci->commit);
245 ci->committer = git_commit_committer(ci->commit);
246 ci->summary = git_commit_summary(ci->commit);
247 ci->msg = git_commit_message(ci->commit);
248
249 return ci;
250
251 err:
252 commitinfo_free(ci);
253
254 return NULL;
255 }
256
257 int
258 refs_cmp(const void *v1, const void *v2)
259 {
260 struct referenceinfo *r1 = (struct referenceinfo *)v1;
261 struct referenceinfo *r2 = (struct referenceinfo *)v2;
262 time_t t1, t2;
263 int r;
264
265 if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
266 return r;
267
268 t1 = r1->ci->author ? r1->ci->author->when.time : 0;
269 t2 = r2->ci->author ? r2->ci->author->when.time : 0;
270 if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
271 return r;
272
273 return strcmp(git_reference_shorthand(r1->ref),
274 git_reference_shorthand(r2->ref));
275 }
276
277 int
278 getrefs(struct referenceinfo **pris, size_t *prefcount)
279 {
280 struct referenceinfo *ris = NULL;
281 struct commitinfo *ci = NULL;
282 git_reference_iterator *it = NULL;
283 const git_oid *id = NULL;
284 git_object *obj = NULL;
285 git_reference *dref = NULL, *r, *ref = NULL;
286 size_t i, refcount;
287
288 *pris = NULL;
289 *prefcount = 0;
290
291 if (git_reference_iterator_new(&it, repo))
292 return -1;
293
294 for (refcount = 0; !git_reference_next(&ref, it); ) {
295 if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
296 git_reference_free(ref);
297 ref = NULL;
298 continue;
299 }
300
301 switch (git_reference_type(ref)) {
302 case GIT_REF_SYMBOLIC:
303 if (git_reference_resolve(&dref, ref))
304 goto err;
305 r = dref;
306 break;
307 case GIT_REF_OID:
308 r = ref;
309 break;
310 default:
311 continue;
312 }
313 if (!git_reference_target(r) ||
314 git_reference_peel(&obj, r, GIT_OBJ_ANY))
315 goto err;
316 if (!(id = git_object_id(obj)))
317 goto err;
318 if (!(ci = commitinfo_getbyoid(id)))
319 break;
320
321 if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
322 err(1, "realloc");
323 ris[refcount].ci = ci;
324 ris[refcount].ref = r;
325 refcount++;
326
327 git_object_free(obj);
328 obj = NULL;
329 git_reference_free(dref);
330 dref = NULL;
331 }
332 git_reference_iterator_free(it);
333
334 /* sort by type, date then shorthand name */
335 qsort(ris, refcount, sizeof(*ris), refs_cmp);
336
337 *pris = ris;
338 *prefcount = refcount;
339
340 return 0;
341
342 err:
343 git_object_free(obj);
344 git_reference_free(dref);
345 commitinfo_free(ci);
346 for (i = 0; i < refcount; i++) {
347 commitinfo_free(ris[i].ci);
348 git_reference_free(ris[i].ref);
349 }
350 free(ris);
351
352 return -1;
353 }
354
355 FILE *
356 efopen(const char *name, const char *flags)
357 {
358 FILE *fp;
359
360 if (!(fp = fopen(name, flags)))
361 err(1, "fopen: '%s'", name);
362
363 return fp;
364 }
365
366 /* Escape characters below as HTML 2.0 / XML 1.0. */
367 void
368 xmlencode(FILE *fp, const char *s, size_t len)
369 {
370 size_t i;
371
372 for (i = 0; *s && i < len; s++, i++) {
373 switch(*s) {
374 case '<': fputs("<", fp); break;
375 case '>': fputs(">", fp); break;
376 case '\'': fputs("'", fp); break;
377 case '&': fputs("&", fp); break;
378 case '"': fputs(""", fp); break;
379 default: fputc(*s, fp);
380 }
381 }
382 }
383
384 int
385 mkdirp(const char *path)
386 {
387 char tmp[PATH_MAX], *p;
388
389 if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
390 errx(1, "path truncated: '%s'", path);
391 for (p = tmp + (tmp[0] == '/'); *p; p++) {
392 if (*p != '/')
393 continue;
394 *p = '\0';
395 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
396 return -1;
397 *p = '/';
398 }
399 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
400 return -1;
401 return 0;
402 }
403
404 void
405 printtimez(FILE *fp, const git_time *intime)
406 {
407 struct tm *intm;
408 time_t t;
409 char out[32];
410
411 t = (time_t)intime->time;
412 if (!(intm = gmtime(&t)))
413 return;
414 strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
415 fputs(out, fp);
416 }
417
418 void
419 printtime(FILE *fp, const git_time *intime)
420 {
421 struct tm *intm;
422 time_t t;
423 char out[32];
424
425 t = (time_t)intime->time + (intime->offset * 60);
426 if (!(intm = gmtime(&t)))
427 return;
428 strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
429 if (intime->offset < 0)
430 fprintf(fp, "%s -%02d%02d", out,
431 -(intime->offset) / 60, -(intime->offset) % 60);
432 else
433 fprintf(fp, "%s +%02d%02d", out,
434 intime->offset / 60, intime->offset % 60);
435 }
436
437 void
438 printtimeshort(FILE *fp, const git_time *intime)
439 {
440 struct tm *intm;
441 time_t t;
442 char out[32];
443
444 t = (time_t)intime->time;
445 if (!(intm = gmtime(&t)))
446 return;
447 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
448 fputs(out, fp);
449 }
450
451 void
452 writeheader(FILE *fp, const char *title)
453 {
454 fputs("<!DOCTYPE html>\n"
455 "<html lang=en>\n<head>\n"
456 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
457 "<title>", fp);
458 xmlencode(fp, title, strlen(title));
459 if (title[0] && strippedname[0])
460 fputs(" - ", fp);
461 xmlencode(fp, strippedname, strlen(strippedname));
462 if (description[0])
463 fputs(" - ", fp);
464 xmlencode(fp, description, strlen(description));
465 fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
466 fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
467 name, relpath);
468 fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed (tags)\" href=\"%stags.xml\" />\n",
469 name, relpath);
470 fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
471 fputs("</head>\n<body>\n<header>\n<nav>", fp);
472 fprintf(fp, "<span style=\"float:right\"><a href=\"../%s\">⏎</a></span>", relpath);
473 fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
474 fprintf(fp, "<a href=\"%sfiles.html\">Files</a> | ", relpath);
475 fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath);
476 if (submodules)
477 fprintf(fp, " | <a href=\"%sfile/%s.html\">Submodules</a>",
478 relpath, submodules);
479 if (readme)
480 fprintf(fp, " | <a href=\"%sfile/%s.html\">README</a>",
481 relpath, readme);
482 if (license)
483 fprintf(fp, " | <a href=\"%sfile/%s.html\">LICENSE</a>",
484 relpath, license);
485 fputs("</nav>\n<h1>", fp);
486 xmlencode(fp, strippedname, strlen(strippedname));
487 fputs("</h1><p>", fp);
488 xmlencode(fp, description, strlen(description));
489 fputs("</p>", fp);
490 if (cloneurl[0]) {
491 fputs("<pre>git clone ", fp);
492 xmlencode(fp, cloneurl, strlen(cloneurl));
493 fputs("</pre>", fp);
494 }
495 fputs("\n</header>\n<main>\n", fp);
496 }
497
498 void
499 writefooter(FILE *fp)
500 {
501 fputs("</main>\n</body>\n</html>\n", fp);
502 }
503
504 int
505 writeblobhtml(FILE *fp, const git_blob *blob)
506 {
507 size_t n = 0, i, prev;
508 const char *nfmt = "<a href=\"#l%d\" class=\"line\" id=\"l%d\">%5d</a> ";
509 const char *s = git_blob_rawcontent(blob);
510 git_off_t len = git_blob_rawsize(blob);
511
512 fputs("<pre id=\"blob\">\n", fp);
513
514 if (len > 0) {
515 for (i = 0, prev = 0; i < (size_t)len; i++) {
516 if (s[i] != '\n')
517 continue;
518 n++;
519 fprintf(fp, nfmt, n, n, n);
520 xmlencode(fp, &s[prev], i - prev + 1);
521 prev = i + 1;
522 }
523 /* trailing data */
524 if ((len - prev) > 0) {
525 n++;
526 fprintf(fp, nfmt, n, n, n);
527 xmlencode(fp, &s[prev], len - prev);
528 }
529 }
530
531 fputs("</pre>\n", fp);
532
533 return n;
534 }
535
536 void
537 printcommit(FILE *fp, struct commitinfo *ci)
538 {
539 fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
540 relpath, ci->oid, ci->oid);
541
542 if (ci->parentoid[0])
543 fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
544 relpath, ci->parentoid, ci->parentoid);
545
546 if (ci->author) {
547 fputs("<b>Author:</b> ", fp);
548 xmlencode(fp, ci->author->name, strlen(ci->author->name));
549 fputs(" <<a href=\"mailto:", fp);
550 xmlencode(fp, ci->author->email, strlen(ci->author->email));
551 fputs("\">", fp);
552 xmlencode(fp, ci->author->email, strlen(ci->author->email));
553 fputs("</a>>\n<b>Date:</b> ", fp);
554 printtime(fp, &(ci->author->when));
555 fputc('\n', fp);
556 }
557 if (ci->msg) {
558 fputc('\n', fp);
559 xmlencode(fp, ci->msg, strlen(ci->msg));
560 fputc('\n', fp);
561 }
562 }
563
564 void
565 printshowfile(FILE *fp, struct commitinfo *ci)
566 {
567 const git_diff_delta *delta;
568 const git_diff_hunk *hunk;
569 const git_diff_line *line;
570 git_patch *patch;
571 size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
572 char linestr[80];
573 int c;
574
575 printcommit(fp, ci);
576
577 if (!ci->deltas)
578 return;
579
580 if (ci->filecount > 1000 ||
581 ci->ndeltas > 1000 ||
582 ci->addcount > 100000 ||
583 ci->delcount > 100000) {
584 fputs("Diff is too large, output suppressed.\n", fp);
585 return;
586 }
587
588 /* diff stat */
589 fputs("<b>Diffstat:</b>\n<table>", fp);
590 for (i = 0; i < ci->ndeltas; i++) {
591 delta = git_patch_get_delta(ci->deltas[i]->patch);
592
593 switch (delta->status) {
594 case GIT_DELTA_ADDED: c = 'A'; break;
595 case GIT_DELTA_COPIED: c = 'C'; break;
596 case GIT_DELTA_DELETED: c = 'D'; break;
597 case GIT_DELTA_MODIFIED: c = 'M'; break;
598 case GIT_DELTA_RENAMED: c = 'R'; break;
599 case GIT_DELTA_TYPECHANGE: c = 'T'; break;
600 default: c = ' '; break;
601 }
602 if (c == ' ')
603 fprintf(fp, "<tr><td>%c", c);
604 else
605 fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
606
607 fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
608 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
609 if (strcmp(delta->old_file.path, delta->new_file.path)) {
610 fputs(" -> ", fp);
611 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
612 }
613
614 add = ci->deltas[i]->addcount;
615 del = ci->deltas[i]->delcount;
616 changed = add + del;
617 total = sizeof(linestr) - 2;
618 if (changed > total) {
619 if (add)
620 add = ((float)total / changed * add) + 1;
621 if (del)
622 del = ((float)total / changed * del) + 1;
623 }
624 memset(&linestr, '+', add);
625 memset(&linestr[add], '-', del);
626
627 fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
628 ci->deltas[i]->addcount + ci->deltas[i]->delcount);
629 fwrite(&linestr, 1, add, fp);
630 fputs("</span><span class=\"d\">", fp);
631 fwrite(&linestr[add], 1, del, fp);
632 fputs("</span></td></tr>\n", fp);
633 }
634 fprintf(fp, "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
635 ci->filecount, ci->filecount == 1 ? "" : "s",
636 ci->addcount, ci->addcount == 1 ? "" : "s",
637 ci->delcount, ci->delcount == 1 ? "" : "s");
638
639 for (i = 0; i < ci->ndeltas; i++) {
640 patch = ci->deltas[i]->patch;
641 delta = git_patch_get_delta(patch);
642 fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
643 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
644 fputs(".html\">", fp);
645 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
646 fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
647 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
648 fprintf(fp, ".html\">");
649 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
650 fprintf(fp, "</a></b>\n");
651
652 /* check binary data */
653 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
654 fputs("Binary files differ.\n", fp);
655 continue;
656 }
657
658 nhunks = git_patch_num_hunks(patch);
659 for (j = 0; j < nhunks; j++) {
660 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
661 break;
662
663 fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
664 xmlencode(fp, hunk->header, hunk->header_len);
665 fputs("</a>", fp);
666
667 for (k = 0; ; k++) {
668 if (git_patch_get_line_in_hunk(&line, patch, j, k))
669 break;
670 if (line->old_lineno == -1)
671 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
672 i, j, k, i, j, k);
673 else if (line->new_lineno == -1)
674 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
675 i, j, k, i, j, k);
676 else
677 fputc(' ', fp);
678 xmlencode(fp, line->content, line->content_len);
679 if (line->old_lineno == -1 || line->new_lineno == -1)
680 fputs("</a>", fp);
681 }
682 }
683 }
684 }
685
686 void
687 writelogline(FILE *fp, struct commitinfo *ci)
688 {
689 fputs("<tr><td>", fp);
690 if (ci->author)
691 printtimeshort(fp, &(ci->author->when));
692 fputs("</td><td>", fp);
693 if (ci->summary) {
694 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
695 xmlencode(fp, ci->summary, strlen(ci->summary));
696 fputs("</a>", fp);
697 }
698 fputs("</td><td>", fp);
699 if (ci->author)
700 xmlencode(fp, ci->author->name, strlen(ci->author->name));
701 fputs("</td><td class=\"num\">", fp);
702 fprintf(fp, "%zu", ci->filecount);
703 fputs("</td><td class=\"num\">", fp);
704 fprintf(fp, "+%zu", ci->addcount);
705 fputs("</td><td class=\"num\">", fp);
706 fprintf(fp, "-%zu", ci->delcount);
707 fputs("</td></tr>\n", fp);
708 }
709
710 int
711 writelog(FILE *fp, const git_oid *oid)
712 {
713 struct commitinfo *ci;
714 git_revwalk *w = NULL;
715 git_oid id;
716 char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
717 FILE *fpfile;
718 int r;
719
720 git_revwalk_new(&w, repo);
721 git_revwalk_push(w, oid);
722 git_revwalk_simplify_first_parent(w);
723
724 while (!git_revwalk_next(&id, w)) {
725 relpath = "";
726
727 if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
728 break;
729
730 git_oid_tostr(oidstr, sizeof(oidstr), &id);
731 r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
732 if (r < 0 || (size_t)r >= sizeof(path))
733 errx(1, "path truncated: 'commit/%s.html'", oidstr);
734 r = access(path, F_OK);
735
736 /* optimization: if there are no log lines to write and
737 the commit file already exists: skip the diffstat */
738 if (!nlogcommits && !r)
739 continue;
740
741 if (!(ci = commitinfo_getbyoid(&id)))
742 break;
743 /* diffstat: for stagit HTML required for the log.html line */
744 if (commitinfo_getstats(ci) == -1)
745 goto err;
746
747 if (nlogcommits < 0) {
748 writelogline(fp, ci);
749 } else if (nlogcommits > 0) {
750 writelogline(fp, ci);
751 nlogcommits--;
752 if (!nlogcommits && ci->parentoid[0])
753 fputs("<tr><td></td><td colspan=\"5\">"
754 "More commits remaining [...]</td>"
755 "</tr>\n", fp);
756 }
757
758 if (cachefile)
759 writelogline(wcachefp, ci);
760
761 /* check if file exists if so skip it */
762 if (r) {
763 relpath = "../";
764 fpfile = efopen(path, "w");
765 writeheader(fpfile, ci->summary);
766 fputs("<pre>", fpfile);
767 printshowfile(fpfile, ci);
768 fputs("</pre>\n", fpfile);
769 writefooter(fpfile);
770 fclose(fpfile);
771 }
772 err:
773 commitinfo_free(ci);
774 }
775 git_revwalk_free(w);
776
777 relpath = "";
778
779 return 0;
780 }
781
782 void
783 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
784 {
785 fputs("<entry>\n", fp);
786
787 fputs("<id>", fp);
788 xmlencode(fp, baseurl, strlen(baseurl));
789 fputs("/", fp);
790 xmlencode(fp, strippedname, strlen(strippedname));
791 fprintf(fp, "/commit/%s.html</id>\n", ci->oid);
792 if (ci->author) {
793 fputs("<published>", fp);
794 printtimez(fp, &(ci->author->when));
795 fputs("</published>\n", fp);
796 }
797 if (ci->committer) {
798 fputs("<updated>", fp);
799 printtimez(fp, &(ci->committer->when));
800 fputs("</updated>\n", fp);
801 }
802 if (ci->summary) {
803 fputs("<title type=\"text\">", fp);
804 if (tag && tag[0]) {
805 fputs("[", fp);
806 xmlencode(fp, tag, strlen(tag));
807 fputs("] ", fp);
808 }
809 xmlencode(fp, ci->summary, strlen(ci->summary));
810 fputs("</title>\n", fp);
811 }
812 fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.html\" />\n",
813 ci->oid);
814
815 if (ci->author) {
816 fputs("<author>\n<name>", fp);
817 xmlencode(fp, ci->author->name, strlen(ci->author->name));
818 fputs("</name>\n<email>", fp);
819 xmlencode(fp, ci->author->email, strlen(ci->author->email));
820 fputs("</email>\n</author>\n", fp);
821 }
822
823 fputs("<content type=\"text\">", fp);
824 fprintf(fp, "commit %s\n", ci->oid);
825 if (ci->parentoid[0])
826 fprintf(fp, "parent %s\n", ci->parentoid);
827 if (ci->author) {
828 fputs("Author: ", fp);
829 xmlencode(fp, ci->author->name, strlen(ci->author->name));
830 fputs(" <", fp);
831 xmlencode(fp, ci->author->email, strlen(ci->author->email));
832 fputs(">\nDate: ", fp);
833 printtime(fp, &(ci->author->when));
834 fputc('\n', fp);
835 }
836 if (ci->msg) {
837 fputc('\n', fp);
838 xmlencode(fp, ci->msg, strlen(ci->msg));
839 }
840 fputs("\n</content>\n</entry>\n", fp);
841 }
842
843 int
844 writeatom(FILE *fp, int all)
845 {
846 struct referenceinfo *ris = NULL;
847 size_t refcount = 0;
848 struct commitinfo *ci;
849 git_revwalk *w = NULL;
850 git_oid id;
851 size_t i, m = 100; /* last 'm' commits */
852
853 fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
854 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
855 xmlencode(fp, strippedname, strlen(strippedname));
856 fputs(", branch HEAD</title>\n<subtitle>", fp);
857 xmlencode(fp, description, strlen(description));
858 fputs("</subtitle>\n<link href=\"", fp);
859 xmlencode(fp, baseurl, strlen(baseurl));
860 fputs("/", fp);
861 xmlencode(fp, strippedname, strlen(strippedname));
862 fputs("/atom.xml\" rel=\"self\"/>\n<id>", fp);
863 xmlencode(fp, baseurl, strlen(baseurl));
864 fputs("/", fp);
865 xmlencode(fp, strippedname, strlen(strippedname));
866 fputs("</id>", fp);
867
868 /* all commits or only tags? */
869 if (all) {
870 git_revwalk_new(&w, repo);
871 git_revwalk_push_head(w);
872 git_revwalk_simplify_first_parent(w);
873 for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
874 if (!(ci = commitinfo_getbyoid(&id)))
875 break;
876 printcommitatom(fp, ci, "");
877 commitinfo_free(ci);
878 }
879 git_revwalk_free(w);
880 } else if (getrefs(&ris, &refcount) != -1) {
881 /* references: tags */
882 for (i = 0; i < refcount; i++) {
883 if (git_reference_is_tag(ris[i].ref))
884 printcommitatom(fp, ris[i].ci,
885 git_reference_shorthand(ris[i].ref));
886
887 commitinfo_free(ris[i].ci);
888 git_reference_free(ris[i].ref);
889 }
890 free(ris);
891 }
892
893 fputs("</feed>\n", fp);
894
895 return 0;
896 }
897
898 int
899 writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize)
900 {
901 char tmp[PATH_MAX] = "", *d;
902 const char *p;
903 int lc = 0;
904 FILE *fp;
905
906 if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
907 errx(1, "path truncated: '%s'", fpath);
908 if (!(d = dirname(tmp)))
909 err(1, "dirname");
910 if (mkdirp(d))
911 return -1;
912
913 for (p = fpath, tmp[0] = '\0'; *p; p++) {
914 if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
915 errx(1, "path truncated: '../%s'", tmp);
916 }
917 relpath = tmp;
918
919 fp = efopen(fpath, "w");
920 writeheader(fp, filename);
921 fputs("<h2> ", fp);
922 xmlencode(fp, filename, strlen(filename));
923 fprintf(fp, " (%juB)", (uintmax_t)filesize);
924 fputs("</h2>", fp);
925
926 if (git_blob_is_binary((git_blob *)obj)) {
927 fputs("<p>Binary file.</p>\n", fp);
928 } else {
929 lc = writeblobhtml(fp, (git_blob *)obj);
930 if (ferror(fp))
931 err(1, "fwrite");
932 }
933 writefooter(fp);
934 fclose(fp);
935
936 relpath = "";
937
938 return lc;
939 }
940
941 const char *
942 filemode(git_filemode_t m)
943 {
944 static char mode[11];
945
946 memset(mode, '-', sizeof(mode) - 1);
947 mode[10] = '\0';
948
949 if (S_ISREG(m))
950 mode[0] = '-';
951 else if (S_ISBLK(m))
952 mode[0] = 'b';
953 else if (S_ISCHR(m))
954 mode[0] = 'c';
955 else if (S_ISDIR(m))
956 mode[0] = 'd';
957 else if (S_ISFIFO(m))
958 mode[0] = 'p';
959 else if (S_ISLNK(m))
960 mode[0] = 'l';
961 else if (S_ISSOCK(m))
962 mode[0] = 's';
963 else
964 mode[0] = '?';
965
966 if (m & S_IRUSR) mode[1] = 'r';
967 if (m & S_IWUSR) mode[2] = 'w';
968 if (m & S_IXUSR) mode[3] = 'x';
969 if (m & S_IRGRP) mode[4] = 'r';
970 if (m & S_IWGRP) mode[5] = 'w';
971 if (m & S_IXGRP) mode[6] = 'x';
972 if (m & S_IROTH) mode[7] = 'r';
973 if (m & S_IWOTH) mode[8] = 'w';
974 if (m & S_IXOTH) mode[9] = 'x';
975
976 if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
977 if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
978 if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
979
980 return mode;
981 }
982
983 int
984 writefilestree(FILE *fp, git_tree *tree, const char *path)
985 {
986 const git_tree_entry *entry = NULL;
987 git_object *obj = NULL;
988 git_off_t filesize;
989 const char *entryname;
990 char filepath[PATH_MAX], entrypath[PATH_MAX];
991 size_t count, i;
992 int lc, r, ret;
993
994 count = git_tree_entrycount(tree);
995 for (i = 0; i < count; i++) {
996 if (!(entry = git_tree_entry_byindex(tree, i)) ||
997 !(entryname = git_tree_entry_name(entry)))
998 return -1;
999 joinpath(entrypath, sizeof(entrypath), path, entryname);
1000
1001 r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1002 entrypath);
1003 if (r < 0 || (size_t)r >= sizeof(filepath))
1004 errx(1, "path truncated: 'file/%s.html'", entrypath);
1005
1006 if (!git_tree_entry_to_object(&obj, repo, entry)) {
1007 switch (git_object_type(obj)) {
1008 case GIT_OBJ_BLOB:
1009 break;
1010 case GIT_OBJ_TREE:
1011 /* NOTE: recurses */
1012 ret = writefilestree(fp, (git_tree *)obj,
1013 entrypath);
1014 git_object_free(obj);
1015 if (ret)
1016 return ret;
1017 continue;
1018 default:
1019 git_object_free(obj);
1020 continue;
1021 }
1022
1023 filesize = git_blob_rawsize((git_blob *)obj);
1024 lc = writeblob(obj, filepath, entryname, filesize);
1025
1026 fputs("<tr><td>", fp);
1027 fputs(filemode(git_tree_entry_filemode(entry)), fp);
1028 fprintf(fp, "</td><td><a href=\"%s", relpath);
1029 xmlencode(fp, filepath, strlen(filepath));
1030 fputs("\">", fp);
1031 xmlencode(fp, entrypath, strlen(entrypath));
1032 fputs("</a></td><td class=\"num\">", fp);
1033 if (lc > 0)
1034 fprintf(fp, "%dL", lc);
1035 else
1036 fprintf(fp, "%juB", (uintmax_t)filesize);
1037 fputs("</td></tr>\n", fp);
1038 git_object_free(obj);
1039 } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1040 /* commit object in tree is a submodule */
1041 fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
1042 relpath);
1043 xmlencode(fp, entrypath, strlen(entrypath));
1044 fputs("</a></td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
1045 }
1046 }
1047
1048 return 0;
1049 }
1050
1051 int
1052 writefiles(FILE *fp, const git_oid *id)
1053 {
1054 git_tree *tree = NULL;
1055 git_commit *commit = NULL;
1056 int ret = -1;
1057
1058 fputs("<table id=\"files\"><thead>\n<tr>"
1059 "<th>Mode</th><th>Name</th><th>Size</th>"
1060 "</tr>\n</thead><tbody>\n", fp);
1061
1062 if (!git_commit_lookup(&commit, repo, id) &&
1063 !git_commit_tree(&tree, commit))
1064 ret = writefilestree(fp, tree, "");
1065
1066 fputs("</tbody></table>", fp);
1067
1068 git_commit_free(commit);
1069 git_tree_free(tree);
1070
1071 return ret;
1072 }
1073
1074 int
1075 writerefs(FILE *fp)
1076 {
1077 struct referenceinfo *ris = NULL;
1078 struct commitinfo *ci;
1079 size_t count, i, j, refcount;
1080 const char *titles[] = { "Branches", "Tags" };
1081 const char *ids[] = { "branches", "tags" };
1082 const char *s;
1083
1084 if (getrefs(&ris, &refcount) == -1)
1085 return -1;
1086
1087 for (i = 0, j = 0, count = 0; i < refcount; i++) {
1088 if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1089 if (count)
1090 fputs("</tbody></table><br/>\n", fp);
1091 count = 0;
1092 j = 1;
1093 }
1094
1095 /* print header if it has an entry (first). */
1096 if (++count == 1) {
1097 fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
1098 "<thead>\n<tr><td><b>Name</b></td>"
1099 "<td><b>Last commit date</b></td>"
1100 "<td><b>Author</b></td>\n</tr>\n"
1101 "</thead><tbody>\n",
1102 titles[j], ids[j]);
1103 }
1104
1105 ci = ris[i].ci;
1106 s = git_reference_shorthand(ris[i].ref);
1107
1108 fputs("<tr><td>", fp);
1109 xmlencode(fp, s, strlen(s));
1110 fputs("</td><td>", fp);
1111 if (ci->author)
1112 printtimeshort(fp, &(ci->author->when));
1113 fputs("</td><td>", fp);
1114 if (ci->author)
1115 xmlencode(fp, ci->author->name, strlen(ci->author->name));
1116 fputs("</td></tr>\n", fp);
1117 }
1118 /* table footer */
1119 if (count)
1120 fputs("</tbody></table><br/>\n", fp);
1121
1122 for (i = 0; i < refcount; i++) {
1123 commitinfo_free(ris[i].ci);
1124 git_reference_free(ris[i].ref);
1125 }
1126 free(ris);
1127
1128 return 0;
1129 }
1130
1131 void
1132 usage(char *argv0)
1133 {
1134 fprintf(stderr, "%s [-c cachefile | -l commits] repodir\n", argv0);
1135 exit(1);
1136 }
1137
1138 int
1139 main(int argc, char *argv[])
1140 {
1141 git_object *obj = NULL;
1142 const git_oid *head = NULL;
1143 mode_t mask;
1144 FILE *fp, *fpread;
1145 char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1146 char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1147 size_t n;
1148 int i, fd;
1149
1150 for (i = 1; i < argc; i++) {
1151 if (argv[i][0] != '-') {
1152 if (repodir)
1153 usage(argv[0]);
1154 repodir = argv[i];
1155 } else if (argv[i][1] == 'c') {
1156 if (nlogcommits > 0 || i + 1 >= argc)
1157 usage(argv[0]);
1158 cachefile = argv[++i];
1159 } else if (argv[i][1] == 'l') {
1160 if (cachefile || i + 1 >= argc)
1161 usage(argv[0]);
1162 errno = 0;
1163 nlogcommits = strtoll(argv[++i], &p, 10);
1164 if (argv[i][0] == '\0' || *p != '\0' ||
1165 nlogcommits <= 0 || errno)
1166 usage(argv[0]);
1167 }
1168 }
1169 if (!repodir)
1170 usage(argv[0]);
1171
1172 if (!realpath(repodir, repodirabs))
1173 err(1, "realpath");
1174
1175 git_libgit2_init();
1176
1177 #ifdef __OpenBSD__
1178 if (unveil(repodir, "r") == -1)
1179 err(1, "unveil: %s", repodir);
1180 if (unveil(".", "rwc") == -1)
1181 err(1, "unveil: .");
1182 if (cachefile && unveil(cachefile, "rwc") == -1)
1183 err(1, "unveil: %s", cachefile);
1184
1185 if (cachefile) {
1186 if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1187 err(1, "pledge");
1188 } else {
1189 if (pledge("stdio rpath wpath cpath", NULL) == -1)
1190 err(1, "pledge");
1191 }
1192 #endif
1193
1194 if (git_repository_open_ext(&repo, repodir,
1195 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1196 fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1197 return 1;
1198 }
1199
1200 /* find HEAD */
1201 if (!git_revparse_single(&obj, repo, "HEAD"))
1202 head = git_object_id(obj);
1203 git_object_free(obj);
1204
1205 /* use directory name as name */
1206 if ((name = strrchr(repodirabs, '/')))
1207 name++;
1208 else
1209 name = "";
1210
1211 /* strip .git suffix */
1212 if (!(strippedname = strdup(name)))
1213 err(1, "strdup");
1214 if ((p = strrchr(strippedname, '.')))
1215 if (!strcmp(p, ".git"))
1216 *p = '\0';
1217
1218 /* read description or .git/description */
1219 joinpath(path, sizeof(path), repodir, "description");
1220 if (!(fpread = fopen(path, "r"))) {
1221 joinpath(path, sizeof(path), repodir, ".git/description");
1222 fpread = fopen(path, "r");
1223 }
1224 if (fpread) {
1225 if (!fgets(description, sizeof(description), fpread))
1226 description[0] = '\0';
1227 fclose(fpread);
1228 }
1229
1230 /* read url or .git/url */
1231 joinpath(path, sizeof(path), repodir, "url");
1232 if (!(fpread = fopen(path, "r"))) {
1233 joinpath(path, sizeof(path), repodir, ".git/url");
1234 fpread = fopen(path, "r");
1235 }
1236 if (fpread) {
1237 if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1238 cloneurl[0] = '\0';
1239 cloneurl[strcspn(cloneurl, "\n")] = '\0';
1240 fclose(fpread);
1241 }
1242
1243 /* check LICENSE */
1244 for (i = 0; i < sizeof(licensefiles) / sizeof(*licensefiles) && !license; i++) {
1245 if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1246 git_object_type(obj) == GIT_OBJ_BLOB)
1247 license = licensefiles[i] + strlen("HEAD:");
1248 git_object_free(obj);
1249 }
1250
1251 /* check README */
1252 for (i = 0; i < sizeof(readmefiles) / sizeof(*readmefiles) && !readme; i++) {
1253 if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1254 git_object_type(obj) == GIT_OBJ_BLOB)
1255 readme = readmefiles[i] + strlen("HEAD:");
1256 git_object_free(obj);
1257 }
1258
1259 if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1260 git_object_type(obj) == GIT_OBJ_BLOB)
1261 submodules = ".gitmodules";
1262 git_object_free(obj);
1263
1264 /* log for HEAD */
1265 fp = efopen("log.html", "w");
1266 relpath = "";
1267 mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1268 writeheader(fp, "Log");
1269 fputs("<table id=\"log\"><thead>\n<tr><th>Date</th>"
1270 "<th>Commit message</th>"
1271 "<th>Author</th><th>Files</th>"
1272 "<th>+</th>"
1273 "<th>-</th></tr>\n</thead><tbody>\n", fp);
1274
1275 if (cachefile && head) {
1276 /* read from cache file (does not need to exist) */
1277 if ((rcachefp = fopen(cachefile, "r"))) {
1278 if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1279 errx(1, "%s: no object id", cachefile);
1280 if (git_oid_fromstr(&lastoid, lastoidstr))
1281 errx(1, "%s: invalid object id", cachefile);
1282 }
1283
1284 /* write log to (temporary) cache */
1285 if ((fd = mkstemp(tmppath)) == -1)
1286 err(1, "mkstemp");
1287 if (!(wcachefp = fdopen(fd, "w")))
1288 err(1, "fdopen: '%s'", tmppath);
1289 /* write last commit id (HEAD) */
1290 git_oid_tostr(buf, sizeof(buf), head);
1291 fprintf(wcachefp, "%s\n", buf);
1292
1293 writelog(fp, head);
1294
1295 if (rcachefp) {
1296 /* append previous log to log.html and the new cache */
1297 while (!feof(rcachefp)) {
1298 n = fread(buf, 1, sizeof(buf), rcachefp);
1299 if (ferror(rcachefp))
1300 err(1, "fread");
1301 if (fwrite(buf, 1, n, fp) != n ||
1302 fwrite(buf, 1, n, wcachefp) != n)
1303 err(1, "fwrite");
1304 }
1305 fclose(rcachefp);
1306 }
1307 fclose(wcachefp);
1308 } else {
1309 if (head)
1310 writelog(fp, head);
1311 }
1312
1313 fputs("</tbody></table>", fp);
1314 writefooter(fp);
1315 fclose(fp);
1316
1317 /* files for HEAD */
1318 fp = efopen("files.html", "w");
1319 writeheader(fp, "Files");
1320 if (head)
1321 writefiles(fp, head);
1322 writefooter(fp);
1323 fclose(fp);
1324
1325 /* summary page with branches and tags */
1326 fp = efopen("refs.html", "w");
1327 writeheader(fp, "Refs");
1328 writerefs(fp);
1329 writefooter(fp);
1330 fclose(fp);
1331
1332 /* Atom feed */
1333 fp = efopen("atom.xml", "w");
1334 writeatom(fp, 1);
1335 fclose(fp);
1336
1337 /* Atom feed for tags / releases */
1338 fp = efopen("tags.xml", "w");
1339 writeatom(fp, 0);
1340 fclose(fp);
1341
1342 /* rename new cache file on success */
1343 if (cachefile && head) {
1344 if (rename(tmppath, cachefile))
1345 err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1346 umask((mask = umask(0)));
1347 if (chmod(cachefile,
1348 (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1349 err(1, "chmod: '%s'", cachefile);
1350 }
1351
1352 /* cleanup */
1353 git_repository_free(repo);
1354 git_libgit2_shutdown();
1355
1356 return 0;
1357 }