stagit

stagit.c

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