dotfiles

My PC Dotfiles & Website

site/git/stagit/stagit.c (40724B)


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