AGENTS.md (view raw)
1# legit — Agent Guide
2
3A Go git web frontend. Pronounced like a beret-wearing Frenchman.
4
5## Licensing
6
7See `NOTICE` in the project root for the full licensing breakdown. In short:
8
9- **Original code** by Anirudh Oppiliappan: MIT (see `license`)
10- **Modifications by Maxwell Jensen** <maxwelljensen@posteo.net>: EUPL-1.2
11
12The `license` file remains MIT. Each file's copyright and license is documented in `NOTICE` rather than file headers. New files should add an entry to `NOTICE` and may include a short comment header referencing it.
13
14## Essential Commands
15
16| Action | Command |
17|--------|---------|
18| Build | `go build -o legit` |
19| Run | `go run . --config ./config.yaml` |
20| Lint/Typecheck | `golangci-lint run` or `go vet ./...` |
21| Tests | `go test ./...` |
22
23No Makefile. No `go generate` targets. No pre-existing test files.
24
25## Project Structure
26
27```
28├── main.go # Entrypoint: flags, config, unveil, serve
29├── config/
30│ └── config.go # YAML config struct + reader
31├── routes/
32│ ├── routes.go # Handlers: Index, RepoIndex, RepoTree, FileContent, Log, Diff, Refs, Archive, ServeStatic
33│ ├── handler.go # Route registration via http.NewServeMux + git HTTP protocol multiplexing
34│ ├── template.go # Template rendering helpers, chroma syntax highlighting, file display
35│ ├── util.go # Helpers: getDisplayName, isGoModule, getDescription, isIgnored, isUnlisted, MIME helpers
36│ └── git.go # Git HTTP smart protocol: InfoRefs(), UploadPack() via exec'd git-upload-pack
37├── git/
38│ ├── git.go # GitRepo struct: Open, Commits, LastCommit, FileContent, Tags, Branches, FindMainBranch, WriteTar
39│ ├── tree.go # FileTree listing, NiceTree struct
40│ ├── diff.go # Diff/NiceDiff structs, go-gitdiff parsing
41│ └── service/
42│ ├── service.go # git-upload-pack exec wrapper, pack-line/flush wire protocol
43│ └── write_flusher.go # http.Flusher-aware writer for streaming smart protocol responses
44├── templates/ # Go html/template files (10 templates)
45├── static/ # style.css + legit.png favicon
46├── config.yaml # Default config
47├── contrib/ # Dockerfile, docker-compose.yml, systemd unit
48├── flake.nix # Nix flake for build + Docker image
49└── .github/workflows/ # Docker build + push to ghcr.io on master/tags
50```
51
52## Application Architecture & Request Flow
53
541. **`main.go`**: reads YAML config, calls `UnveilPaths()` (no-op on non-OpenBSD via build tags), creates `http.ServeMux` from `routes.Handlers()`, starts HTTP server.
55
562. **Routing** (`routes/handler.go`): uses Go 1.22+ pattern-based `http.NewServeMux` with `{name}`, `{ref}`, `{rest...}` path values. A key pattern — the root `/{name}` and `/{name}/{rest...}` handlers go through **`Multiplex()`** which detects git HTTP smart protocol requests (`info/refs?service=git-upload-pack`, `git-upload-pack` POST) and routes them to git backend, else falls through to `RepoIndex` for web rendering.
57
583. **Git operations** (`git/` package): uses `go-git/v5` (pinned to v5.6.1 via replace directive in go.mod). `git.Open(path, ref)` resolves refs to hashes and returns a `GitRepo`. File browsing, commit log, tree listing all use the in-memory go-git API.
59
604. **Git HTTP smart protocol** (`routes/git.go` + `git/service/`): shells out to `git-upload-pack --stateless-rpc` for clone/fetch over HTTPS. **Push is explicitly blocked** at the handler level with `"no pushing allowed!"`.
61
625. **Archive downloads** (`routes.go:Archive`): streams tar.gz on-the-fly via `GitRepo.WriteTar()` + `compress/gzip`.
63
646. **Templates**: all rendered via `template.ParseGlob(tpath)` per request (not cached — re-parsed every time). Templates are split into `define` blocks: `head`, `nav`, `repoheader`, `index`, `repo`, `tree`, `file`, `log`, `commit`, `refs`, `404`, `500`.
65
66## Key Gotchas
67
68- **`go.mod replace directives`**: `go-git/v5` is pinned to v5.6.1 (not v5.13.x as in go.sum) via a replace directive. `go-gitdiff` has `sergi/go-diff` replaced to v1.1.0. These overrides are intentional — don't remove them.
69- **No template caching**: `template.ParseGlob` is called per-request in every handler. This is by design (hot-reload friendly) but a perf footgun.
70- **`getAllRepos()` is unused** — leftover code that does recursive repo scanning (bare repo detection via HEAD file). The active Index handler reads repos at the top level of `scanPath` only.
71- **`description` file**: per-repo descriptions are read from a plain `description` file at the repo root, not from git config.
72- **`securejoin` everywhere**: all path construction from user input (repo name, file paths) uses `github.com/cyphar/filepath-securejoin` — a security-critical pattern. Never use raw `filepath.Join` with user input.
73- **Static path traversal**: `ServeStatic` goes through `securejoin` too — `{file}` pattern is safe.
74- **Build tags for unveil**: `unveil.go` is `//go:build openbsd`, `unveil_stub.go` is `//go:build !openbsd`. OpenBSD only: calls `unix.Unveil()` to restrict filesystem access.
75- **Docker build copies `config.yaml`** into the image. In production, mount a volume override.
76
77## Route Table
78
79```
80GET / → Index
81GET /static/{file} → ServeStatic
82GET /{name} → Multiplex (detects git protocol)
83POST /{name} → Multiplex (push blocked)
84GET /{name}/tree/{ref}/{rest...} → RepoTree
85GET /{name}/blob/{ref}/{rest...} → FileContent
86GET /{name}/log/{ref} → Log
87GET /{name}/archive/{file} → Archive (only .tar.gz)
88GET /{name}/commit/{ref} → Diff
89GET /{name}/refs/ → Refs
90GET /{name}/{rest...} → Multiplex
91POST /{name}/{rest...} → Multiplex
92```
93
94## Config (`config.yaml`)
95
96| Field | Description |
97|-------|-------------|
98| `repo.scanPath` | Directory to scan for repos (flat, no subdirs in active path) |
99| `repo.readme` | Ordered list of readme filenames to look for |
100| `repo.mainBranch` | Ordered list of branch names to try as default |
101| `repo.ignore` | Repo names to 404 |
102| `repo.unlisted` | Repo names to hide from index (still accessible via URL) |
103| `meta.syntaxHighlight` | Chroma style name for highlighting; empty = no highlight |
104| `server.name` | Used for go-import meta tags and clone URLs |
105
106## Naming & Style Patterns
107
108- All handlers are methods on `deps struct { c *config.Config }` — the dependency injection pattern.
109- Template data uses `map[string]any` (or `map[string]interface{}` — mixed style).
110- Git repo interface: `git.Open(path, ref)` returns `*GitRepo`, then method calls on it.
111- `DisplayName` = repo name with `.git` suffix stripped.
112- Redundant `return` statements exist at the end of several handlers — existing style, don't "fix".
113- The `service` package uses adapted code from `charmbracelet/soft-serve` and `sosedoff/gitkit`.
114
115## Testing
116
117No tests exist yet. The project has no test files. When writing tests:
118- `git.Open()` needs an actual git repo on disk — use `git.PlainInit()` to create temp repos in test setup.
119- Route tests need an `httptest.ResponseRecorder` + the `deps` struct with a test config pointing at a temp dir with repos.
120- The `service` package exec's `git` binary — tests need `git` available in PATH.
121
122## Deploy
123
124- Run behind TLS-terminating proxy (nginx, relayd).
125- Systemd unit at `contrib/legit.service` with `ProtectSystem=strict`.
126- Docker images: `ghcr.io/icyphox/legit:{master,latest,vX.Y.Z}`.
127- Docker compose at `contrib/docker-compose.yml` mounts repos, config, templates, and static from host.