#!/usr/bin/env python3
"""Push skin files to all repos and regenerate the landing page repo grid."""
import sqlite3, time, os, glob, re, html, hashlib
REPOS_DIR = "/var/fossil/repos"
SKIN_DIR = os.path.dirname(os.path.abspath(__file__))
WWW_DIR = os.path.join(os.path.dirname(SKIN_DIR), "www")
INDEX_HTML = os.path.join(WWW_DIR, "index.html")
# Icons: skin/icons/<name>.svg (Phosphor duotone, MIT). A meta file's first line names one;
# any other text is used as-is (an emoji). Default when nothing is set:
ICONS_DIR = os.path.join(SKIN_DIR, "icons")
DEFAULT_ICON = "package"
def icon_html(icon):
"""Inline the named SVG icon, or return the text escaped (emoji fallback)."""
path = os.path.join(ICONS_DIR, f"{icon}.svg")
if re.fullmatch(r"[a-z0-9-]+", icon) and os.path.exists(path):
svg = open(path).read()
svg = re.sub(r"<!--.*?-->", "", svg, flags=re.S)
svg = svg.replace("<svg", '<svg aria-hidden="true"', 1)
return re.sub(r"\s+", " ", svg).strip()
return html.escape(icon)
# Default public caps: wiki readable only
DEFAULT_PUBLIC_CAPS = "jh"
def sidecar_meta(fossil_path):
"""Read optional sidecar metadata for one repo.
Format:
line 1: emoji icon
line 2: landing-page description
line 3: optional Fossil caps for the "nobody" user (not logged in)
line 4: optional Fossil caps for the "anonymous" user (defaults to line 3)
"""
meta_path = fossil_path.replace(".fossil", ".meta")
icon = DEFAULT_ICON
desc = ""
nobody_caps = DEFAULT_PUBLIC_CAPS
anon_caps = None
if os.path.exists(meta_path):
lines = open(meta_path).read().splitlines()
if len(lines) >= 1 and lines[0].strip():
icon = lines[0].strip()
if len(lines) >= 2 and lines[1].strip():
desc = lines[1].strip()
if len(lines) >= 3 and lines[2].strip():
nobody_caps = lines[2].strip()
if len(lines) >= 4 and lines[3].strip():
anon_caps = lines[3].strip()
return icon, desc, nobody_caps, anon_caps or nobody_caps
# ── Skin parts pushed to every repo ───────────────────────────────────────
parts = {
"css": open(f"{SKIN_DIR}/css.txt").read(),
"header": open(f"{SKIN_DIR}/header.txt").read(),
"footer": open(f"{SKIN_DIR}/footer.txt").read(),
"skin-detail": "white-foreground",
"logo-image": open(f"{SKIN_DIR}/logo.png", "rb").read(),
"logo-mimetype": "image/png",
}
# Home page: the first of these wiki pages that exists, unless index-page is already set.
HOME_PAGES = ("Home", "index")
# ── Apply skin to repos ────────────────────────────────────────────────────
repos = sorted(glob.glob(f"{REPOS_DIR}/*.fossil"))
mtime = int(time.time())
for repo in repos:
_, _, nobody_caps, anon_caps = sidecar_meta(repo)
con = sqlite3.connect(repo)
for name, value in parts.items():
con.execute("INSERT OR REPLACE INTO config(name,value,mtime) VALUES(?,?,?)",
(name, value, mtime))
# Public access defaults to wiki-only unless a repo's .meta file overrides it.
con.execute("UPDATE user SET cap=?, mtime=? WHERE login='nobody'", (nobody_caps, mtime))
con.execute("UPDATE user SET cap=?, mtime=? WHERE login='anonymous'", (anon_caps, mtime))
if not con.execute("SELECT 1 FROM config WHERE name='index-page'").fetchone():
pages = {r[0] for r in con.execute("SELECT substr(tagname,6) FROM tag WHERE tagname LIKE 'wiki-%'")}
for page in HOME_PAGES:
if page in pages:
con.execute("INSERT INTO config(name,value,mtime) VALUES('index-page',?,?)",
(f"/wiki?name={page}", mtime))
print(f" home page of {os.path.basename(repo)} -> {page}")
break
con.commit()
con.close()
print(f" skinned {os.path.basename(repo)}")
# ── Regenerate landing page repo grid ─────────────────────────────────────
def repo_meta(fossil_path):
"""Read project-name and description from a .fossil file.
Falls back to filename if not set. Also checks for an optional
.meta file alongside the .fossil for icon/desc overrides."""
name = os.path.basename(fossil_path).replace(".fossil", "")
desc = ""
icon = DEFAULT_ICON
# Read project-name from SQLite config
try:
con = sqlite3.connect(fossil_path)
row = con.execute("SELECT value FROM config WHERE name='project-name'").fetchone()
if row:
name = row[0]
row = con.execute("SELECT value FROM config WHERE name='project-description'").fetchone()
if row and row[0]:
desc = row[0]
con.close()
except Exception:
pass
# Optional sidecar file: /var/fossil/repos/<name>.meta
# Format:
# line 1: emoji icon
# line 2: landing-page description
# line 3/4: optional caps for nobody / anonymous
meta_path = fossil_path.replace(".fossil", ".meta")
if os.path.exists(meta_path):
icon_override, desc_override, _, _ = sidecar_meta(fossil_path)
icon = icon_override
if desc_override:
desc = desc_override
return name, icon, desc
def make_card(name, icon, desc, slug):
esc_name = html.escape(name)
esc_desc = html.escape(desc)
esc_icon = icon_html(icon)
return (
f' <a class="repo-card" href="/{slug}/">\n'
f' <div class="repo-icon">{esc_icon}</div>\n'
f' <div class="repo-info">\n'
f' <div class="repo-name">{esc_name}</div>\n'
f' <div class="repo-desc">{esc_desc}</div>\n'
f' </div>\n'
f' <span class="repo-arrow">→</span>\n'
f' </a>'
)
cards = []
for repo in repos:
slug = os.path.basename(repo).replace(".fossil", "")
name, icon, desc = repo_meta(repo)
cards.append(make_card(name, icon, desc, slug))
grid_html = "\n\n".join(cards)
index = open(INDEX_HTML).read()
index = re.sub(
r'<!-- REPO-GRID-START -->.*?<!-- REPO-GRID-END -->',
f'<!-- REPO-GRID-START -->\n{grid_html}\n <!-- REPO-GRID-END -->',
index,
flags=re.DOTALL,
)
open(INDEX_HTML, "w").write(index)
print(" regenerated index.html")
print("done.")