neocgit

a more 'modern' version of cgit
Log | Files | Refs | Submodules | README | LICENSE | git clone https://git.ne02ptzero.me/git/neocgit

ui-shared.c (34278B)


      1 /* ui-shared.c: common web output functions
      2  *
      3  * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
      4  *
      5  * Licensed under GNU General Public License v2
      6  *   (see COPYING for full license text)
      7  */
      8 
      9 #include "cgit.h"
     10 #include "ui-shared.h"
     11 #include "cmd.h"
     12 #include "html.h"
     13 #include "version.h"
     14 #include "ui-log.h"
     15 #include "ui-refs.h"
     16 
     17 static const char cgit_doctype[] =
     18 "<!DOCTYPE html>\n";
     19 
     20 static char *http_date(time_t t)
     21 {
     22 	static char day[][4] =
     23 		{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
     24 	static char month[][4] =
     25 		{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
     26 		 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
     27 	struct tm *tm = gmtime(&t);
     28 	return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday],
     29 		   tm->tm_mday, month[tm->tm_mon], 1900 + tm->tm_year,
     30 		   tm->tm_hour, tm->tm_min, tm->tm_sec);
     31 }
     32 
     33 void cgit_print_error(const char *fmt, ...)
     34 {
     35 	va_list ap;
     36 	va_start(ap, fmt);
     37 	cgit_vprint_error(fmt, ap);
     38 	va_end(ap);
     39 }
     40 
     41 void cgit_vprint_error(const char *fmt, va_list ap)
     42 {
     43 	va_list cp;
     44 	html("<div class='error'>");
     45 	va_copy(cp, ap);
     46 	html_vtxtf(fmt, cp);
     47 	va_end(cp);
     48 	html("</div>\n");
     49 }
     50 
     51 const char *cgit_httpscheme(void)
     52 {
     53 	if (ctx.env.https && !strcmp(ctx.env.https, "on"))
     54 		return "https://";
     55 	else
     56 		return "http://";
     57 }
     58 
     59 char *cgit_hosturl(void)
     60 {
     61 	if (ctx.env.http_host)
     62 		return xstrdup(ctx.env.http_host);
     63 	if (!ctx.env.server_name)
     64 		return NULL;
     65 	if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
     66 		return xstrdup(ctx.env.server_name);
     67 	return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
     68 }
     69 
     70 char *cgit_currenturl(void)
     71 {
     72 	const char *root = cgit_rooturl();
     73 	size_t len = strlen(root);
     74 
     75 	if (!ctx.qry.url)
     76 		return xstrdup(root);
     77 	if (len && root[len - 1] == '/')
     78 		return fmtalloc("%s%s", root, ctx.qry.url);
     79 	return fmtalloc("%s/%s", root, ctx.qry.url);
     80 }
     81 
     82 const char *cgit_rooturl(void)
     83 {
     84 	if (ctx.cfg.virtual_root)
     85 		return ctx.cfg.virtual_root;
     86 	else
     87 		return ctx.cfg.script_name;
     88 }
     89 
     90 const char *cgit_loginurl(void)
     91 {
     92 	static const char *login_url;
     93 	if (!login_url)
     94 		login_url = fmtalloc("%s?p=login", cgit_rooturl());
     95 	return login_url;
     96 }
     97 
     98 char *cgit_repourl(const char *reponame)
     99 {
    100 	if (ctx.cfg.virtual_root)
    101 		return fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame);
    102 	else
    103 		return fmtalloc("?r=%s", reponame);
    104 }
    105 
    106 char *cgit_fileurl(const char *reponame, const char *pagename,
    107 		   const char *filename, const char *query)
    108 {
    109 	struct strbuf sb = STRBUF_INIT;
    110 	char *delim;
    111 
    112 	if (ctx.cfg.virtual_root) {
    113 		strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame,
    114 			    pagename, (filename ? filename:""));
    115 		delim = "?";
    116 	} else {
    117 		strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename,
    118 			    (filename ? filename : ""));
    119 		delim = "&amp;";
    120 	}
    121 	if (query)
    122 		strbuf_addf(&sb, "%s%s", delim, query);
    123 	return strbuf_detach(&sb, NULL);
    124 }
    125 
    126 char *cgit_pageurl(const char *reponame, const char *pagename,
    127 		   const char *query)
    128 {
    129 	return cgit_fileurl(reponame, pagename, NULL, query);
    130 }
    131 
    132 const char *cgit_repobasename(const char *reponame)
    133 {
    134 	/* I assume we don't need to store more than one repo basename */
    135 	static char rvbuf[1024];
    136 	int p;
    137 	const char *rv;
    138 	strncpy(rvbuf, reponame, sizeof(rvbuf));
    139 	if (rvbuf[sizeof(rvbuf)-1])
    140 		die("cgit_repobasename: truncated repository name '%s'", reponame);
    141 	p = strlen(rvbuf)-1;
    142 	/* strip trailing slashes */
    143 	while (p && rvbuf[p] == '/') rvbuf[p--] = 0;
    144 	/* strip trailing .git */
    145 	if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) {
    146 		p -= 3; rvbuf[p--] = 0;
    147 	}
    148 	/* strip more trailing slashes if any */
    149 	while ( p && rvbuf[p] == '/') rvbuf[p--] = 0;
    150 	/* find last slash in the remaining string */
    151 	rv = strrchr(rvbuf,'/');
    152 	if (rv)
    153 		return ++rv;
    154 	return rvbuf;
    155 }
    156 
    157 const char *cgit_snapshot_prefix(const struct cgit_repo *repo)
    158 {
    159 	if (repo->snapshot_prefix)
    160 		return repo->snapshot_prefix;
    161 
    162 	return cgit_repobasename(repo->url);
    163 }
    164 
    165 static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
    166 {
    167 	char *delim = "?";
    168 
    169 	if (always_root || page)
    170 		html_attr(cgit_rooturl());
    171 	else {
    172 		char *currenturl = cgit_currenturl();
    173 		html_attr(currenturl);
    174 		free(currenturl);
    175 	}
    176 
    177 	if (page) {
    178 		htmlf("?p=%s", page);
    179 		delim = "&amp;";
    180 	}
    181 	if (search) {
    182 		html(delim);
    183 		html("q=");
    184 		html_attr(search);
    185 		delim = "&amp;";
    186 	}
    187 	if (sort) {
    188 		html(delim);
    189 		html("s=");
    190 		html_attr(sort);
    191 		delim = "&amp;";
    192 	}
    193 	if (ofs) {
    194 		html(delim);
    195 		htmlf("ofs=%d", ofs);
    196 	}
    197 }
    198 
    199 static void site_link(const char *page, const char *name, const char *title,
    200 		      const char *class, const char *search, const char *sort, int ofs, int always_root)
    201 {
    202 	html("<a");
    203 	if (title) {
    204 		html(" title='");
    205 		html_attr(title);
    206 		html("'");
    207 	}
    208 	if (class) {
    209 		html(" class='");
    210 		html_attr(class);
    211 		html("'");
    212 	}
    213 	html(" href='");
    214 	site_url(page, search, sort, ofs, always_root);
    215 	html("'>");
    216 	html_txt(name);
    217 	html("</a>");
    218 }
    219 
    220 void cgit_index_link(const char *name, const char *title, const char *class,
    221 		     const char *pattern, const char *sort, int ofs, int always_root)
    222 {
    223 	site_link(NULL, name, title, class, pattern, sort, ofs, always_root);
    224 }
    225 
    226 static char *repolink(const char *title, const char *class, const char *page,
    227 		      const char *head, const char *path)
    228 {
    229 	char *delim = "?";
    230 
    231 	html("<a");
    232 	if (title) {
    233 		html(" title='");
    234 		html_attr(title);
    235 		html("'");
    236 	}
    237 	if (class) {
    238 		html(" class='");
    239 		html_attr(class);
    240 		html("'");
    241 	}
    242 	html(" href='");
    243 	if (ctx.cfg.virtual_root) {
    244 		html_url_path(ctx.cfg.virtual_root);
    245 		html_url_path(ctx.repo->url);
    246 		if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
    247 			html("/");
    248 		if (page) {
    249 			html_url_path(page);
    250 			html("/");
    251 			if (path)
    252 				html_url_path(path);
    253 		}
    254 	} else {
    255 		html_url_path(ctx.cfg.script_name);
    256 		html("?url=");
    257 		html_url_arg(ctx.repo->url);
    258 		if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
    259 			html("/");
    260 		if (page) {
    261 			html_url_arg(page);
    262 			html("/");
    263 			if (path)
    264 				html_url_arg(path);
    265 		}
    266 		delim = "&amp;";
    267 	}
    268 	if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
    269 		html(delim);
    270 		html("h=");
    271 		html_url_arg(head);
    272 		delim = "&amp;";
    273 	}
    274 	return fmt("%s", delim);
    275 }
    276 
    277 static void reporevlink(const char *page, const char *name, const char *title,
    278 			const char *class, const char *head, const char *rev,
    279 			const char *path)
    280 {
    281 	char *delim;
    282 
    283 	delim = repolink(title, class, page, head, path);
    284 	if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) {
    285 		html(delim);
    286 		html("id=");
    287 		html_url_arg(rev);
    288 	}
    289 	html("'>");
    290 	html_txt(name);
    291 	html("</a>");
    292 }
    293 
    294 void cgit_summary_link(const char *name, const char *title, const char *class,
    295 		       const char *head)
    296 {
    297 	reporevlink(NULL, name, title, class, head, NULL, NULL);
    298 }
    299 
    300 void cgit_tag_link(const char *name, const char *title, const char *class,
    301 		   const char *tag)
    302 {
    303 	reporevlink("tag", name, title, class, tag, NULL, NULL);
    304 }
    305 
    306 void cgit_tree_link(const char *name, const char *title, const char *class,
    307 		    const char *head, const char *rev, const char *path)
    308 {
    309 	reporevlink("tree", name, title, class, head, rev, path);
    310 }
    311 
    312 void cgit_plain_link(const char *name, const char *title, const char *class,
    313 		     const char *head, const char *rev, const char *path)
    314 {
    315 	reporevlink("plain", name, title, class, head, rev, path);
    316 }
    317 
    318 void cgit_blame_link(const char *name, const char *title, const char *class,
    319 		     const char *head, const char *rev, const char *path)
    320 {
    321 	reporevlink("blame", name, title, class, head, rev, path);
    322 }
    323 
    324 void cgit_log_link(const char *name, const char *title, const char *class,
    325 		   const char *head, const char *rev, const char *path,
    326 		   int ofs, const char *grep, const char *pattern, int showmsg,
    327 		   int follow)
    328 {
    329 	char *delim;
    330 
    331 	delim = repolink(title, class, "log", head, path);
    332 	if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
    333 		html(delim);
    334 		html("id=");
    335 		html_url_arg(rev);
    336 		delim = "&amp;";
    337 	}
    338 	if (grep && pattern) {
    339 		html(delim);
    340 		html("qt=");
    341 		html_url_arg(grep);
    342 		delim = "&amp;";
    343 		html(delim);
    344 		html("q=");
    345 		html_url_arg(pattern);
    346 	}
    347 	if (ofs > 0) {
    348 		html(delim);
    349 		html("ofs=");
    350 		htmlf("%d", ofs);
    351 		delim = "&amp;";
    352 	}
    353 	if (showmsg) {
    354 		html(delim);
    355 		html("showmsg=1");
    356 		delim = "&amp;";
    357 	}
    358 	if (follow) {
    359 		html(delim);
    360 		html("follow=1");
    361 	}
    362 	html("'>");
    363 	html_txt(name);
    364 	html("</a>");
    365 }
    366 
    367 void cgit_commit_link(const char *name, const char *title, const char *class,
    368 		      const char *head, const char *rev, const char *path)
    369 {
    370 	char *delim;
    371 
    372 	delim = repolink(title, class, "commit", head, path);
    373 	if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
    374 		html(delim);
    375 		html("id=");
    376 		html_url_arg(rev);
    377 		delim = "&amp;";
    378 	}
    379 	if (ctx.qry.difftype) {
    380 		html(delim);
    381 		htmlf("dt=%d", ctx.qry.difftype);
    382 		delim = "&amp;";
    383 	}
    384 	if (ctx.qry.context > 0 && ctx.qry.context != 3) {
    385 		html(delim);
    386 		html("context=");
    387 		htmlf("%d", ctx.qry.context);
    388 		delim = "&amp;";
    389 	}
    390 	if (ctx.qry.ignorews) {
    391 		html(delim);
    392 		html("ignorews=1");
    393 		delim = "&amp;";
    394 	}
    395 	if (ctx.qry.follow) {
    396 		html(delim);
    397 		html("follow=1");
    398 	}
    399 	html("'>");
    400 	if (name[0] != '\0') {
    401 		if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
    402 			html_ntxt(name, ctx.cfg.max_msg_len - 3);
    403 			html("...");
    404 		} else
    405 			html_txt(name);
    406 	} else
    407 		html_txt("(no commit message)");
    408 	html("</a>");
    409 }
    410 
    411 void cgit_refs_link(const char *name, const char *title, const char *class,
    412 		    const char *head, const char *rev, const char *path)
    413 {
    414 	reporevlink("refs", name, title, class, head, rev, path);
    415 }
    416 
    417 void cgit_snapshot_link(const char *name, const char *title, const char *class,
    418 			const char *head, const char *rev,
    419 			const char *archivename)
    420 {
    421 	reporevlink("snapshot", name, title, class, head, rev, archivename);
    422 }
    423 
    424 void cgit_diff_link(const char *name, const char *title, const char *class,
    425 		    const char *head, const char *new_rev, const char *old_rev,
    426 		    const char *path)
    427 {
    428 	char *delim;
    429 
    430 	delim = repolink(title, class, "diff", head, path);
    431 	if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) {
    432 		html(delim);
    433 		html("id=");
    434 		html_url_arg(new_rev);
    435 		delim = "&amp;";
    436 	}
    437 	if (old_rev) {
    438 		html(delim);
    439 		html("id2=");
    440 		html_url_arg(old_rev);
    441 		delim = "&amp;";
    442 	}
    443 	if (ctx.qry.difftype) {
    444 		html(delim);
    445 		htmlf("dt=%d", ctx.qry.difftype);
    446 		delim = "&amp;";
    447 	}
    448 	if (ctx.qry.context > 0 && ctx.qry.context != 3) {
    449 		html(delim);
    450 		html("context=");
    451 		htmlf("%d", ctx.qry.context);
    452 		delim = "&amp;";
    453 	}
    454 	if (ctx.qry.ignorews) {
    455 		html(delim);
    456 		html("ignorews=1");
    457 		delim = "&amp;";
    458 	}
    459 	if (ctx.qry.follow) {
    460 		html(delim);
    461 		html("follow=1");
    462 	}
    463 	html("'>");
    464 	html_txt(name);
    465 	html("</a>");
    466 }
    467 
    468 void cgit_patch_link(const char *name, const char *title, const char *class,
    469 		     const char *head, const char *rev, const char *path)
    470 {
    471 	reporevlink("patch", name, title, class, head, rev, path);
    472 }
    473 
    474 void cgit_stats_link(const char *name, const char *title, const char *class,
    475 		     const char *head, const char *path)
    476 {
    477 	reporevlink("stats", name, title, class, head, NULL, path);
    478 }
    479 
    480 static void cgit_self_link(char *name, const char *title, const char *class)
    481 {
    482 	if (!strcmp(ctx.qry.page, "repolist"))
    483 		cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
    484 				ctx.qry.ofs, 1);
    485 	else if (!strcmp(ctx.qry.page, "summary"))
    486 		cgit_summary_link(name, title, class, ctx.qry.head);
    487 	else if (!strcmp(ctx.qry.page, "tag"))
    488 		cgit_tag_link(name, title, class, ctx.qry.has_sha1 ?
    489 			       ctx.qry.sha1 : ctx.qry.head);
    490 	else if (!strcmp(ctx.qry.page, "tree"))
    491 		cgit_tree_link(name, title, class, ctx.qry.head,
    492 			       ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    493 			       ctx.qry.path);
    494 	else if (!strcmp(ctx.qry.page, "plain"))
    495 		cgit_plain_link(name, title, class, ctx.qry.head,
    496 				ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    497 				ctx.qry.path);
    498 	else if (!strcmp(ctx.qry.page, "blame"))
    499 		cgit_blame_link(name, title, class, ctx.qry.head,
    500 				ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    501 				ctx.qry.path);
    502 	else if (!strcmp(ctx.qry.page, "log"))
    503 		cgit_log_link(name, title, class, ctx.qry.head,
    504 			      ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    505 			      ctx.qry.path, ctx.qry.ofs,
    506 			      ctx.qry.grep, ctx.qry.search,
    507 			      ctx.qry.showmsg, ctx.qry.follow);
    508 	else if (!strcmp(ctx.qry.page, "commit"))
    509 		cgit_commit_link(name, title, class, ctx.qry.head,
    510 				 ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    511 				 ctx.qry.path);
    512 	else if (!strcmp(ctx.qry.page, "patch"))
    513 		cgit_patch_link(name, title, class, ctx.qry.head,
    514 				ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    515 				ctx.qry.path);
    516 	else if (!strcmp(ctx.qry.page, "refs"))
    517 		cgit_refs_link(name, title, class, ctx.qry.head,
    518 			       ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    519 			       ctx.qry.path);
    520 	else if (!strcmp(ctx.qry.page, "snapshot"))
    521 		cgit_snapshot_link(name, title, class, ctx.qry.head,
    522 				   ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
    523 				   ctx.qry.path);
    524 	else if (!strcmp(ctx.qry.page, "diff"))
    525 		cgit_diff_link(name, title, class, ctx.qry.head,
    526 			       ctx.qry.sha1, ctx.qry.sha2,
    527 			       ctx.qry.path);
    528 	else if (!strcmp(ctx.qry.page, "stats"))
    529 		cgit_stats_link(name, title, class, ctx.qry.head,
    530 				ctx.qry.path);
    531 	else {
    532 		/* Don't known how to make link for this page */
    533 		repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
    534 		html("><!-- cgit_self_link() doesn't know how to make link for page '");
    535 		html_txt(ctx.qry.page);
    536 		html("' -->");
    537 		html_txt(name);
    538 		html("</a>");
    539 	}
    540 }
    541 
    542 void cgit_object_link(struct object *obj)
    543 {
    544 	char *page, *shortrev, *fullrev, *name;
    545 
    546 	fullrev = oid_to_hex(&obj->oid);
    547 	shortrev = xstrdup(fullrev);
    548 	shortrev[10] = '\0';
    549 	if (obj->type == OBJ_COMMIT) {
    550 		cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
    551 				 ctx.qry.head, fullrev, NULL);
    552 		return;
    553 	} else if (obj->type == OBJ_TREE)
    554 		page = "tree";
    555 	else if (obj->type == OBJ_TAG)
    556 		page = "tag";
    557 	else
    558 		page = "blob";
    559 	name = fmt("%s %s...", type_name(obj->type), shortrev);
    560 	reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
    561 }
    562 
    563 static struct string_list_item *lookup_path(struct string_list *list,
    564 					    const char *path)
    565 {
    566 	struct string_list_item *item;
    567 
    568 	while (path && path[0]) {
    569 		if ((item = string_list_lookup(list, path)))
    570 			return item;
    571 		if (!(path = strchr(path, '/')))
    572 			break;
    573 		path++;
    574 	}
    575 	return NULL;
    576 }
    577 
    578 void cgit_submodule_link(const char *class, char *path, const char *rev)
    579 {
    580 	struct string_list *list;
    581 	struct string_list_item *item;
    582 	char tail, *dir;
    583 	size_t len;
    584 
    585 	len = 0;
    586 	tail = 0;
    587 	list = &ctx.repo->submodules;
    588 	item = lookup_path(list, path);
    589 	if (!item) {
    590 		len = strlen(path);
    591 		tail = path[len - 1];
    592 		if (tail == '/') {
    593 			path[len - 1] = 0;
    594 			item = lookup_path(list, path);
    595 		}
    596 	}
    597 	if (item || ctx.repo->module_link) {
    598 		html("<a ");
    599 		if (class)
    600 			htmlf("class='%s' ", class);
    601 		html("href='");
    602 		if (item) {
    603 			html_attrf(item->util, rev);
    604 		} else {
    605 			dir = strrchr(path, '/');
    606 			if (dir)
    607 				dir++;
    608 			else
    609 				dir = path;
    610 			html_attrf(ctx.repo->module_link, dir, rev);
    611 		}
    612 		html("'>");
    613 		html_txt(path);
    614 		html("</a>");
    615 	} else {
    616 		html("<span");
    617 		if (class)
    618 			htmlf(" class='%s'", class);
    619 		html(">");
    620 		html_txt(path);
    621 		html("</span>");
    622 	}
    623 	html_txtf(" @ %.7s", rev);
    624 	if (item && tail)
    625 		path[len - 1] = tail;
    626 }
    627 
    628 const struct date_mode *cgit_date_mode(enum date_mode_type type)
    629 {
    630 	static struct date_mode mode;
    631 	mode.type = type;
    632 	mode.local = ctx.cfg.local_time;
    633 	return &mode;
    634 }
    635 
    636 static void print_rel_date(time_t t, int tz, double value,
    637 	const char *class, const char *suffix)
    638 {
    639 	htmlf("<span class='%s' title='", class);
    640 	html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
    641 	htmlf("'>%.0f %s</span>", value, suffix);
    642 }
    643 
    644 void cgit_print_age(time_t t, int tz, time_t max_relative)
    645 {
    646 	time_t now, secs;
    647 
    648 	if (!t)
    649 		return;
    650 	time(&now);
    651 	secs = now - t;
    652 	if (secs < 0)
    653 		secs = 0;
    654 
    655 	if (secs > max_relative && max_relative >= 0) {
    656 		html("<span title='");
    657 		html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
    658 		html("'>");
    659 		html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT)));
    660 		html("</span>");
    661 		return;
    662 	}
    663 
    664 	if (secs < TM_HOUR * 2) {
    665 		print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min.");
    666 		return;
    667 	}
    668 	if (secs < TM_DAY * 2) {
    669 		print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours");
    670 		return;
    671 	}
    672 	if (secs < TM_WEEK * 2) {
    673 		print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days");
    674 		return;
    675 	}
    676 	if (secs < TM_MONTH * 2) {
    677 		print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks");
    678 		return;
    679 	}
    680 	if (secs < TM_YEAR * 2) {
    681 		print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months");
    682 		return;
    683 	}
    684 	print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years");
    685 }
    686 
    687 void cgit_print_http_headers(void)
    688 {
    689 	if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
    690 		return;
    691 
    692 	if (ctx.page.status)
    693 		htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
    694 	if (ctx.page.mimetype && ctx.page.charset)
    695 		htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype,
    696 		      ctx.page.charset);
    697 	else if (ctx.page.mimetype)
    698 		htmlf("Content-Type: %s\n", ctx.page.mimetype);
    699 	if (ctx.page.size)
    700 		htmlf("Content-Length: %zd\n", ctx.page.size);
    701 	if (ctx.page.filename) {
    702 		html("Content-Disposition: inline; filename=\"");
    703 		html_header_arg_in_quotes(ctx.page.filename);
    704 		html("\"\n");
    705 	}
    706 	if (!ctx.env.authenticated)
    707 		html("Cache-Control: no-cache, no-store\n");
    708 	htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
    709 	htmlf("Expires: %s\n", http_date(ctx.page.expires));
    710 	if (ctx.page.etag)
    711 		htmlf("ETag: \"%s\"\n", ctx.page.etag);
    712 	html("\n");
    713 	if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
    714 		exit(0);
    715 }
    716 
    717 void cgit_redirect(const char *url, bool permanent)
    718 {
    719 	htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
    720 	html("Location: ");
    721 	html_url_path(url);
    722 	html("\n\n");
    723 }
    724 
    725 static void print_rel_vcs_link(const char *url)
    726 {
    727 	html("<link rel='vcs-git' href='");
    728 	html_attr(url);
    729 	html("' title='");
    730 	html_attr(ctx.repo->name);
    731 	html(" Git repository'/>\n");
    732 }
    733 
    734 void cgit_print_docstart(void)
    735 {
    736 	char *host = cgit_hosturl();
    737 
    738 	if (ctx.cfg.embedded) {
    739 		if (ctx.cfg.header)
    740 			html_include(ctx.cfg.header);
    741 		return;
    742 	}
    743 
    744 	html(cgit_doctype);
    745 	html("<html lang='en'>\n");
    746 	html("<head>\n");
    747 	html("<title>");
    748 	html_txt(ctx.page.title);
    749 	html("</title>\n");
    750 	htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
    751 	if (ctx.cfg.robots && *ctx.cfg.robots)
    752 		htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
    753 	html("<link rel='stylesheet' type='text/css' href='");
    754 	html_attr(ctx.cfg.css);
    755 	html("'/>\n");
    756 	if (ctx.cfg.favicon) {
    757 		html("<link rel='shortcut icon' href='");
    758 		html_attr(ctx.cfg.favicon);
    759 		html("'/>\n");
    760 	}
    761 	if (host && ctx.repo && ctx.qry.head) {
    762 		char *fileurl;
    763 		struct strbuf sb = STRBUF_INIT;
    764 		strbuf_addf(&sb, "h=%s", ctx.qry.head);
    765 
    766 		html("<link rel='alternate' title='Atom feed' href='");
    767 		html(cgit_httpscheme());
    768 		html_attr(host);
    769 		fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
    770 				       sb.buf);
    771 		html_attr(fileurl);
    772 		html("' type='application/atom+xml'/>\n");
    773 		strbuf_release(&sb);
    774 		free(fileurl);
    775 	}
    776 	if (ctx.cfg.head_include)
    777 		html_include(ctx.cfg.head_include);
    778         html("<link rel='stylesheet' href='https://use.fontawesome.com/releases/v5.1.0/css/all.css' integrity='sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt' crossorigin='anonymous'>");
    779 	html("</head>\n");
    780 	html("<body>\n");
    781 	if (ctx.cfg.header)
    782 		html_include(ctx.cfg.header);
    783 	free(host);
    784 }
    785 
    786 void cgit_print_docend(void)
    787 {
    788 	html("</div> <!-- class=content -->\n");
    789 	if (ctx.cfg.embedded) {
    790 		html("</div> <!-- id=cgit -->\n");
    791 		if (ctx.cfg.footer)
    792 			html_include(ctx.cfg.footer);
    793 		return;
    794 	}
    795 	if (ctx.cfg.footer)
    796 		html_include(ctx.cfg.footer);
    797 	else {
    798 		htmlf("<div class='footer'>generated by <a href='https://git.ne02ptzero.me/neocgit/'>neocgit %s</a> "
    799 			"(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
    800 		html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
    801 		html("</div>\n");
    802 	}
    803 	html("</div> <!-- id=cgit -->\n");
    804 	html("</body>\n</html>\n");
    805 }
    806 
    807 void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
    808 {
    809 	va_list ap;
    810 	ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
    811 	ctx.page.status = code;
    812 	ctx.page.statusmsg = msg;
    813 	cgit_print_layout_start();
    814 	va_start(ap, fmt);
    815 	cgit_vprint_error(fmt, ap);
    816 	va_end(ap);
    817 	cgit_print_layout_end();
    818 }
    819 
    820 void cgit_print_layout_start(void)
    821 {
    822 	cgit_print_http_headers();
    823         cgit_print_docstart();
    824         cgit_print_pageheader();
    825 }
    826 
    827 void cgit_print_layout_end(void)
    828 {
    829 	cgit_print_docend();
    830 }
    831 
    832 static const char *get_clone_type(char *url)
    833 {
    834     if (strncmp("git://", url, sizeof("git://") - 1) == 0)
    835         return "GIT";
    836     if (strncmp("http://", url, sizeof("http://") - 1) == 0)
    837         return "HTTP";
    838     if (strncmp("https://", url, sizeof("https://") - 1) == 0)
    839         return "HTTPS";
    840     else
    841         return "SSH";
    842 }
    843 
    844 static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix)
    845 {
    846 	struct strbuf **url_list = strbuf_split_str(txt, ' ', 0);
    847 	int i;
    848 
    849 	for (i = 0; url_list[i]; i++) {
    850 		strbuf_rtrim(url_list[i]);
    851 		if (url_list[i]->len == 0)
    852 			continue;
    853 		if (suffix && *suffix)
    854 			strbuf_addf(url_list[i], "/%s", suffix);
    855 	}
    856 
    857         _html("<div class='repo-url-container'>") {
    858             _html("<ul class='repo-url'>") {
    859                 _html("<li class='select'>") {
    860                     _html("<select>") {
    861                         for (i = 0; url_list[i] != NULL; i++)
    862                             htmlf("<option "
    863                                 "onclick=\"document.getElementById('clone-url-value')"
    864                                 ".value='%s'\">%s</option>",
    865                                 url_list[i]->buf, get_clone_type(url_list[i]->buf));
    866                     } _html("</select>");
    867                 } html("</li>");
    868                 html("<li class='select-arrow'><i class='fa fa-caret-down'></i></li>");
    869 
    870                 _html("<li>") {
    871                     htmlf("<input onclick='this.select()', type='text' "
    872                         "class='input-copy' id='clone-url-value' value='%s' readonly />",
    873                         url_list[0]->buf);
    874                 } html("</li>");
    875 
    876                 _html("<li>") {
    877                     html("<div class='copy' "
    878                         "onclick=\"document.getElementById('clone-url-value').select();"
    879                         "document.execCommand('copy')\">"
    880                         "<i class='fa fa-copy'></i></div>");
    881                 } html("</li>");
    882 
    883             } _html("</ul>");
    884         } _html("</div>");
    885 
    886 	strbuf_list_free(url_list);
    887 }
    888 
    889 void cgit_add_clone_urls(void (*fn)(const char *))
    890 {
    891 	if (ctx.repo->clone_url)
    892 		add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL);
    893 	else if (ctx.cfg.clone_prefix)
    894 		add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
    895 }
    896 
    897 static int print_branch_option(const char *refname, const struct object_id *oid,
    898 			       int flags, void *cb_data)
    899 {
    900 	char *name = (char *)refname;
    901 	html_option(name, name, ctx.qry.sha1 == NULL ? ctx.qry.head : "");
    902 	return 0;
    903 }
    904 
    905 void cgit_add_hidden_formfields(int incl_head, int incl_search,
    906 				const char *page)
    907 {
    908 	if (!ctx.cfg.virtual_root) {
    909 		struct strbuf url = STRBUF_INIT;
    910 
    911 		strbuf_addf(&url, "%s/%s", ctx.qry.repo, page);
    912 		if (ctx.qry.vpath)
    913 			strbuf_addf(&url, "/%s", ctx.qry.vpath);
    914 		html_hidden("url", url.buf);
    915 		strbuf_release(&url);
    916 	}
    917 
    918 	if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
    919 	    strcmp(ctx.qry.head, ctx.repo->defbranch))
    920 		html_hidden("h", ctx.qry.head);
    921 
    922 	if (ctx.qry.showmsg)
    923 		html_hidden("showmsg", "1");
    924 
    925 	if (incl_search) {
    926 		if (ctx.qry.grep)
    927 			html_hidden("qt", ctx.qry.grep);
    928 		if (ctx.qry.search)
    929 			html_hidden("q", ctx.qry.search);
    930 	}
    931 }
    932 
    933 static const char *hc(const char *page)
    934 {
    935 	if (!ctx.qry.page)
    936 		return NULL;
    937 
    938 	return strcmp(ctx.qry.page, page) ? NULL : "active";
    939 }
    940 
    941 static void cgit_print_path_crumbs(char *path)
    942 {
    943 	char *old_path = ctx.qry.path;
    944 	char *p = path, *q, *end = path + strlen(path);
    945 
    946 	ctx.qry.path = NULL;
    947 	cgit_self_link("root", NULL, NULL);
    948 	ctx.qry.path = p = path;
    949 	while (p < end) {
    950 		if (!(q = strchr(p, '/')))
    951 			q = end;
    952 		*q = '\0';
    953 		html_txt("/");
    954 		cgit_self_link(p, NULL, NULL);
    955 		if (q < end)
    956 			*q = '/';
    957 		p = q + 1;
    958 	}
    959 	ctx.qry.path = old_path;
    960 }
    961 
    962 static void print_header(void)
    963 {
    964 	char *logo = NULL, *logo_link = NULL;
    965         char *root_url = cgit_rooturl();
    966 
    967         _html("<div class='summary-menu'>") {
    968             _html("<ul>") {
    969             _html("<li class='path'>") {
    970             htmlf("<a href='%s'>%s</a>", root_url, ctx.cfg.root_title);
    971             if (ctx.repo)
    972             {
    973                 htmlf(" / <a href='/%s'>%s</a>", ctx.repo->name, ctx.repo->name);
    974                 htmlf(" / %s", ctx.qry.page);
    975             }
    976             } _html("</li>");
    977 
    978             if (ctx.repo)
    979             {
    980                 HTML_LINK("<li><a href='" URL() "'>Summary</a></li>");
    981                 HTML_LINK("<li><a href='" URL("log") "'>Commits (%d)</a></li>", cgit_count_commits());
    982                 HTML_LINK("<li><a href='" URL("refs") "'>Branches (%d)</a></li>", cgit_count_branches());
    983                 htmlf("<li><a href='#'>Releases (%d)</a></li>", cgit_count_tags());
    984                 HTML_LINK("<li><a href='" URL("issues") "'>Issues</a></li>");
    985             }
    986             else
    987             {
    988                 htmlf("<li class='site-desc'>%s</li>", ctx.cfg.root_desc);
    989             }
    990             } _html("</ul>");
    991         } _html("</div>");
    992 }
    993 
    994 void cgit_print_pageheader(void)
    995 {
    996 	html("<div id='cgit'>");
    997 	if (!ctx.env.authenticated || !ctx.cfg.noheader)
    998 		print_header();
    999 
   1000     if (ctx.env.authenticated && ctx.repo) {
   1001         _html("<div class='summary-header'>") {
   1002             if (ctx.repo->logo != NULL)
   1003                 htmlf("<img src='/%s' />", ctx.repo->logo);
   1004             else
   1005                 htmlf("<img src='%s' />", ctx.cfg.logo);
   1006             htmlf("<span class='repo-name'>%s</span>", ctx.repo->name);
   1007             htmlf("<span class='repo-desc'>%s</span>", ctx.repo->desc);
   1008             htmlf("<span class='repo-author'>%s</span>", ctx.repo->owner == NULL ? "" : ctx.repo->owner);
   1009             cgit_add_clone_urls(NULL);
   1010         } _html("</div>");
   1011 
   1012         if (strcmp("commit", ctx.qry.page) == 0 || strcmp("issues", ctx.qry.page) == 0)
   1013             goto end;
   1014 
   1015         _html("<div class='summary-branches'>") {
   1016             _html("<ul>") {
   1017                 _html("<li>") {
   1018                         _html("<form method='get'>") {
   1019                             cgit_add_hidden_formfields(0, 1, ctx.qry.page);
   1020                             _html("<ul class='select-container'>") {
   1021 
   1022                                 html("<li class='select'>"); {
   1023                                     html("<select name='h' onchange='this.form.submit();'>\n");
   1024                                     for_each_branch_ref(print_branch_option, ctx.qry.head);
   1025 
   1026                                     if (ctx.qry.sha1 != NULL)
   1027                                         htmlf("<option value='%s' selected='selected'>%s</option>",
   1028                                             ctx.qry.sha1, ctx.qry.sha1);
   1029 
   1030                                     html("</select>");
   1031                                 } html("</li>");
   1032 
   1033                                 html("<li class='select-arrow'>"); {
   1034                                     html("<i class='fa fa-caret-down'></i>");
   1035                                 } html("</li>");
   1036 
   1037                             }_html("</ul>");
   1038                         } html("</form>");
   1039                 } _html("</li>");
   1040 
   1041                 _html("<li class='repo-name'>") {
   1042                     htmlf("<a href='/%s?%s=%s'>%s</a>", ctx.repo->name,
   1043                         ctx.qry.sha1 != NULL ? "id" : "h",
   1044                         ctx.qry.sha1 != NULL ? ctx.qry.sha1 : ctx.qry.head,
   1045                         ctx.repo->name);
   1046 
   1047                     if (ctx.qry.path)
   1048                     {
   1049                         int z = 0;
   1050 
   1051                         for (int i = 0; ctx.qry.path[i] != '\0'; i++)
   1052                         {
   1053                             if (ctx.qry.path[i] != '/' || i == 0)
   1054                                 continue;
   1055 
   1056                             ctx.qry.path[i] = 0;
   1057                             htmlf("/<a href='/%s/tree/%s?%s=%s'>%s</a>",
   1058                                 ctx.repo->name, ctx.qry.path,
   1059                                 ctx.qry.sha1 != NULL ? "id" : "h",
   1060                                 ctx.qry.sha1 != NULL ? ctx.qry.sha1 : ctx.qry.head,
   1061                                 ctx.qry.path + z);
   1062                             ctx.qry.path[i] = '/';
   1063                             z = ++i;
   1064                         }
   1065 
   1066                         htmlf("/<a href='/%s/tree/%s?%s=%s'>%s</a>",
   1067                         ctx.repo->name, ctx.qry.path, 
   1068                         ctx.qry.sha1 != NULL ? "id" : "h",
   1069                         ctx.qry.sha1 != NULL ? ctx.qry.sha1 : ctx.qry.head,
   1070                         ctx.qry.path + z);
   1071                     }
   1072                 } _html("</li>");
   1073 
   1074                 _html("<li class='history'>") {
   1075                     if (ctx.qry.path != NULL)
   1076                         htmlf("<a href='/%s/log/%s?h=%s'>History</a>", ctx.repo->name, ctx.qry.path, ctx.qry.head);
   1077                     else
   1078                         htmlf("<a href='/%s/log?h=%s'>History</a>", ctx.repo->name, ctx.qry.head);
   1079                 } _html("</li>");
   1080             } _html("</ul>");
   1081         } _html("</div>");
   1082 
   1083         if (strcmp("log", ctx.qry.page) == 0)
   1084             goto end;
   1085 
   1086         _html("<div class='summary-last-commit'>") {
   1087             _html("<div class='last-commit commit'>") {
   1088                 struct commit           *commit;
   1089                 struct commitinfo       *info;
   1090                 struct object_id        oid;
   1091 
   1092                 if (ctx.qry.path)
   1093                     commit = cgit_get_last_commit_from_path(ctx.qry.path);
   1094                 else if (ctx.qry.sha1)
   1095                     commit = cgit_get_last_commit_from_path(".");
   1096                 else
   1097                 {
   1098                     get_oid(ctx.qry.head, &oid);
   1099                     commit = lookup_commit_reference(&oid);
   1100                 }
   1101                 clear_object_flags(ALL_REV_FLAGS);
   1102                 info = cgit_parse_commit(commit);
   1103 
   1104                 _html("<ul>") {
   1105 
   1106                     _html("<li class='avatar'>") {
   1107                         gravatar_img(info->committer_email, 13);
   1108                     } html("</li>");
   1109 
   1110                     _html("<li class='commit-info'><ul>") {
   1111                         htmlf("<li class='title'><a href='/%s/commit/?id=%s'>%s</a></li>",
   1112                             ctx.repo->name, oid_to_hex(&commit->object.oid), info->subject);
   1113                         htmlf("<li class='author'>%s authored ", info->author);
   1114                         cgit_print_age(info->author_date, info->author_tz, -1);
   1115                         html(" ago</li>");
   1116                     } html("</ul></li>");
   1117 
   1118                     _html("<li class='hash'><ul>") {
   1119                         html("<li class='input'><input type='text' value='");
   1120                         html_txt(oid_to_hex(&commit->object.oid));
   1121                         html("' readonly id='last-commit-value' /></li>");
   1122 
   1123                         html("<li class='copy'><span "
   1124                         "onclick=\"document.getElementById('last-commit-value')"
   1125                         ".select(); document.execCommand('copy')\">"
   1126                         "<i class='fa fa-copy'></i></span></li>");
   1127 
   1128                     } html("</ul></li>");
   1129 
   1130                 } _html("</ul>");
   1131             } _html("</div>");
   1132         } _html("</div>");
   1133     }
   1134 
   1135 end:
   1136     html("<div class='content'>");
   1137 }
   1138 
   1139 void cgit_print_filemode(unsigned short mode)
   1140 {
   1141 	if (S_ISDIR(mode))
   1142 		html("d");
   1143 	else if (S_ISLNK(mode))
   1144 		html("l");
   1145 	else if (S_ISGITLINK(mode))
   1146 		html("m");
   1147 	else
   1148 		html("-");
   1149 	html_fileperm(mode >> 6);
   1150 	html_fileperm(mode >> 3);
   1151 	html_fileperm(mode);
   1152 }
   1153 
   1154 void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
   1155 				  const char *ref)
   1156 {
   1157 	struct object_id oid;
   1158 
   1159 	/*
   1160 	 * Prettify snapshot names by stripping leading "v" or "V" if the tag
   1161 	 * name starts with {v,V}[0-9] and the prettify mapping is injective,
   1162 	 * i.e. each stripped tag can be inverted without ambiguities.
   1163 	 */
   1164 	if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
   1165 	    (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
   1166 	    ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
   1167 	     (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
   1168 	     (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
   1169 		ref++;
   1170 
   1171 	strbuf_addf(filename, "%s-%s", base, ref);
   1172 }
   1173 
   1174 void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
   1175 			       const char *separator)
   1176 {
   1177 	const struct cgit_snapshot_format* f;
   1178 	struct strbuf filename = STRBUF_INIT;
   1179 	const char *basename;
   1180 	size_t prefixlen;
   1181 
   1182 	basename = cgit_snapshot_prefix(repo);
   1183 	if (starts_with(ref, basename))
   1184 		strbuf_addstr(&filename, ref);
   1185 	else
   1186 		cgit_compose_snapshot_prefix(&filename, basename, ref);
   1187 
   1188 	prefixlen = filename.len;
   1189 	for (f = cgit_snapshot_formats; f->suffix; f++) {
   1190 		if (!(repo->snapshots & cgit_snapshot_format_bit(f)))
   1191 			continue;
   1192 		strbuf_setlen(&filename, prefixlen);
   1193 		strbuf_addstr(&filename, f->suffix);
   1194 		cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
   1195 				   filename.buf);
   1196 		if (cgit_snapshot_get_sig(ref, f)) {
   1197 			strbuf_addstr(&filename, ".asc");
   1198 			html(" (");
   1199 			cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
   1200 					   filename.buf);
   1201 			html(")");
   1202 		}
   1203 		html(separator);
   1204 	}
   1205 	strbuf_release(&filename);
   1206 }
   1207 
   1208 void cgit_set_title_from_path(const char *path)
   1209 {
   1210 	size_t path_len, path_index, path_last_end;
   1211 	char *new_title;
   1212 
   1213 	if (!path)
   1214 		return;
   1215 
   1216 	path_len = strlen(path);
   1217 	new_title = xmalloc(path_len + 3 + strlen(ctx.page.title) + 1);
   1218 	new_title[0] = '\0';
   1219 
   1220 	for (path_index = path_len, path_last_end = path_len; path_index-- > 0;) {
   1221 		if (path[path_index] == '/') {
   1222 			if (path_index == path_len - 1) {
   1223 				path_last_end = path_index - 1;
   1224 				continue;
   1225 			}
   1226 			strncat(new_title, &path[path_index + 1], path_last_end - path_index - 1);
   1227 			strcat(new_title, "\\");
   1228 			path_last_end = path_index;
   1229 		}
   1230 	}
   1231 	if (path_last_end)
   1232 		strncat(new_title, path, path_last_end);
   1233 
   1234 	strcat(new_title, " - ");
   1235 	strcat(new_title, ctx.page.title);
   1236 	ctx.page.title = new_title;
   1237 }