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