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.
- 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--basedefaults 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:
landedholds 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_fromlists the chain, oldest first. - The changeset's author is HEAD's author and its committer is you. Its message is HEAD's unless
-mgives one. Its metadata keeps HEAD'sagentandrun_id, and addslander(your agent),lander_run_id(your run id, when you have one),land_key(with--key) andsquashed_from.showprints them. - The target's move is recorded for your agent;
op loglists the land as one operation, andop undocan 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) andtheirs(the target's head it was merged with), by full id;- each conflicting file: its
path, itskind, 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,modeortype_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 finds | Result |
|---|---|
| A changeset squashed from this HEAD | status: replayed, the first land's result, nothing written. |
| A changeset squashed from another head | E_INVALID_ARG (exit 2), details.key and details.landed: the key was used for another change. |
| None | The 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 asLandReq(swarmops schema LandReq); thereresolutions.againstmust 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 eachE_INVALID_ARG(exit 2). --max-attemptsoutside 1 to 32 isE_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).
| Field | Type | Required | Description |
|---|---|---|---|
attempts | integer | yes | How many times the land tried to publish. |
auto_merged | array of string | no | The paths both sides changed that merged cleanly. Omitted when empty. |
base | string | yes | The changeset the change was taken from: what head's first-parent chain added after it is what landed. |
files_changed | integer | yes | How many paths the landed changeset changes. |
landed | string | yes | The landed changeset: the change squashed into one changeset on the target. |
rebased_onto | string | yes | The target's head the change landed on: landed's parent. |
relocated | boolean | yes | The target moved after the land read it, and the publish put the change on the new head itself, without another attempt. |
squashed_from | array of string | yes | The changesets the land squashed, oldest first: head's first-parent chain after base, ending at head. |
status | LandStatus | yes | landed, or replayed when a land with the same idempotency_key had landed the change already. |
target | string | yes | The ref it landed on. |
warnings | array of string | no | Problems 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.reportlists 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.reasonsays 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).