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