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