commit 26f271b922b2189c77fea1e47269dfcbe4b3705c
parent abb3ac18981b4a308e329717cd34b171ac2d0472
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 2 Jun 2026 22:38:10 -0700
files: collapsible directory tree listing (dirs grouped first)
Replace stagit's flat files.html table with a no-JS <details> tree built
from git HEAD via bin/filetree-html, spliced in by stagit-update like the
markdown/readme passes. Rows are sorted dirs-before-files at each level.
Diffstat:
3 files changed, 107 insertions(+), 0 deletions(-)
diff --git a/assets/style.css b/assets/style.css
@@ -41,6 +41,22 @@ pre { overflow-x: auto; }
.url { color: #888; font-size: 0.9em; }
.linecount { color: #888; padding-right: 1em; text-align: right; }
#files tr > td:first-child { display: none; }
+#filetree summary {
+ cursor: pointer;
+ list-style: none;
+ padding: 0.05em 0;
+}
+#filetree summary::-webkit-details-marker { display: none; }
+#filetree summary::before { content: "\25b8\00a0"; color: #888; }
+#filetree details[open] > summary::before { content: "\25be\00a0"; }
+#filetree .tree { margin-left: 1.1em; }
+#filetree .f {
+ display: flex;
+ justify-content: space-between;
+ gap: 2em;
+ padding: 0.05em 0;
+}
+#filetree .sz { color: #888; white-space: nowrap; }
.A { color: #080; }
.D { color: #a00; }
.H { font-weight: bold; background: #f4f4f4; }
@@ -74,6 +90,7 @@ img { max-width: 100%; }
hr { border-top-color: #333; }
.desc { color: #999; }
.url, .linecount, .O { color: #777; }
+ #filetree .sz, #filetree summary::before { color: #777; }
.A { color: #7ee787; }
.D { color: #ff7b72; }
.H { background: #222; }
diff --git a/bin/filetree-html b/bin/filetree-html
@@ -0,0 +1,66 @@
+#!/bin/sh
+# Emit a collapsible <details> file tree (HTML fragment) for a bare repo's HEAD.
+# Used by stagit-update to replace stagit's flat <table id="files"> in files.html.
+# Output: a single <div id="filetree">...</div>. Directories are collapsed
+# <details>; files link to the same file/<path>.html blob pages stagit generates.
+# Usage: filetree-html <gitdir>
+set -eu
+
+repo=$1
+
+# Sort rows so that within each directory subdirs group before files (both
+# alphabetical): build a key prefixing every dir component with "0" and the file
+# leaf with "1", sort on it, then drop the key. Directory subtrees stay
+# contiguous, which the tree-builder below relies on.
+git --git-dir="$repo" ls-tree -r --long HEAD \
+| awk -F'\t' '{
+ np = split($2, c, "/"); key = ""
+ for (i = 1; i < np; i++) key = key "0" c[i] "/"
+ print key "1" c[np] "\t" $0
+}' \
+| LC_ALL=C sort \
+| cut -f2- \
+| awk '
+function esc(s) {
+ gsub(/&/, "\\&", s); gsub(/</, "\\<", s)
+ gsub(/>/, "\\>", s); gsub(/"/, "\\"", s)
+ return s
+}
+function hsize(n) {
+ if (n == "-" || n == "") return ""
+ if (n < 1024) return n "B"
+ if (n < 1048576) return sprintf("%.1fK", n / 1024)
+ if (n < 1073741824) return sprintf("%.1fM", n / 1048576)
+ return sprintf("%.1fG", n / 1073741824)
+}
+BEGIN { print "<div id=\"filetree\">"; depth = 0 }
+{
+ # line: <mode> <type> <sha> <size>\t<path>
+ tab = index($0, "\t")
+ meta = substr($0, 1, tab - 1)
+ path = substr($0, tab + 1)
+ split(meta, m, /[ \t]+/); size = m[4]
+ np = split(path, c, "/")
+
+ # how many leading dir components match the currently-open dirs
+ k = 0
+ while (k < depth && k < np - 1 && cur[k + 1] == c[k + 1]) k++
+
+ # close dirs deeper than the shared prefix
+ while (depth > k) { print "</div></details>"; depth-- }
+
+ # open dirs for the new path
+ for (i = k + 1; i <= np - 1; i++) {
+ print "<details><summary>" esc(c[i]) "/</summary><div class=\"tree\">"
+ cur[i] = c[i]; depth++
+ }
+
+ # the file row
+ print "<div class=\"f\"><a href=\"file/" esc(path) ".html\">" esc(c[np]) \
+ "</a><span class=\"sz\">" hsize(size) "</span></div>"
+}
+END {
+ while (depth > 0) { print "</div></details>"; depth-- }
+ print "</div>"
+}
+'
diff --git a/bin/stagit-update b/bin/stagit-update
@@ -54,6 +54,28 @@ render_markdown() {
done
}
+# Replace stagit's flat <table id="files"> in files.html with a collapsible
+# <details> directory tree built from git HEAD (via bin/filetree-html). No-op if
+# files.html or the helper is missing.
+render_file_tree() {
+ out=$1
+ repo=$2
+ [ -f "$out/files.html" ] || return 0
+ [ -x "$REPOS/bin/filetree-html" ] || return 0
+ tmp=$(mktemp)
+ "$REPOS/bin/filetree-html" "$repo" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
+ awk -v frag="$tmp" '
+ !done && /<table id="files">/ {
+ while ((getline line < frag) > 0) print line
+ close(frag); skip=1; done=1; next
+ }
+ skip && /<\/table>/ { skip=0; next }
+ skip { next }
+ { print }
+ ' "$out/files.html" > "$out/files.html.tmp" && mv "$out/files.html.tmp" "$out/files.html"
+ rm -f "$tmp"
+}
+
build_one() {
repo=$1
[ -d "$repo" ] || return 0
@@ -69,6 +91,7 @@ build_one() {
(cd "$out" && stagit -c "$CACHE/$name.priv.cache" "$repo")
link_assets "$out"
render_markdown "$out" "$repo"
+ render_file_tree "$out" "$repo"
if [ -f "$repo/public" ]; then
out=$PUB/$name
@@ -76,6 +99,7 @@ build_one() {
(cd "$out" && stagit -c "$CACHE/$name.pub.cache" "$repo")
link_assets "$out"
render_markdown "$out" "$repo"
+ render_file_tree "$out" "$repo"
# bare repo mirror for `git clone` (skip server-side bits)
mkdir -p "$PUB/git/$name.git"
rsync -a --delete --exclude=hooks --exclude=public "$repo/" "$PUB/git/$name.git/"