# `Exgit.Workspace`
[🔗](https://github.com/ivarvong/exgit/blob/v0.1.0/lib/exgit/workspace.ex#L1)

An agent-loop working tree on top of a git ref.

A workspace pairs `(repository, base_ref, head_tree)`:

  * `:base_ref` — the starting point ("HEAD", a branch, a commit SHA).
  * `:head_tree` — the current working tree's SHA. `nil` when the
    workspace is pristine; reads in that state pass through to
    `base_ref`. Set to a 20-byte tree SHA after the first write.

Every state of the workspace is a real git tree object. Snapshots
are 20-byte SHAs you can persist and replay; commits are an O(1)
hash-and-store on top of the head tree; branching the workspace
for parallel exploration is `ws_b = ws_a` — the struct is a value,
no copy needed.

## Lifecycle

    ws = Exgit.Workspace.open(repo, "main")
    {:ok, ws} = Exgit.Workspace.write(ws, "lib/foo.ex", new_source)
    {:ok, ws} = Exgit.Workspace.rm(ws, "lib/old.ex")

    {:ok, content, ws} = Exgit.Workspace.read(ws, "lib/foo.ex")
    {:ok, [{:modified, "lib/foo.ex"}, {:deleted, "lib/old.ex"}], ws}
      = Exgit.Workspace.diff(ws)

    {:ok, commit_sha, ws} =
      Exgit.Workspace.commit(ws,
        message: "agent: refactor",
        author: %{name: "agent", email: "agent@example.com"},
        update_ref: "refs/heads/agent-turn-1")

## Snapshot / restore

    saved = Exgit.Workspace.snapshot(ws)   # :pristine | <<20-byte sha>>
    ws = Exgit.Workspace.restore(ws, saved)

Snapshots are opaque values you can stash anywhere — a database,
another conversation, a Linear comment. To replay an agent's run
end-to-end, restore from the saved value.

## Branching

Pass the same workspace to two parallel computations; each gets its
own threaded state.

    ws_a = ws
    ws_b = ws

    {:ok, ws_a} = Exgit.Workspace.write(ws_a, "lib/x.ex", "...")
    {:ok, ws_b} = Exgit.Workspace.write(ws_b, "lib/x.ex", "different")

`ws_a` and `ws_b` now diverge. The underlying object store is shared
(each write puts new blobs/trees) but neither workspace's `head_tree`
references the other's writes.

## VFS integration

When `:vfs` is loaded, `Exgit.Workspace` implements `VFS.Mountable`
and can be mounted into a `%VFS{}` mount table. See
`Exgit.Workspace.VFS`.

# `change`

```elixir
@type change() :: {:added | :modified | :deleted, String.t()}
```

An entry returned by `diff/1`. Path is relative to the repo root.

# `identity`

```elixir
@type identity() :: String.t() | %{name: String.t(), email: String.t()}
```

Identity for `commit/2`. Either a pre-formatted git identity string
(`"Name <email> ts +tz"`) used verbatim, or a `%{name:, email:}` map
which is rendered with the current timestamp at UTC.

# `snapshot`

```elixir
@type snapshot() :: :pristine | binary()
```

Opaque snapshot value. Either the sentinel `:pristine` or a 20-byte
tree SHA.

# `t`

```elixir
@type t() :: %Exgit.Workspace{
  base_ref: String.t() | binary(),
  head_tree: binary() | nil,
  repo: Exgit.Repository.t()
}
```

# `checkout`

```elixir
@spec checkout(t(), String.t()) :: {:ok, t()}
```

Switch the workspace's `base_ref`. Discards any uncommitted writes
(`head_tree` is reset to `nil`).

# `commit`

```elixir
@spec commit(
  t(),
  keyword()
) :: {:ok, binary(), t()} | {:error, term()}
```

Materialize the working tree as a commit object.

Required options:

  * `:message` — commit message (a binary, with or without trailing newline).
  * `:author` — `t:identity/0`.

Optional:

  * `:committer` — defaults to `:author`.
  * `:update_ref` — `false` (default) leaves refs untouched; the
    caller takes the returned commit SHA. A binary like
    `"refs/heads/agent-turn-1"` writes that ref to point at the new
    commit and tracks it as the workspace's new `base_ref`. The ref
    may already exist; it is overwritten.

After commit, `head_tree` is cleared and `base_ref` advances to
identify where the new commit lives:

  * `update_ref: false` → `base_ref` becomes the commit SHA (binary).
  * `update_ref: "refs/heads/foo"` → `base_ref` becomes that string.

Returns `{:error, :nothing_to_commit}` if the workspace is pristine.

# `diff`

```elixir
@spec diff(t()) :: {:ok, [change()], t()} | {:error, term()}
```

Compare the workspace's working state against `base_ref`, returning
a list of `{:added | :modified | :deleted, path}` entries.

A pristine workspace returns `{:ok, [], ws}` immediately.

# `exists?`

```elixir
@spec exists?(t(), String.t()) :: {boolean(), t()}
```

Whether `path` exists in the current working state.

# `ls`

```elixir
@spec ls(t(), String.t()) :: {:ok, [String.t()], t()} | {:error, term()}
```

List names of entries directly under `path`. Sorted lexicographically.

# `materialize`

```elixir
@spec materialize(t()) :: {:ok, t()} | {:error, term()}
```

Convert a lazy partial-clone repo to eager mode by prefetching every
object reachable from the workspace's effective ref. After this,
streaming ops (`walk/1`, `Exgit.FS.grep/4`) work without per-blob
network round-trips.

No-op for already-eager repos.

# `open`

```elixir
@spec open(Exgit.Repository.t(), String.t()) :: t()
```

Open a workspace over `repo` rooted at `ref` (default `"HEAD"`).

The workspace starts pristine — `head_tree` is `nil`, reads go
straight to `ref`.

# `read`

```elixir
@spec read(t(), String.t()) :: {:ok, binary(), t()} | {:error, term()}
```

Read the file at `path`. Returns the blob bytes plus the threaded
workspace.

# `restore`

```elixir
@spec restore(t(), snapshot()) :: t()
```

Replace the workspace's head tree with a previously-captured snapshot.

The snapshot's referenced objects must already be in the underlying
repo's object store — this is the case when the snapshot was produced
by a workspace sharing the same store.

# `rm`

```elixir
@spec rm(t(), String.t(), keyword()) :: {:ok, t()} | {:error, term()}
```

Remove the entry at `path`.

Options:

  * `:recursive` — when `true`, removing a directory removes its
    contents. Default `false`; without it, directory removal returns
    `{:error, :eisdir}`.

# `snapshot`

```elixir
@spec snapshot(t()) :: snapshot()
```

Capture the workspace's working state as an opaque snapshot value.

Returns `:pristine` for a workspace with no writes, otherwise the
20-byte tree SHA. Persistable, transferable, and replayable via
`restore/2`.

# `stat`

```elixir
@spec stat(t(), String.t()) :: {:ok, Exgit.FS.stat(), t()} | {:error, term()}
```

Stat the entry at `path`. Returns `%{type: :blob | :tree | :submodule, mode:, size:}`.

# `walk`

```elixir
@spec walk(t()) :: Enumerable.t()
```

Stream every blob path under the workspace's working state. Like
`Exgit.FS.walk/2`, requires the underlying repo to be `:eager` —
call `materialize/1` first on lazy partial-clone repos.

# `write`

```elixir
@spec write(t(), String.t(), binary()) :: {:ok, t()} | {:error, term()}
```

Write `content` to `path`. Creates intermediate directories
implicitly. Refuses to overwrite a directory.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
