# legit — Agent Guide A Go git web frontend. Pronounced like a beret-wearing Frenchman. ## Licensing See `NOTICE` in the project root for the full licensing breakdown. In short: - **Original code** by Anirudh Oppiliappan: MIT (see `license`) - **Modifications by Maxwell Jensen** : EUPL-1.2 The `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. ## Essential Commands | Action | Command | |--------|---------| | Build | `go build -o legit` | | Run | `go run . --config ./config.yaml` | | Lint/Typecheck | `golangci-lint run` or `go vet ./...` | | Tests | `go test ./...` | No Makefile. No `go generate` targets. No pre-existing test files. ## Project Structure ``` ├── main.go # Entrypoint: flags, config, unveil, serve ├── config/ │ └── config.go # YAML config struct + reader ├── routes/ │ ├── routes.go # Handlers: Index, RepoIndex, RepoTree, FileContent, Log, Diff, Refs, Archive, ServeStatic │ ├── handler.go # Route registration via http.NewServeMux + git HTTP protocol multiplexing │ ├── template.go # Template rendering helpers, chroma syntax highlighting, file display │ ├── util.go # Helpers: getDisplayName, isGoModule, getDescription, isIgnored, isUnlisted, MIME helpers │ └── git.go # Git HTTP smart protocol: InfoRefs(), UploadPack() via exec'd git-upload-pack ├── git/ │ ├── git.go # GitRepo struct: Open, Commits, LastCommit, FileContent, Tags, Branches, FindMainBranch, WriteTar │ ├── tree.go # FileTree listing, NiceTree struct │ ├── diff.go # Diff/NiceDiff structs, go-gitdiff parsing │ └── service/ │ ├── service.go # git-upload-pack exec wrapper, pack-line/flush wire protocol │ └── write_flusher.go # http.Flusher-aware writer for streaming smart protocol responses ├── templates/ # Go html/template files (10 templates) ├── static/ # style.css + legit.png favicon ├── config.yaml # Default config ├── contrib/ # Dockerfile, docker-compose.yml, systemd unit ├── flake.nix # Nix flake for build + Docker image └── .github/workflows/ # Docker build + push to ghcr.io on master/tags ``` ## Application Architecture & Request Flow 1. **`main.go`**: reads YAML config, calls `UnveilPaths()` (no-op on non-OpenBSD via build tags), creates `http.ServeMux` from `routes.Handlers()`, starts HTTP server. 2. **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. 3. **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. 4. **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!"`. 5. **Archive downloads** (`routes.go:Archive`): streams tar.gz on-the-fly via `GitRepo.WriteTar()` + `compress/gzip`. 6. **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`. ## Key Gotchas - **`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. - **No template caching**: `template.ParseGlob` is called per-request in every handler. This is by design (hot-reload friendly) but a perf footgun. - **`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. - **`description` file**: per-repo descriptions are read from a plain `description` file at the repo root, not from git config. - **`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. - **Static path traversal**: `ServeStatic` goes through `securejoin` too — `{file}` pattern is safe. - **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. - **Docker build copies `config.yaml`** into the image. In production, mount a volume override. ## Route Table ``` GET / → Index GET /static/{file} → ServeStatic GET /{name} → Multiplex (detects git protocol) POST /{name} → Multiplex (push blocked) GET /{name}/tree/{ref}/{rest...} → RepoTree GET /{name}/blob/{ref}/{rest...} → FileContent GET /{name}/log/{ref} → Log GET /{name}/archive/{file} → Archive (only .tar.gz) GET /{name}/commit/{ref} → Diff GET /{name}/refs/ → Refs GET /{name}/{rest...} → Multiplex POST /{name}/{rest...} → Multiplex ``` ## Config (`config.yaml`) | Field | Description | |-------|-------------| | `repo.scanPath` | Directory to scan for repos (flat, no subdirs in active path) | | `repo.readme` | Ordered list of readme filenames to look for | | `repo.mainBranch` | Ordered list of branch names to try as default | | `repo.ignore` | Repo names to 404 | | `repo.unlisted` | Repo names to hide from index (still accessible via URL) | | `meta.syntaxHighlight` | Chroma style name for highlighting; empty = no highlight | | `server.name` | Used for go-import meta tags and clone URLs | ## Naming & Style Patterns - All handlers are methods on `deps struct { c *config.Config }` — the dependency injection pattern. - Template data uses `map[string]any` (or `map[string]interface{}` — mixed style). - Git repo interface: `git.Open(path, ref)` returns `*GitRepo`, then method calls on it. - `DisplayName` = repo name with `.git` suffix stripped. - Redundant `return` statements exist at the end of several handlers — existing style, don't "fix". - The `service` package uses adapted code from `charmbracelet/soft-serve` and `sosedoff/gitkit`. ## Testing No tests exist yet. The project has no test files. When writing tests: - `git.Open()` needs an actual git repo on disk — use `git.PlainInit()` to create temp repos in test setup. - Route tests need an `httptest.ResponseRecorder` + the `deps` struct with a test config pointing at a temp dir with repos. - The `service` package exec's `git` binary — tests need `git` available in PATH. ## Deploy - Run behind TLS-terminating proxy (nginx, relayd). - Systemd unit at `contrib/legit.service` with `ProtectSystem=strict`. - Docker images: `ghcr.io/icyphox/legit:{master,latest,vX.Y.Z}`. - Docker compose at `contrib/docker-compose.yml` mounts repos, config, templates, and static from host.