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

Pure-Elixir git client — clone, fetch, push, plus a path-oriented
filesystem API for reading blobs by path.

No `git` binary, no libgit2, no shelling out.

## Top-level entry points

  * `init/1` — create an empty repo (memory or on-disk).
  * `open/2` — open an existing on-disk repo.
  * `clone/2` — full, lazy (`lazy: true`), or partial
    (`filter: {:blob, :none}`) clone.
  * `fetch/3` — fetch updates into an existing repo.
  * `push/3` — push objects + ref updates to a remote.

See individual function docs for options and semantics. See also
`Exgit.FS`, `Exgit.Walk`, `Exgit.Diff`, `Exgit.Repository`, and
the `Exgit.Transport` protocol.

# `remote`

```elixir
@type remote() ::
  String.t() | Exgit.Transport.File.t() | Exgit.Transport.HTTP.t() | struct()
```

A remote endpoint for `clone/2`, `fetch/3`, and `push/3`.

Either a URL string (`"https://..."` or `"file://..."`), one of the
built-in transport structs (`Exgit.Transport.File`,
`Exgit.Transport.HTTP`), or any struct implementing the
`Exgit.Transport` protocol.

# `clone`

```elixir
@spec clone(
  remote(),
  keyword()
) :: {:ok, Exgit.Repository.t()} | {:error, term()}
```

Clone a git repository.

    {:ok, repo} = Exgit.clone("https://github.com/user/repo")

## Options

  * `:path` — persistent on-disk clone. When set, the clone is
    backed by `ObjectStore.Disk`/`RefStore.Disk` and survives
    process death. Default: in-memory (lost on process exit).

  * `:lazy` — when `true`, defer object fetching. Returns a
    `%Repository{mode: :lazy}` whose object store is an
    `ObjectStore.Promisor` that fetches on demand. The initial
    clone pulls **refs only** — any subsequent object read
    triggers a `want <sha>` fetch against the transport. Great
    for agent loops that read a handful of files from large
    repos. Default: `false` (full clone).

  * `:filter` — partial-clone filter spec. Valid values:
    `{:blob, :none}`, `{:blob, {:limit, bytes}}`,
    `{:tree, depth}`, `{:raw, "filter=..."}`. When set, the
    server ships a pack that omits blobs (or trees) matching
    the filter; omitted objects are fetched on demand. Implies
    `:lazy`-like semantics for omitted objects but keeps the
    default branch's commit+tree history resident.

  * `:if_unsupported` — `:error` (default) or `:ignore`. When
    `:filter` is set but the server doesn't advertise `filter`
    capability, `:error` fails the clone and `:ignore` proceeds
    as a full clone.

  * `:remote` — remote name to record. Default: `"origin"`.

## Matrix of modes

| Options | Clone-time fetch | On read |
|---|---|---|
| `clone(url)` | refs + all objects | local always |
| `clone(url, lazy: true)` | refs only | per-object fetch |
| `clone(url, filter: {:blob, :none})` | refs + commits + trees | blobs fetched on read |
| `clone(url, filter: ..., lazy: true)` | refs only | everything on read |

## `:path` + `:lazy` / `:filter`

On-disk partial clones are not yet supported; `:path` combined
with `:lazy` or `:filter` returns
`{:error, :disk_partial_clone_unsupported}`.

## Returns

`{:ok, %Repository{}}` with `:mode => :eager` for full clones
and `:mode => :lazy` for any form of partial/lazy clone. Use
`Exgit.Repository.materialize/2` to convert `:lazy` → `:eager`
after reading what you need.

# `fetch`

```elixir
@spec fetch(Exgit.Repository.t(), remote(), keyword()) ::
  {:ok, Exgit.Repository.t()} | {:error, term()}
```

Fetch updates from a remote into an existing repository.

    {:ok, repo} = Exgit.fetch(repo, "https://github.com/user/repo")

Lists the remote's refs, fetches any objects reachable from them,
and records remote-tracking refs under `refs/remotes/<remote>/`
(for branches) and `refs/tags/` (for tags). Local branches and
`HEAD` are never moved.

## Options

  * `:remote` — remote name to record tracking refs under.
    Default: `"origin"`.

  * `:prefix` — ref prefixes to list on the remote.
    Default: `["refs/heads/", "refs/tags/"]`.

## Returns

`{:ok, %Repository{}}` with the updated object and ref stores, or
`{:error, reason}` when listing refs or fetching the pack fails.

# `init`

```elixir
@spec init(keyword()) :: {:ok, Exgit.Repository.t()} | {:error, term()}
```

Create an empty bare repository.

    {:ok, repo} = Exgit.init()
    {:ok, repo} = Exgit.init(path: "/tmp/repo.git")

## Options

  * `:path` — create the repo on disk at this path, backed by
    `ObjectStore.Disk`/`RefStore.Disk`. Default: in-memory
    (lost on process exit).

## Returns

`{:ok, %Repository{}}` with `HEAD` pointing at `refs/heads/main`.
When `:path` is set and any directory or file write fails, returns
`{:error, {:init_failed, reason}}` with the underlying posix reason.

# `open`

```elixir
@spec open(
  Path.t(),
  keyword()
) :: {:ok, Exgit.Repository.t()} | {:error, term()}
```

Open an existing on-disk repository.

    {:ok, repo} = Exgit.open("/tmp/repo.git")

`path` must point at a git directory — a bare repo or a `.git`
directory — containing a `HEAD` file and an `objects` directory.
The repo's `config` file is read when present; a missing or
unparseable config falls back to an empty `Exgit.Config`.

## Returns

`{:ok, %Repository{}}` backed by `ObjectStore.Disk`/`RefStore.Disk`,
or `{:error, {:not_a_repository, reason}}` when the path doesn't
look like a git directory.

# `push`

```elixir
@spec push(Exgit.Repository.t(), remote(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Push local refs to a remote.

The `:refspecs` keyword takes a list where each entry is one of:

  * `"refs/heads/main"` — a ref name; pushes the local sha for that
    ref to the same name on the remote (creating it if absent).
  * `{:delete, "refs/heads/branch"}` — delete the ref on the remote.

When `:refspecs` is omitted, the repo's current branch — `HEAD`'s
symref target — is pushed. If `HEAD` is not a symbolic ref (e.g.
detached or unset), the push fails with `{:error, :no_refspecs}`.

## Errors

  * `{:error, :no_refspecs}` — `:refspecs` omitted and `HEAD`
    doesn't point at a branch.
  * `{:error, {:local_ref_not_found, name}}` — a named refspec
    doesn't resolve in the local ref store. The whole push fails;
    nothing is sent to the remote. (Deleting a ref the remote
    doesn't have remains a silent no-op.)

Returns `{:ok, %{ref_results: [...]}}` on success.

---

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