steid

@jamesgill /

steid/plans/handover-archive.md
7.2 KBCode·Blame·Raw
1# Handover — `archive` (archive download + code search)
2
3Branch `feat/archive-search`. Two commits, both compiling, tests green at each.
4Fold this into `progress.md` (decisions), `current.md` (holes) and `ui.md` (UX) at
5merge time.
6
7## What shipped
8
9- `GET /{handle}/repos/{name}/archive/{rev}.tar.gz` and `.zip` — streamed `git archive`.
10- `GET /{handle}/repos/{name}/search?q=&rev=` — fixed-string `git grep` over one revision.
11- Entry points: two links under the clone URL in the About sidebar; a search box on the
12 right of the landing page's toolbar row. Both are the slots `ui.md`'s table reserved.
13- The blob's line-number cells now carry `id="L{n}"`.
14
15## progress.md material — decisions worth not rediscovering
16
17- **`GitArchive` is a fourth narrow port, not a `GitQuery` method.** `GitQuery`'s whole
18 contract is *capped bytes, collected*; an archive is as large as the repository. It
19 therefore lives in `infrastructure/git.rs` beside the protocol server, whose shape it
20 shares, and reuses `web::git::GitBody` verbatim — one subprocess's stdout becoming a
21 response body is now written once and used twice.
22- **The archive use case is two steps and that is what makes the `ETag` cheap.**
23 `archive_repo` authorizes and resolves, handing back an `ArchiveTarget` that nothing
24 else can construct; `open_archive` consumes it and runs git. A conditional request is
25 answered from step one, so a repeat download of a tag costs one `cat-file`, not a
26 pack. The `ETag` is the resolved commit, so `main` correctly re-downloads after a push
27 and a tag never does.
28- **`git grep` is the one command in `git_query.rs` whose exit status is an answer.** It
29 exits 1 for "matched nothing". `run_within_allowing` takes an allowed-exit list and
30 `NO_MATCHES` names the exception at the call site, so the module's rule — a non-zero
31 exit is always a fault — is broken deliberately in one visible place rather than
32 softened everywhere.
33- **`git grep -z` is not optional.** Without it the field separator is a colon, and a
34 path may contain colons, so `src/a:b.rs:12:3:text` has no unambiguous reading. With
35 `-z` the record is `<commit>:<path> NUL <line> NUL <column> NUL <text> LF`, and the
36 parser walks NULs rather than splitting lines — which is also what survives a newline
37 inside a *path*.
38- **`RepoPath` refuses a colon, so a file named `a:b.rs` is unsearchable as well as
39 unbrowsable.** The parser drops those hits rather than linking to a page that must
40 404. Pre-existing constraint, newly visible; there is a test pinning it.
41- **A timeout is a page, not a 500.** `search_repo` catches `GitQueryError::is_timeout`
42 and returns `Searched::TimedOut`, which is the first real consumer of the timeout kind
43 the previous commit added.
44- **Archive and search both 404 a private repository for a stranger**, matching every
45 other read page. See the open question below — the brief asked for 401.
46- **A subtlety in the dev loop, not in the code:** `topcoat dev` in this worktree exited
47 rather than restarting after two of the three source edits made while the server was
48 running, and had to be relaunched by hand. Nothing in the log explained it. Worth
49 watching; may be two `topcoat dev` instances sharing a `target/`.
50
51## current.md material — holes opened, shortcuts taken
52
53- **`git grep` output is collected whole before it is capped.** `run` reads the
54 subprocess's stdout into memory, so a one-letter query against a large repository
55 allocates all of git's output even though only 200 hits are kept. `git grep` has no
56 portable global maximum; `-m` is per file. Bounded in practice by the 20-second
57 timeout, not by anything deliberate.
58- **The search box searches the revision being browsed but the results page does not
59 offer a revision switcher.** Changing revision from the results means editing the URL.
60 The switcher lives in `browse.rs` and is private; exposing it is a one-word change
61 when someone wants it.
62- **A query is `trim()`ed**, so a search for a trailing space is a search without it.
63 Conventional, and wrong for anyone hunting trailing whitespace.
64- **Matched lines are cut at 500 characters**, and the cut can remove the match itself
65 on a minified line. The alternative was letting one bundle make the page megabytes.
66- **Highlighting re-scans the line for every occurrence** and ignores the column git
67 reported, so `GrepHit::column` is carried but unused by the page.
68- **No paging on search**, the same v1 decision as the log's 50.
69- **The archive endpoint has no rate limiting.** It is the most expensive anonymous
70 request in Steid — a full pack of the repository per hit, with a 20-second-plus
71 ceiling nothing enforces because `git archive` does not go through `GitQuery::run`.
72 Cheap to abuse, and the first thing to look at if an instance is ever put under load.
73- **`git archive` is not covered by the read timeout.** Deliberately: a legitimately
74 large repository takes longer than twenty seconds to pack, and killing that is worse
75 than the request being slow. It is bounded only by `kill_on_drop` when the client
76 goes away.
77- **A late `git archive` failure is a truncated download**, not an error — the status is
78 known only after headers have been sent. Logged, same as the protocol server's.
79
80## ui.md material
81
82- **Archive download** and **Code search** in "Where the next features go" are both
83 filled in; the table entries can be marked done.
84- **The download links are for the revision being viewed**, not always the default
85 branch — someone reading a tag wants that tag's tarball. An empty repository shows no
86 links, because there is nothing to pack.
87- **The search box is the width of the About sidebar** on `sm` and up and full width
88 below it, so the two columns of the landing page line up. On the search page itself it
89 is full width.
90- **`<mark>` is `bg-primary/25`**, which is the only place other than the active tab and
91 the latest-commit dot where the primary colour appears on a repository page. It marks
92 the thing being looked for, which is the same rule.
93- **Every search state is a bordered panel with one sentence**: no commits, no query, no
94 matches, query too long, timed out. They read as the same object because they are.
95- **A long matched line scrolls inside its own `<code>`**, verified at 800px — the page
96 itself does not scroll horizontally.
97
98## Verified
99
100- `cargo test` — 503 pass. `cargo clippy --all-targets` — zero warnings. `cargo fmt`.
101- Against a real instance on :3103, this repository pushed into it with 7 branches and
102 two annotated tags: both archives downloaded and **extracted** (`tar tzf`, `unzip`)
103 with the `steid-main/` and `steid-v0.2.0/` prefix directories; `If-None-Match` on the
104 returned `ETag` answered 304; unknown revision, unknown extension, and a private
105 repository as an anonymous visitor all answered 404.
106- Search verified live for results, no matches, no query, truncation at 200, an
107 over-long query, a private repository (404) and an unknown revision (404).
108- Both colour modes looked at by flipping the layout's `class="dark"`, restored after.
109- **Not verified:** the timeout state in a browser — it is unit-tested through the
110 in-memory port, but no repository here is slow enough to trigger a real 20-second
111 grep. The `Searched::TimedOut` page has never been rendered.