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