SwarmOps
ReferenceCommandsswarmops commit
View as Markdown

swarmops commit#

Record a changeset from a list of changes; with --advance, also move that ref to it.

Metadata
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:

  1. --parent REV, when given: any revision, resolved once, in the snapshot the commit reads in.
  2. 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).
  3. Else main's head as it is when the command runs, or nothing when main does 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: put writes the path, creating or replacing the entry; delete removes it and takes neither content nor mode.
  • content, for a put, 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, from blob put, by its 64 hex digits).
  • mode: file (the default), exec or symlink, 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#

WhatLimitPast it
The change list's text64 MiBE_TOO_LARGE, details.scope request, before it is parsed
One put's content512 MiBE_TOO_LARGE, scope put
A commit's contents together1 GiBE_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, then land the result with a land key (Publish a change).
  • A chain of commits, each with --parent the 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's file content may not then name this process's own stdin or stdout (E_INVALID_ARG): send such bytes inline or in base64.

Failures#

ErrorExitWhen
E_IDENTITY_REQUIRED5Neither --as nor SWARMOPS_AGENT names an agent (whoami).
E_INVALID_ARG2A 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_LARGE1A limit in the table above.
E_EMPTY_COMMIT1The list changes nothing, without --allow-empty.
E_TYPE_CONFLICT1A change puts an entry below a file, or a file onto a directory.
E_REV_NOT_FOUND6--parent names a ref or id the store does not hold.
E_CAS_MISMATCH4With --advance, REF no longer names the parent. Nothing was recorded.
E_BUSY4Another 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).

FieldTypeRequiredDescription
advancedAdvanced or nullnoThe 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_writtenintegeryesHow 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.
changesetstringyesThe new changeset's id.
files_changedintegeryesHow many paths the changeset changes.
parentstring or nullnoThe changeset it was committed on top of; omitted for a root commit.
replayedbooleannoThe 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.
rootstringyesThe new root tree's id.
warningsarray of stringnoProblems 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_empty is 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.actual holds what it does, null when 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).

See also#

Edit on GitHub

On this page