stagit

My fork of stagit

git clone git://git.shimmy1996.com/stagit.git

stagit.c (34116B)

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