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