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 ./cmd/legit` |
19| Run | `go run ./cmd/legit --config ./config.yaml` |
20| Release build | `goreleaser --snapshot --clean` |
21| Lint/Typecheck | `golangci-lint run` or `go vet ./...` |
22| Tests | `go test ./...` |
23
24No Makefile. No `go generate` targets. No pre-existing test files.
25
26## Project Structure
27
28```
29├── cmd/legit/
30│ ├── main.go # Entrypoint: args (go-arg), config (viper), unveil, serve
31│ ├── unveil.go # OpenBSD unveil (build tag: openbsd)
32│ └── unveil_stub.go # Unveil no-op (build tag: !openbsd)
33├── internal/
34│ ├── config/
35│ │ └── config.go # YAML config struct + viper reader
36│ ├── routes/
37│ │ ├── routes.go # Handlers: Index, RepoIndex, RepoTree, FileContent, Log, Diff, Refs, Archive, ServeStatic
38│ │ ├── handler.go # Route registration via http.NewServeMux + git HTTP protocol multiplexing
39│ │ ├── template.go # Template rendering helpers, chroma syntax highlighting, file display
40│ │ ├── util.go # Helpers: getDisplayName, isGoModule, getDescription, isIgnored, isUnlisted, MIME helpers
41│ │ └── git.go # Git HTTP smart protocol: InfoRefs(), UploadPack() via exec'd git-upload-pack
42│ └── git/
43│ ├── git.go # GitRepo struct: Open, Commits, LastCommit, FileContent, Tags, Branches, FindMainBranch, WriteTar
44│ ├── tree.go # FileTree listing, NiceTree struct
45│ ├── diff.go # Diff/NiceDiff structs, go-gitdiff parsing
46│ └── service/
47│ ├── service.go # git-upload-pack exec wrapper, pack-line/flush wire protocol
48│ └── write_flusher.go # http.Flusher-aware writer for streaming smart protocol responses
49├── templates/ # Go html/template files (10 templates)
50├── static/ # style.css + legit.png favicon
51├── assets/ # EUPL badge SVG
52├── config.yaml # Default config
53├── .goreleaser.yaml # GoReleaser release build config
54├── contrib/ # Dockerfile, docker-compose.yml, systemd unit
55├── flake.nix # Nix flake for build + Docker image
56└── .github/workflows/ # Docker build + push to ghcr.io on master/tags
57```
58
59## Application Architecture & Request Flow
60
611. **`cmd/legit/main.go`**: parses args via `go-arg`, reads config via `viper` (with `LEGIT_` env var overrides), calls `UnveilPaths()` (no-op on non-OpenBSD via build tags), creates `http.ServeMux` from `routes.Handlers()`, starts HTTP server. Version info (`legit --version`) injected via goreleaser ldflags.
62
632. **Routing** (`internal/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.
64
653. **Git operations** (`internal/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.
66
674. **Git HTTP smart protocol** (`internal/routes/git.go` + `internal/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!"`.
68
695. **Archive downloads** (`internal/routes/routes.go:Archive`): streams tar.gz on-the-fly via `GitRepo.WriteTar()` + `compress/gzip`.
70
716. **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`.
72
73## Key Gotchas
74
75- **`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.
76- **No template caching**: `template.ParseGlob` is called per-request in every handler. This is by design (hot-reload friendly) but a perf footgun.
77- **`description` file**: per-repo descriptions are read from a plain `description` file at the repo root, not from git config.
78- **`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.
79- **Static path traversal**: `ServeStatic` goes through `securejoin` too — `{file}` pattern is safe.
80- **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.
81- **Docker build copies `config.yaml`** into the image. In production, mount a volume override.
82
83## Route Table
84
85```
86GET / → Index
87GET /static/{file} → ServeStatic
88GET /{name} → Multiplex (detects git protocol)
89POST /{name} → Multiplex (push blocked)
90GET /{name}/tree/{ref}/{rest...} → RepoTree
91GET /{name}/blob/{ref}/{rest...} → FileContent
92GET /{name}/log/{ref} → Log
93GET /{name}/archive/{file} → Archive (only .tar.gz)
94GET /{name}/commit/{ref} → Diff
95GET /{name}/refs/ → Refs
96GET /{name}/{rest...} → Multiplex
97POST /{name}/{rest...} → Multiplex
98```
99
100## Config (`config.yaml`)
101
102| Field | Description |
103|-------|-------------|
104| `repo.scanPath` | Directory to scan for repos (flat, no subdirs in active path) |
105| `repo.readme` | Ordered list of readme filenames to look for |
106| `repo.mainBranch` | Ordered list of branch names to try as default |
107| `repo.ignore` | Repo names to 404 |
108| `repo.unlisted` | Repo names to hide from index (still accessible via URL) |
109| `meta.syntaxHighlight` | Chroma style name for highlighting; empty = no highlight |
110| `server.name` | Used for go-import meta tags and clone URLs |
111
112## Naming & Style Patterns
113
114- All handlers are methods on `deps struct { c *config.Config }` — the dependency injection pattern.
115- Template data uses `map[string]any` (or `map[string]interface{}` — mixed style).
116- Git repo interface: `git.Open(path, ref)` returns `*GitRepo`, then method calls on it.
117- `DisplayName` = repo name with `.git` suffix stripped.
118- Redundant `return` statements exist at the end of several handlers — existing style, don't "fix".
119- The `service` package uses adapted code from `charmbracelet/soft-serve` and `sosedoff/gitkit`.
120
121## Testing
122
123No tests exist yet. The project has no test files. When writing tests:
124- `git.Open()` needs an actual git repo on disk — use `git.PlainInit()` to create temp repos in test setup.
125- Route tests need an `httptest.ResponseRecorder` + the `deps` struct with a test config pointing at a temp dir with repos.
126- The `service` package exec's `git` binary — tests need `git` available in PATH.
127
128## Deploy
129
130- Run behind TLS-terminating proxy (nginx, relayd).
131- Systemd unit at `contrib/legit.service` with `ProtectSystem=strict`.
132- Docker images: `ghcr.io/<owner>/legit:{master,latest,vX.Y.Z}` (uses `${{ github.repository }}` from the workflow).
133- Docker compose at `contrib/docker-compose.yml` mounts repos, config, templates, and static from host.