repos/stagit

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