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