Skip to content
fossil-skin Artifact [a022af168b]
Login

Artifact [a022af168b]

Artifact a022af168b980620c8ce24c46b885e4c9a33e534a19b93018e737d3b0cbd26c3:


#!/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.")