# Handover — `archive` (archive download + code search)

Branch `feat/archive-search`. Two commits, both compiling, tests green at each.
Fold this into `progress.md` (decisions), `current.md` (holes) and `ui.md` (UX) at
merge time.

## What shipped

- `GET /{handle}/repos/{name}/archive/{rev}.tar.gz` and `.zip` — streamed `git archive`.
- `GET /{handle}/repos/{name}/search?q=&rev=` — fixed-string `git grep` over one revision.
- Entry points: two links under the clone URL in the About sidebar; a search box on the
  right of the landing page's toolbar row. Both are the slots `ui.md`'s table reserved.
- The blob's line-number cells now carry `id="L{n}"`.

## progress.md material — decisions worth not rediscovering

- **`GitArchive` is a fourth narrow port, not a `GitQuery` method.** `GitQuery`'s whole
  contract is *capped bytes, collected*; an archive is as large as the repository. It
  therefore lives in `infrastructure/git.rs` beside the protocol server, whose shape it
  shares, and reuses `web::git::GitBody` verbatim — one subprocess's stdout becoming a
  response body is now written once and used twice.
- **The archive use case is two steps and that is what makes the `ETag` cheap.**
  `archive_repo` authorizes and resolves, handing back an `ArchiveTarget` that nothing
  else can construct; `open_archive` consumes it and runs git. A conditional request is
  answered from step one, so a repeat download of a tag costs one `cat-file`, not a
  pack. The `ETag` is the resolved commit, so `main` correctly re-downloads after a push
  and a tag never does.
- **`git grep` is the one command in `git_query.rs` whose exit status is an answer.** It
  exits 1 for "matched nothing". `run_within_allowing` takes an allowed-exit list and
  `NO_MATCHES` names the exception at the call site, so the module's rule — a non-zero
  exit is always a fault — is broken deliberately in one visible place rather than
  softened everywhere.
- **`git grep -z` is not optional.** Without it the field separator is a colon, and a
  path may contain colons, so `src/a:b.rs:12:3:text` has no unambiguous reading. With
  `-z` the record is `<commit>:<path> NUL <line> NUL <column> NUL <text> LF`, and the
  parser walks NULs rather than splitting lines — which is also what survives a newline
  inside a *path*.
- **`RepoPath` refuses a colon, so a file named `a:b.rs` is unsearchable as well as
  unbrowsable.** The parser drops those hits rather than linking to a page that must
  404. Pre-existing constraint, newly visible; there is a test pinning it.
- **A timeout is a page, not a 500.** `search_repo` catches `GitQueryError::is_timeout`
  and returns `Searched::TimedOut`, which is the first real consumer of the timeout kind
  the previous commit added.
- **Archive and search both 404 a private repository for a stranger**, matching every
  other read page. See the open question below — the brief asked for 401.
- **A subtlety in the dev loop, not in the code:** `topcoat dev` in this worktree exited
  rather than restarting after two of the three source edits made while the server was
  running, and had to be relaunched by hand. Nothing in the log explained it. Worth
  watching; may be two `topcoat dev` instances sharing a `target/`.

## current.md material — holes opened, shortcuts taken

- **`git grep` output is collected whole before it is capped.** `run` reads the
  subprocess's stdout into memory, so a one-letter query against a large repository
  allocates all of git's output even though only 200 hits are kept. `git grep` has no
  portable global maximum; `-m` is per file. Bounded in practice by the 20-second
  timeout, not by anything deliberate.
- **The search box searches the revision being browsed but the results page does not
  offer a revision switcher.** Changing revision from the results means editing the URL.
  The switcher lives in `browse.rs` and is private; exposing it is a one-word change
  when someone wants it.
- **A query is `trim()`ed**, so a search for a trailing space is a search without it.
  Conventional, and wrong for anyone hunting trailing whitespace.
- **Matched lines are cut at 500 characters**, and the cut can remove the match itself
  on a minified line. The alternative was letting one bundle make the page megabytes.
- **Highlighting re-scans the line for every occurrence** and ignores the column git
  reported, so `GrepHit::column` is carried but unused by the page.
- **No paging on search**, the same v1 decision as the log's 50.
- **The archive endpoint has no rate limiting.** It is the most expensive anonymous
  request in Steid — a full pack of the repository per hit, with a 20-second-plus
  ceiling nothing enforces because `git archive` does not go through `GitQuery::run`.
  Cheap to abuse, and the first thing to look at if an instance is ever put under load.
- **`git archive` is not covered by the read timeout.** Deliberately: a legitimately
  large repository takes longer than twenty seconds to pack, and killing that is worse
  than the request being slow. It is bounded only by `kill_on_drop` when the client
  goes away.
- **A late `git archive` failure is a truncated download**, not an error — the status is
  known only after headers have been sent. Logged, same as the protocol server's.

## ui.md material

- **Archive download** and **Code search** in "Where the next features go" are both
  filled in; the table entries can be marked done.
- **The download links are for the revision being viewed**, not always the default
  branch — someone reading a tag wants that tag's tarball. An empty repository shows no
  links, because there is nothing to pack.
- **The search box is the width of the About sidebar** on `sm` and up and full width
  below it, so the two columns of the landing page line up. On the search page itself it
  is full width.
- **`<mark>` is `bg-primary/25`**, which is the only place other than the active tab and
  the latest-commit dot where the primary colour appears on a repository page. It marks
  the thing being looked for, which is the same rule.
- **Every search state is a bordered panel with one sentence**: no commits, no query, no
  matches, query too long, timed out. They read as the same object because they are.
- **A long matched line scrolls inside its own `<code>`**, verified at 800px — the page
  itself does not scroll horizontally.

## Verified

- `cargo test` — 503 pass. `cargo clippy --all-targets` — zero warnings. `cargo fmt`.
- Against a real instance on :3103, this repository pushed into it with 7 branches and
  two annotated tags: both archives downloaded and **extracted** (`tar tzf`, `unzip`)
  with the `steid-main/` and `steid-v0.2.0/` prefix directories; `If-None-Match` on the
  returned `ETag` answered 304; unknown revision, unknown extension, and a private
  repository as an anonymous visitor all answered 404.
- Search verified live for results, no matches, no query, truncation at 200, an
  over-long query, a private repository (404) and an unknown revision (404).
- Both colour modes looked at by flipping the layout's `class="dark"`, restored after.
- **Not verified:** the timeout state in a browser — it is unit-tested through the
  in-memory port, but no repository here is slow enough to trigger a real 20-second
  grep. The `Searched::TimedOut` page has never been rendered.
