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