Splux Linux

source-based Linux using SPS

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

4a8b50b


List repo contributors and make languages open a file list.

Date
Commit4a8b50b8908265800fc2fdae0bd248dd3eb244b6
GitHubmirror

Parents

Stat

 README.md             |  13 ++-
 site/assets/git.js    | 172 +++++++++++++++++++++++++--
 site/assets/style.css |  19 +++
 tools/build-git.sh    | 318 +++++++++++++++++++++++++++++++++++++++++---------
 tools/git-lang.awk    |   6 +-
 tools/git.awk         | 144 +++++++++++++++++++++--
 6 files changed, 586 insertions(+), 86 deletions(-)

Diff

diff --git a/README.md b/README.md
index 82d327c..ac4a92e 100644
--- a/README.md
+++ b/README.md
@@ -78,11 +78,14 @@ repository there. `SITE_PREFIX` stays empty so links are rooted at `/`.
 ## Git
 
 `/git/` is a live HTML mirror of SPS, sps-core, sps-extra, and this
-repository. Languages match GitHub Linguist. Commits show the GitHub
-account that uploaded them, with avatars, and `/git/users/` holds
-profiles. The Pages workflow rebuilds those pages; the browser also
-asks GitHub for anything newer. There is no git server and no pull
-requests. Clone from GitHub:
+repository. Languages match GitHub Linguist. Open a language to see
+how many files use it (`/git/<repo>/lang/<Lang>/`). Commits show the
+GitHub account that uploaded them, with avatars. In a repository,
+People lists that tree's commit authors (`/git/<repo>/people/`).
+`/git/users/` holds profiles across all official trees. The Pages
+workflow rebuilds those pages; the browser also asks GitHub for
+anything newer. There is no git server and no pull requests. Clone
+from GitHub:
 
 ```sh
 git clone https://github.com/RobertFlexx/SPS.git
diff --git a/site/assets/git.js b/site/assets/git.js
index 48b2dee..d8fff6d 100644
--- a/site/assets/git.js
+++ b/site/assets/git.js
@@ -131,6 +131,12 @@
 			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("/");
+		}
 		return out;
 	}
 
@@ -176,6 +182,18 @@
 		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;
@@ -242,7 +260,7 @@
 		return html;
 	}
 
-	function paintLangs(root, langs) {
+	function paintLangs(root, langs, repo) {
 		if (!root || !langs || !langs.length)
 			return;
 		var total = 0;
@@ -256,17 +274,28 @@
 			var w = Math.round((langs[i].bytes * 1000) / total) / 10;
 			if (w <= 0 && langs[i].bytes > 0)
 				w = 0.1;
-			html += "<span class=\"lang-seg\" style=\"width:" + w +
-				"%;background:" + esc(langs[i].color || colorOf(langs[i].name)) +
-				"\" title=\"" + esc(langs[i].name) + " " + w + "%\"></span>";
+			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;
-			html += "<li><span class=\"lang-dot\" style=\"background:" +
+			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></li>";
+				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;
@@ -313,8 +342,11 @@
 		if (!input)
 			return;
 		var table = document.getElementById("git-repos") ||
+			document.getElementById("git-people-table") ||
+			document.getElementById("git-lang-table") ||
 			document.querySelector("table.git-tree") ||
-			document.querySelector("table.git-log");
+			document.querySelector("table.git-log") ||
+			document.querySelector("table.pkgs");
 		if (!table)
 			return;
 		function apply() {
@@ -354,6 +386,95 @@
 		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)
@@ -535,7 +656,7 @@
 			if (langs && langs.length) {
 				var box = document.getElementById("git-langs");
 				if (box) {
-					paintLangs(box, langs);
+					paintLangs(box, langs, repo);
 					changed++;
 				}
 			}
@@ -632,6 +753,36 @@
 				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 = "<p class=\"git-nav\"><a href=\"" +
+					esc(repoHref(info.repo)) + "\">" + esc(info.repo) +
+					"</a> · people</p>";
+				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 === "user" && validLogin(info.user)) {
 			return fetchUser(info.user).then(function (u) {
 				return Promise.all(REPOS.map(function (repo) {
@@ -680,6 +831,9 @@
 		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 === "user")
 			work = work.then(function () { return refreshUser(info); })
 				.catch(function () {});
diff --git a/site/assets/style.css b/site/assets/style.css
index e454089..d451817 100644
--- a/site/assets/style.css
+++ b/site/assets/style.css
@@ -652,6 +652,11 @@ pre.block.git-blob {
 	height: 100%;
 	background: var(--accent);
 	min-width: 1px;
+	text-decoration: none;
+}
+
+a.lang-seg:hover {
+	opacity: 0.85;
 }
 
 .lang-list {
@@ -667,6 +672,15 @@ pre.block.git-blob {
 	margin-right: 1rem;
 }
 
+.lang-list a {
+	color: inherit;
+	text-decoration: none;
+}
+
+.lang-list a:hover {
+	color: var(--fg);
+}
+
 .lang-dot {
 	display: inline-block;
 	width: 0.65rem;
@@ -680,6 +694,11 @@ pre.block.git-blob {
 	margin: 0 0 0.5rem;
 }
 
+.lang-badge a {
+	color: inherit;
+	text-decoration: none;
+}
+
 .people {
 	display: flex;
 	flex-wrap: wrap;
diff --git a/tools/build-git.sh b/tools/build-git.sh
index ad97c80..28af5af 100755
--- a/tools/build-git.sh
+++ b/tools/build-git.sh
@@ -237,6 +237,63 @@ who_cell() {
 		-v avatar="$4" -v verified="$5"
 }
 
+lang_slug() {
+	printf '%s\n' "$1" | tr ' /' '--'
+}
+
+# In-repo nav. Links go through ../../../../git/<repo>/ so they work
+# without a trailing slash on the current URL.
+git_nav() {
+	_repo=$1
+	_here=$2
+	_r="../../../../git/${_repo}/"
+	_gh=$(github_of "$_repo")
+	printf '<p class="git-nav">'
+	if [ "$_here" = home ]; then
+		printf '%s' "$(esc "$_repo")"
+	else
+		printf '<a href="%s">%s</a>' "$(esc "$_r")" "$(esc "$_repo")"
+	fi
+	printf ' · '
+	if [ "$_here" = log ]; then
+		printf 'log'
+	else
+		printf '<a href="%slog/">log</a>' "$(esc "$_r")"
+	fi
+	printf ' · '
+	if [ "$_here" = files ]; then
+		printf 'files'
+	else
+		printf '<a href="%stree/">files</a>' "$(esc "$_r")"
+	fi
+	printf ' · '
+	if [ "$_here" = refs ]; then
+		printf 'refs'
+	else
+		printf '<a href="%srefs/">refs</a>' "$(esc "$_r")"
+	fi
+	printf ' · '
+	if [ "$_here" = people ]; then
+		printf 'people'
+	else
+		printf '<a href="%speople/">people</a>' "$(esc "$_r")"
+	fi
+	printf ' · '
+	if [ "$_here" = langs ] || [ "$_here" = lang ]; then
+		printf 'languages'
+	else
+		printf '<a href="%slang/">languages</a>' "$(esc "$_r")"
+	fi
+	printf ' · <a href="%s">GitHub</a>' "$(esc "$_gh")"
+	if [ "$_here" = commit ]; then
+		printf ' · commit'
+	fi
+	if [ "$_here" = blob ]; then
+		printf ' · file'
+	fi
+	printf '</p>\n'
+}
+
 mkdir -p "$OUT/git" "$OUT/data" "$OUT/git/users"
 
 # Refresh Linguist map when the network allows; keep the committed map otherwise.
@@ -297,7 +354,10 @@ build_repo() {
 	nfiles=$(printf '%s' "$nfiles" | tr -d ' ')
 
 	base=$OUT/git/$name
-	mkdir -p "$base/log" "$base/refs" "$base/tree" "$base/commit" "$base/blob"
+	mkdir -p "$base/log" "$base/refs" "$base/tree" "$base/commit" "$base/blob" \
+		"$base/people" "$base/lang"
+	userpfx="../../../../git/users/"
+	reporoot="../../../../git/$name/"
 
 	scan=$base/.scan.tsv
 	filelang=$base/.filelang.tsv
@@ -341,19 +401,102 @@ build_repo() {
 	}' "$clog" >>"$all_commits"
 
 	awk -F '\t' -v OFS='\t' '{
-		if ($7 == "") next
 		login=$7
-		av=$8
-		c[login]++
-		if (av != "" && !(login in avatar))
-			avatar[login]=av
+		name=$6
+		key = (login != "") ? login : name
+		if (key == "") next
+		c[key]++
+		if (login != "")
+			haslogin[key]=login
+		if ($8 != "" && !(key in avatar))
+			avatar[key]=$8
+		if (name != "" && !(key in author))
+			author[key]=name
+		epoch=$3+0
+		if (!(key in last) || epoch >= last[key]) {
+			last[key]=epoch
+			iso[key]=$4
+			subj[key]=$5
+			sha[key]=$1
+		}
 	}
 	END {
-		for (l in c)
-			print l, avatar[l], c[l]
+		for (k in c)
+			print (k in haslogin) ? haslogin[k] : "", avatar[k], c[k], \
+				(k in author) ? author[k] : k, iso[k], subj[k], sha[k]
 	}' "$clog" | LC_ALL=C sort -t "$(printf '\t')" -k3,3nr >"$base/.people.tsv"
 	if [ -s "$base/.people.tsv" ]; then
-		cat "$base/.people.tsv" >>"$all_people"
+		awk -F '\t' -v OFS='\t' '$1 != "" { print $1, $2, $3 }' \
+			"$base/.people.tsv" >>"$all_people"
+	fi
+
+	# Languages with file counts for the bar and the language index.
+	awk -F '\t' -v OFS='\t' -v fl="$filelang" '
+		BEGIN {
+			while ((getline < fl) > 0) {
+				path=$1
+				name=$2
+				col=$4
+				sz=$5+0
+				group=($6 != "") ? $6 : $2
+				if (name != "") {
+					key = name SUBSEP path
+					if (!(key in seen)) {
+						seen[key]=1
+						nf[name]++
+						fsz[name]+=sz
+					}
+					if (col != "" && !(name in color))
+						color[name]=col
+				}
+				if (group != "" && group != name) {
+					key = group SUBSEP path
+					if (!(key in seen)) {
+						seen[key]=1
+						nf[group]++
+						fsz[group]+=sz
+					}
+				}
+			}
+			close(fl)
+		}
+		{
+			gh[$1]=$2+0
+			if ($3 != "")
+				color[$1]=$3
+			bar[$1]=1
+		}
+		END {
+			for (k in nf)
+				all[k]=1
+			for (k in bar)
+				all[k]=1
+			n=0
+			for (k in all) {
+				n++
+				names[n]=k
+			}
+			for (i = 1; i <= n; i++) {
+				for (j = i + 1; j <= n; j++) {
+					bi = (names[i] in gh) ? gh[names[i]] : fsz[names[i]]+0
+					bj = (names[j] in gh) ? gh[names[j]] : fsz[names[j]]+0
+					if (bj > bi) {
+						t=names[i]; names[i]=names[j]; names[j]=t
+					}
+				}
+			}
+			for (i = 1; i <= n; i++) {
+				k=names[i]
+				print k, gh[k]+0, color[k], nf[k]+0, fsz[k]+0
+			}
+		}
+	' "$langtmp" >"$base/.lang-index.tsv"
+	awk -F '\t' -v OFS='\t' '$2 + 0 > 0 { print $1, $2, $3, $4 }' \
+		"$base/.lang-index.tsv" >"$base/.langs-bar.tsv"
+	if [ -s "$base/.langs-bar.tsv" ]; then
+		mv "$base/.langs-bar.tsv" "$langtmp"
+	else
+		rm -f "$base/.langs-bar.tsv"
 	fi
 
 	GIT_KIND=repo
@@ -367,11 +510,7 @@ build_repo() {
 		printf '<h2>%s</h2>\n' "$(esc "$name")"
 		printf '%s\n' '<hr class="rule">'
 		printf '<p>%s</p>\n' "$(esc "$repo_desc")"
-		printf '%s\n' '<p class="git-nav">'
-		printf '<a href="log/">log</a> · <a href="tree/">files</a> · <a href="refs/">refs</a> · '
-		printf '<a href="../users/">people</a> · '
-		printf '<a href="%s">GitHub</a>\n' "$(esc "$gh")"
-		printf '%s\n' '</p>'
+		git_nav "$name" home
 		printf '%s\n' '<p class="live-note" id="git-live" hidden>Showing newer commits from GitHub.</p>'
 		printf '%s\n' '<div class="info" id="git-info">'
 		printf '<div class="dl-row"><span class="muted">HEAD</span><span id="git-head"><a href="commit/%s/"><code>%s</code></a> (%s)</span></div>\n' \
@@ -386,19 +525,20 @@ build_repo() {
 		if [ -s "$langtmp" ]; then
 			printf '%s\n' '<h3>Languages</h3>'
 			printf '%s\n' '<div id="git-langs">'
-			awk -f "$AWK" -v mode=langs "$langtmp"
+			awk -f "$AWK" -v mode=langs -v langpfx="${reporoot}lang/" "$langtmp"
 			printf '%s\n' '</div>'
 		else
 			printf '%s\n' '<div id="git-langs"></div>'
 		fi
 		if [ -s "$base/.people.tsv" ]; then
 			printf '%s\n' '<h3>People</h3>'
-			awk -f "$AWK" -v mode=people -v userpfx="../users/" "$base/.people.tsv"
+			awk -f "$AWK" -v mode=people -v userpfx="$userpfx" "$base/.people.tsv"
+			printf '<p><a href="%speople/">All contributors</a></p>\n' "$(esc "$reporoot")"
 		fi
 		printf '%s\n' '<h3>Recent commits</h3>'
 		printf '%s\n' '<div id="git-recent">'
-		awk -f "$AWK" -v mode=commits -v commitpfx="commit/" \
-			-v userpfx="../users/" -v limit=20 "$clog"
+		awk -f "$AWK" -v mode=commits -v commitpfx="${reporoot}commit/" \
+			-v userpfx="$userpfx" -v limit=20 "$clog"
 		printf '%s\n' '</div>'
 		printf '%s\n' '<p><a href="log/">Full log</a></p>'
 		if git -C "$dir" cat-file -e HEAD:README.md 2>/dev/null; then
@@ -414,14 +554,13 @@ build_repo() {
 	GIT_KIND=log
 	page_begin "$base/log/index.html" "Log - $name - Splux Git" "Commit log for $name"
 	{
-		printf '<p class="git-nav"><a href="../">%s</a> · log · <a href="../../users/">people</a></p>\n' \
-			"$(esc "$name")"
+		git_nav "$name" log
 		printf '<h2>Log</h2>\n'
 		printf '%s\n' '<hr class="rule">'
 		printf '%s\n' '<p class="live-note" id="git-live" hidden>Showing newer commits from GitHub.</p>'
 		printf '%s\n' '<div id="git-log">'
-		awk -f "$AWK" -v mode=commits -v commitpfx="../commit/" \
-			-v userpfx="../../users/" "$clog"
+		awk -f "$AWK" -v mode=commits -v commitpfx="${reporoot}commit/" \
+			-v userpfx="$userpfx" "$clog"
 		printf '%s\n' '</div>'
 	} >>"$base/log/index.html"
 	page_end "$base/log/index.html"
@@ -429,7 +568,7 @@ build_repo() {
 	GIT_KIND=refs
 	page_begin "$base/refs/index.html" "Refs - $name - Splux Git" "Branches and tags for $name"
 	{
-		printf '<p class="git-nav"><a href="../">%s</a> · refs</p>\n' "$(esc "$name")"
+		git_nav "$name" refs
 		printf '<h2>Refs</h2>\n'
 		printf '%s\n' '<hr class="rule">'
 		printf '%s\n' '<h3>Branches</h3>'
@@ -479,14 +618,13 @@ build_repo() {
 		GIT_PATH=
 		page_begin "$cdir/index.html" "$shortc - $name - Splux Git" "$subject"
 		{
-			printf '<p class="git-nav"><a href="../../">%s</a> · <a href="../../log/">log</a> · commit</p>\n' \
-				"$(esc "$name")"
+			git_nav "$name" commit
 			printf '<h2><code>%s</code></h2>\n' "$(esc "$shortc")"
 			printf '%s\n' '<hr class="rule">'
 			printf '<p>%s</p>\n' "$(esc "$subject")"
 			printf '%s\n' '<div class="info">'
 			printf '<div class="dl-row"><span class="muted">Author</span><span>%s</span></div>\n' \
-				"$(who_cell "../../../users/" "$login" "$author" "$avatar" "$verified")"
+				"$(who_cell "$userpfx" "$login" "$author" "$avatar" "$verified")"
 			printf '<div class="dl-row"><span class="muted">Date</span><span><time datetime="%s">%s</time></span></div>\n' \
 				"$(esc "$iso")" "$(esc "$iso")"
 			printf '<div class="dl-row"><span class="muted">Commit</span><span><code>%s</code></span></div>\n' \
@@ -556,8 +694,7 @@ build_repo() {
 		fi
 		page_begin "$tdest" "$title" "HEAD tree for $name"
 		{
-			printf '<p class="git-nav"><a href="%s">%s</a> · files</p>\n' \
-				"$(esc "$up_repo")" "$(esc "$name")"
+			git_nav "$name" files
 			if [ -n "$treepath" ]; then
 				printf '<h2>%s</h2>\n' "$(esc "$treepath")"
 			else
@@ -598,7 +735,7 @@ build_repo() {
 				fi
 			done >"$ent"
 			if [ -s "$ent" ]; then
-				awk -f "$AWK" -v mode=entries "$ent"
+				awk -f "$AWK" -v mode=entries -v langpfx="${reporoot}lang/" "$ent"
 			else
 				printf '%s\n' '<p class="muted">empty directory</p>'
 			fi
@@ -627,41 +764,21 @@ build_repo() {
 		GIT_SHA=
 		GIT_PATH=$path
 		page_begin "$bdest" "$path - $name - Splux Git" "$path at HEAD"
-		parent=${path%/*}
-		depth=1
-		rest=$path
-		while [ "$rest" != "${rest#*/}" ]
-		do
-			depth=$((depth + 1))
-			rest=${rest#*/}
-		done
-		up=""
-		j=0
-		while [ "$j" -le "$depth" ]
-		do
-			up="../$up"
-			j=$((j + 1))
-		done
 		fl=$(lookup_lang "$path" "$filelang")
 		flang=${fl%%$tab*}
 		fcolor=${fl#*$tab}
 		[ "$fcolor" = "$fl" ] && fcolor=
 		{
-			if [ "$parent" = "$path" ]; then
-				dirhref="${up}tree/"
-			else
-				dirhref="${up}tree/${parent}/"
-			fi
-			printf '<p class="git-nav"><a href="%s">%s</a> · <a href="%s">directory</a></p>\n' \
-				"$(esc "$up")" "$(esc "$name")" "$(esc "$dirhref")"
+			git_nav "$name" blob
 			printf '<h2>%s</h2>\n' "$(esc "$path")"
 			printf '%s\n' '<hr class="rule">'
 			if [ -n "$flang" ]; then
-				printf '<p class="lang-badge"><span class="lang-dot" style="background:%s"></span> %s</p>\n' \
+				printf '<p class="lang-badge"><a href="%slang/%s/"><span class="lang-dot" style="background:%s"></span> %s</a></p>\n' \
+					"$(esc "$reporoot")" "$(esc "$(lang_slug "$flang")")" \
 					"$(esc "${fcolor:-#888888}")" "$(esc "$flang")"
 			fi
 			printf '<p class="muted">%s bytes at <a href="%scommit/%s/">%s</a> · <a href="%s/blob/main/%s">GitHub</a></p>\n' \
-				"$(esc "$sz")" "$(esc "$up")" "$(esc "$head")" "$(esc "$short")" \
+				"$(esc "$sz")" "$(esc "$reporoot")" "$(esc "$head")" "$(esc "$short")" \
 				"$(esc "$gh")" "$(esc "$path")"
 			szn=$(printf '%s' "$sz" | tr -d ' ')
 			case $szn in
@@ -678,12 +795,97 @@ build_repo() {
 		page_end "$bdest"
 	done
 
+	GIT_KIND=people
+	GIT_SHA=
+	GIT_PATH=
+	page_begin "$base/people/index.html" "People - $name - Splux Git" \
+		"Commit authors in $name"
+	{
+		git_nav "$name" people
+		printf '%s\n' '<h2>People</h2>'
+		printf '%s\n' '<hr class="rule">'
+		printf '<p>Everyone who authored a commit in %s. GitHub accounts link to their profile. Authors without a GitHub login are listed by git name. <a href="../../../../git/users/">All people</a></p>\n' \
+			"$(esc "$name")"
+		printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
+		printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="person"></p>'
+		if [ -s "$base/.people.tsv" ]; then
+			awk -f "$AWK" -v mode=peoplelist -v userpfx="$userpfx" \
+				-v commitpfx="${reporoot}commit/" "$base/.people.tsv"
+		else
+			printf '%s\n' '<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></tbody></table>'
+		fi
+	} >>"$base/people/index.html"
+	page_end "$base/people/index.html"
+
+	GIT_KIND=langs
+	GIT_PATH=
+	page_begin "$base/lang/index.html" "Languages - $name - Splux Git" \
+		"Languages in $name"
+	{
+		git_nav "$name" langs
+		printf '%s\n' '<h2>Languages</h2>'
+		printf '%s\n' '<hr class="rule">'
+		printf '%s\n' '<p>Click a language to see how many files use it.</p>'
+		printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="language"></p>'
+		if [ -s "$base/.lang-index.tsv" ]; then
+			awk -f "$AWK" -v mode=langindex -v langpfx="${reporoot}lang/" \
+				"$base/.lang-index.tsv"
+		else
+			printf '%s\n' '<p class="muted">No languages recorded.</p>'
+		fi
+	} >>"$base/lang/index.html"
+	page_end "$base/lang/index.html"
+
+	if [ -s "$base/.lang-index.tsv" ]; then
+		while IFS= read -r _lline || [ -n "${_lline-}" ]
+		do
+			[ -n "${_lline-}" ] || continue
+			langname=$(tsv_cut "$_lline" 1)
+			nlangfiles=$(tsv_cut "$_lline" 4)
+			langbytes=$(tsv_cut "$_lline" 5)
+			[ -n "$langname" ] || continue
+			lslug=$(lang_slug "$langname")
+			ldest=$base/lang/$lslug/index.html
+			GIT_KIND=lang
+			GIT_PATH=$langname
+			page_begin "$ldest" "$langname - $name - Splux Git" \
+				"$langname files in $name"
+			{
+				git_nav "$name" lang
+				printf '<h2>%s</h2>\n' "$(esc "$langname")"
+				printf '%s\n' '<hr class="rule">'
+				printf '<p>%s files' "$(esc "${nlangfiles:-0}")"
+				if [ -n "$langbytes" ] && [ "$langbytes" != 0 ]; then
+					printf ', %s bytes' "$(esc "$langbytes")"
+				fi
+				printf ' · <a href="%slang/">all languages</a></p>\n' \
+					"$(esc "$reporoot")"
+				printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="file"></p>'
+				lf=$base/.lang-files.tsv
+				awk -F '\t' -v OFS='\t' -v lang="$langname" \
+					-v blobpfx="${reporoot}blob/" '
+					$2 == lang || $6 == lang {
+						print "file", $5, $1, blobpfx $1 "/", $2
+					}
+				' "$filelang" | LC_ALL=C sort -t "$(printf '\t')" -k3,3 >"$lf"
+				if [ -s "$lf" ]; then
+					awk -f "$AWK" -v mode=entries -v showlang=no "$lf"
+				else
+					printf '%s\n' '<p class="muted">No files listed for this language.</p>'
+				fi
+			} >>"$ldest"
+			page_end "$ldest"
+			rm -f "$lf"
+		done <"$base/.lang-index.tsv"
+	fi
+
 	day=${lastiso%%T*}
 	printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
 		"$name" "$repo_desc" "$short" "$head" "$ncommits" "$nfiles" "$day" "$gh" \
 		>>"$index_rows"
 
-	rm -f "$langtmp" "$clog" "$base/.entries.tsv" "$base/.people.tsv" "$filelang"
+	rm -f "$langtmp" "$clog" "$base/.entries.tsv" "$base/.people.tsv" \
+		"$filelang" "$base/.lang-index.tsv"
 	printf '%s\n' "git: $name ($ncommits commits, $nfiles files)" >&2
 }
 
@@ -746,7 +948,7 @@ page_begin "$OUT/git/users/index.html" "People - Splux Git" "GitHub accounts tha
 	printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
 	if [ -s "$users_full" ]; then
 		awk -F '\t' -v OFS='\t' '{ print $1, $4, $11 }' "$users_full" |
-			awk -f "$AWK" -v mode=people -v userpfx="./"
+			awk -f "$AWK" -v mode=people -v userpfx="../../../../git/users/"
 	else
 		printf '%s\n' '<p class="muted">No GitHub accounts recorded for these commits.</p>'
 	fi
@@ -827,8 +1029,8 @@ if [ -s "$users_full" ]; then
 					print $2, $3, $4, $5, $6, $7, $8, $9, $10, $1
 				}
 			' "$all_commits" |
-				awk -f "$AWK" -v mode=commits -v repopfx="../../" \
-					-v userpfx="../"
+				awk -f "$AWK" -v mode=commits -v repopfx="../../../../git/" \
+					-v userpfx="../../../../git/users/"
 			printf '%s\n' '</div>'
 		} >>"$udest"
 		page_end "$udest"
@@ -870,7 +1072,7 @@ page_begin "$OUT/git/index.html" "Git - Splux Linux" "Always-updating browse of
 	if [ -s "$users_full" ]; then
 		printf '%s\n' '<h3>People</h3>'
 		awk -F '\t' -v OFS='\t' '{ print $1, $4, $11 }' "$users_full" |
-			awk -f "$AWK" -v mode=people -v userpfx="users/"
+			awk -f "$AWK" -v mode=people -v userpfx="../../../../git/users/"
 	fi
 	printf '%s\n' '<h3>Clone</h3>'
 	printf '%s\n' '<pre class="block">git clone https://github.com/RobertFlexx/SPS.git'
diff --git a/tools/git-lang.awk b/tools/git-lang.awk
index 5ba028a..e111cd6 100644
--- a/tools/git-lang.awk
+++ b/tools/git-lang.awk
@@ -157,8 +157,10 @@ BEGIN {
 
 mode == "count" {
 	detect($2, $3)
-	if (filelang != "")
-		print $2 "\t" d_name "\t" d_type "\t" d_color >> filelang
+	if (filelang != "") {
+		g = (d_group != "") ? d_group : d_name
+		print $2 "\t" d_name "\t" d_type "\t" d_color "\t" $1 "\t" g >> filelang
+	}
 	if (skip_stats($2))
 		next
 	if (d_type != "programming" && d_type != "markup")
diff --git a/tools/git.awk b/tools/git.awk
index df59e48..7933240 100644
--- a/tools/git.awk
+++ b/tools/git.awk
@@ -2,12 +2,18 @@
 # mode=one       escape $0 (or -v text=) as one HTML string
 # mode=who       -v login= -v name= -v avatar= -v verified= -v userpfx=
 # mode=pre       escape stdin as a <pre class="block git-blob">
-# mode=langs     TSV lang<TAB>bytes<TAB>color
+# mode=langs     TSV lang<TAB>bytes<TAB>color[<TAB>nfiles]
+#                -v langpfx=
+# mode=langindex TSV lang<TAB>ghbytes<TAB>color<TAB>nfiles<TAB>filebytes
+#                -v langpfx=
 # mode=commits   TSV hash short epoch iso subject author login avatar verified [repo]
 #                -v commitpfx=  -v userpfx=  -v repopfx=
-# mode=people    TSV login avatar count
+# mode=people    TSV login avatar count [name]
 #                -v userpfx=
+# mode=peoplelist TSV login avatar count name iso subject hash
+#                -v userpfx= -v commitpfx=
 # mode=entries   TSV kind size name href [lang]
+#                -v langpfx=  -v showlang=no
 
 function esc(s) {
 	gsub(/&/, "\\&amp;", s)
@@ -23,6 +29,17 @@ function pct(n, d) {
 	return int((n * 1000 + d / 2) / d) / 10
 }
 
+function slug(s) {
+	gsub(/[ \/]/, "-", s)
+	return s
+}
+
+function langhref(name) {
+	if (langpfx == "" || name == "")
+		return ""
+	return langpfx slug(name) "/"
+}
+
 function who(login, name, avatar,    html, av, label) {
 	label = login != "" ? login : name
 	if (login != "") {
@@ -42,7 +59,7 @@ BEGIN {
 	FS = "\t"
 	limit = limit + 0
 	if (userpfx == "")
-		userpfx = "../users/"
+		userpfx = "../../../../git/users/"
 	if (mode == "one") {
 		if (text != "")
 			printf "%s", esc(text)
@@ -73,9 +90,20 @@ mode == "langs" {
 	lang[n] = $1
 	bytes[n] = $2 + 0
 	col[n] = $3
+	nfiles[n] = $4 + 0
 	total += bytes[n]
 }
 
+mode == "langindex" {
+	n++
+	lang[n] = $1
+	bytes[n] = $2 + 0
+	col[n] = $3
+	nfiles[n] = $4 + 0
+	fbytes[n] = $5 + 0
+	total += (bytes[n] > 0) ? bytes[n] : fbytes[n]
+}
+
 mode == "commits" {
 	if (limit > 0 && n >= limit)
 		next
@@ -96,6 +124,18 @@ mode == "people" {
 	login[n] = $1
 	avatar[n] = $2
 	count[n] = $3
+	pname[n] = $4
+}
+
+mode == "peoplelist" {
+	n++
+	login[n] = $1
+	avatar[n] = $2
+	count[n] = $3
+	pname[n] = $4
+	iso[n] = $5
+	subject[n] = $6
+	hash[n] = $7
 }
 
 mode == "entries" {
@@ -126,17 +166,59 @@ END {
 			style = "width:" w "%"
 			if (col[i] != "")
 				style = style ";background:" col[i]
-			printf "<span class=\"lang-seg\" style=\"%s\" title=\"%s %s%%\"></span>\n", \
-				style, esc(lang[i]), w
+			href = langhref(lang[i])
+			title = esc(lang[i]) " " w "%"
+			if (href != "")
+				printf "<a class=\"lang-seg\" href=\"%s\" style=\"%s\" title=\"%s\"></a>\n", \
+					esc(href), style, title
+			else
+				printf "<span class=\"lang-seg\" style=\"%s\" title=\"%s\"></span>\n", \
+					style, title
 		}
 		print "</div>"
 		print "<ul class=\"lang-list\">"
-		for (i = 1; i <= n; i++)
-			printf "<li><span class=\"lang-dot\" style=\"background:%s\"></span> %s <span class=\"muted\">%s%%</span></li>\n", \
-				esc(col[i] != "" ? col[i] : "#888888"), esc(lang[i]), pct(bytes[i], total)
+		for (i = 1; i <= n; i++) {
+			href = langhref(lang[i])
+			dot = esc(col[i] != "" ? col[i] : "#888888")
+			extra = pct(bytes[i], total) "%"
+			if (nfiles[i] > 0)
+				extra = extra " · " nfiles[i] " files"
+			if (href != "")
+				printf "<li><a href=\"%s\"><span class=\"lang-dot\" style=\"background:%s\"></span> %s <span class=\"muted\">%s</span></a></li>\n", \
+					esc(href), dot, esc(lang[i]), extra
+			else
+				printf "<li><span class=\"lang-dot\" style=\"background:%s\"></span> %s <span class=\"muted\">%s</span></li>\n", \
+					dot, esc(lang[i]), extra
+		}
 		print "</ul>"
 		exit
 	}
+	if (mode == "langindex") {
+		print "<table class=\"pkgs git-langs\" id=\"git-lang-table\">"
+		print "<thead><tr><th>Language</th><th>Files</th><th>Bytes</th><th></th></tr></thead>"
+		print "<tbody>"
+		for (i = 1; i <= n; i++) {
+			href = langhref(lang[i])
+			dot = esc(col[i] != "" ? col[i] : "#888888")
+			sz = bytes[i] > 0 ? bytes[i] : fbytes[i]
+			w = pct(sz, total)
+			printf "<tr data-search=\"%s\">", esc(tolower(lang[i]))
+			printf "<td>"
+			if (href != "")
+				printf "<a href=\"%s\"><span class=\"lang-dot\" style=\"background:%s\"></span> %s</a>", \
+					esc(href), dot, esc(lang[i])
+			else
+				printf "<span class=\"lang-dot\" style=\"background:%s\"></span> %s", \
+					dot, esc(lang[i])
+			printf "</td>"
+			printf "<td class=\"muted\">%s</td>", (nfiles[i] > 0 ? nfiles[i] : "")
+			printf "<td class=\"muted\">%s</td>", (sz > 0 ? sz : "")
+			printf "<td class=\"muted\">%s%%</td>", w
+			print "</tr>"
+		}
+		print "</tbody></table>"
+		exit
+	}
 	if (mode == "commits") {
 		print "<table class=\"pkgs git-log\">"
 		if (repopfx != "")
@@ -154,7 +236,7 @@ END {
 			printf "<tr data-sha=\"%s\" data-login=\"%s\" data-repo=\"%s\">", \
 				esc(hash[i]), esc(login[i]), esc(repo[i])
 			printf "<td><a href=\"%s\"><code>%s</code></a></td>", href, esc(short[i])
-			if (repopfx != "")
+			if (repopfx != "" && repo[i] != "")
 				printf "<td><a href=\"%s%s/\">%s</a></td>", \
 					repopfx, esc(repo[i]), esc(repo[i])
 			printf "<td><a href=\"%s\">%s</a></td>", href, esc(subject[i])
@@ -171,7 +253,8 @@ END {
 	if (mode == "people") {
 		print "<div class=\"people\" id=\"git-people\">"
 		for (i = 1; i <= n; i++) {
-			printf "<span class=\"person\">%s", who(login[i], login[i], avatar[i])
+			label = pname[i] != "" ? pname[i] : login[i]
+			printf "<span class=\"person\">%s", who(login[i], label, avatar[i])
 			if (count[i] != "")
 				printf " <span class=\"muted\">%s</span>", esc(count[i])
 			print "</span>"
@@ -179,9 +262,40 @@ END {
 		print "</div>"
 		exit
 	}
+	if (mode == "peoplelist") {
+		print "<table class=\"pkgs git-people\" id=\"git-people-table\">"
+		print "<thead><tr><th>Person</th><th>Commits</th><th>Last commit</th><th>When</th></tr></thead>"
+		print "<tbody>"
+		for (i = 1; i <= n; i++) {
+			label = pname[i] != "" ? pname[i] : login[i]
+			search = tolower(login[i] " " label)
+			printf "<tr data-login=\"%s\" data-search=\"%s\">", \
+				esc(login[i]), esc(search)
+			printf "<td>%s</td>", who(login[i], label, avatar[i])
+			printf "<td class=\"muted\">%s</td>", esc(count[i])
+			if (hash[i] != "" && commitpfx != "") {
+				chref = commitpfx esc(hash[i]) "/"
+				printf "<td><a href=\"%s\">%s</a></td>", chref, esc(subject[i])
+			} else
+				printf "<td>%s</td>", esc(subject[i])
+			day = iso[i]
+			sub(/T.*/, "", day)
+			if (iso[i] != "")
+				printf "<td><time datetime=\"%s\">%s</time></td>", \
+					esc(iso[i]), esc(day)
+			else
+				printf "<td></td>"
+			print "</tr>"
+		}
+		print "</tbody></table>"
+		exit
+	}
 	if (mode == "entries") {
 		print "<table class=\"pkgs git-tree\">"
-		print "<thead><tr><th></th><th>Name</th><th>Language</th><th>Size</th></tr></thead>"
+		if (showlang == "no")
+			print "<thead><tr><th></th><th>Name</th><th>Size</th></tr></thead>"
+		else
+			print "<thead><tr><th></th><th>Name</th><th>Language</th><th>Size</th></tr></thead>"
 		print "<tbody>"
 		for (i = 1; i <= n; i++) {
 			label = kind[i] == "dir" ? "dir" : "file"
@@ -189,7 +303,13 @@ END {
 			printf "<tr data-search=\"%s %s\">", esc(tolower(name[i])), esc(tolower(elang[i]))
 			printf "<td class=\"muted\">%s</td>", label
 			printf "<td><a href=\"%s\">%s</a></td>", esc(href[i]), esc(name[i])
-			printf "<td class=\"muted\">%s</td>", esc(elang[i])
+			if (showlang != "no") {
+				lhref = langhref(elang[i])
+				if (lhref != "" && elang[i] != "")
+					printf "<td><a href=\"%s\">%s</a></td>", esc(lhref), esc(elang[i])
+				else
+					printf "<td class=\"muted\">%s</td>", esc(elang[i])
+			}
 			printf "<td class=\"muted\">%s</td>", esc(sz)
 			print "</tr>"
 		}