stagit

custom fork of stagit
Index Commits Files Refs README LICENSE
stagit.c (47930B)
   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<table id=\"repo-header-table\"><tr><td id=\"repo-logo\">", fp);
 544     fprintf(fp, "<a href=\"https://git.kloeckner.com.ar\"><img src=\"%slogo.png\" alt=\"\" width=\"64\" height=\"64\" /></a>",
 545             relpath, relpath);
 546     fputs("</td><td id=\"repo-header\"><h1 id=\"repo-name\">", fp);
 547     xmlencode(fp, strippedname, strlen(strippedname));
 548     fputs("</h1><span id=\"repo-desc\">", fp);
 549     xmlencode(fp, description, strlen(description));
 550     fputs("</span>", fp);
 551 
 552     fputs("</td></tr></table>", fp);
 553 
 554     fputs("<div id=\"repo-top-buttons\">\n", fp);
 555     fprintf(fp, "<a <a href=\"https://git.kloeckner.com.ar\">Index</a>  ", relpath);
 556     fprintf(fp, "<a href=\"%slog.html\">Commits</a>  ", relpath);
 557     fprintf(fp, "<a href=\"%sfiles.html\">Files</a>  ", relpath);
 558     fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath);
 559     if (submodules)
 560         fprintf(fp, "  <a href=\"%sfile/%s.html\">Submodules</a>",
 561                 relpath, submodules);
 562     if (readme)
 563         fprintf(fp, "  <a href=\"%sreadme.html\">README</a>", relpath);
 564     if (license)
 565         fprintf(fp, "  <a href=\"%sfile/%s.html\">LICENSE</a>",
 566                 relpath, license);
 567 
 568     if (cloneurl[0]) {
 569         fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
 570         xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
 571         fputs("\">", fp);
 572         xmlencode(fp, cloneurl, strlen(cloneurl));
 573         fputs("</a></td></tr>", fp);
 574     }
 575 
 576     /* fputs("</div>\n<hr/>\n<div id=\"content\">\n", fp); */
 577     fputs("</div>\n", fp);
 578 }
 579 
 580 void
 581 writefooter(FILE *fp)
 582 {
 583     /* fputs("</div>\n</div>\n</body>\n</html>\n", fp); */
 584     fputs("</div>\n</div>\n</body>\n", fp);
 585     fputs("<footer>Generated with <a href=\"https://git.kloeckner.com.ar/stagit/\">Stagit</a></footer>\n", fp);
 586     fputs("</html>\n", fp);
 587 }
 588 
 589 size_t
 590 writeblobhtml(FILE *fp, const git_blob *blob)
 591 {
 592     size_t n = 0, i, len, prev;
 593     const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%4zu</a> ";
 594     const char *s = git_blob_rawcontent(blob);
 595 
 596     len = git_blob_rawsize(blob);
 597     fputs("<pre id=\"blob\">\n", fp);
 598 
 599     if (len > 0) {
 600         for (i = 0, prev = 0; i < len; i++) {
 601             if (s[i] != '\n')
 602                 continue;
 603             n++;
 604             fprintf(fp, nfmt, n, n, n);
 605             xmlencodeline(fp, &s[prev], i - prev + 1);
 606             putc('\n', fp);
 607             prev = i + 1;
 608         }
 609         /* trailing data */
 610         if ((len - prev) > 0) {
 611             n++;
 612             fprintf(fp, nfmt, n, n, n);
 613             xmlencodeline(fp, &s[prev], len - prev);
 614         }
 615     }
 616 
 617     fputs("</pre>\n", fp);
 618 
 619     return n;
 620 }
 621 
 622 void
 623 printcommit(FILE *fp, struct commitinfo *ci)
 624 {
 625     fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
 626         relpath, ci->oid, ci->oid);
 627 
 628     if (ci->parentoid[0])
 629         fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
 630             relpath, ci->parentoid, ci->parentoid);
 631 
 632     if (ci->author) {
 633         fputs("<b>Author:</b> ", fp);
 634         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 635         fputs(" &lt;<a href=\"mailto:", fp);
 636         xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
 637         fputs("\">", fp);
 638         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 639         fputs("</a>&gt;\n<b>Date:</b>   ", fp);
 640         printtime(fp, &(ci->author->when));
 641         putc('\n', fp);
 642     }
 643     if (ci->msg) {
 644         putc('\n', fp);
 645         xmlencode(fp, ci->msg, strlen(ci->msg));
 646         putc('\n', fp);
 647     }
 648 }
 649 
 650 void
 651 printshowfile(FILE *fp, struct commitinfo *ci)
 652 {
 653     const git_diff_delta *delta;
 654     const git_diff_hunk *hunk;
 655     const git_diff_line *line;
 656     git_patch *patch;
 657     size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
 658     char linestr[80];
 659     int c;
 660 
 661     printcommit(fp, ci);
 662 
 663     if (!ci->deltas)
 664         return;
 665 
 666     if (ci->filecount > 1000   ||
 667         ci->ndeltas   > 1000   ||
 668         ci->addcount  > 100000 ||
 669         ci->delcount  > 100000) {
 670         fputs("Diff is too large, output suppressed.\n", fp);
 671         return;
 672     }
 673 
 674     /* diff stat */
 675     fputs("<b>Diffstat:</b>\n<table>", fp);
 676     for (i = 0; i < ci->ndeltas; i++) {
 677         delta = git_patch_get_delta(ci->deltas[i]->patch);
 678 
 679         switch (delta->status) {
 680         case GIT_DELTA_ADDED:      c = 'A'; break;
 681         case GIT_DELTA_COPIED:     c = 'C'; break;
 682         case GIT_DELTA_DELETED:    c = 'D'; break;
 683         case GIT_DELTA_MODIFIED:   c = 'M'; break;
 684         case GIT_DELTA_RENAMED:    c = 'R'; break;
 685         case GIT_DELTA_TYPECHANGE: c = 'T'; break;
 686         default:                   c = ' '; break;
 687         }
 688         if (c == ' ')
 689             fprintf(fp, "<tr><td>%c", c);
 690         else
 691             fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
 692 
 693         fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
 694         xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 695         if (strcmp(delta->old_file.path, delta->new_file.path)) {
 696             fputs(" -&gt; ", fp);
 697             xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 698         }
 699 
 700         add = ci->deltas[i]->addcount;
 701         del = ci->deltas[i]->delcount;
 702         changed = add + del;
 703         total = sizeof(linestr) - 2;
 704         if (changed > total) {
 705             if (add)
 706                 add = ((float)total / changed * add) + 1;
 707             if (del)
 708                 del = ((float)total / changed * del) + 1;
 709         }
 710         memset(&linestr, '+', add);
 711         memset(&linestr[add], '-', del);
 712 
 713         fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
 714                 ci->deltas[i]->addcount + ci->deltas[i]->delcount);
 715         fwrite(&linestr, 1, add, fp);
 716         fputs("</span><span class=\"d\">", fp);
 717         fwrite(&linestr[add], 1, del, fp);
 718         fputs("</span></td></tr>\n", fp);
 719     }
 720     fprintf(fp, "</table></pre><pre id=\"commit-diff\">%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
 721         ci->filecount, ci->filecount == 1 ? "" : "s",
 722             ci->addcount,  ci->addcount  == 1 ? "" : "s",
 723             ci->delcount,  ci->delcount  == 1 ? "" : "s");
 724 
 725     for (i = 0; i < ci->ndeltas; i++) {
 726         patch = ci->deltas[i]->patch;
 727         delta = git_patch_get_delta(patch);
 728         fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
 729         percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 730         fputs(".html\">", fp);
 731         xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 732         fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
 733         percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 734         fprintf(fp, ".html\">");
 735         xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 736         fprintf(fp, "</a></b>\n");
 737 
 738         /* check binary data */
 739         if (delta->flags & GIT_DIFF_FLAG_BINARY) {
 740             fputs("Binary files differ.\n", fp);
 741             continue;
 742         }
 743 
 744         nhunks = git_patch_num_hunks(patch);
 745         for (j = 0; j < nhunks; j++) {
 746             if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 747                 break;
 748 
 749             fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
 750             xmlencode(fp, hunk->header, hunk->header_len);
 751             fputs("</a>", fp);
 752 
 753             for (k = 0; ; k++) {
 754                 if (git_patch_get_line_in_hunk(&line, patch, j, k))
 755                     break;
 756                 if (line->old_lineno == -1)
 757                     fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
 758                         i, j, k, i, j, k);
 759                 else if (line->new_lineno == -1)
 760                     fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
 761                         i, j, k, i, j, k);
 762                 else
 763                     putc(' ', fp);
 764                 xmlencodeline(fp, line->content, line->content_len);
 765                 putc('\n', fp);
 766                 if (line->old_lineno == -1 || line->new_lineno == -1)
 767                     fputs("</a>", fp);
 768             }
 769         }
 770     }
 771 }
 772 
 773 void
 774 writelogline(FILE *fp, struct commitinfo *ci)
 775 {
 776     /* make entire table row clickable */
 777     fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'commit/%s.html'\">",
 778             ci->oid);
 779 
 780     fputs("<td id=\"log-date\">", fp);
 781     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 782     if (ci->author)
 783         printtimeshort(fp, &(ci->author->when));
 784     fputs("</a></td>",fp);
 785 
 786     fputs("<td id=\"log-summary\">", fp);
 787     if (ci->summary) {
 788         fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 789         xmlencode(fp, ci->summary, strlen(ci->summary));
 790         fputs("</a>",fp);
 791     }
 792     fputs("</td>", fp);
 793 
 794     fputs("<td id=\"log-author\">", fp);
 795     if (ci->author) {
 796         fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 797         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 798         fputs("</a>",fp);
 799     }
 800     fputs("</td>", fp);
 801 
 802     fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp);
 803     fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
 804     fprintf(fp, "%zu", ci->filecount);
 805     fputs("</a></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->addcount);
 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->delcount);
 815     fputs("</a></td></tr>\n", fp);
 816 }
 817 
 818 int
 819 writelog(FILE *fp, const git_oid *oid)
 820 {
 821     struct commitinfo *ci;
 822     git_revwalk *w = NULL;
 823     git_oid id;
 824     char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
 825     FILE *fpfile;
 826     size_t remcommits = 0;
 827     int r;
 828 
 829     git_revwalk_new(&w, repo);
 830     git_revwalk_push(w, oid);
 831 
 832     while (!git_revwalk_next(&id, w)) {
 833         relpath = "";
 834 
 835         if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
 836             break;
 837 
 838         git_oid_tostr(oidstr, sizeof(oidstr), &id);
 839         r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
 840         if (r < 0 || (size_t)r >= sizeof(path))
 841             errx(1, "path truncated: 'commit/%s.html'", oidstr);
 842         r = access(path, F_OK);
 843 
 844         /* optimization: if there are no log lines to write and
 845            the commit file already exists: skip the diffstat */
 846         if (!nlogcommits) {
 847             remcommits++;
 848             if (!r)
 849                 continue;
 850         }
 851 
 852         if (!(ci = commitinfo_getbyoid(&id)))
 853             break;
 854         /* diffstat: for stagit HTML required for the log.html line */
 855         if (commitinfo_getstats(ci) == -1)
 856             goto err;
 857 
 858         if (nlogcommits != 0) {
 859             writelogline(fp, ci);
 860             if (nlogcommits > 0)
 861                 nlogcommits--;
 862         }
 863 
 864         if (cachefile)
 865             writelogline(wcachefp, ci);
 866 
 867         /* check if file exists if so skip it */
 868         if (r) {
 869             relpath = "../";
 870             fpfile = efopen(path, "w");
 871             writeheader(fpfile, ci->summary);
 872             fputs("<div id=\"content\">\n", fpfile);
 873             fputs("<pre id=\"commit-summary\">", fpfile);
 874             printshowfile(fpfile, ci);
 875             printf("%s\n", ci->oid);
 876             fputs("</pre>\n", fpfile);
 877             writefooter(fpfile);
 878             checkfileerror(fpfile, path, 'w');
 879             fclose(fpfile);
 880         }
 881 err:
 882         commitinfo_free(ci);
 883     }
 884     git_revwalk_free(w);
 885 
 886     if (nlogcommits == 0 && remcommits != 0) {
 887         fprintf(fp, "<tr><td></td><td colspan=\"5\">"
 888                 "%zu more commits remaining, fetch the repository"
 889                 "</td></tr>\n", remcommits);
 890     }
 891 
 892     relpath = "";
 893 
 894     return 0;
 895 }
 896 
 897 void
 898 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
 899 {
 900     fputs("<entry>\n", fp);
 901 
 902     fprintf(fp, "<id>%s</id>\n", ci->oid);
 903     if (ci->author) {
 904         fputs("<published>", fp);
 905         printtimez(fp, &(ci->author->when));
 906         fputs("</published>\n", fp);
 907     }
 908     if (ci->committer) {
 909         fputs("<updated>", fp);
 910         printtimez(fp, &(ci->committer->when));
 911         fputs("</updated>\n", fp);
 912     }
 913     if (ci->summary) {
 914         fputs("<title type=\"text\">", fp);
 915         if (tag && tag[0]) {
 916             fputs("[", fp);
 917             xmlencode(fp, tag, strlen(tag));
 918             fputs("] ", fp);
 919         }
 920         xmlencode(fp, ci->summary, strlen(ci->summary));
 921         fputs("</title>\n", fp);
 922     }
 923     fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
 924             baseurl, ci->oid);
 925 
 926     if (ci->author) {
 927         fputs("<author>\n<name>", fp);
 928         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 929         fputs("</name>\n<email>", fp);
 930         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 931         fputs("</email>\n</author>\n", fp);
 932     }
 933 
 934     fputs("<content type=\"text\">", fp);
 935     fprintf(fp, "commit %s\n", ci->oid);
 936     if (ci->parentoid[0])
 937         fprintf(fp, "parent %s\n", ci->parentoid);
 938     if (ci->author) {
 939         fputs("Author: ", fp);
 940         xmlencode(fp, ci->author->name, strlen(ci->author->name));
 941         fputs(" &lt;", fp);
 942         xmlencode(fp, ci->author->email, strlen(ci->author->email));
 943         fputs("&gt;\nDate:   ", fp);
 944         printtime(fp, &(ci->author->when));
 945         putc('\n', fp);
 946     }
 947     if (ci->msg) {
 948         putc('\n', fp);
 949         xmlencode(fp, ci->msg, strlen(ci->msg));
 950     }
 951     fputs("\n</content>\n</entry>\n", fp);
 952 }
 953 
 954 int
 955 writeatom(FILE *fp, int all)
 956 {
 957     struct referenceinfo *ris = NULL;
 958     size_t refcount = 0;
 959     struct commitinfo *ci;
 960     git_revwalk *w = NULL;
 961     git_oid id;
 962     size_t i, m = 100; /* last 'm' commits */
 963 
 964     fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
 965           "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
 966     xmlencode(fp, strippedname, strlen(strippedname));
 967     fputs(", branch HEAD</title>\n<subtitle>", fp);
 968     xmlencode(fp, description, strlen(description));
 969     fputs("</subtitle>\n", fp);
 970 
 971     /* all commits or only tags? */
 972     if (all) {
 973         git_revwalk_new(&w, repo);
 974         git_revwalk_push_head(w);
 975         for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
 976             if (!(ci = commitinfo_getbyoid(&id)))
 977                 break;
 978             printcommitatom(fp, ci, "");
 979             commitinfo_free(ci);
 980         }
 981         git_revwalk_free(w);
 982     } else if (getrefs(&ris, &refcount) != -1) {
 983         /* references: tags */
 984         for (i = 0; i < refcount; i++) {
 985             if (git_reference_is_tag(ris[i].ref))
 986                 printcommitatom(fp, ris[i].ci,
 987                                 git_reference_shorthand(ris[i].ref));
 988 
 989             commitinfo_free(ris[i].ci);
 990             git_reference_free(ris[i].ref);
 991         }
 992         free(ris);
 993     }
 994 
 995     fputs("</feed>\n", fp);
 996 
 997     return 0;
 998 }
 999 
1000 void
1001 writeblobraw(const git_blob *blob, const char *fpath, const char *filename, git_off_t filesize)
1002 {
1003     char tmp[PATH_MAX] = "";
1004     const char *p;
1005     size_t lc = 0;
1006     FILE *fp;
1007 
1008     mkdirfile(fpath);
1009 
1010     if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1011         errx(1, "path truncated: '%s'", fpath);
1012 
1013     for (p = fpath, tmp[0] = '\0'; *p; p++) {
1014         if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1015             errx(1, "path truncated: '../%s'", tmp);
1016     }
1017 
1018     fp = efopen(fpath, "w");
1019     fwrite(git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob), 1, fp);
1020     fclose(fp);
1021 }
1022 
1023 size_t
1024 writeblob(git_object *obj, const char *fpath, const char *rpath, const char *filename, const char *path, size_t filesize)
1025 {
1026     char tmp[PATH_MAX] = "", *file_parent;
1027     const char *p, *oldrelpath;
1028     int lc = 0;
1029     FILE *fp;
1030 
1031     mkdirfile(fpath);
1032 
1033     if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1034         errx(1, "path truncated: '%s'", fpath);
1035 
1036     file_parent = strrchr(tmp, '/');
1037     for (p = fpath, tmp[0] = '\0'; *p; p++) {
1038         if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1039             errx(1, "path truncated: '../%s'", tmp);
1040     }
1041 
1042     oldrelpath = relpath;
1043     relpath = tmp;
1044 
1045     if (file_parent == NULL)
1046         file_parent = "files";
1047     else {
1048         *file_parent = '\0';
1049         file_parent = strrchr(tmp, '/');
1050         if (file_parent == NULL)
1051             file_parent = tmp;
1052         else
1053             ++file_parent;
1054     }
1055 
1056     fp = efopen(fpath, "w");
1057     writeheader(fp, filename);
1058     /* fputs("<hr>\n", fp); */
1059     fputs("<div id=\"content\">\n", fp);
1060     fputs("<script>"
1061               "function toggleLineNumbers() {"
1062               "  var lines = document.querySelectorAll('.line');"
1063               "  var lineCheckbox = document.getElementById('line-checkbox');"
1064 
1065               "  lines.forEach(function(element) {"
1066               "    if (lineCheckbox.checked) {"
1067               "      element.style.display = 'inline';"
1068               "    } else {"
1069               "      element.style.display = 'none';"
1070               "    }"
1071               "  });"
1072           " localStorage.setItem('line-checkbox', lineCheckbox.checked);"
1073               "}"
1074           "function setCheckboxState() {"
1075           "  var lineCheckbox = document.getElementById('line-checkbox');"
1076           "  var savedState = localStorage.getItem('line-checkbox');"
1077           "    if (savedState !== null) {"
1078           "      lineCheckbox.checked = savedState === 'true';"
1079           "      toggleLineNumbers();"
1080           "    }"
1081           "}"
1082           "document.addEventListener('DOMContentLoaded', setCheckboxState);"
1083               "</script>", fp);
1084 
1085     fputs("<div id=\"open-file-header\"><div id=\"open-file-name\"><a id=\"file-parent-path\" href=\"", fp);
1086     xmlencode(fp, relpath, strlen(relpath));
1087     fputs("/>", fp);
1088     xmlencode(fp, path, strlen(path));
1089     fputs(".html\">", fp);
1090     xmlencode(fp, path, strlen(path));
1091     fprintf(fp, "</a>%s", strlen(path) == 0 ? "" : "/");
1092     xmlencode(fp, filename, strlen(filename));
1093     fprintf(fp, " (%zuB)</div>\n\n", filesize);
1094 
1095     fputs("<div id=\"line-checkbox-div\"><input type=\"checkbox\""
1096         "id=\"line-checkbox\" onchange=\"toggleLineNumbers()\" checked>", fp);
1097     fputs("<label for=\"line-checkbox\">Line numbers</label></div>", fp);
1098 
1099     // fputs("<p id=\"openfile-name\"> ", fp);
1100     // xmlencode(fp, filename, strlen(filename));
1101     // fprintf(fp, " (%zuB)", filesize);
1102 
1103     fprintf(fp, "<div id=\"file-raw\"><a href=\"%s%s\">raw</a></div></div>", relpath, rpath);
1104     /* fputs("<hr>\n", fp); */
1105 
1106     if (git_blob_is_binary((git_blob *)obj))
1107         fputs("<p id=\"binary-file\">Binary file.</p>\n", fp);
1108     else
1109         lc = writeblobhtml(fp, (git_blob *)obj);
1110 
1111     writefooter(fp);
1112     checkfileerror(fp, fpath, 'w');
1113     fclose(fp);
1114 
1115     relpath = oldrelpath;
1116 
1117     return lc;
1118 }
1119 
1120 const char *
1121 filemode(git_filemode_t m)
1122 {
1123     static char mode[11];
1124 
1125     memset(mode, '-', sizeof(mode) - 1);
1126     mode[10] = '\0';
1127 
1128     if (S_ISREG(m))
1129         mode[0] = '-';
1130     else if (S_ISBLK(m))
1131         mode[0] = 'b';
1132     else if (S_ISCHR(m))
1133         mode[0] = 'c';
1134     else if (S_ISDIR(m))
1135         mode[0] = 'd';
1136     else if (S_ISFIFO(m))
1137         mode[0] = 'p';
1138     else if (S_ISLNK(m))
1139         mode[0] = 'l';
1140     else if (S_ISSOCK(m))
1141         mode[0] = 's';
1142     else
1143         mode[0] = '?';
1144 
1145     if (m & S_IRUSR) mode[1] = 'r';
1146     if (m & S_IWUSR) mode[2] = 'w';
1147     if (m & S_IXUSR) mode[3] = 'x';
1148     if (m & S_IRGRP) mode[4] = 'r';
1149     if (m & S_IWGRP) mode[5] = 'w';
1150     if (m & S_IXGRP) mode[6] = 'x';
1151     if (m & S_IROTH) mode[7] = 'r';
1152     if (m & S_IWOTH) mode[8] = 'w';
1153     if (m & S_IXOTH) mode[9] = 'x';
1154 
1155     if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
1156     if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
1157     if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
1158 
1159     return mode;
1160 }
1161 
1162 int
1163 writefilestree(FILE *fp, git_tree *tree, const char *path)
1164 {
1165     const git_tree_entry *entry = NULL;
1166     git_object *obj = NULL;
1167     FILE *fp_subtree;
1168     const char *entryname, *oldrelpath;
1169     char filepath[PATH_MAX], rawpath[PATH_MAX], entrypath[PATH_MAX], tmp[PATH_MAX], tmp2[PATH_MAX], oid[8];
1170     char* parent;
1171     size_t count, i, lc, filesize;
1172     int r, rf, ret, is_obj_tree;
1173 
1174     if (strlen(path) > 0) {
1175         fputs("<h2 id=\"dir-title\">Directory: ", fp);
1176         xmlencode(fp, path, strlen(path));
1177         fputs("</h2>\n\n", fp);
1178 
1179         fputs("<table id=\"dir-files\"><thead id=\"legends\">\n<tr>"
1180                 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>"
1181                 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>"
1182                 "</tr>\n</thead><tbody>\n", fp);
1183     } else {
1184         fputs("<table id=\"files\"><thead id=\"legends\">\n<tr>"
1185                 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>"
1186                 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>"
1187                 "</tr>\n</thead><tbody>\n", fp);
1188     }
1189 
1190     if (strlen(path) > 0) {
1191         if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
1192             errx(1, "path truncated: '%s'", path);
1193         parent = strrchr(tmp, '/');
1194         if (parent == NULL)
1195             parent = "files";
1196         else {
1197             *parent = '\0';
1198             parent = strrchr(tmp, '/');
1199             if (parent == NULL)
1200                 parent = tmp;
1201             else
1202                 ++parent;
1203         }
1204 
1205         fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'../");
1206         percentencode(fp, parent, strlen(parent));
1207         fputs(".html\'\">", fp);
1208 
1209         fputs("<td id=\"file-mode\"><a href=\"../", fp);
1210         xmlencode(fp, parent, strlen(parent));
1211         fputs(".html\">d---------</a></td>", fp);
1212 
1213         fputs("<td id=\"dir-name\"><a href=\"../", fp);
1214         xmlencode(fp, parent, strlen(parent));
1215         fputs(".html\">..</a></td>", fp);
1216 
1217         fputs("<td id=\"dir-size\"><a href=\"../", fp);
1218         xmlencode(fp, parent, strlen(parent));
1219         fputs(".html\">.</a></td></tr>\n", fp);
1220     }
1221 
1222     count = git_tree_entrycount(tree);
1223 
1224     /* print directories first if any */
1225     for (i = 0; i < count; i++) {
1226         if (!(entry = git_tree_entry_byindex(tree, i)) ||
1227             !(entryname = git_tree_entry_name(entry)))
1228             return -1;
1229 
1230         joinpath(entrypath, sizeof(entrypath), path, entryname);
1231 
1232         r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1233                  entrypath);
1234         if (r < 0 || (size_t)r >= sizeof(filepath))
1235             errx(1, "path truncated: 'file/%s.html'", entrypath);
1236         rf = snprintf(rawpath, sizeof(rawpath), "raw/%s",
1237                  entrypath);
1238         if (rf < 0 || (size_t)rf >= sizeof(rawpath))
1239             errx(1, "path truncated: 'raw/%s'", entrypath);
1240 
1241         if (!git_tree_entry_to_object(&obj, repo, entry)) {
1242             if (git_object_type(obj) == GIT_OBJ_TREE) {
1243                 mkdirfile(filepath);
1244 
1245                 if (strlcpy(tmp, relpath, sizeof(tmp)) >= sizeof(tmp))
1246                     errx(1, "path truncated: '%s'", relpath);
1247                 if (strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1248                     errx(1, "path truncated: '../%s'", tmp);
1249 
1250                 oldrelpath = relpath;
1251                 relpath = tmp;
1252                 fp_subtree = efopen(filepath, "w");
1253                 strlcpy(tmp2, "Files - ", sizeof(tmp2));
1254                 if (strlcat(tmp2, entrypath, sizeof(tmp2)) >= sizeof(tmp2))
1255                     errx(1, "path truncated: '%s'", tmp2);
1256                 writeheader(fp_subtree, tmp2);
1257                 /* fputs("<hr/>\n", fp_subtree); */
1258                 fputs("<div id=\"content\">\n", fp_subtree);
1259 
1260                 /* NOTE: recurses */
1261                 ret = writefilestree(fp_subtree, (git_tree *)obj,
1262                                      entrypath);
1263                 writefooter(fp_subtree);
1264                 relpath = oldrelpath;
1265                 lc = 0;
1266                 is_obj_tree = 1;
1267                 if (ret)
1268                     return ret;
1269 
1270                 /* make entire table row clickable */
1271                 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s",
1272                         relpath);
1273 
1274                 percentencode(fp, filepath, strlen(filepath));
1275                 fputs("\'\"><td id=\"file-mode\">", fp);
1276 
1277                 fprintf(fp, "<a href=\"%s", relpath);
1278                 percentencode(fp, filepath, strlen(filepath));
1279                 fputs("\">",fp);
1280                 fputs(filemode(git_tree_entry_filemode(entry)), fp);
1281                 fputs("</a></td>", fp);
1282 
1283                 if (git_object_type(obj) == GIT_OBJ_TREE)
1284                     fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath);
1285                 else
1286                     fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath);
1287 
1288                 percentencode(fp, filepath, strlen(filepath));
1289                 fputs("\">", fp);
1290                 xmlencode(fp, entryname, strlen(entryname));
1291                 fputs("</a></td>", fp);
1292 
1293                 if (lc > 0) {
1294                     fputs("<td id=\"file-size\" class=\"num\">", fp);
1295                     fprintf(fp, "<a href=\"%s", relpath);
1296                     percentencode(fp, filepath, strlen(filepath));
1297                     fputs("\">", fp);
1298                     fprintf(fp, "%zuL", lc);
1299                 }
1300                 else if (!is_obj_tree) {
1301                     fputs("<td id=\"file-size\" class=\"num\">", fp);
1302                     fprintf(fp, "<a href=\"%s", relpath);
1303                     percentencode(fp, filepath, strlen(filepath));
1304                     fputs("\">", fp);
1305                     fprintf(fp, "%zuB", filesize);
1306                 }
1307                 else if (is_obj_tree) {
1308                     fputs("<td id=\"dir-size\">", fp);
1309                     fprintf(fp, "<a href=\"%s", relpath);
1310                     percentencode(fp, filepath, strlen(filepath));
1311                     fputs("\">.", fp);
1312                 }
1313 
1314                 fputs("</a></td></tr>\n", fp);
1315                 git_object_free(obj);
1316             }
1317         }
1318     }
1319 
1320     /* print all files skipping directories */
1321     for (i = 0; i < count; i++) {
1322         if (!(entry = git_tree_entry_byindex(tree, i)) ||
1323             !(entryname = git_tree_entry_name(entry)))
1324             return -1;
1325         joinpath(entrypath, sizeof(entrypath), path, entryname);
1326 
1327         r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1328                  entrypath);
1329         if (r < 0 || (size_t)r >= sizeof(filepath))
1330             errx(1, "path truncated: 'file/%s.html'", entrypath);
1331         rf = snprintf(rawpath, sizeof(rawpath), "raw/%s",
1332                  entrypath);
1333         if (rf < 0 || (size_t)rf >= sizeof(rawpath))
1334             errx(1, "path truncated: 'raw/%s'", entrypath);
1335 
1336         if (!git_tree_entry_to_object(&obj, repo, entry)) {
1337             switch (git_object_type(obj)) {
1338             case GIT_OBJ_BLOB:
1339                 is_obj_tree = 0;
1340                 filesize = git_blob_rawsize((git_blob *)obj);
1341                 lc = writeblob(obj, filepath, rawpath, entryname, path, filesize);
1342                 writeblobraw((git_blob *)obj, rawpath, entryname, filesize);
1343                 break;
1344             case GIT_OBJ_TREE:
1345                 continue;
1346             default:
1347                 git_object_free(obj);
1348                 continue;
1349             }
1350 
1351             /* make entire table row clickable */
1352             fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s",
1353                     relpath);
1354             percentencode(fp, filepath, strlen(filepath));
1355             fputs("\'\"><td id=\"file-mode\">", fp);
1356 
1357             fprintf(fp, "<a href=\"%s", relpath);
1358             percentencode(fp, filepath, strlen(filepath));
1359             fputs("\">",fp);
1360             fputs(filemode(git_tree_entry_filemode(entry)), fp);
1361             fputs("</a></td>", fp);
1362 
1363             if (git_object_type(obj) == GIT_OBJ_TREE)
1364                 fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath);
1365             else
1366                 fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath);
1367 
1368             percentencode(fp, filepath, strlen(filepath));
1369             fputs("\">", fp);
1370 
1371             xmlencode(fp, entryname, strlen(entryname));
1372 
1373             fputs("</a></td>", fp);
1374 
1375             if (lc > 0) {
1376                 fputs("<td id=\"file-size\" class=\"num\">", fp);
1377                 fprintf(fp, "<a href=\"%s", relpath);
1378                 percentencode(fp, filepath, strlen(filepath));
1379                 fputs("\">", fp);
1380                 fprintf(fp, "%zuL", lc);
1381             }
1382             else if (!is_obj_tree) {
1383                 fputs("<td id=\"file-size\" class=\"num\">", fp);
1384                 fprintf(fp, "<a href=\"%s", relpath);
1385                 percentencode(fp, filepath, strlen(filepath));
1386                 fputs("\">", fp);
1387                 fprintf(fp, "%zuB", filesize);
1388             }
1389             else if (is_obj_tree) {
1390                 fputs("<td id=\"dir-size\">", fp);
1391                 fprintf(fp, "<a href=\"%s", relpath);
1392                 percentencode(fp, filepath, strlen(filepath));
1393                 fputs("\">.", fp);
1394             }
1395 
1396             fputs("</a></td></tr>\n", fp);
1397             git_object_free(obj);
1398 
1399         } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1400             /* commit object in tree is a submodule */
1401             fputs("<tr><td id=\"file-mode\">m---------</td>", fp);
1402             fprintf(fp, "<td><a href=\"%sfile/.gitmodules.html\">", relpath);
1403             xmlencode(fp, entrypath, strlen(entrypath));
1404             fputs("</a> @ ", fp);
1405             git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1406             xmlencode(fp, oid, strlen(oid));
1407             fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
1408         }
1409     }
1410 
1411     fputs("</tbody></table>", fp);
1412     return 0;
1413 }
1414 
1415 int
1416 writefiles(FILE *fp, const git_oid *id)
1417 {
1418     git_tree *tree = NULL;
1419     git_commit *commit = NULL;
1420     int ret = -1;
1421 
1422     if (!git_commit_lookup(&commit, repo, id) &&
1423         !git_commit_tree(&tree, commit))
1424         ret = writefilestree(fp, tree, "");
1425 
1426     git_commit_free(commit);
1427     git_tree_free(tree);
1428 
1429     return ret;
1430 }
1431 
1432 int
1433 writerefs(FILE *fp)
1434 {
1435     struct referenceinfo *ris = NULL;
1436     struct commitinfo *ci;
1437     size_t count, i, j, refcount;
1438     const char *titles[] = { "Branches", "Tags" };
1439     const char *ids[] = { "branches", "tags" };
1440     const char *s;
1441 
1442     if (getrefs(&ris, &refcount) == -1)
1443         return -1;
1444 
1445     for (i = 0, j = 0, count = 0; i < refcount; i++) {
1446         if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1447             if (count)
1448                 fputs("</tbody></table><br/>\n", fp);
1449             count = 0;
1450             j = 1;
1451         }
1452 
1453         /* print header if it has an entry (first). */
1454         if (++count == 1) {
1455             fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
1456                         "<thead id=\"legends\">\n<tr><td><b>Name</b></td>"
1457                     "<td><b>Last commit date</b></td>"
1458                     "<td><b>Author</b></td>\n</tr>\n"
1459                     "</thead><tbody>\n",
1460                      titles[j], ids[j]);
1461         }
1462 
1463         ci = ris[i].ci;
1464         s = git_reference_shorthand(ris[i].ref);
1465 
1466         fputs("<tr><td>", fp);
1467         xmlencode(fp, s, strlen(s));
1468         fputs("</td><td>", fp);
1469         if (ci->author)
1470             printtimeshort(fp, &(ci->author->when));
1471         fputs("</td><td>", fp);
1472         if (ci->author)
1473             xmlencode(fp, ci->author->name, strlen(ci->author->name));
1474         fputs("</td></tr>\n", fp);
1475     }
1476     /* table footer */
1477     if (count)
1478         fputs("</tbody></table><br/>\n", fp);
1479 
1480     for (i = 0; i < refcount; i++) {
1481         commitinfo_free(ris[i].ci);
1482         git_reference_free(ris[i].ref);
1483     }
1484     free(ris);
1485 
1486     return 0;
1487 }
1488 
1489 void
1490 usage(char *argv0)
1491 {
1492     fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
1493             "[-u baseurl] repodir\n", argv0);
1494     exit(1);
1495 }
1496 
1497 void
1498 process_output_md(const char* text, unsigned int size, void* fp)
1499 {
1500     fprintf((FILE *)fp, "%.*s", size, text);
1501 }
1502 
1503 int
1504 main(int argc, char *argv[])
1505 {
1506     git_object *obj = NULL;
1507     const git_oid *head = NULL;
1508     mode_t mask;
1509     FILE *fp, *fpread;
1510     char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1511     char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1512     size_t n;
1513     int i, fd, r;
1514 
1515     for (i = 1; i < argc; i++) {
1516         if (argv[i][0] != '-') {
1517             if (repodir)
1518                 usage(argv[0]);
1519             repodir = argv[i];
1520         } else if (argv[i][1] == 'c') {
1521             if (nlogcommits > 0 || i + 1 >= argc)
1522                 usage(argv[0]);
1523             cachefile = argv[++i];
1524         } else if (argv[i][1] == 'l') {
1525             if (cachefile || i + 1 >= argc)
1526                 usage(argv[0]);
1527             errno = 0;
1528             nlogcommits = strtoll(argv[++i], &p, 10);
1529             if (argv[i][0] == '\0' || *p != '\0' ||
1530                 nlogcommits <= 0 || errno)
1531                 usage(argv[0]);
1532         } else if (argv[i][1] == 'u') {
1533             if (i + 1 >= argc)
1534                 usage(argv[0]);
1535             baseurl = argv[++i];
1536         }
1537     }
1538     if (!repodir)
1539         usage(argv[0]);
1540 
1541     if (!realpath(repodir, repodirabs))
1542         err(1, "realpath");
1543 
1544     /* do not search outside the git repository:
1545        GIT_CONFIG_LEVEL_APP is the highest level currently */
1546     git_libgit2_init();
1547     for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
1548         git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
1549     /* do not require the git repository to be owned by the current user */
1550     git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
1551 
1552 #ifdef __OpenBSD__
1553     if (unveil(repodir, "r") == -1)
1554         err(1, "unveil: %s", repodir);
1555     if (unveil(".", "rwc") == -1)
1556         err(1, "unveil: .");
1557     if (cachefile && unveil(cachefile, "rwc") == -1)
1558         err(1, "unveil: %s", cachefile);
1559 
1560     if (cachefile) {
1561         if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1562             err(1, "pledge");
1563     } else {
1564         if (pledge("stdio rpath wpath cpath", NULL) == -1)
1565             err(1, "pledge");
1566     }
1567 #endif
1568 
1569     if (git_repository_open_ext(&repo, repodir,
1570         GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1571         fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1572         return 1;
1573     }
1574 
1575     /* find HEAD */
1576     if (!git_revparse_single(&obj, repo, "HEAD"))
1577         head = git_object_id(obj);
1578     git_object_free(obj);
1579 
1580     /* use directory name as name */
1581     if ((name = strrchr(repodirabs, '/')))
1582         name++;
1583     else
1584         name = "";
1585 
1586     /* strip .git suffix */
1587     if (!(strippedname = strdup(name)))
1588         err(1, "strdup");
1589     if ((p = strrchr(strippedname, '.')))
1590         if (!strcmp(p, ".git"))
1591             *p = '\0';
1592 
1593     printf("%s\n", strippedname);
1594 
1595     /* read description or .git/description */
1596     joinpath(path, sizeof(path), repodir, "description");
1597     if (!(fpread = fopen(path, "r"))) {
1598         joinpath(path, sizeof(path), repodir, ".git/description");
1599         fpread = fopen(path, "r");
1600     }
1601     if (fpread) {
1602         if (!fgets(description, sizeof(description), fpread))
1603             description[0] = '\0';
1604         checkfileerror(fpread, path, 'r');
1605         fclose(fpread);
1606     }
1607 
1608     /* read url or .git/url */
1609     joinpath(path, sizeof(path), repodir, "url");
1610     if (!(fpread = fopen(path, "r"))) {
1611         joinpath(path, sizeof(path), repodir, ".git/url");
1612         fpread = fopen(path, "r");
1613     }
1614     if (fpread) {
1615         if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1616             cloneurl[0] = '\0';
1617         checkfileerror(fpread, path, 'r');
1618         fclose(fpread);
1619         cloneurl[strcspn(cloneurl, "\n")] = '\0';
1620     }
1621 
1622     /* check LICENSE */
1623     for (i = 0; i < LEN(licensefiles) && !license; i++) {
1624         if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1625             git_object_type(obj) == GIT_OBJ_BLOB)
1626             license = licensefiles[i] + strlen("HEAD:");
1627         git_object_free(obj);
1628     }
1629 
1630     /* check README */
1631     for (i = 0; i < LEN(readmefiles) && !readme; i++) {
1632         if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1633             git_object_type(obj) == GIT_OBJ_BLOB)
1634             readme = readmefiles[i] + strlen("HEAD:");
1635             r = i;
1636         git_object_free(obj);
1637     }
1638 
1639     if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1640         git_object_type(obj) == GIT_OBJ_BLOB)
1641         submodules = ".gitmodules";
1642     git_object_free(obj);
1643 
1644     /* about page */
1645     if (readme) {
1646         fp = efopen("readme.html", "w");
1647         writeheader(fp, "README");
1648 
1649         git_revparse_single(&obj, repo, readmefiles[r]);
1650         const char *s = git_blob_rawcontent((git_blob *)obj);
1651         if (r == 1) {
1652             git_off_t len = git_blob_rawsize((git_blob *)obj);
1653             fputs("<div id=\"readme\">", fp);
1654             if (md_html(s, len, process_output_md, fp, MD_FLAG_TABLES | MD_FLAG_TASKLISTS |
1655                 MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS, 0))
1656                 err(1, "error parsing markdown");
1657             fputs("</div>\n", fp);
1658         } else {
1659             fputs("<pre id=\"readme\">", fp);
1660             xmlencode(fp, s, strlen(s));
1661             fputs("</pre>\n", fp);
1662         }
1663         git_object_free(obj);
1664         writefooter(fp);
1665         fclose(fp);
1666     }
1667 
1668     /* log for HEAD */
1669     fp = efopen("log.html", "w");
1670     relpath = "";
1671     mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1672     writeheader(fp, "Commits");
1673     fputs("<div id=\"content\">\n", fp);
1674     fputs("<table id=\"log\"><thead id=\"legends\">\n<tr>"
1675           "<td id=\"log-date\"><b>Date</b></td>"
1676           "<td id=\"log-summary\"><b>Commit message</b></td>"
1677           "<td id=\"log-author\"><b>Author</b></td>"
1678           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>Files</b></td>"
1679           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>+</b></td>"
1680           "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
1681 
1682     if (cachefile && head) {
1683         /* read from cache file (does not need to exist) */
1684         if ((rcachefp = fopen(cachefile, "r"))) {
1685             if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1686                 errx(1, "%s: no object id", cachefile);
1687             if (git_oid_fromstr(&lastoid, lastoidstr))
1688                 errx(1, "%s: invalid object id", cachefile);
1689         }
1690 
1691         /* write log to (temporary) cache */
1692         if ((fd = mkstemp(tmppath)) == -1)
1693             err(1, "mkstemp");
1694         if (!(wcachefp = fdopen(fd, "w")))
1695             err(1, "fdopen: '%s'", tmppath);
1696         /* write last commit id (HEAD) */
1697         git_oid_tostr(buf, sizeof(buf), head);
1698         fprintf(wcachefp, "%s\n", buf);
1699 
1700         writelog(fp, head);
1701 
1702         if (rcachefp) {
1703             /* append previous log to log.html and the new cache */
1704             while (!feof(rcachefp)) {
1705                 n = fread(buf, 1, sizeof(buf), rcachefp);
1706                 if (ferror(rcachefp))
1707                     break;
1708                 if (fwrite(buf, 1, n, fp) != n ||
1709                     fwrite(buf, 1, n, wcachefp) != n)
1710                         break;
1711             }
1712             checkfileerror(rcachefp, cachefile, 'r');
1713             fclose(rcachefp);
1714         }
1715         checkfileerror(wcachefp, tmppath, 'w');
1716         fclose(wcachefp);
1717     } else {
1718         if (head)
1719             writelog(fp, head);
1720     }
1721 
1722     fputs("</tbody></table>", fp);
1723     writefooter(fp);
1724     checkfileerror(fp, "log.html", 'w');
1725     fclose(fp);
1726 
1727     /* files for HEAD */
1728     fp = efopen("files.html", "w");
1729     writeheader(fp, "Files");
1730     if (head)
1731         writefiles(fp, head);
1732     if (readme) {
1733         git_revparse_single(&obj, repo, readmefiles[r]);
1734         const char *s = git_blob_rawcontent((git_blob *)obj);
1735         if (r == 1) {
1736             git_off_t len = git_blob_rawsize((git_blob *)obj);
1737             fputs("<div id=\"readme\">", fp);
1738             if (md_html(s, len, process_output_md, fp, MD_FLAG_TABLES | MD_FLAG_TASKLISTS |
1739                 MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS, 0))
1740                 err(1, "error parsing markdown");
1741             fputs("</div>\n", fp);
1742         } else {
1743             fputs("<pre id=\"readme\">", fp);
1744             xmlencode(fp, s, strlen(s));
1745             fputs("</pre>\n", fp);
1746         }
1747         git_object_free(obj);
1748     }
1749     writefooter(fp);
1750     checkfileerror(fp, "files.html", 'w');
1751     fclose(fp);
1752 
1753     /* summary page with branches and tags */
1754     fp = efopen("refs.html", "w");
1755     writeheader(fp, "Refs");
1756     fputs("<div id=\"content\">\n<div id=\"refs\">\n", fp);
1757     writerefs(fp);
1758     writefooter(fp);
1759     checkfileerror(fp, "refs.html", 'w');
1760     fclose(fp);
1761 
1762     /* Atom feed */
1763     fp = efopen("atom.xml", "w");
1764     writeatom(fp, 1);
1765     checkfileerror(fp, "atom.xml", 'w');
1766     fclose(fp);
1767 
1768     /* Atom feed for tags / releases */
1769     fp = efopen("tags.xml", "w");
1770     writeatom(fp, 0);
1771     checkfileerror(fp, "tags.xml", 'w');
1772     fclose(fp);
1773 
1774     /* rename new cache file on success */
1775     if (cachefile && head) {
1776         if (rename(tmppath, cachefile))
1777             err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1778         umask((mask = umask(0)));
1779         if (chmod(cachefile,
1780             (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1781             err(1, "chmod: '%s'", cachefile);
1782     }
1783 
1784     /* cleanup */
1785     git_repository_free(repo);
1786     git_libgit2_shutdown();
1787 
1788     return 0;
1789 }