stagit

git site generator
Contents

stagit.c

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