Skip to content

Latest commit

 

History

History
294 lines (216 loc) · 14.5 KB

commit.md

File metadata and controls

294 lines (216 loc) · 14.5 KB

swarmops commit

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

title: "swarmops commit"
description: "Record a changeset from a list of changes; with --advance, also move that ref to it."
group: "write"
since: "0.1.0"
mutates: true
identity_required: true
mcp_tool: "commit"
envelope_op: "commit"
exit_codes: [0, 1, 2, 4, 5]
errors: [E_IDENTITY_REQUIRED, E_TOO_LARGE, E_EMPTY_COMMIT, E_TYPE_CONFLICT, E_CAS_MISMATCH, E_INVALID_ARG]
syntax: "swarmops commit [OPTIONS] --message <MESSAGE> --changes <FILE>"

Syntax

swarmops commit [OPTIONS] --message <MESSAGE> --changes <FILE>

Required arguments

--message

Syntax: -m, --message <MESSAGE>.

The commit message.

--changes

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.

--parent

Syntax: --parent <REV>.

The changeset to commit on top of (default: main's head, if main exists).

--allow-empty

Syntax: --allow-empty.

Record the changeset even if it changes nothing.

--advance

Syntax: --advance <REF>.

Also move REF (main or release/<name>, and in a FORMAT 2 store a branch such as feature/x; never a tag) 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.

--key

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

CLI

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.

MCP

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

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, 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

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_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