Splux Linux

source-based Linux using SPS

splux-site · log · files · refs · tags · releases · people · languages · GitHub · file

site/assets/git.js


JavaScript

45873 bytes at 4c4f12c · GitHub

// Live GitHub refresh for Splux Git. Baked HTML stays if this fails.
// New commits, language bars, and account pages update in the browser.
(function () {
	var API = "https://api.github.com";
	var OWNER = "RobertFlexx";
	var ACCEPT = { Accept: "application/vnd.github+json" };
	var REPOS = ["SPS", "sps-core", "sps-extra", "sps-community", "splux-site"];
	var TTL = 120000;
	var COLORS = {};

	var thisScript = document.currentScript ||
		document.querySelector("script[src*=\"git.js\"]");
	var dataRoot = (thisScript && thisScript.getAttribute("data-root")) || "./";
	var gitHost = (thisScript && thisScript.getAttribute("data-git")) ||
		"https://splux.robertflexx.dev/git";
	gitHost = String(gitHost).replace(/\/+$/, "");
	if (dataRoot === "/")
		dataRoot = "/";

	var main = document.getElementById("main");

	function esc(s) {
		return String(s == null ? "" : s)
			.replace(/&/g, "&")
			.replace(/</g, "&lt;")
			.replace(/>/g, "&gt;")
			.replace(/"/g, "&quot;");
	}

	function validLogin(name) {
		return /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/.test(name) &&
			name.length <= 39;
	}

	function formatLocal(iso) {
		var d = new Date(iso);
		if (isNaN(d.getTime()))
			return iso;
		try {
			return d.toLocaleString(undefined, {
				year: "numeric",
				month: "short",
				day: "numeric",
				hour: "numeric",
				minute: "2-digit",
				timeZoneName: "short"
			});
		} catch (e) {
			return d.toString();
		}
	}

	function localizeTimes(root) {
		var times = (root || document).querySelectorAll("time[datetime]");
		for (var i = 0; i < times.length; i++) {
			var iso = times[i].getAttribute("datetime");
			if (iso)
				times[i].textContent = formatLocal(iso);
		}
	}

	function dayOf(iso) {
		iso = String(iso || "");
		var n = iso.indexOf("T");
		return n === -1 ? iso : iso.slice(0, n);
	}

	function json(url) {
		return fetch(url, { headers: ACCEPT, cache: "no-store" }).then(function (r) {
			if (!r.ok)
				throw new Error(String(r.status));
			return r.json();
		});
	}

	function cached(key, url) {
		try {
			var raw = sessionStorage.getItem(key);
			if (raw) {
				var o = JSON.parse(raw);
				if (o && (Date.now() - o.t) < TTL)
					return Promise.resolve(o.v);
			}
		} catch (e) {}
		return json(url).then(function (v) {
			try {
				sessionStorage.setItem(key, JSON.stringify({ t: Date.now(), v: v }));
			} catch (e2) {}
			return v;
		});
	}

	function colorsReady() {
		if (Object.keys(COLORS).length)
			return Promise.resolve(COLORS);
		return Promise.all([
			fetch(dataRoot + "data/linguist-colors.json", { cache: "force-cache" })
				.then(function (r) { return r.ok ? r.json() : {}; }),
			fetch(dataRoot + "data/linguist-fams.json", { cache: "force-cache" })
				.then(function (r) { return r.ok ? r.json() : {}; })
				.catch(function () { return {}; })
		]).then(function (rows) {
			COLORS = rows[0] || {};
			if (window.SpluxRender && SpluxRender.setFams)
				SpluxRender.setFams(rows[1] || {});
			return COLORS;
		}).catch(function () { return COLORS; });
	}

	function colorOf(name) {
		return COLORS[name] || "#888888";
	}

	function parsePath() {
		var p = location.pathname.replace(/\/index\.html$/, "").replace(/\/+$/, "");
		var parts = p.split("/").filter(Boolean);
		var out = { kind: "", repo: "", user: "", sha: "", path: "" };
		if (parts[0] !== "git")
			return out;
		if (parts.length === 1) {
			out.kind = "index";
			return out;
		}
		if (parts[1] === "users") {
			out.kind = parts.length === 2 ? "users" : "user";
			out.user = parts[2] || "";
			return out;
		}
		out.repo = parts[1];
		if (parts.length === 2) {
			out.kind = "repo";
			return out;
		}
		out.kind = parts[2];
		if (out.kind === "commit")
			out.sha = parts[3] || "";
		else if (out.kind === "tree" || out.kind === "blob")
			out.path = parts.slice(3).join("/");
		else if (out.kind === "lang") {
			if (parts.length === 3)
				out.kind = "langs";
			else
				out.path = parts.slice(3).join("/");
		} else if (out.kind === "tags") {
			if (parts.length > 3) {
				out.kind = "tag";
				out.path = decodeSeg(parts.slice(3).join("/"));
			}
		} else if (out.kind === "releases") {
			if (parts[3] === "latest")
				out.kind = "latest";
			else if (parts[3] === "tag" && parts[4]) {
				out.kind = "release";
				out.path = decodeSeg(parts.slice(4).join("/"));
			}
		}
		return out;
	}

	function pageInfo() {
		var info = {
			kind: (main && main.getAttribute("data-kind")) || "",
			repo: (main && main.getAttribute("data-repo")) || "",
			user: (main && main.getAttribute("data-user")) || "",
			sha: (main && main.getAttribute("data-sha")) || "",
			path: (main && main.getAttribute("data-path")) || ""
		};
		var missing = document.title.indexOf("404") === 0 ||
			(main && main.querySelector("h2") &&
				main.querySelector("h2").textContent === "404");
		if (!info.kind || missing) {
			var parsed = parsePath();
			if (parsed.kind) {
				info = parsed;
				info.hydrate = missing ? true : info.hydrate;
				if (missing)
					info.hydrate = true;
			}
		}
		return info;
	}

	function showLive() {
		var el = document.getElementById("git-live");
		if (el)
			el.hidden = false;
	}

	function userHref(login) {
		return gitHost + "/users/" + encodeURIComponent(login) + "/";
	}

	function commitHref(repo, sha) {
		return gitHost + "/" + encodeURIComponent(repo) + "/commit/" +
			encodeURIComponent(sha) + "/";
	}

	function decodeSeg(s) {
		s = String(s || "");
		try {
			return decodeURIComponent(s);
		} catch (e) {
			return s;
		}
	}

	function navHtml(repo, here) {
		var r = repoHref(repo);
		var bits = [];
		if (here === "home")
			bits.push(esc(repo));
		else
			bits.push("<a href=\"" + esc(r) + "\">" + esc(repo) + "</a>");
		bits.push(here === "log"
			? "log"
			: "<a href=\"" + esc(r + "log/") + "\">log</a>");
		bits.push(here === "files"
			? "files"
			: "<a href=\"" + esc(r + "tree/") + "\">files</a>");
		bits.push(here === "refs"
			? "refs"
			: "<a href=\"" + esc(r + "refs/") + "\">refs</a>");
		bits.push(here === "tags" || here === "tag"
			? "tags"
			: "<a href=\"" + esc(r + "tags/") + "\">tags</a>");
		bits.push(here === "releases" || here === "release" || here === "latest"
			? "releases"
			: "<a href=\"" + esc(r + "releases/") + "\">releases</a>");
		bits.push(here === "people"
			? "people"
			: "<a href=\"" + esc(r + "people/") + "\">people</a>");
		bits.push(here === "langs" || here === "lang"
			? "languages"
			: "<a href=\"" + esc(r + "lang/") + "\">languages</a>");
		bits.push("<a href=\"https://github.com/" + OWNER + "/" +
			encodeURIComponent(repo) + "\">GitHub</a>");
		if (here === "commit")
			bits.push("commit");
		if (here === "blob")
			bits.push("file");
		if (here === "tag")
			bits.push("tag");
		if (here === "release")
			bits.push("release");
		if (here === "latest")
			bits.push("latest");
		return "<p class=\"git-nav\">" + bits.join(" · ") + "</p>";
	}

	function tagHref(repo, name) {
		return repoHref(repo) + "tags/" + encodeURIComponent(name) + "/";
	}

	function releaseHref(repo, name) {
		return repoHref(repo) + "releases/tag/" + encodeURIComponent(name) + "/";
	}

	function archiveHref(repo, tag, kind) {
		return "https://github.com/" + OWNER + "/" + encodeURIComponent(repo) +
			"/archive/refs/tags/" + encodeURIComponent(tag) + "." + kind;
	}

	function humansize(n) {
		n = Number(n) || 0;
		if (n >= 1073741824)
			return (n / 1073741824).toFixed(1) + " GB";
		if (n >= 1048576) {
			var mb = n / 1048576;
			return (mb >= 10 ? mb.toFixed(0) : mb.toFixed(1)) + " MB";
		}
		if (n >= 1024)
			return (n / 1024).toFixed(0) + " KB";
		if (n > 0)
			return String(n) + " B";
		return "0 B";
	}

	function renderText(text, lang, path) {
		if (window.SpluxRender) {
			if (SpluxRender.isMarkdown(lang, path))
				return SpluxRender.markdown(text);
			return SpluxRender.highlight(text,
				lang || SpluxRender.langFromPath(path) || "");
		}
		return "<pre class=\"block git-blob\">" + esc(text) + "</pre>";
	}

	function sourceArchivesHtml(repo, tag) {
		return "<h3>Source</h3><ul class=\"plain\">" +
			"<li><a href=\"" + esc(archiveHref(repo, tag, "zip")) + "\">" +
			esc(tag) + ".zip</a></li>" +
			"<li><a href=\"" + esc(archiveHref(repo, tag, "tar.gz")) + "\">" +
			esc(tag) + ".tar.gz</a></li></ul>";
	}

	function repoHref(repo) {
		return gitHost + "/" + encodeURIComponent(repo) + "/";
	}

	function slugLang(name) {
		return String(name || "").replace(/[ \/]/g, "-");
	}

	function langHref(repo, name) {
		return repoHref(repo) + "lang/" + encodeURIComponent(slugLang(name)) + "/";
	}

	function peopleHref(repo) {
		return repoHref(repo) + "people/";
	}

	function avatarOf(login, url) {
		if (url && /^https:\/\/(avatars\.githubusercontent\.com\/|github\.com\/)/.test(url))
			return url;
		if (login && validLogin(login))
			return "https://github.com/" + login + ".png?size=48";
		return "";
	}

	function whoHtml(login, name, avatar) {
		var label = login || name || "";
		if (login && validLogin(login)) {
			var av = avatarOf(login, avatar);
			var html = "<a class=\"who\" href=\"" + esc(userHref(login)) + "\">";
			if (av)
				html += "<img class=\"avatar\" src=\"" + esc(av) +
					"\" width=\"24\" height=\"24\" alt=\"\" loading=\"lazy\">";
			html += esc(login) + "</a>";
			return html;
		}
		return esc(label);
	}

	function firstLine(msg) {
		msg = String(msg || "");
		var n = msg.indexOf("\n");
		return n === -1 ? msg : msg.slice(0, n);
	}

	function fromCommit(c, repo) {
		var commit = (c && c.commit) || {};
		var author = (c && c.author) || {};
		var committer = (c && c.committer) || {};
		var login = author.login || committer.login || "";
		return {
			sha: c.sha || "",
			short: (c.sha || "").slice(0, 7),
			iso: (commit.committer && commit.committer.date) ||
				(commit.author && commit.author.date) || "",
			subject: firstLine(commit.message),
			author: (commit.author && commit.author.name) || login,
			login: login,
			avatar: author.avatar_url || committer.avatar_url || "",
			verified: !!(commit.verification && commit.verification.verified),
			repo: repo || ""
		};
	}

	function commitRow(it, opts) {
		opts = opts || {};
		var href = commitHref(it.repo || opts.repo, it.sha);
		var html = "<tr data-sha=\"" + esc(it.sha) + "\" data-login=\"" +
			esc(it.login) + "\" data-repo=\"" + esc(it.repo || opts.repo || "") + "\">";
		html += "<td><a href=\"" + esc(href) + "\"><code>" + esc(it.short) + "</code></a></td>";
		if (opts.showRepo && it.repo)
			html += "<td><a href=\"" + esc(repoHref(it.repo)) + "\">" +
				esc(it.repo) + "</a></td>";
		html += "<td><a href=\"" + esc(href) + "\">" + esc(it.subject) + "</a></td>";
		html += "<td>" + whoHtml(it.login, it.author, it.avatar);
		if (it.verified)
			html += " <span class=\"verified\">verified</span>";
		html += "</td>";
		html += "<td><time datetime=\"" + esc(it.iso) + "\">" +
			esc(dayOf(it.iso)) + "</time></td></tr>";
		return html;
	}

	function paintLangs(root, langs, repo) {
		if (!root || !langs || !langs.length)
			return;
		var total = 0;
		var i;
		for (i = 0; i < langs.length; i++)
			total += langs[i].bytes || 0;
		if (total <= 0)
			return;
		var html = "<div class=\"lang-bar\" role=\"img\" aria-label=\"Languages\">";
		for (i = 0; i < langs.length; i++) {
			var w = Math.round((langs[i].bytes * 1000) / total) / 10;
			if (w <= 0 && langs[i].bytes > 0)
				w = 0.1;
			var href = repo ? langHref(repo, langs[i].name) : "";
			var style = "width:" + w + "%;background:" +
				esc(langs[i].color || colorOf(langs[i].name));
			var title = esc(langs[i].name) + " " + w + "%";
			if (href)
				html += "<a class=\"lang-seg\" href=\"" + esc(href) +
					"\" style=\"" + style + "\" title=\"" + title + "\"></a>";
			else
				html += "<span class=\"lang-seg\" style=\"" + style +
					"\" title=\"" + title + "\"></span>";
		}
		html += "</div><ul class=\"lang-list\">";
		for (i = 0; i < langs.length; i++) {
			var p = Math.round((langs[i].bytes * 1000) / total) / 10;
			var item = "<span class=\"lang-dot\" style=\"background:" +
				esc(langs[i].color || colorOf(langs[i].name)) + "\"></span> " +
				esc(langs[i].name) + " <span class=\"muted\">" + p + "%</span>";
			if (repo)
				html += "<li><a href=\"" + esc(langHref(repo, langs[i].name)) +
					"\">" + item + "</a></li>";
			else
				html += "<li>" + item + "</li>";
		}
		html += "</ul>";
		root.innerHTML = html;
	}

	function langsFromApi(obj) {
		var rows = [];
		var name;
		for (name in obj) {
			if (Object.prototype.hasOwnProperty.call(obj, name))
				rows.push({ name: name, bytes: obj[name], color: colorOf(name) });
		}
		rows.sort(function (a, b) { return b.bytes - a.bytes; });
		return rows;
	}

	function mergeCommitRows(table, items, repo, showRepo) {
		if (!table || !items || !items.length)
			return 0;
		var body = table.tBodies[0] || table;
		var seen = {};
		var existing = body.querySelectorAll("tr[data-sha]");
		var i;
		for (i = 0; i < existing.length; i++)
			seen[existing[i].getAttribute("data-sha")] = true;
		var added = 0;
		var html = "";
		for (i = 0; i < items.length; i++) {
			if (!items[i].sha || seen[items[i].sha])
				continue;
			seen[items[i].sha] = true;
			html += commitRow(items[i], { repo: repo || items[i].repo, showRepo: showRepo });
			added++;
		}
		if (html) {
			body.insertAdjacentHTML("afterbegin", html);
			localizeTimes(body);
		}
		return added;
	}

	function bindFilter() {
		var input = document.getElementById("git-filter");
		if (!input)
			return;
		var table = document.getElementById("git-repos") ||
			document.getElementById("git-people-table") ||
			document.getElementById("git-lang-table") ||
			document.getElementById("git-tag-table") ||
			document.getElementById("git-asset-table") ||
			document.querySelector("table.git-tree") ||
			document.querySelector("table.git-log") ||
			document.querySelector("table.pkgs");
		var list = document.getElementById("git-releases");
		if (!table && !list)
			return;
		function apply() {
			var q = (input.value || "").toLowerCase();
			var i;
			if (table) {
				var rows = table.querySelectorAll("tbody tr");
				for (i = 0; i < rows.length; i++) {
					var hay = rows[i].getAttribute("data-search") ||
						rows[i].textContent || "";
					rows[i].hidden = q !== "" &&
						hay.toLowerCase().indexOf(q) === -1;
				}
			}
			if (list) {
				var arts = list.querySelectorAll("article.release");
				for (i = 0; i < arts.length; i++) {
					var hay2 = arts[i].getAttribute("data-search") ||
						arts[i].textContent || "";
					arts[i].hidden = q !== "" &&
						hay2.toLowerCase().indexOf(q) === -1;
				}
			}
		}
		input.addEventListener("input", apply);
	}

	function fetchCommits(repo, n) {
		n = n || 30;
		return cached("git-commits-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/commits?per_page=" + n).then(function (list) {
			var out = [];
			for (var i = 0; i < list.length; i++)
				out.push(fromCommit(list[i], repo));
			return out;
		});
	}

	function fetchLangs(repo) {
		return cached("git-langs-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/languages").then(langsFromApi);
	}

	function fetchRepo(repo) {
		return cached("git-repo-" + repo, API + "/repos/" + OWNER + "/" + repo);
	}

	function fetchUser(login) {
		return cached("git-user-" + login, API + "/users/" + login);
	}

	function fetchContributors(repo) {
		return cached("git-contrib-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/contributors?per_page=100");
	}

	function peopleRow(it) {
		var search = ((it.login || "") + " " + (it.name || "")).toLowerCase();
		var html = "<tr data-login=\"" + esc(it.login || "") +
			"\" data-search=\"" + esc(search) + "\">";
		html += "<td>" + whoHtml(it.login, it.name, it.avatar) + "</td>";
		html += "<td class=\"muted\">" + esc(String(it.count || "")) + "</td>";
		if (it.sha && it.repo)
			html += "<td><a href=\"" + esc(commitHref(it.repo, it.sha)) + "\">" +
				esc(it.subject || "") + "</a></td>";
		else
			html += "<td>" + esc(it.subject || "") + "</td>";
		if (it.iso)
			html += "<td><time datetime=\"" + esc(it.iso) + "\">" +
				esc(dayOf(it.iso)) + "</time></td>";
		else
			html += "<td></td>";
		return html + "</tr>";
	}

	function mergePeopleRows(table, items) {
		if (!table || !items || !items.length)
			return 0;
		var body = table.tBodies[0] || table;
		var seen = {};
		var existing = body.querySelectorAll("tr[data-login]");
		var i;
		for (i = 0; i < existing.length; i++) {
			var k = existing[i].getAttribute("data-login") ||
				existing[i].textContent;
			if (k)
				seen[k] = true;
		}
		var added = 0;
		var html = "";
		for (i = 0; i < items.length; i++) {
			var key = items[i].login || items[i].name || "";
			if (!key || seen[key])
				continue;
			seen[key] = true;
			html += peopleRow(items[i]);
			added++;
		}
		if (html) {
			body.insertAdjacentHTML("beforeend", html);
			localizeTimes(body);
		}
		return added;
	}

	function refreshPeople(info) {
		if (!knownRepo(info.repo))
			return;
		return fetchContributors(info.repo).then(function (list) {
			if (!list || !list.length)
				return;
			var items = [];
			for (var i = 0; i < list.length; i++) {
				if (!list[i] || list[i].type === "Anonymous")
					continue;
				items.push({
					login: list[i].login || "",
					name: list[i].login || "",
					avatar: list[i].avatar_url || "",
					count: list[i].contributions || "",
					repo: info.repo
				});
			}
			var table = document.getElementById("git-people-table");
			if (table && mergePeopleRows(table, items))
				showLive();
			var strip = document.getElementById("git-people");
			if (strip && items.length && !strip.querySelector(".person")) {
				var html = "";
				for (var j = 0; j < items.length; j++)
					html += "<span class=\"person\">" +
						whoHtml(items[j].login, items[j].name, items[j].avatar) +
						" <span class=\"muted\">" + esc(String(items[j].count)) +
						"</span></span>";
				strip.innerHTML = html;
				showLive();
			}
		});
	}

	function fetchContents(repo, path) {
		var url = API + "/repos/" + OWNER + "/" + repo + "/contents";
		if (path)
			url += "/" + path.split("/").map(encodeURIComponent).join("/");
		return json(url);
	}

	function decodeContent(item) {
		if (!item || item.encoding !== "base64" || !item.content)
			return "";
		try {
			return decodeURIComponent(escape(atob(item.content.replace(/\s/g, ""))));
		} catch (e) {
			try {
				return atob(item.content.replace(/\s/g, ""));
			} catch (e2) {
				return "";
			}
		}
	}

	function paintCommit(info, c) {
		if (!main || !c)
			return;
		var it = fromCommit(c, info.repo);
		var parents = c.parents || [];
		var files = c.files || [];
		var html = navHtml(info.repo, "commit");
		html += "<h2><code>" + esc(it.short) + "</code></h2><hr class=\"rule\">";
		html += "<p>" + esc(it.subject) + "</p>";
		html += "<div class=\"info\">";
		html += "<div class=\"dl-row\"><span class=\"muted\">Author</span><span>" +
			whoHtml(it.login, it.author, it.avatar) +
			(it.verified ? " <span class=\"verified\">verified</span>" : "") +
			"</span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">Date</span><span><time datetime=\"" +
			esc(it.iso) + "\">" + esc(formatLocal(it.iso)) + "</time></span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">Commit</span><span><code>" +
			esc(it.sha) + "</code></span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"" +
			esc(c.html_url || "") + "\">mirror</a></span></div></div>";
		html += "<h3>Parents</h3><ul class=\"plain\">";
		for (var i = 0; i < parents.length; i++) {
			html += "<li><a href=\"" + esc(commitHref(info.repo, parents[i].sha)) +
				"\"><code>" + esc((parents[i].sha || "").slice(0, 7)) +
				"</code></a></li>";
		}
		html += "</ul><h3>Files</h3><ul class=\"plain\">";
		for (var j = 0; j < files.length && j < 80; j++) {
			html += "<li>" + esc(files[j].filename || "") +
				" <span class=\"muted\">" + esc(files[j].status || "") +
				"</span></li>";
		}
		html += "</ul>";
		if (c.stats)
			html += "<p class=\"muted\">+" + esc(String(c.stats.additions || 0)) +
				" / -" + esc(String(c.stats.deletions || 0)) + "</p>";
		var patch = "";
		for (var k = 0; k < files.length; k++) {
			if (files[k].patch)
				patch += "diff --git a/" + (files[k].filename || "") +
					" b/" + (files[k].filename || "") + "\n" +
					files[k].patch + "\n";
		}
		if (patch)
			html += "<h3>Diff</h3>" + renderText(patch, "Diff", "commit.diff");
		main.innerHTML = html;
		document.title = it.short + " - " + info.repo + " - Splux Git";
		localizeTimes(main);
	}

	function paintBlob(info, item) {
		if (!main || !item)
			return;
		var html = navHtml(info.repo, "blob");
		html += "<h2>" + esc(info.path) + "</h2><hr class=\"rule\">";
		if (item.html_url)
			html += "<p class=\"muted\"><a href=\"" + esc(item.html_url) +
				"\">GitHub</a></p>";
		if (item.type === "file") {
			var text = decodeContent(item);
			var lang = (window.SpluxRender && SpluxRender.langFromPath(info.path)) || "";
			if (lang)
				html += "<p class=\"lang-badge\">" + esc(lang) + "</p>";
			if (text)
				html += renderText(text, lang, info.path);
			else
				html += "<p>Open the GitHub mirror for this file.</p>";
		} else {
			html += "<p>Not a file.</p>";
		}
		main.innerHTML = html;
		document.title = info.path + " - " + info.repo + " - Splux Git";
	}

	function paintTree(info, list) {
		if (!main || !list)
			return;
		if (!Array.isArray(list)) {
			paintBlob(info, list);
			return;
		}
		var html = navHtml(info.repo, "files");
		html += "<h2>" + esc(info.path || "Files") + "</h2><hr class=\"rule\">";
		html += "<table class=\"pkgs git-tree\"><thead><tr><th></th><th>Name</th><th>Size</th></tr></thead><tbody>";
		for (var i = 0; i < list.length; i++) {
			var it = list[i];
			var kind = it.type === "dir" ? "dir" : "file";
			var href = gitHost + "/" + encodeURIComponent(info.repo) + "/" +
				(kind === "dir" ? "tree/" : "blob/") +
				(it.path || it.name || "") + "/";
			html += "<tr data-search=\"" + esc((it.name || "").toLowerCase()) + "\">";
			html += "<td class=\"muted\">" + kind + "</td>";
			html += "<td><a href=\"" + esc(href) + "\">" + esc(it.name || "") +
				(kind === "dir" ? "/" : "") + "</a></td>";
			html += "<td class=\"muted\">" + esc(it.size ? String(it.size) : "") +
				"</td></tr>";
		}
		html += "</tbody></table>";
		main.innerHTML = html;
		document.title = (info.path || "Files") + " - " + info.repo + " - Splux Git";
	}

	function paintUser(u, commitsHtml) {
		if (!main || !u)
			return;
		var login = u.login || "";
		var html = "<p class=\"git-nav\"><a href=\"" + esc(gitHost + "/") +
			"\">git</a> · <a href=\"" + esc(gitHost + "/users/") +
			"\">people</a></p>";
		html += "<div class=\"profile\" id=\"git-profile\">";
		if (u.avatar_url)
			html += "<img class=\"avatar profile-avatar\" src=\"" +
				esc(u.avatar_url) + "\" width=\"80\" height=\"80\" alt=\"\">";
		html += "<div class=\"profile-body\"><h2>" + esc(login) + "</h2>";
		if (u.name && u.name !== login)
			html += "<p class=\"profile-name\">" + esc(u.name) + "</p>";
		if (u.bio)
			html += "<p>" + esc(u.bio) + "</p>";
		html += "<div class=\"info\">";
		if (u.html_url)
			html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"" +
				esc(u.html_url) + "\">" + esc(login) + "</a></span></div>";
		if (u.company)
			html += "<div class=\"dl-row\"><span class=\"muted\">Company</span><span>" +
				esc(u.company) + "</span></div>";
		if (u.location)
			html += "<div class=\"dl-row\"><span class=\"muted\">Location</span><span>" +
				esc(u.location) + "</span></div>";
		if (u.blog)
			html += "<div class=\"dl-row\"><span class=\"muted\">URL</span><span><a href=\"" +
				esc(u.blog) + "\">" + esc(u.blog) + "</a></span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">Public repos</span><span>" +
			esc(String(u.public_repos || 0)) + "</span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">Followers</span><span>" +
			esc(String(u.followers || 0)) + "</span></div>";
		html += "</div></div></div>";
		html += "<h3>Commits in Splux trees</h3><div id=\"git-user-log\">" +
			(commitsHtml || "<p class=\"muted\">None in the baked trees yet.</p>") +
			"</div>";
		main.innerHTML = html;
		document.title = login + " - Splux Git";
	}

	function fetchTags(repo) {
		return cached("git-tags-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/tags?per_page=100");
	}

	function fetchReleases(repo) {
		return cached("git-releases-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/releases?per_page=100").then(function (list) {
			var out = [];
			for (var i = 0; i < (list || []).length; i++) {
				if (list[i] && !list[i].draft)
					out.push(list[i]);
			}
			return out;
		});
	}

	function fetchLatestRelease(repo) {
		return cached("git-latest-" + repo, API + "/repos/" + OWNER + "/" +
			repo + "/releases/latest");
	}

	function fetchReleaseByTag(repo, tag) {
		return cached("git-rel-" + repo + "-" + tag, API + "/repos/" + OWNER +
			"/" + repo + "/releases/tags/" + encodeURIComponent(tag));
	}

	function tagRow(it, repo, relset) {
		var name = it.name || "";
		var sha = (it.commit && it.commit.sha) || it.sha || "";
		var short = sha.slice(0, 7);
		var hasRel = relset && relset[name];
		var search = name.toLowerCase();
		var html = "<tr data-tag=\"" + esc(name) + "\" data-sha=\"" +
			esc(sha) + "\" data-search=\"" + esc(search) + "\">";
		html += "<td><a href=\"" + esc(tagHref(repo, name)) +
			"\"><code>" + esc(name) + "</code></a></td>";
		html += "<td>";
		if (sha)
			html += "<a href=\"" + esc(commitHref(repo, sha)) +
				"\"><code>" + esc(short) + "</code></a>";
		html += "</td><td></td><td></td><td>";
		if (hasRel)
			html += "<a href=\"" + esc(releaseHref(repo, name)) +
				"\">release</a>";
		html += "</td></tr>";
		return html;
	}

	function mergeTagRows(table, tags, repo, relset) {
		if (!table || !tags || !tags.length)
			return 0;
		var body = table.tBodies[0] || table;
		var seen = {};
		var existing = body.querySelectorAll("tr[data-tag]");
		var i;
		for (i = 0; i < existing.length; i++)
			seen[existing[i].getAttribute("data-tag")] = true;
		var added = 0;
		var html = "";
		for (i = 0; i < tags.length; i++) {
			var name = tags[i] && tags[i].name;
			if (!name || seen[name])
				continue;
			seen[name] = true;
			html += tagRow(tags[i], repo, relset);
			added++;
		}
		if (html) {
			body.insertAdjacentHTML("afterbegin", html);
			localizeTimes(body);
		}
		return added;
	}

	function releaseArticle(rel, repo, latestTag) {
		var tag = rel.tag_name || "";
		var title = rel.name || tag;
		var login = (rel.author && rel.author.login) || "";
		var avatar = (rel.author && rel.author.avatar_url) || "";
		var iso = rel.published_at || rel.created_at || "";
		var search = (tag + " " + title + " " + login).toLowerCase();
		var nassets = (rel.assets && rel.assets.length) || 0;
		var react = (rel.reactions && rel.reactions.total_count) || 0;
		var html = "<article class=\"release\" data-tag=\"" + esc(tag) +
			"\" data-search=\"" + esc(search) + "\">";
		html += "<p class=\"meta\">";
		if (latestTag && tag === latestTag)
			html += "<span class=\"badge latest\">Latest</span> ";
		if (rel.prerelease)
			html += "<span class=\"badge\">Pre-release</span> ";
		if (iso)
			html += "<time datetime=\"" + esc(iso) + "\">" +
				esc(dayOf(iso)) + "</time> ";
		html += whoHtml(login, login, avatar) + "</p>";
		html += "<h3><a href=\"" + esc(releaseHref(repo, tag)) + "\">" +
			esc(title) + "</a></h3>";
		html += "<p class=\"muted\"><a href=\"" + esc(tagHref(repo, tag)) +
			"\"><code>" + esc(tag) + "</code></a>";
		if (nassets)
			html += " · " + nassets + " files";
		if (react)
			html += " · " + react + " reactions";
		html += "</p>";
		if (rel.body) {
			var excerpt = String(rel.body).replace(/\s+/g, " ");
			if (excerpt.length > 400)
				excerpt = excerpt.slice(0, 400) + "...";
			html += "<p class=\"summary\">" + esc(excerpt) + "</p>";
		}
		html += "</article>";
		return html;
	}

	function mergeReleaseList(root, releases, repo, latestTag) {
		if (!root || !releases || !releases.length)
			return 0;
		var seen = {};
		var existing = root.querySelectorAll("article.release[data-tag]");
		var i;
		for (i = 0; i < existing.length; i++)
			seen[existing[i].getAttribute("data-tag")] = true;
		var added = 0;
		var html = "";
		for (i = 0; i < releases.length; i++) {
			var tag = releases[i] && releases[i].tag_name;
			if (!tag || seen[tag])
				continue;
			seen[tag] = true;
			html += releaseArticle(releases[i], repo, latestTag);
			added++;
		}
		if (html) {
			root.insertAdjacentHTML("afterbegin", html);
			localizeTimes(root);
		}
		return added;
	}

	function paintTags(info, tags, releases) {
		if (!main)
			return;
		var relset = {};
		var i;
		for (i = 0; releases && i < releases.length; i++) {
			if (releases[i] && releases[i].tag_name)
				relset[releases[i].tag_name] = true;
		}
		var html = navHtml(info.repo, "tags");
		html += "<h2>Tags</h2><hr class=\"rule\">";
		html += "<p>Every tag in this tree. A tag with a GitHub release links through to that page.</p>";
		html += "<p><label for=\"git-filter\">Filter</label> <input id=\"git-filter\" type=\"search\" placeholder=\"tag\"></p>";
		if (!tags || !tags.length)
			html += "<p class=\"muted\">No tags in this tree.</p>";
		else {
			html += "<table class=\"pkgs git-tags\" id=\"git-tag-table\"><thead><tr><th>Tag</th><th>Commit</th><th>When</th><th>Message</th><th></th></tr></thead><tbody>";
			for (i = 0; i < tags.length; i++)
				html += tagRow(tags[i], info.repo, relset);
			html += "</tbody></table>";
		}
		main.innerHTML = html;
		document.title = "Tags - " + info.repo + " - Splux Git";
		bindFilter();
	}

	function paintTag(info, tagObj, releases) {
		if (!main)
			return;
		var name = info.path || (tagObj && tagObj.name) || "";
		var sha = (tagObj && tagObj.commit && tagObj.commit.sha) ||
			(tagObj && tagObj.sha) || "";
		var rel = null;
		var i;
		for (i = 0; releases && i < releases.length; i++) {
			if (releases[i] && releases[i].tag_name === name) {
				rel = releases[i];
				break;
			}
		}
		var html = navHtml(info.repo, "tag");
		html += "<h2><code>" + esc(name) + "</code></h2><hr class=\"rule\">";
		html += "<div class=\"info\">";
		if (sha)
			html += "<div class=\"dl-row\"><span class=\"muted\">Commit</span><span><a href=\"" +
				esc(commitHref(info.repo, sha)) + "\"><code>" +
				esc(sha.slice(0, 7)) + "</code></a></span></div>";
		if (rel)
			html += "<div class=\"dl-row\"><span class=\"muted\">Release</span><span><a href=\"" +
				esc(releaseHref(info.repo, name)) + "\">" +
				esc(name) + "</a></span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"https://github.com/" +
			OWNER + "/" + encodeURIComponent(info.repo) + "/releases/tag/" +
			encodeURIComponent(name) + "\">mirror</a></span></div></div>";
		html += sourceArchivesHtml(info.repo, name);
		main.innerHTML = html;
		document.title = name + " - " + info.repo + " - Splux Git";
	}

	function assetsTable(assets) {
		if (!assets || !assets.length)
			return "";
		var html = "<h3>Assets</h3><table class=\"pkgs git-assets\" id=\"git-asset-table\"><thead><tr><th>File</th><th>Size</th><th>Downloads</th></tr></thead><tbody>";
		for (var i = 0; i < assets.length; i++) {
			var a = assets[i];
			html += "<tr data-search=\"" + esc((a.name || "").toLowerCase()) + "\">";
			html += "<td><a href=\"" + esc(a.browser_download_url || "") + "\">" +
				esc(a.name || "") + "</a></td>";
			html += "<td class=\"muted\">" + esc(humansize(a.size)) + "</td>";
			html += "<td class=\"muted\">" + esc(String(a.download_count || 0)) +
				"</td></tr>";
		}
		html += "</tbody></table>";
		return html;
	}

	function paintRelease(info, rel, latestTag, here) {
		if (!main || !rel)
			return;
		var tag = rel.tag_name || info.path || "";
		var title = rel.name || tag;
		var login = (rel.author && rel.author.login) || "";
		var avatar = (rel.author && rel.author.avatar_url) || "";
		var iso = rel.published_at || rel.created_at || "";
		var html = navHtml(info.repo, here || "release");
		html += "<h2>" + esc(title) + "</h2><hr class=\"rule\">";
		html += "<p class=\"meta\">";
		if (latestTag && tag === latestTag)
			html += "<span class=\"badge latest\">Latest</span> ";
		if (rel.prerelease)
			html += "<span class=\"badge\">Pre-release</span> ";
		html += "</p><div class=\"info\">";
		html += "<div class=\"dl-row\"><span class=\"muted\">Tag</span><span><a href=\"" +
			esc(tagHref(info.repo, tag)) + "\"><code>" + esc(tag) +
			"</code></a></span></div>";
		if (iso)
			html += "<div class=\"dl-row\"><span class=\"muted\">Date</span><span><time datetime=\"" +
				esc(iso) + "\">" + esc(formatLocal(iso)) + "</time></span></div>";
		if (login)
			html += "<div class=\"dl-row\"><span class=\"muted\">Author</span><span>" +
				whoHtml(login, login, avatar) + "</span></div>";
		if (rel.assets && rel.assets.length)
			html += "<div class=\"dl-row\"><span class=\"muted\">Files</span><span>" +
				esc(String(rel.assets.length)) + "</span></div>";
		html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"" +
			esc(rel.html_url || ("https://github.com/" + OWNER + "/" +
				info.repo + "/releases/tag/" + encodeURIComponent(tag))) +
			"\">mirror</a></span></div></div>";
		if (rel.body)
			html += "<h3>Notes</h3>" + renderText(rel.body, "Markdown", tag + ".md");
		html += assetsTable(rel.assets);
		html += sourceArchivesHtml(info.repo, tag);
		main.innerHTML = html;
		document.title = title + " - " + info.repo + " - Splux Git";
		localizeTimes(main);
	}

	function paintReleases(info, releases, latestTag) {
		if (!main)
			return;
		var html = navHtml(info.repo, "releases");
		html += "<h2>Releases</h2><hr class=\"rule\">";
		html += "<p>GitHub releases for this tree. ISO files and other assets stay on GitHub. Notes are Markdown. <a href=\"" +
			esc(repoHref(info.repo) + "releases/latest/") + "\">Latest</a></p>";
		html += "<p><label for=\"git-filter\">Filter</label> <input id=\"git-filter\" type=\"search\" placeholder=\"release\"></p>";
		if (!releases || !releases.length)
			html += "<p class=\"muted\">No GitHub releases yet.</p>";
		else {
			html += "<div class=\"release-list\" id=\"git-releases\">";
			for (var i = 0; i < releases.length; i++)
				html += releaseArticle(releases[i], info.repo, latestTag);
			html += "</div>";
		}
		main.innerHTML = html;
		document.title = "Releases - " + info.repo + " - Splux Git";
		bindFilter();
		localizeTimes(main);
	}

	function knownRepo(name) {
		for (var i = 0; i < REPOS.length; i++) {
			if (REPOS[i] === name)
				return true;
		}
		return false;
	}

	function refreshRepo(info) {
		var repo = info.repo;
		if (!knownRepo(repo))
			return;
		return colorsReady().then(function () {
			return Promise.all([
				fetchLangs(repo).catch(function () { return null; }),
				fetchCommits(repo, info.kind === "log" ? 100 : 20).catch(function () { return null; }),
				fetchRepo(repo).catch(function () { return null; }),
				fetchTags(repo).catch(function () { return null; }),
				fetchReleases(repo).catch(function () { return null; }),
				fetchLatestRelease(repo).catch(function () { return null; })
			]);
		}).then(function (rows) {
			var langs = rows[0];
			var commits = rows[1];
			var meta = rows[2];
			var tags = rows[3];
			var releases = rows[4];
			var latest = rows[5];
			var changed = 0;
			if (langs && langs.length) {
				var box = document.getElementById("git-langs");
				if (box) {
					paintLangs(box, langs, repo);
					changed++;
				}
			}
			if (commits && commits.length) {
				var wrap = document.getElementById("git-recent") ||
					document.getElementById("git-log");
				var table = wrap ? wrap.querySelector("table.git-log") : null;
				if (table && mergeCommitRows(table, commits, repo, false))
					changed++;
				if (meta && document.getElementById("git-head")) {
					var sha = commits[0].sha;
					document.getElementById("git-head").innerHTML =
						"<a href=\"" + esc(commitHref(repo, sha)) + "\"><code>" +
						esc(commits[0].short) + "</code></a>";
				}
			}
			if (meta && document.getElementById("git-ncommits") &&
				typeof meta.size === "number") {
				/* keep baked commit count unless GitHub reports more via commits list */
			}
			if (tags && document.getElementById("git-ntags")) {
				var ntagsEl = document.getElementById("git-ntags");
				var ntags = String(tags.length);
				if (ntagsEl.textContent.replace(/\s/g, "") !== ntags) {
					ntagsEl.innerHTML = "<a href=\"" +
						esc(repoHref(repo) + "tags/") + "\">" +
						esc(ntags) + "</a>";
					changed++;
				}
			}
			if (releases && document.getElementById("git-nreleases")) {
				var nrelEl = document.getElementById("git-nreleases");
				var nrel = String(releases.length);
				if (nrelEl.textContent.replace(/\s/g, "") !== nrel) {
					nrelEl.innerHTML = "<a href=\"" +
						esc(repoHref(repo) + "releases/") + "\">" +
						esc(nrel) + "</a>";
					changed++;
				}
			}
			if (latest && latest.tag_name && document.getElementById("git-latest")) {
				var latestEl = document.getElementById("git-latest");
				if (latestEl.textContent.replace(/\s/g, "") !== latest.tag_name) {
					latestEl.innerHTML = "<a href=\"" +
						esc(repoHref(repo) + "releases/latest/") +
						"\"><code>" + esc(latest.tag_name) + "</code></a>";
					changed++;
				}
			}
			if (changed)
				showLive();
		});
	}

	function refreshTags(info) {
		if (!knownRepo(info.repo))
			return;
		return Promise.all([
			fetchTags(info.repo),
			fetchReleases(info.repo).catch(function () { return []; })
		]).then(function (rows) {
			var tags = rows[0] || [];
			var releases = rows[1] || [];
			var relset = {};
			for (var i = 0; i < releases.length; i++) {
				if (releases[i] && releases[i].tag_name)
					relset[releases[i].tag_name] = true;
			}
			var table = document.getElementById("git-tag-table");
			if (table && mergeTagRows(table, tags, info.repo, relset))
				showLive();
		});
	}

	function refreshReleases(info) {
		if (!knownRepo(info.repo))
			return;
		return Promise.all([
			fetchReleases(info.repo),
			fetchLatestRelease(info.repo).catch(function () { return null; })
		]).then(function (rows) {
			var releases = rows[0] || [];
			var latest = rows[1];
			var latestTag = (latest && latest.tag_name) ||
				(releases[0] && releases[0].tag_name) || "";
			var list = document.getElementById("git-releases");
			if (list && mergeReleaseList(list, releases, info.repo, latestTag))
				showLive();
		});
	}

	function refreshIndex() {
		return Promise.all(REPOS.map(function (repo) {
			return fetchRepo(repo).then(function (meta) {
				return fetchCommits(repo, 1).then(function (c) {
					return { repo: repo, meta: meta, commits: c };
				});
			}).catch(function () { return null; });
		})).then(function (rows) {
			var table = document.getElementById("git-repos");
			if (!table)
				return;
			var changed = 0;
			for (var i = 0; i < rows.length; i++) {
				if (!rows[i] || !rows[i].commits || !rows[i].commits.length)
					continue;
				var tr = table.querySelector("tr[data-repo=\"" + rows[i].repo + "\"]");
				if (!tr)
					continue;
				var sha = rows[i].commits[0].sha;
				var short = rows[i].commits[0].short;
				var cells = tr.querySelectorAll("td");
				if (cells[2]) {
					var cur = cells[2].textContent.replace(/\s/g, "");
					if (cur !== short) {
						cells[2].innerHTML = "<a href=\"" +
							esc(commitHref(rows[i].repo, sha)) + "\"><code>" +
							esc(short) + "</code></a>";
						changed++;
					}
				}
				if (cells[4] && rows[i].commits[0].iso)
					cells[4].textContent = dayOf(rows[i].commits[0].iso);
			}
			if (changed)
				showLive();
		});
	}

	function refreshUser(info) {
		if (!info.user || !validLogin(info.user))
			return;
		return fetchUser(info.user).then(function (u) {
			var nameEl = document.querySelector(".profile-name");
			var bio = document.querySelector(".profile-body p:not(.profile-name)");
			if (u.name && nameEl)
				nameEl.textContent = u.name;
			if (u.bio && bio)
				bio.textContent = u.bio;
			showLive();
		}).catch(function () {});
	}

	function hydrate(info) {
		if (!info.hydrate || !main)
			return Promise.resolve();
		if (info.kind === "commit" && knownRepo(info.repo) && info.sha) {
			return json(API + "/repos/" + OWNER + "/" + info.repo +
				"/commits/" + info.sha).then(function (c) {
				paintCommit(info, c);
			});
		}
		if (info.kind === "blob" && knownRepo(info.repo) && info.path) {
			return fetchContents(info.repo, info.path).then(function (item) {
				paintBlob(info, item);
			});
		}
		if (info.kind === "tree" && knownRepo(info.repo)) {
			return fetchContents(info.repo, info.path).then(function (item) {
				paintTree(info, item);
			});
		}
		if (info.kind === "people" && knownRepo(info.repo)) {
			return fetchContributors(info.repo).then(function (list) {
				var items = [];
				for (var i = 0; list && i < list.length; i++) {
					if (!list[i] || list[i].type === "Anonymous")
						continue;
					items.push({
						login: list[i].login || "",
						name: list[i].login || "",
						avatar: list[i].avatar_url || "",
						count: list[i].contributions || "",
						repo: info.repo
					});
				}
				var html = navHtml(info.repo, "people");
				html += "<h2>People</h2><hr class=\"rule\">";
				html += "<p>Everyone who authored a commit in " + esc(info.repo) +
					". <a href=\"" + esc(gitHost + "/users/") +
					"\">All people</a></p>";
				html += "<table class=\"pkgs git-people\" id=\"git-people-table\"><thead><tr><th>Person</th><th>Commits</th><th>Last commit</th><th>When</th></tr></thead><tbody>";
				for (var j = 0; j < items.length; j++)
					html += peopleRow(items[j]);
				html += "</tbody></table>";
				main.innerHTML = html;
				document.title = "People - " + info.repo + " - Splux Git";
				bindFilter();
			});
		}
		if (info.kind === "tags" && knownRepo(info.repo)) {
			return Promise.all([
				fetchTags(info.repo),
				fetchReleases(info.repo).catch(function () { return []; })
			]).then(function (rows) {
				paintTags(info, rows[0], rows[1]);
			});
		}
		if (info.kind === "tag" && knownRepo(info.repo) && info.path) {
			return Promise.all([
				fetchTags(info.repo),
				fetchReleases(info.repo).catch(function () { return []; })
			]).then(function (rows) {
				var tags = rows[0] || [];
				var found = null;
				for (var i = 0; i < tags.length; i++) {
					if (tags[i] && tags[i].name === info.path) {
						found = tags[i];
						break;
					}
				}
				paintTag(info, found || { name: info.path }, rows[1]);
			});
		}
		if (info.kind === "releases" && knownRepo(info.repo)) {
			return Promise.all([
				fetchReleases(info.repo),
				fetchLatestRelease(info.repo).catch(function () { return null; })
			]).then(function (rows) {
				var latest = rows[1];
				paintReleases(info, rows[0], latest && latest.tag_name);
			});
		}
		if ((info.kind === "release" || info.kind === "latest") &&
			knownRepo(info.repo)) {
			var work = info.kind === "latest"
				? fetchLatestRelease(info.repo)
				: fetchReleaseByTag(info.repo, info.path);
			return Promise.all([
				work,
				fetchLatestRelease(info.repo).catch(function () { return null; })
			]).then(function (rows) {
				var latest = rows[1];
				paintRelease(info, rows[0], latest && latest.tag_name,
					info.kind === "latest" ? "latest" : "release");
			});
		}
		if (info.kind === "user" && validLogin(info.user)) {
			return fetchUser(info.user).then(function (u) {
				return Promise.all(REPOS.map(function (repo) {
					return cached("git-author-" + repo + "-" + info.user,
						API + "/repos/" + OWNER + "/" + repo +
						"/commits?author=" + encodeURIComponent(info.user) +
						"&per_page=20").catch(function () { return []; });
				})).then(function (lists) {
					var items = [];
					for (var i = 0; i < lists.length; i++) {
						for (var j = 0; j < lists[i].length; j++)
							items.push(fromCommit(lists[i][j], REPOS[i]));
					}
					items.sort(function (a, b) {
						if (a.iso < b.iso) return 1;
						if (a.iso > b.iso) return -1;
						return 0;
					});
					var table = "";
					if (items.length) {
						table = "<table class=\"pkgs git-log\"><thead><tr><th>Commit</th><th>Repository</th><th>Subject</th><th>Author</th><th>When</th></tr></thead><tbody>";
						for (var k = 0; k < items.length; k++)
							table += commitRow(items[k], { showRepo: true });
						table += "</tbody></table>";
					}
					paintUser(u, table);
					localizeTimes(main);
				});
			});
		}
		return Promise.resolve();
	}

	function ready() {
		bindFilter();
		localizeTimes(document);
		if (window.SpluxRender && main)
			SpluxRender.apply(main);
		var info = pageInfo();
		var work = colorsReady();
		if (info.hydrate) {
			work = work.then(function () { return hydrate(info); })
				.catch(function () {});
			return work;
		}
		if (info.kind === "index" || info.kind === "users")
			work = work.then(refreshIndex).catch(function () {});
		if (info.kind === "repo" || info.kind === "log")
			work = work.then(function () { return refreshRepo(info); })
				.catch(function () {});
		if (info.kind === "people" || info.kind === "repo")
			work = work.then(function () { return refreshPeople(info); })
				.catch(function () {});
		if (info.kind === "tags" || info.kind === "tag" || info.kind === "refs")
			work = work.then(function () { return refreshTags(info); })
				.catch(function () {});
		if (info.kind === "releases" || info.kind === "release" ||
			info.kind === "latest")
			work = work.then(function () { return refreshReleases(info); })
				.catch(function () {});
		if (info.kind === "user")
			work = work.then(function () { return refreshUser(info); })
				.catch(function () {});
		return work;
	}

	if (document.readyState === "loading")
		document.addEventListener("DOMContentLoaded", ready);
	else
		ready();
})();