clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

main
forge / lib / urls.ts
urls.ts 3.6K
1// URL scheme. Every page is a directory with an index.html, so URLs read2// like paths: /cat/forge/tree/main/lib/ and /cat/forge/blob/main/lib/git.ts/3//4// Branch names may contain "/", which would make /tree/<branch>/<path>5// ambiguous. "~" is illegal in git ref names, so it encodes "/" reversibly.6import type { Both, Config, RepoUrls } from './types.ts';7import { Hosts, hostsConfig, target } from './hosts.ts';89export const encRef = (ref: string): string => ref.replaceAll('/', '~');10const href = (p: string): string => p.split('/').map((s) => encodeURIComponent(s).replace(/%7E/g, '~')).join('/');1112function fill(template: string, vars: Record<string, string>): string {13 return template.replace(/\{(\w+)\}/g, (_, k: string) => encodeURIComponent(vars[k] ?? ''));14}1516// A deploy's own hostname, once the hook has given it one (see hosts.ts).17// Deploys from before hostnames keep their path URLs.18let hosts: Hosts | null = null;19function hostUrl(owner: string, repo: string, site: string, branch?: string): string | null {20 hosts ??= new Hosts(hostsConfig());21 if (!hosts.cfg.pagesDomain) return null;22 try {23 const label = hosts.labelOf(target(owner, repo, site, branch));24 return label ? `${hosts.cfg.scheme}://${label}.${hosts.cfg.pagesDomain}/` : null;25 } catch {26 return null;27 }28}2930export function repoUrls(cfg: Config, owner: string, name: string): RepoUrls {31 const base = `/${owner}/${name}/`;32 // Each helper returns { path, href }: the on-disk permalink and the encoded link.33 const both = (p: string): Both => ({ path: p, href: href(p) });34 return {35 home: both(base),36 log: (b) => both(`${base}log/${encRef(b)}/`),37 tree: (b, p = '') => both(`${base}tree/${encRef(b)}/${p ? p + '/' : ''}`),38 // Files live under /blob/ so a file named index.html can't collide with39 // its directory's listing page.40 blob: (b, p) => both(`${base}blob/${encRef(b)}/${p}/`),41 blobBase: (b) => both(`${base}blob/${encRef(b)}/`),42 // A file can't have children, so .../<file>/source/ never collides.43 blobSource: (b, p) => both(`${base}blob/${encRef(b)}/${p}/source/`),44 raw: (b, p) => both(`${base}raw/${encRef(b)}/${p}`),45 files: (b) => both(`${base}files/${encRef(b)}.txt`),46 // The search index (lib/search.ts), fetched when the search opens.47 search: both(`${base}search.json`),48 // The default branch's code index (lib/code-index.ts), read by range requests.49 code: both(`${base}code.db`),50 commit: (sha) => both(`${base}commit/${sha}/`),51 refs: both(`${base}refs/`),52 // Branch lists, fetched on demand so a new branch doesn't change every page.53 branchMenu: (kind) => both(`${base}branches.${kind}.html`),54 reviews: both(`${base}reviews/`),55 review: (id) => both(`${base}reviews/${id}/`),56 reviewDiff: (id) => both(`${base}reviews/${id}/diff/`),57 revision: (id, n) => both(`${base}reviews/${id}/r${n}/`),58 sites: both(`${base}sites/`),59 // A release's file, hard-linked there by the hooks and the build.60 download: (tag, file) => both(`${base}releases/download/${encRef(tag)}/${file}`),61 insights: both(`${base}insights/`),62 // Each tag's notes and release files (site/repo/releases.njk).63 releases: both(`${base}releases/`),64 wiki: (p = '') => both(`${base}wiki/${p ? p + '/' : ''}`),65 atom: both(`${base}atom.xml`),66 site: (site) => hostUrl(owner, name, site) ?? fill(cfg.sitesUrl, { owner, repo: name, site }),67 preview: (site, branch) => hostUrl(owner, name, site, branch) ?? fill(cfg.previewsUrl, { owner, repo: name, site, branch: encRef(branch) }),68 cloneSsh: `${cfg.ssh}:${owner}/${name}.git`,69 cloneHttp: cfg.baseUrl ? `${cfg.baseUrl}/${owner}/${name}.git` : null,70 };71}