]> git.armaanb.net Git - stagit.git/blob - urmoms.c
prettification
[stagit.git] / urmoms.c
1 #include <sys/stat.h>
2
3 #include <err.h>
4 #include <inttypes.h>
5 #include <libgen.h>
6 #include <limits.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <unistd.h>
11
12 #include "git2.h"
13
14 struct commitinfo {
15         const git_oid *id;
16
17         char oid[GIT_OID_HEXSZ + 1];
18         char parentoid[GIT_OID_HEXSZ + 1];
19
20         const git_signature *author;
21         const char *summary;
22         const char *msg;
23
24         git_diff_stats *stats;
25         git_diff       *diff;
26         git_commit     *commit;
27         git_commit     *parent;
28         git_tree       *commit_tree;
29         git_tree       *parent_tree;
30
31         size_t addcount;
32         size_t delcount;
33         size_t filecount;
34 };
35
36 static git_repository *repo;
37
38 static const char *relpath = "";
39 static const char *repodir;
40
41 static char name[255];
42 static char description[255];
43 static int hasreadme, haslicense;
44
45 void
46 commitinfo_free(struct commitinfo *ci)
47 {
48         if (!ci)
49                 return;
50
51         git_diff_stats_free(ci->stats);
52         git_diff_free(ci->diff);
53         git_commit_free(ci->commit);
54 }
55
56 struct commitinfo *
57 commitinfo_getbyoid(const git_oid *id)
58 {
59         struct commitinfo *ci;
60         int error;
61
62         if (!(ci = calloc(1, sizeof(struct commitinfo))))
63                 err(1, "calloc");
64
65         ci->id = id;
66         if (git_commit_lookup(&(ci->commit), repo, id))
67                 goto err;
68
69         /* TODO: show tags when commit has it */
70         git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
71         git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
72
73         ci->author = git_commit_author(ci->commit);
74         ci->summary = git_commit_summary(ci->commit);
75         ci->msg = git_commit_message(ci->commit);
76
77         if ((error = git_commit_tree(&(ci->commit_tree), ci->commit)))
78                 goto err; /* TODO: handle error */
79         if (!(error = git_commit_parent(&(ci->parent), ci->commit, 0))) {
80                 if ((error = git_commit_tree(&(ci->parent_tree), ci->parent)))
81                         goto err;
82         } else {
83                 ci->parent = NULL;
84                 ci->parent_tree = NULL;
85         }
86
87         if ((error = git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, NULL)))
88                 goto err;
89         if (git_diff_get_stats(&(ci->stats), ci->diff))
90                 goto err;
91
92         ci->addcount = git_diff_stats_insertions(ci->stats);
93         ci->delcount = git_diff_stats_deletions(ci->stats);
94         ci->filecount = git_diff_stats_files_changed(ci->stats);
95
96         /* TODO: show tag when commit has it */
97
98         return ci;
99
100 err:
101         commitinfo_free(ci);
102         free(ci);
103
104         return NULL;
105 }
106
107 int
108 writeheader(FILE *fp)
109 {
110         fputs("<!DOCTYPE HTML>"
111                 "<html dir=\"ltr\" lang=\"en\">\n<head>\n"
112                 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
113                 "<meta http-equiv=\"Content-Language\" content=\"en\" />\n", fp);
114         fprintf(fp, "<title>%s%s%s</title>\n", name, description[0] ? " - " : "", description);
115         fprintf(fp, "<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
116         fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
117                 name, relpath);
118         fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
119         fputs("</head>\n<body>\n\n", fp);
120         fprintf(fp, "<table><tr><td><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>"
121                 "<td><h1>%s</h1><span class=\"desc\">%s</span></td></tr><tr><td></td><td>\n",
122                 relpath, name, description);
123         fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
124         fprintf(fp, "<a href=\"%sfiles.html\">Files</a>", relpath);
125         if (hasreadme)
126                 fprintf(fp, " | <a href=\"%sreadme.html\">README</a>", relpath);
127         if (haslicense)
128                 fprintf(fp, " | <a href=\"%slicense.html\">LICENSE</a>", relpath);
129         fputs("</td></tr></table>\n<hr/><div id=\"content\">\n", fp);
130
131         return 0;
132 }
133
134 int
135 writefooter(FILE *fp)
136 {
137         return !fputs("</div></body>\n</html>", fp);
138 }
139
140 FILE *
141 efopen(const char *name, const char *flags)
142 {
143         FILE *fp;
144
145         if (!(fp = fopen(name, flags)))
146                 err(1, "fopen");
147
148         return fp;
149 }
150
151 /* Escape characters below as HTML 2.0 / XML 1.0. */
152 void
153 xmlencode(FILE *fp, const char *s, size_t len)
154 {
155         size_t i;
156
157         for (i = 0; *s && i < len; s++, i++) {
158                 switch(*s) {
159                 case '<':  fputs("&lt;",   fp); break;
160                 case '>':  fputs("&gt;",   fp); break;
161                 case '\'': fputs("&apos;", fp); break;
162                 case '&':  fputs("&amp;",  fp); break;
163                 case '"':  fputs("&quot;", fp); break;
164                 default:   fputc(*s, fp);
165                 }
166         }
167 }
168
169 /* Some implementations of basename(3) return a pointer to a static
170  * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX).
171  * This is a wrapper function that is compatible with both versions.
172  * The program will error out if basename(3) failed, this can only happen
173  * with the OpenBSD version. */
174 char *
175 xbasename(const char *path)
176 {
177         char *p, *b;
178
179         if (!(p = strdup(path)))
180                 err(1, "strdup");
181         if (!(b = basename(p)))
182                 err(1, "basename");
183         if (!(b = strdup(b)))
184                 err(1, "strdup");
185         free(p);
186
187         return b;
188 }
189
190 void
191 printtimeformat(FILE *fp, const git_time *intime, const char *fmt)
192 {
193         struct tm *intm;
194         time_t t;
195         char out[32];
196
197         t = (time_t) intime->time + (intime->offset * 60);
198         intm = gmtime(&t);
199         strftime(out, sizeof(out), fmt, intm);
200         fputs(out, fp);
201 }
202
203 void
204 printtimez(FILE *fp, const git_time *intime)
205 {
206         printtimeformat(fp, intime, "%Y-%m-%dT%H:%M:%SZ");
207 }
208
209 void
210 printtime(FILE *fp, const git_time *intime)
211 {
212         printtimeformat(fp, intime, "%a %b %e %T %Y");
213 }
214
215 void
216 printtimeshort(FILE *fp, const git_time *intime)
217 {
218         printtimeformat(fp, intime, "%Y-%m-%d %H:%M");
219 }
220
221 void
222 writeblobhtml(FILE *fp, const git_blob *blob)
223 {
224         xmlencode(fp, git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob));
225 }
226
227 void
228 printcommit(FILE *fp, struct commitinfo *ci)
229 {
230         /* TODO: show tag when commit has it */
231         fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
232                 relpath, ci->oid, ci->oid);
233
234         if (ci->parentoid[0])
235                 fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
236                         relpath, ci->parentoid, ci->parentoid);
237
238 #if 0
239         if ((count = (int)git_commit_parentcount(commit)) > 1) {
240                 fprintf(fp, "<b>Merge:</b>");
241                 for (i = 0; i < count; i++) {
242                         git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
243                         fprintf(fp, " <a href=\"%scommit/%s.html\">%s</a>",
244                                 relpath, buf, buf);
245                 }
246                 fputc('\n', fp);
247         }
248 #endif
249         if (ci->author) {
250                 fprintf(fp, "<b>Author:</b> ");
251                 xmlencode(fp, ci->author->name, strlen(ci->author->name));
252                 fprintf(fp, " &lt;<a href=\"mailto:");
253                 xmlencode(fp, ci->author->email, strlen(ci->author->email));
254                 fputs("\">", fp);
255                 xmlencode(fp, ci->author->email, strlen(ci->author->email));
256                 fputs("</a>&gt;\n<b>Date:</b>   ", fp);
257                 printtime(fp, &(ci->author->when));
258                 fputc('\n', fp);
259         }
260         fputc('\n', fp);
261
262         if (ci->msg)
263                 xmlencode(fp, ci->msg, strlen(ci->msg));
264
265         fputc('\n', fp);
266 }
267
268 void
269 printshowfile(struct commitinfo *ci)
270 {
271         const git_diff_delta *delta;
272         const git_diff_hunk *hunk;
273         const git_diff_line *line;
274         git_patch *patch;
275         git_buf statsbuf;
276         size_t ndeltas, nhunks, nhunklines;
277         FILE *fp;
278         size_t i, j, k;
279         char path[PATH_MAX];
280
281         snprintf(path, sizeof(path), "commit/%s.html", ci->oid);
282         /* check if file exists if so skip it */
283         if (!access(path, F_OK))
284                 return;
285
286         fp = efopen(path, "w+b");
287         writeheader(fp);
288         fputs("<pre>\n", fp);
289         printcommit(fp, ci);
290
291         memset(&statsbuf, 0, sizeof(statsbuf));
292
293         /* diff stat */
294         if (ci->stats) {
295                 if (!git_diff_stats_to_buf(&statsbuf, ci->stats,
296                     GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_SHORT, 80)) {
297                         if (statsbuf.ptr && statsbuf.ptr[0]) {
298                                 fprintf(fp, "<b>Diffstat:</b>\n");
299                                 fputs(statsbuf.ptr, fp);
300                         }
301                 }
302         }
303
304         fputs("<hr/>", fp);
305
306         ndeltas = git_diff_num_deltas(ci->diff);
307         for (i = 0; i < ndeltas; i++) {
308                 if (git_patch_from_diff(&patch, ci->diff, i)) {
309                         git_patch_free(patch);
310                         break; /* TODO: handle error */
311                 }
312
313                 delta = git_patch_get_delta(patch);
314                 fprintf(fp, "<b>diff --git a/<a href=\"%sfile/%s\">%s</a> b/<a href=\"%sfile/%s\">%s</a></b>\n",
315                         relpath, delta->old_file.path, delta->old_file.path,
316                         relpath, delta->new_file.path, delta->new_file.path);
317
318                 /* check binary data */
319                 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
320                         fputs("Binary files differ\n", fp);
321                         git_patch_free(patch);
322                         continue;
323                 }
324
325                 nhunks = git_patch_num_hunks(patch);
326                 for (j = 0; j < nhunks; j++) {
327                         if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
328                                 break; /* TODO: handle error ? */
329
330                         fprintf(fp, "<span class=\"h\">%s</span>\n", hunk->header);
331
332                         for (k = 0; ; k++) {
333                                 if (git_patch_get_line_in_hunk(&line, patch, j, k))
334                                         break;
335                                 if (line->old_lineno == -1)
336                                         fprintf(fp, "<span class=\"i\"><a href=\"#h%zu-%zu\" id=\"h%zu-%zu\">+",
337                                                 j, k, j, k);
338                                 else if (line->new_lineno == -1)
339                                         fprintf(fp, "<span class=\"d\"><a href=\"#h%zu-%zu\" id=\"h%zu-%zu\">-",
340                                                 j, k, j, k);
341                                 else
342                                         fputc(' ', fp);
343                                 xmlencode(fp, line->content, line->content_len);
344                                 if (line->old_lineno == -1 || line->new_lineno == -1)
345                                         fputs("</a></span>", fp);
346                         }
347                 }
348                 git_patch_free(patch);
349         }
350         git_buf_free(&statsbuf);
351
352         fputs( "</pre>\n", fp);
353         writefooter(fp);
354         fclose(fp);
355         return;
356 }
357
358 int
359 writelog(FILE *fp)
360 {
361         struct commitinfo *ci;
362         git_revwalk *w = NULL;
363         git_oid id;
364         size_t len;
365         int ret = 0;
366
367         mkdir("commit", 0755);
368
369         git_revwalk_new(&w, repo);
370         git_revwalk_push_head(w);
371
372         /* TODO: also make "expanded" log ? (with message body) */
373         fputs("<table><thead>\n<tr><td align=\"right\">Age</td><td>Commit message</td><td>Author</td>"
374               "<td align=\"right\">Files</td><td align=\"right\">+</td><td align=\"right\">-</td></tr>\n</thead><tbody>\n", fp);
375         while (!git_revwalk_next(&id, w)) {
376                 relpath = "";
377
378                 if (!(ci = commitinfo_getbyoid(&id)))
379                         break;
380
381                 fputs("<tr><td align=\"right\">", fp);
382                 if (ci->author)
383                         printtimeshort(fp, &(ci->author->when));
384                 fputs("</td><td>", fp);
385                 if (ci->summary) {
386                         fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
387                         if ((len = strlen(ci->summary)) > 79) {
388                                 xmlencode(fp, ci->summary, 76);
389                                 fputs("...", fp);
390                         } else {
391                                 xmlencode(fp, ci->summary, len);
392                         }
393                         fputs("</a>", fp);
394                 }
395                 fputs("</td><td>", fp);
396                 if (ci->author)
397                         xmlencode(fp, ci->author->name, strlen(ci->author->name));
398                 fputs("</td><td align=\"right\">", fp);
399                 fprintf(fp, "%zu", ci->filecount);
400                 fputs("</td><td align=\"right\">", fp);
401                 fprintf(fp, "+%zu", ci->addcount);
402                 fputs("</td><td align=\"right\">", fp);
403                 fprintf(fp, "-%zu", ci->delcount);
404                 fputs("</td></tr>\n", fp);
405
406                 relpath = "../";
407                 printshowfile(ci);
408
409                 commitinfo_free(ci);
410         }
411         fprintf(fp, "</tbody></table>");
412
413         git_revwalk_free(w);
414         relpath = "";
415
416         return ret;
417 }
418
419 void
420 printcommitatom(FILE *fp, struct commitinfo *ci)
421 {
422         fputs("<entry>\n", fp);
423
424         fprintf(fp, "<id>%s</id>\n", ci->oid);
425         if (ci->author) {
426                 fputs("<updated>", fp);
427                 printtimez(fp, &(ci->author->when));
428                 fputs("</updated>\n", fp);
429         }
430         if (ci->summary) {
431                 fputs("<title type=\"text\">", fp);
432                 xmlencode(fp, ci->summary, strlen(ci->summary));
433                 fputs("</title>\n", fp);
434         }
435
436         fputs("<content type=\"text\">", fp);
437         fprintf(fp, "commit %s\n", ci->oid);
438         if (ci->parentoid[0])
439                 fprintf(fp, "parent %s\n", ci->parentoid);
440
441 #if 0
442         if ((count = (int)git_commit_parentcount(commit)) > 1) {
443                 fprintf(fp, "Merge:");
444                 for (i = 0; i < count; i++) {
445                         git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
446                         fprintf(fp, " %s", buf);
447                 }
448                 fputc('\n', fp);
449         }
450 #endif
451
452         if (ci->author) {
453                 fprintf(fp, "Author: ");
454                 xmlencode(fp, ci->author->name, strlen(ci->author->name));
455                 fprintf(fp, " &lt;");
456                 xmlencode(fp, ci->author->email, strlen(ci->author->email));
457                 fprintf(fp, "&gt;\nDate:   ");
458                 printtime(fp, &(ci->author->when));
459         }
460         fputc('\n', fp);
461
462         if (ci->msg)
463                 xmlencode(fp, ci->msg, strlen(ci->msg));
464         fputs("\n</content>\n", fp);
465         if (ci->author) {
466                 fputs("<author><name>", fp);
467                 xmlencode(fp, ci->author->name, strlen(ci->author->name));
468                 fputs("</name>\n<email>", fp);
469                 xmlencode(fp, ci->author->email, strlen(ci->author->email));
470                 fputs("</email>\n</author>\n", fp);
471         }
472         fputs("</entry>\n", fp);
473 }
474
475 int
476 writeatom(FILE *fp)
477 {
478         struct commitinfo *ci;
479         git_revwalk *w = NULL;
480         git_oid id;
481         size_t i, m = 100; /* max */
482
483         fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", fp);
484         fputs("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
485         xmlencode(fp, name, strlen(name));
486         fputs(", branch master</title>\n<subtitle>", fp);
487
488         xmlencode(fp, description, strlen(description));
489         fputs("</subtitle>\n", fp);
490
491         git_revwalk_new(&w, repo);
492         git_revwalk_push_head(w);
493
494         for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
495                 if (!(ci = commitinfo_getbyoid(&id)))
496                         break;
497                 printcommitatom(fp, ci);
498                 commitinfo_free(ci);
499         }
500         git_revwalk_free(w);
501
502         fputs("</feed>", fp);
503
504         return 0;
505 }
506
507 int
508 writefiles(FILE *fp)
509 {
510         const git_index_entry *entry;
511         git_index *index;
512         size_t count, i;
513
514         fputs("<table><thead>\n"
515               "<tr><td>Mode</td><td>Name</td><td align=\"right\">Size</td></tr>\n"
516               "</thead><tbody>\n", fp);
517
518         git_repository_index(&index, repo);
519         count = git_index_entrycount(index);
520
521         for (i = 0; i < count; i++) {
522                 entry = git_index_get_byindex(index, i);
523                 fputs("<tr><td>", fp);
524                 fprintf(fp, "%u", entry->mode); /* TODO: fancy print, like: "-rw-r--r--" */
525                 fprintf(fp, "</td><td><a href=\"%sfile/", relpath);
526                 xmlencode(fp, entry->path, strlen(entry->path));
527                 fputs("\">", fp);
528                 xmlencode(fp, entry->path, strlen(entry->path));
529                 fputs("</a></td><td align=\"right\">", fp);
530                 fprintf(fp, "%" PRIu64, entry->file_size);
531                 fputs("</td></tr>\n", fp);
532         }
533
534         fputs("</tbody></table>", fp);
535
536         return 0;
537 }
538
539 int
540 main(int argc, char *argv[])
541 {
542         git_object *obj = NULL;
543         const git_error *e = NULL;
544         FILE *fp, *fpread;
545         char path[PATH_MAX], *p;
546         int status;
547
548         if (argc != 2) {
549                 fprintf(stderr, "%s <repodir>\n", argv[0]);
550                 return 1;
551         }
552         repodir = argv[1];
553
554         git_libgit2_init();
555
556         if ((status = git_repository_open(&repo, repodir)) < 0) {
557                 e = giterr_last();
558                 fprintf(stderr, "error %d/%d: %s\n", status, e->klass, e->message);
559                 return status;
560         }
561
562         /* use directory name as name */
563         p = xbasename(repodir);
564         snprintf(name, sizeof(name), "%s", p);
565         free(p);
566
567         /* read description or .git/description */
568         snprintf(path, sizeof(path), "%s%s%s",
569                 repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
570         if (!(fpread = fopen(path, "r+b"))) {
571                 snprintf(path, sizeof(path), "%s%s%s",
572                         repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
573                 fpread = fopen(path, "r+b");
574         }
575         if (fpread) {
576                 if (!fgets(description, sizeof(description), fpread))
577                         description[0] = '\0';
578                 fclose(fpread);
579         }
580
581         /* check LICENSE */
582         haslicense = !git_revparse_single(&obj, repo, "HEAD:LICENSE");
583         /* check README */
584         hasreadme = !git_revparse_single(&obj, repo, "HEAD:README");
585
586         /* read LICENSE */
587         if (!git_revparse_single(&obj, repo, "HEAD:LICENSE")) {
588                 fp = efopen("license.html", "w+b");
589                 writeheader(fp);
590                 fputs("<pre>\n", fp);
591                 writeblobhtml(fp, (git_blob *)obj);
592                 if (ferror(fp))
593                         err(1, "fwrite");
594                 fputs("</pre>\n", fp);
595                 writefooter(fp);
596
597                 fclose(fp);
598         }
599
600         /* read README */
601         if (!git_revparse_single(&obj, repo, "HEAD:README")) {
602                 fp = efopen("readme.html", "w+b");
603                 writeheader(fp);
604                 fputs("<pre>\n", fp);
605                 writeblobhtml(fp, (git_blob *)obj);
606                 if (ferror(fp))
607                         err(1, "fwrite");
608                 fputs("</pre>\n", fp);
609                 writefooter(fp);
610                 fclose(fp);
611         }
612
613         fp = efopen("log.html", "w+b");
614         writeheader(fp);
615         writelog(fp);
616         writefooter(fp);
617         fclose(fp);
618
619         fp = efopen("files.html", "w+b");
620         writeheader(fp);
621         writefiles(fp);
622         writefooter(fp);
623         fclose(fp);
624
625         /* Atom feed */
626         fp = efopen("atom.xml", "w+b");
627         writeatom(fp);
628         fclose(fp);
629
630         /* cleanup */
631         git_repository_free(repo);
632         git_libgit2_shutdown();
633
634         return 0;
635 }