# Radiant Forge A self-hosted Git repository browser. Single Go binary, static HTML, no JavaScript. Uses the `git` CLI for all Git operations. All assets (CSS, fonts, JS, SVG logo, templates) are embedded via `//go:embed`. ## Build & Run go build . ./forge -scan-path /srv/git -listen :8080 Flags: `-listen`, `-scan-path`, `-title`, `-base-url`, `-non-bare`, `-username`, `-password`. ## Project Layout ``` main.go Server init, repo scanning, HTTP mux, basic auth handler.go All HTTP handlers + route dispatcher git.go Types, diff collapsing, MIME map, tree sorting git_cli.go Git CLI backend (all git operations shell out to `git`) template.go Template loading, embed directives, template FuncMap go.mod Module: forge static/ style.css Single stylesheet, embedded at compile time radiant.svg Logo, embedded fonts/ 5 TTF files (RethinkSans, IBM Plex Mono), embedded js/ Syntax highlighting (hirad.js, hiril.js), embedded templates/ layout.html Base layout (header, nav, footer, wraps all pages) index.html Repository list page home.html Repo home: branch selector + file tree + content viewer log.html Paginated commit log commit.html Commit detail with unified diff refs.html Branches and tags tables error.html Error page ``` ## Architecture ### Server struct (`main.go`) `server` holds: `repos map[string]*RepoInfo`, `sorted []string` (by last update), `tmpl *templateSet`, `title`, `baseURL`, `scanPath`, `username`, `password`. Startup: parse flags -> `scanRepositories()` -> `loadTemplates()` -> `http.ListenAndServe`. Repository scanning checks top-level dirs in `scan-path` for bare repos (`HEAD` + `objects/` + `refs/`) or non-bare repos (`.git` subdir, opt-in via `-non-bare`). Repos are private by default; they are only served if a `public` file exists in the git directory. Reads optional `description` and `owner` files from the git directory. Optional HTTP basic auth via `-username` and `-password` (both must be set together). ### Routing (`handler.go`) `route()` is the main dispatcher. URL structure: ``` / -> handleIndex (repo list) /:repo/ -> handleSummary (repo home) /:repo/refs -> handleRefs (branches + tags) /:repo/log/:ref?page=N -> handleLog (paginated commit log) /:repo/tree/:ref/path... -> handleTree (file/dir browser) /:repo/commit/:hash -> handleCommit (commit detail + diff) /:repo/raw/:ref/path... -> handleRaw (raw file download) /style.css -> serveCSS /radiant.svg -> serveLogo /js/* -> serveJS /fonts/* -> serveFont ``` Static assets are registered on the mux directly; everything else goes through `route()`. ### Template data flow All pages receive a `pageData` struct: - `SiteTitle`, `BaseURL`, `Repo`, `Description`, `Section`, `Ref`, `CommitHash` - `Data any` — page-specific data struct (e.g. `homeData`, `logData`, etc.) Each page template defines `{{define "content"}}` which `layout.html` renders via `{{template "content" .}}`. Templates are loaded once at startup. Each page template is parsed together with `layout.html` into its own `*template.Template`. Rendered via `templateSet.render()`. ### Template functions (`template.go` FuncMap) `shortHash`, `timeAgo`, `formatDate`, `add`, `diffFileName`, `statusLabel`, `formatSize`, `diffBar`, `langClass`, `parentPath`, `indent`, `autolink`. `timeAgo`, `diffBar`, and `autolink` return raw `template.HTML`. `indent` returns a CSS `padding-left` style string based on tree depth. `autolink` HTML-escapes text and wraps `http://`/`https://` URLs in `` tags. ### Repo home page (`handleSummary` / `handleTree` -> `renderHome`) `renderHome(w, repo, ref, blob, activePath)` is the shared renderer for both the summary page and the tree/file browser. - If `ref` is empty, defaults to `getDefaultBranch()` (HEAD's branch name, or first available branch, or "main") - Resolves the ref to a commit hash - Builds the file tree via `buildTreeNodes()` — returns a flat `[]TreeNode` list with depth markers, expanding only the directories on the active path - Fetches branches via `getBranches()` for the branch selector dropdown - Gets README from the active directory (or root) - Template data struct: `homeData` with fields `DefaultRef`, `Branches`, `Tree`, `Readme`, `LastCommit`, `ActiveBlob`, `ActivePath`, `IsEmpty` The branch selector is a pure-HTML `
/` dropdown (no JS). ### Git operations (`git_cli.go`) All git operations shell out to the `git` CLI via `exec.Command`. This supports SHA256 repositories which `go-git` cannot handle. Key functions: - `resolveRef(refStr)` — resolve ref name or HEAD to a commit hash - `resolveRefAndPath(segments)` — progressively try longer segment prefixes as ref names (handles refs with slashes like `release/v1.0`) - `getDefaultBranch()` — HEAD's branch name, fallback to first branch, then "main" - `getBranches()` / `getTags()` — returns `[]RefInfo` sorted by date descending - `getTree(hash, path)` — list directory entries, sorted dirs-first then alphabetical - `buildTreeNodes(hash, activePath)` — recursive flat tree for template rendering - `getBlob(hash, path)` — file content, up to 1MB, with binary/UTF-8 detection - `getRawBlob(hash, path)` — raw `io.ReadCloser` for downloads - `getLog(hash, page, perPage)` — paginated commit list - `getDiff(hash)` — unified diff with context collapsing (`diffContextLines = 5`) - `getCommit(hash)` — single commit info - `getReadme(hash, dir)` — finds readme/README.md/README.txt in a directory - `isTreePath(hash, path)` — checks if a path is a directory Key types (`git.go`): `RepoInfo`, `CommitInfo`, `TreeNode`, `BlobInfo`, `RefInfo`, `DiffFile`, `DiffHunk`, `DiffLine`, `TreeEntryInfo`, `DiffStats`. ### CSS (`static/style.css`) CSS custom properties in `:root` for colors and fonts. Two font families: `RethinkSans` (sans-serif, body text) and `IBM Plex Mono` (monospace, code/hashes). Light theme: beige background (`#d3d1d1`), dark text (`#112`), blue links (`#223377`). Diff colors: green adds (`#c8e6c9`/`#2e7d32`), red deletes (`#ffcdd2`/`#c62828`). Font sizes are limited to three values: `0.875rem` (small), `1rem` (base), and `1.125rem` (large). Do not introduce other font sizes. Margins and padding use `0.25rem` increments (e.g. `0.25rem`, `0.5rem`, `0.75rem`, `1rem`, `1.5rem`, `2rem`). Do not use arbitrary values like `0.3rem` or `0.4rem`. There is a single `@media (max-width: 720px)` breakpoint for mobile. To hide an element on mobile, add the `desktop` class to it in the template HTML. Do not add per-element CSS rules in the media query for hiding. ## Conventions - No JavaScript anywhere. All interactivity is pure HTML (links, `
`). The only JS files are syntax highlighting scripts served as static assets. - All assets are embedded — the binary is fully self-contained. - Templates use Go's `html/template` with a shared layout pattern. - Git operations shell out to the `git` CLI (no `go-git` dependency). - Errors in git operations generally return `nil`/empty rather than propagating to the user (e.g. `getBranches` errors are silently ignored). - Handler functions follow the pattern: build page-specific data struct, wrap in `pageData`, call `s.tmpl.render()`. - CSS uses a flat structure with class-based selectors, no BEM or similar methodology. - Repos must have a `public` file in the git directory to be listed.