gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mremain/README.md

37.0 KB

  1. # gitoria.worldapi.org
  2. Git hosting for all projects, written in **Hybriel** (hl:web), login via **ident**. Source of truth: `CONCEPT.md`.
  3. Built so far: repos with their own address (#6), the repo homepage (#8), code browsing (#9), tickets (#10), pull requests (#11) with a Merge button for the owner (#17), releases (#12),
  4. every repo view as its own server-rendered page (#16), push and pull over HTTPS with access tokens and over SSH with keys (#7).
  5. ## Hybriel (vendored)
  6. * `bin/hybriel` + `plugins/` (core crypto data fetch fs http http1 mpackdb proc time web) = hybriel **master 06617221** (2026-10-03,
  7. antcolony mission 074: plugin allocators 3a781359 + 413f60e4 (#126, plugins allocate with malloc via plugin_api.zig), mpackdb
  8. frees per-operation buffers 2cb7ae5e, http1 request owns its parse 773de63e, event order f0ac2d2d (plugin ABI field — bin and
  9. .so must match); no lambda/parameter semantics change. Built from a read-only `git archive 06617221` into `~/scratch-074/src`
  10. (removed), sha256 `21059cc7…d77bdb`; old copy `.scratch/pre-074/` = 190aa11d. Gates 200/0, 46/0, 44/0. Memory proof:
  11. `bash .scratch/w074/curlloop.sh <appDir> / 20 <label>` (tracker README), `node .scratch/w074/memtest.mjs <appDir>
  12. .scratch/w074/real/storage <label> 0mugibaf6fds` (Chrome, port 8760, Chrome 8761–8764) and `bash .scratch/w074/longcurl.sh
  13. <appDir> <label> <N>` (signed-in curl over the 10 memtest pages, RSS every 100) — all need a fresh real-data tar in
  14. `.scratch/w074/real/storage` (deleted after the run).
  15. Before: master 190aa11d (2026-10-02,
  16. antcolony mission 072: #126 GC by bytes + returned closure scopes, #48 lambda params copy (`&p` = reference; audit: nothing in
  17. gitoria needs `&`), #127 big SSR pages flat incl. mountKids by reference (04df4428), fc838894 GC correctness (string index /
  18. plugin error read freed memory); built from a read-only `git archive 190aa11d` into `~/scratch-072/src` (removed), sha256
  19. `860f5e61…0a23626`; old copy `.scratch/pre-072/` = ff51cf46). Tracker README `/` (2.3 MB HTML): 0.56 s per load, RSS flat
  20. ~320 MB over 20 loads (old 9 → 16 s, 4.7–6.2 GB). Memory proof: `node .scratch/w072/memtest.mjs <appDir> <storage copy> <label>
  21. <userId>` (port 8720, Chrome 8721–8724; env MEM_ONLY/MEM_SKIP/LOADS/CLONES), `bash .scratch/w072/curlloop.sh <appDir> <path> N
  22. <label>` (needs a real-data copy in `.scratch/w072/real/storage`, deleted after each run — re-tar it).
  23. Before: master ff51cf46 (2026-10-01, antcolony mission 048: #113 mpackdb unique generated ids, #115/#116 lambda members on faces, #118, client SVG namespace, #111 hl:markdown
  24. (not vendored — gitoria has its own lib/markdown.hl), session sync 0b17f65b/64527baa — see "Lesson" below; built the same way into
  25. `~/scratch-048-gitoria/src`, sha256 `f3b93a53…983588d3`; old copy `.scratch/pre-048/` = 317d4754 + the pre-048 tests/).
  26. Before: master 317d4754 (2026-09-26, mission 038; includes 7cb9f8fc = #107: an emit in flight when its socket closes is carried over, a page being left starts nothing
  27. — fixes the Firefox pull-back that rolled antcolony mission 037 back; also #103/#104, #105 `headers`, #106 `let` per loop pass, #94 files in
  28. emit, #82 reconnect; before: #83 hashed `/__hl/…?v=` URLs + immutable cache, #88–#91 Bytes / chunked bodies / base64 / run stdin,
  29. #95–#97, #100, #101, #45 hl:web, #80 `run()`, #81 `*path`, #44 `sessionDomain`), built from a read-only
  30. `git archive master` (never inside Anton's repo) into `~/scratch-038/src`: `/media/STORAGE/projects/hybriel/native/zig-toolchain/zig
  31. build -Doptimize=ReleaseFast -Dtarget=x86_64-linux-gnu.2.39` in `native/`, binary `native/zig-out/bin/hybriel`, sha256 `fc7481fb…48670a8`.
  32. Older copies: `.scratch/pre-038/` (13ef4f9b bin/ + plugins/ + the pre-038 `browser.mjs`), `.scratch/pre-037/`, `.scratch/pre-035/`, `.scratch/pre-033*/`.
  33. * **No local patch** (`grep -rn "LOCAL PATCH" plugins` finds nothing): a re-vendor is copy binary + plugins, run ALL THREE gates
  34. (browser.mjs, push.mjs, ssh.mjs — deploy.sh only runs browser.mjs).
  35. * **069 round 2: master 1a096ad3 (#127) NOT adopted either**: the Markdown component still grows on big READMEs. Repro:
  36. `.scratch/w069/repro` (big-md variant, `run.sh <bin> <plugins> N`), see STATUS.
  37. * **069 (2026-10-02), master 8efba065 NOT adopted**: the gates were green, but big server-rendered READMEs grow memory and get
  38. slower on every load (tracker README 12 s → 47 s). See STATUS. Memory proof: `node .scratch/w069/memtest.mjs <appDir> <storage copy>
  39. <label> <userId>`. It uses port 8760 and Chrome 8761–8764, does 200 signed-in loads and 20 clones, and prints RSS as JSON.
  40. Lambda audit: `python3 .scratch/w069/lambdas.py` / `lambdas2.py`.
  41. * **Lesson (048, hybriel 64527baa)**: a face that takes `session` answers with a `sync` of every component member derived from
  42. `session` through server code (`tokens`, `keyItems`, `me`, `codeData` …) — the member is ALREADY fresh when the handler goes
  43. on after `emit server`. A handler that adds the returned row must skip a row with the same id (components/tokens.hl, sshkeys.hl,
  44. tickets.hl `addTicket` do), or the row is listed twice.
  45. The session cookie's `Domain` is the manifest setting `sessionDomain` (project.hl).
  46. ## Run (dev, Loreana)
  47. ```bash
  48. cd /media/STORAGE/projects/gitoria.worldapi.org
  49. GITORIA_PORT=8360 GITORIA_PUBLIC_URL=http://localhost:8360 ./bin/hybriel project.hl
  50. ```
  51. Config (environment, or a `.env` beside `project.hl` — never printed or committed):
  52. | Variable | Default | |
  53. |---|---|---|
  54. | `GITORIA_PUBLIC_URL` | `https://gitoria.worldapi.org` | the main address; a repo lives at `<scheme>://<slug>.<host[:port] of this>` |
  55. | `GITORIA_PORT` | 8360 | |
  56. | `HL_HOST` | 0.0.0.0 | `127.0.0.1` on Byrodin behind nginx |
  57. | `GITORIA_WATCH` | on | `0` = no dev watcher (the container) |
  58. | `GITORIA_STORAGE` | `./storage/mpackdb` | table directory (`repos.db`, `users.db`) |
  59. | `GITORIA_GIT` | `<launch dir>/storage/git` | the bare git repositories, `<slug>.git` each (absolute path; the `git` binary must be installed) |
  60. | `GITORIA_TICKETS_URL` | `https://tickets.worldapi.org` | where a repo's tickets live (see "Tickets") |
  61. | `GITORIA_SESSIONS` | `.sessions/` | |
  62. | `GITORIA_COOKIE_DOMAIN` | `.<host of the public url>` | the session cookie's Domain (`-` = host-only; a host without a dot or an IP gets host-only) |
  63. | `IDENT_URL`, `IDENT_EXCHANGE_URL`, `IDENT_API_KEY`, `IDENT_API_SECRET` | as in tickets | login via ident |
  64. ## Files
  65. Code order (antcolony `docs/code-order.md`, mission 002): `project.hl` is the MAP (config, routes, who hears what, an index
  66. comment); the logic is in `lib/`, one file per topic with its central logic, the noise in a `-helpers` file; API routes,
  67. faces and pages are thin wrappers. `let` only where a variable is reassigned (or re-bound in a loop body).
  68. | file | what |
  69. |---|---|
  70. | `project.hl` | the map: PWA settings, routes, audience, session cookie, the server |
  71. | `lib/repos.hl` | the repos table and every write to it: create, rows, owner checks (`ownsRepo`, `mayPush` = the push rule of HTTPS and SSH), main-branch setting, tickets connection |
  72. | `lib/repos-helpers.hl` | slug rules (`slugError`, `reserved`), the row a page shows, `withChange`, tab titles, the Markdown read view |
  73. | `lib/users.hl` | users table, ident exchange (`exchangeCode`, `userOfLoginCode`), display names, session → user, `tagOf` |
  74. | `lib/git.hl` | git on the server: `git()` / `gitRaw()`, branches, default branch, `initRepo`, `isEmptyNow`, `tmpDir` |
  75. | `lib/git-helpers.hl` | path/name/id checks, tab fields, sizes, the `\|\|\|PR` / `\|\|\|RL` subject parsers |
  76. | `lib/homepage.hl` | the repo homepage (README.md / `$docs`) |
  77. | `lib/code.hl` | the code browser (`/code`, `/branch`, `/commit`) and "Make main" (`makeMain`) |
  78. | `lib/pulls.hl` | pull requests, the Merge (`mergePullNow`), the list after a merge (`pullsView`) |
  79. | `lib/releases.hl` | releases |
  80. | `lib/tickets.hl` | a repo's tickets (read, open, comment, state), the connect flow (`finishConnect`), `notifyPush` |
  81. | `lib/tickets-helpers.hl` | `GITORIA_TICKETS_URL`, answer shapes, `#12` references (`refsOf`, `refParts`, `fixedBy`) |
  82. | `lib/transport.hl` / `lib/transport-helpers.hl` | git over HTTPS (`gitTransport`, `callGit`) / Basic password, plain answers, protocol env, pkt-line |
  83. | `lib/tokens.hl`, `lib/sshkeys.hl` | access tokens; ssh keys (+ `keyLine` for the sshd container, `sshUrl`) |
  84. | `lib/api.hl` / `lib/api-helpers.hl` | the function routes (`/api/repos…`, `/login/callback`, `/settings/connect…`, `/__git/keys`, `/__git/access`) / JSON-Markdown-text answers, `goTo`, `sshGuard`, `safeNext` |
  85. | `lib/markdown.hl` | Markdown → blocks (copied from tickets) |
  86. | `lib/util.hl` | env, storage dir, addresses (`publicUrl`, `slugOfHost`, `domainFor`), lists, `newestFirst`, text checks, Vienna time |
  87. | `components/*.hl` | pages and parts (shell `main.hl`, `list.hl`, `readme.hl`, `code/branch/commit.hl` + `codebrowser.hl`, `pulls`, `releases`, `tickets`, `settings`, `tokens`, `sshkeys`, `repohead`, `markdown`); CSS `components/styles.hl` |
  88. | `tools/migrate-short-ids.hl` | one-off (mission 039) |
  89. | `tests/` | gates `browser.mjs`, `push.mjs`, `ssh.mjs`; `realdata-baseline.mjs` + `realdata-compare.py` (same output on live data); `letcount.py` |
  90. Imports go one way (Hybriel refuses a cycle): util, git-helpers, markdown ← users ← repos-helpers ← git ← repos ← tickets-helpers ←
  91. homepage / code / pulls / releases ← tickets ← tokens / sshkeys ← transport ← api-helpers ← api ← project.hl.
  92. ## How a repo gets its address
  93. * A repo is one row in `storage/mpackdb/repos.db` (`@id` key, unique index on `slug`): `{ slug, description, owner, created }`.
  94. * **Slug**: unique in the whole system (one table, unique index); 2–40 characters `a–z 0–9 -`, starts and ends with a
  95. letter/digit, no `--`, not one of the reserved names (`lib/repos-helpers.hl` `reserved`: technical host names only — www, api, git, mail, …; app names like ident or tickets are allowed).
  96. * **Address** = `<slug>.gitoria.worldapi.org`. nginx sends `gitoria.worldapi.org` and `*.gitoria.worldapi.org` to this one
  97. app (wildcard vhost, wildcard DNS and certificate: the architect's).
  98. * **Pages (gitoria#16)**: every page component declares `host = null` and hl:web hands it the request's host without port
  99. (hybriel#74) — on the server, on the first load and on every hl:web navigation. `lib/util.hl slugOfHost` → '' (main address)
  100. or the slug. Routes (`project.hl`): `/` → `components/index.hl` (main address: `list.hl`, the repo list + "Create a repository";
  101. repo address: `readme.hl`), `/code` `/code/*` → `code.hl`, `/branch/*` → `branch.hl`, `/commit/*` → `commit.hl` (all three
  102. show `codebrowser.hl`), `/pulls` → `pulls.hl`, `/releases` → `releases.hl`, `/tickets` → `tickets.hl`, `/login/failed` →
  103. `loginfailed.hl`. Each repo page starts with `repohead.hl` (name, description, nav; unknown address → "No such repository").
  104. Links inside a repo are hl:web navigations (no page load). Rendered on the server with their content: the repo list,
  105. the repo head, the Readme's address/owner/created, the Tickets list (hl:fetch answers at once).
  106. * **Git on the server**: git is read with hl:proc `run()`, which waits for the program (hybriel#80), so the README, the file
  107. list / file, branches, commits, pulls and releases are in the first HTML and in every hl:web navigation (`lib/homepage.hl`
  108. `homepageNow`, `lib/code.hl browseNow`, `lib/pulls.hl pullsNow`, `lib/releases.hl releasesNow`). No browser script is involved; `/host.js` is gone.
  109. * **The path of `/code/*path`, `/branch/*path`, `/commit/*path`**: hl:web binds the rest of the URL to the page member `path`
  110. (hybriel#81); the pages hand it to `codebrowser.hl`.
  111. * **Login**: ident's login *button* flow (`<ident>/login?key=&return=<main>/login/callback`), the code exchanged server side.
  112. * **Identity selector** (gitoria#14, as in tickets): ident's `<ident-selector>` sits beside the button in the header; choosing an identity
  113. hands its one-time code (`/login.js` → hidden `#identcode` → face `gitoriaLogin`) to the server for the same exchange — no reload, open
  114. tabs of the session follow. ident answers only a registered origin, so the selector shows on the main address only (the shell gives it
  115. the class `onrepo` from the request's host, `components/styles.hl` hides it); the button stays the way in there.
  116. The app is registered in ident with ONE origin, the main address. The session cookie carries `Domain=.gitoria.worldapi.org`
  117. (hl:web `sessionDomain`, hybriel#44), so the login holds on every repo address. A login started at
  118. `<slug>.…` returns through the main address and on to that repo (`?next=`, added by `/login.js` at the click: a path, or a full URL of `<valid-slug>.<host>`;
  119. `lib/api-helpers.hl` `safeNext`). The first login asks for a display name (shown as the repo's owner). A failed login redirects to
  120. `/login/failed` (the reason parked in `session.data.loginError`; since mission 002 also in a browser that already had a session — `&req`, see STATUS "Lessons").
  121. * **Short ids (ident#23, antcolony mission 039)**: `users.identity` holds what ident's exchange answers — since ident#23 the identity's
  122. public 5-character short id (`a68sz`), before that the per-app id (32 hex); `lib/users.hl isIdentId` accepts both (the old
  123. `isHex` check refused short ids). The switch: one-off `tools/migrate-short-ids.hl` (old → short id, idempotent, never
  124. `finish`), gate `node tests/short-id-switch.mjs` (ports 8724/8725, no browser), runbook
  125. `antcolony-docs/docs/short-id-switch.md` (Byrodin: `/CONTAINERS/projects/antcolony/docs/short-id-switch.md` once synced).
  126. * **Create** (web form, face `gitoriaCreate`): any logged-in user with a display name. New repos reach every open list live.
  127. * **API** (public reads): `GET /api/repos`, `GET /api/repos/:slug`; `Accept: text/markdown` gives the Markdown read view.
  128. Creating is not in the API yet (needs API tokens — as in tickets `users.hl`).
  129. * Creating a repo also runs `git init --bare -b main` in `<GITORIA_GIT>/<slug>.git` (`lib/git.hl initRepo`); pushing to it: see "Push and pull".
  130. ## Push and pull (gitoria#7)
  131. Git over **HTTPS**, answered by this app itself with the `git` binary (`lib/transport.hl`; design: `docs/git-backend.md`). SSH: see below.
  132. * **Clone URL**: `https://<slug>.gitoria.worldapi.org/<slug>.git` (on the repo address, so the clone's folder is named like the repo). Paths
  133. `/<slug>.git/info/refs?service=…`, `/<slug>.git/git-upload-pack`, `/<slug>.git/git-receive-pack` (function routes, after the page routes in `project.hl`).
  134. * **Read** (clone, fetch, pull) needs no login: every repo is public. **Write** (push) needs the **access token of the repo's owner** as the
  135. password (any user name); anybody else's token → 403, no/unknown token → 401 with the way to get one. The token check is in `gitTransport`,
  136. before git is started.
  137. * **Access tokens** (`lib/tokens.hl`, `components/tokens.hl`, section `#tokens` of the main address for a logged-in user): name → token `gtr_` + 40 hex, shown ONCE;
  138. only its sha256 is stored (`storage/mpackdb/tokens.db`); list, remove (works at once); at most 20 per user. Faces `gitoriaMakeToken`, `gitoriaRemoveToken`.
  139. * **The "Add code to this repository" box** (`components/repohead.hl`, gitoria#20): plain text (no toggle), only on the
  140. **Code page** of a repo that has no branch yet (`lib/git.hl isEmptyNow`) — never on Readme / Pull requests / Releases /
  141. Tickets / Settings, and never once there is a commit. The clone command, the commands for a new project and for an
  142. existing one, and where the token comes from.
  143. * **How the body gets to git**: hl:proc `run()` has no stdin, so the request body is written to `<GITORIA_GIT>/.tmp/<random>.in` (a String holds raw bytes;
  144. hl:fs writes them exactly) and `sh -c 'git upload-pack|receive-pack --stateless-rpc "$1" < "$2" > "$3"'` (paths as arguments, no user text in the script)
  145. writes the answer to `.out`, which is read back as the response; both files are removed. A gzip request is unpacked first. `Git-Protocol: version=…`
  146. → `GIT_PROTOCOL` (v0, v1 and v2 tested). No hook: pulls and releases are read from the commits.
  147. * **Behind nginx** (the architect's vhost): `client_max_body_size` must allow pushes (say `500m`) and `proxy_request_buffering` stays **on** (default).
  148. git sends a big push chunked; hl:http1 reads chunked request bodies since hybriel#89 (antcolony mission 035: the old 411 hint is gone, a direct
  149. client without proxy pushes too — gate-checked). `proxy_read_timeout` ≥ 300s. The whole body is held in memory (≤ 500 MB).
  150. Kept on purpose (035): the temp files + `sh -c` (hl:proc `run()` could now take `stdin` + `binary`, hybriel#88/#91, but stdin
  151. crosses the plugin ABI as hex = twice the memory for a ≤ 500 MB body, and gzip would still need a second program) and `base64 -d`
  152. for the Basic header (hl:crypto `fromBase64(...).toString()` aborts the request on bytes that are not UTF-8).
  153. * Test: `node tests/push.mjs` (own ident + server + Chrome + the real git client; a small proxy in the gate plays nginx).
  154. Other ports: `GITORIA_GATE_PORT=8750 GITORIA_GATE_IDENT_PORT=8751 GITORIA_GATE_PROXY=8752 GITORIA_GATE_CHROME=8753-8757` (048: 46/0).
  155. ## Git over SSH (gitoria#7)
  156. A small **sshd container** (`docker/sshd`, service `gitoria-sshd` in `docker-compose.yml`) whose only job is `git-upload-pack` / `git-receive-pack`. It keeps no user list:
  157. * **Keys** (`lib/sshkeys.hl`, `components/sshkeys.hl`, section `#sshkeys` of the main address for a logged-in user): paste a PUBLIC key + a name; checked with `ssh-keygen -l`
  158. (ed25519, ecdsa, sk-…, RSA ≥ 2048; a private key, DSA, junk, a second copy of a key are refused); list with fingerprint; remove; ≤ 20 per user. Table `storage/mpackdb/sshkeys.db`.
  159. Faces `gitoria{Add,Remove}Key`. A key works at once and stops at once (sshd asks again on every login).
  160. * **Login**: `AuthorizedKeysCommand` (`gitoria-keys`) → `GET /__git/keys?type&key` (`lib/api.hl gitKeys` → `lib/sshkeys.hl keyLine`) → `restrict,command="gitoria-shell <user id>" <key>`. The forced command
  161. `gitoria-shell` accepts only `git-upload-pack|git-receive-pack '<slug>.git'` (slug validated, no shell, no forwarding, no tty), asks `GET /__git/access?user&slug&write`
  162. and only then runs git on `/repos/<slug>.git`. Same rule as HTTPS: **read = everyone, push = the repo's owner**.
  163. * **The two internal routes** exist only when `GITORIA_SSH_SECRET` is set; every call needs `X-Gitoria-Secret` = that secret, and a call that came through the public proxy
  164. (`X-Forwarded-For` / `X-Real-IP`) is refused. **SSH is offered on the site only when the secret is set** (the keys section and the ssh commands in the "add code" box are hidden otherwise).
  165. * **Deploy (the architect)**: `GITORIA_SSH_SECRET=<long random text>` (and optionally `GITORIA_SSH_PORT`, default 2222) in `.env` beside `docker-compose.yml` (both services read it);
  166. open the port in the firewall (port 22 belongs to the host's sshd, hence 2222: address `ssh://[email protected]:2222/<slug>.git`; with `GITORIA_SSH_PORT=22` the box shows `[email protected]:<slug>.git`);
  167. `gitoria.worldapi.org` (not only the wildcard) must resolve to Byrodin directly — **Cloudflare's proxy does not carry ssh** (use a grey-cloud/DNS-only record for the ssh host or a
  168. separate name; the address in the box uses the site's host name). The Hybriel image needs `openssh-client` (Dockerfile). `./storage/git` is mounted into the sshd container; its `git` account takes
  169. the uid of that folder's owner; host keys live in `storage/sshd-hostkeys/` (clients keep trusting the server). `docker compose up -d --build` builds both.
  170. * Test: `node tests/ssh.mjs` (048: `… GITORIA_GATE_SSH_PORT=8758` + the push ports → 44/0) (builds and starts the REAL sshd container on port 8708 with host networking; real ssh + git: clone, push 3 MB, RSA + ed25519 keys, pull, unknown key, no shell,
  171. path tricks, no forwarding, another user may read not push, removed key stops at once). Needs docker.
  172. ## The repo homepage (`/` of a repo address)
  173. * The repo's **README.md** (root, any case) is shown as a page; if the repo has a **`$docs`** folder, **all Markdown files inside it**
  174. (subfolders too, in path order, at most 30) form the homepage instead and the root README is not shown. Read from the
  175. repo's `HEAD` (the branch setting comes with #9). No commit / no README → "This repository has no README.md yet."
  176. * Reading = the `git` binary via hl:proc (`lib/homepage.hl`: `ls-tree -r`, `cat-file blob HEAD:<path>`, argv list, paths only from git,
  177. 15 s limit, ≤ 5000 lines a file),
  178. read while the page is built on the server (`homepageNow`).
  179. * Markdown → HTML (`lib/markdown.hl` copied from tickets, plus tables, block quotes, rules; `components/markdown.hl`): built as
  180. elements from parsed data, never an HTML string — raw HTML in a README is shown as text, only http(s)/mailto/`/…`/`#…` links
  181. are links. Not rendered: images, nested lists (shown as typed).
  182. ## Browsing code (`/code`, `/branch/<name>`, `/commit/<id>` of a repo address)
  183. * **`/code`** = the repo's main branch at its last commit. Main branch = the owner's setting (repo field `branch`), else `main`,
  184. else the first branch. The owner sets it on the code page: "Make main" beside each other branch (only the owner sees it; the
  185. face checks the owner again). The homepage (README / `$docs`) reads the same main branch.
  186. * **`/branch/<name>`** = that branch at its last commit (a name with slashes works: the longest existing branch name wins).
  187. **`/commit/<id>`** = the whole project at that commit (`<id>` = 4–40 hex characters, resolved to the full id).
  188. * After each of them a path: `/code/src/a.txt`, `/branch/feature/x/src`, `/commit/<id>/src`. A folder shows its entries
  189. (folders first, then files, with size), a file its numbered lines (at most 2000 lines, at most 1 MB). Also on the page: the
  190. crumbs, the latest commit, all branches, the latest 20 commits (each links to `/commit/<id>`).
  191. * Read with the `git` binary (`lib/code.hl` `browse`: `for-each-ref`, `log`, `cat-file`, `ls-tree`, argv lists, `--literal-pathspecs`).
  192. Read on the server while the page is built (`browseNow`). `/code`, `/branch/*`, `/commit/*` are three pages sharing
  193. `codebrowser.hl`; a link inside the app is an hl:web navigation. "Make main" (face `gitoriaSetBranch`) answers with the view again.
  194. * **Only UTF-8 text is shown**: a binary file or one that is not valid UTF-8 says so instead (its bytes would break the
  195. page's socket). Paths with `:`, quotes, backslashes or control characters are not browsable (`lib/git-helpers.hl` `safePath`).
  196. * Not built: the Markdown read view / API of code, a diff of a commit, images, syntax highlighting, downloading a tree.
  197. ## Tickets (`/tickets` of a repo address)
  198. * The tickets are **not stored in gitoria**: they live in tickets.worldapi.org, in a project named `<slug>.<host of GITORIA_PUBLIC_URL>`
  199. (e.g. `myrepo.gitoria.worldapi.org`; a repo slug has no dot, so it never meets another repo's project or the dotted app projects).
  200. Anyone logged in to tickets sees them like any tickets project. `lib/tickets.hl` talks to tickets' public API.
  201. * **List**: `GET <tickets>/api/projects/<project>/tickets` (public), newest update first, at most 200 shown: number, subject (as text), state,
  202. last update; each links to the ticket in tickets (read, comment and change the state there — the list view only is in gitoria).
  203. No project yet (404) → "No ticket yet". Tickets down → "tickets.worldapi.org did not answer". Read when the page is built on
  204. the server (hl:fetch is synchronous): the list is in the first HTML. A ticket opened here shows up live on every open tickets page of that repo.
  205. * **Connect (gitoria#18)**: the repo's **settings** (`/settings`, owner only) has "Tickets: connect". It sends the owner to
  206. `<tickets>/connect?app=gitoria&label=<slug>&return=<repo address>/settings/connected&state=<nonce>` (tickets asks which project they are
  207. admin of); tickets returns `?code&state`; gitoria checks the nonce (parked in the session, bound to the repo), exchanges the code from
  208. the server (`POST /api/connect/exchange`) and stores the key per repo in `repos.db` (`tktKey`, never sent to a page). "Forget the
  209. connection" clears it locally (tickets can also disconnect). Not connected → `/tickets` says so and points to Settings.
  210. `GITORIA_TICKETS_TOKEN` and the auto-created `<slug>.<host>` project are gone (they were the old way).
  211. * **Open a ticket**: any logged-in user with a display name: `POST <api>/tickets` with `Authorization: Bearer <key>` and
  212. `X-Tickets-Identity: <the user's ident id>` — the person is the author in tickets, under the project's roles (they must have logged
  213. in to tickets once).
  214. * **`#N` links both ways**: `#12` in a commit subject (code page, latest commits) or a pull request title links ticket 12. A push
  215. (and the Merge button) runs `notifyPush`: every commit of the last 100 on any branch whose subject names `#N` posts a comment
  216. "Mentioned in commit …" on ticket N (once per commit and ticket, remembered in `ticketlinks.db`), and a **merged** `|||PR` whose
  217. title says `fixes|closes|resolves #N` sets ticket N to state `review` with a comment. Done as the repo's owner. Commits that
  218. exist when the repo is connected are only marked, not announced.
  219. * Not built: showing a ticket's text / comments inside gitoria, editing a ticket from gitoria, a state filter, API of gitoria for tickets.
  220. ## Pull requests (`/pulls` of a repo address)
  221. * Nothing is stored: the list is read from git each time (`lib/pulls.hl` `pulls`). A **pull request is a commit** whose subject starts
  222. `|||PR ` (target = the repo's main branch, i.e. the owner's setting, else `main`) or `|||PR|<branch>] ` (target = `<branch>`).
  223. Anything else (marker not at the start, no space after `]`, an invalid branch name) is a normal commit.
  224. * Title = the text after the marker (empty → the source branch's name). Source = the branch that holds the commit and is not the target
  225. (`for-each-ref --contains`); one request per source branch (its newest marker commit); 50 at most, from the latest 500 commits of all branches.
  226. * State: **merged** when the commit is in the target branch (`merge-base --is-ancestor`), else **open**; a target that does not exist is
  227. shown as "(no such branch)". Merging is done with git itself (push to the target) — no merge button yet.
  228. * Read on the server while the page is built (`pullsNow`).
  229. ## Releases (`/releases` of a repo address)
  230. * Nothing is stored: read from git each time (`lib/releases.hl` `releases`). A **release is a commit on the repo's main branch** whose subject starts
  231. `|||RL ` (patch +1), `|||RL|med ` (minor +1, patch 0), `|||RL|mj ` (major +1, minor and patch 0) or `|||RL|<version> ` (set by hand,
  232. `1.1.1a`: three numbers, then letters/digits/`.`/`-`). `|||RL` alone counts as `|||RL `. Anything else is a normal commit.
  233. * Versions are counted from 0.0.0 over the main branch's history, oldest to newest (first `|||RL ` = 0.0.1); after a hand-set version the count
  234. goes on from its numbers. A hand-set version already released is not a release. Shown newest first (200 at most, latest marked): version, text
  235. after the marker (else the short commit id; links to `/commit/<id>`), author, date. Read on the server while the page is built (`releasesNow`).
  236. * A release is only that: version + commit. No git tag is written, no archive to download (the concept does not say what else it holds).
  237. ## PWA (installable app, antcolony mission 046)
  238. The installable app, the same way calendar.worldapi.org and tracker do it: hl:web's own manifest and service worker from
  239. settings in `project.hl`, no JavaScript of ours.
  240. * `appIcons` (192 + 512 PNG, each `any` and `maskable`), `appTouchIcon` (180 PNG), `appFavicon` (`/icons/favicon.svg`),
  241. `appThemeColor` = token `darker` (the header, rgb(15, 20, 25)), `appBackgroundColor` = token `dark` (rgb(25, 30, 35));
  242. name = `appTitle` "gitoria". hl:web serves `/__hl/manifest.webmanifest` (start_url/scope `/`, display standalone) and
  243. `/__hl/sw.js`, and links manifest, apple-touch-icon and theme-color from every head. `/favicon.ico` is a real icon
  244. (16/32/48). Each icon has its own `file` route in `project.hl`. Every repo address is its own origin: it gets the same
  245. manifest and its own worker (installed from a repo address, the app opens that repo's Readme).
  246. * **Icons** (`icons/`): `icon.svg` is the source (512, hand-written: a git branch — trunk with two commits, a branch
  247. curving off to a third; `#569bd4` on rgb(25,30,35); everything inside the maskable safe zone, a circle of radius 204, so
  248. one image serves `any` and `maskable`); `favicon.svg` is the same drawing, thicker, cropped tight on a rounded tile.
  249. Rendered on Loreana:
  250. ```bash
  251. rsvg-convert -w 192 -h 192 icons/icon.svg -o icons/icon-192.png
  252. rsvg-convert -w 512 -h 512 icons/icon.svg -o icons/icon-512.png
  253. rsvg-convert -w 180 -h 180 icons/icon.svg -o icons/apple-touch-icon.png
  254. for s in 16 32 48; do rsvg-convert -w $s -h $s icons/favicon.svg -o /tmp/fav-$s.png; done
  255. magick /tmp/fav-16.png /tmp/fav-32.png /tmp/fav-48.png icons/favicon.ico
  256. ```
  257. * **Offline**: `offline = [ Index ]` — the worker precaches the shell (runtime, modules, CSS, manifest, icons) and the
  258. document of `/`. Navigations are network-first (an online visit of `/` refreshes the kept copy); without a network `/`
  259. comes from the cache AS LAST SEEN (main address: the repo list; repo address: its Readme) and every other page (code,
  260. branch, commit, pulls, releases, tickets, settings) gets hl:web's "Unavailable offline" page (503). The shell
  261. (`components/main.hl`) shows "You are offline. Repositories and code need the network." while `navigator.onLine` is
  262. false: hl:web gives a page no connection state and no mount hook, so an invisible `netProbe` runs an endless 1 s CSS
  263. animation (`components/styles.hl` `@keyframes gitoria-net-tick`) whose `animationiteration` handler reads `navigator.onLine`.
  264. A server that is down while the device is online shows no note.
  265. ## Test
  266. `node tests/browser.mjs` (200 checks; commits real files into the gate's bare repos) — own gitoria + own ident + own tickets (copies without `.env`, mail sink; `tests/ticketskit.mjs`) + headless Chromes; `*.gitoria.test`
  267. is mapped to 127.0.0.1 inside Chrome (`HL_CHROME_ARGS`, `tests/cdp.mjs`). Ports 8700–8709 (gitoria 8700, ident 8701, tickets 8702, Chromes 8703–8709); another range:
  268. `GITORIA_GATE_PORT=8710 GITORIA_GATE_IDENT_PORT=8711 GITORIA_GATE_TICKETS_PORT=8712 GITORIA_GATE_CHROME=8713-8719 node tests/browser.mjs`. Screenshots in `.scratch/gate-*.png`, server log `.scratch/gate-server.log`.
  269. Last run (mission 002 code order, ports 8750–8759, tickets from a `git archive HEAD` copy because the tickets working tree was
  270. mid-change: `GITORIA_GATE_TICKETS_DIR=$PWD/.scratch/w082/tickets-head GITORIA_GATE_PORT=8750 GITORIA_GATE_IDENT_PORT=8751
  271. GITORIA_GATE_TICKETS_PORT=8752 GITORIA_GATE_FIREFOX=8759 GITORIA_GATE_CHROME=8753-8758` → 200/0; push.mjs (`GITORIA_GATE_PROXY=8752
  272. GITORIA_GATE_CHROME=8753-8757`) 46/0; ssh.mjs (+ `GITORIA_GATE_SSH_PORT=8758`) 44/0.
  273. Before (antcolony mission 074, hybriel 06617221, ports 8760–8769): `GITORIA_GATE_PORT=8760 GITORIA_GATE_IDENT_PORT=8761 GITORIA_GATE_TICKETS_PORT=8762 GITORIA_GATE_FIREFOX=8769 GITORIA_GATE_CHROME=8763-8769` → `200 passed, 0 failed`; push.mjs (`GITORIA_GATE_PROXY=8762`) 46/0, ssh.mjs (`GITORIA_GATE_SSH_PORT=8769 GITORIA_GATE_CHROME=8763-8768`) 44/0. Mission 072 (hybriel 190aa11d): `GITORIA_GATE_PORT=8720 GITORIA_GATE_IDENT_PORT=8721 GITORIA_GATE_TICKETS_PORT=8722 GITORIA_GATE_FIREFOX=8729 GITORIA_GATE_CHROME=8723-8728` → `200 passed, 0 failed`; push.mjs (`GITORIA_GATE_PROXY=8722`) 46/0, ssh.mjs (`GITORIA_GATE_SSH_PORT=8729`) 44/0. Mission 048: ports 8750–8758 → `199 passed, 0 failed`. Before (antcolony mission 046): `GITORIA_GATE_PORT=8720 GITORIA_GATE_IDENT_PORT=8721 GITORIA_GATE_TICKETS_PORT=8722 GITORIA_GATE_CHROME=8723-8727 GITORIA_GATE_FIREFOX=8728 node tests/browser.mjs` → `199 passed, 0 failed` (the Firefox port defaults to 8699 — set it inside your range).
  274. * PWA block (antcolony mission 046, at the END of the gate): manifest + icons over HTTP (real PNG sizes, favicon blue, `/favicon.ico` an
  275. ICO), then the gate's Chromes are CLOSED and one fresh Chrome gets `--unsafely-treat-insecure-origin-as-secure=<main>,<alpha>`
  276. (a service worker needs a secure context; plain-http `*.gitoria.test` is not one — the live https site needs nothing):
  277. Chrome's installability + manifest verdict, worker scope `/` on the main AND a repo address; offline: the tab's network
  278. is cut (the note appears live), the SERVER is stopped (CDP offline does not reach the worker's own fetches), reload of `/`
  279. shows header + note + the list as last seen, alpha's `/` its Readme, `/code` "Unavailable offline"; server restarted,
  280. back online. Screenshots `.scratch/gate-pwa-phone-{online,offline}.png` (390 px).
  281. * gitoria#16 block: `firstHtml(path, host)` fetches the FIRST HTML with a Host header (node's fetch cannot set one) — `/` (README), /code,
  282. /code/src, a file, /branch/main, /branch/feature/x, /commit/<id>, /pulls, /releases, /tickets hold their real content and no "Loading";
  283. the main address the repo list; hybriel#43: a code view in a second tab of the session keeps its elements through a login and a
  284. logout of the other tab; every view of an unknown address "No such repository"; `/host.js` 404; in Chrome every view loads
  285. directly, and Readme → Code → Releases → Tickets → Pulls → Readme plus a folder + Back keep `window.__navMarker` (no page load).
  286. * Re-vendor block: `/__hl/app.css` has the token file's `--dark` in `:root` (hybriel#39); a repo description
  287. `</script><b id="xss">…` stays inside the seed (`\u003c`) and shows as text (hybriel#34); `Domain=.gitoria.test` on the cookie (`sessionDomain`).
  288. * Navigation LOGGED IN (the creator saw full page loads in Firefox): a fresh Chrome logs in with the button on a repo address, then
  289. real clicks Readme → Code → Releases → Pulls → Tickets → Code → a folder keep `window.__navMarker`; the same on an empty repo the user
  290. owns, and with the WebSocket closed (POST fallback). The same two runs in a **real Firefox** (`tests/firefox.mjs`: headless
  291. `/usr/bin/firefox` over WebDriver BiDi, no driver/npm; hosts mapped with the pref `network.dns.localDomains`; BiDi port
  292. `GITORIA_GATE_FIREFOX`, default 8699).
  293. * By hand: `curl -s -H 'Host: <slug>.gitoria.test:8720' http://127.0.0.1:8720/code` against a running dev server.
  294. * Lesson: the views are in the FIRST HTML now, so "content is there" no longer means "page is live" — `await hydrated(page)` before
  295. clicking a button with a handler ("Make main" was clicked on the dead SSR page and flaked).
  296. ### Same output (a cleanup must not change what the app answers; mission 002)
  297. On a COPY of the live data (Byrodin → Loreana, deleted after the run):
  298. ```bash
  299. # on Byrodin:
  300. tar -C /CONTAINERS/projects/gitoria.worldapi.org -cf - storage/mpackdb storage/git | ssh loreana 'mkdir -p /media/STORAGE/projects/gitoria.worldapi.org/.scratch/realdata && tar -C /media/STORAGE/projects/gitoria.worldapi.org/.scratch/realdata -xf -'
  301. # on Loreana, in the repo:
  302. git archive <old commit> | (mkdir -p .scratch/old && tar -x -C .scratch/old) && cp -a bin .scratch/old/
  303. GITORIA_REALDATA=.scratch/realdata node tests/realdata-baseline.mjs .scratch/old 8750 .scratch/out-old # app 8750, sshd 8758, fake tickets 8759
  304. GITORIA_REALDATA=.scratch/realdata node tests/realdata-baseline.mjs . 8750 .scratch/out-new
  305. python3 tests/realdata-compare.py .scratch/out-old .scratch/out-new -v # exit 0 = the same
  306. python3 tests/letcount.py . # root .hl files, project.hl lines, lets
  307. ```
  308. `realdata-baseline.mjs` signs in as the creator (az5b2 → users @id `0mugibaf6fds`) with a session file and fetches every page of
  309. every repo (signed in and out: `/`, code, files, folders, branches, commits, pulls, releases, tickets, settings, unknown
  310. addresses), the browser modules, the API (JSON + Markdown), the git refs advertisements, the refusals, the login / connect
  311. routes, the internal ssh routes; then WRITES on its own copy: faces (create, token, key, Make main, Merge, …), clone + push over
  312. HTTPS (token) and over SSH (the real sshd container), removal of key and token, logout. The connected repo's tickets are a
  313. snapshot served locally (nothing reaches live tickets). Two runs of the SAME code compare clean (the masks change nothing alone).
  314. ## Deploy
  315. `./deploy.sh` (gates → backup → rsync → restart → 200). First deploy = the architect's: folder, `.env`, wildcard vhost
  316. (`server_name gitoria.worldapi.org *.gitoria.worldapi.org;`, WebSocket upgrade headers), wildcard DNS + certificate, and the app
  317. registered in ident with origin `https://gitoria.worldapi.org`. Container port 45004 (`docker-compose.yml`).
  318. deploy.sh's rsync does not delete: after the mission-002 move the old root `.hl` files (api, git, users, …) stay on Byrodin
  319. unused (project.hl imports only `lib/`); remove them by hand once if wanted.
  320. ## History and worker briefs
  321. - `LOG.md` — append-only history, one dated line per step (moved here from the antcolony LOG on 2026-10-01).
  322. - `missions/NNN-*.md` — worker briefs for this app; `reports/NNN-*.md` — their reports (same name). Numbered per
  323. project since 2026-10-01 (antcolony#40); older text, code comments and commits use the old antcolony numbers →
  324. map: `/media/STORAGE/projects/antcolony-docs/docs/mission-map.md` (Byrodin: `/CONTAINERS/projects/antcolony/docs/mission-map.md`).

Branches

  • mainmain branch

Latest commits

  • 4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mre
  • 86605446mission 002 (code order) 4/4: README file map + import order + 'Same output' test + gate run with a tickets HEAD copy, STATUS (entry, lessons), LOG, report; tests/realdata-baseline.mjs + realdata-compare.py (a cleanup answers the same on live data: pages, modules, API, git over HTTPS and SSH, faces), tests/letcount.pymre
  • cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
  • 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
  • 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
  • fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
  • 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
  • 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
  • e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
  • 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
  • 3dc43108antcolony#40: mission references point to the moved missionsmre
  • 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre