all repos — cgit @ 8a6f98afe944b434a614203c98691307ad9ccbc8

a hyperfast web frontend for git written in c

ui-shared.c (view raw)

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