web: rename a database from its settings page
A database's name lives in two places — the metadata row and the on-disk
store directory its path names — so a rename has to move both. The order
is creation's: the row first, where a name already taken is refused by the
unique index before anything on disk changes, then the store, then the
handle the remotesapi memoized under the old path. A store that will not
move rolls the row back, so the two halves never disagree about where a
database lives; only a failed rollback is escalated to a human, because
it is the one outcome no later request can repair.
storage.MoveStore is one os.Rename behind the containment guard DeleteStore
already used, extracted here as containedPath. It refuses an occupied
destination outright: os.Rename over an empty directory would succeed and
swallow it.
No redirect is left behind — the old address simply stops resolving, as on
git.sr.ht — so an existing clone needs its remote replaced, and a companion
of a git repository is re-provisioned under its old name by the next push
to that repository. The README's quickstart now says both.
storage: create databases empty so the first push needs no --force
Every automatic creation path wrote an "Initialize data repository"
commit through WriteEmptyRepo, and that commit is history. dolt decides
fast-forward on the client (actions.CanFastForward over the remotesapi),
so the server cannot forgive the collision: pushing a database that has a
root commit of its own — a beads tracker, anything grown locally — was
rejected as a non-fast-forward and could only land with --force. That is
the whole reason the companion-database recipe starts with a forced push.
push-to-create already provisioned an empty store for this exact reason.
Give the other two paths the same default: /internal/repos, whose caller
is git.sr.ht's post-update hook and therefore fires before its user has
ever pushed, now always provisions empty, and the web form does unless
its new "initialize with an empty commit" checkbox is ticked. The
checkbox buys what an empty store cannot offer — a database that can be
cloned before anything is pushed to it, since dolt refuses a store with
no commits as "contains no Dolt data".
Which is also why the overview of a database with no branches now teaches
push rather than clone: the clone box there quoted a command that could
not work. A store that fails to open is deliberately not treated as
empty — an unreadable database must not be advertised as a fresh one.
remoteapi: prove an empty store takes an unrelated first push
This file had not compiled since the logrus->slog migration removed the
package-level `logger` its serve goroutines still called. The
`integration` build tag keeps it out of every default build, test and vet
run, so nothing ever said so — the suite simply never ran again.
With it compiling, add the scenario the storage layer's contract is
actually about: a database provisioned empty ahead of its first push (the
web create form, or git.sr.ht's hook through /internal/repos) accepts a
push from a local database with a root commit of its own, with no
--force. Mutating the provisioning to InitStore fails it exactly as a
user hits it today: "unknown push error; no common ancestor".
ci: publish the apk into artifacts.sr.ht as well
The abuild output goes on to the S3 bucket phoebe re-indexes, and now also to
the ~bigbes/main channel of artifacts.sr.ht, which indexes and signs it in the
same request. One PUT per file with the shared working token; a 409 says the
job ran twice and is a warning rather than a failure.
beads: show the stored rows behind an issue on its detail pane
Every field on the detail pane is a reading of a row: the Issue struct names
the columns bd surfaces and drops the rest, humanizeEvent turns two JSON blobs
into a sentence, an Edge keeps an id and a type out of a dependency row. A
reading that went wrong looks exactly like one that did not, so the detail modes
now carry the rows they were built from — the issue's own row first, then the
rows belonging to it in labels, dependencies, comments and events, in the order
the tables were read and the columns came back in. custom_statuses is read for
the lane and is deliberately absent: its rows describe the tracker's statuses,
not this issue. Related tables are capped at RawMax rows and counted whole, so a
busy issue's audit log cannot turn the pane into a second page.
A raw view may not report an absent cell and an empty string as the same thing,
which is the one place cell()'s flattening of a real NULL to "" is wrong.
rowCells.isNull is now the single site that decides, shared by cell and the raw
row, so the two can never disagree; RawCell carries Null beside Value and drops
browse's "NULL" text, which would have put the collision straight back.
The section renders as a collapsed <details> in the stream layout's Past Stand
idiom — it is a check on the rendering, not the reason a reader came. Values are
escaped and deliberately not run through beadLinks.Text: a linkifier rewrites
what it is given, and a value that has been rewritten is no longer the value
that is stored. A cell holding no value renders as a NULL chip, a cell storing
those four characters as the text it stores, an empty one as (empty); a long
value scrolls inside its own cell.
mcpsrv: list_issues names every clipped table it drew from
Data.Truncated/ShownOf only ever meant the issues/dependencies read that
decides list_issues' total and truncated, so a clipped labels table took
label pills off every card and narrowed the label filter in silence, and a
clipped custom_statuses table could put a card in the wrong lane with
table_truncated still reading false.
list_issues now carries beads.Data.Clipped as clipped: one entry per
clipped table in read order, named, with the rows read against the rows
that exist and what that specific clip costs this listing. table_truncated
and table_total are unchanged; clipped is additive, an empty list on a
complete read.
web: the milestones page says when its rollup is partial
MilestoneView has carried the clip since ebbd30f and list_milestones has
reported it since a419499; milestones.html rendered nothing, so the page
was the last surface presenting a partial rollup as whole-tracker
arithmetic. Every number on it is a rollup — "1/4 done", "1995 of 2000
issues carry no milestone label" — and arithmetic over the rows that were
read renders exactly like arithmetic over the tracker.
The notice is the detail pane's, word for word and class for class: it is
the same fact about the same read, and a second phrasing for it would be
a second style. It sits above the rollup and outside the
milestones/no-milestones branch, because "No milestones." over a clipped
read is the reading most easily mistaken for a fact about the tracker.
A milestone can lose members here rather than merely undercount them —
membership comes from labels — so the fixture makes that concrete: m1 has
five members, one of them past the cap, and the page counts four while
saying why.
beads: the board names every clipped table it draws from
The board raised its truncation flag for a clipped issues or dependencies
table and for nothing else, so a clipped labels table took the pills off
every card and the options out of the label filter in silence, and a
clipped custom_statuses table could put a card in the wrong lane with the
counts still reading as facts.
The flag could not grow to hold that. It is paired with a count line —
"the first N of M issues" — so it can only mean the reads that decide N
and M; widening it would have made a bool that means four different
things and a number that no longer follows from it. Data.Truncated and
ShownOf are therefore byte-for-byte what they were (mcpsrv's list_issues
reads them as they are), and the wider fact is a new field: Clipped, one
entry per clipped table the board reads, in read order, each naming the
table, the rows read against the rows that exist, and what the board lost
by the rest.
The banner follows. The count line now prints only when the issues read
was actually short — "the first 4 of 4 issues" was a wrong sentence, not
a warning — and every other clipped table gets its own line beneath it.
The detail pane keeps its single sentence from 82d997d; a pane that never
drew a lane has no use for a board's list.
TestBoardTruncationIsUnchanged is now TestBoardTruncationFlagIsUnchanged:
the flag is what it was, which is the claim that survives. Its "comments
does not flip the board" subtest survives as "a clip in a table the board
does not read" and now also asserts comments reaches neither the flag nor
the list, plus a new subtest pinning that labels and custom_statuses are
reported without touching the flag.
docs: spell out the clipped-read answers
web: the detail pane says when its read was clipped
The board has always carried a truncation banner; the detail pane it
shares a template with carried nothing, so an issue assembled from
tables clipped at 2000 rows looked complete, and an id in the tail of a
big tracker was answered "Issue not found." — which the page cannot
know.
Two lines, both in the banner's idiom. A pane built from a clipped read
says so and names the tracker's true size when the issues table itself
was cut; a miss over such a read says the id was not among the rows
read, which is not the same as saying it does not exist. A complete read
keeps the plain "Issue not found.", and the board's own banner is
untouched.
mcpsrv: report the projection's clip on get_issue and list_milestones
Max clips every table read at 2000 rows. list_issues has always said so
(table_truncated, table_total); the other two answers computed over the
same read said nothing, so a rollup over a prefix of the tracker read as
arithmetic about the whole of it, and an id in the tail of a big tracker
was answered "no such issue" — a claim neither read can support.
get_issue now carries the two fields on every answer and splits the
miss: a complete read keeps the plain refusal, a clipped one says the id
was not among the rows read, names the cap and the true total, and ships
the payload (issue: null beside table_truncated) so the two misses are
one field apart rather than one adjective apart. list_milestones carries
the same two fields.
beads: report a clipped read from the remaining projections
docs: read_rows answers strings or null
beads: stop flattening a stored NULL string
mcpsrv: answer a real NULL as null
web: stop printing store paths to the owner
web: stop printing browse errors to the reader
The overview rendered the browse layer's own error text into the page, under
"Could not read history: " — dolt internals and the store's path on our disk,
which nothing else on this surface discloses and which a reader can do nothing
with.
The page now carries a fixed sentence and the reason goes to the log with the
database id (slog + scribe.Err). The view field is a bool rather than a message,
so no error string can reach the template by being assigned to it later. The
empty-database state the page already reported ("No commits.") is untouched:
that is a fact about the database, not a failure.
web: tell a database outage from a missing database
loadRepoForBrowse turned any error from GetRepoByOwnerAndName into a 404, so a
metadata store that could not answer told every reader on the instance that
their database does not exist — for as long as Postgres was down, in the voice
reserved for "there is nothing here". loadRepoForAdmin did the same on the
settings page.
Both classify through repoLookupFailed now: db.ErrNotFound keeps the 404, and
it stays the same 404 the visibility rule renders for a PRIVATE database the
caller may not see, so a masked database and a missing one remain
indistinguishable. Anything else is a 500 through the shared error page, with
the cause logged (slog + scribe.Err) and never rendered.
beads: report a clipped read from every projection
mcpsrv: name the cursor a commit-log miss was asked about