AGENTS.md 7.5 KiB raw
1
# Radiant Forge
2
3
A self-hosted Git repository browser. Single Go binary, static HTML, no JavaScript.
4
Uses the `git` CLI for all Git operations. All assets (CSS, fonts, JS, SVG logo,
5
templates) are embedded via `//go:embed`.
6
7
## Build & Run
8
9
    go build .
10
    ./forge -scan-path /srv/git -listen :8080
11
12
Flags: `-listen`, `-scan-path`, `-title`, `-base-url`, `-non-bare`,
13
`-username`, `-password`.
14
15
## Project Layout
16
17
```
18
main.go          Server init, repo scanning, HTTP mux, basic auth
19
handler.go       All HTTP handlers + route dispatcher
20
git.go           Types, diff collapsing, MIME map, tree sorting
21
git_cli.go       Git CLI backend (all git operations shell out to `git`)
22
template.go      Template loading, embed directives, template FuncMap
23
go.mod           Module: forge
24
static/
25
  style.css      Single stylesheet, embedded at compile time
26
  radiant.svg    Logo, embedded
27
  fonts/         5 TTF files (RethinkSans, IBM Plex Mono), embedded
28
  js/            Syntax highlighting (hirad.js, hiril.js), embedded
29
templates/
30
  layout.html    Base layout (header, nav, footer, wraps all pages)
31
  index.html     Repository list page
32
  home.html      Repo home: branch selector + file tree + content viewer
33
  log.html       Paginated commit log
34
  commit.html    Commit detail with unified diff
35
  refs.html      Branches and tags tables
36
  error.html     Error page
37
```
38
39
## Architecture
40
41
### Server struct (`main.go`)
42
43
`server` holds: `repos map[string]*RepoInfo`, `sorted []string` (by last update),
44
`tmpl *templateSet`, `title`, `baseURL`, `scanPath`, `username`, `password`.
45
46
Startup: parse flags -> `scanRepositories()` -> `loadTemplates()` -> `http.ListenAndServe`.
47
48
Repository scanning checks top-level dirs in `scan-path` for bare repos
49
(`HEAD` + `objects/` + `refs/`) or non-bare repos (`.git` subdir, opt-in via `-non-bare`).
50
Repos are private by default; they are only served if a `public` file exists in the
51
git directory. Reads optional `description` and `owner` files from the git directory.
52
53
Optional HTTP basic auth via `-username` and `-password` (both must be set together).
54
55
### Routing (`handler.go`)
56
57
`route()` is the main dispatcher. URL structure:
58
59
```
60
/                          -> handleIndex       (repo list)
61
/:repo/                    -> handleSummary     (repo home)
62
/:repo/refs                -> handleRefs        (branches + tags)
63
/:repo/log/:ref?page=N    -> handleLog         (paginated commit log)
64
/:repo/tree/:ref/path...  -> handleTree        (file/dir browser)
65
/:repo/commit/:hash       -> handleCommit      (commit detail + diff)
66
/:repo/raw/:ref/path...   -> handleRaw         (raw file download)
67
/style.css                 -> serveCSS
68
/radiant.svg               -> serveLogo
69
/js/*                      -> serveJS
70
/fonts/*                   -> serveFont
71
```
72
73
Static assets are registered on the mux directly; everything else goes through `route()`.
74
75
### Template data flow
76
77
All pages receive a `pageData` struct:
78
- `SiteTitle`, `BaseURL`, `Repo`, `Description`, `Section`, `Ref`, `CommitHash`
79
- `Data any` — page-specific data struct (e.g. `homeData`, `logData`, etc.)
80
81
Each page template defines `{{define "content"}}` which `layout.html` renders via
82
`{{template "content" .}}`.
83
84
Templates are loaded once at startup. Each page template is parsed together with
85
`layout.html` into its own `*template.Template`. Rendered via `templateSet.render()`.
86
87
### Template functions (`template.go` FuncMap)
88
89
`shortHash`, `timeAgo`, `formatDate`, `add`, `diffFileName`, `statusLabel`,
90
`formatSize`, `diffBar`, `langClass`, `parentPath`, `indent`, `autolink`.
91
92
`timeAgo`, `diffBar`, and `autolink` return raw `template.HTML`.
93
`indent` returns a CSS `padding-left` style string based on tree depth.
94
`autolink` HTML-escapes text and wraps `http://`/`https://` URLs in `<a>` tags.
95
96
### Repo home page (`handleSummary` / `handleTree` -> `renderHome`)
97
98
`renderHome(w, repo, ref, blob, activePath)` is the shared renderer for both the
99
summary page and the tree/file browser.
100
101
- If `ref` is empty, defaults to `getDefaultBranch()` (HEAD's branch name, or first
102
  available branch, or "main")
103
- Resolves the ref to a commit hash
104
- Builds the file tree via `buildTreeNodes()` — returns a flat `[]TreeNode` list with
105
  depth markers, expanding only the directories on the active path
106
- Fetches branches via `getBranches()` for the branch selector dropdown
107
- Gets README from the active directory (or root)
108
- Template data struct: `homeData` with fields `DefaultRef`, `Branches`,
109
  `Tree`, `Readme`, `LastCommit`, `ActiveBlob`, `ActivePath`, `IsEmpty`
110
111
The branch selector is a pure-HTML `<details>/<summary>` dropdown (no JS).
112
113
### Git operations (`git_cli.go`)
114
115
All git operations shell out to the `git` CLI via `exec.Command`. This supports
116
SHA256 repositories which `go-git` cannot handle.
117
118
Key functions:
119
- `resolveRef(refStr)` — resolve ref name or HEAD to a commit hash
120
- `resolveRefAndPath(segments)` — progressively try longer segment prefixes as
121
  ref names (handles refs with slashes like `release/v1.0`)
122
- `getDefaultBranch()` — HEAD's branch name, fallback to first branch, then "main"
123
- `getBranches()` / `getTags()` — returns `[]RefInfo` sorted by date descending
124
- `getTree(hash, path)` — list directory entries, sorted dirs-first then alphabetical
125
- `buildTreeNodes(hash, activePath)` — recursive flat tree for template rendering
126
- `getBlob(hash, path)` — file content, up to 1MB, with binary/UTF-8 detection
127
- `getRawBlob(hash, path)` — raw `io.ReadCloser` for downloads
128
- `getLog(hash, page, perPage)` — paginated commit list
129
- `getDiff(hash)` — unified diff with context collapsing (`diffContextLines = 5`)
130
- `getCommit(hash)` — single commit info
131
- `getReadme(hash, dir)` — finds readme/README.md/README.txt in a directory
132
- `isTreePath(hash, path)` — checks if a path is a directory
133
134
Key types (`git.go`): `RepoInfo`, `CommitInfo`, `TreeNode`, `BlobInfo`, `RefInfo`,
135
`DiffFile`, `DiffHunk`, `DiffLine`, `TreeEntryInfo`, `DiffStats`.
136
137
### CSS (`static/style.css`)
138
139
CSS custom properties in `:root` for colors and fonts. Two font families:
140
`RethinkSans` (sans-serif, body text) and `IBM Plex Mono` (monospace, code/hashes).
141
142
Light theme: beige background (`#d3d1d1`), dark text (`#112`), blue links (`#223377`).
143
Diff colors: green adds (`#c8e6c9`/`#2e7d32`), red deletes (`#ffcdd2`/`#c62828`).
144
145
Font sizes are limited to three values: `0.875rem` (small), `1rem` (base), and
146
`1.125rem` (large). Do not introduce other font sizes.
147
148
Margins and padding use `0.25rem` increments (e.g. `0.25rem`, `0.5rem`, `0.75rem`,
149
`1rem`, `1.5rem`, `2rem`). Do not use arbitrary values like `0.3rem` or `0.4rem`.
150
151
There is a single `@media (max-width: 720px)` breakpoint for mobile. To hide an
152
element on mobile, add the `desktop` class to it in the template HTML. Do not add
153
per-element CSS rules in the media query for hiding.
154
155
## Conventions
156
157
- No JavaScript anywhere. All interactivity is pure HTML (links, `<details>`).
158
  The only JS files are syntax highlighting scripts served as static assets.
159
- All assets are embedded — the binary is fully self-contained.
160
- Templates use Go's `html/template` with a shared layout pattern.
161
- Git operations shell out to the `git` CLI (no `go-git` dependency).
162
- Errors in git operations generally return `nil`/empty rather than propagating to the user
163
  (e.g. `getBranches` errors are silently ignored).
164
- Handler functions follow the pattern: build page-specific data struct, wrap in `pageData`,
165
  call `s.tmpl.render()`.
166
- CSS uses a flat structure with class-based selectors, no BEM or similar methodology.
167
- Repos must have a `public` file in the git directory to be listed.