PNG  IHDR!@ PLTE>O`jqv PtEXtPage
stagit.c - metalympiada
git clone git://metalympiada.org/
Log | Files | README | LICENSE

stagit.c (33046B)


   1 /*
   2 MIT/X Consortium License
   3 
   4 stagit (c) 2015-2022 Hiltjo Posthuma <hiltjo@codemadness.org>
   5 
   6 $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp$
   7 $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp$
   8 (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
   9 
  10 Permission is hereby granted, free of charge, to any person obtaining a
  11 copy of this software and associated documentation files (the "Software"),
  12 to deal in the Software without restriction, including without limitation
  13 the rights to use, copy, modify, merge, publish, distribute, sublicense,
  14 and/or sell copies of the Software, and to permit persons to whom the
  15 Software is furnished to do so, subject to the following conditions:
  16 
  17 The above copyright notice and this permission notice shall be included in
  18 all copies or substantial portions of the Software.
  19 
  20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
  23 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  26 DEALINGS IN THE SOFTWARE.
  27 */
  28 #include <sys/stat.h>
  29 #include <sys/types.h>
  30 
  31 #include <err.h>
  32 #include <errno.h>
  33 #include <libgen.h>
  34 #include <limits.h>
  35 #include <stdint.h>
  36 #include <stdio.h>
  37 #include <stdlib.h>
  38 #include <string.h>
  39 #include <time.h>
  40 #include <unistd.h>
  41 
  42 #include <git2.h>
  43 
  44 #define LEN(s)    (sizeof(s)/sizeof(*s))
  45 
  46 struct deltainfo {
  47 	git_patch *patch;
  48 
  49 	size_t addcount;
  50 	size_t delcount;
  51 };
  52 
  53 struct commitinfo {
  54 	const git_oid *id;
  55 
  56 	char oid[GIT_OID_HEXSZ + 1];
  57 	char parentoid[GIT_OID_HEXSZ + 1];
  58 
  59 	const git_signature *author;
  60 	const git_signature *committer;
  61 	const char          *summary;
  62 	const char          *msg;
  63 
  64 	git_diff   *diff;
  65 	git_commit *commit;
  66 	git_commit *parent;
  67 	git_tree   *commit_tree;
  68 	git_tree   *parent_tree;
  69 
  70 	size_t addcount;
  71 	size_t delcount;
  72 	size_t filecount;
  73 
  74 	struct deltainfo **deltas;
  75 	size_t ndeltas;
  76 };
  77 
  78 /* reference and associated data for sorting */
  79 struct referenceinfo {
  80 	struct git_reference *ref;
  81 	struct commitinfo *ci;
  82 };
  83 
  84 static git_repository *repo;
  85 
  86 static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
  87 static const char *relpath = "";
  88 static const char *repodir;
  89 
  90 static char *name = "";
  91 static char *strippedname = "";
  92 static char description[255];
  93 static char cloneurl[1024];
  94 static char *submodules;
  95 static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" };
  96 static char *license;
  97 static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
  98 static char *readme;
  99 static long long nlogcommits = -1; /* -1 indicates not used */
 100 
 101 /* cache */
 102 static git_oid lastoid;
 103 static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
 104 static FILE *rcachefp, *wcachefp;
 105 static const char *cachefile;
 106 
 107 /*
 108  * Copy string src to buffer dst of size dsize.  At most dsize-1
 109  * chars will be copied.  Always NUL terminates (unless dsize == 0).
 110  * Returns strlen(src); if retval >= dsize, truncation occurred.
 111  */
 112 size_t
 113 strlcpy(char *dst, const char *src, size_t dsize)
 114 {
 115 	const char *osrc = src;
 116 	size_t nleft = dsize;
 117 
 118 	/* Copy as many bytes as will fit. */
 119 	if (nleft != 0) {
 120 		while (--nleft != 0) {
 121 			if ((*dst++ = *src++) == '\0')
 122 				break;
 123 		}
 124 	}
 125 
 126 	/* Not enough room in dst, add NUL and traverse rest of src. */
 127 	if (nleft == 0) {
 128 		if (dsize != 0)
 129 			*dst = '\0';		/* NUL-terminate dst */
 130 		while (*src++)
 131 			;
 132 	}
 133 
 134 	return(src - osrc - 1);	/* count does not include NUL */
 135 }
 136 
 137 /*
 138  * Appends src to string dst of size dsize (unlike strncat, dsize is the
 139  * full size of dst, not space left).  At most dsize-1 characters
 140  * will be copied.  Always NUL terminates (unless dsize <= strlen(dst)).
 141  * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
 142  * If retval >= dsize, truncation occurred.
 143  */
 144 size_t
 145 strlcat(char *dst, const char *src, size_t dsize)
 146 {
 147 	const char *odst = dst;
 148 	const char *osrc = src;
 149 	size_t n = dsize;
 150 	size_t dlen;
 151 
 152 	/* Find the end of dst and adjust bytes left but don't go past end. */
 153 	while (n-- != 0 && *dst != '\0')
 154 		dst++;
 155 	dlen = dst - odst;
 156 	n = dsize - dlen;
 157 
 158 	if (n-- == 0)
 159 		return(dlen + strlen(src));
 160 	while (*src != '\0') {
 161 		if (n != 0) {
 162 			*dst++ = *src;
 163 			n--;
 164 		}
 165 		src++;
 166 	}
 167 	*dst = '\0';
 168 
 169 	return(dlen + (src - osrc));	/* count does not include NUL */
 170 }
 171 
 172 /* Handle read or write errors for a FILE * stream */
 173 void
 174 checkfileerror(FILE *fp, const char *name, int mode)
 175 {
 176 	if (mode == 'r' && ferror(fp))
 177 		errx(1, "read error: %s", name);
 178 	else if (mode == 'w' && (fflush(fp) || ferror(fp)))
 179 		errx(1, "write error: %s", name);
 180 }
 181 
 182 void
 183 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
 184 {
 185 	int r;
 186 
 187 	r = snprintf(buf, bufsiz, "%s%s%s",
 188 		path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 189 	if (r < 0 || (size_t)r >= bufsiz)
 190 		errx(1, "path truncated: '%s%s%s'",
 191 			path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 192 }
 193 
 194 void
 195 deltainfo_free(struct deltainfo *di)
 196 {
 197 	if (!di)
 198 		return;
 199 	git_patch_free(di->patch);
 200 	memset(di, 0, sizeof(*di));
 201 	free(di);
 202 }
 203 
 204 int
 205 commitinfo_getstats(struct commitinfo *ci)
 206 {
 207 	struct deltainfo *di;
 208 	git_diff_options opts;
 209 	git_diff_find_options fopts;
 210 	const git_diff_delta *delta;
 211 	const git_diff_hunk *hunk;
 212 	const git_diff_line *line;
 213 	git_patch *patch = NULL;
 214 	size_t ndeltas, nhunks, nhunklines;
 215 	size_t i, j, k;
 216 
 217 	if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
 218 		goto err;
 219 	if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
 220 		if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
 221 			ci->parent = NULL;
 222 			ci->parent_tree = NULL;
 223 		}
 224 	}
 225 
 226 	git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
 227 	opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
 228 	              GIT_DIFF_IGNORE_SUBMODULES |
 229 		      GIT_DIFF_INCLUDE_TYPECHANGE;
 230 	if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
 231 		goto err;
 232 
 233 	if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
 234 		goto err;
 235 	/* find renames and copies, exact matches (no heuristic) for renames. */
 236 	fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
 237 	               GIT_DIFF_FIND_EXACT_MATCH_ONLY;
 238 	if (git_diff_find_similar(ci->diff, &fopts))
 239 		goto err;
 240 
 241 	ndeltas = git_diff_num_deltas(ci->diff);
 242 	if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
 243 		err(1, "calloc");
 244 
 245 	for (i = 0; i < ndeltas; i++) {
 246 		if (git_patch_from_diff(&patch, ci->diff, i))
 247 			goto err;
 248 
 249 		if (!(di = calloc(1, sizeof(struct deltainfo))))
 250 			err(1, "calloc");
 251 		di->patch = patch;
 252 		ci->deltas[i] = di;
 253 
 254 		delta = git_patch_get_delta(patch);
 255 
 256 		/* skip stats for binary data */
 257 		if (delta->flags & GIT_DIFF_FLAG_BINARY)
 258 			continue;
 259 
 260 		nhunks = git_patch_num_hunks(patch);
 261 		for (j = 0; j < nhunks; j++) {
 262 			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 263 				break;
 264 			for (k = 0; ; k++) {
 265 				if (git_patch_get_line_in_hunk(&line, patch, j, k))
 266 					break;
 267 				if (line->old_lineno == -1) {
 268 					di->addcount++;
 269 					ci->addcount++;
 270 				} else if (line->new_lineno == -1) {
 271 					di->delcount++;
 272 					ci->delcount++;
 273 				}
 274 			}
 275 		}
 276 	}
 277 	ci->ndeltas = i;
 278 	ci->filecount = i;
 279 
 280 	return 0;
 281 
 282 err:
 283 	git_diff_free(ci->diff);
 284 	ci->diff = NULL;
 285 	git_tree_free(ci->commit_tree);
 286 	ci->commit_tree = NULL;
 287 	git_tree_free(ci->parent_tree);
 288 	ci->parent_tree = NULL;
 289 	git_commit_free(ci->parent);
 290 	ci->parent = NULL;
 291 
 292 	if (ci->deltas)
 293 		for (i = 0; i < ci->ndeltas; i++)
 294 			deltainfo_free(ci->deltas[i]);
 295 	free(ci->deltas);
 296 	ci->deltas = NULL;
 297 	ci->ndeltas = 0;
 298 	ci->addcount = 0;
 299 	ci->delcount = 0;
 300 	ci->filecount = 0;
 301 
 302 	return -1;
 303 }
 304 
 305 void
 306 commitinfo_free(struct commitinfo *ci)
 307 {
 308 	size_t i;
 309 
 310 	if (!ci)
 311 		return;
 312 	if (ci->deltas)
 313 		for (i = 0; i < ci->ndeltas; i++)
 314 			deltainfo_free(ci->deltas[i]);
 315 
 316 	free(ci->deltas);
 317 	git_diff_free(ci->diff);
 318 	git_tree_free(ci->commit_tree);
 319 	git_tree_free(ci->parent_tree);
 320 	git_commit_free(ci->commit);
 321 	git_commit_free(ci->parent);
 322 	memset(ci, 0, sizeof(*ci));
 323 	free(ci);
 324 }
 325 
 326 struct commitinfo *
 327 commitinfo_getbyoid(const git_oid *id)
 328 {
 329 	struct commitinfo *ci;
 330 
 331 	if (!(ci = calloc(1, sizeof(struct commitinfo))))
 332 		err(1, "calloc");
 333 
 334 	if (git_commit_lookup(&(ci->commit), repo, id))
 335 		goto err;
 336 	ci->id = id;
 337 
 338 	git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
 339 	git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
 340 
 341 	ci->author = git_commit_author(ci->commit);
 342 	ci->committer = git_commit_committer(ci->commit);
 343 	ci->summary = git_commit_summary(ci->commit);
 344 	ci->msg = git_commit_message(ci->commit);
 345 
 346 	return ci;
 347 
 348 err:
 349 	commitinfo_free(ci);
 350 
 351 	return NULL;
 352 }
 353 
 354 int
 355 refs_cmp(const void *v1, const void *v2)
 356 {
 357 	const struct referenceinfo *r1 = v1, *r2 = v2;
 358 	time_t t1, t2;
 359 	int r;
 360 
 361 	if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
 362 		return r;
 363 
 364 	t1 = r1->ci->author ? r1->ci->author->when.time : 0;
 365 	t2 = r2->ci->author ? r2->ci->author->when.time : 0;
 366 	if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
 367 		return r;
 368 
 369 	return strcmp(git_reference_shorthand(r1->ref),
 370 	              git_reference_shorthand(r2->ref));
 371 }
 372 
 373 int
 374 getrefs(struct referenceinfo **pris, size_t *prefcount)
 375 {
 376 	struct referenceinfo *ris = NULL;
 377 	struct commitinfo *ci = NULL;
 378 	git_reference_iterator *it = NULL;
 379 	const git_oid *id = NULL;
 380 	git_object *obj = NULL;
 381 	git_reference *dref = NULL, *r, *ref = NULL;
 382 	size_t i, refcount;
 383 
 384 	*pris = NULL;
 385 	*prefcount = 0;
 386 
 387 	if (git_reference_iterator_new(&it, repo))
 388 		return -1;
 389 
 390 	for (refcount = 0; !git_reference_next(&ref, it); ) {
 391 		if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
 392 			git_reference_free(ref);
 393 			ref = NULL;
 394 			continue;
 395 		}
 396 
 397 		switch (git_reference_type(ref)) {
 398 		case GIT_REF_SYMBOLIC:
 399 			if (git_reference_resolve(&dref, ref))
 400 				goto err;
 401 			r = dref;
 402 			break;
 403 		case GIT_REF_OID:
 404 			r = ref;
 405 			break;
 406 		default:
 407 			continue;
 408 		}
 409 		if (!git_reference_target(r) ||
 410 		    git_reference_peel(&obj, r, GIT_OBJ_ANY))
 411 			goto err;
 412 		if (!(id = git_object_id(obj)))
 413 			goto err;
 414 		if (!(ci = commitinfo_getbyoid(id)))
 415 			break;
 416 
 417 		if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
 418 			err(1, "realloc");
 419 		ris[refcount].ci = ci;
 420 		ris[refcount].ref = r;
 421 		refcount++;
 422 
 423 		git_object_free(obj);
 424 		obj = NULL;
 425 		git_reference_free(dref);
 426 		dref = NULL;
 427 	}
 428 	git_reference_iterator_free(it);
 429 
 430 	/* sort by type, date then shorthand name */
 431 	qsort(ris, refcount, sizeof(*ris), refs_cmp);
 432 
 433 	*pris = ris;
 434 	*prefcount = refcount;
 435 
 436 	return 0;
 437 
 438 err:
 439 	git_object_free(obj);
 440 	git_reference_free(dref);
 441 	commitinfo_free(ci);
 442 	for (i = 0; i < refcount; i++) {
 443 		commitinfo_free(ris[i].ci);
 444 		git_reference_free(ris[i].ref);
 445 	}
 446 	free(ris);
 447 
 448 	return -1;
 449 }
 450 
 451 FILE *
 452 efopen(const char *filename, const char *flags)
 453 {
 454 	FILE *fp;
 455 
 456 	if (!(fp = fopen(filename, flags)))
 457 		err(1, "fopen: '%s'", filename);
 458 
 459 	return fp;
 460 }
 461 
 462 /* Percent-encode, see RFC3986 section 2.1. */
 463 void
 464 percentencode(FILE *fp, const char *s, size_t len)
 465 {
 466 	static char tab[] = "0123456789ABCDEF";
 467 	unsigned char uc;
 468 	size_t i;
 469 
 470 	for (i = 0; *s && i < len; s++, i++) {
 471 		uc = *s;
 472 		/* NOTE: do not encode '/' for paths or ",-." */
 473 		if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
 474 		    uc == '[' || uc == ']') {
 475 			putc('%', fp);
 476 			putc(tab[(uc >> 4) & 0x0f], fp);
 477 			putc(tab[uc & 0x0f], fp);
 478 		} else {
 479 			putc(uc, fp);
 480 		}
 481 	}
 482 }
 483 
 484 /* Escape characters below as HTML 2.0 / XML 1.0. */
 485 void
 486 xmlencode(FILE *fp, const char *s, size_t len)
 487 {
 488 	size_t i;
 489 
 490 	for (i = 0; *s && i < len; s++, i++) {
 491 		switch(*s) {
 492 		case '<':  fputs("&lt;",   fp); break;
 493 		case '>':  fputs("&gt;",   fp); break;
 494 		case '\'': fputs("&#39;",  fp); break;
 495 		case '&':  fputs("&amp;",  fp); break;
 496 		case '"':  fputs("&quot;", fp); break;
 497 		default:   putc(*s, fp);
 498 		}
 499 	}
 500 }
 501 
 502 /* Escape characters below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */
 503 void
 504 xmlencodeline(FILE *fp, const char *s, size_t len)
 505 {
 506 	size_t i;
 507 
 508 	for (i = 0; *s && i < len; s++, i++) {
 509 		switch(*s) {
 510 		case '<':  fputs("&lt;",   fp); break;
 511 		case '>':  fputs("&gt;",   fp); break;
 512 		case '\'': fputs("&#39;",  fp); break;
 513 		case '&':  fputs("&amp;",  fp); break;
 514 		case '"':  fputs("&quot;", fp); break;
 515 		case '\r': break; /* ignore CR */
 516 		case '\n': break; /* ignore LF */
 517 		default:   putc(*s, fp);
 518 		}
 519 	}
 520 }
 521 
 522 int
 523 mkdirp(const char *path)
 524 {
 525 	char tmp[PATH_MAX], *p;
 526 
 527 	if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
 528 		errx(1, "path truncated: '%s'", path);
 529 	for (p = tmp + (tmp[0] == '/'); *p; p++) {
 530 		if (*p != '/')
 531 			continue;
 532 		*p = '\0';
 533 		if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 534 			return -1;
 535 		*p = '/';
 536 	}
 537 	if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
 538 		return -1;
 539 	return 0;
 540 }
 541 
 542 void
 543 printtimez(FILE *fp, const git_time *intime)
 544 {
 545 	struct tm *intm;
 546 	time_t t;
 547 	char out[32];
 548 
 549 	t = (time_t)intime->time;
 550 	if (!(intm = gmtime(&t)))
 551 		return;
 552 	strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
 553 	fputs(out, fp);
 554 }
 555 
 556 void
 557 printtime(FILE *fp, const git_time *intime)
 558 {
 559 	struct tm *intm;
 560 	time_t t;
 561 	char out[32];
 562 
 563 	t = (time_t)intime->time + (intime->offset * 60);
 564 	if (!(intm = gmtime(&t)))
 565 		return;
 566 	strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
 567 	if (intime->offset < 0)
 568 		fprintf(fp, "%s -%02d%02d", out,
 569 		            -(intime->offset) / 60, -(intime->offset) % 60);
 570 	else
 571 		fprintf(fp, "%s +%02d%02d", out,
 572 		            intime->offset / 60, intime->offset % 60);
 573 }
 574 
 575 void
 576 printtimeshort(FILE *fp, const git_time *intime)
 577 {
 578 	struct tm *intm;
 579 	time_t t;
 580 	char out[32];
 581 
 582 	t = (time_t)intime->time;
 583 	if (!(intm = gmtime(&t)))
 584 		return;
 585 	strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
 586 	fputs(out, fp);
 587 }
 588 
 589 void
 590 writeheader(FILE *fp, const char *title)
 591 {
 592 	fputs("<div class=\"stagit\"><title>", fp);
 593 	xmlencode(fp, title, strlen(title));
 594 	if (title[0] && strippedname[0])
 595 		fputs(" - ", fp);
 596 	xmlencode(fp, strippedname, strlen(strippedname));
 597 	fputs("</title><table><tr><td>", fp);
 598         fputs("</td><td>", fp);
 599 	fputs("<span class=\"desc\">", fp);
 600 	xmlencode(fp, description, strlen(description));
 601 	fputs("</span></td></tr>", fp);
 602 	if (cloneurl[0]) {
 603 		fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
 604 		xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
 605 		fputs("\">", fp);
 606 		xmlencode(fp, cloneurl, strlen(cloneurl));
 607 		fputs("</a></td></tr>", fp);
 608 	}
 609 	fputs("<tr><td></td><td>\n", fp);
 610 	fprintf(fp, "<a href=\"%slog.png\">Log</a> | ", relpath);
 611 	fprintf(fp, "<a href=\"%sfiles.png\">Files</a>", relpath);
 612 	if (submodules)
 613 		fprintf(fp, " | <a href=\"%sfile/%s.png\">Submodules</a>",
 614 		        relpath, submodules);
 615 	if (readme)
 616 		fprintf(fp, " | <a href=\"%sfile/%s.png\">README</a>",
 617 		        relpath, readme);
 618 	if (license)
 619 		fprintf(fp, " | <a href=\"%sfile/%s.png\">LICENSE</a>",
 620 		        relpath, license);
 621 	fputs("</td></tr></table>\n<hr/>\n<div class=\"content\">\n", fp);
 622 }
 623 
 624 void
 625 writefooter(FILE *fp)
 626 {
 627 	fputs("</div></div>", fp);
 628 }
 629 
 630 size_t
 631 writeblobpng(FILE *fp, const git_blob *blob)
 632 {
 633 	size_t n = 0, i, len, prev;
 634 	const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%4zu</a> ";
 635 	const char *s = git_blob_rawcontent(blob);
 636 
 637 	len = git_blob_rawsize(blob);
 638 	fputs("<pre class=\"blob\">\n", fp);
 639 
 640 	if (len > 0) {
 641 		for (i = 0, prev = 0; i < len; i++) {
 642 			if (s[i] != '\n')
 643 				continue;
 644 			n++;
 645 			fprintf(fp, nfmt, n, n, n);
 646 			xmlencodeline(fp, &s[prev], i - prev + 1);
 647 			putc('\n', fp);
 648 			prev = i + 1;
 649 		}
 650 		/* trailing data */
 651 		if ((len - prev) > 0) {
 652 			n++;
 653 			fprintf(fp, nfmt, n, n, n);
 654 			xmlencodeline(fp, &s[prev], len - prev);
 655 		}
 656 	}
 657 
 658 	fputs("</pre>", fp);
 659 
 660 	return n;
 661 }
 662 
 663 void
 664 printcommit(FILE *fp, struct commitinfo *ci)
 665 {
 666 	fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.png\">%s</a>\n",
 667 		relpath, ci->oid, ci->oid);
 668 
 669 	if (ci->parentoid[0])
 670 		fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.png\">%s</a>\n",
 671 			relpath, ci->parentoid, ci->parentoid);
 672 
 673 	if (ci->author) {
 674 		fputs("<b>Author:</b> ", fp);
 675 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
 676 		fputs(" &lt;<a href=\"mailto:", fp);
 677 		xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
 678 		fputs("\">", fp);
 679 		xmlencode(fp, ci->author->email, strlen(ci->author->email));
 680 		fputs("</a>&gt;\n<b>Date:</b>   ", fp);
 681 		printtime(fp, &(ci->author->when));
 682 		putc('\n', fp);
 683 	}
 684 	if (ci->msg) {
 685 		putc('\n', fp);
 686 		xmlencode(fp, ci->msg, strlen(ci->msg));
 687 		putc('\n', fp);
 688 	}
 689 }
 690 
 691 void
 692 printshowfile(FILE *fp, struct commitinfo *ci)
 693 {
 694 	const git_diff_delta *delta;
 695 	const git_diff_hunk *hunk;
 696 	const git_diff_line *line;
 697 	git_patch *patch;
 698 	size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
 699 	char linestr[80];
 700 	int c;
 701 
 702 	printcommit(fp, ci);
 703 
 704 	if (!ci->deltas)
 705 		return;
 706 
 707 	if (ci->filecount > 1000   ||
 708 	    ci->ndeltas   > 1000   ||
 709 	    ci->addcount  > 100000 ||
 710 	    ci->delcount  > 100000) {
 711 		fputs("Diff is too large, output suppressed.\n", fp);
 712 		return;
 713 	}
 714 
 715 	/* diff stat */
 716 	fputs("<b>Diffstat:</b>\n<table>", fp);
 717 	for (i = 0; i < ci->ndeltas; i++) {
 718 		delta = git_patch_get_delta(ci->deltas[i]->patch);
 719 
 720 		switch (delta->status) {
 721 		case GIT_DELTA_ADDED:      c = 'A'; break;
 722 		case GIT_DELTA_COPIED:     c = 'C'; break;
 723 		case GIT_DELTA_DELETED:    c = 'D'; break;
 724 		case GIT_DELTA_MODIFIED:   c = 'M'; break;
 725 		case GIT_DELTA_RENAMED:    c = 'R'; break;
 726 		case GIT_DELTA_TYPECHANGE: c = 'T'; break;
 727 		default:                   c = ' '; break;
 728 		}
 729 		if (c == ' ')
 730 			fprintf(fp, "<tr><td>%c", c);
 731 		else
 732 			fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
 733 
 734 		fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
 735 		xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 736 		if (strcmp(delta->old_file.path, delta->new_file.path)) {
 737 			fputs(" -&gt; ", fp);
 738 			xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 739 		}
 740 
 741 		add = ci->deltas[i]->addcount;
 742 		del = ci->deltas[i]->delcount;
 743 		changed = add + del;
 744 		total = sizeof(linestr) - 2;
 745 		if (changed > total) {
 746 			if (add)
 747 				add = ((float)total / changed * add) + 1;
 748 			if (del)
 749 				del = ((float)total / changed * del) + 1;
 750 		}
 751 		memset(&linestr, '+', add);
 752 		memset(&linestr[add], '-', del);
 753 
 754 		fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
 755 		        ci->deltas[i]->addcount + ci->deltas[i]->delcount);
 756 		fwrite(&linestr, 1, add, fp);
 757 		fputs("</span><span class=\"d\">", fp);
 758 		fwrite(&linestr[add], 1, del, fp);
 759 		fputs("</span></td></tr>\n", fp);
 760 	}
 761 	fprintf(fp, "</table></pre><pre class=\"blob\">%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
 762 		ci->filecount, ci->filecount == 1 ? "" : "s",
 763 	        ci->addcount,  ci->addcount  == 1 ? "" : "s",
 764 	        ci->delcount,  ci->delcount  == 1 ? "" : "s");
 765 
 766 	fputs("<hr/>", fp);
 767 
 768 	for (i = 0; i < ci->ndeltas; i++) {
 769 		patch = ci->deltas[i]->patch;
 770 		delta = git_patch_get_delta(patch);
 771 		fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
 772 		percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 773 		fputs(".png\">", fp);
 774 		xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
 775 		fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
 776 		percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 777 		fprintf(fp, ".png\">");
 778 		xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
 779 		fprintf(fp, "</a></b>\n");
 780 
 781 		/* check binary data */
 782 		if (delta->flags & GIT_DIFF_FLAG_BINARY) {
 783 			fputs("Binary files differ.\n", fp);
 784 			continue;
 785 		}
 786 
 787 		nhunks = git_patch_num_hunks(patch);
 788 		for (j = 0; j < nhunks; j++) {
 789 			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
 790 				break;
 791 
 792 			fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
 793 			xmlencode(fp, hunk->header, hunk->header_len);
 794 			fputs("</a>", fp);
 795 
 796 			for (k = 0; ; k++) {
 797 				if (git_patch_get_line_in_hunk(&line, patch, j, k))
 798 					break;
 799 				if (line->old_lineno == -1)
 800 					fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
 801 						i, j, k, i, j, k);
 802 				else if (line->new_lineno == -1)
 803 					fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
 804 						i, j, k, i, j, k);
 805 				else
 806 					putc(' ', fp);
 807 				xmlencodeline(fp, line->content, line->content_len);
 808 				putc('\n', fp);
 809 				if (line->old_lineno == -1 || line->new_lineno == -1)
 810 					fputs("</a>", fp);
 811 			}
 812 		}
 813 	}
 814 }
 815 
 816 void
 817 writelogline(FILE *fp, struct commitinfo *ci)
 818 {
 819 	fputs("<tr><td>", fp);
 820 	if (ci->author)
 821 		printtimeshort(fp, &(ci->author->when));
 822 	fputs("</td><td>", fp);
 823 	if (ci->summary) {
 824 		fprintf(fp, "<a href=\"%scommit/%s.png\">", relpath, ci->oid);
 825 		xmlencode(fp, ci->summary, strlen(ci->summary));
 826 		fputs("</a>", fp);
 827 	}
 828 	fputs("</td><td>", fp);
 829 	if (ci->author)
 830 		xmlencode(fp, ci->author->name, strlen(ci->author->name));
 831 	fputs("</td><td class=\"num\" align=\"right\">", fp);
 832 	fprintf(fp, "%zu", ci->filecount);
 833 	fputs("</td><td class=\"num\" align=\"right\">", fp);
 834 	fprintf(fp, "+%zu", ci->addcount);
 835 	fputs("</td><td class=\"num\" align=\"right\">", fp);
 836 	fprintf(fp, "-%zu", ci->delcount);
 837 	fputs("</td></tr>\n", fp);
 838 }
 839 
 840 int
 841 writelog(FILE *fp, const git_oid *oid)
 842 {
 843 	struct commitinfo *ci;
 844 	git_revwalk *w = NULL;
 845 	git_oid id;
 846 	char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
 847 	FILE *fpfile;
 848 	size_t remcommits = 0;
 849 	int r;
 850 
 851 	git_revwalk_new(&w, repo);
 852 	git_revwalk_push(w, oid);
 853 
 854 	while (!git_revwalk_next(&id, w)) {
 855 		relpath = "";
 856 
 857 		if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
 858 			break;
 859 
 860 		git_oid_tostr(oidstr, sizeof(oidstr), &id);
 861 		r = snprintf(path, sizeof(path), "commit/%s.png", oidstr);
 862 		if (r < 0 || (size_t)r >= sizeof(path))
 863 			errx(1, "path truncated: 'commit/%s.png'", oidstr);
 864 		r = access(path, F_OK);
 865 
 866 		/* optimization: if there are no log lines to write and
 867 		   the commit file already exists: skip the diffstat */
 868 		if (!nlogcommits) {
 869 			remcommits++;
 870 			if (!r)
 871 				continue;
 872 		}
 873 
 874 		if (!(ci = commitinfo_getbyoid(&id)))
 875 			break;
 876 		/* diffstat: for stagit HTML required for the log.png line */
 877 		if (commitinfo_getstats(ci) == -1)
 878 			goto err;
 879 
 880 		if (nlogcommits != 0) {
 881 			writelogline(fp, ci);
 882 			if (nlogcommits > 0)
 883 				nlogcommits--;
 884 		}
 885 
 886 		if (cachefile)
 887 			writelogline(wcachefp, ci);
 888 
 889 		/* check if file exists if so skip it */
 890 		if (r) {
 891 			relpath = "../";
 892 			fpfile = efopen(path, "w");
 893 			writeheader(fpfile, ci->summary);
 894 			fputs("<pre>", fpfile);
 895 			printshowfile(fpfile, ci);
 896 			fputs("</pre>\n", fpfile);
 897 			writefooter(fpfile);
 898 			checkfileerror(fpfile, path, 'w');
 899 			fclose(fpfile);
 900 		}
 901 err:
 902 		commitinfo_free(ci);
 903 	}
 904 	git_revwalk_free(w);
 905 
 906 	if (nlogcommits == 0 && remcommits != 0) {
 907 		fprintf(fp, "<tr><td></td><td colspan=\"5\">"
 908 		        "%zu more commits remaining, fetch the repository"
 909 		        "</td></tr>\n", remcommits);
 910 	}
 911 
 912 	relpath = "";
 913 
 914 	return 0;
 915 }
 916 
 917 size_t
 918 writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
 919 {
 920 	char tmp[PATH_MAX] = "", *d;
 921 	const char *p;
 922 	size_t lc = 0;
 923 	FILE *fp;
 924 
 925 	if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
 926 		errx(1, "path truncated: '%s'", fpath);
 927 	if (!(d = dirname(tmp)))
 928 		err(1, "dirname");
 929 	if (mkdirp(d))
 930 		return -1;
 931 
 932 	for (p = fpath, tmp[0] = '\0'; *p; p++) {
 933 		if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
 934 			errx(1, "path truncated: '../%s'", tmp);
 935 	}
 936 	relpath = tmp;
 937 
 938 	fp = efopen(fpath, "w");
 939 	writeheader(fp, filename);
 940 	fputs("<p> ", fp);
 941 	xmlencode(fp, filename, strlen(filename));
 942 	fprintf(fp, " (%zuB)", filesize);
 943 	fputs("</p><hr/>", fp);
 944 
 945 	if (git_blob_is_binary((git_blob *)obj))
 946 		fputs("<p>Binary file.</p>\n", fp);
 947 	else
 948 		lc = writeblobpng(fp, (git_blob *)obj);
 949 
 950 	writefooter(fp);
 951 	checkfileerror(fp, fpath, 'w');
 952 	fclose(fp);
 953 
 954 	relpath = "";
 955 
 956 	return lc;
 957 }
 958 
 959 int
 960 writefilestree(FILE *fp, git_tree *tree, const char *path)
 961 {
 962 	const git_tree_entry *entry = NULL;
 963 	git_object *obj = NULL;
 964 	const char *entryname;
 965 	char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8];
 966 	size_t count, i, lc, filesize;
 967 	int r, ret;
 968 
 969 	count = git_tree_entrycount(tree);
 970 	for (i = 0; i < count; i++) {
 971 		if (!(entry = git_tree_entry_byindex(tree, i)) ||
 972 		    !(entryname = git_tree_entry_name(entry)))
 973 			return -1;
 974 		joinpath(entrypath, sizeof(entrypath), path, entryname);
 975 
 976 		r = snprintf(filepath, sizeof(filepath), "file/%s.png",
 977 		         entrypath);
 978 		if (r < 0 || (size_t)r >= sizeof(filepath))
 979 			errx(1, "path truncated: 'file/%s.png'", entrypath);
 980 
 981 		if (!git_tree_entry_to_object(&obj, repo, entry)) {
 982 			switch (git_object_type(obj)) {
 983 			case GIT_OBJ_BLOB:
 984 				break;
 985 			case GIT_OBJ_TREE:
 986 				/* NOTE: recurses */
 987 				ret = writefilestree(fp, (git_tree *)obj,
 988 				                     entrypath);
 989 				git_object_free(obj);
 990 				if (ret)
 991 					return ret;
 992 				continue;
 993 			default:
 994 				git_object_free(obj);
 995 				continue;
 996 			}
 997 
 998 			filesize = git_blob_rawsize((git_blob *)obj);
 999 			lc = writeblob(obj, filepath, entryname, filesize);
1000 
1001 			fprintf(fp, "<tr><td><a href=\"%s", relpath);
1002 			percentencode(fp, filepath, strlen(filepath));
1003 			fputs("\">", fp);
1004 			xmlencode(fp, entrypath, strlen(entrypath));
1005 			fputs("</a></td><td class=\"num\" align=\"right\">", fp);
1006 			if (lc > 0)
1007 				fprintf(fp, "%zuL", lc);
1008 			else
1009 				fprintf(fp, "%zuB", filesize);
1010 			fputs("</td></tr>\n", fp);
1011 			git_object_free(obj);
1012 		} else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1013 			/* commit object in tree is a submodule */
1014 			fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.png\">",
1015 				relpath);
1016 			xmlencode(fp, entrypath, strlen(entrypath));
1017 			fputs("</a> @ ", fp);
1018 			git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1019 			xmlencode(fp, oid, strlen(oid));
1020 			fputs("</td><td class=\"num\" align=\"right\"></td></tr>", fp);
1021 		}
1022 	}
1023 
1024 	return 0;
1025 }
1026 
1027 int
1028 writefiles(FILE *fp, const git_oid *id)
1029 {
1030 	git_tree *tree = NULL;
1031 	git_commit *commit = NULL;
1032 	int ret = -1;
1033 
1034 	fputs("<table id=\"files\"><thead>\n<tr>"
1035 	      "<td><b>File name</b></td>"
1036 	      "<td class=\"num\" align=\"right\"><b>Size</b></td>"
1037 	      "</tr>\n</thead><tbody>\n", fp);
1038 
1039 	if (!git_commit_lookup(&commit, repo, id) &&
1040 	    !git_commit_tree(&tree, commit))
1041 		ret = writefilestree(fp, tree, "");
1042 
1043 	fputs("</tbody></table>", fp);
1044 
1045 	git_commit_free(commit);
1046 	git_tree_free(tree);
1047 
1048 	return ret;
1049 }
1050 
1051 void
1052 usage(char *argv0)
1053 {
1054 	fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
1055 	        "[-u baseurl] repodir\n", argv0);
1056 	exit(1);
1057 }
1058 
1059 int
1060 main(int argc, char *argv[])
1061 {
1062 	git_object *obj = NULL, *readme_obj = NULL;
1063 	const git_oid *head = NULL;
1064 	mode_t mask;
1065 	FILE *fp, *fpread;
1066 	char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1067 	char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1068 	size_t n;
1069 	int i, fd;
1070 
1071 	for (i = 1; i < argc; i++) {
1072 		if (argv[i][0] != '-') {
1073 			if (repodir)
1074 				usage(argv[0]);
1075 			repodir = argv[i];
1076 		} else if (argv[i][1] == 'c') {
1077 			if (nlogcommits > 0 || i + 1 >= argc)
1078 				usage(argv[0]);
1079 			cachefile = argv[++i];
1080 		} else if (argv[i][1] == 'l') {
1081 			if (cachefile || i + 1 >= argc)
1082 				usage(argv[0]);
1083 			errno = 0;
1084 			nlogcommits = strtoll(argv[++i], &p, 10);
1085 			if (argv[i][0] == '\0' || *p != '\0' ||
1086 			    nlogcommits <= 0 || errno)
1087 				usage(argv[0]);
1088 		} else if (argv[i][1] == 'u') {
1089 			if (i + 1 >= argc)
1090 				usage(argv[0]);
1091 			baseurl = argv[++i];
1092 		}
1093 	}
1094 	if (!repodir)
1095 		usage(argv[0]);
1096 
1097 	if (!realpath(repodir, repodirabs))
1098 		err(1, "realpath");
1099 
1100 	/* do not search outside the git repository:
1101 	   GIT_CONFIG_LEVEL_APP is the highest level currently */
1102 	git_libgit2_init();
1103 	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
1104 		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
1105 	/* do not require the git repository to be owned by the current user */
1106 	//git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
1107 
1108 #ifdef __OpenBSD__
1109 	if (unveil(repodir, "r") == -1)
1110 		err(1, "unveil: %s", repodir);
1111 	if (unveil(".", "rwc") == -1)
1112 		err(1, "unveil: .");
1113 	if (cachefile && unveil(cachefile, "rwc") == -1)
1114 		err(1, "unveil: %s", cachefile);
1115 
1116 	if (cachefile) {
1117 		if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1118 			err(1, "pledge");
1119 	} else {
1120 		if (pledge("stdio rpath wpath cpath", NULL) == -1)
1121 			err(1, "pledge");
1122 	}
1123 #endif
1124 
1125 	if (git_repository_open_ext(&repo, repodir,
1126 		GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1127 		fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1128 		return 1;
1129 	}
1130 
1131 	/* find HEAD */
1132 	if (!git_revparse_single(&obj, repo, "HEAD"))
1133 		head = git_object_id(obj);
1134 	git_object_free(obj);
1135 
1136 	/* use directory name as name */
1137 	if ((name = strrchr(repodirabs, '/')))
1138 		name++;
1139 	else
1140 		name = "";
1141 
1142 	/* strip .git suffix */
1143 	if (!(strippedname = strdup(name)))
1144 		err(1, "strdup");
1145 	if ((p = strrchr(strippedname, '.')))
1146 		if (!strcmp(p, ".git"))
1147 			*p = '\0';
1148 
1149 	/* read description or .git/description */
1150 	joinpath(path, sizeof(path), repodir, "description");
1151 	if (!(fpread = fopen(path, "r"))) {
1152 		joinpath(path, sizeof(path), repodir, ".git/description");
1153 		fpread = fopen(path, "r");
1154 	}
1155 	if (fpread) {
1156 		if (!fgets(description, sizeof(description), fpread))
1157 			description[0] = '\0';
1158 		checkfileerror(fpread, path, 'r');
1159 		fclose(fpread);
1160 	}
1161 
1162 	/* read url or .git/url */
1163 	joinpath(path, sizeof(path), repodir, "url");
1164 	if (!(fpread = fopen(path, "r"))) {
1165 		joinpath(path, sizeof(path), repodir, ".git/url");
1166 		fpread = fopen(path, "r");
1167 	}
1168 	if (fpread) {
1169 		if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1170 			cloneurl[0] = '\0';
1171 		checkfileerror(fpread, path, 'r');
1172 		fclose(fpread);
1173 		cloneurl[strcspn(cloneurl, "\n")] = '\0';
1174 	}
1175 
1176 	/* check LICENSE */
1177 	for (size_t i = 0; i < LEN(licensefiles) && !license; i++) {
1178 		if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1179 		    git_object_type(obj) == GIT_OBJ_BLOB)
1180 			license = licensefiles[i] + strlen("HEAD:");
1181 		git_object_free(obj);
1182 	}
1183 
1184 	/* check README */
1185 	for (size_t i = 0; i < LEN(readmefiles) && !readme; i++) {
1186 		if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1187 		    git_object_type(obj) == GIT_OBJ_BLOB) {
1188 			readme = readmefiles[i] + strlen("HEAD:");
1189 			readme_obj = obj;
1190 		} else {
1191         	    git_object_free(obj);
1192 		}
1193 	}
1194 
1195 	if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1196 	    git_object_type(obj) == GIT_OBJ_BLOB)
1197 		submodules = ".gitmodules";
1198 	git_object_free(obj);
1199 
1200 	/* log for HEAD */
1201 	fp = efopen("log.png", "w");
1202 	relpath = "";
1203 	mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1204 	writeheader(fp, "Log");
1205 	fputs("<table class=\"log\"><thead>\n<tr><td><b>Date</b></td>"
1206 	      "<td><b>Commit message</b></td>"
1207 	      "<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>"
1208 	      "<td class=\"num\" align=\"right\"><b>+</b></td>"
1209 	      "<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
1210 
1211 	if (cachefile && head) {
1212 		/* read from cache file (does not need to exist) */
1213 		if ((rcachefp = fopen(cachefile, "r"))) {
1214 			if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1215 				errx(1, "%s: no object id", cachefile);
1216 			if (git_oid_fromstr(&lastoid, lastoidstr))
1217 				errx(1, "%s: invalid object id", cachefile);
1218 		}
1219 
1220 		/* write log to (temporary) cache */
1221 		if ((fd = mkstemp(tmppath)) == -1)
1222 			err(1, "mkstemp");
1223 		if (!(wcachefp = fdopen(fd, "w")))
1224 			err(1, "fdopen: '%s'", tmppath);
1225 		/* write last commit id (HEAD) */
1226 		git_oid_tostr(buf, sizeof(buf), head);
1227 		fprintf(wcachefp, "%s\n", buf);
1228 
1229 		writelog(fp, head);
1230 
1231 		if (rcachefp) {
1232 			/* append previous log to log.png and the new cache */
1233 			while (!feof(rcachefp)) {
1234 				n = fread(buf, 1, sizeof(buf), rcachefp);
1235 				if (ferror(rcachefp))
1236 					break;
1237 				if (fwrite(buf, 1, n, fp) != n ||
1238 				    fwrite(buf, 1, n, wcachefp) != n)
1239 					    break;
1240 			}
1241 			checkfileerror(rcachefp, cachefile, 'r');
1242 			fclose(rcachefp);
1243 		}
1244 		checkfileerror(wcachefp, tmppath, 'w');
1245 		fclose(wcachefp);
1246 	} else {
1247 		if (head)
1248 			writelog(fp, head);
1249 	}
1250 
1251 	fputs("</tbody></table>", fp);
1252 	writefooter(fp);
1253 	checkfileerror(fp, "log.png", 'w');
1254 	fclose(fp);
1255 
1256 	/* files for HEAD */
1257 	fp = efopen("files.png", "w");
1258 	writeheader(fp, "Files");
1259 	if (head)
1260 		writefiles(fp, head);
1261 	writefooter(fp);
1262 	checkfileerror(fp, "files.png", 'w');
1263 	fclose(fp);
1264 
1265 	/* index = files + README */
1266 	fp = efopen("index.png", "w");
1267 	writeheader(fp, "Index");
1268 	if (head)
1269 		writefiles(fp, head);
1270 	if (readme_obj) {
1271                 const char *s = git_blob_rawcontent((git_blob *)readme_obj);
1272         	size_t len = git_blob_rawsize((git_blob *)readme_obj);
1273         	fputs("<pre class=\"readme\">\n", fp);
1274         	xmlencode(fp, s, len);
1275         	fputs("</pre>", fp);
1276     		git_object_free(readme_obj);
1277 	}
1278 	writefooter(fp);
1279 	checkfileerror(fp, "index.png", 'w');
1280 	fclose(fp);
1281 
1282 	/* rename new cache file on success */
1283 	if (cachefile && head) {
1284 		if (rename(tmppath, cachefile))
1285 			err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1286 		umask((mask = umask(0)));
1287 		if (chmod(cachefile,
1288 		    (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1289 			err(1, "chmod: '%s'", cachefile);
1290 	}
1291 
1292 	/* cleanup */
1293 	git_repository_free(repo);
1294 	git_libgit2_shutdown();
1295 
1296 	return 0;
1297 }
,D08IDATxoW~ߌ<2."gQ@Fe$֓/a|%B܃)3e! @/A^6bm`Y[!)")8,В3o/S^oN N ڴtJL 0)U$`JY:6TGLg)J!JNEfGز͙mNN 2Tiqc;•bNP3q)c`""'&h !&$Nqsr@I ֤m&zSuRRmantF9!k:$򄦸O5 ZB47Dʌ+0 Cz:t: Wq/8 I ̖Dys6YPcq./Nf(ˇ;PXtyej6qB}$5/e)5B"8JIUXtHMRѰ0xxЩŷœ$.2›gR $>H{fv|~0G_Ŝ=^0P Pc:q*NkT;Ib861MцSyKkv>>] o\[芏4&@= c 7%aa{bPa 95/P4lyId(PUt2TPw`uۮXsn۱y|,gv%0& ZK \PYu[$zȵ*X'dL֪D0)%k$q%r8ܜkd'Be{M09Wp 13M]_n﮶QKǹX8pRWhJIO`L :C zycwAUPBPbʍV[ t&OGVEۑC´HwQpS=9"MsUפjƎ6۔P,RhY2\hV`ѷ\&.#Zѡ}`]4 FN̾d t[Ey17m݅9]㖛7spGrxVo6\S&st0jbN *`ãh)wj[4`H=Z%ίStI3 ][Z+.6&Aߏ$Â( ZVE@ r撤G<d(liʃ ^d۬2\Q,VsU31M*pk(Xv *ODv{ܯVR)$7/^R.cElX+2SB_M8~ca1%M ~;5dh=0E^:כs9zdt-wvKd;O 9D Mu3@n :ז!^vA}6ͫ f]PǷY5bǺjslxgs%x ?yؼP%T| 95DXVɐ^χelp 9,!k j( &c 0;lټ }0ׇ9C+J/ཧ(?!Z6`~R5eu φ͎HǷ ]`3: Df?fK.ӫ!#j}3@ T~l+*Uθ̡񇁶L"VŮXjA3j4C_p0׫ه:SYj٘,t043FNOG_42a[ V{f6D+h0 o[ 3ϛE#:B+ǨV vo=q-XbL?։{^74pnC#1 ׊8ع얰RXR0S6߷'A y?Uҳ (o_Y,j^RN>FP +1YX> 0_ wgVDk$uZ ![zZf_~Vb̮Qz;Q[-J6܍giml% HvYVktN1ݯ/JDbP6{)+zeCfr>D)A{" c9ˑ{J@ldsYPC9Ha8Q}ͱϲxV1Fn)?EnƮ'*<'0$/:/ˣvݦ U>- gVh(yw|چ[&JF 5d˨nSv!e rL=kitXLn ? s-r?*6 Ef-ln.&h hge7AIɎC҆wjwQTmcSfz Xr5-IG)h[C8=1a<(fƲo`#y&O멗v( [c4PrX|[,7nN@*+ v9sŀe8 pD|xAD:!qt*g̖4kK+AOL /u3~&͍|GPc'V1FY|K a/nނK$0&M짖X ;PO@f*>w:>fC] `c)C".˒]gḽN 4>~b8lp]cu {xE{FkHL#.wb75Ċd?_U /V4J)0iȉ ֟V@>",w@`9a e;zN>6H0֛v5,j*¨{< &qqXhafi7%CWNCˀX-_H}!u xM ,ǝž}$4`cEnvg['3'frҼNn^5mXqQsfKD7 Rۮ&tmۡ!4G@疬 yI7/ftM2@#%uYeNO#ՄtUPc<׽2f$ʊBAr16яJnFhp#]aN\&a2z>=ģ(=f°}ȃn@AT F7@^p;qھx"bXy[nvX<fN@rm\bW\C`f!B,WnL\ACW/ʬp';ߥca6c1qIIENDB`