#!/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, subprocess
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")
# Default icon assigned when no meta file exists for a repo
DEFAULT_ICON = "📦"
# Admin user synced across all repos (enables single sign-on via login group)
ADMIN_USER = "dmestas"
ADMIN_PASS = "craftdesign2026"
# ── 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",
}
# ── Apply skin to repos ────────────────────────────────────────────────────
repos = sorted(glob.glob(f"{REPOS_DIR}/*.fossil"))
mtime = int(time.time())
for repo in repos:
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: wiki readable, nothing else (no clone, no timeline, no files)
con.execute("UPDATE user SET cap='jh', mtime=? WHERE login='nobody'", (mtime,))
con.execute("UPDATE user SET cap='jh', mtime=? WHERE login='anonymous'", (mtime,))
con.commit()
con.close()
# Sync admin password so login group single sign-on works with one password
subprocess.run(
["fossil", "user", "password", ADMIN_USER, ADMIN_PASS, "-R", repo],
check=True, capture_output=True,
)
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: two lines — first is the emoji icon, second is the description
meta_path = fossil_path.replace(".fossil", ".meta")
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()
return name, icon, desc
def make_card(name, icon, desc, slug):
esc_name = html.escape(name)
esc_desc = html.escape(desc)
esc_icon = html.escape(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.")