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