# Current

> Keep this file short. One active step, one ordered backlog. Completed work moves to
> [progress.md](progress.md). If this file starts reading like a changelog, it has
> drifted — that's exactly what went wrong last time.

## Active: Milestone 4a — Clone over HTTP

**Goal:** `git clone https://host/{handle}/repos/{name}.git` works against a public
repository, for anyone, with no credentials. The protocol is delegated to `git
http-backend` per [0001](decisions/0001-git-over-http-not-ssh.md).

**Out of scope:** personal access tokens, HTTP Basic, push, cloning a private repo —
all of that is 4b. Also out: browsing a tree in the UI (Milestone 5), and any repo
statistic the clone path could tempt us into computing.

### Steps

- [ ] Probe `git http-backend`'s actual contract — which CGI variables it reads, how it
      reports failure, what it does with an unauthorised path. Findings to
      `progress.md`; no application code in this step.
- [ ] Application: `GitProtocolServer` port — a CGI-shaped request/response pair, plus
      `GitOperation` (Read/Write) as the thing authorization is decided on
- [ ] Infrastructure: `GitHttpBackend` adapter, spawning through the existing `run_git`
      invoker; streams stdin in and stdout out, parsing CGI headers off the front
- [ ] Application: `serve_git` use case — resolves the repository, enforces visibility,
      refuses writes outright, and only then delegates
- [ ] Web: the three git routes under `/{handle}/repos/{name}.git/`, with `body_limit`
      raised
- [ ] Verify with a real `git clone` of a repo with enough refs to trigger a gzipped
      request body

### Done when

`git clone http://127.0.0.1:3000/{handle}/repos/{name}.git` produces a working
checkout of a public repository, with no credentials, and the cloned history matches
the origin. A private repository is not clonable by anyone yet — not even its owner.
`git push` is refused.

### Settled

- **`git http-backend`, not direct `--stateless-rpc`.** Both put identical bytes on the
  wire for a modern clone of a small repo; the difference is entirely in the tail, which
  is what wide adoption means. Measured before choosing: a client cloning a repo with
  201 refs **gzip-compresses the POST body** (5KB here), on protocol v0 *and* v2. A
  direct implementation must therefore inflate request bodies and forward
  `Git-Protocol` itself, and gets neither the dumb-protocol fallback nor the header set.
  The failure mode decided it — a direct implementation passes against a one-ref test
  repo and breaks on the first real one.
- **The clone URL is `/{handle}/repos/{name}.git`**, matching the page at
  `/{handle}/repos/{name}`. Scoped rather than root-level, per
  [0003](decisions/0003-scoped-urls.md); the `.git` suffix separates protocol from page.
- **Only the three known endpoints are routed** — `info/refs`, `git-upload-pack`,
  `git-receive-pack`. `http-backend` will otherwise serve dumb-protocol object files
  under any path handed to it, which would be a read of a repository nothing
  authorized. The router is the allowlist.
- **Authorization is decided before the subprocess is spawned**, from the service name
  in the request, not from anything `http-backend` reports back. By the time git is
  running it is too late to refuse.
- **Milestone 4 was split.** See [ROADMAP.md](ROADMAP.md#why-this-order).

### Open

- **What a private repository answers to an anonymous clone.** 4a has no credentials at
  all, so 404 is the only honest answer and matches `view_repo`'s "absent, not
  forbidden" rule. But git only sends credentials *after* a 401, so 4b will need a 401
  with `WWW-Authenticate` on exactly the case that 404s today — which leaks that the
  repository exists. Gitea and GitHub both accept that leak. Decide it in 4b, with the
  tension recorded rather than rediscovered.

### Watch for

- **`body_limit` will reject pushes and large fetches.** `topcoat-router` caps request
  bodies; the git routes need it raised. Expect a confusing failure rather than a clear
  one — noted since [0001](decisions/0001-git-over-http-not-ssh.md).
- **CGI header parsing sits in front of a stream.** `http-backend` writes headers, a
  blank line, then the body. Reading the headers must not buffer the body — that is the
  whole reason this transport was judged viable on `Body::into_data_stream`.
- **A subprocess per request**, unlike Milestone 3's once-per-creation. Fork/exec cost
  now sits on a hot path; measure before assuming it is fine.
- **`http-backend` reports failure through CGI status lines**, not exit codes alone. A
  non-zero exit and a `404 Not Found` on stdout mean different things.
- **The advertisement must not be cached.** `Cache-Control: no-cache` on `info/refs`, or
  clients fetch a stale ref list and fail to find commits that exist.

### Carried over — small, unblocked

- **An orphaned repo directory is possible** if the process dies between the record
  write and the filesystem write, and it then blocks re-creating that name. The durable
  fix is a reconciliation sweep on boot
  ([architecture.md](architecture.md#db-plus-filesystem-writes)); clearing one is a
  manual `rm` today, since repo deletion does not exist.
- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
  constraint, but surfaces as an opaque storage error rather than "name taken".
- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
  data directory ever moves to Linux.
- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
  today. Topcoat's `font-fontsource` feature handles it.
- **Light mode is untested.** The palette defines it; nobody has looked at it.
- **No rate limiting** on `/auth/login` or `/auth/setup`.
- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
  enforced on read, so this is tidiness, not a hole.
- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
  rather than hypothetical.

## Backlog

Ordered. Pull from the top.

1. **Milestone 4b — Push and tokens.** Personal access tokens over HTTP Basic, `git
   push`, private clone. Open decisions when it starts: how tokens are hashed (session
   token hashing already exists to copy), whether tokens carry scopes, and the 401-vs-404
   tension above.
2. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value
   objects** — `ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port
   returning `String`s is an anaemic pass-through that pushes validation into the page.
   Also the point to measure fork/exec cost per page view, and to reconsider `gix` for
   the read path ([0006](decisions/0006-git-binary-behind-narrow-ports.md)).
3. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
   whether writing or projects/showcases is the better first portfolio feature.

## Open questions

- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
  by its own authors). Expect churn that isn't feature work.
- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
  dropped purely to avoid an npm build step — see [ui.md](ui.md).

## Routing findings (Milestone 0)

- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
  silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
  stable is now 1.97.1.
- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
  so pages can live in any module. Layering is our choice, not the framework's.
- `module_router!` derives each URL from the module tree rather than a path string.
  Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
  handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a
  parameterised root segment still has to coexist with static ones. Worth checking how
  `module_router!` handles that before committing to it.
- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
  injected as handler arguments. Parses are memoized per request.
- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
  `NotFoundError` to render a branded 404.
- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
- `Body` is a boxed `http_body::Body` used for both requests and responses, with
  `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
  stream both directions without buffering. This is what makes Milestone 4 viable.
