<!-- Generated by crates/swarmops-cli/src/reference.rs from the code: do not edit. Change the code, then run SWARMOPS_REGEN_REFERENCE=1 with the regen_reference test. -->

# swarmops git import

Bring a git repository's history in: every commit --ref reaches that the store does not hold becomes a changeset, parents first, and --into moves to the changeset of --ref's commit, created if it does not exist.

The repository is read on this host, at PATH, relative to the working directory: a work tree, which holds .git, or a bare repository. Each changeset keeps its git commit's id in its git.oid metadata, and its author and committer in its signatures; it records no agent. Run again, it takes only the commits after the one --into's head holds, and writes nothing when that is --ref's commit. A head that holds no git commit, or one --ref does not reach, fails with E\_GIT\_IMPORT\_DIVERGED (exit 3), and nothing is written.

Content a changeset cannot hold as git has it is refused by class (E\_GIT\_IMPORT\_LOSSY, naming the commit, the path and the class) unless --lossy allows that class: then it is dropped or normalized, and the changeset names the class in its git.lossy metadata.

```yaml
title: "swarmops git import"
description: "Bring a git repository's history in: every commit --ref reaches that the store does not hold, then move --into to it."
group: "git"
since: "0.1.0"
mutates: true
identity_required: true
mcp_tool: ""
envelope_op: "git_import"
exit_codes: [0, 1, 3, 6]
errors: [E_GIT_NOT_A_REPO, E_GIT_REF_NOT_FOUND, E_GIT_IMPORT_DIVERGED, E_GIT_IMPORT_LOSSY, E_GIT_UNSUPPORTED, E_GIT_READ]
syntax: "swarmops git import [OPTIONS] <PATH>"
```

## Syntax

```text
swarmops git import [OPTIONS] <PATH>
```

## Required arguments

### `PATH`

Syntax: `<PATH>`.

The git repository's directory.

## Optional arguments

The command also takes the [global options](../cli.md#global-options).

### `--ref`

Syntax: `--ref <GIT_REF>`.
Default: `HEAD`.

The commit to import, with every commit it reaches: HEAD, a ref's name or a commit id.

### `--into`

Syntax: `--into <REF>`.
Default: `main`.

The ref to move to that commit's changeset: main or release/&lt;name&gt;.

### `--lossy`

Syntax: `--lossy <CLASS>...`.

A class of content to drop or normalize instead of refusing it, or all; repeatable. Values: `gitlinks`: A submodule's entry: dropped; `paths`: A name that is not UTF-8, or is . or ..: dropped; `modes`: A mode git does not write, such as 100664: normalized; `tree_order`: A tree out of git's order, or naming an entry twice: put in order; `empty_trees`: An entry naming an empty tree: dropped; `nonstandard`: A commit whose bytes the changeset cannot keep: kept as its fields hold it; `all`: Every class.

### `--gitlinks`

Syntax: `--gitlinks <WHAT>`.

What to do with a submodule's entry: refuse it, or drop it as --lossy gitlinks does; given, it decides that class whatever --lossy says. Values: `refuse`: Refuse the commit that holds one (E\_GIT\_IMPORT\_LOSSY); `drop`: Drop it, and name the class in the changeset's git.lossy metadata.

## Examples

### CLI

```sh
swarmops --repo [tmp]/copy --as agent/worker-17 git import [git]
```

```json
{
  "ok": true,
  "op": "git_import",
  "api": 1,
  "result": {
    "imported": 2,
    "skipped": 0,
    "head_changeset": "[imported]",
    "git_head_sha": "[id4]",
    "target_ref": "main",
    "ref_moved": true,
    "lossy": [],
    "lossy_total": 0
  },
  "meta": {
    "repo": "[tmp]/copy",
    "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.

## Details

### What a run does

`git import` is a client-side command: it reads a git repository on this host and writes the
store that `--repo` (else `SWARMOPS_REPO`) names. No server ever sees the request, and there is no
MCP tool for it. A run goes in this order:

1. **Check the target.** `--into` must be a ref name of the store (`main` or `release/<name>`),
   checked before the repository is opened: `--into feature/x` is `E_REF_NAME_INVALID`, whatever
   the path holds.
2. **Open the repository.** The path is a work tree that holds `.git`, or a bare repository, taken
   relative to the working directory. It is opened isolated: no global or system git
   configuration and no `GIT_*` variable apply, and replacement objects (`refs/replace/`) and
   grafts never do. A SHA-256 repository, reftable refs, a shallow clone, a FIFO, a socket or a
   device where a file of the git directory is read, and such a file read whole that is longer
   than 16 MiB are refused here, before any object is read.
3. **Resolve `--ref` and read the target.** The tip is the commit `--ref` names. A target that
   exists has a base: the git commit its head changeset holds in its `git.oid` metadata, or, for
   a changeset made in the store, the commit it exports to.
4. **Walk.** From the tip over every parent, first parents first, the walk stops at the base and
   at every commit the store already holds an import of. What it stopped at is `skipped`.
5. **Check the ancestry.** The base must be the tip (then nothing is written) or an ancestor of
   it. Anything else is `E_GIT_IMPORT_DIVERGED`, and nothing is written.
6. **Convert and record.** The new commits become changesets, parents first, up to 8,192 to a
   journal frame. Their trees and blobs are stored first, and each frame's commits get their
   external id entries once the frame is durable.
7. **Move the target.** The target moves from the head it had to the tip's changeset by
   compare-and-swap, recorded for your identity. A target that does not exist is created.

### Choose the commit: `--ref`

`--ref` names one commit, and the import takes it with every commit it reaches:

- `HEAD`, the default: the repository's own `HEAD`, which in a bare clone names the remote's
  default branch.
- A ref's name, short or full, looked up in git's order: as given, then under `refs/`,
  `refs/tags/`, `refs/heads/`, `refs/remotes/`, and as `refs/remotes/<name>/HEAD`. An annotated
  tag is peeled to its commit.
- A commit id of 40 hex digits, either case, which wins over a ref of the same name.

There is no revision syntax: `--ref stable~1` names no ref and fails with `E_GIT_REF_NOT_FOUND`
(exit 6), whose hint lists the repository's refs (`git -C <path> for-each-ref`). A name that
peels to a tree or a blob fails the same way, with `details.kind` saying what it named.

### Choose the target: `--into`

`--into` is the store ref the import moves, `main` by default, or `release/<name>`; a store has no
other refs. It decides where the import goes on from:

| The target | What the import does |
| --- | --- |
| Does not exist | Imports everything `--ref` reaches that the store does not hold, then creates the target. |
| Holds an imported changeset | Goes on from the commit that changeset came from (`git.oid`). |
| Holds a changeset made in the store | Goes on from the commit that changeset exports to, so that commits made in git on top of an export come back on the changeset they were made on. |
| Holds `--ref`'s commit already | Writes nothing: imports 0, skips 1, and `ref_moved` is `false`. |

A head that `--ref` does not reach (a rewritten git history, or a change landed in the store and
not exported yet) fails with `E_GIT_IMPORT_DIVERGED` (exit 3). The details name the target, its
head, the git commit it stands for (`base`) and the tip (`git_head`). The hint is the target's
log, or `swarmops verify --deep` when the walk passed over an external id entry whose changeset
stands for another commit (`details.external_id`). Nothing is merged.

A ref of its own keeps a git branch apart from `main`: `--into release/<name>` imports it there,
and `land` can then take it onto `main`.

### Allow loss: `--lossy` and `--gitlinks`

By default the import keeps every commit exactly, and refuses one it cannot keep with
`E_GIT_IMPORT_LOSSY` (exit 1): `details` names the `commit`, the `class` and the `path` (also as
`path_base64` when it is not UTF-8; empty for `nonstandard`), and the hint names the `--lossy`
class that would allow it. The import stops before the frame that would hold that commit, and the
target does not move.

`--lossy <class>` allows one class; it repeats, and `all` allows every class. `--gitlinks` decides
the `gitlinks` class alone, whatever `--lossy` says:

| Line | Classes allowed |
| --- | --- |
| `--lossy gitlinks` | `gitlinks` |
| `--gitlinks drop` | `gitlinks` |
| `--lossy all --gitlinks refuse` | every class but `gitlinks` |
| `--lossy gitlinks --gitlinks refuse` | none |
| `--lossy modes --lossy tree_order` | `modes`, `tree_order` |

A changeset taken with a loss names its classes in its `git.lossy` metadata. The result lists the
first 100 such commits in `lossy`, each with its changeset and classes, in the order imported, and
`lossy_total` counts them all. [What import refuses](../../guides/git-round-trip.md#what-import-refuses)
says what each class drops or normalizes.

A loss shows again at the export: a commit taken with a loss renders to other bytes than the commit
it came from, and so does every commit above it. [`git export`](../commands/git-export.md)
writes them as new commits (`rewritten`), and refuses to put them on a branch that holds the
originals (`lossy_history`).

A commit the store holds already is reused as it is, so a later import without `--lossy` that
reaches a commit an earlier import took with a loss skips it rather than refusing it, and reports
`lossy_total` 0 ([known limitations](../../known-limitations.md#git-import-and-export)).

### What the store records

- **One changeset per commit.** Its author, committer and their times are the commit's, its
  `git.oid` metadata the commit's id, and a merge keeps all its parents in their order. The same
  git commit is the same changeset in every store.
- **No agent.** An imported changeset records no agent: `log`, `show` and `op log` give its agent
  as `unknown/unknown`, and `show`'s `signatures` give the git author and committer.
- **Operations in `op log`.** Each journal frame of new changesets is an operation of kind
  `commit`. The target's move is an operation of its own, of kind `ref_move`, whose actor is your
  identity. An import into a ref of its own that finds every commit known writes only the
  `ref_move`.
- **External ids.** Each imported commit gets an external id entry naming its changeset. That is
  how a later run knows where to stop, and how an export finds the commit again.
- **`meta.recovered`.** What the import's writes repaired of what earlier writers left, added up,
  on success only.

### Run it again

The import is incremental. Run again, it walks only until it meets a commit the store holds, and
takes only the commits above it. With nothing new it writes nothing: `imported` 0, `skipped` 1,
`ref_moved` `false`, and `old_head` the head it found.

`skipped` counts where the walk stopped, not the history the store holds: the base, and each
commit an earlier import made that the walk met. Importing the same commit into a second ref
writes no changeset, imports 0, skips 1, and only creates that ref.

### Patterns

**Bring a repository in, once.** Import from a bare clone, which the export can later write as
well:

```sh
git clone --bare <remote-url> ~/src/acme.git
swarmops git import ~/src/acme.git --ref stable --into main
```

**Take in what git gained.** Fetch, then import the same branch into the same ref. Only the new
commits come in, and `main` moves to the last of them:

```sh
git -C ~/src/acme.git fetch origin stable:stable
swarmops git import ~/src/acme.git --ref stable --into main
```

**Bring commits made in git on top of an export.** When an export stops with `branch_ahead`, its
hint is the import that brings those commits in, by the branch's full name:

```sh
swarmops git import ~/src/acme.git --ref refs/heads/stable --into main
```

**Land a git branch.** Import it into a ref of its own, land that ref onto `main`, and delete the
ref with the id it holds; [One writer per trunk](../../operate/one-writer-per-trunk.md#change-the-trunk-without-breaking-the-rule)
runs it end to end.

**Take back an import's move.** `swarmops op log` shows the target's move as a `ref_move`
operation, and `swarmops op undo <op>` moves the target back to what it named before, or deletes
it if the import created it. The changesets stay in the store, so the same import run again
writes no changeset and moves the target again.

### Interruptions and concurrency

The import writes frame by frame, each frame one write transaction under the store's writer lock,
so agents commit and land between its frames. The target moves last:

- **The target moved during the import** (an agent landed on it): `E_CAS_MISMATCH` (exit 4,
  retryable), with every commit recorded. The same import run again moves the target if what it
  names leads to the tip, and fails with `E_GIT_IMPORT_DIVERGED` otherwise.
- **The writer lock stayed held.** A frame that waits out the lock is tried again three times,
  then the import stops with `E_BUSY` (exit 4).
- **A store failure** (`E_IO_TRANSIENT`, `E_IO`, `E_STORE_CORRUPT`, …) stops the import after the
  last frame that committed, and it never writes again in that run. `details.imported` and
  `details.last_commit` (null for none) say how far it got, and the hint is to run the same import
  again, which continues after them, unless the store is damaged (`swarmops verify`). A frame
  that committed but failed after its commit point is `E_IO` with `details.committed` `true`.

### Failure cases

| Code | Exit | When |
| --- | --- | --- |
| [`E_IDENTITY_REQUIRED`](../errors.md#e_identity_required) | 5 | No `--as` and no `SWARMOPS_AGENT`: checked before the store is opened. |
| [`E_REF_NAME_INVALID`](../errors.md#e_ref_name_invalid) | 1 | `--into` is not `main` or `release/<name>`. |
| [`E_GIT_NOT_A_REPO`](../errors.md#e_git_not_a_repo) | 6 | No git repository at the path. |
| [`E_GIT_UNSUPPORTED`](../errors.md#e_git_unsupported) | 1 | A repository or commit the import does not read; `details.reason` says which. A `shallow` clone's hint deepens it (`git -C <path> fetch --unshallow`). |
| [`E_GIT_REF_NOT_FOUND`](../errors.md#e_git_ref_not_found) | 6 | `--ref` names nothing, or no commit. |
| [`E_GIT_READ`](../errors.md#e_git_read) | 1 | An object is missing (a partial clone lacks blobs), damaged or unparsable, with the hint `git -C <path> fsck`; or a read failed (`io`). |
| [`E_TOO_LARGE`](../errors.md#e_too_large) | 1 | An object longer than a store object holds, refused by its header before it is read. |
| [`E_GIT_IMPORT_LOSSY`](../errors.md#e_git_import_lossy) | 1 | Content of a class the line does not allow. |
| [`E_GIT_IMPORT_DIVERGED`](../errors.md#e_git_import_diverged) | 3 | The target's head is not the import of the tip or of an ancestor of it. Nothing is written. |
| [`E_CAS_MISMATCH`](../errors.md#e_cas_mismatch) | 4 | The target moved while the import wrote. Every commit is recorded. |
| [`E_BUSY`](../errors.md#e_busy) | 4 | The store's writer lock stayed held through three retries. |

### Cost

An import reads and converts each new tree and blob once, and seeds its conversion with what the
target's head holds, so an incremental run reads mostly what changed. It holds the new files of one
commit in memory together, and onto a target whose head was made in the store it walks the whole
history below that head; [known limitations](../../known-limitations.md#git-import-and-export)
gives both costs. The [performance ledger](../../perf.md) has no measurement of the import yet.

### Go further

- [Keep git: import, land and export](../../guides/git-round-trip.md) runs the round trip
  command by command.
- [The daily loop](../../operate/daily-loop.md#once-bring-the-repository-in) brings a
  repository in and [takes in what git gained](../../operate/daily-loop.md#1-take-in-what-git-gained).
- [One writer per trunk](../../operate/one-writer-per-trunk.md#tell-a-divergence-from-a-lagging-trunk)
  tells a real divergence from a trunk that waits for its review.
- [Git import](../../architecture.md#git-import) in the architecture overview walks through each
  step.

## Return information

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `git_head_sha` | `string` | yes | `git_ref`'s commit: its git id, 40 lowercase hex digits. |
| `head_changeset` | `string` | yes | The changeset of `git_ref`'s commit, which `target_ref` names now. |
| `imported` | `integer` | yes | How many commits became changesets in this import. |
| `lossy` | `array of LossyCommitOut` | yes | The commits this import took lossily, as the request's `lossy` allowed: the first 100, in the order imported. `lossy_total` counts them all. |
| `lossy_total` | `integer` | yes | How many commits this import took lossily, the ones past the first 100 included. |
| `old_head` | `string or null` | no | What `target_ref` named when the import started; omitted when it did not exist, and the import created it. |
| `ref_moved` | `boolean` | yes | Whether this import moved `target_ref`, or created it; `false` when the ref named `head_changeset` already, after an earlier import of the same commit or a concurrent one. |
| `skipped` | `integer` | yes | How many commits the import found in the store already, where its walk stopped: the commit of the target's head, or commits an earlier import made. Importing the commit the target names already imports 0 and skips 1. |
| `target_ref` | `string` | yes | The ref the import moved, or found at `head_changeset` already. |
| `warnings` | `array of string` | no | Problems the import met that did not stop it, such as failed housekeeping after the target's move became durable; the import itself succeeded. Omitted when empty. |

Envelope `op`: `git_import`.

Exit codes:

- `0`: Success.
- `1`: The operation failed.
- `3`: The change conflicts with what it lands on: resolve it, then run the operation again.
- `6`: Something the request names does not exist.

## Errors

- [`E_GIT_NOT_A_REPO`](../errors.md#e_git_not_a_repo) (exit 6): `git import` found no git repository it can open at `git_path` (`details.git_path`).
- [`E_GIT_REF_NOT_FOUND`](../errors.md#e_git_ref_not_found) (exit 6): `git import`: the source repository has no `git_ref`, or it names no commit (`details.git_ref`; `details.kind` the object it names instead, such as a tree).
- [`E_GIT_IMPORT_DIVERGED`](../errors.md#e_git_import_diverged) (exit 3): `git import`: the target ref's head is not the import of `git_ref`'s commit or of an ancestor of it, because the source was rewritten or a native changeset was committed on the target, so nothing was written; there is no silent merge.
- [`E_GIT_IMPORT_LOSSY`](../errors.md#e_git_import_lossy) (exit 1): `git import`: a commit holds content a changeset cannot keep as git has it; `details` names the `commit`, the `path` (also as `path_base64` when it is not UTF-8) and the lossy `class`, which the request's `lossy` can allow.
- [`E_GIT_UNSUPPORTED`](../errors.md#e_git_unsupported) (exit 1): `git import` does not read the source: a SHA-256 repository, reftable refs, a shallow clone, a FIFO, a socket or a device where it reads a file of the git directory, a file it reads whole there that is longer than 16 MiB, a commit of more than 255 parents, or an entry whose mode is no kind of entry; `details.reason` says which (`object_format`, `ref_storage`, `shallow`, `special_file`, `file_too_long`, `parents`, `mode`), `details.file` names the file of `special_file` and `file_too_long` by its path in the git directory (`details.size` and `details.max` give the long one's length and the limit), and `details.commit` names the commit of `parents` and `mode`.
- [`E_GIT_READ`](../errors.md#e_git_read) (exit 1): `git import` could not read the source: an object is missing (as in a partial clone), does not hash to its id or does not parse, or the filesystem refused a read (`details.reason`; `details.object` names the object when there is one).

Any command that opens a store may also fail with a store error, such as [`E_NOT_A_REPO`](../errors.md#e_not_a_repo), [`E_IO`](../errors.md#e_io) or [`E_STORE_CORRUPT`](../errors.md#e_store_corrupt); a command line the grammar refuses is [`E_INVALID_ARG`](../errors.md#e_invalid_arg) (exit 2).

## See also

- [Operating SwarmOps: Once: bring the repository in](../../operations.md#once-bring-the-repository-in)
- [`swarmops git export`](git-export.md)
- [CLI reference](../cli.md)
- [Error codes](../errors.md)
