crates/gunnar-client/Cargo.toml
[package]
name = "gunnar-client"
description = "gunnar's git client: clone/fetch over SSH, send-pack (push), pull-mirroring and gix porcelain. Runs on a normal OS, with real working trees, ~/.ssh and ssh-agent."
readme = "README.md"
# 1.5.1 (2026-10-26, lane GUN-INSTALL / G-BOTH, the R5C publish): master differs from crates.io, so
# it gets its own version; a patch: cargo-semver-checks finds no API change against crates.io 1.5.0.
version = "1.6.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
# On the `cargo install gunnar` chain (owner order 2026-10-22).
publish = true
authors.workspace = true
repository.workspace = true
[features]
# **`porcelain` is ON by default, so `gunnar` — the git command a human types —
# is unchanged.** What the feature buys back is a client that is *only* the wire.
#
# This crate is two crates wearing one name:
#
# * the WIRE — `send_pack` (the half of git gix does not have), `ssh` (the
# russh dialler, via `nornir-git`'s transport), `transport`, and `mirror`'s
# ref PLAN (`plan_ref_sync` / `is_mirrored_ref`, which are two `BTreeMap`s and
# a string prefix). Object ids are `gix-hash`, a 32-byte array and a
# two-variant enum, and that is the entire gix surface it needs.
# * the PORCELAIN — `clone`, `fetch`, `push`, `status`, `attributes`, `objects`,
# `shallow`, `pack_source`, `wire`, and `mirror`'s on-disk registry. Every one
# of those opens a `gix::Repository` on a real filesystem with a real
# worktree.
#
# `gunnar-server`'s replication is the first caller that wants the first
# half and none of the second: it takes its objects from the storage ENGINE
# (`GitServe::emit_pack`) and only needs somebody to dial `git-receive-pack` and
# frame the conversation. Before this split, saying so cost the appliance +128
# packages — gix, ~57 `gix-*`, and the reqwest/rustls/icu tail under
# `nornir-git`'s gix — for a repository handle it never opened.
#
# The `porcelain` name is deliberate and matches `nornir-git`'s own: a caller is
# choosing history and worktrees, not a dependency.
default = ["porcelain"]
porcelain = [
"dep:gix",
"dep:gix-object",
"dep:gix-error",
"dep:gunnar-pack",
"nornir-git/porcelain",
]
[dependencies]
# The v0 push wire format, donation-shaped and consumed from BOTH ends:
# `gunnar-wire`'s receive-pack reads what this crate's send-pack writes. The
# grammar used to live in `src/send_pack.rs` and a second copy of it in
# `gunnar-wire`; that module is now a seam over this crate.
#
# `features = ["blocking-io"]` is named out loud because the workspace entry
# turns default features OFF, so that a server consumer pays nothing for a
# pkt-line framer it already has. A client wants the driver.
gunnar-sendpack = { workspace = true, features = ["blocking-io"] }
# The framing layer. Every byte of the send-pack conversation goes through it.
gunnar-pktline = { workspace = true }
# The pack WRITER. gix has no send-pack, so the client writes its own packs,
# with the same writer the server uses to serve upload-pack (the crate layout).
#
# `delta` is the encoder half (`gunnar-delta`). It is on HERE and nowhere
# else: a pushing client writes packs out of loose objects, so it has no deltas
# to reuse and must compute them, while the server's `write_pack_copy` reuses
# what the client already paid for and needs no encoder at all. Measured before
# this edge existed, on 19 real repositories: gunnar-client stored 407.08 MiB on
# the server at 0.0% deltified, stock git 88.14 MiB at 74.0%.
#
# OPTIONAL, riding `porcelain`: it is reached from `push.rs` and `pack_source.rs`
# (which build a pack out of a worktree's loose objects) and from one
# `Error::Pack` variant. The replication half never writes a pack — the storage
# engine emits one — so with the feature off `gunnar-pack`, `gunnar-delta` and
# the `gix-object` 0.63 tail under them leave the graph.
gunnar-pack = { workspace = true, features = ["delta"], optional = true }
# The client half of git-over-SSH, REUSED not twinned:
# `parse_ssh_url`, `ls_remote` (protocol-v2 ls-refs + v0 fallback, sha256 oids),
# `connect_upload_pack` (russh → blocking I/O bridge with the in-runtime drop),
# `gitio::clone_or_fetch`, `gitio::set_head_and_checkout`.
#
# `default-features = false` comes from the workspace entry; `porcelain` above
# turns nornir-git's own porcelain back on. What is left with it off is exactly
# `ssh`/`sshkey`/`deploykey`, which is what `src/ssh.rs` names.
nornir-git = { workspace = true }
# The CLIENT is a normal-OS program — the no-OS rule binds the server only.
# gix's porcelain is used
# directly and deliberately: `status` and the blocking git transport are exactly
# the features the server may never have, and exactly the ones a git client is
# made of.
# Only two features are added on top of the workspace set, and each is here for
# one named caller: `status` (which brings dirwalk + index + blob-diff) for
# `porcelain::status`, and `blocking-network-client` for the git transport
# `fetch::fetch_with_io` drives. Checkout and tree editing are NOT enabled;
# those verbs delegate to `nornir-git`, so pulling `worktree-mutation` in here
# would be paying for a second implementation of something already reused.
#
# OPTIONAL, riding `porcelain`. Nothing in the wire half opens a repository.
gix = { workspace = true, features = ["status", "blocking-network-client"], optional = true }
# The PLUMBING half of the same object model. Until 2026-10-02 it was
# deliberately a DIFFERENT major than the one inside `gix` above (`gix-object`
# 0.63 vs the 0.64 gix 0.87 carried); the gix 0.88 wave put both on 0.65, so the
# object model is now ONE type set. `gunnar-pack` — a leaf shared with `edda` —
# is written against the plumbing crate, and `src/pack_source.rs` is the single
# 3-method bridge that lets a `gix::Repository` be its object source. `gix-hash`
# is NOT duplicated: gix 0.88 asks for ^0.27 and this workspace pins 0.27.
#
# OPTIONAL, riding `porcelain`: `pack_source.rs`'s bridge is its only caller.
gix-object = { workspace = true, optional = true }
# The `Find` bridge above returns gix 0.88's `gix_error::ExnResult`.
gix-error = { workspace = true, optional = true }
# NOT optional, and that is the point of the split: object ids are the wire's
# vocabulary. `gix-hash` is already in the appliance graph today.
gix-hash = { workspace = true }
russh = { workspace = true }
ssh-key = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
# The mirror registry is a small JSON file under the mirror root. No database.
serde_json = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
# `tests/push_deltas.rs` drives `write_pack` — the whole-object writer this
# client no longer calls — as its NEGATIVE CONTROL, so the 0%-deltified reading
# it produces proves the `verify-pack -v` parser can tell the two apart. An
# integration test links only the crate and its dev-dependencies, so the normal
# edge above does not reach it.
gunnar-pack = { workspace = true, features = ["delta"] }
# `tests/lfs_checkout.rs` clones WITH a checkout (`fetch_then_checkout`), which
# is gix's `worktree-mutation`. The crate itself never checks out (those verbs
# delegate to `nornir-git`), and until nornir-git 0.2.4 moved to gix 0.90 its
# gix 0.88 lent the feature to this one by unification. The test names it now.
gix = { workspace = true, features = ["worktree-mutation", "blocking-network-client"] }