stagit

My personal build of stagit

stagit.c (40253B)

   1 #include <sys/stat.h>
   2 #include <sys/types.h>
   3 
   4 #include <err.h>
   5 #include <errno.h>
   6 #include <libgen.h>
   7 #include <limits.h>
   8 #include <stdint.h>
   9 #include <stdio.h>
  10 #include <stdlib.h>
  11 #include <stdbool.h>
  12 #include <string.h>
  13 #include <time.h>
  14 #include <unistd.h>
  15 #include <fcntl.h>
  16 
  17 #include <git2.h>
  18 #include "cmark.h"
  19 
  20 #include "compat.h"
  21 
  22 #define LEN(s) (sizeof(s)/sizeof(*s))
  23 
  24 struct deltainfo {
  25   git_patch *patch;
  26 
  27   size_t addcount;
  28   size_t delcount;
  29 };
  30 
  31 struct commitinfo {
  32   const git_oid *id;
  33 
  34   char oid[GIT_OID_HEXSZ + 1];
  35   char parentoid[GIT_OID_HEXSZ + 1];
  36 
  37   const git_signature *author;
  38   const git_signature *committer;
  39   const char          *summary;
  40   const char          *msg;
  41 
  42   git_diff   *diff;
  43   git_commit *commit;
  44   git_commit *parent;
  45   git_tree   *commit_tree;
  46   git_tree   *parent_tree;
  47 
  48   size_t addcount;
  49   size_t delcount;
  50   size_t filecount;
  51 
  52   struct deltainfo **deltas;
  53   size_t ndeltas;
  54 };
  55 
  56 /* reference and associated data for sorting */
  57 struct referenceinfo {
  58   struct git_reference *ref;
  59   struct commitinfo *ci;
  60 };
  61 
  62 static git_repository *repo;
  63 
  64 static const char *line_number_format_strings[7] = {
  65  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%1zu</a> ",
  66  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%2zu</a> ",
  67  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%3zu</a> ",
  68  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%4zu</a> ",
  69  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%5zu</a> ",
  70  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%6zu</a> ",
  71  "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> ",
  72 };
  73 
  74 static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
  75 static const char *relpath = "";
  76 static const char *repodir;
  77 
  78 static char *name = "";
  79 static char *strippedname = "";
  80 static char description[255];
  81 static char cloneurl[1024];
  82 static char *submodules;
  83 static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:COPYING" };
  84 static char *license;
  85 static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
  86 static char *readme;
  87 static long long nlogcommits = -1; /* < 0 indicates not used */
  88 
  89 /* cache */
  90 static git_oid lastoid;
  91 static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
  92 static FILE *rcachefp, *wcachefp;
  93 static const char *cachefile;
  94 
  95 void joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
  96 {
  97   int r;
  98 
  99   r = snprintf(buf, bufsiz, "%s%s%s",
 100       path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 101   if (r < 0 || (size_t)r >= bufsiz)
 102     errx(1, "path truncated: '%s%s%s'",
 103         path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 104 }
 105 
 106 void deltainfo_free(struct deltainfo *di)
 107 {
 108   if (!di)
 109     return;
 110   git_patch_free(di->patch);
 111   memset(di, 0, sizeof(*di));
 112   free(di);
 113 }
 114 
 115 int commitinfo_getstats(struct commitinfo *ci)
 116 {
 117   struct deltainfo *di;
 118   git_diff_options opts;
 119   git_diff_find_options fopts;
 120   const git_diff_delta *delta;
 121   const git_diff_hunk *hunk;
 122   const git_diff_line *line;
 123   git_patch *patch = NULL;
 124   size_t ndeltas, nhunks, nhunklines;
 125   size_t i, j, k;
 126 
 127   if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
 128     goto err;
 129   if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
 130     if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
 131       ci->parent = NULL;
 132       ci->parent_tree = NULL;
 133     }
 134   }
 135 
 136   git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
 137   opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
 138     GIT_DIFF_IGNORE_SUBMODULES |
 139     GIT_DIFF_INCLUDE_TYPECHANGE;
 140   if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
 141     goto err;
 142 
 143   if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
 144     goto err;
 145   /* find renames and copies, exact matches (no heuristic) for renames. */
 146   fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
 147     GIT_DIFF_FIND_EXACT_MATCH_ONLY;
 148   if (git_diff_find_similar(ci->diff, &fopts))
 149     goto err;
 150 
 151   ndeltas = git_diff_num_deltas(ci->diff);
 152   if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
 153     err(1, "calloc");
 154 
 155   for (i = 0; i < ndeltas; i++) {
 156     if (git_patch_from_diff(&patch, ci->diff, i))
 157       goto err;
 158 
 159     if (!(di = calloc(1, sizeof(struct deltainfo))))
 160       err(1, "calloc");
 161     di->patch = patch;
 162     ci->deltas[i] = di;
 163 
 164     delta = git_patch_get_delta(patch);
 165 
 166     /* skip stats for binary data */
 167     if (delta->flags & GIT_DIFF_FLAG_BINARY)
 168       continue;
 169 
 170     nhunks = git_patch_num_hunks(patch);
 171     for (j = 0; j < nhunks; j++) {
 172       if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 173         break;
 174       for (k = 0; ; k++) {
 175         if (git_patch_get_line_in_hunk(&line, patch, j, k))
 176           break;
 177         if (line->old_lineno == -1) {
 178           di->addcount++;
 179           ci->addcount++;
 180         } else if (line->new_lineno == -1) {
 181           di->delcount++;
 182           ci->delcount++;
 183         }
 184       }
 185     }
 186   }
 187   ci->ndeltas = i;
 188   ci->filecount = i;
 189 
 190   return 0;
 191 
 192 err:
 193   git_diff_free(ci->diff);
 194   ci->diff = NULL;
 195   git_tree_free(ci->commit_tree);
 196   ci->commit_tree = NULL;
 197   git_tree_free(ci->parent_tree);
 198   ci->parent_tree = NULL;
 199   git_commit_free(ci->parent);
 200   ci->parent = NULL;
 201 
 202   if (ci->deltas)
 203     for (i = 0; i < ci->ndeltas; i++)
 204       deltainfo_free(ci->deltas[i]);
 205   free(ci->deltas);
 206   ci->deltas = NULL;
 207   ci->ndeltas = 0;
 208   ci->addcount = 0;
 209   ci->delcount = 0;
 210   ci->filecount = 0;
 211 
 212   return -1;
 213 }
 214 
 215 void commitinfo_free(struct commitinfo *ci)
 216 {
 217   size_t i;
 218 
 219   if (!ci)
 220     return;
 221   if (ci->deltas)
 222     for (i = 0; i < ci->ndeltas; i++)
 223       deltainfo_free(ci->deltas[i]);
 224 
 225   free(ci->deltas);
 226   git_diff_free(ci->diff);
 227   git_tree_free(ci->commit_tree);
 228   git_tree_free(ci->parent_tree);
 229   git_commit_free(ci->commit);
 230   git_commit_free(ci->parent);
 231   memset(ci, 0, sizeof(*ci));
 232   free(ci);
 233 }
 234 
 235   struct commitinfo *
 236 commitinfo_getbyoid(const git_oid *id)
 237 {
 238   struct commitinfo *ci;
 239 
 240   if (!(ci = calloc(1, sizeof(struct commitinfo))))
 241     err(1, "calloc");
 242 
 243   if (git_commit_lookup(&(ci->commit), repo, id))
 244     goto err;
 245   ci->id = id;
 246 
 247   git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
 248   git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
 249 
 250   ci->author = git_commit_author(ci->commit);
 251   ci->committer = git_commit_committer(ci->commit);
 252   ci->summary = git_commit_summary(ci->commit);
 253   ci->msg = git_commit_message(ci->commit);
 254 
 255   return ci;
 256 
 257 err:
 258   commitinfo_free(ci);
 259 
 260   return NULL;
 261 }
 262 
 263 int refs_cmp(const void *v1, const void *v2)
 264 {
 265   const struct referenceinfo *r1 = v1, *r2 = v2;
 266   time_t t1, t2;
 267   int r;
 268 
 269   if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
 270     return r;
 271 
 272   t1 = r1->ci->author ? r1->ci->author->when.time : 0;
 273   t2 = r2->ci->author ? r2->ci->author->when.time : 0;
 274   if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
 275     return r;
 276 
 277   return strcmp(git_reference_shorthand(r1->ref),
 278       git_reference_shorthand(r2->ref));
 279 }
 280 
 281   int
 282 getrefs(struct referenceinfo **pris, size_t *prefcount)
 283 {
 284   struct referenceinfo *ris = NULL;
 285   struct commitinfo *ci = NULL;
 286   git_reference_iterator *it = NULL;
 287   const git_oid *id = NULL;
 288   git_object *obj = NULL;
 289   git_reference *dref = NULL, *r, *ref = NULL;
 290   size_t i, refcount;
 291 
 292   *pris = NULL;
 293   *prefcount = 0;
 294 
 295   if (git_reference_iterator_new(&it, repo))
 296     return -1;
 297 
 298   for (refcount = 0; !git_reference_next(&ref, it); ) {
 299     if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
 300       git_reference_free(ref);
 301       ref = NULL;
 302       continue;
 303     }
 304 
 305     switch (git_reference_type(ref)) {
 306       case GIT_REF_SYMBOLIC:
 307         if (git_reference_resolve(&dref, ref))
 308           goto err;
 309         r = dref;
 310         break;
 311       case GIT_REF_OID:
 312         r = ref;
 313         break;
 314       default:
 315         continue;
 316     }
 317     if (!git_reference_target(r) ||
 318         git_reference_peel(&obj, r, GIT_OBJ_ANY))
 319       goto err;
 320     if (!(id = git_object_id(obj)))
 321       goto err;
 322     if (!(ci = commitinfo_getbyoid(id)))
 323       break;
 324 
 325     if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
 326       err(1, "realloc");
 327     ris[refcount].ci = ci;
 328     ris[refcount].ref = r;
 329     refcount++;
 330 
 331     git_object_free(obj);
 332     obj = NULL;
 333     git_reference_free(dref);
 334     dref = NULL;
 335   }
 336   git_reference_iterator_free(it);
 337 
 338   /* sort by type, date then shorthand name */
 339   qsort(ris, refcount, sizeof(*ris), refs_cmp);
 340 
 341   *pris = ris;
 342   *prefcount = refcount;
 343 
 344   return 0;
 345 
 346 err:
 347   git_object_free(obj);
 348   git_reference_free(dref);
 349   commitinfo_free(ci);
 350   for (i = 0; i < refcount; i++) {
 351     commitinfo_free(ris[i].ci);
 352     git_reference_free(ris[i].ref);
 353   }
 354   free(ris);
 355 
 356   return -1;
 357 }
 358 
 359 FILE * efopen(const char *filename, const char *flags)
 360 {
 361   FILE *fp;
 362 
 363   if (!(fp = fopen(filename, flags)))
 364     err(1, "fopen: '%s'", filename);
 365 
 366   return fp;
 367 }
 368 
 369 /* Escape characters below as HTML 2.0 / XML 1.0. */
 370 void xmlencode(FILE *fp, const char *s, size_t len)
 371 {
 372   size_t i;
 373 
 374   for (i = 0; *s && i < len; s++, i++) {
 375     switch(*s) {
 376       case '<':  fputs("&lt;",   fp); break;
 377       case '>':  fputs("&gt;",   fp); break;
 378       case '\'': fputs("&#39;",  fp); break;
 379       case '&':  fputs("&amp;",  fp); break;
 380       case '"':  fputs("&quot;", fp); break;
 381       default:   putc(*s, fp);
 382     }
 383   }
 384 }
 385 
 386 int mkdirp(const char *path)
 387 {
 388   char tmp[PATH_MAX], *p;
 389 
 390   if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
 391     errx(1, "path truncated: '%s'", path);
 392   for (p = tmp + (tmp[0] == '/'); *p; p++) {
 393     if (*p != '/')
 394       continue;
 395     *p = '\0';
 396     if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 397       return -1;
 398     *p = '/';
 399   }
 400   if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 401     return -1;
 402   return 0;
 403 }
 404 
 405 void printtimez(FILE *fp, const git_time *intime)
 406 {
 407   struct tm *intm;
 408   time_t t;
 409   char out[32];
 410 
 411   t = (time_t)intime->time;
 412   if (!(intm = gmtime(&t)))
 413     return;
 414   strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
 415   fputs(out, fp);
 416 }
 417 
 418 void printtime(FILE *fp, const git_time *intime)
 419 {
 420   struct tm *intm;
 421   time_t t;
 422   char out[32];
 423 
 424   t = (time_t)intime->time + (intime->offset * 60);
 425   if (!(intm = gmtime(&t)))
 426     return;
 427   strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
 428   if (intime->offset < 0)
 429     fprintf(fp, "%s -%02d%02d", out,
 430         -(intime->offset) / 60, -(intime->offset) % 60);
 431   else
 432     fprintf(fp, "%s +%02d%02d", out,
 433         intime->offset / 60, intime->offset % 60);
 434 }
 435 
 436 void formattime(FILE *fp, const char *fstring, const git_time *intime)
 437 {
 438   struct tm *intm;
 439   time_t t;
 440   char out[32];
 441 
 442   t = (time_t)intime->time;
 443   if (!(intm = gmtime(&t)))
 444     return;
 445   strftime(out, sizeof(out), fstring, intm);
 446   fputs(out, fp);
 447 }
 448 
 449 void writeheader(FILE *fp, const char *title)
 450 {
 451   fputs("<!DOCTYPE html>\n"
 452   "<html>\n<head>\n"
 453   "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n"
 454   "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n"
 455   "<title>", fp);
 456   xmlencode(fp, strippedname, strlen(strippedname));
 457   if (title != NULL) {
 458     fprintf(fp, " &mdash; ");
 459     xmlencode(fp, title, strlen(title));
 460   }
 461   fprintf(fp,
 462           "</title>\n"
 463           "<link rel=\"icon\" "
 464           "type=\"image/svg\" "
 465           "href=\"/icons/favicon.svg\" />\n");
 466   fprintf(fp,
 467           "<link rel=\"stylesheet\" "
 468           "type=\"text/css\" "
 469           "href=\"/styles.css\" />\n",
 470           relpath);
 471   fputs("</head>\n<body>\n", fp);
 472 
 473   // The navigation bar
 474   fputs("<header>\n"
 475     "<nav>\n"
 476     "<a href=\"https://git.pablopie.xyz\">\n"
 477     "<img aria-hidden=\"true\" "
 478     "alt=\"Website logo\" "
 479     "src=\"/icons/favicon.svg\">\n"
 480     "git.pablopie.xyz\n"
 481     "</a>\n"
 482     "</nav>\n"
 483     "</header>\n", fp);
 484 
 485   fputs("<main>\n<h1>", fp);
 486   xmlencode(fp, strippedname, strlen(strippedname));
 487   fputs("</h1>\n<p>", fp);
 488   xmlencode(fp, description, strlen(description));
 489   fputs("</p>\n<nav>\n<ul>\n", fp);
 490   if (cloneurl[0]) {
 491     fputs("<li>git clone: <a href=\"", fp);
 492     xmlencode(fp, cloneurl, strlen(cloneurl));
 493     fputs("\">", fp);
 494     xmlencode(fp, cloneurl, strlen(cloneurl));
 495     fputs("</a>\n</li>\n", fp);
 496   }
 497   fprintf(fp, "<li><a href=\"%slog.html\">Log</a></li>\n", relpath);
 498   fprintf(fp, "<li><a href=\"%sfiles.html\">Files</a></li>\n", relpath);
 499   fprintf(fp, "<li><a href=\"%srefs.html\">Refs</a></li>\n", relpath);
 500   if (submodules)
 501     fprintf(fp, "<li><a href=\"%sfile/%s.html\">Submodules</a></li>\n",
 502         relpath, submodules);
 503   if (readme)
 504     fprintf(fp, "<li><a href=\"%s%s\">README</a></li>\n", relpath, readme);
 505   if (license)
 506     fprintf(fp,
 507         "<li><a rel=\"license\" href=\"%sfile/%s.html\">LICENSE</a></li>\n",
 508         relpath, license);
 509 }
 510 
 511 void writefooter(FILE *fp)
 512 {
 513   fputs("</div>\n</main>\n", fp);
 514   fputs("<footer>"
 515       "made with ❤️ by "
 516       "<a rel=\"author\" href=\"https://pablopie.xyz/\">@pablo</a>"
 517       "</footer>\n", fp);
 518   fputs("</body>\n</html>\n", fp);
 519 }
 520 
 521 /*
 522  * Writes the raw contents of a blob in a file
 523  */
 524 ssize_t writeblobraw(const git_blob *blob, const char *fpath)
 525 {
 526   char tmp[PATH_MAX] = "", *d;
 527 
 528   if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
 529     errx(1, "path truncated: '%s'", fpath);
 530   if (!(d = dirname(tmp)))
 531     err(1, "dirname");
 532   if (mkdirp(d))
 533     return -1;
 534 
 535   int fd = open(
 536     fpath, 
 537     O_CREAT | O_WRONLY, 
 538     S_IRUSR | S_IWUSR | S_IROTH | S_IRGRP
 539   );
 540   if (fd == -1) return -1;
 541 
 542   int len = git_blob_rawsize(blob);
 543   const void *s = git_blob_rawcontent(blob);
 544   size_t size = write(fd, s, len);
 545   close(fd);
 546 
 547   return size;
 548 }
 549 
 550 /* Counts the number of lines in a string */
 551 inline size_t line_count(const char *s, size_t len)
 552 {
 553   size_t result = 1;
 554   for (size_t i = 0; i < len; i++) if (s[i] == '\n') result++;
 555   return result;
 556 }
 557 
 558 /* Returns floor(log(n, 10)) */
 559 inline size_t floor_log(size_t n)
 560 {
 561   if (n == 0) return 1;
 562 
 563   size_t result = 0;
 564 
 565   while (n > 0) {
 566     n /= 10;
 567     result++;
 568   }
 569 
 570   return result;
 571 }
 572 
 573 size_t writeblobhtml(FILE *fp, const git_blob *blob)
 574 {
 575   size_t n = 0, i, len, prev;
 576   const char *s = git_blob_rawcontent(blob);
 577 
 578   len = git_blob_rawsize(blob);
 579   fputs("<pre id=\"blob\">\n", fp);
 580 
 581   size_t l_pad = floor_log(line_count(s, len));
 582   const char *nfmt = line_number_format_strings[l_pad < 7 ? l_pad - 1 : 6];
 583 
 584   if (len > 0) {
 585     for (i = 0, prev = 0; i < len; i++) {
 586       if (s[i] != '\n')
 587         continue;
 588       n++;
 589       fprintf(fp, nfmt, n, n, n);
 590       xmlencode(fp, &s[prev], i - prev + 1);
 591       prev = i + 1;
 592     }
 593     /* trailing data */
 594     if ((len - prev) > 0) {
 595       n++;
 596       fprintf(fp, nfmt, n, n, n);
 597       xmlencode(fp, &s[prev], len - prev);
 598     }
 599   }
 600 
 601   fputs("</pre>\n", fp);
 602   return n;
 603 }
 604 
 605 void printcommit(FILE *fp, struct commitinfo *ci)
 606 {
 607   fprintf(fp,
 608       "<article class=\"commit\">\n"
 609       "<dl>\n"
 610       "<dt>Commit</dt>\n"
 611       "<dd><a href=\"%scommit/%s.html\">%s</a></dd>\n",
 612       relpath, ci->oid, ci->oid);
 613 
 614   if (ci->parentoid[0])
 615     fprintf(fp,
 616         "<dt>Parent</dt>\n"
 617         "<dd><a href=\"%scommit/%s.html\">%s</a></dd>\n",
 618         relpath, ci->parentoid, ci->parentoid);
 619 
 620   if (ci->author) {
 621     fputs("<dt>Author</dt>\n<dd>", fp);
 622     xmlencode(fp, ci->author->name, strlen(ci->author->name));
 623     fputs(" &lt;<a href=\"mailto:", fp);
 624     xmlencode(fp, ci->author->email, strlen(ci->author->email));
 625     fputs("\">", fp);
 626     xmlencode(fp, ci->author->email, strlen(ci->author->email));
 627     fputs("</a>&gt;</dd>\n"
 628           "<dt>Date</dt>\n"
 629           "<dd><time datetime=\"",
 630           fp);
 631     printtimez(fp, &(ci->author->when));
 632     fputs("\">", fp);
 633     printtime(fp, &(ci->author->when));
 634     fputs("</time></dd>\n", fp);
 635   }
 636 
 637   fputs("</dl>\n", fp);
 638 
 639   /* Split the message in paragraphs */
 640   if (ci->msg) {
 641     size_t total_len = strlen(ci->msg);
 642     size_t i = 0, len = 0;
 643     char *s = ci->msg;
 644 
 645     while (i < total_len) {
 646       /* If we encounter the string "\n\n" */
 647       if (ci->msg[i] == '\n' && i + 1 < total_len && ci->msg[i + 1] == '\n') {
 648         fputs("<p>", fp);
 649         xmlencode(fp, s, len);
 650         fputs("</p>\n", fp);
 651 
 652         while (ci->msg[i] == '\n') i++;
 653         s = ci->msg + i;
 654         len = 0;
 655       } else {
 656         i++;
 657         len++;
 658       }
 659     }
 660 
 661     if (len > 0) {
 662       fputs("<p>", fp);
 663       xmlencode(fp, s, len);
 664       fputs("</p>\n", fp);
 665     }
 666   }
 667 
 668   fputs("</article>\n", fp);
 669 }
 670 
 671 void printshowfile(FILE *fp, struct commitinfo *ci)
 672 {
 673   const git_diff_delta *delta;
 674   const git_diff_hunk *hunk;
 675   const git_diff_line *line;
 676   git_patch *patch;
 677   size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
 678 
 679   printcommit(fp, ci);
 680 
 681   if (!ci->deltas)
 682     return;
 683 
 684   if (ci->filecount > 1000   ||
 685       ci->ndeltas   > 1000   ||
 686       ci->addcount  > 100000 ||
 687       ci->delcount  > 100000) {
 688     fputs("<p>Diff is too large, output suppressed.</p>\n", fp);
 689     return;
 690   }
 691 
 692   /* diff stat */
 693   fprintf(fp,
 694       "<h2>Diffstat</h2>\n"
 695       "<p>%zu file%s changed, %zu insertion%s, %zu deletion%s\n</p>",
 696       ci->filecount, ci->filecount == 1 ? "" : "s",
 697       ci->addcount,  ci->addcount  == 1 ? "" : "s",
 698       ci->delcount,  ci->delcount  == 1 ? "" : "s");
 699 
 700   fputs("<div class=\"table-container\">\n"
 701         "<table>\n"
 702         "<thead>\n"
 703         "<tr>\n"
 704         "<td>Status</td>\n"
 705         "<td>File Name</td>\n"
 706         "<td>N&deg; Changes</td>\n"
 707         "<td>Insertions</td>\n"
 708         "<td align=\"right\">Deletions</td>\n"
 709         "</tr>\n"
 710         "</thead>\n"
 711         "<tbody>\n",
 712         fp);
 713 
 714   for (i = 0; i < ci->ndeltas; i++) {
 715     delta = git_patch_get_delta(ci->deltas[i]->patch);
 716 
 717     fputs("<tr>\n", fp);
 718 
 719     switch (delta->status) {
 720       case GIT_DELTA_ADDED:      fputs("<td>Added</td>\n", fp);       break;
 721       case GIT_DELTA_COPIED:     fputs("<td>Copied</td>\n", fp);      break;
 722       case GIT_DELTA_DELETED:    fputs("<td>Deleted</td>\n", fp);     break;
 723       case GIT_DELTA_MODIFIED:   fputs("<td>Modified</td>\n", fp);    break;
 724       case GIT_DELTA_RENAMED:    fputs("<td>Renamed</td>\n", fp);     break;
 725       case GIT_DELTA_TYPECHANGE: fputs("<td>Type change</td>\n", fp); break;
 726       default:                   fputs("<td></td>\n", fp);            break;
 727     }
 728 
 729     fprintf(fp, "<td><a href=\"#h%zu\">", i);
 730     xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 731     if (strcmp(delta->old_file.path, delta->new_file.path)) {
 732       fputs(" -&gt; ", fp);
 733       xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 734     }
 735 
 736     fprintf(fp,
 737         "</a></td>\n"
 738         "<td>%zu</td>\n"
 739         "<td>%zu</td>\n"
 740         "<td align=\"right\">%zu</td>\n",
 741         ci->deltas[i]->addcount + ci->deltas[i]->delcount,
 742         ci->deltas[i]->addcount,
 743         ci->deltas[i]->delcount);
 744   }
 745 
 746   fputs("</tbody>\n</table>\n</div>\n", fp);
 747 
 748 
 749   for (i = 0; i < ci->ndeltas; i++) {
 750     fputs("<div class=\"codeblock\">\n<pre>", fp);
 751     patch = ci->deltas[i]->patch;
 752     delta = git_patch_get_delta(patch);
 753     fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
 754     xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 755     fputs(".html\">", fp);
 756     xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 757     fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
 758     xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 759     fprintf(fp, ".html\">");
 760     xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 761     fprintf(fp, "</a></b>\n");
 762 
 763     /* check binary data */
 764     if (delta->flags & GIT_DIFF_FLAG_BINARY) {
 765       fputs("Binary files differ.</pre>\n</div>\n", fp);
 766       continue;
 767     }
 768 
 769     nhunks = git_patch_num_hunks(patch);
 770     for (j = 0; j < nhunks; j++) {
 771       if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 772         break;
 773 
 774       fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
 775       xmlencode(fp, hunk->header, hunk->header_len);
 776       fputs("</a>", fp);
 777 
 778       for (k = 0; ; k++) {
 779         if (git_patch_get_line_in_hunk(&line, patch, j, k))
 780           break;
 781         if (line->old_lineno == -1)
 782           fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
 783               i, j, k, i, j, k);
 784         else if (line->new_lineno == -1)
 785           fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
 786               i, j, k, i, j, k);
 787         else
 788           putc(' ', fp);
 789         xmlencode(fp, line->content, line->content_len);
 790         if (line->old_lineno == -1 || line->new_lineno == -1)
 791           fputs("</a>", fp);
 792       }
 793     }
 794 
 795     fputs("</pre>\n</div>\n", fp);
 796   }
 797 }
 798 
 799 void writelogline(FILE *fp, struct commitinfo *ci)
 800 {
 801   fputs("<article>\n", fp);
 802 
 803   if (ci->author) {
 804     fputs("<div>\n<span>", fp);
 805     xmlencode(fp, ci->author->name, strlen(ci->author->name));
 806     fputs("</span>\n", fp);
 807 
 808     fputs("<time datetime=\"", fp);
 809     formattime(fp, "%Y-%m-%d %H:%M", &(ci->author->when));
 810     fputs("\">", fp);
 811     formattime(fp, "%d/%m/%Y %H:%M", &(ci->author->when));
 812     fputs("</time>\n</div>\n", fp);
 813   }
 814 
 815   if (ci->summary) {
 816     fprintf(fp, "<p>\n<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 817     xmlencode(fp, ci->summary, strlen(ci->summary));
 818     fputs("</a>\n</p>\n", fp);
 819   } else {
 820     fprintf(
 821       fp,
 822       "<p>\n<a href=\"%scommit/%s.html\">Commit %s</a>\n</p>\n",
 823       relpath,
 824       ci->oid,
 825       ci->oid
 826     );
 827   }
 828 
 829   fprintf(fp, "</article>\n");
 830 }
 831 
 832 int writelog(FILE *fp, const git_oid *oid)
 833 {
 834   struct commitinfo *ci;
 835   git_revwalk *w = NULL;
 836   git_oid id;
 837   char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
 838   FILE *fpfile;
 839   int r;
 840 
 841   git_revwalk_new(&w, repo);
 842   git_revwalk_push(w, oid);
 843   git_revwalk_simplify_first_parent(w);
 844 
 845   while (!git_revwalk_next(&id, w)) {
 846     relpath = "";
 847 
 848     if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
 849       break;
 850 
 851     git_oid_tostr(oidstr, sizeof(oidstr), &id);
 852     r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
 853     if (r < 0 || (size_t)r >= sizeof(path))
 854       errx(1, "path truncated: 'commit/%s.html'", oidstr);
 855     r = access(path, F_OK);
 856 
 857     /* optimization: if there are no log lines to write and
 858        the commit file already exists: skip the diffstat */
 859     if (!nlogcommits && !r)
 860       continue;
 861 
 862     if (!(ci = commitinfo_getbyoid(&id)))
 863       break;
 864     /* diffstat: for stagit HTML required for the log.html line */
 865     if (commitinfo_getstats(ci) == -1)
 866       goto err;
 867 
 868     if (nlogcommits < 0) {
 869       writelogline(fp, ci);
 870     } else if (nlogcommits > 0) {
 871       writelogline(fp, ci);
 872       nlogcommits--;
 873       // TODO: Rewrite this
 874       if (!nlogcommits && ci->parentoid[0])
 875         fputs("<tr><td></td><td colspan=\"5\">"
 876             "More commits remaining [...]</td>"
 877             "</tr>\n", fp);
 878     }
 879 
 880     if (cachefile)
 881       writelogline(wcachefp, ci);
 882 
 883     /* check if file exists if so skip it */
 884     if (r) {
 885       relpath = "../";
 886       fpfile = efopen(path, "w");
 887       writeheader(fpfile, ci->summary);
 888       fputs("</ul>\n", fpfile);
 889 
 890       printshowfile(fpfile, ci);
 891       writefooter(fpfile);
 892       fclose(fpfile);
 893     }
 894 err:
 895     commitinfo_free(ci);
 896   }
 897   git_revwalk_free(w);
 898 
 899   relpath = "";
 900 
 901   return 0;
 902 }
 903 
 904 void printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
 905 {
 906   fputs("<entry>\n", fp);
 907 
 908   fprintf(fp, "<id>%s</id>\n", ci->oid);
 909   if (ci->author) {
 910     fputs("<published>", fp);
 911     printtimez(fp, &(ci->author->when));
 912     fputs("</published>\n", fp);
 913   }
 914   if (ci->committer) {
 915     fputs("<updated>", fp);
 916     printtimez(fp, &(ci->committer->when));
 917     fputs("</updated>\n", fp);
 918   }
 919   if (ci->summary) {
 920     fputs("<title type=\"text\">", fp);
 921     if (tag && tag[0]) {
 922       fputs("[", fp);
 923       xmlencode(fp, tag, strlen(tag));
 924       fputs("] ", fp);
 925     }
 926     xmlencode(fp, ci->summary, strlen(ci->summary));
 927     fputs("</title>\n", fp);
 928   }
 929   fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
 930       baseurl, ci->oid);
 931 
 932   if (ci->author) {
 933     fputs("<author>\n<name>", fp);
 934     xmlencode(fp, ci->author->name, strlen(ci->author->name));
 935     fputs("</name>\n<email>", fp);
 936     xmlencode(fp, ci->author->email, strlen(ci->author->email));
 937     fputs("</email>\n</author>\n", fp);
 938   }
 939 
 940   fputs("<content type=\"text\">", fp);
 941   fprintf(fp, "commit %s\n", ci->oid);
 942   if (ci->parentoid[0])
 943     fprintf(fp, "parent %s\n", ci->parentoid);
 944   if (ci->author) {
 945     fputs("Author: ", fp);
 946     xmlencode(fp, ci->author->name, strlen(ci->author->name));
 947     fputs(" &lt;", fp);
 948     xmlencode(fp, ci->author->email, strlen(ci->author->email));
 949     fputs("&gt;\nDate:   ", fp);
 950     printtime(fp, &(ci->author->when));
 951     putc('\n', fp);
 952   }
 953   if (ci->msg) {
 954     putc('\n', fp);
 955     xmlencode(fp, ci->msg, strlen(ci->msg));
 956   }
 957   fputs("\n</content>\n</entry>\n", fp);
 958 }
 959 
 960 int writeatom(FILE *fp, int all)
 961 {
 962   struct referenceinfo *ris = NULL;
 963   size_t refcount = 0;
 964   struct commitinfo *ci;
 965   git_revwalk *w = NULL;
 966   git_oid id;
 967   size_t i, m = 100; /* last 'm' commits */
 968 
 969   fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
 970       "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
 971   xmlencode(fp, strippedname, strlen(strippedname));
 972   fputs(", branch HEAD</title>\n<subtitle>", fp);
 973   xmlencode(fp, description, strlen(description));
 974   fputs("</subtitle>\n", fp);
 975 
 976   /* all commits or only tags? */
 977   if (all) {
 978     git_revwalk_new(&w, repo);
 979     git_revwalk_push_head(w);
 980     git_revwalk_simplify_first_parent(w);
 981     for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
 982       if (!(ci = commitinfo_getbyoid(&id)))
 983         break;
 984       printcommitatom(fp, ci, "");
 985       commitinfo_free(ci);
 986     }
 987     git_revwalk_free(w);
 988   } else if (getrefs(&ris, &refcount) != -1) {
 989     /* references: tags */
 990     for (i = 0; i < refcount; i++) {
 991       if (git_reference_is_tag(ris[i].ref))
 992         printcommitatom(fp, ris[i].ci,
 993             git_reference_shorthand(ris[i].ref));
 994 
 995       commitinfo_free(ris[i].ci);
 996       git_reference_free(ris[i].ref);
 997     }
 998     free(ris);
 999   }
1000 
1001   fputs("</feed>\n", fp);
1002 
1003   return 0;
1004 }
1005 
1006 size_t writeblob(git_object *obj, const char *fpath, const char *blobpath, const char *filename, size_t filesize)
1007 {
1008   char tmp[PATH_MAX] = "", *d;
1009   const char *p;
1010   size_t lc = 0;
1011   FILE *fp;
1012 
1013   if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1014     errx(1, "path truncated: '%s'", fpath);
1015   if (!(d = dirname(tmp)))
1016     err(1, "dirname");
1017   if (mkdirp(d))
1018     return -1;
1019 
1020   for (p = fpath, tmp[0] = '\0'; *p; p++) {
1021     if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1022       errx(1, "path truncated: '../%s'", tmp);
1023   }
1024   relpath = tmp;
1025 
1026   fp = efopen(fpath, "w");
1027   writeheader(fp, filename);
1028   fprintf(fp, "<li><a href=\"%s%s\">raw file (blob)</a></li>", relpath, blobpath);
1029   fputs("</ul>\n</nav>\n", fp);
1030   fputs("<h2>", fp);
1031   xmlencode(fp, filename, strlen(filename));
1032   fprintf(fp, " (%zuB)", filesize);
1033   fputs("</h2>\n<div class=\"codeblock\">\n", fp);
1034 
1035   if (git_blob_is_binary((git_blob *)obj)) {
1036     fputs("<pre>Binary file</pre>\n", fp);
1037   } else {
1038     lc = writeblobhtml(fp, (git_blob *)obj);
1039     if (ferror(fp))
1040       err(1, "fwrite");
1041   }
1042 
1043   fputs("</div>\n", fp);
1044   writefooter(fp);
1045   fclose(fp);
1046 
1047   relpath = "";
1048   return lc;
1049 }
1050 
1051 const char * filemode(git_filemode_t m)
1052 {
1053   static char mode[11];
1054 
1055   memset(mode, '-', sizeof(mode) - 1);
1056   mode[10] = '\0';
1057 
1058   if (S_ISREG(m))
1059     mode[0] = '-';
1060   else if (S_ISBLK(m))
1061     mode[0] = 'b';
1062   else if (S_ISCHR(m))
1063     mode[0] = 'c';
1064   else if (S_ISDIR(m))
1065     mode[0] = 'd';
1066   else if (S_ISFIFO(m))
1067     mode[0] = 'p';
1068   else if (S_ISLNK(m))
1069     mode[0] = 'l';
1070   else if (S_ISSOCK(m))
1071     mode[0] = 's';
1072   else
1073     mode[0] = '?';
1074 
1075   if (m & S_IRUSR) mode[1] = 'r';
1076   if (m & S_IWUSR) mode[2] = 'w';
1077   if (m & S_IXUSR) mode[3] = 'x';
1078   if (m & S_IRGRP) mode[4] = 'r';
1079   if (m & S_IWGRP) mode[5] = 'w';
1080   if (m & S_IXGRP) mode[6] = 'x';
1081   if (m & S_IROTH) mode[7] = 'r';
1082   if (m & S_IWOTH) mode[8] = 'w';
1083   if (m & S_IXOTH) mode[9] = 'x';
1084 
1085   if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
1086   if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
1087   if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
1088 
1089   return mode;
1090 }
1091 
1092 int writefilestree(FILE *fp, git_tree *tree, const char *path)
1093 {
1094   const git_tree_entry *entry = NULL;
1095   git_object *obj = NULL;
1096   const char *entryname;
1097   char filepath[PATH_MAX], blobpath[PATH_MAX], entrypath[PATH_MAX], oid[8];
1098   size_t count, i, lc, filesize;
1099   int r, ret;
1100 
1101   count = git_tree_entrycount(tree);
1102   for (i = 0; i < count; i++) {
1103     if (!(entry = git_tree_entry_byindex(tree, i)) ||
1104         !(entryname = git_tree_entry_name(entry)))
1105       return -1;
1106     joinpath(entrypath, sizeof(entrypath), path, entryname);
1107 
1108     r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1109         entrypath);
1110     if (r < 0 || (size_t)r >= sizeof(filepath))
1111       errx(1, "path truncated: 'file/%s.html'", entrypath);
1112     r = snprintf(blobpath, sizeof(filepath), "blob/%s",
1113         entrypath);
1114     if (r < 0 || (size_t)r >= sizeof(filepath))
1115       errx(1, "path truncated: 'file/%s.html'", entrypath);
1116 
1117     if (!git_tree_entry_to_object(&obj, repo, entry)) {
1118       switch (git_object_type(obj)) {
1119         case GIT_OBJ_BLOB:
1120           break;
1121         case GIT_OBJ_TREE:
1122           /* NOTE: recurses */
1123           ret = writefilestree(fp, (git_tree *)obj,
1124               entrypath);
1125           git_object_free(obj);
1126           if (ret)
1127             return ret;
1128           continue;
1129         default:
1130           git_object_free(obj);
1131           continue;
1132       }
1133 
1134       filesize = git_blob_rawsize((git_blob *)obj);
1135       lc = writeblob(obj, filepath, blobpath, entryname, filesize);
1136       if (writeblobraw((git_blob *)obj, blobpath) < 0)
1137         return 1;
1138 
1139       fputs("<tr>\n<td><code>", fp);
1140       fputs(filemode(git_tree_entry_filemode(entry)), fp);
1141       fprintf(fp, "</code></td>\n<td><a href=\"%s", relpath);
1142       xmlencode(fp, filepath, strlen(filepath));
1143       fputs("\">", fp);
1144       xmlencode(fp, entrypath, strlen(entrypath));
1145       fputs("</a></td>\n<td align=\"right\">", fp);
1146       if (lc > 0)
1147         fprintf(fp, "%zuL", lc);
1148       else
1149         fprintf(fp, "%zuB", filesize);
1150       fputs("</td>\n</tr>\n", fp);
1151       git_object_free(obj);
1152     } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1153       /* commit object in tree is a submodule */
1154       fprintf(fp, 
1155           "<tr>\n"
1156           "<td><code>m---------</code></td>\n"
1157           "<td><a href=\"%sfile/.gitmodules.html\">",
1158           relpath);
1159       xmlencode(fp, entrypath, strlen(entrypath));
1160       fputs("</a> @ ", fp);
1161       git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1162       xmlencode(fp, oid, strlen(oid));
1163       fputs("</td>\n<td align=\"right\"></td>\n</tr>\n", fp);
1164     }
1165   }
1166 
1167   return 0;
1168 }
1169 
1170 int writefiles(FILE *fp, const git_oid *id)
1171 {
1172   git_tree *tree = NULL;
1173   git_commit *commit = NULL;
1174   int ret = -1;
1175 
1176   fputs("<div class=\"table-container\">\n"
1177         "<table>\n"
1178         "<colgroup>\n"
1179         "<col style=\"width: 80pt;\" />\n"
1180         "<col />\n<col />\n"
1181         "</colgroup>\n"
1182         "<thead>\n<tr>\n"
1183         "<td>Mode</td>\n<td>Name</td>\n"
1184         "<td align=\"right\">Size</td>\n"
1185         "</tr>\n</thead>\n"
1186         "<tbody>\n", fp);
1187 
1188   if (!git_commit_lookup(&commit, repo, id) &&
1189       !git_commit_tree(&tree, commit))
1190     ret = writefilestree(fp, tree, "");
1191 
1192   fputs("</tbody>\n</table>\n</div>\n", fp);
1193 
1194   git_commit_free(commit);
1195   git_tree_free(tree);
1196 
1197   return ret;
1198 }
1199 
1200 int writerefs(FILE *fp)
1201 {
1202   struct referenceinfo *ris = NULL;
1203   struct commitinfo *ci;
1204   size_t count, i, j, refcount;
1205   const char *titles[] = { "Branches", "Tags" };
1206   const char *ids[] = { "branches", "tags" };
1207   const char *s;
1208 
1209   if (getrefs(&ris, &refcount) == -1)
1210     return -1;
1211 
1212   for (i = 0, j = 0, count = 0; i < refcount; i++) {
1213     if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1214       if (count) fputs("</tbody>\n</table>\n</div>\n", fp);
1215       count = 0;
1216       j = 1;
1217     }
1218 
1219     /* print header if it has an entry (first). */
1220     if (++count == 1) {
1221       fprintf(fp,
1222           "<h2>%s</h2>\n"
1223           "<div class=\"table-container\">\n"
1224           "<table id=\"%s\">\n"
1225           "<thead>\n<tr>\n<td>Name</td>"
1226           "<td>Last commit date</td>"
1227           "<td>Author</td>\n</tr>\n"
1228           "</thead>\n<tbody>\n",
1229           titles[j], ids[j]);
1230     }
1231 
1232     ci = ris[i].ci;
1233     s = git_reference_shorthand(ris[i].ref);
1234 
1235     fputs("<tr>\n<td>", fp);
1236     xmlencode(fp, s, strlen(s));
1237     fputs("</td>\n<td>", fp);
1238     if (ci->author) {
1239       fputs("<time datetime=\"", fp);
1240       formattime(fp, "%Y-%m-%d %H:%M", &(ci->author->when));
1241       fputs("\">", fp);
1242       formattime(fp, "%d/%m/%Y %H:%M", &(ci->author->when));
1243       fputs("</time>\n", fp);
1244     }
1245     fputs("</td>\n<td>", fp);
1246     if (ci->author)
1247       xmlencode(fp, ci->author->name, strlen(ci->author->name));
1248     fputs("</td>\n</tr>\n", fp);
1249   }
1250   /* table footer */
1251   if (count)
1252     fputs("</tbody>\n</table>\n</div>\n", fp);
1253 
1254   for (i = 0; i < refcount; i++) {
1255     commitinfo_free(ris[i].ci);
1256     git_reference_free(ris[i].ref);
1257   }
1258   free(ris);
1259 
1260   return 0;
1261 }
1262 
1263 void usage(char *argv0)
1264 {
1265   fprintf(stderr, "%s [-c cachefile | -l commits] "
1266       "[-u baseurl] repodir\n", argv0);
1267   exit(1);
1268 }
1269 
1270 /* Writes a Markdown README to the file using CMark */
1271 void write_markdown_readme(FILE *fp, git_blob *readme)
1272 {
1273   const char *markdown = git_blob_rawcontent(readme);
1274   char *html = cmark_markdown_to_html(markdown, 
1275                                       strlen(markdown), CMARK_OPT_SAFE);
1276   writeheader(fp, NULL);
1277   fputs("</ul>\n</nav>\n", fp);
1278   fprintf(fp, "<section id=\"readme\">\n");
1279   fprintf(fp, "%s", html);
1280   fprintf(fp, "</section>\n");
1281   writefooter(fp);
1282 
1283   free(html);
1284 }
1285 
1286 int main(int argc, char *argv[])
1287 {
1288   git_object *obj = NULL;
1289   const git_oid *head = NULL;
1290   mode_t mask;
1291   FILE *fp, *fpread;
1292   char path[PATH_MAX], *p;
1293   char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1294   size_t n;
1295   int i, fd;
1296 
1297   for (i = 1; i < argc; i++) {
1298     if (argv[i][0] != '-') {
1299       if (repodir)
1300         usage(argv[0]);
1301       repodir = argv[i];
1302     } else if (argv[i][1] == 'c') {
1303       if (nlogcommits > 0 || i + 1 >= argc)
1304         usage(argv[0]);
1305       cachefile = argv[++i];
1306     } else if (argv[i][1] == 'l') {
1307       if (cachefile || i + 1 >= argc)
1308         usage(argv[0]);
1309       errno = 0;
1310       nlogcommits = strtoll(argv[++i], &p, 10);
1311       if (argv[i][0] == '\0' || *p != '\0' ||
1312           nlogcommits <= 0 || errno)
1313         usage(argv[0]);
1314     } else if (argv[i][1] == 'u') {
1315       if (i + 1 >= argc)
1316         usage(argv[0]);
1317       baseurl = argv[++i];
1318     }
1319   }
1320   if (!repodir)
1321     usage(argv[0]);
1322 
1323   git_libgit2_init();
1324 
1325 #ifdef __OpenBSD__
1326   if (unveil(repodir, "r") == -1)
1327     err(1, "unveil: %s", repodir);
1328   if (unveil(".", "rwc") == -1)
1329     err(1, "unveil: .");
1330   if (cachefile && unveil(cachefile, "rwc") == -1)
1331     err(1, "unveil: %s", cachefile);
1332 
1333   if (cachefile) {
1334     if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1335       err(1, "pledge");
1336   } else {
1337     if (pledge("stdio rpath wpath cpath", NULL) == -1)
1338       err(1, "pledge");
1339   }
1340 #endif
1341 
1342   if (git_repository_open_ext(&repo, repodir,
1343         GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1344     fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1345     return 1;
1346   }
1347 
1348   /* find HEAD */
1349   if (!git_revparse_single(&obj, repo, "HEAD"))
1350     head = git_object_id(obj);
1351   git_object_free(obj);
1352 
1353   /* use directory name as name */
1354   if ((name = strrchr(repodir, '/')))
1355     name++;
1356   else
1357     name = "";
1358 
1359   /* strip .git suffix */
1360   if (!(strippedname = strdup(name)))
1361     err(1, "strdup");
1362   if ((p = strrchr(strippedname, '.')))
1363     if (!strcmp(p, ".git"))
1364       *p = '\0';
1365 
1366   /* read description or .git/description */
1367   joinpath(path, sizeof(path), repodir, "description");
1368   if (!(fpread = fopen(path, "r"))) {
1369     joinpath(path, sizeof(path), repodir, ".git/description");
1370     fpread = fopen(path, "r");
1371   }
1372   if (fpread) {
1373     if (!fgets(description, sizeof(description), fpread))
1374       description[0] = '\0';
1375     fclose(fpread);
1376   }
1377 
1378   /* read url or .git/url */
1379   joinpath(path, sizeof(path), repodir, "url");
1380   if (!(fpread = fopen(path, "r"))) {
1381     joinpath(path, sizeof(path), repodir, ".git/url");
1382     fpread = fopen(path, "r");
1383   }
1384   if (fpread) {
1385     if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1386       cloneurl[0] = '\0';
1387     cloneurl[strcspn(cloneurl, "\n")] = '\0';
1388     fclose(fpread);
1389   }
1390 
1391   /* check LICENSE */
1392   for (i = 0; i < LEN(licensefiles) && !license; i++) {
1393     if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1394         git_object_type(obj) == GIT_OBJ_BLOB)
1395       license = licensefiles[i] + strlen("HEAD:");
1396     git_object_free(obj);
1397   }
1398 
1399   /* check README */
1400   for (i = 0; i < LEN(readmefiles) && !readme; i++) {
1401     if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1402         git_object_type(obj) == GIT_OBJ_BLOB)
1403     {
1404       readme = readmefiles[i] + strlen("HEAD:");
1405       char *ext = readme + strlen("README.");
1406 
1407       // If the README is a Markdown file, use CMark to render it as HTML
1408       if (!strcmp(ext, "md") && !git_blob_is_binary((git_blob *)obj))
1409       {
1410         readme = "README.html";
1411         fp = efopen(readme, "w");
1412 
1413         write_markdown_readme(fp, (git_blob*)obj);
1414         fclose(fp);
1415       }
1416       else
1417       {
1418         char tmp[510];
1419         sprintf(tmp, "%sfile/%s.html", relpath, readme);
1420         readme = tmp;
1421       }
1422     }
1423     git_object_free(obj);
1424   }
1425 
1426   if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1427       git_object_type(obj) == GIT_OBJ_BLOB)
1428     submodules = ".gitmodules";
1429   git_object_free(obj);
1430 
1431   /* Create the blob directory */
1432   mkdir("blob", S_IRWXU | S_IRWXG | S_IRWXO);
1433 
1434   /* log for HEAD */
1435   fp = efopen("log.html", "w");
1436   relpath = "";
1437   mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1438   writeheader(fp, "Log");
1439   fputs("</ul>\n</nav>\n", fp);
1440   fputs("<div class=\"article-list\">\n", fp);
1441 
1442   if (cachefile && head) {
1443     /* Read from cache file (does not need to exist) */
1444     if ((rcachefp = fopen(cachefile, "r"))) {
1445       if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1446         errx(1, "%s: no object id", cachefile);
1447       if (git_oid_fromstr(&lastoid, lastoidstr))
1448         errx(1, "%s: invalid object id", cachefile);
1449     }
1450 
1451     /* Write log to (temporary) cache */
1452     if ((fd = mkstemp(tmppath)) == -1)
1453       err(1, "mkstemp");
1454     if (!(wcachefp = fdopen(fd, "w")))
1455       err(1, "fdopen: '%s'", tmppath);
1456     /* Write last commit id (HEAD) */
1457     git_oid_tostr(buf, sizeof(buf), head);
1458     fprintf(wcachefp, "%s\n", buf);
1459 
1460     writelog(fp, head);
1461 
1462     if (rcachefp) {
1463       /* append previous log to log.html and the new cache */
1464       while (!feof(rcachefp)) {
1465         n = fread(buf, 1, sizeof(buf), rcachefp);
1466         if (ferror(rcachefp))
1467           err(1, "fread");
1468         if (fwrite(buf, 1, n, fp) != n ||
1469             fwrite(buf, 1, n, wcachefp) != n)
1470           err(1, "fwrite");
1471       }
1472       fclose(rcachefp);
1473     }
1474     fclose(wcachefp);
1475   } else {
1476     if (head)
1477       writelog(fp, head);
1478   }
1479 
1480   fputs("</tbody>\n</table>\n</div>\n", fp);
1481   writefooter(fp);
1482   fclose(fp);
1483 
1484   /* files for HEAD */
1485   fp = efopen("files.html", "w");
1486   writeheader(fp, "Files");
1487   fputs("</ul>\n</nav>\n", fp);
1488   if (head)
1489     writefiles(fp, head);
1490   writefooter(fp);
1491   fclose(fp);
1492 
1493   /* summary page with branches and tags */
1494   fp = efopen("refs.html", "w");
1495   writeheader(fp, "Refs");
1496   fputs("</ul>\n</nav>\n", fp);
1497   writerefs(fp);
1498   writefooter(fp);
1499   fclose(fp);
1500 
1501   /* Link the main page */
1502   if (access("index.html", F_OK) == 0) remove("index.html");
1503   if (readme) symlink(readme, "index.html");
1504   else symlink("log.html" , "index.html");
1505 
1506   /* Atom feed */
1507   fp = efopen("atom.xml", "w");
1508   writeatom(fp, 1);
1509   fclose(fp);
1510 
1511   /* Atom feed for tags / releases */
1512   fp = efopen("tags.xml", "w");
1513   writeatom(fp, 0);
1514   fclose(fp);
1515 
1516   /* rename new cache file on success */
1517   if (cachefile && head) {
1518     if (rename(tmppath, cachefile))
1519       err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1520     umask((mask = umask(0)));
1521     if (chmod(cachefile,
1522           (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1523       err(1, "chmod: '%s'", cachefile);
1524   }
1525 
1526   /* cleanup */
1527   git_repository_free(repo);
1528   git_libgit2_shutdown();
1529 
1530   return 0;
1531 }