# gsg — a modular git browser
cgit's weight, Hugo's composability, served dynamically. A **module**
computes one piece of a view; a **route** binds a renderer to a URL pattern.
Adding a feature means adding a module and calling it from a template —
`internal/core` never changes.
One dependency (`goldmark`, which itself has none) plus the `git` binary. Go 1.22+ (uses `net/http`
pattern wildcards, so there is no router dependency either).
```
go build -o gsg .
./gsg -root /srv/git -addr :8080 -name "rschanz.org"
./gsg -root /srv/git -templates ./templates # re-parse the theme per request
./gsg -root /srv/git -notices /etc/gsg/notices.yaml -admin-addr 127.0.0.1:8081
```
## The idea
```go
type Module func(c *Ctx, args ...string) (any, error)
```
Templates reach modules through one funcmap entry:
```gotmpl
{{ with mod "meta" }}
{{ .Title }}
{{ end }}
{{ $d := mod "diff" }}
{{ with modOr "readme" }}{{ .HTML }}{{ end }}
```
- **Lazy.** A template that calls `mod "authors"` is what *causes* authors to
be computed. Views that don't ask don't pay.
- **Memoized twice.** Once per request (`Ctx.cache`), and once process-wide
against the resolved commit SHA (`core.LRU`).
- **No manifest.** Dependencies are declared by use. There is no module list
to keep in sync with the templates.
`internal/modules/example_authors.go` is a live demonstration: 30 lines, an
`init()`, and one `{{ with mod "authors" }}` block. Nothing else moved.
## Pages and endpoints are the same mechanism
A route is `{pattern, renderer}`. `core.View` renders a template,
`core.JSON` serializes an endpoint. Both take a `*Ctx`, both call modules.
```go
{Name: "commit", Immutable: true,
Pattern: "GET /{repo}/commit/{rev}",
Render: core.View{Template: "commit.html"}},
{Name: "api-commit", Immutable: true,
Pattern: "GET /{repo}/api/commit/{rev}",
Render: core.JSON{Build: api.CommitEndpoint}},
```
### Endpoints use DTOs, not module values
`internal/api` defines explicit structs and never marshals a module's return
value. Go marshals every exported field by default, so serializing a module
type means the day someone adds `Body` to `gitx.Commit`, commit messages are
public. The allowlist is one readable file.
```
GET /api/repos.json site index
GET /{repo}/api/repo.json slug, title, description, owner, section,
clone_url, default_branch, last_commit
GET /{repo}/api/commit/{sha} sha, short, authored, parents, and
file/line counts — no subject, no body
```
## Routes
| Route | URL |
|---|---|
| index | `/` |
| repo | `/{repo}/` — default branch, last update, latest commit, README |
| files | `/{repo}/tree/{ref}/{path}` |
| file | `/{repo}/blob/{ref}/{path}` |
| **raw bytes** | `/{repo}/raw/{ref}/{path}` |
| log | `/{repo}/log/{ref}` |
| refs | `/{repo}/refs/` |
| commit | `/{repo}/commit/{sha}` |
| **compare** | `/{repo}/compare/{base}..{head}` or `{base}...{head}` |
| **branch graph** | `/{repo}/graph/` |
### Compare
`a..b` is the literal endpoint difference. `a...b` diffs from the merge
base, which is what you want when reviewing a branch — it excludes changes
that landed on the base since the branch forked. Verified against
`git merge-base` on a diverged fixture: `topic..main` lists `topic.txt` as
removed, `topic...main` does not.
### Raw files
A repository can contain an `.html` file with a `