SwarmOps
ReferenceCommandsswarmops land
View as Markdown

swarmops land#

Merge a change three-way onto a ref and publish it there as one new changeset.

The change is HEAD with its first-parent chain since the base, squashed: the new changeset's parent is the ref's head, it holds what the chain changed merged with what the ref gained since the base, and the ref moves to it in the same transaction. A ref that moves meanwhile makes the land merge again onto its new head and retry, up to --max-attempts times. A conflict is E_CONFLICT (exit 3), with each conflicting file in details.report, and nothing is recorded; land again with --resolve to resolve it without a checkout.

Metadata
Group
write
Since
0.1.0
Mutates
yes
Identity required
yes
MCP tool
land
Envelope op
land
Exit codes
013

Syntax#

swarmops land [OPTIONS] <HEAD>

Required arguments#

Syntax: <HEAD>.

The changeset to land: the change's head, such as the changeset a commit made.

Optional arguments#

The command also takes the global options.

Syntax: --base <REV>.

Where the change starts: a first-parent ancestor of HEAD, whose own changes are not landed (default: the merge base of HEAD and the ref's head, along first parents).

Syntax: --target <REF>. Default: main.

The ref to land on: main or release/<name>.

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

The new changeset's message (default: HEAD's).

Syntax: --key <KEY>.

Makes a retry of this land safe: 1 to 64 letters, digits and '-', such as a uuid v7. Make a new one for each change you land, and give it again only to retry that land.

A land with a key first looks down the ref's first parents, to the change's base, for a changeset landed with the same key: one squashed from this HEAD is the land made before, which the land replays (status: replayed) without writing anything; one from another head is E_INVALID_ARG. Without a key, the same land run again finds its change on the ref already: E_NOTHING_TO_LAND.

Syntax: --max-attempts <N>. Default: 8.

How many times to try to publish before E_CAS_RETRY_EXHAUSTED: 1 to 32.

Syntax: --max-conflict-bytes <N>. Default: 32768.

The most bytes of hunk text an E_CONFLICT report carries; past them it leaves the texts out (truncated) and keeps every line position and blob id.

Syntax: --resolve <FILE>.

Resolve the conflicts an E_CONFLICT report named, without a checkout: resolutions as a JSON object in FILE, or - for stdin, at most 64 MiB (swarmops schema ResolutionsInput).

{"against": THEIRS, "files": {PATH: RESOLUTION, ...}}: THEIRS is the report's theirs, by its full id, and each RESOLUTION one of {"take": "ours"}, {"take": "theirs"}, {"content": CONTENT} with an optional "mode" (CONTENT as a commit's change gives it: inline, base64, a file, or a stored blob by id) and {"delete": true}. A resolution decides its path and every path below it; the rest of the merge stands.

A resolution applies while the ref holds its path as THEIRS did, whether the path conflicts or not. A path the ref changed since is stale: the land fails with E_CONFLICT again, its report naming the path in stale, as it does for each conflict left unresolved. Read the report's theirs, and resolve against it.

Examples#

swarmops --repo [tmp]/store --as agent/worker-17 land [head] --key land-7
{
  "ok": true,
  "op": "land",
  "api": 1,
  "result": {
    "status": "landed",
    "landed": "[landed]",
    "rebased_onto": "[theirs]",
    "target": "main",
    "base": "[t0]",
    "attempts": 1,
    "relocated": false,
    "files_changed": 1,
    "squashed_from": [
      "[head]"
    ]
  },
  "meta": {
    "repo": "[tmp]/store",
    "elapsed_ms": "[elapsed_ms]",
    "resolved": {
      "[head]": "[head]",
      "main": "[theirs]"
    },
    "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#

land is how agents publish to a shared ref. It takes a change, merges it three-way onto whatever the target ref holds now, and publishes the result there as one new changeset, the ref's move with it, in one transaction. Nothing is recorded unless the whole land succeeds. Commit and land is the guide; this section is the command's behaviour in full.

What a land publishes#

The change is HEAD with its first-parent chain after the base:

        base ── c1 ── c2 ── HEAD         the change: c1, c2 and HEAD
          │
  main: base ── t1 ── t2                 what main gained since the base
                       │
                       └── landed        one new changeset, parent t2
  • --base defaults to the merge base of HEAD and the target's head, along first parents. Given, it must be a first-parent ancestor of HEAD; it need not be on the target.
  • The chain is squashed: landed holds what the chain changed merged with what the target gained since the base, its parent is the target's head (rebased_onto), and the target stays linear. squashed_from lists the chain, oldest first.
  • The changeset's author is HEAD's author and its committer is you. Its message is HEAD's unless -m gives one. Its metadata keeps HEAD's agent and run_id, and adds lander (your agent), lander_run_id (your run id, when you have one), land_key (with --key) and squashed_from. show prints them.
  • The target's move is recorded for your agent; op log lists the land as one operation, and op undo can take it back.

The target#

--target is main (the default) or release/<name>, and it must exist: a name of another form is E_REF_NAME_INVALID (exit 1), and a ref that does not exist is E_REV_NOT_FOUND (exit 6). Land on a release ref shows a release ref.

When the target moves meanwhile#

A land reads the target, merges, and publishes by compare-and-swap. When another writer moved the target in between, the land merges again onto the new head and tries again, up to --max-attempts times (8 by default, 1 to 32). attempts says how many it took. relocated says the publish found the target moved and put the change on the new head itself, without another attempt. When every attempt loses, the land fails with E_CAS_RETRY_EXHAUSTED (exit 4, retryable), details.attempts and details.actual set, and nothing recorded: run the same land again. Two agents landing at once shows the race.

Conflicts#

A change that does not merge is E_CONFLICT (exit 3), and nothing is recorded. The target is as the other writers left it. details.report names:

  • base, ours (your change's head) and theirs (the target's head it was merged with), by full id;
  • each conflicting file: its path, its kind, the blob ids of the sides that hold it, its hunks (each side's lines, numbered from 1, both ends included), and the target's changeset and agent that last changed it (theirs_changeset, theirs_agent);
  • kinds: content (the same or adjacent lines of a text file), binary, delete_modify, modify_delete, add_add, mode or type_change;
  • auto_merged, the paths both sides changed that merged cleanly.

Hunks carry their texts until they pass --max-conflict-bytes (32,768 by default); past it the report sets truncated and leaves every text out, keeping the line positions and the blob ids, from which cat reads any side. With --max-conflict-bytes 0 a report carries no hunk text. The error's hint is the cat of the first conflicting path at theirs, then the land again with --resolve.

Resolving without a checkout#

--resolve FILE (or - for stdin) lands again with a decision for each conflicting path:

{"against": "<the report's theirs, full id>",
 "files": {"src/lib.rs": {"take": "theirs"},
           "src/new.rs": {"content": {"inline": "pub fn hi() {}\n"}},
           "docs/old.md": {"delete": true}}}

Each path takes exactly one of take (ours or theirs), content (with an optional mode, the content in any form a commit's change takes) or delete, and decides that path and every path below it. A resolution applies while the target holds its path as against did. When the target changed the path again in the meantime, the land fails with a fresh E_CONFLICT whose report names the path in stale: read the new theirs, and resolve against it. A conflict left without a resolution fails the land the same way. Resolve a conflict walks through one.

--key: a retry that is safe#

A land key is 1 to 64 letters, digits and -, such as a uuid v7; anything else is E_INVALID_ARG, before anything is read. With a key, the land first looks down the target's first parents, to the change's base, for a changeset landed with the same key:

What it findsResult
A changeset squashed from this HEADstatus: replayed, the first land's result, nothing written.
A changeset squashed from another headE_INVALID_ARG (exit 2), details.key and details.landed: the key was used for another change.
NoneThe land runs.

Without a key, a land run again after it succeeded fails with E_NOTHING_TO_LAND (exit 1), details.reason no_changes. A HEAD that is on the target already fails with reason on_target. Make a new key for each change you land, and give it again only to retry that land (Land keys).

Patterns#

  • Many agents, one trunk: each agent commits its change on the head it read, with --parent, and lands it with a key. Landings are serialised by the target's compare-and-swap; a non-conflicting race costs a re-merge, not a failure.
  • On E_CONFLICT, an agent reads the report, decides each path, and lands again with --resolve; nothing needs to be undone, since nothing was recorded.
  • On exit 4, run the same command again with the same key.
  • Through MCP the tool is land, taking the same fields as LandReq (swarmops schema LandReq); there resolutions.against must be a full 64-hex id (Landing through MCP).

Other failures#

  • A base that is not a first-parent ancestor of HEAD, a HEAD and target that share no first-parent ancestor (give --base), and a chain that holds a merge changeset (more than one parent) are each E_INVALID_ARG (exit 2).
  • --max-attempts outside 1 to 32 is E_INVALID_ARG, before anything is read.
  • No identity is E_IDENTITY_REQUIRED (exit 5).
  • E_BUSY (exit 4): another writer held the store's lock for the whole wait; nothing was recorded.

Performance#

A lost race re-merges only the paths the change touched onto what the target gained, rather than the whole change from its base. Its cost is not measured yet (a land's incremental re-merge).

Return information#

result is a LandResp (swarmops schema LandResp prints its JSON Schema).

FieldTypeRequiredDescription
attemptsintegeryesHow many times the land tried to publish.
auto_mergedarray of stringnoThe paths both sides changed that merged cleanly. Omitted when empty.
basestringyesThe changeset the change was taken from: what head's first-parent chain added after it is what landed.
files_changedintegeryesHow many paths the landed changeset changes.
landedstringyesThe landed changeset: the change squashed into one changeset on the target.
rebased_ontostringyesThe target's head the change landed on: landed's parent.
relocatedbooleanyesThe target moved after the land read it, and the publish put the change on the new head itself, without another attempt.
squashed_fromarray of stringyesThe changesets the land squashed, oldest first: head's first-parent chain after base, ending at head.
statusLandStatusyeslanded, or replayed when a land with the same idempotency_key had landed the change already.
targetstringyesThe ref it landed on.
warningsarray of stringnoProblems found after the land became durable, such as failed housekeeping; the land itself succeeded. Omitted when empty.

Envelope op: land.

Exit codes:

  • 0: Success.
  • 1: The operation failed.
  • 3: The change conflicts with what it lands on: resolve it, then run the operation again.

Errors#

  • E_CONFLICT (exit 3): The change conflicts with the target: details.report lists each conflicting file with its kind, its hunks and who changed it on the target.
  • E_NOTHING_TO_LAND (exit 1): The land has nothing to add to the target: its head is on the target already, or its change is (details.reason says which).

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