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