fuzzyworld-tools / commit
Redirect short commit IDs to commit pages; generator v5
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")