stagit


stagit

~ Unnamed repository; edit this file 'description' to name the repository.
log | files | refs | archive | README | LICENSE

stagit.c (42996B)

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