repos/stagit

fork of stagit, a static git page generator
Commits Files Refs README LICENSE
stagit.c (50354B)
   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 <string.h>
  12 #include <time.h>
  13 #include <unistd.h>
  14 
  15 #include <git2.h>
  16 #include <md4c-html.h>
  17 
  18 #include "compat.h"
  19 
  20 #define LEN(s)    (sizeof(s)/sizeof(*s))
  21 
  22 /* #define DATE_SHORT_FMT    "%Y-%m-%d %H:%M" */
  23 #define DATE_SHORT_FMT    "%H:%M %d-%m-%Y"
  24 #define TABWIDTH    4
  25 
  26 struct deltainfo {
  27     git_patch *patch;
  28 
  29     size_t addcount;
  30     size_t delcount;
  31 };
  32 
  33 struct commitinfo {
  34     const git_oid *id;
  35 
  36     char oid[GIT_OID_HEXSZ + 1];
  37     char parentoid[GIT_OID_HEXSZ + 1];
  38 
  39     const git_signature *author;
  40     const git_signature *committer;
  41     const char          *summary;
  42     const char          *msg;
  43 
  44     git_diff   *diff;
  45     git_commit *commit;
  46     git_commit *parent;
  47     git_tree   *commit_tree;
  48     git_tree   *parent_tree;
  49 
  50     size_t addcount;
  51     size_t delcount;
  52     size_t filecount;
  53 
  54     struct deltainfo **deltas;
  55     size_t ndeltas;
  56 };
  57 
  58 /* reference and associated data for sorting */
  59 struct referenceinfo {
  60     struct git_reference *ref;
  61     struct commitinfo *ci;
  62 };
  63 
  64 static git_repository *repo;
  65 
  66 static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
  67 static const char *relpath = "";
  68 static const char *repodir;
  69 
  70 static char *name = "";
  71 static char *strippedname = "";
  72 static char description[255];
  73 static char cloneurl[1024];
  74 static char *submodules;
  75 static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" };
  76 static char *license;
  77 static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
  78 static char *readme;
  79 static long long nlogcommits = -1; /* -1 indicates not used */
  80 
  81 /* cache */
  82 static git_oid lastoid;
  83 static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
  84 static FILE *rcachefp, *wcachefp;
  85 static const char *cachefile;
  86 
  87 /* Handle read or write errors for a FILE * stream */
  88 void
  89 checkfileerror(FILE *fp, const char *name, int mode)
  90 {
  91     if (mode == 'r' && ferror(fp))
  92         errx(1, "read error: %s", name);
  93     else if (mode == 'w' && (fflush(fp) || ferror(fp)))
  94         errx(1, "write error: %s", name);
  95 }
  96 
  97 void
  98 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
  99 {
 100     int r;
 101 
 102     r = snprintf(buf, bufsiz, "%s%s%s",
 103         path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 104     if (r < 0 || (size_t)r >= bufsiz)
 105         errx(1, "path truncated: '%s%s%s'",
 106             path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 107 }
 108 
 109 void
 110 deltainfo_free(struct deltainfo *di)
 111 {
 112     if (!di)
 113         return;
 114     git_patch_free(di->patch);
 115     memset(di, 0, sizeof(*di));
 116     free(di);
 117 }
 118 
 119 int
 120 commitinfo_getstats(struct commitinfo *ci)
 121 {
 122     struct deltainfo *di;
 123     git_diff_options opts;
 124     git_diff_find_options fopts;
 125     const git_diff_delta *delta;
 126     const git_diff_hunk *hunk;
 127     const git_diff_line *line;
 128     git_patch *patch = NULL;
 129     size_t ndeltas, nhunks, nhunklines;
 130     size_t i, j, k;
 131 
 132     if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
 133         goto err;
 134     if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
 135         if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
 136             ci->parent = NULL;
 137             ci->parent_tree = NULL;
 138         }
 139     }
 140 
 141     git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
 142     opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
 143                   GIT_DIFF_IGNORE_SUBMODULES |
 144               GIT_DIFF_INCLUDE_TYPECHANGE;
 145     if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
 146         goto err;
 147 
 148     if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
 149         goto err;
 150     /* find renames and copies, exact matches (no heuristic) for renames. */
 151     fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
 152                    GIT_DIFF_FIND_EXACT_MATCH_ONLY;
 153     if (git_diff_find_similar(ci->diff, &fopts))
 154         goto err;
 155 
 156     ndeltas = git_diff_num_deltas(ci->diff);
 157     if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
 158         err(1, "calloc");
 159 
 160     for (i = 0; i < ndeltas; i++) {
 161         if (git_patch_from_diff(&patch, ci->diff, i))
 162             goto err;
 163 
 164         if (!(di = calloc(1, sizeof(struct deltainfo))))
 165             err(1, "calloc");
 166         di->patch = patch;
 167         ci->deltas[i] = di;
 168 
 169         delta = git_patch_get_delta(patch);
 170 
 171         /* skip stats for binary data */
 172         if (delta->flags & GIT_DIFF_FLAG_BINARY)
 173             continue;
 174 
 175         nhunks = git_patch_num_hunks(patch);
 176         for (j = 0; j < nhunks; j++) {
 177             if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 178                 break;
 179             for (k = 0; ; k++) {
 180                 if (git_patch_get_line_in_hunk(&line, patch, j, k))
 181                     break;
 182                 if (line->old_lineno == -1) {
 183                     di->addcount++;
 184                     ci->addcount++;
 185                 } else if (line->new_lineno == -1) {
 186                     di->delcount++;
 187                     ci->delcount++;
 188                 }
 189             }
 190         }
 191     }
 192     ci->ndeltas = i;
 193     ci->filecount = i;
 194 
 195     return 0;
 196 
 197 err:
 198     git_diff_free(ci->diff);
 199     ci->diff = NULL;
 200     git_tree_free(ci->commit_tree);
 201     ci->commit_tree = NULL;
 202     git_tree_free(ci->parent_tree);
 203     ci->parent_tree = NULL;
 204     git_commit_free(ci->parent);
 205     ci->parent = NULL;
 206 
 207     if (ci->deltas)
 208         for (i = 0; i < ci->ndeltas; i++)
 209             deltainfo_free(ci->deltas[i]);
 210     free(ci->deltas);
 211     ci->deltas = NULL;
 212     ci->ndeltas = 0;
 213     ci->addcount = 0;
 214     ci->delcount = 0;
 215     ci->filecount = 0;
 216 
 217     return -1;
 218 }
 219 
 220 void
 221 commitinfo_free(struct commitinfo *ci)
 222 {
 223     size_t i;
 224 
 225     if (!ci)
 226         return;
 227     if (ci->deltas)
 228         for (i = 0; i < ci->ndeltas; i++)
 229             deltainfo_free(ci->deltas[i]);
 230 
 231     free(ci->deltas);
 232     git_diff_free(ci->diff);
 233     git_tree_free(ci->commit_tree);
 234     git_tree_free(ci->parent_tree);
 235     git_commit_free(ci->commit);
 236     git_commit_free(ci->parent);
 237     memset(ci, 0, sizeof(*ci));
 238     free(ci);
 239 }
 240 
 241 struct commitinfo *
 242 commitinfo_getbyoid(const git_oid *id)
 243 {
 244     struct commitinfo *ci;
 245 
 246     if (!(ci = calloc(1, sizeof(struct commitinfo))))
 247         err(1, "calloc");
 248 
 249     if (git_commit_lookup(&(ci->commit), repo, id))
 250         goto err;
 251     ci->id = id;
 252 
 253     git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
 254     git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
 255 
 256     ci->author = git_commit_author(ci->commit);
 257     ci->committer = git_commit_committer(ci->commit);
 258     ci->summary = git_commit_summary(ci->commit);
 259     ci->msg = git_commit_message(ci->commit);
 260 
 261     return ci;
 262 
 263 err:
 264     commitinfo_free(ci);
 265 
 266     return NULL;
 267 }
 268 
 269 int
 270 refs_cmp(const void *v1, const void *v2)
 271 {
 272     const struct referenceinfo *r1 = v1, *r2 = v2;
 273     time_t t1, t2;
 274     int r;
 275 
 276     if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
 277         return r;
 278 
 279     t1 = r1->ci->author ? r1->ci->author->when.time : 0;
 280     t2 = r2->ci->author ? r2->ci->author->when.time : 0;
 281     if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
 282         return r;
 283 
 284     return strcmp(git_reference_shorthand(r1->ref),
 285                   git_reference_shorthand(r2->ref));
 286 }
 287 
 288 int
 289 getrefs(struct referenceinfo **pris, size_t *prefcount)
 290 {
 291     struct referenceinfo *ris = NULL;
 292     struct commitinfo *ci = NULL;
 293     git_reference_iterator *it = NULL;
 294     const git_oid *id = NULL;
 295     git_object *obj = NULL;
 296     git_reference *dref = NULL, *r, *ref = NULL;
 297     size_t i, refcount;
 298 
 299     *pris = NULL;
 300     *prefcount = 0;
 301 
 302     if (git_reference_iterator_new(&it, repo))
 303         return -1;
 304 
 305     for (refcount = 0; !git_reference_next(&ref, it); ) {
 306         if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
 307             git_reference_free(ref);
 308             ref = NULL;
 309             continue;
 310         }
 311 
 312         switch (git_reference_type(ref)) {
 313         case GIT_REF_SYMBOLIC:
 314             if (git_reference_resolve(&dref, ref))
 315                 goto err;
 316             r = dref;
 317             break;
 318         case GIT_REF_OID:
 319             r = ref;
 320             break;
 321         default:
 322             continue;
 323         }
 324         if (!git_reference_target(r) ||
 325             git_reference_peel(&obj, r, GIT_OBJ_ANY))
 326             goto err;
 327         if (!(id = git_object_id(obj)))
 328             goto err;
 329         if (!(ci = commitinfo_getbyoid(id)))
 330             break;
 331 
 332         if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
 333             err(1, "realloc");
 334         ris[refcount].ci = ci;
 335         ris[refcount].ref = r;
 336         refcount++;
 337 
 338         git_object_free(obj);
 339         obj = NULL;
 340         git_reference_free(dref);
 341         dref = NULL;
 342     }
 343     git_reference_iterator_free(it);
 344 
 345     /* sort by type, date then shorthand name */
 346     qsort(ris, refcount, sizeof(*ris), refs_cmp);
 347 
 348     *pris = ris;
 349     *prefcount = refcount;
 350 
 351     return 0;
 352 
 353 err:
 354     git_object_free(obj);
 355     git_reference_free(dref);
 356     commitinfo_free(ci);
 357     for (i = 0; i < refcount; i++) {
 358         commitinfo_free(ris[i].ci);
 359         git_reference_free(ris[i].ref);
 360     }
 361     free(ris);
 362 
 363     return -1;
 364 }
 365 
 366 FILE *
 367 efopen(const char *filename, const char *flags)
 368 {
 369     FILE *fp;
 370 
 371     if (!(fp = fopen(filename, flags)))
 372         err(1, "fopen: '%s'", filename);
 373 
 374     return fp;
 375 }
 376 
 377 /* Percent-encode, see RFC3986 section 2.1. */
 378 void
 379 percentencode(FILE *fp, const char *s, size_t len)
 380 {
 381     static char tab[] = "0123456789ABCDEF";
 382     unsigned char uc;
 383     size_t i;
 384 
 385     for (i = 0; *s && i < len; s++, i++) {
 386         uc = *s;
 387         /* NOTE: do not encode '/' for paths or ",-." */
 388         if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
 389             uc == '[' || uc == ']') {
 390             putc('%', fp);
 391             putc(tab[(uc >> 4) & 0x0f], fp);
 392             putc(tab[uc & 0x0f], fp);
 393         } else {
 394             putc(uc, fp);
 395         }
 396     }
 397 }
 398 
 399 /* Escape characters below as HTML 2.0 / XML 1.0. */
 400 void
 401 xmlencode(FILE *fp, const char *s, size_t len)
 402 {
 403     size_t i;
 404 
 405     for (i = 0; *s && i < len; s++, i++) {
 406         switch(*s) {
 407         case '<':  fputs("&lt;",   fp); break;
 408         case '>':  fputs("&gt;",   fp); break;
 409         case '\'': fputs("&#39;",  fp); break;
 410         case '&':  fputs("&amp;",  fp); break;
 411         case '"':  fputs("&quot;", fp); break;
 412         default:   putc(*s, fp);
 413         }
 414     }
 415 }
 416 
 417 /* Escape characters below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */
 418 void
 419 xmlencodeline(FILE *fp, const char *s, size_t len)
 420 {
 421     size_t i;
 422 
 423     for (i = 0; *s && i < len; s++, i++) {
 424         switch(*s) {
 425         case '<':  fputs("&lt;",   fp); break;
 426         case '>':  fputs("&gt;",   fp); break;
 427         case '\'': fputs("&#39;",  fp); break;
 428         case '&':  fputs("&amp;",  fp); break;
 429         case '"':  fputs("&quot;", fp); break;
 430         case '\t': fprintf(fp, "%*c", TABWIDTH, ' '); break;
 431         case '\r': break; /* ignore CR */
 432         case '\n': break; /* ignore LF */
 433         default:   putc(*s, fp);
 434         }
 435     }
 436 }
 437 
 438 int
 439 mkdirp(const char *path)
 440 {
 441     char tmp[PATH_MAX], *p;
 442 
 443     if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
 444         errx(1, "path truncated: '%s'", path);
 445     for (p = tmp + (tmp[0] == '/'); *p; p++) {
 446         if (*p != '/')
 447             continue;
 448         *p = '\0';
 449         if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 450             return -1;
 451         *p = '/';
 452     }
 453     if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 454         return -1;
 455     return 0;
 456 }
 457 
 458 int
 459 mkdirfile(const char *path)
 460 {
 461     char *d;
 462     char tmp[PATH_MAX];
 463     if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
 464         errx(1, "path truncated: '%s'", path);
 465     if (!(d = dirname(tmp)))
 466         err(1, "dirname");
 467     if (mkdirp(d))
 468         return -1;
 469     return 0;
 470 }
 471 
 472 void
 473 printtimez(FILE *fp, const git_time *intime)
 474 {
 475     struct tm *intm;
 476     time_t t;
 477     char out[32];
 478 
 479     t = (time_t)intime->time;
 480     if (!(intm = gmtime(&t)))
 481         return;
 482     strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
 483     fputs(out, fp);
 484 }
 485 
 486 void
 487 printtime(FILE *fp, const git_time *intime)
 488 {
 489     struct tm *intm;
 490     time_t t;
 491     char out[32];
 492 
 493     t = (time_t)intime->time + (intime->offset * 60);
 494     if (!(intm = gmtime(&t)))
 495         return;
 496     strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
 497     if (intime->offset < 0)
 498         fprintf(fp, "%s -%02d%02d", out,
 499                     -(intime->offset) / 60, -(intime->offset) % 60);
 500     else
 501         fprintf(fp, "%s +%02d%02d", out,
 502                     intime->offset / 60, intime->offset % 60);
 503 }
 504 
 505 void
 506 printtimeshort(FILE *fp, const git_time *intime)
 507 {
 508     struct tm *intm;
 509     time_t t;
 510     char out[32];
 511 
 512     tzset();
 513     t = (time_t)intime->time;
 514     if (!(intm = localtime(&t)))
 515         return;
 516     strftime(out, sizeof(out), DATE_SHORT_FMT, intm);
 517     fputs(out, fp);
 518 }
 519 
 520 void
 521 writeheader(FILE *fp, const char *title)
 522 {
 523     fputs("<!DOCTYPE html>\n"
 524         "<html>\n<head>\n"
 525         "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
 526         "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
 527         "<title>", fp);
 528     xmlencode(fp, title, strlen(title));
 529     if (title[0] && strippedname[0])
 530         fputs(" - ", fp);
 531     xmlencode(fp, strippedname, strlen(strippedname));
 532     if (description[0])
 533         fputs(" - ", fp);
 534     xmlencode(fp, description, strlen(description));
 535     fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
 536     fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
 537     xmlencode(fp, name, strlen(name));
 538     fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath);
 539     fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
 540     xmlencode(fp, name, strlen(name));
 541     fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
 542     fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
 543     fputs("</head>\n<body>\n", fp);
 544     fputs("<div id=\"webpage-header-link\">\n", fp);
 545     fputs("<a href=\"https://kloeckner.com.ar/\">https://kloeckner.com.ar\n", fp);
 546     fputs("</a></div>\n<body>\n", fp);
 547     fputs("<table id=\"repo-header-table\"><tr><td id=\"repo-logo\">", fp);
 548     fprintf(fp, "<a href=\"https://git.kloeckner.com.ar\"><img src=\"%slogo.png\" alt=\"\" width=\"64\" height=\"64\" /></a>",
 549             relpath, relpath);
 550     fputs("</td><td id=\"repo-header\"><h1 id=\"repo-name\"><a href=\"/\">repos/</a>", fp);
 551     xmlencode(fp, strippedname, strlen(strippedname));
 552     fputs("</h1><span id=\"repo-desc\">", fp);
 553     xmlencode(fp, description, strlen(description));
 554     fputs("</span>", fp);
 555 
 556     fputs("</td></tr></table>", fp);
 557 
 558     fputs("<div id=\"repo-top-buttons\">\n", fp);
 559     // fprintf(fp, "<a <a href=\"https://git.kloeckner.com.ar\">Index</a>  ", relpath);
 560     fprintf(fp, "<a href=\"%slog.html\">Commits</a>  ", relpath);
 561     fprintf(fp, "<a href=\"%sfiles.html\">Files</a>  ", relpath);
 562     fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath);
 563     if (submodules)
 564         fprintf(fp, "  <a href=\"%sfile/%s.html\">Submodules</a>",
 565                 relpath, submodules);
 566     if (readme)
 567         fprintf(fp, "  <a href=\"%sreadme.html\">README</a>", relpath);
 568     if (license)
 569         fprintf(fp, "  <a href=\"%sfile/%s.html\">LICENSE</a>",
 570                 relpath, license);
 571 
 572     if (cloneurl[0]) {
 573         fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
 574         xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
 575         fputs("\">", fp);
 576         xmlencode(fp, cloneurl, strlen(cloneurl));
 577         fputs("</a></td></tr>", fp);
 578     }
 579 
 580     /* fputs("</div>\n<hr/>\n<div id=\"content\">\n", fp); */
 581     fputs("</div>\n", fp);
 582 }
 583 
 584 void
 585 writefooter(FILE *fp)
 586 {
 587     /* fputs("</div>\n</div>\n</body>\n</html>\n", fp); */
 588     fputs("</div>\n</div>\n</body>\n", fp);
 589     fputs("<footer>Generated with <a href=\"https://git.kloeckner.com.ar/stagit/\">Stagit</a></footer>\n", fp);
 590     fputs("</html>\n", fp);
 591 }
 592 
 593 size_t
 594 writeblobhtml(FILE *fp, const git_blob *blob)
 595 {
 596     size_t n = 0, i, len, prev;
 597     const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%4zu</a> ";
 598     const char *s = git_blob_rawcontent(blob);
 599 
 600     len = git_blob_rawsize(blob);
 601     fputs("<pre id=\"blob\">\n", fp);
 602 
 603     if (len > 0) {
 604         for (i = 0, prev = 0; i < len; i++) {
 605             if (s[i] != '\n')
 606                 continue;
 607             n++;
 608             fprintf(fp, nfmt, n, n, n);
 609             xmlencodeline(fp, &s[prev], i - prev + 1);
 610             putc('\n', fp);
 611             prev = i + 1;
 612         }
 613         /* trailing data */
 614         if ((len - prev) > 0) {
 615             n++;
 616             fprintf(fp, nfmt, n, n, n);
 617             xmlencodeline(fp, &s[prev], len - prev);
 618         }
 619     }
 620 
 621     fputs("</pre>\n", fp);
 622 
 623     return n;
 624 }
 625 
 626 void
 627 printcommit(FILE *fp, struct commitinfo *ci)
 628 {
 629     fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
 630         relpath, ci->oid, ci->oid);
 631 
 632     if (ci->parentoid[0])
 633         fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
 634             relpath, ci->parentoid, ci->parentoid);
 635 
 636     if (ci->author) {
 637         fputs("<b>Author:</b> ", fp);
 638         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 639         fputs(" &lt;<a href=\"mailto:", fp);
 640         xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
 641         fputs("\">", fp);
 642         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 643         fputs("</a>&gt;\n<b>Date:</b>   ", fp);
 644         printtime(fp, &(ci->author->when));
 645         putc('\n', fp);
 646     }
 647     if (ci->msg) {
 648         putc('\n', fp);
 649         xmlencode(fp, ci->msg, strlen(ci->msg));
 650         putc('\n', fp);
 651     }
 652 }
 653 
 654 void
 655 printshowfile(FILE *fp, struct commitinfo *ci)
 656 {
 657     const git_diff_delta *delta;
 658     const git_diff_hunk *hunk;
 659     const git_diff_line *line;
 660     git_patch *patch;
 661     size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
 662     char linestr[80];
 663     int c;
 664 
 665     printcommit(fp, ci);
 666 
 667     if (!ci->deltas)
 668         return;
 669 
 670     if (ci->filecount > 1000   ||
 671         ci->ndeltas   > 1000   ||
 672         ci->addcount  > 100000 ||
 673         ci->delcount  > 100000) {
 674         fputs("Diff is too large, output suppressed.\n", fp);
 675         return;
 676     }
 677 
 678     /* diff stat */
 679     fputs("<b>Diffstat:</b>\n<table>", fp);
 680     for (i = 0; i < ci->ndeltas; i++) {
 681         delta = git_patch_get_delta(ci->deltas[i]->patch);
 682 
 683         switch (delta->status) {
 684         case GIT_DELTA_ADDED:      c = 'A'; break;
 685         case GIT_DELTA_COPIED:     c = 'C'; break;
 686         case GIT_DELTA_DELETED:    c = 'D'; break;
 687         case GIT_DELTA_MODIFIED:   c = 'M'; break;
 688         case GIT_DELTA_RENAMED:    c = 'R'; break;
 689         case GIT_DELTA_TYPECHANGE: c = 'T'; break;
 690         default:                   c = ' '; break;
 691         }
 692         if (c == ' ')
 693             fprintf(fp, "<tr><td>%c", c);
 694         else
 695             fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
 696 
 697         fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
 698         xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 699         if (strcmp(delta->old_file.path, delta->new_file.path)) {
 700             fputs(" -&gt; ", fp);
 701             xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 702         }
 703 
 704         add = ci->deltas[i]->addcount;
 705         del = ci->deltas[i]->delcount;
 706         changed = add + del;
 707         total = sizeof(linestr) - 2;
 708         if (changed > total) {
 709             if (add)
 710                 add = ((float)total / changed * add) + 1;
 711             if (del)
 712                 del = ((float)total / changed * del) + 1;
 713         }
 714         memset(&linestr, '+', add);
 715         memset(&linestr[add], '-', del);
 716 
 717         fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
 718                 ci->deltas[i]->addcount + ci->deltas[i]->delcount);
 719         fwrite(&linestr, 1, add, fp);
 720         fputs("</span><span class=\"d\">", fp);
 721         fwrite(&linestr[add], 1, del, fp);
 722         fputs("</span></td></tr>\n", fp);
 723     }
 724     fprintf(fp, "</table></pre><pre id=\"commit-diff\">%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
 725         ci->filecount, ci->filecount == 1 ? "" : "s",
 726             ci->addcount,  ci->addcount  == 1 ? "" : "s",
 727             ci->delcount,  ci->delcount  == 1 ? "" : "s");
 728 
 729     for (i = 0; i < ci->ndeltas; i++) {
 730         patch = ci->deltas[i]->patch;
 731         delta = git_patch_get_delta(patch);
 732         fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
 733         percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 734         fputs(".html\">", fp);
 735         xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 736         fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
 737         percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 738         fprintf(fp, ".html\">");
 739         xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 740         fprintf(fp, "</a></b>\n");
 741 
 742         /* check binary data */
 743         if (delta->flags & GIT_DIFF_FLAG_BINARY) {
 744             fputs("Binary files differ.\n", fp);
 745             continue;
 746         }
 747 
 748         nhunks = git_patch_num_hunks(patch);
 749         for (j = 0; j < nhunks; j++) {
 750             if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 751                 break;
 752 
 753             fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
 754             xmlencode(fp, hunk->header, hunk->header_len);
 755             fputs("</a>", fp);
 756 
 757             for (k = 0; ; k++) {
 758                 if (git_patch_get_line_in_hunk(&line, patch, j, k))
 759                     break;
 760                 if (line->old_lineno == -1)
 761                     fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
 762                         i, j, k, i, j, k);
 763                 else if (line->new_lineno == -1)
 764                     fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
 765                         i, j, k, i, j, k);
 766                 else
 767                     putc(' ', fp);
 768                 xmlencodeline(fp, line->content, line->content_len);
 769                 putc('\n', fp);
 770                 if (line->old_lineno == -1 || line->new_lineno == -1)
 771                     fputs("</a>", fp);
 772             }
 773         }
 774     }
 775 }
 776 
 777 void
 778 writelogline(FILE *fp, struct commitinfo *ci)
 779 {
 780     /* make entire table row clickable */
 781     fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'commit/%s.html'\">",
 782             ci->oid);
 783 
 784     fputs("<td id=\"log-date\">", fp);
 785     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 786     if (ci->author)
 787         printtimeshort(fp, &(ci->author->when));
 788     fputs("</a></td>",fp);
 789 
 790     fputs("<td id=\"log-summary\">", fp);
 791     if (ci->summary) {
 792         fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 793         xmlencode(fp, ci->summary, strlen(ci->summary));
 794         fputs("</a>",fp);
 795     }
 796     fputs("</td>", fp);
 797 
 798     fputs("<td id=\"log-author\">", fp);
 799     if (ci->author) {
 800         fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 801         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 802         fputs("</a>",fp);
 803     }
 804     fputs("</td>", fp);
 805 
 806     fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp);
 807     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 808     fprintf(fp, "%zu", ci->filecount);
 809     fputs("</a></td>",fp);
 810 
 811     fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp);
 812     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 813     fprintf(fp, "+%zu", ci->addcount);
 814     fputs("</a></td>",fp);
 815 
 816     fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp);
 817     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 818     fprintf(fp, "-%zu", ci->delcount);
 819     fputs("</a></td></tr>\n", fp);
 820 }
 821 
 822 int
 823 writelog(FILE *fp, const git_oid *oid)
 824 {
 825     struct commitinfo *ci;
 826     git_revwalk *w = NULL;
 827     git_oid id;
 828     char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
 829     FILE *fpfile;
 830     size_t remcommits = 0;
 831     int r;
 832 
 833     git_revwalk_new(&w, repo);
 834     git_revwalk_push(w, oid);
 835 
 836     while (!git_revwalk_next(&id, w)) {
 837         relpath = "";
 838 
 839         if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
 840             break;
 841 
 842         git_oid_tostr(oidstr, sizeof(oidstr), &id);
 843         r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
 844         if (r < 0 || (size_t)r >= sizeof(path))
 845             errx(1, "path truncated: 'commit/%s.html'", oidstr);
 846         r = access(path, F_OK);
 847 
 848         /* optimization: if there are no log lines to write and
 849            the commit file already exists: skip the diffstat */
 850         if (!nlogcommits) {
 851             remcommits++;
 852             if (!r)
 853                 continue;
 854         }
 855 
 856         if (!(ci = commitinfo_getbyoid(&id)))
 857             break;
 858         /* diffstat: for stagit HTML required for the log.html line */
 859         if (commitinfo_getstats(ci) == -1)
 860             goto err;
 861 
 862         if (nlogcommits != 0) {
 863             writelogline(fp, ci);
 864             if (nlogcommits > 0)
 865                 nlogcommits--;
 866         }
 867 
 868         if (cachefile)
 869             writelogline(wcachefp, ci);
 870 
 871         /* check if file exists if so skip it */
 872         if (r) {
 873             relpath = "../";
 874             fpfile = efopen(path, "w");
 875             writeheader(fpfile, ci->summary);
 876             fputs("<div id=\"content\">\n", fpfile);
 877             fputs("<pre id=\"commit-summary\">", fpfile);
 878             printshowfile(fpfile, ci);
 879             printf("%s\n", ci->oid);
 880             fputs("</pre>\n", fpfile);
 881             writefooter(fpfile);
 882             checkfileerror(fpfile, path, 'w');
 883             fclose(fpfile);
 884         }
 885 err:
 886         commitinfo_free(ci);
 887     }
 888     git_revwalk_free(w);
 889 
 890     if (nlogcommits == 0 && remcommits != 0) {
 891         fprintf(fp, "<tr><td></td><td colspan=\"5\">"
 892                 "%zu more commits remaining, fetch the repository"
 893                 "</td></tr>\n", remcommits);
 894     }
 895 
 896     relpath = "";
 897 
 898     return 0;
 899 }
 900 
 901 void
 902 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
 903 {
 904     fputs("<entry>\n", fp);
 905 
 906     fprintf(fp, "<id>%s</id>\n", ci->oid);
 907     if (ci->author) {
 908         fputs("<published>", fp);
 909         printtimez(fp, &(ci->author->when));
 910         fputs("</published>\n", fp);
 911     }
 912     if (ci->committer) {
 913         fputs("<updated>", fp);
 914         printtimez(fp, &(ci->committer->when));
 915         fputs("</updated>\n", fp);
 916     }
 917     if (ci->summary) {
 918         fputs("<title type=\"text\">", fp);
 919         if (tag && tag[0]) {
 920             fputs("[", fp);
 921             xmlencode(fp, tag, strlen(tag));
 922             fputs("] ", fp);
 923         }
 924         xmlencode(fp, ci->summary, strlen(ci->summary));
 925         fputs("</title>\n", fp);
 926     }
 927     fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
 928             baseurl, ci->oid);
 929 
 930     if (ci->author) {
 931         fputs("<author>\n<name>", fp);
 932         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 933         fputs("</name>\n<email>", fp);
 934         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 935         fputs("</email>\n</author>\n", fp);
 936     }
 937 
 938     fputs("<content type=\"text\">", fp);
 939     fprintf(fp, "commit %s\n", ci->oid);
 940     if (ci->parentoid[0])
 941         fprintf(fp, "parent %s\n", ci->parentoid);
 942     if (ci->author) {
 943         fputs("Author: ", fp);
 944         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 945         fputs(" &lt;", fp);
 946         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 947         fputs("&gt;\nDate:   ", fp);
 948         printtime(fp, &(ci->author->when));
 949         putc('\n', fp);
 950     }
 951     if (ci->msg) {
 952         putc('\n', fp);
 953         xmlencode(fp, ci->msg, strlen(ci->msg));
 954     }
 955     fputs("\n</content>\n</entry>\n", fp);
 956 }
 957 
 958 int
 959 writeatom(FILE *fp, int all)
 960 {
 961     struct referenceinfo *ris = NULL;
 962     size_t refcount = 0;
 963     struct commitinfo *ci;
 964     git_revwalk *w = NULL;
 965     git_oid id;
 966     size_t i, m = 100; /* last 'm' commits */
 967 
 968     fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
 969           "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
 970     xmlencode(fp, strippedname, strlen(strippedname));
 971     fputs(", branch HEAD</title>\n<subtitle>", fp);
 972     xmlencode(fp, description, strlen(description));
 973     fputs("</subtitle>\n", fp);
 974 
 975     /* all commits or only tags? */
 976     if (all) {
 977         git_revwalk_new(&w, repo);
 978         git_revwalk_push_head(w);
 979         for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
 980             if (!(ci = commitinfo_getbyoid(&id)))
 981                 break;
 982             printcommitatom(fp, ci, "");
 983             commitinfo_free(ci);
 984         }
 985         git_revwalk_free(w);
 986     } else if (getrefs(&ris, &refcount) != -1) {
 987         /* references: tags */
 988         for (i = 0; i < refcount; i++) {
 989             if (git_reference_is_tag(ris[i].ref))
 990                 printcommitatom(fp, ris[i].ci,
 991                                 git_reference_shorthand(ris[i].ref));
 992 
 993             commitinfo_free(ris[i].ci);
 994             git_reference_free(ris[i].ref);
 995         }
 996         free(ris);
 997     }
 998 
 999     fputs("</feed>\n", fp);
1000 
1001     return 0;
1002 }
1003 
1004 void
1005 writeblobraw(const git_blob *blob, const char *fpath, const char *filename, git_off_t filesize)
1006 {
1007     char tmp[PATH_MAX] = "";
1008     const char *p;
1009     size_t lc = 0;
1010     FILE *fp;
1011 
1012     mkdirfile(fpath);
1013 
1014     if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1015         errx(1, "path truncated: '%s'", fpath);
1016 
1017     for (p = fpath, tmp[0] = '\0'; *p; p++) {
1018         if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1019             errx(1, "path truncated: '../%s'", tmp);
1020     }
1021 
1022     fp = efopen(fpath, "w");
1023     fwrite(git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob), 1, fp);
1024     fclose(fp);
1025 }
1026 
1027 void
1028 process_output_md(const char* text, unsigned int size, void* fp)
1029 {
1030     fprintf((FILE *)fp, "%.*s", size, text);
1031 }
1032 
1033 int
1034 ismarkdownfile(const char *name)
1035 {
1036     const char *exts[] = { ".md", ".markdown" };
1037     const char *dot;
1038     size_t i, len;
1039 
1040     if (!name)
1041         return 0;
1042     dot = strrchr(name, '.');
1043     if (!dot)
1044         return 0;
1045     for (i = 0; i < LEN(exts); i++) {
1046         len = strlen(exts[i]);
1047         if (!strncasecmp(dot, exts[i], len))
1048             return 1;
1049     }
1050     return 0;
1051 }
1052 
1053 void
1054 writeblobmd(FILE *fp, const git_blob *blob, const char *rawprefix)
1055 {
1056     const char *s = git_blob_rawcontent(blob);
1057     git_off_t len = git_blob_rawsize(blob);
1058 
1059     fputs("<div id=\"md-content\">", fp);
1060     if (md_html(s, len, process_output_md, fp,
1061             MD_FLAG_TABLES |
1062             MD_FLAG_TASKLISTS |
1063             MD_FLAG_PERMISSIVEEMAILAUTOLINKS |
1064             MD_FLAG_PERMISSIVEURLAUTOLINKS, 0))
1065         err(1, "error parsing markdown");
1066     fputs("</div>\n", fp);
1067     fputs("<script>"
1068             "document.querySelectorAll('#md-content img').forEach(img => {"
1069             "const src = img.getAttribute('src');"
1070             "if (src && !src.startsWith('/') && !src.startsWith('http'))"
1071             "img.src = '", fp);
1072     fputs(rawprefix, fp);
1073     fputs("raw/' + src;"
1074             "});"
1075             "</script>\n", fp);
1076 }
1077 
1078 size_t
1079 writeblob(git_object *obj, const char *fpath, const char *rpath, const char *filename, const char *path, size_t filesize)
1080 {
1081     char tmp[PATH_MAX] = "", *file_parent;
1082     const char *p, *oldrelpath;
1083     int lc = 0;
1084     FILE *fp;
1085 
1086     mkdirfile(fpath);
1087 
1088     if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1089         errx(1, "path truncated: '%s'", fpath);
1090 
1091     file_parent = strrchr(tmp, '/');
1092     for (p = fpath, tmp[0] = '\0'; *p; p++) {
1093         if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1094             errx(1, "path truncated: '../%s'", tmp);
1095     }
1096 
1097     oldrelpath = relpath;
1098     relpath = tmp;
1099 
1100     if (file_parent == NULL)
1101         file_parent = "files";
1102     else {
1103         *file_parent = '\0';
1104         file_parent = strrchr(tmp, '/');
1105         if (file_parent == NULL)
1106             file_parent = tmp;
1107         else
1108             ++file_parent;
1109     }
1110 
1111     fp = efopen(fpath, "w");
1112     writeheader(fp, filename);
1113     /* fputs("<hr>\n", fp); */
1114     fputs("<div id=\"content\">\n", fp);
1115     fputs("<script>"
1116               "function toggleLineNumbers() {"
1117               "  var lines = document.querySelectorAll('.line');"
1118               "  var lineCheckbox = document.getElementById('line-checkbox');"
1119 
1120               "  lines.forEach(function(element) {"
1121               "    if (lineCheckbox.checked) {"
1122               "      element.style.display = 'inline';"
1123               "    } else {"
1124               "      element.style.display = 'none';"
1125               "    }"
1126               "  });"
1127           " localStorage.setItem('line-checkbox', lineCheckbox.checked);"
1128               "}"
1129           "function setCheckboxState() {"
1130           "  var lineCheckbox = document.getElementById('line-checkbox');"
1131           "  var savedState = localStorage.getItem('line-checkbox');"
1132           "    if (savedState !== null) {"
1133           "      lineCheckbox.checked = savedState === 'true';"
1134           "      toggleLineNumbers();"
1135           "    }"
1136           "}"
1137           "document.addEventListener('DOMContentLoaded', setCheckboxState);"
1138               "</script>", fp);
1139 
1140     fputs("<div id=\"open-file-header\"><div id=\"open-file-name\"><a id=\"file-parent-path\" href=\"", fp);
1141     // xmlencode(fp, relpath, strlen(relpath));
1142     // printf("relpath is '%s'\n", relpath);
1143     fputs(relpath, fp);
1144     fputs("file/", fp);
1145     xmlencode(fp, path, strlen(path));
1146     fputs(".html\">", fp);
1147     xmlencode(fp, path, strlen(path));
1148     fprintf(fp, "</a>%s", strlen(path) == 0 ? "" : "/");
1149     xmlencode(fp, filename, strlen(filename));
1150     fprintf(fp, " (%zuB)</div>\n\n", filesize);
1151 
1152     fputs("<div id=\"line-checkbox-div\"><input type=\"checkbox\""
1153         "id=\"line-checkbox\" onchange=\"toggleLineNumbers()\" checked>", fp);
1154     fputs("<label for=\"line-checkbox\">Line numbers</label></div>", fp);
1155 
1156     if (ismarkdownfile(filename)) {
1157         fputs("<div id=\"md-checkbox-div\"><input type=\"checkbox\""
1158             "id=\"md-checkbox\" onchange=\"toggleMdView()\" checked>", fp);
1159         fputs("<label for=\"md-checkbox\">Render</label></div>", fp);
1160     }
1161 
1162     // fputs("<p id=\"openfile-name\"> ", fp);
1163     // xmlencode(fp, filename, strlen(filename));
1164     // fprintf(fp, " (%zuB)", filesize);
1165 
1166     fprintf(fp, "<div id=\"file-raw\"><a href=\"%s%s\">raw</a></div></div>", relpath, rpath);
1167     /* fputs("<hr>\n", fp); */
1168 
1169     if (git_blob_is_binary((git_blob *)obj))
1170         fputs("<p id=\"binary-file\">Binary file.</p>\n", fp);
1171     else if (ismarkdownfile(filename)) {
1172         fputs("<div id=\"md-view\">", fp);
1173         writeblobmd(fp, (git_blob *)obj, relpath);
1174         fputs("</div>\n", fp);
1175         fputs("<div id=\"source-view\">", fp);
1176         lc = writeblobhtml(fp, (git_blob *)obj);
1177         fputs("</div>\n", fp);
1178         fputs("<script>"
1179               "function toggleMdView() {"
1180               "  var mdCheckbox = document.getElementById('md-checkbox');"
1181               "  var mdView = document.getElementById('md-view');"
1182               "  var sourceView = document.getElementById('source-view');"
1183               "  var lineCheckboxDiv = document.getElementById('line-checkbox-div');"
1184               "  if (mdView && sourceView) {"
1185               "    mdView.style.display = mdCheckbox.checked ? 'block' : 'none';"
1186               "    sourceView.style.display = mdCheckbox.checked ? 'none' : 'block';"
1187               "  }"
1188               "  if (lineCheckboxDiv)"
1189               "    lineCheckboxDiv.style.display = mdCheckbox.checked ? 'none' : 'block';"
1190               "  localStorage.setItem('md-checkbox', mdCheckbox.checked);"
1191               "}"
1192               "document.addEventListener('DOMContentLoaded', function() {"
1193               "  var mdCheckbox = document.getElementById('md-checkbox');"
1194               "  var savedState = localStorage.getItem('md-checkbox');"
1195               "  if (savedState !== null) {"
1196               "    mdCheckbox.checked = savedState === 'true';"
1197               "  }"
1198               "  toggleMdView();"
1199               "});"
1200               "</script>", fp);
1201     } else
1202         lc = writeblobhtml(fp, (git_blob *)obj);
1203 
1204     writefooter(fp);
1205     checkfileerror(fp, fpath, 'w');
1206     fclose(fp);
1207 
1208     relpath = oldrelpath;
1209 
1210     return lc;
1211 }
1212 
1213 const char *
1214 filemode(git_filemode_t m)
1215 {
1216     static char mode[11];
1217 
1218     memset(mode, '-', sizeof(mode) - 1);
1219     mode[10] = '\0';
1220 
1221     if (S_ISREG(m))
1222         mode[0] = '-';
1223     else if (S_ISBLK(m))
1224         mode[0] = 'b';
1225     else if (S_ISCHR(m))
1226         mode[0] = 'c';
1227     else if (S_ISDIR(m))
1228         mode[0] = 'd';
1229     else if (S_ISFIFO(m))
1230         mode[0] = 'p';
1231     else if (S_ISLNK(m))
1232         mode[0] = 'l';
1233     else if (S_ISSOCK(m))
1234         mode[0] = 's';
1235     else
1236         mode[0] = '?';
1237 
1238     if (m & S_IRUSR) mode[1] = 'r';
1239     if (m & S_IWUSR) mode[2] = 'w';
1240     if (m & S_IXUSR) mode[3] = 'x';
1241     if (m & S_IRGRP) mode[4] = 'r';
1242     if (m & S_IWGRP) mode[5] = 'w';
1243     if (m & S_IXGRP) mode[6] = 'x';
1244     if (m & S_IROTH) mode[7] = 'r';
1245     if (m & S_IWOTH) mode[8] = 'w';
1246     if (m & S_IXOTH) mode[9] = 'x';
1247 
1248     if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
1249     if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
1250     if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
1251 
1252     return mode;
1253 }
1254 
1255 int
1256 writefilestree(FILE *fp, git_tree *tree, const char *path)
1257 {
1258     const git_tree_entry *entry = NULL;
1259     git_object *obj = NULL;
1260     FILE *fp_subtree;
1261     const char *entryname, *oldrelpath;
1262     char filepath[PATH_MAX], rawpath[PATH_MAX], entrypath[PATH_MAX], tmp[PATH_MAX], tmp2[PATH_MAX], oid[8];
1263     char* parent;
1264     size_t count, i, lc, filesize;
1265     int r, rf, ret, is_obj_tree;
1266 
1267     if (strlen(path) > 0) {
1268         fputs("<h2 id=\"dir-title\">Directory: ", fp);
1269         xmlencode(fp, path, strlen(path));
1270         fputs("</h2>\n\n", fp);
1271 
1272         fputs("<table id=\"dir-files\"><thead id=\"legends\">\n<tr>"
1273                 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>"
1274                 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>"
1275                 "</tr>\n</thead><tbody>\n", fp);
1276     } else {
1277         fputs("<table id=\"files\"><thead id=\"legends\">\n<tr>"
1278                 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>"
1279                 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>"
1280                 "</tr>\n</thead><tbody>\n", fp);
1281     }
1282 
1283     if (strlen(path) > 0) {
1284         if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
1285             errx(1, "path truncated: '%s'", path);
1286         parent = strrchr(tmp, '/');
1287         if (parent == NULL)
1288             parent = "files";
1289         else {
1290             *parent = '\0';
1291             parent = strrchr(tmp, '/');
1292             if (parent == NULL)
1293                 parent = tmp;
1294             else
1295                 ++parent;
1296         }
1297 
1298         fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'../");
1299         percentencode(fp, parent, strlen(parent));
1300         fputs(".html\'\">", fp);
1301 
1302         fputs("<td id=\"file-mode\"><a href=\"../", fp);
1303         xmlencode(fp, parent, strlen(parent));
1304         fputs(".html\">d---------</a></td>", fp);
1305 
1306         fputs("<td id=\"dir-name\"><a href=\"../", fp);
1307         xmlencode(fp, parent, strlen(parent));
1308         fputs(".html\">..</a></td>", fp);
1309 
1310         fputs("<td id=\"dir-size\"><a href=\"../", fp);
1311         xmlencode(fp, parent, strlen(parent));
1312         fputs(".html\">.</a></td></tr>\n", fp);
1313     }
1314 
1315     count = git_tree_entrycount(tree);
1316 
1317     /* print directories first if any */
1318     for (i = 0; i < count; i++) {
1319         if (!(entry = git_tree_entry_byindex(tree, i)) ||
1320             !(entryname = git_tree_entry_name(entry)))
1321             return -1;
1322 
1323         joinpath(entrypath, sizeof(entrypath), path, entryname);
1324 
1325         r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1326                  entrypath);
1327         if (r < 0 || (size_t)r >= sizeof(filepath))
1328             errx(1, "path truncated: 'file/%s.html'", entrypath);
1329         rf = snprintf(rawpath, sizeof(rawpath), "raw/%s",
1330                  entrypath);
1331         if (rf < 0 || (size_t)rf >= sizeof(rawpath))
1332             errx(1, "path truncated: 'raw/%s'", entrypath);
1333 
1334         if (!git_tree_entry_to_object(&obj, repo, entry)) {
1335             if (git_object_type(obj) == GIT_OBJ_TREE) {
1336                 mkdirfile(filepath);
1337 
1338                 if (strlcpy(tmp, relpath, sizeof(tmp)) >= sizeof(tmp))
1339                     errx(1, "path truncated: '%s'", relpath);
1340                 if (strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1341                     errx(1, "path truncated: '../%s'", tmp);
1342 
1343                 oldrelpath = relpath;
1344                 relpath = tmp;
1345                 fp_subtree = efopen(filepath, "w");
1346                 strlcpy(tmp2, "Files - ", sizeof(tmp2));
1347                 if (strlcat(tmp2, entrypath, sizeof(tmp2)) >= sizeof(tmp2))
1348                     errx(1, "path truncated: '%s'", tmp2);
1349                 writeheader(fp_subtree, tmp2);
1350                 /* fputs("<hr/>\n", fp_subtree); */
1351                 fputs("<div id=\"content\">\n", fp_subtree);
1352 
1353                 /* NOTE: recurses */
1354                 ret = writefilestree(fp_subtree, (git_tree *)obj,
1355                                      entrypath);
1356                 writefooter(fp_subtree);
1357                 relpath = oldrelpath;
1358                 lc = 0;
1359                 is_obj_tree = 1;
1360                 if (ret)
1361                     return ret;
1362 
1363                 /* make entire table row clickable */
1364                 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s",
1365                         relpath);
1366 
1367                 percentencode(fp, filepath, strlen(filepath));
1368                 fputs("\'\"><td id=\"file-mode\">", fp);
1369 
1370                 fprintf(fp, "<a href=\"%s", relpath);
1371                 percentencode(fp, filepath, strlen(filepath));
1372                 fputs("\">",fp);
1373                 fputs(filemode(git_tree_entry_filemode(entry)), fp);
1374                 fputs("</a></td>", fp);
1375 
1376                 if (git_object_type(obj) == GIT_OBJ_TREE)
1377                     fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath);
1378                 else
1379                     fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath);
1380 
1381                 percentencode(fp, filepath, strlen(filepath));
1382                 fputs("\">", fp);
1383                 xmlencode(fp, entryname, strlen(entryname));
1384                 fputs("</a></td>", fp);
1385 
1386                 if (lc > 0) {
1387                     fputs("<td id=\"file-size\" class=\"num\">", fp);
1388                     fprintf(fp, "<a href=\"%s", relpath);
1389                     percentencode(fp, filepath, strlen(filepath));
1390                     fputs("\">", fp);
1391                     fprintf(fp, "%zuL", lc);
1392                 }
1393                 else if (!is_obj_tree) {
1394                     fputs("<td id=\"file-size\" class=\"num\">", fp);
1395                     fprintf(fp, "<a href=\"%s", relpath);
1396                     percentencode(fp, filepath, strlen(filepath));
1397                     fputs("\">", fp);
1398                     fprintf(fp, "%zuB", filesize);
1399                 }
1400                 else if (is_obj_tree) {
1401                     fputs("<td id=\"dir-size\">", fp);
1402                     fprintf(fp, "<a href=\"%s", relpath);
1403                     percentencode(fp, filepath, strlen(filepath));
1404                     fputs("\">.", fp);
1405                 }
1406 
1407                 fputs("</a></td></tr>\n", fp);
1408                 git_object_free(obj);
1409             }
1410         }
1411     }
1412 
1413     /* print all files skipping directories */
1414     for (i = 0; i < count; i++) {
1415         if (!(entry = git_tree_entry_byindex(tree, i)) ||
1416             !(entryname = git_tree_entry_name(entry)))
1417             return -1;
1418         joinpath(entrypath, sizeof(entrypath), path, entryname);
1419 
1420         r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1421                  entrypath);
1422         if (r < 0 || (size_t)r >= sizeof(filepath))
1423             errx(1, "path truncated: 'file/%s.html'", entrypath);
1424         rf = snprintf(rawpath, sizeof(rawpath), "raw/%s",
1425                  entrypath);
1426         if (rf < 0 || (size_t)rf >= sizeof(rawpath))
1427             errx(1, "path truncated: 'raw/%s'", entrypath);
1428 
1429         if (!git_tree_entry_to_object(&obj, repo, entry)) {
1430             switch (git_object_type(obj)) {
1431             case GIT_OBJ_BLOB:
1432                 is_obj_tree = 0;
1433                 filesize = git_blob_rawsize((git_blob *)obj);
1434                 lc = writeblob(obj, filepath, rawpath, entryname, path, filesize);
1435                 writeblobraw((git_blob *)obj, rawpath, entryname, filesize);
1436                 break;
1437             case GIT_OBJ_TREE:
1438                 continue;
1439             default:
1440                 git_object_free(obj);
1441                 continue;
1442             }
1443 
1444             /* make entire table row clickable */
1445             fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s",
1446                     relpath);
1447             percentencode(fp, filepath, strlen(filepath));
1448             fputs("\'\"><td id=\"file-mode\">", fp);
1449 
1450             fprintf(fp, "<a href=\"%s", relpath);
1451             percentencode(fp, filepath, strlen(filepath));
1452             fputs("\">",fp);
1453             fputs(filemode(git_tree_entry_filemode(entry)), fp);
1454             fputs("</a></td>", fp);
1455 
1456             if (git_object_type(obj) == GIT_OBJ_TREE)
1457                 fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath);
1458             else
1459                 fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath);
1460 
1461             percentencode(fp, filepath, strlen(filepath));
1462             fputs("\">", fp);
1463 
1464             xmlencode(fp, entryname, strlen(entryname));
1465 
1466             fputs("</a></td>", fp);
1467 
1468             if (lc > 0) {
1469                 fputs("<td id=\"file-size\" class=\"num\">", fp);
1470                 fprintf(fp, "<a href=\"%s", relpath);
1471                 percentencode(fp, filepath, strlen(filepath));
1472                 fputs("\">", fp);
1473                 fprintf(fp, "%zuL", lc);
1474             }
1475             else if (!is_obj_tree) {
1476                 fputs("<td id=\"file-size\" class=\"num\">", fp);
1477                 fprintf(fp, "<a href=\"%s", relpath);
1478                 percentencode(fp, filepath, strlen(filepath));
1479                 fputs("\">", fp);
1480                 fprintf(fp, "%zuB", filesize);
1481             }
1482             else if (is_obj_tree) {
1483                 fputs("<td id=\"dir-size\">", fp);
1484                 fprintf(fp, "<a href=\"%s", relpath);
1485                 percentencode(fp, filepath, strlen(filepath));
1486                 fputs("\">.", fp);
1487             }
1488 
1489             fputs("</a></td></tr>\n", fp);
1490             git_object_free(obj);
1491 
1492         } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1493             /* commit object in tree is a submodule */
1494             fputs("<tr><td id=\"file-mode\">m---------</td>", fp);
1495             fprintf(fp, "<td><a href=\"%sfile/.gitmodules.html\">", relpath);
1496             xmlencode(fp, entrypath, strlen(entrypath));
1497             fputs("</a> @ ", fp);
1498             git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1499             xmlencode(fp, oid, strlen(oid));
1500             fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
1501         }
1502     }
1503 
1504     fputs("</tbody></table>", fp);
1505     return 0;
1506 }
1507 
1508 int
1509 writefiles(FILE *fp, const git_oid *id)
1510 {
1511     git_tree *tree = NULL;
1512     git_commit *commit = NULL;
1513     int ret = -1;
1514 
1515     if (!git_commit_lookup(&commit, repo, id) &&
1516         !git_commit_tree(&tree, commit))
1517         ret = writefilestree(fp, tree, "");
1518 
1519     git_commit_free(commit);
1520     git_tree_free(tree);
1521 
1522     return ret;
1523 }
1524 
1525 int
1526 writerefs(FILE *fp)
1527 {
1528     struct referenceinfo *ris = NULL;
1529     struct commitinfo *ci;
1530     size_t count, i, j, refcount;
1531     const char *titles[] = { "Branches", "Tags" };
1532     const char *ids[] = { "branches", "tags" };
1533     const char *s;
1534 
1535     if (getrefs(&ris, &refcount) == -1)
1536         return -1;
1537 
1538     for (i = 0, j = 0, count = 0; i < refcount; i++) {
1539         if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1540             if (count)
1541                 fputs("</tbody></table><br/>\n", fp);
1542             count = 0;
1543             j = 1;
1544         }
1545 
1546         /* print header if it has an entry (first). */
1547         if (++count == 1) {
1548             fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
1549                         "<thead id=\"legends\">\n<tr><td><b>Name</b></td>"
1550                     "<td><b>Last commit date</b></td>"
1551                     "<td><b>Author</b></td>\n</tr>\n"
1552                     "</thead><tbody>\n",
1553                      titles[j], ids[j]);
1554         }
1555 
1556         ci = ris[i].ci;
1557         s = git_reference_shorthand(ris[i].ref);
1558 
1559         fputs("<tr><td>", fp);
1560         xmlencode(fp, s, strlen(s));
1561         fputs("</td><td>", fp);
1562         if (ci->author)
1563             printtimeshort(fp, &(ci->author->when));
1564         fputs("</td><td>", fp);
1565         if (ci->author)
1566             xmlencode(fp, ci->author->name, strlen(ci->author->name));
1567         fputs("</td></tr>\n", fp);
1568     }
1569     /* table footer */
1570     if (count)
1571         fputs("</tbody></table><br/>\n", fp);
1572 
1573     for (i = 0; i < refcount; i++) {
1574         commitinfo_free(ris[i].ci);
1575         git_reference_free(ris[i].ref);
1576     }
1577     free(ris);
1578 
1579     return 0;
1580 }
1581 
1582 void
1583 usage(char *argv0)
1584 {
1585     fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
1586             "[-u baseurl] repodir\n", argv0);
1587     exit(1);
1588 }
1589 
1590 int
1591 main(int argc, char *argv[])
1592 {
1593     git_object *obj = NULL;
1594     const git_oid *head = NULL;
1595     mode_t mask;
1596     FILE *fp, *fpread;
1597     char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1598     char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1599     size_t n;
1600     int i, fd, r;
1601 
1602     for (i = 1; i < argc; i++) {
1603         if (argv[i][0] != '-') {
1604             if (repodir)
1605                 usage(argv[0]);
1606             repodir = argv[i];
1607         } else if (argv[i][1] == 'c') {
1608             if (nlogcommits > 0 || i + 1 >= argc)
1609                 usage(argv[0]);
1610             cachefile = argv[++i];
1611         } else if (argv[i][1] == 'l') {
1612             if (cachefile || i + 1 >= argc)
1613                 usage(argv[0]);
1614             errno = 0;
1615             nlogcommits = strtoll(argv[++i], &p, 10);
1616             if (argv[i][0] == '\0' || *p != '\0' ||
1617                 nlogcommits <= 0 || errno)
1618                 usage(argv[0]);
1619         } else if (argv[i][1] == 'u') {
1620             if (i + 1 >= argc)
1621                 usage(argv[0]);
1622             baseurl = argv[++i];
1623         }
1624     }
1625     if (!repodir)
1626         usage(argv[0]);
1627 
1628     if (!realpath(repodir, repodirabs))
1629         err(1, "realpath");
1630 
1631     /* do not search outside the git repository:
1632        GIT_CONFIG_LEVEL_APP is the highest level currently */
1633     git_libgit2_init();
1634     for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
1635         git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
1636     /* do not require the git repository to be owned by the current user */
1637     git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
1638 
1639 #ifdef __OpenBSD__
1640     if (unveil(repodir, "r") == -1)
1641         err(1, "unveil: %s", repodir);
1642     if (unveil(".", "rwc") == -1)
1643         err(1, "unveil: .");
1644     if (cachefile && unveil(cachefile, "rwc") == -1)
1645         err(1, "unveil: %s", cachefile);
1646 
1647     if (cachefile) {
1648         if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1649             err(1, "pledge");
1650     } else {
1651         if (pledge("stdio rpath wpath cpath", NULL) == -1)
1652             err(1, "pledge");
1653     }
1654 #endif
1655 
1656     if (git_repository_open_ext(&repo, repodir,
1657         GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1658         fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1659         return 1;
1660     }
1661 
1662     /* find HEAD */
1663     if (!git_revparse_single(&obj, repo, "HEAD"))
1664         head = git_object_id(obj);
1665     git_object_free(obj);
1666 
1667     /* use directory name as name */
1668     if ((name = strrchr(repodirabs, '/')))
1669         name++;
1670     else
1671         name = "";
1672 
1673     /* strip .git suffix */
1674     if (!(strippedname = strdup(name)))
1675         err(1, "strdup");
1676     if ((p = strrchr(strippedname, '.')))
1677         if (!strcmp(p, ".git"))
1678             *p = '\0';
1679 
1680     printf("%s\n", strippedname);
1681 
1682     /* read description or .git/description */
1683     joinpath(path, sizeof(path), repodir, "description");
1684     if (!(fpread = fopen(path, "r"))) {
1685         joinpath(path, sizeof(path), repodir, ".git/description");
1686         fpread = fopen(path, "r");
1687     }
1688     if (fpread) {
1689         if (!fgets(description, sizeof(description), fpread))
1690             description[0] = '\0';
1691         checkfileerror(fpread, path, 'r');
1692         fclose(fpread);
1693     }
1694 
1695     /* read url or .git/url */
1696     joinpath(path, sizeof(path), repodir, "url");
1697     if (!(fpread = fopen(path, "r"))) {
1698         joinpath(path, sizeof(path), repodir, ".git/url");
1699         fpread = fopen(path, "r");
1700     }
1701     if (fpread) {
1702         if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1703             cloneurl[0] = '\0';
1704         checkfileerror(fpread, path, 'r');
1705         fclose(fpread);
1706         cloneurl[strcspn(cloneurl, "\n")] = '\0';
1707     }
1708 
1709     /* check LICENSE */
1710     for (i = 0; i < LEN(licensefiles) && !license; i++) {
1711         if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1712             git_object_type(obj) == GIT_OBJ_BLOB)
1713             license = licensefiles[i] + strlen("HEAD:");
1714         git_object_free(obj);
1715     }
1716 
1717     /* check README */
1718     for (i = 0; i < LEN(readmefiles) && !readme; i++) {
1719         if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1720             git_object_type(obj) == GIT_OBJ_BLOB)
1721             readme = readmefiles[i] + strlen("HEAD:");
1722             r = i;
1723         git_object_free(obj);
1724     }
1725 
1726     if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1727         git_object_type(obj) == GIT_OBJ_BLOB)
1728         submodules = ".gitmodules";
1729     git_object_free(obj);
1730 
1731     /* about page */
1732     if (readme) {
1733         fp = efopen("readme.html", "w");
1734         writeheader(fp, "README");
1735 
1736         git_revparse_single(&obj, repo, readmefiles[r]);
1737         const char *s = git_blob_rawcontent((git_blob *)obj);
1738         if (r == 1) {
1739             writeblobmd(fp, (git_blob *)obj, "");
1740         } else {
1741             fputs("<pre id=\"readme\">", fp);
1742             xmlencode(fp, s, strlen(s));
1743             fputs("</pre>\n", fp);
1744         }
1745         git_object_free(obj);
1746         writefooter(fp);
1747         fclose(fp);
1748     }
1749 
1750     /* log for HEAD */
1751     fp = efopen("log.html", "w");
1752     relpath = "";
1753     mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1754     writeheader(fp, "Commits");
1755     fputs("<div id=\"content\">\n", fp);
1756     fputs("<table id=\"log\"><thead id=\"legends\">\n<tr>"
1757           "<td id=\"log-date\"><b>Date</b></td>"
1758           "<td id=\"log-summary\"><b>Commit message</b></td>"
1759           "<td id=\"log-author\"><b>Author</b></td>"
1760           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>Files</b></td>"
1761           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>+</b></td>"
1762           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
1763 
1764     if (cachefile && head) {
1765         /* read from cache file (does not need to exist) */
1766         if ((rcachefp = fopen(cachefile, "r"))) {
1767             if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1768                 errx(1, "%s: no object id", cachefile);
1769             if (git_oid_fromstr(&lastoid, lastoidstr))
1770                 errx(1, "%s: invalid object id", cachefile);
1771         }
1772 
1773         /* write log to (temporary) cache */
1774         if ((fd = mkstemp(tmppath)) == -1)
1775             err(1, "mkstemp");
1776         if (!(wcachefp = fdopen(fd, "w")))
1777             err(1, "fdopen: '%s'", tmppath);
1778         /* write last commit id (HEAD) */
1779         git_oid_tostr(buf, sizeof(buf), head);
1780         fprintf(wcachefp, "%s\n", buf);
1781 
1782         writelog(fp, head);
1783 
1784         if (rcachefp) {
1785             /* append previous log to log.html and the new cache */
1786             while (!feof(rcachefp)) {
1787                 n = fread(buf, 1, sizeof(buf), rcachefp);
1788                 if (ferror(rcachefp))
1789                     break;
1790                 if (fwrite(buf, 1, n, fp) != n ||
1791                     fwrite(buf, 1, n, wcachefp) != n)
1792                         break;
1793             }
1794             checkfileerror(rcachefp, cachefile, 'r');
1795             fclose(rcachefp);
1796         }
1797         checkfileerror(wcachefp, tmppath, 'w');
1798         fclose(wcachefp);
1799     } else {
1800         if (head)
1801             writelog(fp, head);
1802     }
1803 
1804     fputs("</tbody></table>", fp);
1805     writefooter(fp);
1806     checkfileerror(fp, "log.html", 'w');
1807     fclose(fp);
1808 
1809     /* files for HEAD */
1810     fp = efopen("files.html", "w");
1811     writeheader(fp, "Files");
1812     if (head)
1813         writefiles(fp, head);
1814     if (readme) {
1815         git_revparse_single(&obj, repo, readmefiles[r]);
1816         const char *s = git_blob_rawcontent((git_blob *)obj);
1817         if (r == 1) {
1818             writeblobmd(fp, (git_blob *)obj, "");
1819         } else {
1820             fputs("<pre id=\"readme\">", fp);
1821             xmlencode(fp, s, strlen(s));
1822             fputs("</pre>\n", fp);
1823         }
1824         git_object_free(obj);
1825     }
1826     writefooter(fp);
1827     checkfileerror(fp, "files.html", 'w');
1828     fclose(fp);
1829 
1830     /* summary page with branches and tags */
1831     fp = efopen("refs.html", "w");
1832     writeheader(fp, "Refs");
1833     fputs("<div id=\"content\">\n<div id=\"refs\">\n", fp);
1834     writerefs(fp);
1835     writefooter(fp);
1836     checkfileerror(fp, "refs.html", 'w');
1837     fclose(fp);
1838 
1839     /* Atom feed */
1840     fp = efopen("atom.xml", "w");
1841     writeatom(fp, 1);
1842     checkfileerror(fp, "atom.xml", 'w');
1843     fclose(fp);
1844 
1845     /* Atom feed for tags / releases */
1846     fp = efopen("tags.xml", "w");
1847     writeatom(fp, 0);
1848     checkfileerror(fp, "tags.xml", 'w');
1849     fclose(fp);
1850 
1851     /* rename new cache file on success */
1852     if (cachefile && head) {
1853         if (rename(tmppath, cachefile))
1854             err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1855         umask((mask = umask(0)));
1856         if (chmod(cachefile,
1857             (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1858             err(1, "chmod: '%s'", cachefile);
1859     }
1860 
1861     /* cleanup */
1862     git_repository_free(repo);
1863     git_libgit2_shutdown();
1864 
1865     return 0;
1866 }