steid

@jamesgill /

docs: fold the branches and tags handover into plans

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGBYVHV86DDvJCBKsDFHT6
JamesPatrickGill authored 15 hours agoparent265e887Browse files9b1feb63a416acebab2dd4125250d9b9ee8ee0b5

4 files changed+36 −96

plans/current.md+9 −0View file
@@ -148,6 +148,15 @@ instance, and Steid's own source is pushed to it and browsable there.
148148 `white-space: pre`; noted rather than fixed.
149149 - **The blob header does not name the detected language.** It is only visible as colour.
150150
151+- **No ahead/behind counts on the branches page**, by decision: a `rev-list` per branch
152+ is twenty forks for twenty branches. Waits for a kept-alive git.
153+- **Neither refs page is paginated.** A thousand branches render a thousand rows.
154+- **A repository with tags but no branches** says "No branches"; real but exotic, not
155+ fixture-tested.
156+- **`web::browse::{encode, timestamp}` are now `pub(super)`**, and `repo_stats` takes
157+ `handle` and `name` so the sidebar counts can link. Watch for the next page that wants
158+ `timed_out` and move it to `web::context` then.
159+
151160 ### Carried over — small, unblocked
152161
153162 - **A client that disappears mid-request leaves the body-copy task waiting.** The copy
plans/handover-refs.md+0 −93
@@ -1,93 +0,0 @@
1# Handover — `refs` (branches and tags pages)
2
3Written for the orchestrator to fold into `progress.md`, `current.md` and `ui.md` at
4merge time. Branch `feat/refs-pages`.
5
6## What shipped
7
8`GET /{handle}/repos/{name}/branches` and `GET /{handle}/repos/{name}/tags`, plus the
9wiring that turns the counts beside the revision switcher and the About sidebar's
10Branches and Tags values into links to them.
11
12- `domain::{BranchRow, TagRow}` — a ref plus what one row of a page shows.
13- `GitQuery::branches()` / `GitQuery::tags()`, in `DiskGitQuery` and `InMemoryGitQuery`.
14 `list_refs` is untouched.
15- `application::browse::{list_branches, list_tags}` returning `RefPage<Row>`, and the
16 private `pin_default`.
17- `infrastructure::web::refs` — the two pages, `branches_url`, `tags_url`, `compare_url`.
18
19## Decisions worth not rediscovering (progress.md)
20
21- **`%(HEAD)` is why the page is one process.** `for-each-ref` marks the branch `HEAD`
22 names with `*`, so the default branch arrives in the same output as the branches
23 themselves. Nothing calls `symbolic-ref` or `default_branch` on the happy path, and
24 the compare link's base ref is read out of the pinned row.
25- **Empty fields must be kept when splitting `for-each-ref` output.** `parse_latest_tag`
26 can afford to filter empty fields because it reads two required ones; a multi-field
27 record cannot. A lightweight tag's `%(*objectname)` is empty, and filtering shifts its
28 date into the commit-id slot. `ref_fields` keeps them and drops only the trailing
29 remainder holding git's own record-ending newline. There is a test for exactly this.
30- **Ordering is git's, pinning is the use case's.** `--sort=-committerdate` /
31 `--sort=-creatordate` cost nothing extra in the process that was being run anyway,
32 and re-sorting in Rust would give the same answer. Pinning the default branch is a
33 display decision — it is the branch a visitor came for and it is *not* reliably the
34 most recently pushed — so it lives in `list_branches` as a stable partition, which is
35 what keeps the date order beneath it intact.
36- **A lightweight tag's `%(contents:subject)` is the commit's subject, not the tag's.**
37 It is only read when `%(objecttype)` says `tag`; otherwise the page would attribute
38 the commit's words to a tag that has none. `annotated` is its own field rather than
39 `message.is_some()`, because an annotated tag may carry an empty message.
40- **An annotated tag's `objectname` is the tag object.** The row reports the peeled
41 `%(*objectname)` so the sha it shows is something a visitor can browse.
42- **`RefPage.repo_is_empty` costs one extra process, only when the list is empty.**
43 "No tags yet" and "nothing pushed yet" are different sentences and only the second
44 wants the push snippet, and a tag list alone cannot tell them apart. `default_branch`
45 answers it, and is asked nowhere else on these pages.
46- **A timeout renders as a page state, not a 500.** `refs.rs::timed_out` matches
47 `Error::GitQuery(_)` with `is_timeout()`. This is the **first** place in the web layer
48 that distinguishes a timeout from a fault; every other page still maps both to
49 `server_error`. If a second page wants it, the helper should move to `web::context`.
50
51## Holes and shortcuts (current.md)
52
53- **No ahead/behind counts on the branches page**, deliberately and by brief. It is a
54 `rev-list` per branch — twenty branches would fork twenty extra processes to decorate
55 one page. It stays out until something keeps git alive between questions. Commented in
56 both `port.rs` and `refs.rs`.
57- **The Compare link points at a page this branch does not contain.** The contract is
58 `/{handle}/repos/{name}/compare/{base}...{head}`, each ref percent-encoded whole so a
59 slash in a name stays inside its segment (`main...feature%2Flogin`). It is rendered on
60 every non-default branch row and there are unit tests on the shape. **If the commits
61 agent's compare route differs, `compare_url` in `refs.rs` is the one place to change.**
62- **The tags page leaves the right-hand side short on purpose** for the archive agent's
63 two download links; nothing is rendered there.
64- **`web::browse::{encode, timestamp}` were widened to `pub(super)`.** No other change to
65 those functions.
66- **`repo_stats` in `repo.rs` gained `handle` and `name` parameters** so the sidebar's
67 counts can be links. One call site.
68- **A repository with commits but no branches** (tags pushed alone) says "No branches",
69 and its tags page still works. Real but exotic; not tested against a live fixture.
70- **Neither page is paginated.** A repository with a thousand branches renders a thousand
71 rows. One process still, but a large page.
72
73## UX notes (ui.md)
74
75- The entry-point table's "Branches, Tags → the counts beside the switcher become links"
76 row is **done**, and so is the note under *The repository page* saying the counts are
77 plain text because the pages do not exist. Both need updating.
78- **Both pages sit under the Code tab.** They are ways into the code rather than places
79 of their own; a tab each for two lists would make the strip advertise plumbing. The
80 cost is that the strip cannot say which of them you are on, which is why each page's
81 heading carries a cross-link to the other (`Tags →` / `Branches →`).
82- **The metadata columns are fixed-width and right-aligned** from `sm` up — a `w-24` date
83 and a `w-16` compare slot, the latter kept empty on the default branch's row. Without
84 them the sha zig-zags down the list, because relative dates vary in width and the one
85 row with no Compare link pulls its whole group right. This is worth keeping in mind for
86 any other list of rows with trailing metadata.
87- **Below `sm` the commit subject and the Compare link are dropped**, not wrapped. The row
88 is a scanning surface and a two-line row destroys the column of names that makes it
89 scannable; at 375px the reserved widths were what pushed the row off the screen.
90- Three empty states, deliberately distinct: an empty repository gets the landing page's
91 push snippet, a repository with history but no tags gets a one-line "No tags yet" with
92 `git push --tags` in mono, and a timeout gets its own panel saying the repository is
93 fine.
plans/progress.md+19 −0View file
@@ -37,6 +37,25 @@ TOML, Dockerfile, TypeScript — at +0.59 MiB on the binary. Decisions worth kee
3737 - **~87 ms per 1,255-line file in release**, uncached, paid on every view; debug is ~8×
3838 slower and not the number to plan against. If it matters: cache per blob object id.
3939
40+**Branches and tags pages (merged 2026-09-05).** `/branches` and `/tags`, one
41+`for-each-ref` each, and the counts beside the switcher and in the sidebar are links.
42+
43+- **`%(HEAD)` is why the page is one process**: `for-each-ref` marks the default branch,
44+ so nothing calls `symbolic-ref` on the happy path, and the compare link's base is read
45+ from the pinned row.
46+- **Keep empty fields when splitting `for-each-ref` output.** A lightweight tag's
47+ `%(*objectname)` is empty; filtering empties shifts its date into the id slot. There is
48+ a test for exactly this.
49+- **Ordering is git's (`--sort=-committerdate`), pinning is the use case's.** The default
50+ branch is a display decision, a stable partition in `list_branches`.
51+- **A lightweight tag's `%(contents:subject)` is the commit's subject**, so it is read only
52+ when `%(objecttype)` is `tag`. `annotated` is its own field because an annotated tag may
53+ carry an empty message. The row shows the peeled `%(*objectname)` so the sha is browsable.
54+- **"No tags yet" and "nothing pushed yet" are different sentences**; telling them apart
55+ costs one `default_branch` call, only when the list is empty.
56+- **`refs.rs::timed_out` is the first web-layer place that renders a timeout as a state**
57+ rather than a 500. When a second page wants it, move it to `web::context`.
58+
4059 ### Milestone 0 — Skeleton · done
4160
4261 Topcoat 0.5 app serving pages, `AppConfig` from `STEID_*` env, SQLite pool in app
plans/ui.md+8 −3View file
@@ -109,8 +109,13 @@ to say, pushing the code below the fold.
109109 - **The latest-commit bar stands in for the per-file last-commit column** that
110110 [0006](decisions/0006-git-binary-behind-narrow-ports.md) defers. It says the same
111111 thing once instead of once per row, for one `git log --max-count=1`.
112- **Counts beside the switcher are text, not links.** `/branches` and `/tags` do not
113 exist; a dead link is worse than a number.
112+- **Counts beside the switcher link to `/branches` and `/tags`.** Both pages sit under
113+ the Code tab — they are ways into the code, not places of their own, and a tab each
114+ for two lists would make the strip advertise plumbing. Each page's heading cross-links
115+ the other. Their rows keep trailing metadata in fixed-width right-aligned columns so
116+ the sha does not zig-zag, and below `sm` drop the subject and Compare link rather than
117+ wrap: a row is a scanning surface. Three distinct empty states — no commits (push
118+ snippet), no tags yet (`git push --tags`), and a timeout that says the repository is fine.
114119 - **Sidebar sections are separated by hairlines, never boxed as cards** — the rule the
115120 profile page settled on, and what makes the page read as one surface.
116121 - **Below `lg` the sidebar is not a sidebar: it comes first**, above the file list, with
@@ -139,7 +144,7 @@ lands in the frame rather than beside it:
139144 |---|---|
140145 | Issues, Pull requests | a tab each, beside Code and Commits |
141146 | A commit page | the sha in the latest-commit bar and in the log |
142| Branches, Tags | the counts beside the switcher become links |
147+| Branches, Tags | done — the counts beside the switcher and in the sidebar |
143148 | Compare | reached from a branch row on the branches page |
144149 | Blame | a toggle in the blob's own header, beside Raw |
145150 | Archive download | two small links under the clone URL |