swarmops commit#
Record a changeset from a list of changes; with --advance, also move that ref to it.
- Group
write- Since
0.1.0- Mutates
- yes
- Identity required
- yes
- MCP tool
commit- Envelope op
commit- Exit codes
- 01245
Syntax#
swarmops commit [OPTIONS] --message <MESSAGE> --changes <FILE>Required arguments#
Syntax: -m, --message <MESSAGE>.
The commit message.
Syntax: --changes <FILE>.
A JSON file holding an array of changes, or - for stdin: at most 64 MiB.
Optional arguments#
The command also takes the global options.
Syntax: --parent <REV>.
The changeset to commit on top of (default: main's head, if main exists).
Syntax: --allow-empty.
Record the changeset even if it changes nothing.
Syntax: --advance <REF>.
Also move REF (main or release/<name>) to the new changeset, in the same transaction: both are recorded, or neither.
The move happens only if REF still names --parent (for a root commit, only if REF does not exist yet), so it is always a one-step fast-forward. Give --parent as the full changeset id you read REF at (from ref list, show, log or a commit's result): a ref name resolves when the command runs, and without --parent the commit builds on REF's head as it is then, so the move catches only a race from that moment on.
After E_CAS_MISMATCH nothing was recorded, neither the changeset nor the move: commit your change without --advance and land it (swarmops land merges it onto REF's head), or re-apply your edits to the content at the error's actual and commit with --parent <actual> --advance REF. Do not re-send your old whole-file puts: they would revert the winner's edits to shared files.
Syntax: --key <UUID>.
A uuid v7 (RFC 9562) that makes a retry of this commit safe: make a new one for each commit, and give it again only to retry that commit.
The commit signs at the key's time, to the second, and records the key as its commit_key metadata, so the same command run again on the same parent makes the same changeset, which the store holds already: the result says replayed: true, with the first run's changeset, and nothing is written. A commit whose parent is the changeset its key made (without --parent, main's head, or REF's with --advance) replays that changeset instead of committing it again on top of itself.
Give --parent as the full changeset id you read: without it the commit builds on main's head (REF's, with --advance) as it is when the command runs, so a retry after that head moved to another changeset commits again, on the new head.
Examples#
SWARMOPS_RUN_ID=run-1 swarmops --repo [tmp]/store --as agent/worker-17 commit -m 'first commit
with a body' --changes [tmp]/changes.json{
"ok": true,
"op": "commit",
"api": 1,
"result": {
"changeset": "[c1]",
"root": "[root1]",
"files_changed": 6,
"blobs_written": 6,
"replayed": false
},
"meta": {
"repo": "[tmp]/store",
"elapsed_ms": "[elapsed_ms]",
"resolved": {},
"auth": "trust"
}
}Exit code 0. Recorded by the CLI tests. A name in square brackets, such as [tmp] or [c1], stands for what differs from run to run: the test's temporary directory, an id, a time.
No recorded example yet.
Details#
commit records one changeset from an explicit list of changes, relative to a parent. There is
no working copy and no staging area: the change list is the whole input. Without
--advance it moves no ref: the change is published by landing it
(land) or by moving a ref to it (ref set).
Commit and land walks through the whole flow.
The parent#
The changeset is built on, in this order of precedence:
--parent REV, when given: any revision, resolved once, in the snapshot the commit reads in.- Else, with
--advance REF, REF's head as it is when the command runs, or nothing when REF does not exist (a root commit that creates REF). - Else
main's head as it is when the command runs, or nothing whenmaindoes not exist (a root commit).
A head taken by default is recorded in meta.resolved under the ref's name. Once main exists,
a root commit needs --advance naming a ref that does not exist yet.
Give --parent as the full id you read the content at. A head read at commit time is not
necessarily the one your edits were based on: another agent may have moved it in between.
The change list#
--changes FILE reads a JSON array of changes from FILE, or from stdin for -, at most 64 MiB
of text. Each change is:
{"path": "src/lib.rs", "op": "put", "content": {"inline": "fn main() {}\n"}, "mode": "file"}path: a repository path,/-separated and relative, with no empty,.or..component and no NUL. Each path may appear in one change only.op:putwrites the path, creating or replacing the entry;deleteremoves it and takes neither content nor mode.content, for aput, one of four forms:{"inline": text}(UTF-8, verbatim),{"base64": text}(standard, padded),{"file": path}(a file on this host, read by the command, a relative path taken from the current directory) or{"blob": id}(a blob the store holds already, fromblob put, by its 64 hex digits).mode:file(the default),execorsymlink, whose content is the link's target.
The whole list is checked before any content is decoded or read, and a refused list writes
nothing. Write a change list shows
renames, moves and the refusals in full; swarmops schema ChangeInput gives the schema.
Limits#
| What | Limit | Past it |
|---|---|---|
| The change list's text | 64 MiB | E_TOO_LARGE, details.scope request, before it is parsed |
| One put's content | 512 MiB | E_TOO_LARGE, scope put |
| A commit's contents together | 1 GiB | E_TOO_LARGE, scope commit |
Contents are measured by the sizes they declare (inline text's length, the bytes base64 decodes
to, a file's length) before any is read. A blob named by id counts toward none of them, so
large files go in with blob put first and are named by id.
What is recorded#
The changeset's author and committer are both your agent name, with the email
<instance>@<family>.swarmops.local, at the current time to the second, UTC. Its metadata holds
agent, run_id when you have one, and commit_key with --key. files_changed counts the
paths the changeset changes: a put of what a path holds already is no change. A list that
changes nothing is E_EMPTY_COMMIT (exit 1) unless --allow-empty is given.
--advance: commit and publish in one transaction#
With --advance REF, the changeset and REF's move to it are one write transaction, one journal
frame, where commit then ref set --expect take two. REF must be main or
release/<name>; any other name is E_REF_NAME_INVALID. The move is a compare-and-swap
against the parent: when REF names anything else, nothing is recorded, neither the changeset
nor the move, and the commit fails with E_CAS_MISMATCH (exit 4, retryable), its
details.actual naming what REF holds and details.committed false. The result's
advanced reports the move: name, old (left out when the move created REF), new and
already_applied.
--advance publishes without a merge: it suits a single writer of a ref, or a script that
re-applies its edits on a failure. With several agents writing at once, commit without it and
land, which merges onto whatever the ref holds by then (Publish
without a land). Through MCP,
advance needs parent as a full 64-hex id.
--key: a retry that is safe#
--key takes a uuid v7, any case; anything else is E_INVALID_ARG, before anything is read.
The commit then signs at the key's time instead of the clock's, and records the key, so the
same command run again on the same parent, by the same agent with the same run id, is the same
changeset: the second
run says replayed: true, with blobs_written 0, and writes nothing. A different agent with
the same key makes a different changeset, since the agent name is part of what is signed and
recorded. Make a
new key for each commit; Commit keys and
Idempotency keys give a generator.
Patterns#
- The two-step publish of an agent: read
main's id, commit on it with--parent, thenlandthe result with a land key (Publish a change). - A chain of commits, each with
--parentthe one before, landed at once by landing the last: the land squashes the chain (Land a chain of commits). - Generated content from a pipe:
--changes -reads the list from stdin. A change'sfilecontent may not then name this process's own stdin or stdout (E_INVALID_ARG): send such bytes inline or in base64.
Failures#
| Error | Exit | When |
|---|---|---|
E_IDENTITY_REQUIRED | 5 | Neither --as nor SWARMOPS_AGENT names an agent (whoami). |
E_INVALID_ARG | 2 | A malformed list, a path named twice, a bad key, a blob id the store holds no blob under, a --parent that is no revision. For a change, details.index and details.path name it. |
E_TOO_LARGE | 1 | A limit in the table above. |
E_EMPTY_COMMIT | 1 | The list changes nothing, without --allow-empty. |
E_TYPE_CONFLICT | 1 | A change puts an entry below a file, or a file onto a directory. |
E_REV_NOT_FOUND | 6 | --parent names a ref or id the store does not hold. |
E_CAS_MISMATCH | 4 | With --advance, REF no longer names the parent. Nothing was recorded. |
E_BUSY | 4 | Another writer held the store's lock for the whole wait. Nothing was committed: run it again. |
Exit 4 is retryable; Retry rules say how. After
E_CAS_MISMATCH, do not re-send your old whole-file puts on top of actual: they would revert
what the winner changed in the same files.
Performance#
On the reference host of the v0.1.0
measurement, one commit alone
took 19 ms (meta.elapsed_ms, p50); the M1 runs
found nearly all of an uncontended commit's time in its fsyncs. 32 processes committing at once
through the binary made 71.7 commits/s together, a commit taking 433 ms at the median and
621 ms at p99, mostly its wait for the store's lock, with no E_BUSY. What --advance saves
under contention is not measured yet (publishes under 32
writers).
Return information#
result is a CommitResp (swarmops schema CommitResp prints its JSON Schema).
| Field | Type | Required | Description |
|---|---|---|---|
advanced | Advanced or null | no | The move of the ref the request's advance named: present exactly when the ref names changeset because of this request, or of an identical one before it (already_applied). A request with advance whose response lacks it went to a store that ignores the field, and moved no ref. |
blobs_written | integer | yes | How many blobs this commit's own writes placed in the object store; blobs that were there already, or that a concurrent writer placed first, count 0. |
changeset | string | yes | The new changeset's id. |
files_changed | integer | yes | How many paths the changeset changes. |
parent | string or null | no | The changeset it was committed on top of; omitted for a root commit. |
replayed | boolean | no | The store held the changeset already, from an identical request before (a retry with the same idempotency_key), so nothing was written. Always written; a response from a store that does not know the field reads as false. |
root | string | yes | The new root tree's id. |
warnings | array of string | no | Problems found after the commit became durable, such as failed housekeeping; the commit itself succeeded. Omitted when empty. |
Envelope op: commit.
Exit codes:
0: Success.1: The operation failed.2: The request or the command line is malformed.4: The operation can succeed when it is run again.5: An identity or policy requirement refused the operation.
Errors#
- E_IDENTITY_REQUIRED (exit 5): A mutation was asked for without an identity.
- E_TOO_LARGE (exit 1): The content is larger than the operation allows.
- E_EMPTY_COMMIT (exit 1): The commit changes nothing and
allow_emptyis off. - E_TYPE_CONFLICT (exit 1): A change puts an entry below a file, or a file onto a directory.
- E_CAS_MISMATCH (exit 4): The ref no longer holds the expected target (or, with
create, exists at another one);details.actualholds what it does,nullwhen the ref does not exist. - E_INVALID_ARG (exit 2): A malformed argument or request, including a command-line usage error.
Any command that opens a store may also fail with a store error, such as E_NOT_A_REPO, E_IO or E_STORE_CORRUPT; a command line the grammar refuses is E_INVALID_ARG (exit 2).