git home / emma home
logo

sgw

Static git web. Emma's fork of stagit.
git clone https://git.y1.nz/archives/sgw.tar.gz
README | Files | Log | Refs | LICENSE

main.c


      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 <stdint.h>
      8 #include <stdio.h>
      9 #include <stdlib.h>
     10 #include <string.h>
     11 #include <time.h>
     12 #include <unistd.h>
     13 #include <limits.h>
     14 
     15 #include <git2.h>
     16 
     17 #include "compat.h"
     18 #include "opt.h"
     19 #include "files.h"
     20 
     21 #define CPUT(f, c)  putc((c), (f))
     22 #define put_xml(fp, s)  put_xml_len((fp), (s), strlen(s))
     23 #define SPUTF(f, s)  fputs((s), (f))
     24 #define LENGTH(s)  (sizeof(s)/sizeof(*s))
     25 
     26 #include "config.h"
     27 
     28 struct DeltaInfo {
     29 	git_patch *patch;
     30 
     31 	size_t add_count;
     32 	size_t del_count;
     33 };
     34 
     35 struct CommitInfo {
     36 	const git_oid *id;
     37 
     38 	char oid[GIT_OID_HEXSZ + 1];
     39 	char parentoid[GIT_OID_HEXSZ + 1];
     40 
     41 	const git_signature *author;
     42 	const git_signature *committer;
     43 	const char *summary;
     44 	const char *msg;
     45 
     46 	git_diff *diff;
     47 	git_commit *commit;
     48 	git_commit *parent;
     49 	git_tree *commit_tree;
     50 	git_tree *parent_tree;
     51 
     52 	size_t add_count;
     53 	size_t del_count;
     54 	size_t file_count;
     55 
     56 	struct DeltaInfo **deltas;
     57 	size_t ndeltas;
     58 };
     59 
     60 /* reference and associated data for sorting */
     61 struct ReferenceInfo {
     62 	struct git_reference *ref;
     63 	struct CommitInfo *ci;
     64 };
     65 
     66 struct Weight {
     67 	size_t bytes;
     68 	size_t lines;
     69 };
     70 
     71 static const char orders[] = { ' ', 'k', 'M', 'G', 'T', 'P', 'E' };
     72 static const char tab[] = "0123456789ABCDEF";
     73 
     74 /* GLOBALS */
     75 // TODO untangle these, pass them around instead...
     76 
     77 static git_repository *repo;
     78 
     79 /* flags + arguments */
     80 static Opt opt;
     81 static const char *repo_dir;
     82 
     83 static const char *license_files[] = { LICENSE_FILES };
     84 static const char *readme_files[] = { README_FILES };
     85 
     86 // TODO extract these into a struct. LMFAO
     87 static char *name = "";
     88 static char *repo_name = "";
     89 static char description[255];
     90 static char clone_url[1024];
     91 static char *submodules;
     92 static const char *license;
     93 static const char *readme;
     94 
     95 /* bad globals for index.... */
     96 // TODO move this stuff to index.h?
     97 static FILE *index_fp;
     98 static char index_fname[255] = "index.html";
     99 
    100 void find_owner();
    101 int put_index_log(FILE *fp, git_repository *repo, const char *description, const char *repo_name, const char *readme);
    102 void put_index_header(FILE *fp);
    103 void put_index_footer(FILE *fp);
    104 
    105 void
    106 get_page_path(char *buf, size_t buf_size, const char *dir, char *name)
    107 {
    108 	int index = 0, chars = 0;
    109 
    110 	// TODO tweak this so that it correctly places slashes between names
    111 	chars = snprintf(buf + index, buf_size - index, "%s", dir);
    112 	if (chars < 0 || (size_t) (index += chars) >= buf_size)
    113 		goto page_path_err;
    114 
    115 	if (chars != 0) buf[index++] = '/';
    116 
    117 	chars = snprintf(buf + index, buf_size - index, PAGE_DEST);
    118 	if (chars < 0 || (size_t) (index += chars) >= buf_size)
    119 		goto page_path_err;
    120 
    121 	if (chars != 0) buf[index++] = '/';
    122 
    123 	chars = snprintf(buf + index, buf_size - index, "%s", name);
    124 	if (chars < 0 || (size_t) (index += chars) >= buf_size)
    125 		goto page_path_err;
    126 
    127 	return;
    128 
    129 page_path_err:
    130 	errx(1, "page path truncated! '%s' / '%s'" "\n", dir, name);
    131 }
    132 
    133 void
    134 get_index_page_path(char *buf, size_t buf_size, const char *dir, char *name)
    135 {
    136 	int index = 0, chars = 0;
    137 
    138 	// TODO tweak this so that it correctly places slashes between names
    139 	chars = snprintf(buf + index, buf_size - index, "%s", dir);
    140 	if (chars < 0 || (size_t) (index += chars) >= buf_size)
    141 		goto page_path_err;
    142 
    143 	const char *format = dir[0] == '\0' ? "%s" : "/%s";
    144 	chars = snprintf(buf + index, buf_size - index, format, name);
    145 	if (chars < 0 || (size_t) (index += chars) >= buf_size)
    146 		goto page_path_err;
    147 
    148 	printf("Index page path: %s" "\n", buf);
    149 
    150 	return;
    151 
    152 page_path_err:
    153 	errx(1, "page path truncated! '%s' / '%s'" "\n", dir, name);
    154 }
    155 
    156 FILE *
    157 git_fopen(char *path, int path_size, const char *file)
    158 {
    159 	FILE *ret;
    160 
    161 	/* try dir/(whatever), */
    162 	/* (+5 removes the leading '.git/'.) */
    163 	join_path(path, path_size, repo_dir, file + 5); 
    164 	if ((ret = fopen(path, "r"))) return ret;
    165 
    166 	/* then try dir/.git/(whatever). */
    167 	join_path(path, path_size, repo_dir, file);
    168 	return fopen(path, "r");
    169 }
    170 
    171 void
    172 find_license()
    173 {
    174 	git_object *obj = NULL;
    175 	int i;
    176 	for (i = 0; i < LENGTH(license_files) && !license; i++) {
    177 		if (
    178 			!git_revparse_single(&obj, repo, license_files[i]) &&
    179 			git_object_type(obj) == GIT_OBJ_BLOB
    180 		)
    181 			license = license_files[i] + strlen("HEAD:");
    182 		git_object_free(obj);
    183 	}
    184 }
    185 
    186 void
    187 find_readme()
    188 {
    189 	git_object *obj = NULL;
    190 	int i;
    191 	readme = NULL;
    192 	for (i = 0; i < LENGTH(readme_files) && !readme; i++) {
    193 		if (
    194 			!git_revparse_single(&obj, repo, readme_files[i]) &&
    195 			git_object_type(obj) == GIT_OBJ_BLOB
    196 		)
    197 			readme = readme_files[i] + strlen("HEAD:");
    198 		git_object_free(obj);
    199 	}
    200 }
    201 
    202 void
    203 delta_info_free(struct DeltaInfo *di)
    204 {
    205 	if (!di) return;
    206 
    207 	git_patch_free(di->patch);
    208 	memset(di, 0, sizeof(*di));
    209 	free(di);
    210 }
    211 
    212 int
    213 commit_info_get_stats(struct CommitInfo *ci)
    214 {
    215 	struct DeltaInfo *di;
    216 	git_diff_options opts;
    217 	git_diff_find_options fopts;
    218 	const git_diff_delta *delta;
    219 	const git_diff_hunk *hunk;
    220 	const git_diff_line *line;
    221 	git_patch *patch = NULL;
    222 	size_t ndeltas, nhunks, nhunklines;
    223 	size_t i, j, k;
    224 
    225 	if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
    226 		goto err;
    227 	if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
    228 		if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
    229 			ci->parent = NULL;
    230 			ci->parent_tree = NULL;
    231 		}
    232 	}
    233 
    234 	git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
    235 	opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
    236 		GIT_DIFF_IGNORE_SUBMODULES |
    237 		GIT_DIFF_INCLUDE_TYPECHANGE;
    238 	if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
    239 		goto err;
    240 
    241 	if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
    242 		goto err;
    243 	/* find renames and copies, exact matches (no heuristic) for renames. */
    244 	fopts.flags |= GIT_DIFF_FIND_RENAMES |
    245 		GIT_DIFF_FIND_COPIES |
    246 		GIT_DIFF_FIND_EXACT_MATCH_ONLY;
    247 	if (git_diff_find_similar(ci->diff, &fopts))
    248 		goto err;
    249 
    250 	ndeltas = git_diff_num_deltas(ci->diff);
    251 	if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct DeltaInfo *))))
    252 		err(1, "calloc");
    253 
    254 	for (i = 0; i < ndeltas; i++) {
    255 		if (git_patch_from_diff(&patch, ci->diff, i))
    256 			goto err;
    257 
    258 		if (!(di = calloc(1, sizeof(struct DeltaInfo))))
    259 			err(1, "calloc");
    260 		di->patch = patch;
    261 		ci->deltas[i] = di;
    262 
    263 		delta = git_patch_get_delta(patch);
    264 
    265 		/* skip stats for binary data */
    266 		if (delta->flags & GIT_DIFF_FLAG_BINARY)
    267 			continue;
    268 
    269 		nhunks = git_patch_num_hunks(patch);
    270 		for (j = 0; j < nhunks; j++) {
    271 			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
    272 				break;
    273 			for (k = 0; ; k++) {
    274 				if (git_patch_get_line_in_hunk(&line, patch, j, k))
    275 					break;
    276 				if (line->old_lineno == -1) {
    277 					di->add_count++;
    278 					ci->add_count++;
    279 				} else if (line->new_lineno == -1) {
    280 					di->del_count++;
    281 					ci->del_count++;
    282 				}
    283 			}
    284 		}
    285 	}
    286 	ci->ndeltas = i;
    287 	ci->file_count = i;
    288 
    289 	return 0;
    290 
    291 err:
    292 	git_diff_free(ci->diff);
    293 	ci->diff = NULL;
    294 	git_tree_free(ci->commit_tree);
    295 	ci->commit_tree = NULL;
    296 	git_tree_free(ci->parent_tree);
    297 	ci->parent_tree = NULL;
    298 	git_commit_free(ci->parent);
    299 	ci->parent = NULL;
    300 
    301 	if (ci->deltas)
    302 		for (i = 0; i < ci->ndeltas; i++)
    303 			delta_info_free(ci->deltas[i]);
    304 	free(ci->deltas);
    305 	ci->deltas = NULL;
    306 	ci->ndeltas = 0;
    307 	ci->add_count = 0;
    308 	ci->del_count = 0;
    309 	ci->file_count = 0;
    310 
    311 	return -1;
    312 }
    313 
    314 void
    315 commit_info_free(struct CommitInfo *ci)
    316 {
    317 	size_t i;
    318 
    319 	if (!ci)
    320 		return;
    321 	if (ci->deltas)
    322 		for (i = 0; i < ci->ndeltas; i++)
    323 			delta_info_free(ci->deltas[i]);
    324 
    325 	free(ci->deltas);
    326 	git_diff_free(ci->diff);
    327 	git_tree_free(ci->commit_tree);
    328 	git_tree_free(ci->parent_tree);
    329 	git_commit_free(ci->commit);
    330 	git_commit_free(ci->parent);
    331 	memset(ci, 0, sizeof(*ci));
    332 	free(ci);
    333 }
    334 
    335 struct CommitInfo *
    336 commit_info_get_by_oid(const git_oid *id)
    337 {
    338 	struct CommitInfo *ci;
    339 
    340 	if (!(ci = calloc(1, sizeof(struct CommitInfo))))
    341 		err(1, "calloc");
    342 
    343 	if (git_commit_lookup(&(ci->commit), repo, id))
    344 		goto err;
    345 	ci->id = id;
    346 
    347 	git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
    348 	git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
    349 
    350 	ci->author = git_commit_author(ci->commit);
    351 	ci->committer = git_commit_committer(ci->commit);
    352 	ci->summary = git_commit_summary(ci->commit);
    353 	ci->msg = git_commit_message(ci->commit);
    354 
    355 	return ci;
    356 
    357 err:
    358 	commit_info_free(ci);
    359 
    360 	return NULL;
    361 }
    362 
    363 int
    364 refs_cmp(const void *v1, const void *v2)
    365 {
    366 	const struct ReferenceInfo *r1 = v1, *r2 = v2;
    367 	time_t t1, t2;
    368 	int r;
    369 
    370 	if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
    371 		return r;
    372 
    373 	t1 = r1->ci->author ? r1->ci->author->when.time : 0;
    374 	t2 = r2->ci->author ? r2->ci->author->when.time : 0;
    375 	if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
    376 		return r;
    377 
    378 	return strcmp(
    379 		git_reference_shorthand(r1->ref),
    380 		git_reference_shorthand(r2->ref)
    381 	);
    382 }
    383 
    384 int
    385 get_refs(struct ReferenceInfo **pris, size_t *prefcount)
    386 {
    387 	struct ReferenceInfo *ris = NULL;
    388 	struct CommitInfo *ci = NULL;
    389 	git_reference_iterator *it = NULL;
    390 	const git_oid *id = NULL;
    391 	git_object *obj = NULL;
    392 	git_reference *dref = NULL, *r, *ref = NULL;
    393 	size_t i, refcount;
    394 
    395 	*pris = NULL;
    396 	*prefcount = 0;
    397 
    398 	if (git_reference_iterator_new(&it, repo))
    399 		return -1;
    400 
    401 	for (refcount = 0; !git_reference_next(&ref, it); ) {
    402 		if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
    403 			git_reference_free(ref);
    404 			ref = NULL;
    405 			continue;
    406 		}
    407 
    408 		switch (git_reference_type(ref)) {
    409 		case GIT_REF_SYMBOLIC:
    410 			if (git_reference_resolve(&dref, ref))
    411 				goto err;
    412 			r = dref;
    413 			break;
    414 		case GIT_REF_OID:
    415 			r = ref;
    416 			break;
    417 		default:
    418 			continue;
    419 		}
    420 		if (
    421 			!git_reference_target(r) ||
    422 			git_reference_peel(&obj, r, GIT_OBJ_ANY)
    423 		)
    424 			goto err;
    425 		if (!(id = git_object_id(obj)))
    426 			goto err;
    427 		if (!(ci = commit_info_get_by_oid(id)))
    428 			break;
    429 
    430 		if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
    431 			err(1, "realloc");
    432 		ris[refcount].ci = ci;
    433 		ris[refcount].ref = r;
    434 		refcount++;
    435 
    436 		git_object_free(obj);
    437 		obj = NULL;
    438 		git_reference_free(dref);
    439 		dref = NULL;
    440 	}
    441 	git_reference_iterator_free(it);
    442 
    443 	/* sort by type, date then shorthand name */
    444 	qsort(ris, refcount, sizeof(*ris), refs_cmp);
    445 
    446 	*pris = ris;
    447 	*prefcount = refcount;
    448 
    449 	return 0;
    450 
    451 err:
    452 	git_object_free(obj);
    453 	git_reference_free(dref);
    454 	commit_info_free(ci);
    455 	for (i = 0; i < refcount; i++) {
    456 		commit_info_free(ris[i].ci);
    457 		git_reference_free(ris[i].ref);
    458 	}
    459 	free(ris);
    460 
    461 	return -1;
    462 }
    463 
    464 /* Percent-encode, see RFC3986 section 2.1. */
    465 void
    466 put_percent_char(FILE *fp, unsigned char uc)
    467 {
    468 	/* NOTE: do not encode '/' for paths or ",-." */
    469 	if (
    470 		uc < ',' ||
    471 		uc >= 127 ||
    472 		(uc >= ':' && uc <= '@') ||
    473 		uc == '[' ||
    474 		uc == ']'
    475 	) {
    476 		CPUT(fp, '%');
    477 		CPUT(fp, tab[(uc >> 4) & 0x0f]);
    478 		CPUT(fp, tab[uc & 0x0f]);
    479 		return;
    480 	}
    481 
    482 	CPUT(fp, uc);
    483 }
    484 
    485 void
    486 put_percent_encoded(FILE *fp, const char *s, size_t len)
    487 {
    488 	size_t i;
    489 
    490 	for (i = 0; *s && i < len; s++, i++)
    491 		put_percent_char(fp, *s);
    492 }
    493 
    494 /* Escape characters below as HTML 2.0 / XML 1.0. */
    495 void
    496 put_xml_char(FILE *fp, const char *s)
    497 {
    498 	switch(*s) {
    499 		case '<': fputs("&lt;", fp); break;
    500 		case '>': fputs("&gt;", fp); break;
    501 		case '\'': fputs("&#39;", fp); break;
    502 		case '&': fputs("&amp;", fp); break;
    503 		case '"': fputs("&quot;", fp); break;
    504 		default: putc(*s, fp);
    505 	}
    506 }
    507 
    508 void
    509 put_xml_len(FILE *fp, const char *s, size_t len)
    510 {
    511 	size_t i = 0;
    512 	for (; *s && i < len; s++, i++)
    513 		put_xml_char(fp, s);
    514 }
    515 
    516 /* terminate on, but don't print '\r' or '\n' */
    517 void
    518 put_xml_line(FILE *fp, const char *s, size_t len)
    519 {
    520 	size_t i = 0;
    521 	for (; *s && i < len; s++, i++) {
    522 		if (*s == '\r' || *s == '\n')
    523 			break;
    524 		put_xml_char(fp, s);
    525 	}
    526 }
    527 
    528 void
    529 printtimez(FILE *fp, const git_time *intime)
    530 {
    531 	struct tm *intm;
    532 	time_t t;
    533 	char out[32];
    534 
    535 	t = (time_t)intime->time;
    536 	if (!(intm = gmtime(&t)))
    537 		return;
    538 	strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
    539 	fputs(out, fp);
    540 }
    541 
    542 void
    543 printtime(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 + (intime->offset * 60);
    550 	if (!(intm = gmtime(&t)))
    551 		return;
    552 	strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
    553 	if (intime->offset < 0) {
    554 		fprintf(
    555 			fp,
    556 			"%s -%02d%02d",
    557 			out,
    558 			-(intime->offset) / 60,
    559 			-(intime->offset) % 60
    560 		);
    561 	} else {
    562 		fprintf(
    563 			fp,
    564 			"%s +%02d%02d", out,
    565 			intime->offset / 60,
    566 			intime->offset % 60
    567 		);
    568 	}
    569 }
    570 
    571 void
    572 printtimeshort(FILE *fp, const git_time *intime)
    573 {
    574 	struct tm *intm;
    575 	time_t t;
    576 	char out[32];
    577 
    578 	t = (time_t)intime->time;
    579 	if (!(intm = gmtime(&t)))
    580 		return;
    581 	strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
    582 	fputs(out, fp);
    583 }
    584 
    585 void
    586 put_absolute_path(FILE *fp, const char * path)
    587 {
    588 	if (opt.base_url[0] != '\0')
    589 		fprintf(fp, "/%s", opt.base_url);
    590 	CPUT(fp, '/');
    591 	fprintf(fp, PAGE_DEST); /* should have no leading/trailing slashes */
    592 	fprintf(fp, "/%s", path);
    593 }
    594 
    595 void
    596 put_header(FILE *fp, const char *title)
    597 {
    598 	fputs(
    599 		"<!DOCTYPE html>\n"
    600 		"<html>" "\n"
    601 		"<head>" "\n"
    602 		"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
    603 		"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
    604 		"<title>",
    605 		fp
    606 	);
    607 	put_xml(fp, title);
    608 	if (title[0] && repo_name[0])
    609 		fputs(" - ", fp);
    610 	put_xml(fp, repo_name);
    611 	if (description[0])
    612 		fputs(" - ", fp);
    613 	put_xml(fp, description);
    614 	SPUTF(fp, "</title>" "\n");
    615 
    616 	SPUTF(fp, "<link rel=\"icon\" type=\"image/png\" href=\"");
    617 	put_absolute_path(fp, "favicon.png");
    618 	SPUTF(fp, "\" />" "\n");
    619 
    620 	SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"");
    621 	put_xml(fp, name);
    622 	SPUTF(fp, " Atom Feed\" href=\"");
    623 	put_absolute_path(fp, "atom.xml");
    624 	SPUTF(fp, "\" />" "\n");
    625 
    626 	SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"");
    627 	put_xml(fp, name);
    628 	SPUTF(fp, " Atom Feed (tags)\" href=\"");
    629 	put_absolute_path(fp, "tags.xml");
    630 	SPUTF(fp, "\" />" "\n");
    631 
    632 	SPUTF(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"");
    633 	fprintf(fp, PAGE_STYLE_HREF);
    634 	SPUTF(fp, "\" />" "\n");
    635 	SPUTF(fp, "</head>" "\n" "<body>" "\n");
    636 	
    637 	fprintf(fp, PAGE_HEADER);
    638 
    639 	SPUTF(fp, "<table>" "<tr><td>");
    640 
    641 	SPUTF(fp, "<a href=\"");
    642 	fprintf(fp, PAGE_LOGO_HREF);
    643 	SPUTF(fp, "\"><img alt=\"logo\" width=\"32\" height=\"32\" src=\"");
    644 	fprintf(fp, PAGE_LOGO_SRC);
    645 	SPUTF(fp, "\"></a>");
    646 
    647 	SPUTF(fp, "</td><td>" "<h1>");
    648 	put_xml(fp, repo_name);
    649 	SPUTF(fp, "</h1>" "<span class=\"desc\">");
    650 	put_xml(fp, description);
    651 	SPUTF(fp, "</span>" "</td></tr>");
    652 	if (clone_url[0]) {
    653 		SPUTF(fp, "<tr class=\"url\"><td></td><td>git clone <a href=\"");
    654 		put_xml(fp, clone_url); /* not percent encoded */
    655 		SPUTF(fp, "\">");
    656 		put_xml(fp, clone_url);
    657 		SPUTF(fp, "</a></td></tr>");
    658 	}
    659 	SPUTF(fp, "<tr><td></td><td>" "\n");
    660 
    661 	if (readme) {
    662 		SPUTF(fp, "<a href=\"");
    663 		put_absolute_path(fp, "files/");
    664 		fprintf(fp, "%s.html", readme);
    665 		SPUTF(fp, "\">README</a> | ");
    666 	}
    667 	
    668 	SPUTF(fp, "<a href=\"");
    669 	put_absolute_path(fp, "files.html");
    670 	SPUTF(fp, "\">Files</a> | ");
    671 
    672 	SPUTF(fp, "<a href=\"");
    673 	put_absolute_path(fp, "log.html");
    674 	SPUTF(fp, "\">Log</a> | ");
    675 
    676 	SPUTF(fp, "<a href=\"");
    677 	put_absolute_path(fp, "refs.html");
    678 	SPUTF(fp, "\">Refs</a>");
    679 
    680 	if (submodules) {
    681 		SPUTF(fp, " | <a href=\"");
    682 		put_absolute_path(fp, "files/");
    683 		fprintf(fp, "%s.html", submodules);
    684 		SPUTF(fp, "\">Submodules</a>");
    685 	}
    686 
    687 	if (license) {
    688 		SPUTF(fp, " | <a href=\"");
    689 		put_absolute_path(fp, "files/");
    690 		fprintf(fp, "%s.html", license);
    691 		SPUTF(fp, "\">LICENSE</a>");
    692 	}
    693 
    694 	SPUTF(fp, "</td></tr></table>" "\n");
    695 	SPUTF(fp, "<hr/>" "\n");
    696 
    697 	SPUTF(fp, "<div id=\"content\">" "\n");
    698 }
    699 
    700 void
    701 put_footer(FILE *fp)
    702 {
    703 	SPUTF(fp, "</div>" "\n");
    704 	fprintf(fp, PAGE_FOOTER);
    705 	SPUTF(fp, "</body>" "\n" "</html>" "\n");
    706 }
    707 
    708 size_t
    709 put_file_html(FILE *fp, const git_blob *blob)
    710 {
    711 	size_t n = 0, i, len, prev;
    712 	const char *number_format =
    713 		"<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu </a>";
    714 	const char *s = git_blob_rawcontent(blob);
    715 
    716 	len = git_blob_rawsize(blob);
    717 	SPUTF(fp, "<pre id=\"blob\">" "\n");
    718 
    719 	if (len > 0) {
    720 		for (i = 0, prev = 0; i < len; i++) {
    721 			if (s[i] != '\n')
    722 				continue;
    723 			n++;
    724 			fprintf(fp, number_format, n, n, n);
    725 			put_xml_line(fp, &s[prev], i - prev + 1);
    726 			SPUTF(fp, "\n");
    727 			prev = i + 1;
    728 		}
    729 		/* trailing data */
    730 		if ((len - prev) > 0) {
    731 			n++;
    732 			fprintf(fp, number_format, n, n, n);
    733 			put_xml_line(fp, &s[prev], len - prev);
    734 		}
    735 	}
    736 
    737 	SPUTF(fp, "</pre>" "\n");
    738 
    739 	return n;
    740 }
    741 
    742 void
    743 print_commit(FILE *fp, struct CommitInfo *ci)
    744 {
    745 	SPUTF(fp, "<b>commit</b> <a href=\"");
    746 	put_absolute_path(fp, "commits/");
    747 	fprintf(fp, "%s.html", ci->oid);
    748 	fprintf(fp, "\">%s</a>" "\n", ci->oid);
    749 
    750 	if (ci->parentoid[0]) {
    751 		SPUTF(fp, "<b>parent</b> <a href=\"");
    752 		put_absolute_path(fp, "commits/");
    753 		fprintf(fp, "%s.html", ci->parentoid);
    754 		fprintf(fp, "\">%s</a>" "\n", ci->parentoid);
    755 	}
    756 
    757 	if (ci->author) {
    758 		SPUTF(fp, "<b>Author:</b> ");
    759 		put_xml(fp, ci->author->name);
    760 		SPUTF(fp, " &lt;<a href=\"mailto:");
    761 		put_xml(fp, ci->author->email); /* not percent encoded */
    762 		SPUTF(fp, "\">");
    763 		put_xml(fp, ci->author->email);
    764 		SPUTF(fp, "</a>&gt;\n<b>Date:</b>   ");
    765 		printtime(fp, &(ci->author->when));
    766 		CPUT(fp, '\n');
    767 	}
    768 	if (ci->msg) {
    769 		CPUT(fp, '\n');
    770 		put_xml(fp, ci->msg);
    771 		CPUT(fp, '\n');
    772 	}
    773 }
    774 
    775 void
    776 put_show_file(FILE *fp, struct CommitInfo *ci)
    777 {
    778 	const git_diff_delta *delta;
    779 	const git_diff_hunk *hunk;
    780 	const git_diff_line *line;
    781 	git_patch *patch;
    782 	size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
    783 	char linestr[80];
    784 	int c;
    785 
    786 	print_commit(fp, ci);
    787 
    788 	if (!ci->deltas)
    789 		return;
    790 
    791 	if (
    792 		ci->file_count > 1000 ||
    793 		ci->ndeltas > 1000 ||
    794 		ci->add_count > 100000 ||
    795 		ci->del_count > 100000
    796 	) {
    797 		fputs("Diff is too large, output suppressed.\n", fp);
    798 		return;
    799 	}
    800 
    801 	/* diff stat */
    802 	fputs("<b>Diffstat:</b>" "\n", fp);
    803 	fputs("<table id=\"diffstat\">", fp);
    804 	for (i = 0; i < ci->ndeltas; i++) {
    805 		delta = git_patch_get_delta(ci->deltas[i]->patch);
    806 
    807 		switch (delta->status) {
    808 		case GIT_DELTA_ADDED: c = 'A'; break;
    809 		case GIT_DELTA_COPIED: c = 'C'; break;
    810 		case GIT_DELTA_DELETED: c = 'D'; break;
    811 		case GIT_DELTA_MODIFIED: c = 'M'; break;
    812 		case GIT_DELTA_RENAMED: c = 'R'; break;
    813 		case GIT_DELTA_TYPECHANGE: c = 'T'; break;
    814 		default: c = ' '; break;
    815 		}
    816 		if (c == ' ')
    817 			fprintf(fp, "<tr><td>%c", c);
    818 		else
    819 			fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
    820 
    821 		fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
    822 		put_xml(fp, delta->old_file.path);
    823 		if (strcmp(delta->old_file.path, delta->new_file.path)) {
    824 			fputs(" -&gt; ", fp);
    825 			put_xml(fp, delta->new_file.path);
    826 		}
    827 
    828 		add = ci->deltas[i]->add_count;
    829 		del = ci->deltas[i]->del_count;
    830 		changed = add + del;
    831 		total = sizeof(linestr) - 2;
    832 		if (changed > total) {
    833 			if (add)
    834 				add = ((float)total / changed * add) + 1;
    835 			if (del)
    836 				del = ((float)total / changed * del) + 1;
    837 		}
    838 		memset(&linestr, '+', add);
    839 		memset(&linestr[add], '-', del);
    840 
    841 		fprintf(
    842 			fp,
    843 			"</a></td>"
    844 			// "<td> | </td>"
    845 			"<td class=\"num\">%zu</td><td><span class=\"i\">",
    846 			ci->deltas[i]->add_count + ci->deltas[i]->del_count
    847 		);
    848 		fwrite(&linestr, 1, add, fp);
    849 		fputs("</span><span class=\"d\">", fp);
    850 		fwrite(&linestr[add], 1, del, fp);
    851 		fputs("</span></td></tr>\n", fp);
    852 	}
    853 	fprintf(
    854 		fp,
    855 		"</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
    856 		ci->file_count, ci->file_count == 1 ? "" : "s",
    857 		ci->add_count, ci->add_count == 1 ? "" : "s",
    858 		ci->del_count, ci->del_count == 1 ? "" : "s"
    859 	);
    860 
    861 	fputs("<hr/>", fp);
    862 
    863 	for (i = 0; i < ci->ndeltas; i++) {
    864 		patch = ci->deltas[i]->patch;
    865 		delta = git_patch_get_delta(patch);
    866 
    867 		SPUTF(fp, "<b>diff --git a/");
    868 		put_xml(fp, delta->old_file.path);
    869 		fprintf(fp, " b/<a id=\"h%zu\" href=\"#h%zu\">", i, i);
    870 		put_xml(fp, delta->new_file.path);
    871 		SPUTF(fp, "</a></b>" "\n");
    872 
    873 		/* check binary data */
    874 		if (delta->flags & GIT_DIFF_FLAG_BINARY) {
    875 			SPUTF(fp, "Binary files differ." "\n");
    876 			continue;
    877 		}
    878 
    879 		nhunks = git_patch_num_hunks(patch);
    880 		for (j = 0; j < nhunks; j++) {
    881 			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
    882 				break;
    883 
    884 			fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
    885 			put_xml_len(fp, hunk->header, hunk->header_len);
    886 			SPUTF(fp, "</a>");
    887 
    888 			for (k = 0; ; k++) {
    889 				if (git_patch_get_line_in_hunk(&line, patch, j, k))
    890 					break;
    891 				if (line->old_lineno == -1)
    892 					fprintf(
    893 						fp,
    894 						"<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
    895 						i, j, k, i, j, k
    896 					);
    897 				else if (line->new_lineno == -1)
    898 					fprintf(
    899 						fp,
    900 						"<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
    901 						i, j, k, i, j, k
    902 					);
    903 				else
    904 					CPUT(fp, ' ');
    905 				put_xml_line(fp, line->content, line->content_len);
    906 				CPUT(fp, '\n');
    907 				if (line->old_lineno == -1 || line->new_lineno == -1)
    908 					SPUTF(fp, "</a>");
    909 			}
    910 		}
    911 	}
    912 }
    913 
    914 void
    915 put_log_line(FILE *fp, struct CommitInfo *ci)
    916 {
    917 	fputs("<tr><td>", fp);
    918 	if (ci->author)
    919 		printtimeshort(fp, &(ci->author->when));
    920 	fputs("</td><td>", fp);
    921 	if (ci->summary) {
    922 		SPUTF(fp, "<a href=\"");
    923 		put_absolute_path(fp, "commits/");
    924 		fprintf(fp, "%s.html", ci->oid);
    925 		SPUTF(fp, "\">");
    926 		put_xml(fp, ci->summary);
    927 		SPUTF(fp, "</a>");
    928 	}
    929 	SPUTF(fp, "</td><td>");
    930 	if (ci->author) put_xml(fp, ci->author->name);
    931 	SPUTF(fp, "</td><td class=\"num\" align=\"right\">");
    932 	fprintf(fp, "%zu", ci->file_count);
    933 	SPUTF(fp, "</td><td class=\"num\" align=\"right\">");
    934 	fprintf(fp, "+%zu", ci->add_count);
    935 	SPUTF(fp, "</td><td class=\"num\" align=\"right\">");
    936 	fprintf(fp, "-%zu", ci->del_count);
    937 	SPUTF(fp, "</td></tr>" "\n");
    938 }
    939 
    940 void
    941 write_commit_page(char *name, struct CommitInfo *ci)
    942 {
    943 	char fname[PATH_MAX];
    944 	get_page_path(fname, sizeof(fname), opt.out_dir, name);
    945 	FILE *fp = fopen_w(fname);
    946 	// [old] fpfile = efopen(path, "w");
    947 
    948 	put_header(fp, ci->summary);
    949 
    950 	SPUTF(fp, "<pre>");
    951 	put_show_file(fp, ci);
    952 	SPUTF(fp, "</pre>" "\n");
    953 
    954 	put_footer(fp);
    955 	check_file_error(fp, fname, 'w');
    956 	fclose(fp);
    957 }
    958 
    959 int
    960 put_log(FILE *fp, const git_oid *oid)
    961 {
    962 	struct CommitInfo *ci;
    963 	git_revwalk *w = NULL;
    964 	git_oid id;
    965 	char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
    966 	size_t remcommits = 0;
    967 	int r;
    968 	int commits_left = MAX_COMMITS;
    969 
    970 	git_revwalk_new(&w, repo);
    971 	git_revwalk_push(w, oid);
    972 
    973 	// TODO avoid processing very large commits
    974 	while (!git_revwalk_next(&id, w)) {
    975 		git_oid_tostr(oidstr, sizeof(oidstr), &id);
    976 		r = snprintf(path, sizeof(path), "commits/%s.html", oidstr);
    977 		if (r < 0 || (size_t)r >= sizeof(path))
    978 			errx(1, "path truncated: 'commits/%s.html'", oidstr);
    979 		r = access(path, F_OK);
    980 
    981 		/* optimization: if there are no log lines to write and
    982 		   the commit file already exists: skip the diffstat */
    983 		if (!commits_left) {
    984 			remcommits++;
    985 			if (!r)
    986 				continue;
    987 		}
    988 
    989 		if (!(ci = commit_info_get_by_oid(&id)))
    990 			break;
    991 		/* diffstat: for stagit HTML required for the log.html line */
    992 		if (commit_info_get_stats(ci) == -1)
    993 			goto err;
    994 
    995 		if (commits_left != 0) {
    996 			put_log_line(fp, ci);
    997 			if (commits_left > 0)
    998 				commits_left--;
    999 		}
   1000 
   1001 		/* check if file exists if so skip it */
   1002 		if (r) write_commit_page(path, ci);
   1003 
   1004 err:
   1005 		commit_info_free(ci);
   1006 	}
   1007 	git_revwalk_free(w);
   1008 
   1009 	if (commits_left == 0 && remcommits != 0)
   1010 		fprintf(
   1011 			fp,
   1012 			"<tr><td></td><td colspan=\"5\">"
   1013 			"%zu more commits remaining, fetch the repository"
   1014 			"</td></tr>" "\n",
   1015 			remcommits
   1016 		);
   1017 
   1018 	return 0;
   1019 }
   1020 
   1021 void
   1022 put_log_html(FILE *fp, const git_oid *head)
   1023 {
   1024 	put_header(fp, "Log");
   1025 	fputs(
   1026 		"<table id=\"log\"><thead>" "\n"
   1027 		"<tr>"
   1028 		"<td><b>Date</b></td>"
   1029 		"<td><b>Commit message</b></td>"
   1030 		"<td><b>Author</b></td>"
   1031 		"<td class=\"num\" align=\"right\"><b>Files</b></td>"
   1032 		"<td class=\"num\" align=\"right\"><b>+</b></td>"
   1033 		"<td class=\"num\" align=\"right\"><b>-</b></td>"
   1034 		"</tr>" "\n"
   1035 		"</thead><tbody>" "\n",
   1036 		fp
   1037 	);
   1038 
   1039 	if (head) put_log(fp, head);
   1040 
   1041 	fputs("</tbody></table>", fp);
   1042 	put_footer(fp);
   1043 }
   1044 
   1045 void
   1046 print_commit_atom(FILE *fp, struct CommitInfo *ci, const char *tag)
   1047 {
   1048 	fputs("<entry>\n", fp);
   1049 
   1050 	fprintf(fp, "<id>%s</id>\n", ci->oid);
   1051 	if (ci->author) {
   1052 		SPUTF(fp, "<published>");
   1053 		printtimez(fp, &(ci->author->when));
   1054 		SPUTF(fp, "</published>\n");
   1055 	}
   1056 	if (ci->committer) {
   1057 		SPUTF(fp, "<updated>");
   1058 		printtimez(fp, &(ci->committer->when));
   1059 		SPUTF(fp, "</updated>\n");
   1060 	}
   1061 	if (ci->summary) {
   1062 		SPUTF(fp, "<title>");
   1063 		if (tag && tag[0]) {
   1064 			SPUTF(fp, "[");
   1065 			put_xml(fp, tag);
   1066 			SPUTF(fp, "] ");
   1067 		}
   1068 		put_xml(fp, ci->summary);
   1069 		SPUTF(fp, "</title>\n");
   1070 	}
   1071 	SPUTF(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"");
   1072 	put_absolute_path(fp, "commits/");
   1073 	fprintf(fp, "%s.html\" />" "\n", ci->oid);
   1074 
   1075 	if (ci->author) {
   1076 		SPUTF(fp, "<author>" "\n" "<name>");
   1077 		put_xml(fp, ci->author->name);
   1078 		SPUTF(fp, "</name>" "\n" "<email>");
   1079 		put_xml(fp, ci->author->email);
   1080 		SPUTF(fp, "</email>" "\n" "</author>" "\n");
   1081 	}
   1082 
   1083 	SPUTF(fp, "<content>");
   1084 	fprintf(fp, "commit %s" "\n", ci->oid);
   1085 	if (ci->parentoid[0])
   1086 		fprintf(fp, "parent %s" "\n", ci->parentoid);
   1087 	if (ci->author) {
   1088 		SPUTF(fp, "Author: ");
   1089 		put_xml(fp, ci->author->name);
   1090 		SPUTF(fp, " &lt;");
   1091 		put_xml(fp, ci->author->email);
   1092 		SPUTF(fp, "&gt;" "\n" "Date:   ");
   1093 		printtime(fp, &(ci->author->when));
   1094 		CPUT(fp, '\n');
   1095 	}
   1096 	if (ci->msg) {
   1097 		CPUT(fp, '\n');
   1098 		put_xml(fp, ci->msg);
   1099 	}
   1100 	SPUTF(fp, "\n" "</content>" "\n" "</entry>" "\n");
   1101 }
   1102 
   1103 int
   1104 write_atom(FILE *fp, int all)
   1105 {
   1106 	struct ReferenceInfo *ris = NULL;
   1107 	size_t refcount = 0;
   1108 	struct CommitInfo *ci;
   1109 	git_revwalk *w = NULL;
   1110 	git_oid id;
   1111 	size_t i, m = 100; /* last 'm' commits */
   1112 
   1113 	SPUTF(
   1114 		fp,
   1115 		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" "\n"
   1116 		"<feed xmlns=\"http://www.w3.org/2005/Atom\">" "\n" "<title>"
   1117 	);
   1118 	put_xml(fp, repo_name);
   1119 	SPUTF(fp, ", branch HEAD</title>" "\n" "<subtitle>");
   1120 	put_xml(fp, description);
   1121 	SPUTF(fp, "</subtitle>" "\n");
   1122 
   1123 	/* all commits or only tags? */
   1124 	if (all) {
   1125 		git_revwalk_new(&w, repo);
   1126 		git_revwalk_push_head(w);
   1127 		for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
   1128 			if (!(ci = commit_info_get_by_oid(&id)))
   1129 				break;
   1130 			print_commit_atom(fp, ci, "");
   1131 			commit_info_free(ci);
   1132 		}
   1133 		git_revwalk_free(w);
   1134 	} else if (get_refs(&ris, &refcount) != -1) {
   1135 		/* references: tags */
   1136 		for (i = 0; i < refcount; i++) {
   1137 			if (git_reference_is_tag(ris[i].ref))
   1138 				print_commit_atom(
   1139 					fp, ris[i].ci,
   1140 					git_reference_shorthand(ris[i].ref)
   1141 				);
   1142 
   1143 			commit_info_free(ris[i].ci);
   1144 			git_reference_free(ris[i].ref);
   1145 		}
   1146 		free(ris);
   1147 	}
   1148 
   1149 	fputs("</feed>\n", fp);
   1150 
   1151 	return 0;
   1152 }
   1153 
   1154 void
   1155 write_binary_file(const char *filename, git_blob *blob) {
   1156 	char dest[PATH_MAX];
   1157 	const char *raw = git_blob_rawcontent(blob);
   1158 	int size = git_blob_rawsize(blob);
   1159 
   1160 	/* set dest */
   1161 	join_path(dest, PATH_MAX, "files", filename);
   1162 
   1163 	/* write the file! */
   1164 	char fname[PATH_MAX];
   1165 	get_page_path(fname, sizeof(fname), opt.out_dir, dest);
   1166 	FILE *fp = fopen_w(fname);
   1167 
   1168 	fwrite(raw, size, 1, fp);
   1169 	check_file_error(fp, fname, 'w');
   1170 	fclose(fp);
   1171 }
   1172 
   1173 void
   1174 put_size(FILE *fp, size_t size, int fixed)
   1175 {
   1176 	int order = 0;
   1177 	int decimal;
   1178 
   1179 	if (size == 0) {
   1180 		fprintf(fp, "  - ");
   1181 		return;
   1182 	}
   1183 
   1184 	while (size > 1000) {
   1185 		order++;
   1186 		decimal = (size / 100) % 10;
   1187 		size /= 1000;
   1188 	}
   1189 
   1190 	if (size < 10 && order > 0) fprintf(fp, "%1zu.%1d", size, decimal);
   1191 	else fprintf(fp, fixed ? "%3zu" : "%zu", size);
   1192 
   1193 	if (order != 0 || fixed) fprintf(fp, "%c", orders[order]);
   1194 }
   1195 
   1196 void
   1197 put_file_title(FILE *fp, const char *name, int is_dir)
   1198 {
   1199 	int i = 0;
   1200 	int len = 0;
   1201 
   1202 	/* project root */
   1203 	if (name[0] == '\0')
   1204 		return;
   1205 
   1206 	if (!DIR_PAGES) {
   1207 		fprintf(fp, "<p>%s</p>", name);
   1208 		return;
   1209 	}
   1210 
   1211 	if (BACKLINKS && !is_dir) {
   1212 		for (i = 0; name[i] != '\0'; i++)
   1213 			if (name[i] == '/') len = i;	
   1214 
   1215 		if (len != 0) {
   1216 			SPUTF(fp, "<p><a href=\"");
   1217 			put_absolute_path(fp, "files/");
   1218 			for (i = 0; i < len; i++) put_percent_char(fp, name[i]);
   1219 			SPUTF(fp, ".html\">");
   1220 			for (i = 0; i < len; i++) put_percent_char(fp, name[i]);
   1221 			fprintf(fp, "</a>%s</p>", name + len);
   1222 			if (!is_dir) SPUTF(fp, "<hr>");
   1223 			return;
   1224 		}
   1225 	}
   1226 
   1227 
   1228 	fprintf(fp, "<p>%s</p>", name);
   1229 	if (!is_dir) SPUTF(fp, "<hr>");
   1230 }
   1231 
   1232 void
   1233 put_binary_blob_stub(FILE *fp, const char *file_url)
   1234 {
   1235 	SPUTF(fp, "<p>Binary file: <a href=\"");
   1236 	put_absolute_path(fp, "files/");
   1237 	fprintf(fp, "%s\">raw</a></p>" "\n", file_url);
   1238 	SPUTF(fp, "<object data=\"");
   1239 	put_absolute_path(fp, "files/");
   1240 	fprintf(fp, "%s\">", file_url);
   1241 	SPUTF(fp, "no preview available.</object></p>" "\n");
   1242 }
   1243 
   1244 struct Weight
   1245 write_blob(
   1246 	git_blob *blob,
   1247 	char *filename,
   1248 	const char *entrypath,
   1249 	const char *name
   1250 ) {
   1251 	char fname[PATH_MAX];
   1252 	FILE *fp;
   1253 	struct Weight ret;
   1254 
   1255 	ret.lines = 0;
   1256 	ret.bytes = 0;
   1257 
   1258 	get_page_path(fname, sizeof(fname), opt.out_dir, filename);
   1259 	fp = fopen_w(fname);
   1260 	
   1261 	put_header(fp, name);
   1262 	put_file_title(fp, entrypath, 0);
   1263 
   1264 	if (git_blob_is_binary(blob)) {
   1265 		put_binary_blob_stub(fp, entrypath); // name);
   1266 		write_binary_file(entrypath, blob);
   1267 		ret.bytes = git_blob_rawsize(blob);
   1268 	} else {
   1269 		ret.lines = put_file_html(fp, blob);
   1270 	}
   1271 
   1272 	put_footer(fp);
   1273 	check_file_error(fp, fname, 'w');
   1274 	fclose(fp);
   1275 
   1276 	return ret;
   1277 }
   1278 
   1279 char
   1280 get_filemode_type(git_filemode_t m)
   1281 {
   1282 	if (S_ISREG(m)) return '-';
   1283 	if (S_ISBLK(m)) return 'b';
   1284 	if (S_ISCHR(m)) return 'c';
   1285 	if (S_ISDIR(m)) return 'd';
   1286 	if (S_ISFIFO(m)) return 'p';
   1287 	if (S_ISLNK(m)) return 'l';
   1288 	if (S_ISSOCK(m)) return 's';
   1289 	return '?';
   1290 }
   1291 
   1292 const char *
   1293 get_filemode(const git_tree_entry *entry)
   1294 {
   1295 	git_filemode_t mode;
   1296 	static char ret[11];
   1297 
   1298 	/* used for backlink: .. */
   1299 	if (entry == NULL) return "d---------";
   1300 
   1301 	mode = git_tree_entry_filemode(entry);
   1302 
   1303 	memset(ret, '-', sizeof(ret) - 1);
   1304 	ret[0] = get_filemode_type(mode);
   1305 	if (mode & S_IRUSR) ret[1] = 'r';
   1306 	if (mode & S_IWUSR) ret[2] = 'w';
   1307 	if (mode & S_IXUSR) ret[3] = 'x';
   1308 	if (mode & S_ISUID) ret[3] = (ret[3] == 'x') ? 's' : 'S';
   1309 	if (mode & S_IRGRP) ret[4] = 'r';
   1310 	if (mode & S_IWGRP) ret[5] = 'w';
   1311 	if (mode & S_IXGRP) ret[6] = 'x';
   1312 	if (mode & S_ISGID) ret[6] = (ret[6] == 'x') ? 's' : 'S';
   1313 	if (mode & S_IROTH) ret[7] = 'r';
   1314 	if (mode & S_IWOTH) ret[8] = 'w';
   1315 	if (mode & S_IXOTH) ret[9] = 'x';
   1316 	if (mode & S_ISVTX) ret[9] = (ret[9] == 'x') ? 't' : 'T';
   1317 	ret[10] = '\0';
   1318 
   1319 	return ret;
   1320 }
   1321 
   1322 void
   1323 put_filetree_file_line(
   1324 	FILE *fp,
   1325 	git_object *obj,
   1326 	const git_tree_entry *entry,
   1327 	char *filename,
   1328 	char *entrypath,
   1329 	const char *name,
   1330 	struct Weight weight
   1331 ) {
   1332 	SPUTF(fp, "<tr><td>");
   1333 	SPUTF(fp, get_filemode(entry));
   1334 	SPUTF(fp, "</td><td><a href=\"");
   1335 	put_absolute_path(fp, "");
   1336 	put_percent_encoded(fp, filename, strlen(filename));
   1337 	SPUTF(fp, "\">");
   1338 	if (DIR_PAGES) put_xml(fp, name);
   1339 	else put_xml(fp, entrypath);
   1340 	SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">");
   1341 	put_size(fp, weight.lines, 1);
   1342 	SPUTF(fp, "</td><td class=\"num\" align=\"right\">");
   1343 	put_size(fp, weight.bytes, 1);
   1344 	SPUTF(fp, "</td></tr>\n");
   1345 }
   1346 
   1347 void
   1348 put_filetree_dir_line(
   1349 	FILE *fp,
   1350 	const git_tree_entry *entry,
   1351 	char *destination,
   1352 	const char *title,
   1353 	size_t lines,
   1354 	size_t bytes
   1355 ) {
   1356 	SPUTF(fp, "<tr><td>");
   1357 	SPUTF(fp, get_filemode(entry));
   1358 	SPUTF(fp, "</td><td><a href=\"");
   1359 	put_absolute_path(fp, "");
   1360 	put_percent_encoded(fp, destination, strlen(destination));
   1361 	SPUTF(fp, "\">");
   1362 	put_xml(fp, title);
   1363 	SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">");
   1364 	put_size(fp, lines, 1);
   1365 	SPUTF(fp, "</td><td class=\"num\" align=\"right\">");
   1366 	put_size(fp, bytes, 1);
   1367 	SPUTF(fp, "</td></tr>\n");
   1368 }
   1369 
   1370 void put_filetree_backlink_line(FILE *fp, const char *path) {
   1371 	char previous_path[PATH_MAX];
   1372 	char previous_filename[PATH_MAX];
   1373 	int i, r, last_slash = 0;
   1374 	
   1375 	/* figure out previous directory */
   1376 	// TODO how to do this more cleanly...
   1377 	for (i = 0; path[i] != '\0'; i++) {
   1378 		previous_path[i] = path[i];
   1379 		if (path[i] == '/') last_slash = i;
   1380 	}
   1381 	previous_path[last_slash] = '\0';
   1382 
   1383 	if (last_slash == 0) {
   1384 		put_filetree_dir_line(fp, NULL, "files.html", "..", 0, 0);
   1385 	} else {
   1386 		r = snprintf(previous_filename, sizeof(previous_filename), "files/%s.html", previous_path);
   1387 		if (r < 0 || (size_t) r >= sizeof(previous_filename))
   1388 			errx(1, "path truncated: 'files/%s.html'", previous_path);
   1389 		put_filetree_dir_line(fp, NULL, previous_filename, "..", 0, 0);
   1390 	}
   1391 }
   1392 
   1393 /* need this here to allow recursion */
   1394 struct Weight write_filetree(
   1395 	const char *path,
   1396 	const char *filename,
   1397 	git_tree *tree
   1398 );
   1399 struct Weight put_file_list(FILE *fp, git_tree *tree, const char *path);
   1400 
   1401 struct Weight
   1402 put_entry_obj(FILE *fp, git_object *obj, const git_tree_entry *entry, char *entrypath, const char *name)
   1403 {
   1404 	struct Weight ret;
   1405 	git_object_t obj_type = git_object_type(obj);
   1406 	char filename[PATH_MAX];
   1407 	int r;
   1408 	
   1409 	/* filename = f("files/%s.html", entrypath) */
   1410 	r = snprintf(filename, sizeof(filename), "files/%s.html", entrypath);
   1411 	if (r < 0 || (size_t) r >= sizeof(filename))
   1412 		errx(1, "path truncated: 'files/%s.html'", entrypath);
   1413 
   1414 	switch (obj_type) {
   1415 	case GIT_OBJ_TREE:
   1416 		if (DIR_PAGES) {
   1417 			/* create dir page */
   1418 			ret = write_filetree(entrypath, filename, (git_tree *) obj);
   1419 			
   1420 			/* add link to that dir page */
   1421 			put_filetree_dir_line(fp, entry, filename, name, ret.lines, ret.bytes);
   1422 		} else {
   1423 			/* list files on this page */
   1424 			ret = put_file_list(fp, (git_tree *) obj, entrypath);
   1425 		}
   1426 		break;
   1427 	case GIT_OBJ_BLOB:
   1428 		ret = write_blob((git_blob *) obj, filename, entrypath, name);
   1429 		put_filetree_file_line(fp, obj, entry, filename, entrypath, name, ret);
   1430 		break;
   1431 	default: break;
   1432 	}
   1433 
   1434 	return ret;
   1435 }
   1436 
   1437 void
   1438 put_submodule_obj(FILE *fp, const git_tree_entry *entry, const char *name)
   1439 {
   1440 	char oid[8];
   1441 
   1442 	SPUTF(fp, "<tr>" "<td>m---------</td>" "<td><a href=\"");
   1443 	put_absolute_path(fp, "files/.gitmodules.html");
   1444 	SPUTF(fp, "\">");
   1445 	put_xml(fp, name);
   1446 	SPUTF(fp, "</a> @ ");
   1447 	git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
   1448 	put_xml(fp, oid);
   1449 	SPUTF(fp, "</td>" "<td class=\"num\" align=\"right\"></td>" "\n");
   1450 	SPUTF(fp, "<td class=\"num\" align=\"right\"></td>" "</tr>" "\n");
   1451 }
   1452 
   1453 struct Weight
   1454 put_file_list(FILE *fp, git_tree *tree, const char *path)
   1455 {
   1456 	struct Weight ret;
   1457 	size_t count;
   1458 	size_t i;
   1459 
   1460 	ret.lines = ret.bytes = 0;
   1461 
   1462 	count = git_tree_entrycount(tree);
   1463 	for (i = 0; i < count; i++) {
   1464 		const git_tree_entry *entry = NULL;
   1465 		const char *name;
   1466 		char entrypath[PATH_MAX];
   1467 		git_object *obj = NULL;
   1468 
   1469 		if (
   1470 			!(entry = git_tree_entry_byindex(tree, i)) ||
   1471 			!(name = git_tree_entry_name(entry))
   1472 		)
   1473 			return ret;
   1474 
   1475 		combine_paths(entrypath, path, name);
   1476 
   1477 		if (!git_tree_entry_to_object(&obj, repo, entry)) {
   1478 			struct Weight weight =
   1479 				put_entry_obj(fp, obj, entry, entrypath, name);
   1480 			ret.bytes += weight.bytes;
   1481 			ret.lines += weight.lines;
   1482 			git_object_free(obj);
   1483 			continue;
   1484 		}
   1485 
   1486 		if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
   1487 			put_submodule_obj(fp, entry, name);
   1488 			continue;
   1489 		}
   1490 	}
   1491 
   1492 	return ret;
   1493 }
   1494 
   1495 struct Weight
   1496 write_filetree(const char *path, const char *filename, git_tree *tree)
   1497 {
   1498 	struct Weight ret;
   1499 	char fname[PATH_MAX] = "";
   1500 	FILE *fp;
   1501 
   1502 	get_page_path(fname, sizeof(fname), opt.out_dir, (char *) filename);
   1503 	fp = fopen_w(fname);
   1504 
   1505 	put_header(fp, "Files");
   1506 	
   1507 	put_file_title(fp, path, 1);
   1508 
   1509 	SPUTF(
   1510 		fp,
   1511 		"<table id=\"files\"><thead>" "\n" "<tr>"
   1512 		"<td><b>Mode</b></td><td><b>Name</b></td>"
   1513 		"<td class=\"num\" align=\"right\"><b>  L </b></td>"
   1514 		"<td class=\"num\" align=\"right\"><b>  B </b></td>"
   1515 		"</tr>" "\n" "</thead><tbody>" "\n"
   1516 	);
   1517 
   1518 	if (tree != NULL) {
   1519 		if (BACKLINKS && path[0] != '\0')
   1520 			put_filetree_backlink_line(fp, path);
   1521 
   1522 		ret = put_file_list(fp, tree, path);
   1523 	}
   1524 
   1525 	SPUTF(fp, "</tbody></table>");
   1526 
   1527 	// TODO how can this get printed before the contents...
   1528 	/*
   1529 	SPUTF(fp, "<p>Totals: ");
   1530 	put_size(fp, ret.lines, 0);
   1531 	SPUTF(fp, "L ");
   1532 	put_size(fp, ret.bytes, 0);
   1533 	SPUTF(fp, "B</p>");
   1534 	*/
   1535 
   1536 	put_footer(fp);
   1537 	check_file_error(fp, fname, 'w');
   1538 	fclose(fp);
   1539 
   1540 	return ret;
   1541 }
   1542 
   1543 void
   1544 walk_git_tree(const git_oid *id)
   1545 {
   1546 	git_tree *tree = NULL;
   1547 	git_commit *commit = NULL;
   1548 	int has_files;
   1549 	
   1550 	if (!id) return;
   1551 
   1552 	has_files = !git_commit_lookup(&commit, repo, id) &&
   1553 		!git_commit_tree(&tree, commit);
   1554 
   1555 	write_filetree("", "files.html", has_files ? tree : NULL);
   1556 
   1557 	git_commit_free(commit);
   1558 	git_tree_free(tree);
   1559 }
   1560 
   1561 int
   1562 put_refs(FILE *fp)
   1563 {
   1564 	struct ReferenceInfo *ris = NULL;
   1565 	struct CommitInfo *ci;
   1566 	size_t count, i, j, refcount;
   1567 	const char *titles[] = { "Branches", "Tags" };
   1568 	const char *ids[] = { "branches", "tags" };
   1569 	const char *s;
   1570 
   1571 	if (get_refs(&ris, &refcount) == -1)
   1572 		return -1;
   1573 
   1574 	for (i = 0, j = 0, count = 0; i < refcount; i++) {
   1575 		if (j == 0 && git_reference_is_tag(ris[i].ref)) {
   1576 			if (count)
   1577 				fputs("</tbody></table><br/>\n", fp);
   1578 			count = 0;
   1579 			j = 1;
   1580 		}
   1581 
   1582 		/* print header if it has an entry (first). */
   1583 		if (++count == 1)
   1584 			fprintf(
   1585 				fp,
   1586 				"<h2>%s</h2><table id=\"%s\">"
   1587 				"<thead>\n<tr><td><b>Name</b></td>"
   1588 				"<td><b>Last commit date</b></td>"
   1589 				"<td><b>Author</b></td>\n</tr>\n"
   1590 				"</thead><tbody>\n",
   1591 				titles[j],
   1592 				ids[j]
   1593 			);
   1594 
   1595 		ci = ris[i].ci;
   1596 		s = git_reference_shorthand(ris[i].ref);
   1597 
   1598 		fputs("<tr><td>", fp);
   1599 		put_xml(fp, s);
   1600 		fputs("</td><td>", fp);
   1601 		if (ci->author)
   1602 			printtimeshort(fp, &(ci->author->when));
   1603 		fputs("</td><td>", fp);
   1604 		if (ci->author)
   1605 			put_xml(fp, ci->author->name);
   1606 		fputs("</td></tr>\n", fp);
   1607 	}
   1608 
   1609 	if (count)
   1610 		fputs("</tbody></table><br/>\n", fp);
   1611 
   1612 	for (i = 0; i < refcount; i++) {
   1613 		commit_info_free(ris[i].ci);
   1614 		git_reference_free(ris[i].ref);
   1615 	}
   1616 	free(ris);
   1617 
   1618 	return 0;
   1619 }
   1620 
   1621 void
   1622 write_log_page(const git_oid *head)
   1623 {
   1624 	FILE *fp;
   1625 	char fname[PATH_MAX];
   1626 
   1627 	get_page_path(fname, sizeof(fname), opt.out_dir, "log.html");
   1628 	fp = fopen_w(fname);
   1629 
   1630 	put_log_html(fp, head);
   1631 
   1632 	check_file_error(fp, fname, 'w');
   1633 	fclose(fp);
   1634 }
   1635 
   1636 void
   1637 write_refs_page()
   1638 {
   1639 	FILE *fp;
   1640 	char fname[PATH_MAX];
   1641 
   1642 	get_page_path(fname, sizeof(fname), opt.out_dir, "refs.html");
   1643 	fp = fopen_w(fname);
   1644 
   1645 	put_header(fp, "Refs");
   1646 	put_refs(fp);
   1647 	put_footer(fp);
   1648 
   1649 	check_file_error(fp, fname, 'w');
   1650 	fclose(fp);
   1651 }
   1652 
   1653 void
   1654 write_feed()
   1655 {
   1656 	FILE *fp;
   1657 	char fname[PATH_MAX];
   1658 
   1659 	get_page_path(fname, sizeof(fname), opt.out_dir, "atom.xml");
   1660 	fp = fopen_w(fname);
   1661 
   1662 	write_atom(fp, 1);
   1663 
   1664 	check_file_error(fp, fname, 'w');
   1665 	fclose(fp);
   1666 }
   1667 
   1668 void
   1669 write_releases_feed()
   1670 {
   1671 	FILE *fp;
   1672 	char fname[PATH_MAX];
   1673 
   1674 	get_page_path(fname, sizeof(fname), opt.out_dir, "tags.xml");
   1675 	fp = fopen_w(fname);
   1676 
   1677 	write_atom(fp, 0);
   1678 
   1679 	check_file_error(fp, fname, 'w');
   1680 	fclose(fp);
   1681 }
   1682 
   1683 void
   1684 find_submodules()
   1685 {
   1686 	git_object *obj = NULL;
   1687 	if (
   1688 		!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
   1689 		git_object_type(obj) == GIT_OBJ_BLOB
   1690 	)
   1691 		submodules = ".gitmodules";
   1692 	git_object_free(obj);
   1693 }
   1694 
   1695 void
   1696 find_description()
   1697 {
   1698 	char path[PATH_MAX];
   1699 	FILE *fp;
   1700 	if ((fp = git_fopen(path, sizeof(path), ".git/description"))) {
   1701 		if (!fgets(description, sizeof(description), fp))
   1702 			description[0] = '\0';
   1703 		check_file_error(fp, path, 'r');
   1704 		fclose(fp);
   1705 	}
   1706 }
   1707 
   1708 void
   1709 find_clone_url()
   1710 {
   1711 	char path[PATH_MAX];
   1712 	FILE *fp;
   1713 	if ((fp = git_fopen(path, sizeof(path), ".git/url"))) {
   1714 		if (!fgets(clone_url, sizeof(clone_url), fp))
   1715 			clone_url[0] = '\0';
   1716 		check_file_error(fp, path, 'r');
   1717 		fclose(fp);
   1718 
   1719 		// TODO ???
   1720 		clone_url[strcspn(clone_url, "\n")] = '\0';
   1721 	}
   1722 }
   1723 
   1724 void
   1725 find_repo_name()
   1726 {
   1727 	char abs_dir[PATH_MAX + 1];
   1728 	char *p;
   1729 
   1730 	/* set abs_dir */
   1731 	if (!realpath(repo_dir, abs_dir))
   1732 		err(1, "realpath");
   1733 
   1734 	if ((name = strrchr(abs_dir, '/'))) name++;
   1735 	else name = "";
   1736 
   1737 	if (!(repo_name = strdup(name))) err(1, "strdup");
   1738 	if ((p = strrchr(repo_name, '.')))
   1739 		if (!strcmp(p, ".git"))
   1740 			*p = '\0';
   1741 
   1742 	if (repo_name[0] == '\0')
   1743 		repo_name = "untitled";
   1744 }
   1745 
   1746 int
   1747 write_repo_pages(const char* dir)
   1748 {
   1749 
   1750 	// TODO fully stop using globals like this....
   1751 	repo_dir = dir;
   1752 
   1753 #ifdef __OpenBSD__
   1754 	if (unveil(repo_dir, "r") == -1) err(1, "unveil: %s", repo_dir);
   1755 	if (unveil(".", "rwc") == -1) err(1, "unveil: .");
   1756 
   1757 	/*
   1758 	if (cachefile) {
   1759 		if (unveil(cachefile, "rwc") == -1) err(1, "unveil: %s", cachefile);
   1760 		if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) err(1, "pledge");
   1761 	} else {
   1762 		if (pledge("stdio rpath wpath cpath", NULL) == -1) err(1, "pledge");
   1763 	}
   1764 	*/
   1765 #endif
   1766 
   1767 	/* open the git repo */
   1768 	int result = git_repository_open_ext(
   1769 		&repo, repo_dir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL
   1770 	);
   1771 	if (result < 0) {
   1772 		fprintf(stderr, "fatal: cannot open repository '%s'" "\n", dir);
   1773 		return 1;
   1774 	}
   1775 
   1776 	/* find HEAD */
   1777 	const git_oid *head = NULL;
   1778 	git_object *obj = NULL;
   1779 	if (!git_revparse_single(&obj, repo, "HEAD"))
   1780 		head = git_object_id(obj);
   1781 	git_object_free(obj);
   1782 
   1783 	/* set global vars... */
   1784 	// TODO repo = open_repo();
   1785 	find_repo_name();
   1786 	find_description();
   1787 	find_clone_url();
   1788 	find_license();
   1789 	find_readme();
   1790 	find_submodules();
   1791 	if (opt.index) find_owner();
   1792 
   1793 	if (opt.index) {
   1794 		put_index_log(index_fp, repo, description, repo_name, readme);
   1795 	}
   1796 
   1797 	if (opt.pages) {
   1798 		write_log_page(head);
   1799 		walk_git_tree(head);
   1800 		write_refs_page();
   1801 		write_feed();
   1802 		write_releases_feed();
   1803 	}
   1804 
   1805 	/* clean up */
   1806 	// TODO close_repo();
   1807 	git_repository_free(repo);
   1808 
   1809 	return 0;
   1810 }
   1811 
   1812 void
   1813 begin_index()
   1814 {
   1815 	get_index_page_path(
   1816 		index_fname,
   1817 		sizeof(index_fname),
   1818 		opt.out_dir,
   1819 		INDEX_DEST
   1820 	);
   1821 	index_fp = fopen_w(index_fname);
   1822 	put_index_header(index_fp);
   1823 }
   1824 
   1825 void
   1826 end_index()
   1827 {
   1828 	put_index_footer(index_fp);	
   1829 	check_file_error(index_fp, index_fname, 'w');
   1830 	fclose(index_fp);
   1831 }
   1832 
   1833 void
   1834 init_lib_git()
   1835 {
   1836 	int i;
   1837 
   1838 	/* do not search outside the git repository:
   1839 	   GIT_CONFIG_LEVEL_APP is the highest level currently */
   1840 	git_libgit2_init();
   1841 	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
   1842 		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
   1843 
   1844 	/* do not require the git repository to be owned by the current user */
   1845 	git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
   1846 }
   1847 
   1848 int
   1849 main(int argc, const char *argv[])
   1850 {
   1851 	int i, result;
   1852 
   1853 	if (parse_opts(&opt, argc, argv) == OPT_FAIL) {
   1854 		PRINT_USAGE(argv[0]);
   1855 		return 1;
   1856 	}
   1857 
   1858 	if (!opt.pages && !opt.index) {
   1859 		printf("Nothing to do, use -i and/or -p." "\n\n");
   1860 		PRINT_USAGE(argv[0]);
   1861 		return 1;
   1862 	}
   1863 
   1864 	init_lib_git();
   1865 
   1866 	if (opt.index) begin_index();
   1867 	for (i = 0; i < opt.repo_count; i++) {
   1868 		result = write_repo_pages(opt.repo_dirs[i]);
   1869 		if(result > 0) return 1;
   1870 	}
   1871 	if (opt.index) end_index();
   1872 
   1873 	git_libgit2_shutdown();
   1874 
   1875 	return 0;
   1876 }

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.