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