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