Skip to content
zit Artifact [fb1e38881b]
Login

Artifact [fb1e38881b]

Artifact fb1e38881b985bcf378f911d996589a1fbd31f56f14e6999fffc3712948937c4:

Instance of technote [b7190dfc66] - Attachments, written where they are read by dmestas on 2026-09-11 15:40:00.
C Attachments,\swritten\swhere\sthey\sare\sread
D 2026-09-11T20:39:24.343
E 2026-09-11T15:40:00 b7190dfc66f324281aae7254e391644fb2452be7
N text/x-markdown
U dmestas
W 4666
# Attachments, written where they are read

zit has been able to *read* an attachment for a long time. The A card
folds, `zit artifact` describes it, a clone carries it, and a fossil-hosted
project's screenshots and patches come across a sync intact. What zit could
not do was write one — and every other thing quarry authors goes through a
zit verb. `ticket new`, `wiki commit`, `forum reply`, `technote add`: the
artifact format is written in one place, by one writer, and read by fossil
on the other side. Attachments were the exception, and an exception in a
format writer is where the second implementation starts.

0.3.13 closes it.

## The verbs

```
zit attach add  TARGET FILE [--name NAME] [--comment TEXT]
                [--mimetype TYPE] [--user NAME] [-R repo]
zit attach rm   TARGET NAME [--user NAME] [-R repo]
zit attach list [TARGET] [-R repo]
```

`add` stores the file as a blob, then writes the attachment artifact —
one `A NAME TARGET SOURCE` card, a comment if you gave one, the writer
clock, a mimetype if you gave one, the author, the checksum — and prints
the artifact's hash and the blob's on two lines. The two writes happen in
**one commit**, which is the detail worth noticing: there is no moment at
which the store holds an attachment naming bytes it does not have, and no
crash that leaves one behind.

`TARGET` is what fossil accepts: a ticket, technote, or forum post's hash,
or a wiki page's name. It is checked for shape and nothing more, because
that is what the format says — an unresolved target is recorded as a
generic reference, and a verb that refused to attach to a ticket it could
not find would be inventing a rule fossil does not have.

`rm` writes an A card with no source. Nothing leaves the store; the
association ends under the format's latest-wins rule, and `list` — which
folds the newest card per name — stops showing it. `list` prints
`NAME SIZE MIMETYPE BLOB DATE USER`, with the target as a first column when
you ask for the whole repository, which is what an export or an audit walks.

## The cap, and why it is lower than fossil's

Fossil has a setting called `attachment-size-limit`: a byte count checked
before the blob is stored, `0` meaning unlimited, and unlimited is its
default. zit honours the same setting the same way, with one deliberate
difference — **unset, the limit is 1 MiB.**

A blob in the DAG travels to every clone, for ever. Fossil's default is
tuned for a project whose attachments are screenshots and patches, uploaded
by people who know what a repository is. quarry is a forge, and a forge's
first-time user will drag a 400 MB screen recording onto a ticket. The
right default is the one that says no to that, names the size and the
limit, and tells you how to lift it: `zit settings attachment-size-limit 0`.

The setting lives in the repository's config frame under a new key. A zit
that predates it reads the repository without complaint and simply sees the
setting as unset.

## What quarry attaches instead

quarry will never write a file's bytes into a hosted repository through
this verb. It attaches a **stub** — a small text blob that says where the
bytes are:

```
quarry-attachment 1
sha256 4f9a…e01c
size 18349022
type video/mp4
name screen-recording.mp4
```

No URL. The bytes are found by content hash in whatever storage the
repository's steward bound in quarry; the A card stays a valid fossil
artifact; the store stays small; nothing is a phantom on sync. zit now
recognises the shape: `attach list` shows such an attachment as
`external:sha256:…` with the size and type it points at, and `zit artifact`
on the blob says `attachment-stub`. A client that does not recognise it
sees a small text file under the attachment's name — degraded, but honest.
The size cap applies to what is actually stored, so a stub always passes.
Nothing on the write side changed; quarry writes the stub through the same
`attach add`, from stdin.

## Proven against fossil

The claim that matters is not that zit writes an A card, but that fossil
reads it as an attachment. So the smoke suite now authors one with zit,
pushes it to a real `fossil server`, and asks fossil's own `attachment`
table: one live row for the target and name zit recorded, with zit's blob
as its source, and the timeline entry fossil renders — *Add attachment
[note.txt] to wiki page [Home]*. That check runs on every qualification.

The first run of it failed, which is worth saying because of *how*: the
row was there, exactly right, and my query had forgotten to name the
repository. The interop was never in doubt once the data was looked at.
It was the harness that needed the fix.

Z e84fcafc1c5df8d19582c56ab21a6305