all repos — cgit @ a6572ce1762e0d571c3e96b5f4eff7c81015a1f2

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