git.y1.nz

sgw

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

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