swarmops git export#
Send the store's history to git: every commit of --ref's history that the last export into --branch does not reach is written into the git repository at PATH, parents first, made durable, and --branch moves to the export of --ref's head, created if it does not exist.
The repository is written on this host, at PATH, relative to the working directory: a bare repository (git init --bare makes one), or a work tree, which holds .git and whose checked-out branch is refused (E_INVALID_ARG). A changeset imported from git exports to the commit it came from, byte for byte; one made in the store exports to a commit with its own signatures and SwarmOps-* trailers, the same commit every time. Run again, it writes only what the branch does not reach, and nothing when the branch is at the export of --ref's head. It needs no identity.
A branch at a commit that is no export of --ref's head or of an ancestor of it fails with E_GIT_EXPORT_DIVERGED (exit 3), details.reason saying why, and nothing is written; --force moves it all the same, still by compare-and-swap. Then git push sends the branch on.
- Group
git- Since
0.1.0- Mutates
- no
- Identity required
- no
- MCP tool
- none
- Envelope op
git_export- Exit codes
- 013
Syntax#
swarmops git export [OPTIONS] <PATH>Required arguments#
Syntax: <PATH>.
The git repository's directory.
Optional arguments#
The command also takes the global options.
Syntax: --ref <REF>.
Default: main.
The store ref whose history to export: main or release/<name>.
Syntax: --branch <BRANCH>.
Default: main.
The git branch to write, refs/heads/<BRANCH>: main, or a trunk named otherwise, such as stable.
Syntax: --force.
Move a branch that diverged all the same, replacing what git holds there (by compare-and-swap; never a checked-out branch).
Examples#
swarmops --repo [tmp]/store git export [git]{
"ok": true,
"op": "git_export",
"api": 1,
"result": {
"exported": 1,
"objects_written": 3,
"head_changeset": "[c1]",
"head_sha": "[id2]",
"branch": "main",
"branch_moved": true,
"incremental": false,
"rewritten": 0
},
"meta": {
"repo": "[tmp]/store",
"elapsed_ms": "[elapsed_ms]",
"resolved": {
"main": "[c1]"
},
"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.
Details#
What a run does#
git export is a client-side command: it reads the store that --repo (else SWARMOPS_REPO)
names and writes a git repository on this host. No server ever sees the request, and there is no
MCP tool for it. A run goes in this order, each step done before anything of the next is written:
- Check the names.
--refmust be a ref name of the store (mainorrelease/<name>), checked before the repository is opened. The repository is opened as the import opens one (isolated from global configuration andGIT_*variables, the same repositories refused), and--branchmust be a branch name git takes. - Refuse a checked-out branch. In a work tree, the branch its
HEADnames is refused withE_INVALID_ARG(exit 2,details.reasonchecked_out), with or without--force, before the store is read. - Read the head. The head of
--refis read in a committed snapshot, taken under the store's writer lock after the journal's tail check, so that nothing the export sends off the host can be taken back by a power loss or a failed sync. - Derive the export ids. The commit id each changeset exports to is derived from the store's own objects, for the head and the whole history below it.
- Compare with the branch. A branch at the head's export is up to date, and nothing is
written. A branch that exists must name the export of the head or of an ancestor of it, the
base; anything else is
E_GIT_EXPORT_DIVERGED, unless--force. - Write. The commits the branch does not reach, parents first; for each, the trees and blobs its first parent does not hold at the same path, then the commit. An object the repository holds in a pack is trusted; a loose one is read back, and written again if it is damaged.
- Make it durable. Every object written or read back is fsynced.
- Record. The ids of the commits made in the store go into the store's external id index, each naming its changeset.
- Move the branch. The branch moves to the head's export by compare-and-swap, through git's
own lock file
refs/heads/<branch>.lock, or is created.
What the store records#
Nothing in its history. An export writes no journal frame, so it needs no identity, does not show
in op log, and the page's metadata says mutates: false. Its writes to the store are two
caches, the derived values and the external id entries. meta.resolved names the head the source
ref held, under the ref's name, on success and on a divergence; meta.recovered says what the
snapshot's tail check repaired, on success only.
Choose the source: --ref#
The store ref whose history goes out, main by default, or release/<name>. A ref the store does
not have fails with E_REV_NOT_FOUND (exit 6), whose hint lists the refs; a name that is no ref
name, such as feature/x, with E_REF_NAME_INVALID (exit 1).
Choose the branch: --branch#
The git branch written, refs/heads/<BRANCH>, main by default; name it after the remote's trunk
(--branch stable), or after a branch that waits for review (--branch review). A branch that
does not exist is created, and incremental is false. A name git does not take, such as a..b,
fails with E_REF_NAME_INVALID, whose hint is git check-ref-format --branch <name>.
The repository must exist already: a path with no repository is E_GIT_NOT_A_REPO (exit 6), whose
hint is git init --bare <path>. A bare repository has no checked-out branch, so it is the one to
export into.
Replace a diverged branch: --force#
--force moves a branch that diverged all the same, replacing what git holds there, and adds a
warning that says so (the branch <name> had diverged (<reason>), and the export moved it all the same). The move is still a compare-and-swap against the commit the export read, so a branch
another writer moves meanwhile still stops it (moved_during_export). When the divergence found
the branch at the export of a changeset of the history, the export goes on from there, writes
only what the branch lacks and reports incremental: true.
--force never moves a checked-out branch, and does nothing for an E_GIT_WRITE.
What git gets#
- An imported changeset exports to the commit it came from, byte for byte, as long as nothing below it was imported with a loss.
- A changeset made in the store exports to a commit with its own author and committer, named
<family>/<instance>with the address<instance>@<family>.swarmops.local, and a trailer block after its message:SwarmOps-Changeset:with the changeset's id, thenSwarmOps-Land-Key:andSwarmOps-Squashed-From:for a landed change. The same store gives the same commit every time. - A history imported with a loss exports as new commits, from the first lossy commit up:
rewrittencounts them, a warning says so, and the signature headers they keep no longer verify.
What git gets shows such a commit in
git log.
Run it again#
The export is incremental: run again, it writes only what the branch's last export does not reach.
With nothing new it writes nothing: exported 0, objects_written 0, branch_moved false.
exported counts the commits written that the repository did not hold, so exporting a history the
repository holds already into a new branch writes no object and only creates the branch.
When the branch diverged#
E_GIT_EXPORT_DIVERGED (exit 3) means the branch names something other than the export of the
head or of an ancestor of it. Nothing is written. details give the reason, the branch, the
commit it names (git_head), the source's head (head_changeset) and, for branch_ahead,
both_moved and source_moved_back, the changeset whose export the branch holds
(exported_changeset). The hint is the next step:
reason | What happened | Hint |
|---|---|---|
branch_ahead | Commits were made in git on top of the export of the head. | swarmops git import <path> --ref refs/heads/<branch> --into <ref>, then export again. |
both_moved | Commits were made in git on top of an older export, and the source moved too. | git -C <path> log refs/heads/<branch> shows the git side; --force replaces it. |
branch_moved | The branch names a commit that is no export of this history. | git -C <path> log refs/heads/<branch>; --force replaces it with the export. |
source_moved_back | The branch holds the export of a changeset that is no ancestor of the head, as after an op undo or a ref set backwards. | swarmops op log names what moved the source; --force moves the branch back. |
lossy_history | The branch holds the original commits of a history imported with a loss. | Export to another branch, or --force replaces the originals. |
moved_during_export | Another writer moved the branch while the export wrote; git_head is null when it deleted it. | Run the same export again. What it wrote stays. |
Patterns#
Export to an empty repository. Make a bare one, export, and check it with git:
git init --bare ~/src/mirror.git
swarmops git export ~/src/mirror.git
git -C ~/src/mirror.git fsck --strict --no-danglingSend landed work to review. Export into a branch of its own, push it, and move the trunk with git once the review passes (the daily loop):
swarmops git export ~/src/acme.git --ref main --branch review
git -C ~/src/acme.git push origin reviewContinue a branch that people also push to. Fetch and import before agents land more, so that
the next export continues the branch; when it stops with branch_ahead, run the import its hint
names, then export again.
Undo a landed change, in git too. After swarmops op undo takes main back, the export stops
with source_moved_back; --force moves the branch back to the export of the new head, and the
push after it must be forced too.
Concurrency and interruptions#
- Agents. The committed snapshot is taken under the store's writer lock, so an export waits
behind committing agents as a writer does, and past the lock's timeout fails with
E_BUSY(exit 4, retryable). Agents commit while it writes git: the export sends the head it read. - Git. The branch moves through
refs/heads/<branch>.lock. A lock file that is there already isE_GIT_WRITEwithdetails.reasonref_locked: another git process holds it, or an export that died left it. Remove it once no git process runs (known limitations). - A cut-short export leaves loose objects that no branch reaches. The same export run again reads them back, writes again any that is damaged, and writes only what is still missing.
Failure cases#
| Code | Exit | When |
|---|---|---|
E_REF_NAME_INVALID | 1 | --ref is not main or release/<name>, or --branch is no branch name git takes. |
E_GIT_NOT_A_REPO | 6 | No git repository at the path. |
E_GIT_UNSUPPORTED | 1 | A repository the export does not write as it is: SHA-256, reftable refs, a shallow clone, a special or too long file in the git directory. |
E_INVALID_ARG | 2 | The branch is checked out in the repository's work tree (details.reason checked_out). |
E_REV_NOT_FOUND | 6 | The store has no --ref. |
E_GIT_EXPORT_DIVERGED | 3 | The branch diverged; nothing written. |
E_GIT_WRITE | 1 | ref_locked, ref_name_conflict (a ref in the branch's way), or io (a write the filesystem refused). Objects written stay; the branch did not move. |
E_GIT_READ | 1 | An object of the repository is missing or damaged, or a read failed. |
E_BUSY | 4 | The writer lock stayed held past its timeout. |
E_STORE_CORRUPT | 1 | An object of the history below the head is missing or damaged; swarmops verify lists it. |
Cost#
Each object an export writes is a loose file, fsynced on its own, so the first export of a long
history writes one file and one fsync per object until git gc packs them. Every export, an
incremental one included, derives the export id of each changeset below the head again from the
store's objects. Known limitations gives both
costs; the performance ledger has no measurement of the export yet.
Go further#
- Keep git: import, land and export runs
the round trip, and When an export stops
walks through
branch_ahead. - One writer per trunk keeps the review branch and the trunk apart.
- The git side says what to watch.
- Git export in the architecture overview walks through each step.
Return information#
result is a GitExportResp (swarmops schema GitExportResp prints its JSON Schema).
| Field | Type | Required | Description |
|---|---|---|---|
branch | string | yes | The branch the export wrote, by its short name. |
branch_moved | boolean | yes | Whether this export moved the branch, or created it; false when the branch named head_sha already, after an earlier export of the same head or a concurrent one. |
exported | integer | yes | How many commits this export wrote to the repository: the commits of the ref's history that the branch's last export did not reach and the repository did not hold. A branch that is up to date exports 0. |
head_changeset | string | yes | The changeset at source_ref's head, which head_sha is the export of. |
head_sha | string | yes | The commit the branch names now, the export of head_changeset: its git id, 40 lowercase hex digits. |
incremental | boolean | yes | Whether the branch held an earlier export that this one continued, so that only the commits after it were written; false for a branch the export created, or one that force moved from a history that was no export of this one. |
objects_written | integer | yes | How many git objects (commits, trees and blobs) this export wrote to the repository. |
old_sha | string or null | no | The commit the branch named when the export started; omitted when the branch did not exist, and the export created it. |
rewritten | integer | yes | How many of the exported commits have another id than the commit they were imported from: a commit the import took lossily (git.lossy) renders to other bytes than the git commit it came from, and so does every commit above it, whose parent has another id now; their signature headers no longer verify. Commits made in the store were never imported and are not counted. |
warnings | array of string | no | What the export has to say that did not stop it, such as how many of its commits it rewrote (rewritten). Omitted when empty. |
Envelope op: git_export.
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_GIT_EXPORT_DIVERGED (exit 3):
git export: the branch holds commits that are not the export of the source ref's head or of an ancestor of it, so the branch did not move; there is no silent merge, and the request'sforcemoves it anyway. - E_GIT_WRITE (exit 1):
git exportcould not write the repository;details.reasonsays what failed:io(the filesystem refused a write;details.pathnames the file, relative to the git directory),ref_locked(the branch's lock file,refs/heads/<branch>.lock, exists: another git process holds it, or an earlier export died holding it, and git itself says to remove it once no git process runs;details.pathnames it) orref_name_conflict(an existing ref stands where the branch's name needs a directory, or the other way round;details.branch).
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).