50 descendants of c754084dc7785c2e
|
2026-08-18
| ||
| 19:21 | repo header: stats strip, ref switcher popover, clone box with download menu check-in: e7fb66c4df user: dmestas tags: trunk | |
| 19:11 | explore: repositories/people tabs, folded contributor table check-in: be4c4284f1 user: dmestas tags: trunk | |
| 18:51 | chrome: lighter dark secondary, centred search, quiet focus ring, no duplicate home heading check-in: 90c75cd912 user: dmestas tags: trunk | |
| 18:40 | tickets: record cmimetype on authored text check-in: f7ba4868d5 user: dmestas tags: trunk | |
| 18:31 | chrome: neutral surface token, full-width bar, anchored animated popover, compact fleet counts check-in: 3c43c1de30 user: dmestas tags: trunk | |
| 18:25 | header: full-width top bar, account popover with avatar check-in: b3ac263fee user: dmestas tags: trunk | |
| 18:19 | palette: whiter dark text, muted orange, themed highlight tokens check-in: 1d80a6cffa user: dmestas tags: trunk | |
| 18:01 | global search: fan out across repos with per-repo facets check-in: ae68223b22 user: dmestas tags: trunk | |
| 16:01 | fleet: carry filter and sort into the live stream check-in: d54bc03817 user: dmestas tags: trunk | |
| 14:54 | gruvbox palette, server-side theme switcher, context-scoped search check-in: 6d9997bbb4 user: dmestas tags: trunk | |
| 14:41 | archive: disk cache, zip format, immutable ETag; tree-bar cleanup check-in: b8afd21148 user: dmestas tags: trunk | |
| 14:11 | theme form controls; drop inline width overrides check-in: 93899cc429 user: dmestas tags: trunk | |
| 14:07 | ticket detail: drop duplicate title, fields as a dl check-in: eb6cc56c07 user: dmestas tags: trunk | |
| 13:55 | tickets: list layout, title first check-in: 3862cf6012 user: dmestas tags: trunk | |
| 13:48 | dates: render relative everywhere; strip markup from list titles check-in: 414f5a0e06 user: dmestas tags: trunk | |
| 13:40 |
nav: pills with compact counts; stop long tokens overflowing
The repo nav's width was a function of its numbers — tk's 25557 and 5809 made a visibly different nav from a small repo's, wrapping to three lines on a phone. Counts go compact (25K, 5.8K, 1.2M, four characters max) with the exact figure in the badge title, and each entry is a pill rather than a word beside a loose number. Nav width is now identical across repositories. Separately: sqlite has wiki pages named after check-in hashes, 40-char unbreakable tokens that pushed the page 300px past a phone viewport. main now breaks over-long words; pre keeps its own scroll. check-in: 15c39013de user: dmestas tags: trunk | |
| 13:25 |
landing page: vendor octicons, real columns, description spans the row
Three fixes for one screenshot. The icons were CSS masks with inline data: URIs — valid in Chrome, invalid under iOS Safari's stricter parsing, so every icon rendered as a grey block. Now an SVG sprite of six Octicons (MIT, ledgered); no encoding to get wrong. Alignment needed real table columns. CSS grid aligns within one grid, so a per-row grid gave every row its own widths and nothing lined up. Measured: stat cells now share right edges across rows exactly. And the description spans the full row instead of sitting in column one, which had made that column as wide as the longest sentence and pushed the numbers off a phone. No horizontal scroll at 320-1280px; the mirror column drops below 30rem. check-in: 60e5e5ebb6 user: dmestas tags: trunk | |
| 13:11 |
landing page: stats as aligned badges, not a sentence
A sentence of stats is a different width in every row, so nothing lines up and the column is hard to scan. Four fixed slots — check-ins, contributors, mirror freshness, last activity — each an icon and a number, in a grid so the numbers align down the page. Durations go compact (34m, 12h, 3d) for the same reason; the exact instant stays in the title and datetime. Every badge keeps a title, because an icon alone states nothing. check-in: 7bf20af78a user: dmestas tags: trunk | |
| 12:44 |
landing page: drop the commit message, show activity signals
An aggregator listing showed whatever the last commit happened to say — written for people already in that repo's context, the widest thing in the row, and on a phone one entry filled the screen. GitHub and Forgejo list description and activity and neither shows a commit message. Replaced with what a stranger can use: last activity, check-ins, contributors, and mirror freshness. The last is there because mirroring created an ambiguity — a repo quiet upstream and a mirror failing for a week look identical if all you see is 'last activity'. Contributors rides the memoized Counts, so it costs nothing per request. check-in: 012cdff6c9 user: dmestas tags: trunk | |
| 11:42 |
repo.delete: destroy a repository, with two brakes
Removing a repository's disk space meant going to the shell and doing it without quarry knowing, which is worse than quarry doing it. Deletion now purges the store, the derived index, and every forge row keyed to the repository — grants, sync users, mirror, webhooks, CI records — in one transaction, so a new repository of the same name cannot inherit somebody's credentials. Two brakes. confirm must equal the name: a yes/no prompt is answered reflexively, typing the name requires having read which repository is about to go. And the store is removed ONLY when it lives under this node's repositories root — quarry serves stores it did not create, and this forge runs on a corpus of exactly those, so deleting one named by --repo would destroy a checkout somebody else manages. Verified: the attempt on pikchr refuses and says to unregister instead. check-in: a8d2831bf4 user: dmestas tags: trunk | |
| 11:33 |
repo.add: seed a new repository's first check-in
A repository with no check-ins has an unborn trunk, so the timeline, the file tree and every clone instruction have nothing to point at. Optional README, license (MIT or BSD-2) and .zitignore give it a born trunk, which is why GitHub offers the same three at creation. The copyright line is left as <copyright holder> rather than filled with the account name: a copyright naming the wrong person is worse than one that is visibly unfinished, and an account is a login, not a legal entity. Seeding is refused on a clone, which arrives with its own history, and paths are checked against the repository root before anything is written. check-in: c5b6d979e2 user: dmestas tags: trunk | |
| 11:17 |
repo.list: filter, sort and page the listing
The front page rendered every repository unsorted and unbounded — the same shape that made the timeline unusable, and it is at 23 now. Adds a name filter, sort by name, recent activity or size, and a limit, with Total alongside Count so a page can say '5 of 23'. Sorting happens BEFORE the limit. The other order gives an arbitrary subset in the right sequence, which reads as correct and is not. Activity comes from the event table's index — one walk stopping at the first row — so the column that makes a forge listing worth reading costs nothing. Adds repo.get, so the settings page and the CLI stop scanning the whole listing to read one repository's settings. check-in: 4a3165db34 user: dmestas tags: trunk | |
| 11:06 |
repo: settings page, descriptions everywhere
repo.update replaces repo.visibility and applies a repository's settings whole — a settings form submits every field it manages, so partial-update semantics would only add a way for the two to disagree about what absent means. The CLI reads current settings before writing, so 'repo private' no longer blanks the description. A settings page at /r/{repo}/settings, admin-gated, and the description now appears on the repository itself rather than only in the listing: a reader arriving from a link otherwise sees nav and a file tree and no statement of what the project is. check-in: a41b410958 user: dmestas tags: trunk | |
| 10:54 |
repo: private repositories
Visibility on the registry, gated at every read path: pages, API, search, file reads and the sync endpoint all answer 404 for a repository the caller may not see. 404 and not 403 — a 403 confirms the repository exists, which is the one thing a private repository must not confirm. The listing filters by visibility too. A list that names a private repository has leaked it whatever the page then refuses to show. The gate lives in Deps.repoFor, where d.repo already resolved the store, so a new op cannot forget it. The API router now resolves identity for ANONYMOUS ops as well: a read op that never looks at credentials cannot tell a repository's own collaborators from strangers, and would lock them out of their own repository. Two faults found while verifying. Repositories named by --repo had no registry row, so taking one private updated nothing and reported success — a security control that claims to work while doing nothing. They are recorded now, and SetRepoVisibility refuses a name it does not hold. Second, the sync path was ungated: a private repository whose pages refuse anonymous readers while zit clone hands over every artifact is not private. Verified: anonymous and unprivileged accounts get 404 everywhere and a short list; a collaborator with read sees it; admins see everything. check-in: f88e97a9fd user: dmestas tags: trunk | |
| 02:09 |
account: manage identity through the API, and stop lying about why
Six ops (account.list/add/passwd/delete/grant/revoke, admin) plus quarry accounts on top of them. Creating a login or granting someone read on a repository no longer needs shell access on the server. quarry account still opens OLTP directly and has to: the FIRST account must exist before any server runs or any admin exists to authenticate as. Everything after that is the API, so the CLI has no privileged path. The verifier never crosses the seam in either direction, and account.add /passwd scrub the password out of any error before it travels — an error is the one value that reaches a log, an HTTP body and a terminal at once. Verify returns a three-valued Decision instead of a bool. Denied and Insufficient are not the same thing and the wire has separate codes for them: an authenticated client short of a capability was being told 401 with a challenge, which invites it to retry the same password forever. Now 403, and deliberately without a challenge. A harness test pinned the old behaviour and was updated with the reason. check-in: 86880118d5 user: dmestas tags: trunk | |
| 01:59 |
repo: a create flow a person can actually use
Nobody picks a filesystem path when they make a repository on a forge, and requiring one made repo.add usable only by the machine's operator. --repos-root gives the server somewhere to put them; path is now optional and the response says where it landed. Repositories gain a description — every forge has one and quarry had nowhere to put it, so the listing could only show names and counts. Three surfaces on one op: quarry repo new|clone|list|remove drives the HTTP API rather than the database, so the CLI has no privileged path and anything it can do a client can; /new is the same three questions a forge always asks; and an empty repository shows how to push to it instead of a bare file table, because it is waiting, not broken. A rejected form comes back as a form with the reason on it — an error page throws the reader's input away. check-in: 87a492fe4f user: dmestas tags: trunk | |
| 01:47 |
repo.add: create an empty repository or adopt one by cloning
Two cases quarry could not do at all. create runs zit init at the path — the 'host my project here' case. clone copies a remote in and records a mirror for it, because adopting a remote repository and then not tracking it is almost never what anyone meant. Cloning takes the bulk lane: a large repository is minutes of network, and an admin asking for it is not a reason to run it unbounded. Both refuse a non-empty target and undo what they made if a later step fails — the store, the OLTP row, or both. Verified: sqlite.org/docsrc adopted in 14s (5,116 check-ins, serving in 2.24ms), and a clone from an unreachable URL leaves no directory, no registration and no row. check-in: 9f78830644 user: dmestas tags: trunk | |
| 01:24 |
repo: add and remove repositories without restarting
Adding a repository meant editing the server's command line and restarting, which took every other repository down with it. The registry now lives in OLTP where 0001 §8 always placed it, and repo.add / repo.remove (admin) register and tear down at runtime. registrar owns a repository's whole lifetime — index handle, pipeline, sync backend, poll goroutine. The startup loop could not: it relied on defer ix.Close() reaching serve's return and on a poll loop that never stops, and both leak the moment a repository is removed. Add persists before it serves, and rolls the record back if opening fails: a registration that serves without being recorded vanishes at the next restart with no trace of why. Remove stops serving and forgets, but leaves the store and index on disk — 'stop serving' and 'destroy' are different intentions and only one is reversible. --repo stays as bootstrap and override. check-in: 2c157378a3 user: dmestas tags: trunk | |
| 01:15 |
api: make the repo set a concurrent registry; serve rejects positionals
Deps.Repos was a bare map, read by every op on the request path and written only at startup. Adding repositories at runtime — the point of the registry ticket — makes that a data race with no symptom until it corrupts one. api.Repos owns the lock; a *Repo is published complete or not at all, and Delete hands back the entry so the caller can close the index and stop the pipeline. serve now refuses leftover arguments. Go's flag package stops at the first non-flag and returns the rest, so one unquoted shell variable silently dropped twelve --repo flags and quarry served the remaining eleven without a word. check-in: 4a479e6ce2 user: dmestas tags: trunk | |
|
2026-08-17
| ||
| 22:47 |
ingest: bound how many stores stream at once
zit frames streams a WHOLE store: 507 MB peak on sqlite, more on tcl. quarry ran one ingest goroutine per repository with nothing between them, so a cold start — or a mirror round landing artifacts in several big stores at once — ran that many censuses simultaneously and the memory added. Observed as zit spiking to gigabytes after pulling sqlite and tcl together. --ingest-limit (default 3) puts them in a work lane. The queue is generous and ingest is never shed: dropping a round would leave an index behind its store until the next poll, trading a correctness surprise for a saving the limit already delivers. check-in: e6fd32e82e user: dmestas tags: trunk | |
| 21:32 |
web: map op error kinds instead of collapsing them to 500
fail() mapped only ErrNotFound, so every other kind rendered as 500 — and a 500 tells a client the request failed, not to try again. Two retryable paths lost that distinction: a single-writer lease refusal (0001 §3) and a shed from a saturated work lane. ErrInvalid now renders 400 and ErrUnavailable 503 with Retry-After, matching what the API router has always done. Verified with the archive endpoint, live for the first time now that zit 10a1236a7a fixes tarball -R: twenty concurrent requests give ten 200s and ten 503s with Retry-After, RSS bounded at 247 MB. check-in: e9d441a552 user: dmestas tags: trunk | |
| 21:26 |
work: bound bulk operations; footer times keep their precision
Nothing in quarry limited concurrency — every zit subprocess ran in the request goroutine with the arrival rate as the only bound. work.Lane is a concurrency limit plus a queue cap that SHEDS past it: a request accepted into a queue it cannot be served from consumes the capacity that would have served someone else. Wired to archive first, which buffers 256 MiB per request on an anonymous route. That is latent rather than live — archive 503s on zit#97 — which is why the bound belongs there now, so the endpoint is born bounded rather than acquiring a limit after someone finds it. Footer precision: '<1ms' was true and useless once most pages went sub-millisecond, collapsing the interesting range into one label. timing.Format keeps ~3 significant figures across five orders of magnitude, so 900µs and 90µs no longer read the same. Fossil has the mirror-image bug: three decimals of seconds prints everything faster than a millisecond as 0.000s. check-in: 45bb75ade2 user: dmestas tags: trunk | |
| 21:14 |
zitcli: byte-bounded content cache for hash-versioned reads
zit cat costs ~147ms even for a small file, against a 3ms process floor, and every file view forked one. Under load that capped the forge at 32 req/s, degrading to 24 at twenty concurrent readers. hash-versioned file page: 393ms -> 18ms 50 concurrent: 201 req/s, still climbing Bounded by BYTES, not entries: values are file contents, and an entry-counted cache holding a 30MB blob is a memory leak with a polite name. Per repository, so a busy store cannot evict a quiet one's entries. resolveVersion only expands hash PREFIXES. An earlier draft fell back to the index's branch table for names, which served tk a README from 2020: tk renamed its trunk to main, so a branch literally called trunk still exists and is six years stale, while zit resolves the word correctly. 0001 §2 makes zit the authority on names and that is not negotiable — so a symbolic version stays uncached, and the way to fix that is to make links carry the resolved hash. check-in: 0cf9a57e88 user: dmestas tags: trunk | |
| 21:00 |
index: fold ticket summaries at write time
The ticket list derived itself per request: GROUP BY over every change row (tk: 35,903), two correlated subqueries per ticket to find the newest title and status — 11,618, each with its own temp b-tree — and a HAVING that filtered after grouping, so paging saved nothing. tk 200ms -> 1ms sqlite 1ms fossil <1ms Written from refreshTicketDoc, which already folds exactly this state for the search doc, so the summary cannot disagree with what search sees. Backfilled at open for indexes written before the table existed. Second instance of the same bug the timeline had, so plan_test now covers this query too. check-in: 4c973ff323 user: dmestas tags: trunk | |
| 20:51 |
web: report where a request's time went, not just how long
The footer said 'rendered in 344ms'; it now says 'rendered in 344ms (zit 270ms)'. One number says a page is slow, not which layer to look at, and quarry's layers differ by three orders of magnitude — an index read is microseconds, zit ls --long on tk is 2.6 seconds. The accumulator rides the request context, which is what makes it correct under load: package-level counters would blend concurrent requests together, and concurrency is what this exists to measure. zit read calls take a context now and use exec.CommandContext, so an abandoned request kills its subprocess instead of leaving it to finish a 2.6-second listing nobody will read. SQL is not instrumented yet; the index does not take a context. check-in: 85b9909d1c user: dmestas tags: trunk | |
| 20:42 |
index: denormalize the timeline into an event table
The timeline assembled itself at read time from a five-way UNION ALL, ending in USE TEMP B-TREE FOR ORDER BY — every request sorted every event in the repository to return fifty. tk cost 536ms; fossil serves the same page in 3-4ms whatever the repository size. This is fossil's shape: one event table with a kind column and an index on date, so the read is an index walk that stops at the limit. Triggers maintain it, like search_fts, so it cannot drift from its sources. isleaf and the ticket label are resolved for the returned page only. tk 536ms -> 4ms sqlite 210ms -> 5ms fossil 163ms -> 3ms pikchr 22ms -> 1ms home 399ms -> 3ms (it asked each repo for its newest event) plan_test asserts the plan, not the types: nothing about the old query was ill-typed, and the defect was only ever visible in EXPLAIN. Also fixes limit being ignored on every wildcard route: the router's second decode pass re-applied defaults to fields the path did not name, so ?limit=200 became 50. OverlayValues sets only what it names. check-in: 450a8ad0f3 user: dmestas tags: trunk | |
| 20:32 |
web: report render time in the footer
Fossil publishes its own ('This page was generated in about 0.003s') and that is what made our timeline regression measurable against it. Same number, same place, every page. semdiff gains data-volatile so the disposability harness compares state rather than the stopwatch — without it a wipe-and-reingest fails on '<1ms' vs '4ms', which is true about the clock and false about the index. check-in: cb17edbefa user: dmestas tags: trunk | |
| 19:11 |
web: compact the file listing
Was ~52px per row across three columns for one short filename; fits 38 entries where 25 fit before. Drops the History column — thirty identical links, and the file page already links its own history — adds dir/file marks, right-aligns sizes with tabular figures, and folds four lines of chrome into one. Names are no longer underlined: every name in the listing is a link, so marking them all marked nothing. The space the History column freed is where the last-change column goes once the index carries per-file data. check-in: 574cf5b97e user: dmestas tags: trunk | |
| 18:57 |
api: pick README markup by extension, not by assumption
A bare README was rendered as markdown 'by convention'. libspatialite's is an autotools plain-text file whose commands carry the root prompt #, so markdown read '# sudo make install' as a heading and turned the build instructions into a dozen h2s. Extension decides now, and anything unrecognized falls through to escaped with its line breaks intact — fossil's rule in wiki_render_by_mimetype. Also recognizes README.wiki.check-in: 1499d5aa5a user: dmestas tags: trunk | |
| 18:49 |
web: repository nav shows counts and current section
Counts tell a reader where a store's content is — pikchr is 245 forum threads and one ticket, tk is 5,809 tickets and one post. The index memoizes them per fold; on tk they cost ~110ms cold and 3ms after. Also: the current section is marked, empty sections are dimmed but stay linked, the repository outranks the forge name in the header, and timestamps render relative with the exact value in the title. check-in: 3a330c9560 user: dmestas tags: trunk | |
| 18:03 | docs: fleet admin and mirroring in running.md check-in: e79c732ee4 user: dmestas tags: trunk | |
| 18:02 |
fleet: mirror upstreams on a schedule
Adds mirror.list/set/delete/run ops (admin), a mirror table in OLTP, and a scheduler that pulls each configured store on its own interval. Pulls run under the store's single-writer lease and re-ingest only when artifacts actually arrived. zitcli.Pull is the one call that cannot pass -R: zit pull refuses the flag and works by working directory. pullChanged reads zit's result positively and errors on an unrecognized line rather than defaulting to 'nothing changed', which would leave a mirror green while it silently stopped tracking. check-in: 9c7b4cff5b user: dmestas tags: trunk | |
| 17:50 |
fleet: per-store ingest health and manual sync
Adds fleet.status and fleet.sync ops (admin) and a /fleet page: last round, artifacts, parse fallbacks, and consecutive failures per store. With eleven repos a wedged store is invisible on a page that shows only counts — its counts are whatever they were when it broke. gate gains gateAt so the fleet pages can ask for Admin at an empty repo rather than Token at a repo; forbid now words the refusal to match. check-in: 5c9f9f41b1 user: dmestas tags: trunk | |
| 17:18 |
ingest: stream the census instead of buffering it
FramesEach hands each artifact to a callback as it is read; peak RSS on sqlite's 13.2GB census drops to 0.48GB. Frames is kept as a collecting wrapper for tests. A callback error kills the child rather than draining the stream, and the cursor is not saved on a failed round. check-in: 05d01cbb93 user: dmestas tags: trunk | |
| 16:58 |
stop tracking agent tooling's files
The harness writes .agent-config/trace/<session>.jsonl into the working tree and it was tracked: 2MB, carried across ten check-ins, growing with every tool call, so each commit stored another revision of a log nobody working on quarry writes or reads. .claude/scheduled_tasks.lock is a lock file and equally not source. Both untracked with 'zit rm', which keeps the files on disk, and already ignored via .zitignore so they do not return as extras. History keeps what was recorded; this stops the accumulation. check-in: 4d8f962778 user: dmestas tags: trunk | |
| 16:58 |
stop tracking agent tooling's files
The harness writes .agent-config/trace/<session>.jsonl into the working tree and it is tracked: 2MB, carried across ten check-ins, growing with every tool call, so each commit stored another revision of a log nobody working on quarry writes or reads. .claude/scheduled_tasks.lock is a lock file and equally not source. Both untracked with 'zit rm' (the files stay on disk — zit keeps them unless asked otherwise) and added to .zitignore so they do not come back as extras. History keeps the revisions already recorded; this only stops the accumulation. Also lands gofmt churn in web_test.go from an earlier formatting pass: import order and one alignment block, no logic. check-in: bcd8d75db7 user: dmestas tags: trunk | |
| 16:53 |
deploy: the image builds and serves — three faults only running it found
The Dockerfile and compose were committed unverified because no container runtime was installed. With one available they were wrong in three ways, none visible by reading: - The Zig download 404s. Naming is zig-<arch>-linux-<version>; I had the older zig-linux-<arch>-<version>. - The zit clone fails: the GitHub mirror is private, so an anonymous clone asks for a username. zit's source now arrives as a named build context, which needs no credentials and pins the exact checkout being shipped. A public fossil mirror exists at fossil.craftdesign.group/zit and would make a clone step viable later. - /data was root-owned. VOLUME creates a missing directory as root while the process runs as uid 10001, so quarry could not open its own database — 'permission denied' on the first line of the log. Now verified end to end: a 90.1MB image, /etc/quarry-build recording both versions, and a container serving libfossil's 3,835 check-ins from a read-only mounted store with zit reading it from inside. check-in: a8cbc48878 user: dmestas tags: trunk | |
| 16:48 |
ignore generated fixture stores
fixtures/out is 599MB of built stores and it is what a 'zit addremove' once swept into this repository, along with a 45MB binary — 559MB of bloat that took a shun and a reclaim to undo. Ignoring the BUILT output rather than the whole directory: the scripts and README beside it are source. .zitignore rather than the ignore-glob setting, because zit says plainly that a root .zitignore overrides the setting, and because a tracked file travels with a clone while a local setting does not. check-in: 52a8ca8b77 user: dmestas tags: trunk | |
| 16:40 |
deploy: Dockerfile, compose stack and a run book
quarry had no packaging at all while notify already had a working Dockerfile and compose. The image ships zit WITH quarry, deliberately: 0001 §2 makes the zit CLI the entire store interface, so zit's behaviour IS quarry's, and today alone three zit builds changed what quarry does — frames got faster, a card fix changed which artifacts parse, stat -R enabled bare-store serving. ZIT_REF is a build argument and /etc/quarry-build records the pair, so a container can answer which zit it carries. compose follows notify's shape, including the constraint its own file documents: dagnats binds loopback in standalone mode, so everything shares its network namespace. quarry joins it too — its embedded NATS uses DontListen and binds nothing, so there is no port to collide with. docs/running.md is the by-hand path, which is what this was actually developed against: the launchd-signed dagnats, notify's bootstrap and workflows, the topic-vs-subscriber addressing, and the two credentials that close the write and push gates. Unverified, and said so in the doc: no container runtime is installed here, so the images have never been built. The two build recipes inside them ARE verified natively — zit compiles from a clean clone with zig build --release=fast, quarry builds fully static with CGO_ENABLED=0. Also lands the publish-throughput measurement as an opt-in test. It is what disproved the hypothesis that synchronous publish was the ingest bottleneck (62k msg/s sync), which redirected the search to zit frames and quarry's folding. check-in: bbe731e042 user: dmestas tags: trunk | |
| 16:35 |
archives: download a check-in as a tarball (blocked on zit#97)
zit#95 landed 'zit tarball', so quarry can offer what every forge does: this check-in as a file. An op, a download route with the same posture as attachments (always a download, never an inline render), and a link beside the clone box. It does not work yet, and the reason is upstream: zit tarball ignores -R and demands a checkout in the working directory, which a server process never has (zit#97). ls honours -R from the same directory against the same store; tarball does not. The route is correct and stays — it starts answering the moment the flag is honoured — and the failure is reported as unavailable rather than not-found, because the repository is there and the archive is what cannot be produced. Not worked around: 0001 §2 makes the CLI the whole store interface, and a checkout materialised per download to dodge the bug would defeat both that and the reproducibility the feature exists for. check-in: c81b757987 user: dmestas tags: trunk | |
| 15:52 |
web: the files page was unusably slow; fix the query, memoise, cap the list
Three separate faults, worst first. Branches() used a correlated subquery — one full scan of checkin per row. On tcl (42,074 check-ins, 1,855 branches) that took 434 SECONDS, which is what made the page look hung rather than slow. One grouped scan with SQLite's bare-column-with-max rule, plus an index on (branch, date): 0.042s. 'ls --long' costs ~4ms per file because it reads each blob for size and the binary flag, so tcl's 2,198-path tip took 8.5s. Fossil never pays this — browse.c builds a directory listing from its own indexed tables and shows no size at all — but quarry reads through the CLI by design, so the answer is to compute once: a check-in's file set is IMMUTABLE, so listings are memoised per version, the tip is warmed after each ingest, and the tree op resolves 'trunk' to its hash so the page asks for the key that was warmed. Only a hash may be cached. Caching 'trunk' would serve a stale tree forever after the next commit — a correctness bug dressed as a performance fix — so the memo refuses anything that can move, with a test pinning it. The branch selector rendered every branch: 1,855 <option> elements, 90% of a 239KB page, and unusable as a control. Now the 25 most recently active, plus the branch being viewed even when it is older. tcl's repo home: 8.5s -> 0.38s, 239KB -> 29KB. sqlite: 5.0s -> 0.32s. check-in: c754084dc7 user: dmestas tags: trunk | |