stagit

static git repository generator

git clone https://9o.is/git/stagit.git

stagit.c

(38756B)


      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 	fputs("</title>\n<link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\" />\n", fp);
    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 	fputs("<meta name=\"darkreader-lock\">\n", fp);
    522 	fputs("<link rel=\"stylesheet\" type=\"text/css\" href=\"/style.css\" />\n", fp);
    523 	fputs("<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n", fp);
    524 	fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n", fp);
    525 	fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n", fp);
    526 	fputs("<link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n", fp);
    527 	fputs("<link rel=\"manifest\" href=\"/site.webmanifest\" />\n", fp);
    528 	fputs("</head>\n<body>\n", fp);
    529 	fputs("<nav aria-label=\"Main\"><div class=\"nav-inner\"><ul>\n", fp);
    530 	fputs("<li><a href=\"/\">\n", fp);
    531 	fputs("<svg aria-hidden=\"true\" focusable=\"false\" viewBox=\"60 140 395 235\" width=\"1.65em\" height=\"1em\">\n", fp);
    532 	fputs("<path fill=\"#89B4F9\" d=\"M64.835068,227.999908 C64.835060,201.044281 64.835060,174.588654 64.835060,147.827759 C119.952759,147.827759 174.637543,147.827759 229.576569,147.827759 C229.576569,221.044449 229.576569,293.936920 229.576569,367.162964 C211.413635,367.162964 193.487869,367.162964 174.971115,367.162964 C174.971115,349.200623 174.971115,331.143860 174.971115,312.670959 C138.115814,312.670959 101.845200,312.670959 64.835068,312.670959 C64.835068,284.334381 64.835068,256.417145 64.835068,227.999908 M119.817665,228.499924 C119.817665,237.981033 119.817665,247.462143 119.817665,257.209167 C138.278030,257.209167 156.353180,257.209167 174.638031,257.209167 C174.638031,238.965073 174.638031,221.035233 174.638031,202.790985 C156.353516,202.790985 138.278290,202.790985 119.817665,202.790985 C119.817665,211.203720 119.817665,219.351807 119.817665,228.499924 z\"/>\n", fp);
    533 	fputs("<path fill=\"#89B4F9\" d=\"M430.999786,202.503693 C437.444763,202.503662 443.389832,202.503662 449.575439,202.503662 C449.575439,257.714081 449.575439,312.279022 449.575439,367.170105 C394.745087,367.170105 340.163940,367.170105 285.208710,367.170105 C285.208710,312.545074 285.208710,257.842651 285.208710,202.503723 C333.674988,202.503723 382.087433,202.503723 430.999786,202.503693 M380.489105,257.476349 C367.047089,257.476349 353.605072,257.476349 340.041931,257.476349 C340.041931,276.088287 340.041931,294.037903 340.041931,312.185791 C358.345459,312.185791 376.423187,312.185791 394.602081,312.185791 C394.602081,293.916534 394.602081,275.986145 394.602081,257.476410 C390.074432,257.476410 385.778351,257.476410 380.489105,257.476349 z\"/>\n", fp);
    534 	fputs("</svg>\n", fp);
    535 	fputs("Blog</a></li>\n", fp);
    536 	fputs("<li><a href=\"/git\">Git</a></li>\n", fp);
    537 	fputs("</ul></div></nav>\n", fp);
    538 	fputs("<main class=\"git\">\n", fp);
    539 	fputs("<header class=\"main\">\n<h1>", fp);
    540 	xmlencode(fp, strippedname, strlen(strippedname));
    541 	fputs("</h1><p>", fp);
    542 	xmlencode(fp, description, strlen(description));
    543 	fputs("</p>", fp);
    544 	if (cloneurl[0]) {
    545 		fputs("<p>git clone <a href=\"", fp);
    546 		xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
    547 		fputs("\">", fp);
    548 		xmlencode(fp, cloneurl, strlen(cloneurl));
    549 		fputs("</a></p>", fp);
    550 	}
    551 	fputs("<nav aria-label=\"Repository\"><ul>\n", fp);
    552 	fprintf(fp, "<li><a href=\"%slog.html\">Log</a></li>", relpath);
    553 	fprintf(fp, "<li><a href=\"%sfiles.html\">Files</a></li>", relpath);
    554 	fprintf(fp, "<li><a href=\"%srefs.html\">Refs</a></li>", relpath);
    555 	if (submodules)
    556 		fprintf(fp, "<li><a href=\"%sfile/%s.html\">Submodules</a></li>",
    557 		        relpath, submodules);
    558 	if (readme)
    559 		fprintf(fp, "<li><a href=\"%sfile/%s.html\">README</a></li>",
    560 		        relpath, readme);
    561 	if (license)
    562 		fprintf(fp, "<li><a href=\"%sfile/%s.html\">LICENSE</a></li>",
    563 		        relpath, license);
    564 	fputs("</ul></nav>\n</header>\n\n", fp);
    565 }
    566 
    567 void
    568 writefooter(FILE *fp)
    569 {
    570 	fputs("</main>\n</body>\n</html>\n", fp);
    571 }
    572 
    573 size_t
    574 writeblobhtml(FILE *fp, const git_blob *blob)
    575 {
    576 	size_t n = 0, i, len, prev;
    577 	const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> ";
    578 	const char *s = git_blob_rawcontent(blob);
    579 
    580 	len = git_blob_rawsize(blob);
    581 	fputs("<pre id=\"blob\">\n", fp);
    582 
    583 	if (len > 0) {
    584 		for (i = 0, prev = 0; i < len; i++) {
    585 			if (s[i] != '\n')
    586 				continue;
    587 			n++;
    588 			fprintf(fp, nfmt, n, n, n);
    589 			xmlencodeline(fp, &s[prev], i - prev + 1);
    590 			putc('\n', fp);
    591 			prev = i + 1;
    592 		}
    593 		/* trailing data */
    594 		if ((len - prev) > 0) {
    595 			n++;
    596 			fprintf(fp, nfmt, n, n, n);
    597 			xmlencodeline(fp, &s[prev], len - prev);
    598 		}
    599 	}
    600 
    601 	fputs("</pre>\n", fp);
    602 
    603 	return n;
    604 }
    605 
    606 void
    607 printcommit(FILE *fp, struct commitinfo *ci)
    608 {
    609 	fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
    610 		relpath, ci->oid, ci->oid);
    611 
    612 	if (ci->parentoid[0])
    613 		fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
    614 			relpath, ci->parentoid, ci->parentoid);
    615 
    616 	if (ci->author) {
    617 		fputs("<b>Author:</b> ", fp);
    618 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
    619 		fputs(" &lt;<a href=\"mailto:", fp);
    620 		xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
    621 		fputs("\">", fp);
    622 		xmlencode(fp, ci->author->email, strlen(ci->author->email));
    623 		fputs("</a>&gt;\n<b>Date:</b>   ", fp);
    624 		printtime(fp, &(ci->author->when));
    625 		putc('\n', fp);
    626 	}
    627 	if (ci->msg) {
    628 		putc('\n', fp);
    629 		xmlencode(fp, ci->msg, strlen(ci->msg));
    630 		putc('\n', fp);
    631 	}
    632 }
    633 
    634 void
    635 printshowfile(FILE *fp, struct commitinfo *ci)
    636 {
    637 	const git_diff_delta *delta;
    638 	const git_diff_hunk *hunk;
    639 	const git_diff_line *line;
    640 	git_patch *patch;
    641 	size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
    642 	char linestr[80];
    643 	int c;
    644 
    645 	printcommit(fp, ci);
    646 
    647 	if (!ci->deltas)
    648 		return;
    649 
    650 	if (ci->filecount > 1000   ||
    651 	    ci->ndeltas   > 1000   ||
    652 	    ci->addcount  > 100000 ||
    653 	    ci->delcount  > 100000) {
    654 		fputs("Diff is too large, output suppressed.\n", fp);
    655 		return;
    656 	}
    657 
    658 	/* diff stat */
    659 	fputs("<b>Diffstat:</b>\n<table>", fp);
    660 	for (i = 0; i < ci->ndeltas; i++) {
    661 		delta = git_patch_get_delta(ci->deltas[i]->patch);
    662 
    663 		switch (delta->status) {
    664 		case GIT_DELTA_ADDED:      c = 'A'; break;
    665 		case GIT_DELTA_COPIED:     c = 'C'; break;
    666 		case GIT_DELTA_DELETED:    c = 'D'; break;
    667 		case GIT_DELTA_MODIFIED:   c = 'M'; break;
    668 		case GIT_DELTA_RENAMED:    c = 'R'; break;
    669 		case GIT_DELTA_TYPECHANGE: c = 'T'; break;
    670 		default:                   c = ' '; break;
    671 		}
    672 		if (c == ' ')
    673 			fprintf(fp, "<tr><td>%c", c);
    674 		else
    675 			fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
    676 
    677 		fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
    678 		xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
    679 		if (strcmp(delta->old_file.path, delta->new_file.path)) {
    680 			fputs(" -&gt; ", fp);
    681 			xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
    682 		}
    683 
    684 		add = ci->deltas[i]->addcount;
    685 		del = ci->deltas[i]->delcount;
    686 		changed = add + del;
    687 		total = sizeof(linestr) - 2;
    688 		if (changed > total) {
    689 			if (add)
    690 				add = ((float)total / changed * add) + 1;
    691 			if (del)
    692 				del = ((float)total / changed * del) + 1;
    693 		}
    694 		memset(&linestr, '+', add);
    695 		memset(&linestr[add], '-', del);
    696 
    697 		fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
    698 		        ci->deltas[i]->addcount + ci->deltas[i]->delcount);
    699 		fwrite(&linestr, 1, add, fp);
    700 		fputs("</span><span class=\"d\">", fp);
    701 		fwrite(&linestr[add], 1, del, fp);
    702 		fputs("</span></td></tr>\n", fp);
    703 	}
    704 	fprintf(fp, "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
    705 		ci->filecount, ci->filecount == 1 ? "" : "s",
    706 	        ci->addcount,  ci->addcount  == 1 ? "" : "s",
    707 	        ci->delcount,  ci->delcount  == 1 ? "" : "s");
    708 
    709 	fputs("<hr/>", fp);
    710 
    711 	for (i = 0; i < ci->ndeltas; i++) {
    712 		patch = ci->deltas[i]->patch;
    713 		delta = git_patch_get_delta(patch);
    714 		fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
    715 		percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
    716 		fputs(".html\">", fp);
    717 		xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
    718 		fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
    719 		percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
    720 		fprintf(fp, ".html\">");
    721 		xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
    722 		fprintf(fp, "</a></b>\n");
    723 
    724 		/* check binary data */
    725 		if (delta->flags & GIT_DIFF_FLAG_BINARY) {
    726 			fputs("Binary files differ.\n", fp);
    727 			continue;
    728 		}
    729 
    730 		nhunks = git_patch_num_hunks(patch);
    731 		for (j = 0; j < nhunks; j++) {
    732 			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
    733 				break;
    734 
    735 			fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
    736 			xmlencode(fp, hunk->header, hunk->header_len);
    737 			fputs("</a>", fp);
    738 
    739 			for (k = 0; ; k++) {
    740 				if (git_patch_get_line_in_hunk(&line, patch, j, k))
    741 					break;
    742 				if (line->old_lineno == -1)
    743 					fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
    744 						i, j, k, i, j, k);
    745 				else if (line->new_lineno == -1)
    746 					fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
    747 						i, j, k, i, j, k);
    748 				else
    749 					putc(' ', fp);
    750 				xmlencodeline(fp, line->content, line->content_len);
    751 				putc('\n', fp);
    752 				if (line->old_lineno == -1 || line->new_lineno == -1)
    753 					fputs("</a>", fp);
    754 			}
    755 		}
    756 	}
    757 }
    758 
    759 void
    760 writelogline(FILE *fp, struct commitinfo *ci)
    761 {
    762 	fputs("<tr><td>", fp);
    763 	if (ci->author)
    764 		printtimeshort(fp, &(ci->author->when));
    765 	fputs("</td><td>", fp);
    766 	if (ci->summary) {
    767 		fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
    768 		xmlencode(fp, ci->summary, strlen(ci->summary));
    769 		fputs("</a>", fp);
    770 	}
    771 	fputs("</td><td>", fp);
    772 	if (ci->author)
    773 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
    774 	fputs("</td><td class=\"num\" align=\"right\">", fp);
    775 	fprintf(fp, "%zu", ci->filecount);
    776 	fputs("</td><td class=\"num\" align=\"right\">", fp);
    777 	fprintf(fp, "+%zu", ci->addcount);
    778 	fputs("</td><td class=\"num\" align=\"right\">", fp);
    779 	fprintf(fp, "-%zu", ci->delcount);
    780 	fputs("</td></tr>\n", fp);
    781 }
    782 
    783 int
    784 writelog(FILE *fp, const git_oid *oid)
    785 {
    786 	struct commitinfo *ci;
    787 	git_revwalk *w = NULL;
    788 	git_oid id;
    789 	char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
    790 	FILE *fpfile;
    791 	size_t remcommits = 0;
    792 	int r;
    793 
    794 	git_revwalk_new(&w, repo);
    795 	git_revwalk_push(w, oid);
    796 
    797 	while (!git_revwalk_next(&id, w)) {
    798 		relpath = "";
    799 
    800 		if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
    801 			break;
    802 
    803 		git_oid_tostr(oidstr, sizeof(oidstr), &id);
    804 		r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
    805 		if (r < 0 || (size_t)r >= sizeof(path))
    806 			errx(1, "path truncated: 'commit/%s.html'", oidstr);
    807 		r = access(path, F_OK);
    808 
    809 		/* optimization: if there are no log lines to write and
    810 		   the commit file already exists: skip the diffstat */
    811 		if (!nlogcommits) {
    812 			remcommits++;
    813 			if (!r)
    814 				continue;
    815 		}
    816 
    817 		if (!(ci = commitinfo_getbyoid(&id)))
    818 			break;
    819 		/* diffstat: for stagit HTML required for the log.html line */
    820 		if (commitinfo_getstats(ci) == -1)
    821 			goto err;
    822 
    823 		if (nlogcommits != 0) {
    824 			writelogline(fp, ci);
    825 			if (nlogcommits > 0)
    826 				nlogcommits--;
    827 		}
    828 
    829 		if (cachefile)
    830 			writelogline(wcachefp, ci);
    831 
    832 		/* check if file exists if so skip it */
    833 		if (r) {
    834 			relpath = "../";
    835 			fpfile = efopen(path, "w");
    836 			writeheader(fpfile, ci->summary);
    837 			fputs("<pre>", fpfile);
    838 			printshowfile(fpfile, ci);
    839 			fputs("</pre>\n", fpfile);
    840 			writefooter(fpfile);
    841 			checkfileerror(fpfile, path, 'w');
    842 			fclose(fpfile);
    843 		}
    844 err:
    845 		commitinfo_free(ci);
    846 	}
    847 	git_revwalk_free(w);
    848 
    849 	if (nlogcommits == 0 && remcommits != 0) {
    850 		fprintf(fp, "<tr><td></td><td colspan=\"5\">"
    851 		        "%zu more commits remaining, fetch the repository"
    852 		        "</td></tr>\n", remcommits);
    853 	}
    854 
    855 	relpath = "";
    856 
    857 	return 0;
    858 }
    859 
    860 void
    861 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
    862 {
    863 	fputs("<entry>\n", fp);
    864 
    865 	fprintf(fp, "<id>%s</id>\n", ci->oid);
    866 	if (ci->author) {
    867 		fputs("<published>", fp);
    868 		printtimez(fp, &(ci->author->when));
    869 		fputs("</published>\n", fp);
    870 	}
    871 	if (ci->committer) {
    872 		fputs("<updated>", fp);
    873 		printtimez(fp, &(ci->committer->when));
    874 		fputs("</updated>\n", fp);
    875 	}
    876 	if (ci->summary) {
    877 		fputs("<title>", fp);
    878 		if (tag && tag[0]) {
    879 			fputs("[", fp);
    880 			xmlencode(fp, tag, strlen(tag));
    881 			fputs("] ", fp);
    882 		}
    883 		xmlencode(fp, ci->summary, strlen(ci->summary));
    884 		fputs("</title>\n", fp);
    885 	}
    886 	fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
    887 	        baseurl, ci->oid);
    888 
    889 	if (ci->author) {
    890 		fputs("<author>\n<name>", fp);
    891 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
    892 		fputs("</name>\n<email>", fp);
    893 		xmlencode(fp, ci->author->email, strlen(ci->author->email));
    894 		fputs("</email>\n</author>\n", fp);
    895 	}
    896 
    897 	fputs("<content>", fp);
    898 	fprintf(fp, "commit %s\n", ci->oid);
    899 	if (ci->parentoid[0])
    900 		fprintf(fp, "parent %s\n", ci->parentoid);
    901 	if (ci->author) {
    902 		fputs("Author: ", fp);
    903 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
    904 		fputs(" &lt;", fp);
    905 		xmlencode(fp, ci->author->email, strlen(ci->author->email));
    906 		fputs("&gt;\nDate:   ", fp);
    907 		printtime(fp, &(ci->author->when));
    908 		putc('\n', fp);
    909 	}
    910 	if (ci->msg) {
    911 		putc('\n', fp);
    912 		xmlencode(fp, ci->msg, strlen(ci->msg));
    913 	}
    914 	fputs("\n</content>\n</entry>\n", fp);
    915 }
    916 
    917 int
    918 writeatom(FILE *fp, int all)
    919 {
    920 	struct referenceinfo *ris = NULL;
    921 	size_t refcount = 0;
    922 	struct commitinfo *ci;
    923 	git_revwalk *w = NULL;
    924 	git_oid id;
    925 	size_t i, m = 100; /* last 'm' commits */
    926 
    927 	fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    928 	      "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
    929 	xmlencode(fp, strippedname, strlen(strippedname));
    930 	fputs(", branch HEAD</title>\n<subtitle>", fp);
    931 	xmlencode(fp, description, strlen(description));
    932 	fputs("</subtitle>\n", fp);
    933 
    934 	/* all commits or only tags? */
    935 	if (all) {
    936 		git_revwalk_new(&w, repo);
    937 		git_revwalk_push_head(w);
    938 		for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
    939 			if (!(ci = commitinfo_getbyoid(&id)))
    940 				break;
    941 			printcommitatom(fp, ci, "");
    942 			commitinfo_free(ci);
    943 		}
    944 		git_revwalk_free(w);
    945 	} else if (getrefs(&ris, &refcount) != -1) {
    946 		/* references: tags */
    947 		for (i = 0; i < refcount; i++) {
    948 			if (git_reference_is_tag(ris[i].ref))
    949 				printcommitatom(fp, ris[i].ci,
    950 				                git_reference_shorthand(ris[i].ref));
    951 
    952 			commitinfo_free(ris[i].ci);
    953 			git_reference_free(ris[i].ref);
    954 		}
    955 		free(ris);
    956 	}
    957 
    958 	fputs("</feed>\n", fp);
    959 
    960 	return 0;
    961 }
    962 
    963 size_t
    964 writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
    965 {
    966 	char tmp[PATH_MAX] = "", *d;
    967 	const char *p;
    968 	size_t lc = 0;
    969 	FILE *fp;
    970 
    971 	if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
    972 		errx(1, "path truncated: '%s'", fpath);
    973 	if (!(d = dirname(tmp)))
    974 		err(1, "dirname");
    975 	if (mkdirp(d))
    976 		return -1;
    977 
    978 	for (p = fpath, tmp[0] = '\0'; *p; p++) {
    979 		if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
    980 			errx(1, "path truncated: '../%s'", tmp);
    981 	}
    982 	relpath = tmp;
    983 
    984 	fp = efopen(fpath, "w");
    985 	writeheader(fp, filename);
    986 	fputs("<section class=\"git-file\">\n", fp);
    987 	fputs("<h2>", fp);
    988 	xmlencode(fp, filename, strlen(filename));
    989 	fputs("</h2>\n", fp);
    990 	fputs("<p>", fp);
    991 	fprintf(fp, " (%zuB)", filesize);
    992 	fputs("</p><hr/>\n", fp);
    993 
    994 	if (git_blob_is_binary((git_blob *)obj))
    995 		fputs("<p>Binary file.</p>\n", fp);
    996 	else
    997 		lc = writeblobhtml(fp, (git_blob *)obj);
    998 
    999 	fputs("</section>\n", fp);
   1000 	writefooter(fp);
   1001 	checkfileerror(fp, fpath, 'w');
   1002 	fclose(fp);
   1003 
   1004 	relpath = "";
   1005 
   1006 	return lc;
   1007 }
   1008 
   1009 const char *
   1010 filemode(git_filemode_t m)
   1011 {
   1012 	static char mode[11];
   1013 
   1014 	memset(mode, '-', sizeof(mode) - 1);
   1015 	mode[10] = '\0';
   1016 
   1017 	if (S_ISREG(m))
   1018 		mode[0] = '-';
   1019 	else if (S_ISBLK(m))
   1020 		mode[0] = 'b';
   1021 	else if (S_ISCHR(m))
   1022 		mode[0] = 'c';
   1023 	else if (S_ISDIR(m))
   1024 		mode[0] = 'd';
   1025 	else if (S_ISFIFO(m))
   1026 		mode[0] = 'p';
   1027 	else if (S_ISLNK(m))
   1028 		mode[0] = 'l';
   1029 	else if (S_ISSOCK(m))
   1030 		mode[0] = 's';
   1031 	else
   1032 		mode[0] = '?';
   1033 
   1034 	if (m & S_IRUSR) mode[1] = 'r';
   1035 	if (m & S_IWUSR) mode[2] = 'w';
   1036 	if (m & S_IXUSR) mode[3] = 'x';
   1037 	if (m & S_IRGRP) mode[4] = 'r';
   1038 	if (m & S_IWGRP) mode[5] = 'w';
   1039 	if (m & S_IXGRP) mode[6] = 'x';
   1040 	if (m & S_IROTH) mode[7] = 'r';
   1041 	if (m & S_IWOTH) mode[8] = 'w';
   1042 	if (m & S_IXOTH) mode[9] = 'x';
   1043 
   1044 	if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
   1045 	if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
   1046 	if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
   1047 
   1048 	return mode;
   1049 }
   1050 
   1051 int
   1052 writefilestree(FILE *fp, git_tree *tree, const char *path)
   1053 {
   1054 	const git_tree_entry *entry = NULL;
   1055 	git_object *obj = NULL;
   1056 	const char *entryname;
   1057 	char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8];
   1058 	size_t count, i, lc, filesize;
   1059 	int r, ret;
   1060 
   1061 	count = git_tree_entrycount(tree);
   1062 	for (i = 0; i < count; i++) {
   1063 		if (!(entry = git_tree_entry_byindex(tree, i)) ||
   1064 		    !(entryname = git_tree_entry_name(entry)))
   1065 			return -1;
   1066 		joinpath(entrypath, sizeof(entrypath), path, entryname);
   1067 
   1068 		r = snprintf(filepath, sizeof(filepath), "file/%s.html",
   1069 		         entrypath);
   1070 		if (r < 0 || (size_t)r >= sizeof(filepath))
   1071 			errx(1, "path truncated: 'file/%s.html'", entrypath);
   1072 
   1073 		if (!git_tree_entry_to_object(&obj, repo, entry)) {
   1074 			switch (git_object_type(obj)) {
   1075 			case GIT_OBJ_BLOB:
   1076 				break;
   1077 			case GIT_OBJ_TREE:
   1078 				/* NOTE: recurses */
   1079 				ret = writefilestree(fp, (git_tree *)obj,
   1080 				                     entrypath);
   1081 				git_object_free(obj);
   1082 				if (ret)
   1083 					return ret;
   1084 				continue;
   1085 			default:
   1086 				git_object_free(obj);
   1087 				continue;
   1088 			}
   1089 
   1090 			filesize = git_blob_rawsize((git_blob *)obj);
   1091 			lc = writeblob(obj, filepath, entryname, filesize);
   1092 
   1093 			fputs("<tr><td>", fp);
   1094 			fputs(filemode(git_tree_entry_filemode(entry)), fp);
   1095 			fprintf(fp, "</td><td><a href=\"%s", relpath);
   1096 			percentencode(fp, filepath, strlen(filepath));
   1097 			fputs("\">", fp);
   1098 			xmlencode(fp, entrypath, strlen(entrypath));
   1099 			fputs("</a></td><td class=\"num\" align=\"right\">", fp);
   1100 			if (lc > 0)
   1101 				fprintf(fp, "%zuL", lc);
   1102 			else
   1103 				fprintf(fp, "%zuB", filesize);
   1104 			fputs("</td></tr>\n", fp);
   1105 			git_object_free(obj);
   1106 		} else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
   1107 			/* commit object in tree is a submodule */
   1108 			fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
   1109 				relpath);
   1110 			xmlencode(fp, entrypath, strlen(entrypath));
   1111 			fputs("</a> @ ", fp);
   1112 			git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
   1113 			xmlencode(fp, oid, strlen(oid));
   1114 			fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
   1115 		}
   1116 	}
   1117 
   1118 	return 0;
   1119 }
   1120 
   1121 int
   1122 writefiles(FILE *fp, const git_oid *id)
   1123 {
   1124 	git_tree *tree = NULL;
   1125 	git_commit *commit = NULL;
   1126 	int ret = -1;
   1127 
   1128 	fputs("<table id=\"files\"><thead>\n<tr>"
   1129 	      "<td><b>Mode</b></td><td><b>Name</b></td>"
   1130 	      "<td class=\"num\" align=\"right\"><b>Size</b></td>"
   1131 	      "</tr>\n</thead><tbody>\n", fp);
   1132 
   1133 	if (!git_commit_lookup(&commit, repo, id) &&
   1134 	    !git_commit_tree(&tree, commit))
   1135 		ret = writefilestree(fp, tree, "");
   1136 
   1137 	fputs("</tbody></table>", fp);
   1138 
   1139 	git_commit_free(commit);
   1140 	git_tree_free(tree);
   1141 
   1142 	return ret;
   1143 }
   1144 
   1145 int
   1146 writerefs(FILE *fp)
   1147 {
   1148 	struct referenceinfo *ris = NULL;
   1149 	struct commitinfo *ci;
   1150 	size_t count, i, j, refcount;
   1151 	const char *titles[] = { "Branches", "Tags" };
   1152 	const char *ids[] = { "branches", "tags" };
   1153 	const char *s;
   1154 
   1155 	if (getrefs(&ris, &refcount) == -1)
   1156 		return -1;
   1157 
   1158 	for (i = 0, j = 0, count = 0; i < refcount; i++) {
   1159 		if (j == 0 && git_reference_is_tag(ris[i].ref)) {
   1160 			if (count)
   1161 				fputs("</tbody></table><br/>\n", fp);
   1162 			count = 0;
   1163 			j = 1;
   1164 		}
   1165 
   1166 		/* print header if it has an entry (first). */
   1167 		if (++count == 1) {
   1168 			fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
   1169 		                "<thead>\n<tr><td><b>Name</b></td>"
   1170 			        "<td><b>Last commit date</b></td>"
   1171 			        "<td><b>Author</b></td>\n</tr>\n"
   1172 			        "</thead><tbody>\n",
   1173 			         titles[j], ids[j]);
   1174 		}
   1175 
   1176 		ci = ris[i].ci;
   1177 		s = git_reference_shorthand(ris[i].ref);
   1178 
   1179 		fputs("<tr><td>", fp);
   1180 		xmlencode(fp, s, strlen(s));
   1181 		fputs("</td><td>", fp);
   1182 		if (ci->author)
   1183 			printtimeshort(fp, &(ci->author->when));
   1184 		fputs("</td><td>", fp);
   1185 		if (ci->author)
   1186 			xmlencode(fp, ci->author->name, strlen(ci->author->name));
   1187 		fputs("</td></tr>\n", fp);
   1188 	}
   1189 	/* table footer */
   1190 	if (count)
   1191 		fputs("</tbody></table><br/>\n", fp);
   1192 
   1193 	for (i = 0; i < refcount; i++) {
   1194 		commitinfo_free(ris[i].ci);
   1195 		git_reference_free(ris[i].ref);
   1196 	}
   1197 	free(ris);
   1198 
   1199 	return 0;
   1200 }
   1201 
   1202 void
   1203 usage(char *argv0)
   1204 {
   1205 	fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
   1206 	        "[-u baseurl] repodir\n", argv0);
   1207 	exit(1);
   1208 }
   1209 
   1210 int
   1211 main(int argc, char *argv[])
   1212 {
   1213 	git_object *obj = NULL;
   1214 	const git_oid *head = NULL;
   1215 	mode_t mask;
   1216 	FILE *fp, *fpread;
   1217 	char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
   1218 	char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
   1219 	size_t n;
   1220 	int i, fd;
   1221 
   1222 	for (i = 1; i < argc; i++) {
   1223 		if (argv[i][0] != '-') {
   1224 			if (repodir)
   1225 				usage(argv[0]);
   1226 			repodir = argv[i];
   1227 		} else if (argv[i][1] == 'c') {
   1228 			if (nlogcommits > 0 || i + 1 >= argc)
   1229 				usage(argv[0]);
   1230 			cachefile = argv[++i];
   1231 		} else if (argv[i][1] == 'l') {
   1232 			if (cachefile || i + 1 >= argc)
   1233 				usage(argv[0]);
   1234 			errno = 0;
   1235 			nlogcommits = strtoll(argv[++i], &p, 10);
   1236 			if (argv[i][0] == '\0' || *p != '\0' ||
   1237 			    nlogcommits <= 0 || errno)
   1238 				usage(argv[0]);
   1239 		} else if (argv[i][1] == 'u') {
   1240 			if (i + 1 >= argc)
   1241 				usage(argv[0]);
   1242 			baseurl = argv[++i];
   1243 		}
   1244 	}
   1245 	if (!repodir)
   1246 		usage(argv[0]);
   1247 
   1248 	if (!realpath(repodir, repodirabs))
   1249 		err(1, "realpath");
   1250 
   1251 	/* do not search outside the git repository:
   1252 	   GIT_CONFIG_LEVEL_APP is the highest level currently */
   1253 	git_libgit2_init();
   1254 	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
   1255 		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
   1256 	/* do not require the git repository to be owned by the current user */
   1257 	git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
   1258 
   1259 #ifdef __OpenBSD__
   1260 	if (unveil(repodir, "r") == -1)
   1261 		err(1, "unveil: %s", repodir);
   1262 	if (unveil(".", "rwc") == -1)
   1263 		err(1, "unveil: .");
   1264 	if (cachefile && unveil(cachefile, "rwc") == -1)
   1265 		err(1, "unveil: %s", cachefile);
   1266 
   1267 	if (cachefile) {
   1268 		if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
   1269 			err(1, "pledge");
   1270 	} else {
   1271 		if (pledge("stdio rpath wpath cpath", NULL) == -1)
   1272 			err(1, "pledge");
   1273 	}
   1274 #endif
   1275 
   1276 	if (git_repository_open_ext(&repo, repodir,
   1277 		GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
   1278 		fprintf(stderr, "%s: cannot open repository\n", argv[0]);
   1279 		return 1;
   1280 	}
   1281 
   1282 	/* find HEAD */
   1283 	if (!git_revparse_single(&obj, repo, "HEAD"))
   1284 		head = git_object_id(obj);
   1285 	git_object_free(obj);
   1286 
   1287 	/* use directory name as name */
   1288 	if ((name = strrchr(repodirabs, '/')))
   1289 		name++;
   1290 	else
   1291 		name = "";
   1292 
   1293 	/* strip .git suffix */
   1294 	if (!(strippedname = strdup(name)))
   1295 		err(1, "strdup");
   1296 	if ((p = strrchr(strippedname, '.')))
   1297 		if (!strcmp(p, ".git"))
   1298 			*p = '\0';
   1299 
   1300 	/* read description or .git/description */
   1301 	joinpath(path, sizeof(path), repodir, "description");
   1302 	if (!(fpread = fopen(path, "r"))) {
   1303 		joinpath(path, sizeof(path), repodir, ".git/description");
   1304 		fpread = fopen(path, "r");
   1305 	}
   1306 	if (fpread) {
   1307 		if (!fgets(description, sizeof(description), fpread))
   1308 			description[0] = '\0';
   1309 		checkfileerror(fpread, path, 'r');
   1310 		fclose(fpread);
   1311 	}
   1312 
   1313 	/* read url or .git/url */
   1314 	joinpath(path, sizeof(path), repodir, "url");
   1315 	if (!(fpread = fopen(path, "r"))) {
   1316 		joinpath(path, sizeof(path), repodir, ".git/url");
   1317 		fpread = fopen(path, "r");
   1318 	}
   1319 	if (fpread) {
   1320 		if (!fgets(cloneurl, sizeof(cloneurl), fpread))
   1321 			cloneurl[0] = '\0';
   1322 		checkfileerror(fpread, path, 'r');
   1323 		fclose(fpread);
   1324 		cloneurl[strcspn(cloneurl, "\n")] = '\0';
   1325 	}
   1326 
   1327 	/* check LICENSE */
   1328 	for (i = 0; i < LEN(licensefiles) && !license; i++) {
   1329 		if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
   1330 		    git_object_type(obj) == GIT_OBJ_BLOB)
   1331 			license = licensefiles[i] + strlen("HEAD:");
   1332 		git_object_free(obj);
   1333 	}
   1334 
   1335 	/* check README */
   1336 	for (i = 0; i < LEN(readmefiles) && !readme; i++) {
   1337 		if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
   1338 		    git_object_type(obj) == GIT_OBJ_BLOB)
   1339 			readme = readmefiles[i] + strlen("HEAD:");
   1340 		git_object_free(obj);
   1341 	}
   1342 
   1343 	if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
   1344 	    git_object_type(obj) == GIT_OBJ_BLOB)
   1345 		submodules = ".gitmodules";
   1346 	git_object_free(obj);
   1347 
   1348 	/* log for HEAD */
   1349 	fp = efopen("log.html", "w");
   1350 	relpath = "";
   1351 	mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
   1352 	writeheader(fp, "Log");
   1353 	fputs("<table id=\"log\"><thead>\n<tr><td><b>Date</b></td>"
   1354 	      "<td><b>Commit message</b></td>"
   1355 	      "<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>"
   1356 	      "<td class=\"num\" align=\"right\"><b>+</b></td>"
   1357 	      "<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
   1358 
   1359 	if (cachefile && head) {
   1360 		/* read from cache file (does not need to exist) */
   1361 		if ((rcachefp = fopen(cachefile, "r"))) {
   1362 			if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
   1363 				errx(1, "%s: no object id", cachefile);
   1364 			if (git_oid_fromstr(&lastoid, lastoidstr))
   1365 				errx(1, "%s: invalid object id", cachefile);
   1366 		}
   1367 
   1368 		/* write log to (temporary) cache */
   1369 		if ((fd = mkstemp(tmppath)) == -1)
   1370 			err(1, "mkstemp");
   1371 		if (!(wcachefp = fdopen(fd, "w")))
   1372 			err(1, "fdopen: '%s'", tmppath);
   1373 		/* write last commit id (HEAD) */
   1374 		git_oid_tostr(buf, sizeof(buf), head);
   1375 		fprintf(wcachefp, "%s\n", buf);
   1376 
   1377 		writelog(fp, head);
   1378 
   1379 		if (rcachefp) {
   1380 			/* append previous log to log.html and the new cache */
   1381 			while (!feof(rcachefp)) {
   1382 				n = fread(buf, 1, sizeof(buf), rcachefp);
   1383 				if (ferror(rcachefp))
   1384 					break;
   1385 				if (fwrite(buf, 1, n, fp) != n ||
   1386 				    fwrite(buf, 1, n, wcachefp) != n)
   1387 					    break;
   1388 			}
   1389 			checkfileerror(rcachefp, cachefile, 'r');
   1390 			fclose(rcachefp);
   1391 		}
   1392 		checkfileerror(wcachefp, tmppath, 'w');
   1393 		fclose(wcachefp);
   1394 	} else {
   1395 		if (head)
   1396 			writelog(fp, head);
   1397 	}
   1398 
   1399 	fputs("</tbody></table>", fp);
   1400 	writefooter(fp);
   1401 	checkfileerror(fp, "log.html", 'w');
   1402 	fclose(fp);
   1403 
   1404 	/* files for HEAD */
   1405 	fp = efopen("files.html", "w");
   1406 	writeheader(fp, "Files");
   1407 	if (head)
   1408 		writefiles(fp, head);
   1409 	writefooter(fp);
   1410 	checkfileerror(fp, "files.html", 'w');
   1411 	fclose(fp);
   1412 
   1413 	/* summary page with branches and tags */
   1414 	fp = efopen("refs.html", "w");
   1415 	writeheader(fp, "Refs");
   1416 	writerefs(fp);
   1417 	writefooter(fp);
   1418 	checkfileerror(fp, "refs.html", 'w');
   1419 	fclose(fp);
   1420 
   1421 	/* Atom feed */
   1422 	fp = efopen("atom.xml", "w");
   1423 	writeatom(fp, 1);
   1424 	checkfileerror(fp, "atom.xml", 'w');
   1425 	fclose(fp);
   1426 
   1427 	/* Atom feed for tags / releases */
   1428 	fp = efopen("tags.xml", "w");
   1429 	writeatom(fp, 0);
   1430 	checkfileerror(fp, "tags.xml", 'w');
   1431 	fclose(fp);
   1432 
   1433 	/* rename new cache file on success */
   1434 	if (cachefile && head) {
   1435 		if (rename(tmppath, cachefile))
   1436 			err(1, "rename: '%s' to '%s'", tmppath, cachefile);
   1437 		umask((mask = umask(0)));
   1438 		if (chmod(cachefile,
   1439 		    (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
   1440 			err(1, "chmod: '%s'", cachefile);
   1441 	}
   1442 
   1443 	/* cleanup */
   1444 	git_repository_free(repo);
   1445 	git_libgit2_shutdown();
   1446 
   1447 	return 0;
   1448 }