steid

@jamesgill /

docs: plan Milestone 5, with the fork/exec cost measured

0006 said to reconsider gix on the read path with a measurement rather than an
intuition, so the measurement is taken before the plan rather than after: every
git invocation costs ~11-12ms regardless of what it asks for, because the cost
is starting the process, not the query. That matches the ~13ms git init from
Milestone 3.

It changes the shape of the decision. A three-call page is ~35ms of pure
overhead, which is liveable; a per-file last-commit column at one call per entry
is ~230ms for twenty files, which is not. So that column becomes a decision the
milestone has to make rather than a detail it can drift into, and the choice
between keeping the binary, running long-lived batch processes, or adopting gix
is recorded with the number attached.

Two further decisions are left open rather than assumed: the ref-versus-path
ambiguity in a tree URL, which is unparseable in general because refs may
contain slashes, and whether writing should jump ahead of browsing given that
portfolio-first is the stated tie-break.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZwc7URWKVhkAuRTWiDmjA
JamesPatrickGill authored 8 days agoparenta814db5Browse files338102bfd13c17811540b4bfb545bcab0388c296

2 files changed+94 −17

plans/current.md+75 −17View file
@@ -6,28 +6,86 @@
66
77 ## Active: Milestone 5 — Repo browsing
88
9**Goal:** a repository's contents are readable on the web — tree, blob, commit log — so
10a visitor can look at code without cloning it. The first milestone where the profile
11starts to look like a portfolio rather than a list of names.
12
13**Not planned yet.** Steps get laid out at the start of the milestone rather than
14guessed at the end of the previous one. Two things are already known and should shape
15that plan:
16
17- **Start with domain value objects** — `ObjectId`, `RefName`, `TreeEntry` — before any
18 adapter. A query port returning `String`s is an anaemic pass-through that pushes
19 validation into the page, which is what
9+**Goal:** a repository's contents are readable on the web — the file tree at a ref, a
10+single file's contents, and the commit log. A visitor can look at code without cloning
11+it, which is the first time the profile behaves like a portfolio rather than a list of
12+names.
13+
14+**Out of scope:** editing files, diffs, blame, syntax highlighting, rendering a README as
15+markdown (that wants the markdown pipeline Milestone 6 brings), search, and a
16+last-commit-per-file column — see Open, where that one is a decision rather than an
17+omission.
18+
19+### Steps
20+
21+- [ ] Domain: `ObjectId`, `RefName`, `TreeEntry` — value objects **before** any adapter,
22+ per [0006](decisions/0006-git-binary-behind-narrow-ports.md)
23+- [ ] Application: `GitQuery` port — `resolve_ref`, `list_tree`, `read_blob`, `log`
24+- [ ] Infrastructure: adapter over the `git` binary, through the existing `git_command`
25+- [ ] Application: `browse_repo` read model, reusing `view_repo`'s visibility rule
26+- [ ] Web: the tree page, and the repository page showing its default branch
27+- [ ] Web: the blob page, and the commit log
28+- [ ] Verify in a browser against a real repository, including an empty one
29+
30+### Done when
31+
32+A visitor can open a public repository from a profile, see its files at the default
33+branch, click into a directory and then a file and read its contents, and open the commit
34+log. A private repository shows none of this to someone who may not see it. An empty
35+repository says so rather than erroring.
36+
37+### Settled
38+
39+- **Value objects come first.** A query port returning `String`s is an anaemic
40+ pass-through that pushes validation into the page, which
2041 [0006](decisions/0006-git-binary-behind-narrow-ports.md) rejected in advance.
21- **Measure fork/exec per page view first.** Milestone 3 paid it once per repository
22 creation and Milestone 4 once per git request; browsing would pay it several times per
23 page. This is the point where `gix` for the read path gets reconsidered, and 0006 says
24 to make that call with a measurement rather than an intuition.
42+
43+- **Fork/exec is ~11–12ms per call, and it is the process, not the query.** Measured on
44+ a 201-commit repository, averaged over 50 runs each: `rev-parse` 11.2ms, `ls-tree`
45+ 11.7ms, `cat-file` 11.6ms, `log -20` 11.8ms, `for-each-ref` 14.4ms. The work is
46+ free; starting git is not — which matches the ~13ms `git init --bare` measured in
47+ Milestone 3. **A three-call page therefore costs ~35ms of pure overhead.** That is the
48+ number the Open decision below turns on, and it is why a per-file last-commit column is
49+ a decision and not a detail: at one call per entry, a twenty-file directory is ~230ms
50+ before any real work.
2551
2652 ### Open
2753
54+- **What to do about the fork/exec cost.** Three ways, and the measurement above is the
55+ input:
56+ 1. **Keep the binary, design pages to make few calls.** Nothing new to learn, ~35ms a
57+ page, and it rules out any per-entry column. Simplest, and honest for a
58+ personal-scale forge.
59+ 2. **`git cat-file --batch`-style long-lived processes.** One process answering many
60+ queries over stdin, so the per-call cost mostly disappears without a second
61+ implementation of git. Costs a process lifecycle and a protocol to parse.
62+ 3. **`gix` for the read path**, which 0006 explicitly left open to revisit here.
63+ Microseconds instead of milliseconds, and the place `gix` is strongest — but it
64+ means two implementations holding assumptions about the same on-disk format, which
65+ is the exact thing 0006 rejected it for.
66+- **The ref-versus-path ambiguity in the URL.** `/{handle}/repos/{name}/tree/{ref}/{path}`
67+ is unparseable in general, because a ref may contain slashes: `tree/feature/x/README`
68+ splits two ways. GitHub resolves it by trying candidate splits against the real ref
69+ list; GitLab inserts a `/-/` separator. A third option is a single-segment ref with the
70+ path after it, refusing refs with slashes. This is a URL shape, so it is expensive to
71+ change later.
2872 - **Whether writing (Milestone 6) should come first.** The ladder puts browsing next, but
29 the vision is portfolio-first and writing is the more distinctive feature. Browsing is
30 the more expected one. Worth a moment's thought before starting rather than after.
73+ the vision is portfolio-first and writing is the more distinctive feature, where
74+ browsing is the more expected one. Worth deciding before starting rather than after.
75+
76+### Watch for
77+
78+- **An empty repository has no `HEAD`.** Steid creates repositories empty, and
79+ Milestone 4 made it easy to have one that was never pushed to. `rev-parse HEAD` fails
80+ rather than returning nothing, and the repository page must say "nothing here yet"
81+ instead of erroring.
82+- **A blob is not necessarily text.** Binary files, invalid UTF-8, and very large files
83+ all reach the same page. Decide what each does rather than discovering it.
84+- **Paths in URLs reach the filesystem indirectly**, via git rather than directly, but a
85+ path is still user input arriving at a subprocess argument. `--` before path arguments,
86+ as `init_bare` already does.
87+- **The commit log is unbounded.** A repository with 50,000 commits needs a limit before
88+ the page renders one.
3189
3290 ### Carried over — small, unblocked
3391
plans/progress.md+19 −0View file
@@ -332,6 +332,25 @@ from the list, and the page is 403 for anyone else.
332332 - **`in_memory.rs` keeps `mod tests` in the middle of the file**, like `sqlite.rs`.
333333 Appending an implementation to the end lands it inside a later impl block.
334334
335+#### Measured, for Milestone 5
336+
337+Taken at the end of 4b, on a 201-commit repository, 50 runs averaged per command:
338+
339+| Command | Per call |
340+|---|---|
341+| `git rev-parse HEAD` | 11.2 ms |
342+| `git ls-tree -l HEAD` | 11.7 ms |
343+| `git cat-file -p HEAD:` | 11.6 ms |
344+| `git log -20 --format=…` | 11.8 ms |
345+| `git for-each-ref` | 14.4 ms |
346+
347+**The cost is starting git, not the query** — every command lands in the same band
348+regardless of the work it does, matching the ~13ms `git init --bare` from Milestone 3.
349+A three-call page is therefore ~35ms of pure overhead, and a per-file last-commit column
350+at one call per entry would be ~230ms for twenty files. This is the input
351+[0006](decisions/0006-git-binary-behind-narrow-ports.md) asked for before reconsidering
352+`gix` on the read path.
353+
335354 ---
336355
337356 ## Reference: what attempt #2 proved