# 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/access/ repo visibility + share-grant policy (imports no core) internal/authn/ login/logout endpoints internal/httpx/ shared admin/share HTTP security helpers internal/modules/ the extension point: one module per concern internal/notices/ operator notices: YAML store + admin UI (imports no core) internal/api/ JSON DTOs -- the serialization allowlist internal/site/ the route table and route-specific request parsers 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/access`** owns the rule for whether an identity may view or share a repo. It deliberately imports nothing from `core`, so the 404 gate, the index module, share minting and share redemption all call the same policy. Do not grow a second visibility check elsewhere. **`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. `Ctx.contentIdentity()` also folds in inputs that change *without* the commit moving: `RepoInfo.GPGKeyring` and `Site.Revision()`. Site revision is the site-level analogue of the keyring; operator notices use it so a banner edit invalidates both the module cache and every ETag. Anything else with that shape belongs there too. `Ctx.Immutable` marks responses served with the year-long immutable cache (see `Server.serve`). Commit-independent chrome must not be rendered into them, because the browser is told never to revalidate; the notices partial skips those pages for exactly this reason. ### 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 `