fuzzyworld

fuzzyworld-tools / commit

Redirect short commit IDs to commit pages; generator v5

1cc6342631be29cc6a81f76b06f1a646ac54ff09 · Pavel Safronov · 2026-10-26 19:44:46 -0700 · parents de3a84832ad1

Claude Code usage: this commit ~124.3K out / ~93.6M cached · cost unknown (no per-model trailer) · session 1 tally 1.9K in / 1.0M out / 299.9M cached · usage by commit and session

Redirect short commit IDs to commit pages; generator v5

Apps link to commit/<git rev-parse --short>.html, but commit pages are
named by full SHA. Each of the last 100 commits now also gets 7-12
character redirect pages; a prefix shared by two commits is skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Usage: 1.9K in / 1.0M out / 299.9M cached (session tally)
rtk:   2.4M saved, 55.8% over 5042 cmds (global/lifetime)
 package.json  |  2 +-
 server/gen.py | 32 ++++++++++++++++++++++++++++++--
 2 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 1449830..2cd7923 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fuzzyworld-tools",
-  "version": "1.0.2",
+  "version": "1.0.3",
   "description": "Tooling for code.fuzzyworld.net and apps.fuzzyworld.net: fw command, deploy, vite plugin.",
   "type": "module",
   "bin": {
diff --git a/server/gen.py b/server/gen.py
index 69027bd..dd6fa3f 100755
--- a/server/gen.py
+++ b/server/gen.py
@@ -33,7 +33,7 @@ BASE_URL = "https://code.fuzzyworld.net"
 APPS_URL = "https://apps.fuzzyworld.net"
 
 # Bump when page templates change, so every repo gets a full rebuild once.
-GEN_VERSION = 4
+GEN_VERSION = 5
 
 LOG_LIMIT = 100
 MAX_TEXT_BYTES = 1024 * 1024
@@ -321,6 +321,29 @@ def commits_table(name, commits):
     return f'<table class="log">{rows}</table>'
 
 
+SHORT_SHA_LENGTHS = range(7, 13)
+
+
+def short_sha_aliases(commits):
+    """{alias: full sha} for 7–12 character prefixes, skipping any prefix two commits share.
+
+    Apps link to `commit/<git rev-parse --short>.html`, and the short length varies by clone.
+    """
+    seen = {}
+    for full in (c[0] for c in commits):
+        for n in SHORT_SHA_LENGTHS:
+            seen.setdefault(full[:n], set()).add(full)
+    return {alias: fulls.pop() for alias, fulls in seen.items() if len(fulls) == 1}
+
+
+def write_short_sha_alias(out, name, alias, full):
+    target = f"{full}.html"
+    body = f'<h1>Commit {esc(alias)}</h1><p>See <a href="{esc(target)}"><code>{esc(full)}</code></a>.</p>'
+    html_out = page(f"{name} {alias}", body, repo=name).replace(
+        "<head>\n", f'<head>\n<meta http-equiv="refresh" content="0; url={esc(target)}">\n', 1)
+    write(os.path.join(out, "commit", alias + ".html"), html_out)
+
+
 def build_commit_page(repo, out, name, sha):
     path = os.path.join(out, "commit", sha + ".html")
     if os.path.exists(path):
@@ -443,6 +466,9 @@ def build_repo(name):
         remove(os.path.join(out, "tree", d, "index.html"))
     for h in old_commits - {c[0] for c in commits}:
         remove(os.path.join(out, "commit", h + ".html"))
+    aliases = short_sha_aliases(commits)
+    for a in set(state.get("aliases", [])) - set(aliases):
+        remove(os.path.join(out, "commit", a + ".html"))
 
     # File pages and raw copies: only where the blob changed. Markdown pages are
     # rebuilt whenever HEAD moves, because their links depend on the whole tree.
@@ -469,6 +495,8 @@ def build_repo(name):
             build_tree_page(out, name, d, files, dirs)
         for c in commits:
             build_commit_page(repo, out, name, c[0])
+        for alias, full in aliases.items():
+            write_short_sha_alias(out, name, alias, full)
         default_branch = git_text(repo, "symbolic-ref", "--short", "HEAD", check=False).strip()
         build_refs_page(repo, out, name, default_branch)
         write(os.path.join(out, "log", "index.html"),
@@ -477,7 +505,7 @@ def build_repo(name):
 
     state = {"version": GEN_VERSION, "head": commits[0][0] if commits else None,
              "files": {p: e[1] for p, e in files.items()}, "dirs": sorted(dirs),
-             "commits": [c[0] for c in commits], "built": int(time.time())}
+             "commits": [c[0] for c in commits], "aliases": sorted(aliases), "built": int(time.time())}
     write(state_path, json.dumps(state))
     print(f"{name}: {len(files)} files, {written} file pages written, {len(commits)} commits")