# AGENTS.md Instructions for AI agents and new contributors working in this repository. Read this before changing code. The README describes what the program does for its users; this file describes why it is built the way it is and which properties must survive your change. ## What this is `gsg` is a self-hosted web frontend for bare git repositories. It serves pages dynamically, reads its per-repository configuration out of each repo's own git config, and is designed so that adding a page, a data source, or a JSON endpoint is a local change rather than a change to the core. The target deployment is one binary next to a gitolite-managed repository root, behind a reverse proxy, on a personal server. ### Goals, in priority order 1. **Extensibility without core churn.** Adding a feature should mean adding one file and calling it from one template. If a change requires editing `internal/core`, that is a signal the design is wrong, not that the core needs another special case. 2. **Coherent theming.** Every page shares one layout, one stylesheet, one set of partials. Views do not carry their own styling. 3. **Correctness over speed of delivery.** This serves attacker-influenced input to a public network. A plausible-looking fix that is not understood is worse than an open bug. 4. **Small dependency surface.** Currently: the Go standard library, `github.com/yuin/goldmark` (which has no dependencies of its own), and the `git` binary. Do not add a dependency without a clear reason that cannot be met by the stdlib. ### Non-goals - **Not a forge.** No accounts, no issues, no pull requests, no write paths. Every route is `GET` and nothing mutates a repository. - **Not a static site generator.** An earlier version generated files ahead of time. That was removed because it made diffs and arbitrary-branch browsing impractical. Do not reintroduce build-time page enumeration. - **Not a general git host.** Cloning is served by something else (git-daemon, ssh, gitolite). This is read-only presentation. ## Architecture The layering is strict. Violating it is the most common way to make this codebase worse. ``` main.go flags, repo discovery, embedded assets internal/gitx/ the ONLY package that knows git exists internal/core/ Ctx, module registry, routes, server, cache, funcmap internal/modules/ the extension point: one module per concern internal/api/ JSON DTOs -- the serialization allowlist internal/site/ the route table templates/ layouts/ + views/ static/ one stylesheet ``` **`internal/gitx`** wraps the `git` binary. Nothing above it may invoke git, parse git output, or know what a packfile is. Everything it returns is a plain Go value. This boundary exists so the implementation can be swapped for go-git or git2go by rewriting one package. **`internal/core`** knows about HTTP, templates, caching and the module registry. It must not know git semantics. If you find yourself importing `gitx` into `core` for anything beyond the `Handle`/`Repo` plumbing that is already there, stop and reconsider. **`internal/modules`** is where features go. A module is: ```go type Module func(c *Ctx, args ...string) (any, error) ``` It registers itself in `init()` and is reached from templates through a single funcmap entry, `mod`. Three properties follow and must be preserved: - **Lazy.** A template calling `mod "authors"` is what *causes* authors to be computed. A view that does not ask does not pay. This is why removing a block from a template is a complete removal of its cost. - **Memoized twice.** Per request in `Ctx.cache`, and process-wide in the SHA-keyed `core.LRU`. - **Declared by use.** There is no module manifest to keep in sync with the templates. Do not add one. **`internal/site/routes.go`** is the file you edit most. A route is a `net/http` pattern plus a renderer. ## Invariants These are not style preferences. Each one is either load-bearing for security or was a real bug that got fixed. Do not break them; if a change seems to require breaking one, say so explicitly rather than working around it. ### 1. User ref input never reaches `git rev-parse` `rev-parse` accepts an expression language: `:/regex` searches history, `@{...}` reads the reflog, and git subcommands treat a leading `-` as an option. `gitx.Resolve` accepts only a hex SHA that already exists, or an exact match against the repository's own ref list. Everything else is rejected with a 404. Any new route that takes a ref-like path segment must resolve it through `Resolve` in `Server.buildCtx`, never pass it onward as a raw string. Regression cases that must keep returning 404 with no side effect: `--upload-pack=touch /tmp/pwned`, `:/secret`, `HEAD@{0}`, `-n`. ### 2. Modules read `c.Commit`, not `c.Ref` `c.Ref` is the ref as the user spelled it. `c.Commit` is that ref resolved to a SHA once, at the top of the request. A ref can move between resolution and render; a module reading `c.Ref` would then produce content that does not match the ETag the server already computed. Use `c.Commit` and fall back to `c.Ref` only when `c.Commit` is empty (an empty repository). ### 3. Anything that changes a module's output belongs in the cache key `Ctx.CacheKey` covers repo, commit, path, module name, args, **and `Ctx.Vars`**. Vars were missing originally and caused a real bug: `/compare/a..b` and `/compare/a...b` resolve to the same *head* commit, so the second request was served the first one's diff. If you add a new input to a module, add it to the key. The cache needs no invalidation logic because it is keyed on immutable SHAs. Do not add TTLs or manual eviction on the assumption that entries go stale — if an entry can go stale, the key is wrong. ### 4. JSON endpoints use DTOs, never module values `internal/api` defines explicit structs with json tags. Go marshals every exported field by default, so serializing a module's return type means the day someone adds a field to `gitx.Commit`, it is public. The allowlist lives in one readable file on purpose. Specific product decision: **commit messages are not exposed over the JSON API.** `api.Commit` has no `Subject` and no `Body`. Do not add them. ### 5. `/raw/` never serves `text/html` A repository can contain an `.html` file with a `