| | @@ -6,28 +6,86 @@ |
| 6 | 6 | |
| 7 | 7 | ## Active: Milestone 5 — Repo browsing |
| 8 | 8 | |
| 9 | | −**Goal:** a repository's contents are readable on the web — tree, blob, commit log — so |
| 10 | | −a visitor can look at code without cloning it. The first milestone where the profile |
| 11 | | −starts 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 |
| 14 | | −guessed at the end of the previous one. Two things are already known and should shape |
| 15 | | −that 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 |
| 20 | 41 | [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. |
| 25 | 51 | |
| 26 | 52 | ### Open |
| 27 | 53 | |
| 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. |
| 28 | 72 | - **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. |
| 31 | 89 | |
| 32 | 90 | ### Carried over — small, unblocked |
| 33 | 91 | |