# Coming from git

> Release: 0.11.0 · Last changed: 2026-11-13 · Source: https://github.com/KeiaiLab/SwarmOps/blob/stable/docs/reference/coming-from-git.md

Where a SwarmOps operation does what a git command does, `so <git verb>` runs it, with git's flags where they mean the same. Where the meaning differs the command keeps its own name (`land` is not `merge`), and the git verb is refused, exit 2, with one line first in `error.hint` that names what to type. Nothing looks like git and does something else: `so` is not a drop-in for the git binary, and a line that ran before git's verbs were read runs as it did.

A git verb that runs is read as the command it stands for (`so status` is `so ws status`): the envelope, its `op`, the help (`so status --help`) and a usage error are that command's, as [the CLI reference](cli.md) gives them. A store has no working tree: `status`, `restore` and the bare forms of `diff`, `commit` and `rebase` need a workspace. Their paths are read from the workspace's root, not from the working directory as git's are; below the root a line that names paths is refused. The hints here are written with `so`; a hint names the program as it was run.

## Text output

Every command prints one JSON envelope unless text is asked for. `--human` prints text, in git's shape for `status`, `log`, `diff` and `commit`, and git's own shape flags ask for it too (`so status -s`, `so log --oneline`, `so diff --stat`). For text from every command:

```sh
export SWARMOPS_FORMAT=human
```

## Classes

| Class | Meaning |
| --- | --- |
| S | The same verb does the same. |
| A | The same verb, read as the SwarmOps command of the same meaning. |
| D | Different on purpose: the verb is refused, and the hint names what to type. |
| N | No such concept: the verb is refused, and the hint says what to do. |
| L | A candidate for later: the notes say what would be built. |

A row can carry two: one form of the verb runs, the others are refused with the hint. `op` is the operation the example's envelope names; a row without one is refused, and its example prints the hint.

## Tier 1: everyday commands

| git | Class | What to type | `op` | Example | Hint when refused |
| --- | --- | --- | --- | --- | --- |
| `git init` | S | `so init DIR` · `so ws create NAME` | `repo_init` | `so init store2` | — |
| `git clone` | A, L | `so clone URL DIR` · `so ws create NAME` | `clone_repo` | `so clone https://remote.invalid/x.git copy` | — |
| `git status` | A | `so status` | `ws_status` | `so status` | — |
| `git add` | N | `so commit -m MSG [PATH]...` | — | `so add a.txt` | `git add: there is no staging area; so commit -m MSG [PATH]... in a workspace takes every change, or the named paths` |
| `git rm` | N | `so commit -m MSG` | — | `so rm a.txt` | `git rm: delete the file in the workspace; so commit -m MSG records it` |
| `git mv` | N | `so commit -m MSG` | — | `so mv a.txt b.txt` | `git mv: move the file in the workspace; so commit -m MSG records it` |
| `git commit` | A | `so commit -m MSG [PATH]...` | `ws_commit` | `so commit -m x` | — |
| `git diff` | S, A | `so diff FROM TO` · `so diff` | `ws_diff` | `so diff` | — |
| `git log` | S, A | `so log [REV] -n N --path P` | `log` | `so log -1` | — |
| `git show` | S | `so show REV` · `so cat REV:PATH` | `show` | `so show main` | — |
| `git branch` | D, L | `so ref list` · `so ref set NAME REV --create` · `so ref delete NAME --expect ID` | — | `so branch` | `git branch: so ref list lists refs; so ref set NAME REV --create makes one; so ref delete NAME --expect ID removes one` |
| `git switch` | N, L | `so ws create NAME --base REV` | — | `so switch main` | `git switch: a workspace does not switch; so ws create NAME --base REV makes another` |
| `git checkout` | N | `so ws create NAME --base REV` · `so restore PATH...` | — | `so checkout main` | `git checkout: so ws create NAME --base REV for another tree; so restore PATH... to drop edits` |
| `git restore` | A | `so restore PATH...` · `so ws discard` | `ws_discard` | `so restore a.txt` | `git restore: name the paths, so restore PATH...; so ws discard drops every edit` |
| `git merge` | D | `so ws land` · `so land HEAD` | — | `so merge main` | `git merge: SwarmOps lands; so ws land from a workspace, or so land HEAD` |
| `git rebase` | S, A | `so rebase HEAD --onto REF` · `so rebase` | `ws_rebase` | `so rebase` | — |
| `git fetch` | S, A | `so fetch REMOTE` · `so fetch` | `fetch` | `so fetch` | — |
| `git pull` | D | `so sync REMOTE --no-push` · `so rebase` | — | `so pull` | `git pull: so sync REMOTE --no-push brings the remote into main; so rebase in a workspace follows it` |
| `git push` | S, A | `so push REMOTE [REF[:BRANCH]]` · `so push` | `push` | `so push` | — |
| `git remote` | S, A | `so remote add NAME URL` · `so remote remove NAME` · `so remote list` · `so remote` | `remote_list` | `so remote -v` | — |
| `git tag` | A, D | `so tag` · `so ref set tags/NAME REV --create` · `so ref delete tags/NAME --expect ID` | `ref_list` | `so tag` | `git tag: so tag lists; so ref set tags/NAME REV --create makes one; so ref delete tags/NAME --expect ID removes one` |
| `git stash` | D, L | `so ws snapshot` | — | `so stash` | `git stash: so ws snapshot records the edits and leaves the files; nothing pops` |
| `git reset` | D | `so ws discard [PATH]...` · `so op undo OP` | — | `so reset` | `git reset: so ws discard [PATH]... drops edits; so op undo OP takes an operation back; so ref set moves a ref` |
| `git revert` | S | `so revert REV --reason TEXT` | `revert` | `so revert main --reason x` | — |
| `git cherry-pick` | N, L | `so land HEAD --base BASE` | — | `so cherry-pick main` | `git cherry-pick: not built; so land HEAD --base BASE lands one change's range` |
| `git blame` | L | `so history REV:PATH` | — | `so blame a.txt` | `git blame: not built yet (M7); so history REV:PATH lists the changesets that touched a path` |
| `git grep` | N | `so cat REV:PATH` | — | `so grep x` | `git grep: search the files of a workspace; so cat REV:PATH reads one at a revision` |
| `git config` | N | `so whoami` · `so remote` | — | `so config user.name` | `git config: identity is --as or SWARMOPS_AGENT (so whoami); the store is --repo or SWARMOPS_REPO; remotes are so remote` |
| `git reflog` | A | `so reflog NAME` · `so op log` | `ref_log` | `so reflog main` | `git reflog: so reflog NAME lists a ref's moves; so op log every operation; nothing expires` |

### Notes on tier 1

- **`git init`**: Makes a store, no working tree; `DIR` is required. Then `so ws create NAME`. `-b`, `--bare` are not taken.
- **`git clone`**: A: positional `DIR` (= `--into`). A store and no files, as `git clone --bare`; then `so ws create NAME`. Later: `--ws NAME`, a new flag: a working tree in the same step.
- **`git status`**: `-s`, `-b`, `-sb`, `--short`, `--branch` ask for the text listing. Paths from the workspace's root.
- **`git add`**: No staging area: a commit takes every change, or the named paths.
- **`git rm`**: Delete the file in the workspace; the next commit records it.
- **`git mv`**: Move the file in the workspace; the next commit records a delete and an add.
- **`git commit`**: `so commit --changes FILE` keeps its meaning (the JSON change list). Takes every change, new files included unless an ignore file names them (git's `-a` leaves new files out); `-a` is accepted. Paths from the workspace's root. No `--amend` (D: `so op undo`, then commit again), no editor.
- **`git diff`**: Lists the changed paths by default; `-p` for the patch. `--stat`, `--name-status`, `-U<n>`. Paths by `--path P`, not `-- P`, from the workspace's root. No `A..B`, no `--staged`.
- **`git log`**: First parents, from `main` unless REV is given, also inside a workspace: its own commits are `so log ID` with the head `so ws info` gives. A: `--oneline`, `-<n>`. No `--graph`, `--author`, `--since`, ranges.
- **`git show`**: The changeset and its changed paths, no patch. `git show REV:PATH` is `so cat REV:PATH`.
- **`git branch`**: A ref list also holds `tags/` and `remote/`; there is no current branch. Later: a `branch` verb once FORMAT 2 branches map to git refs (with M11).
- **`git switch`**: A workspace never switches; make another. Later: unscheduled.
- **`git checkout`**: Not offered under this name (git itself split it).
- **`git restore`**: At least one path, as in git. A file never committed is deleted (git leaves an untracked file). Paths from the workspace's root; below the root the line is refused. `.` is no path (exit 2). No `--staged`, `--source`. Every edit: `so ws discard`.
- **`git merge`**: One squashed changeset onto the target by compare-and-swap; no merge commit.
- **`git rebase`**: Store form: one squashed changeset, no ref moves. Workspace form: the workspace follows its target; `--continue`, `--abort`, `--onto` there. No `-i`.
- **`git fetch`**: `main` never moves on a fetch. No `--all`, `--prune`.
- **`git pull`**: `sync` lands onto the remote's head and moves `main`; a workspace follows by its own rebase.
- **`git push`**: Sends `main` unless REF is named; `main` and `release/*` only. Compare-and-swap, fast-forward only; no `--force`, `-u`, `--tags`.
- **`git remote`**: `so remote`, `so remote -v` list; `so remote rm` removes. No `rename`, `set-url`.
- **`git tag`**: FORMAT 2 stores. Lightweight only; a tag never moves.
- **`git stash`**: Records the edits as a changeset and leaves the files; nothing pops. Later: unscheduled.
- **`git reset`**: `main` moves forward only. A ref: `so ref set NAME REV --expect ID`.
- **`git revert`**: Lands the inverse; `--since` for a range. The reason is required.
- **`git cherry-pick`**: One change's range lands as one changeset. Later: M24 (rewriting with lineage).
- **`git blame`**: The changesets that touched a path. Later: M7.
- **`git grep`**: `rg` or `grep` in a workspace.
- **`git config`**: There is no configuration command.
- **`git reflog`**: A name is required (no store-level HEAD). Every operation: `so op log`. `expire`, `delete`, `exists` are refused with the hint: nothing expires.

## Tier 2: commands scripts and CI use

| git | Class | What to type | `op` | Example | Hint when refused |
| --- | --- | --- | --- | --- | --- |
| `git rev-parse` | D | `so resolve REV` · `so ws info` | — | `so rev-parse main` | `git rev-parse: so resolve REV gives the full id; so ws info a workspace's head and path` |
| `git rev-list` | D | `so log REV -n N` · `so ancestry FROM TO` | — | `so rev-list main` | `git rev-list: so log REV -n N; so ancestry FROM TO` |
| `git ls-files` | D | `so ls REV[:PATH]` | — | `so ls-files` | `git ls-files: so ls REV[:PATH]` |
| `git ls-tree` | D | `so ls REV[:PATH] --depth N` | — | `so ls-tree main` | `git ls-tree: so ls REV[:PATH] --depth N` |
| `git cat-file` | D | `so cat REV:PATH` · `so blob get ID` · `so blob has ID` | — | `so cat-file -p main` | `git cat-file: so cat REV:PATH; so blob get ID; so blob has ID` |
| `git hash-object` | D | `so blob put FILE` | — | `so hash-object a.txt` | `git hash-object: so blob put FILE` |
| `git describe` | N | `so tag` | — | `so describe` | `git describe: not built; so tag lists the tags` |
| `git for-each-ref` | D | `so ref list [--prefix P]` | — | `so for-each-ref` | `git for-each-ref: so ref list [--prefix P]` |
| `git show-ref` | D | `so ref list` | — | `so show-ref` | `git show-ref: so ref list` |
| `git symbolic-ref` | N | `so ws info` | — | `so symbolic-ref HEAD` | `git symbolic-ref: there are no symbolic refs; so ws info gives a workspace's head and target` |
| `git update-ref` | D | `so ref set NAME REV --expect ID` | — | `so update-ref main main` | `git update-ref: so ref set NAME REV --expect ID, or --create; so ref delete; so ref txn` |
| `git merge-base` | S | `so merge-base REV ONTO` · `so ancestry FROM TO` | `merge_base` | `so merge-base main main` | — |
| `git name-rev` | N | `so ref list` | — | `so name-rev main` | `git name-rev: not built; so ref list gives each ref's changeset` |
| `git ls-remote` | D | `so fetch REMOTE` · `so ref list --prefix remote/REMOTE/` · `so sync status REMOTE` | — | `so ls-remote` | `git ls-remote: so fetch REMOTE, then so ref list --prefix remote/REMOTE/; so sync status REMOTE` |
| `git diff-tree` | D | `so diff FROM TO --format names` · `so show REV` | — | `so diff-tree main` | `git diff-tree: so diff FROM TO --format names; so show REV` |
| `git worktree` | A, D | `so worktree list` · `so ws create NAME [--path DIR] [--base REV]` · `so ws list` · `so ws delete NAME` | `ws_list` | `so worktree list` | `git worktree: so ws create NAME [--path DIR] [--base REV]; so ws list; so ws delete NAME` |
| `git clean` | N | `so ws discard PATH...` | — | `so clean -f` | `git clean: so ws discard PATH... drops the named paths' edits, new files included; without a path, every edit` |
| `git archive` | N, L | `so git export PATH` | — | `so archive main` | `git archive: git init --bare PATH, so git export PATH, then git archive there` |
| `git bisect` | N, L | — | — | `so bisect start` | `git bisect: not built` |
| `git shortlog` | N | `so log` | — | `so shortlog` | `git shortlog: so log, counted by author_agent` |

### Notes on tier 2

- **`git ls-files`**: A workspace's files are plain files.
- **`git ls-tree`**: JSON entries, not `<mode> <type> <id>\t<path>`.
- **`git describe`**: Unscheduled.
- **`git symbolic-ref`**: No symbolic refs; no store-level HEAD.
- **`git merge-base`**: Along first parents, with what REV changed. `--is-ancestor`: `so ancestry FROM TO`.
- **`git worktree`**: git names a path, SwarmOps a workspace name.
- **`git archive`**: Later: M25 (archives).
- **`git bisect`**: Later: unscheduled.

## What is not done

| Not done | Why | What to do |
| --- | --- | --- |
| A drop-in for the git binary (`alias git=so`, `--porcelain`, `-z`, git's exit codes, `GIT_DIR`) | The JSON envelope is the machine contract; two contracts would have to be kept alike for ever. | Scripts read the envelope. Stock git works against a store through `swarmops serve`, and `so git export PATH` writes a real git repository. |
| An index: `add`, `rm --cached`, `restore --staged`, `diff --staged`, `commit -p` | Agents commit whole changes; a second state between the files and the store is one more thing to lose in a crash. | `so commit -m MSG PATH...` commits the named paths only. |
| Interactive rebase, an editor for messages | No terminal is assumed; a land squashes to one changeset anyway. | `so rebase` in a workspace; `-m` everywhere; `so op undo OP` to take a step back. |
| `checkout` | Two meanings in one verb. | `so ws create NAME --base REV`; `so restore PATH...`. |
| `merge` with a merge commit, `--force` on push, moving `main` back | The trunk moves forward only, by compare-and-swap. | `so land HEAD`, `so revert REV --reason TEXT`. |
| git's revision words `HEAD`, `^`, `A..B`, `A...B`, `@{upstream}` | No store-level HEAD; first-parent history. | `main`, a ref name, an id prefix, `~N`, `NAME@{N}`; `so ancestry FROM TO` for a range. |
| `stash pop`, `cherry-pick`, `blame`, `bisect`, `describe`, `archive`, `grep` | Not built. | The hint of each, in the tables above. |
| git verbs as MCP tools | The tools are the operations; an alias adds nothing an agent can call. | The existing tools (`ws_status`, `ws_commit`, …). |
| Text by default | One rule for people and agents: an agent under a pseudo-terminal is never surprised. | `--human`, `SWARMOPS_FORMAT=human`, or a shape flag (`-s`, `--oneline`, `--stat`). |
