gitserver

self-hosted git server tooling
git clone https://git.ryansepassi.com/git/gitserver.git
Log | Files | Refs | README

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:
Massets/style.css | 17+++++++++++++++++
Abin/filetree-html | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbin/stagit-update | 24++++++++++++++++++++++++
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(/&/, "\\&amp;", s); gsub(/</, "\\&lt;", s) + gsub(/>/, "\\&gt;", s); gsub(/"/, "\\&quot;", 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/"