crates/nornir-git/Cargo.toml
[package]
name = "nornir-git"
version = "0.2.4"
edition = "2021"
description = "Pure-Rust git helpers over gix — read-side inspection (gitio), history deep-clean / path-removal (gitclean), an SSH transport bridge (russh → gix blocking I/O), and ed25519 OpenSSH deploy-key minting (sshkey). No `git`/`ssh-keygen` subprocess, no libgit2/C; the airgap-clean git leaf lifted out of nornir."
license = "MIT OR Apache-2.0"
repository = "https://git.gunnar.rs/nordisk/edda"
keywords = ["git", "gix", "gitoxide", "history", "ssh"]
categories = ["development-tools"]
exclude = ["/.nornir/"]
[features]
# **`porcelain` is ON by default, so every existing consumer is unchanged.**
#
# Two halves live in this crate and only one of them needs gix:
#
# * the SSH TRANSPORT — [`ssh`] (russh dialler, `parse_ssh_url`,
# `connect_exec`/`upload_pack`/`receive_pack`, the blocking-I/O bridge),
# [`sshkey`] (ed25519 OpenSSH key minting) and [`deploykey`] (where the key
# is). MEASURED 2026-08-30: `src/ssh.rs` names `gix` in COMMENTS only, and
# `sshkey.rs` / `deploykey.rs` not at all. russh + ssh-key, nothing else.
# * the PORCELAIN — [`gitio`], [`gitops`], [`gitclean`], [`fleet`]. These are
# gix, and gix here is asked for with `status`, `blocking-network-client`,
# `blocking-http-transport-reqwest-rust-tls` and `sha256`, which is ~57
# `gix-*` crates plus the whole reqwest/rustls/icu tail.
#
# A caller that only wants to open an authenticated `git-receive-pack` channel
# — which is what gunnar's §11.1 appliance replication is — was paying for all
# of it. `default-features = false` gives it the transport alone.
#
# It is a feature and not a separate crate because the transport and the
# porcelain are one story (`gitio` drives gix's blocking transport over
# [`ssh::SshChannel`]) and splitting the crate would put a seam through the
# middle of that sentence.
default = ["porcelain", "https", "gix-defaults"]
# The gix half. Named `porcelain` rather than `gix` because that is what a
# caller is choosing: history, worktrees, fetch negotiation — not a dependency.
#
# 2026-10-12 (slim client): `porcelain` asks gix for EXACTLY what this crate's
# code calls and nothing else — `default-features = false` on the gix edge below.
# The two things that used to ride in with it unasked are now their own
# features, both ON in `default`, so a consumer that takes the default sees the
# graph it always saw:
porcelain = ["dep:gix"]
# The https fetch fallback in `gitio::clone_or_fetch` (gix's reqwest + rustls
# transport). `clone_or_fetch` coerces every `http(s)://host/owner/repo` URL to
# its SSH form first, so this is reached only for an https URL `https_to_ssh`
# cannot rewrite. A caller that speaks https itself, or never, turns it off and
# drops reqwest / hyper / h2 / rustls-platform-verifier / tokio-rustls.
https = ["porcelain", "gix/blocking-http-transport-reqwest-rust-tls"]
# gix's own `default` set (comfort, extras: blame, merge, notes, mailmap,
# dirwalk, worktree-archive, revparse-regex, …). Nothing in this crate calls it;
# it is here only so `default` keeps the graph it had.
gix-defaults = ["porcelain", "gix/default"]
# Functional-status emit MODE: pull `nornir-testmatrix` and light this crate's
# `selftest::emit` bridge so its real git ops record honest matrix rows. OFF by
# default — release strips the emit to an `#[inline]` no-op and does NOT link
# `nornir-testmatrix`.
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix"]
[dependencies]
anyhow = "1"
# The `deploykey` model (resolution ladder, probe outcome, key state) is carried
# across a UI seam as `state_json`, so it derives Serialize/Deserialize. serde is
# already in this crate's tree via gix — no new transitive weight.
serde = { version = "1", features = ["derive"] }
# Worktree-freshness digest routes through the ONE shared hash leaf
# (`Sha256Hasher`) instead of pulling `sha2` + a hand-rolled hex loop.
nornir-hash = { version = "0.1.0", path = "../nornir-hash" }
# Pure-Rust ed25519 OpenSSH deploy-key minting (`sshkey` module) — the canonical
# home for what nornir + dwarves used to twin. In-process key gen, NO ssh-keygen
# subprocess (L9). `russh` (below) already pulls ssh-key 0.7; declared explicitly
# so `sshkey` uses it directly rather than reaching through `russh::keys`.
# pre-release: no stable yet (2026-10-02); exact pin because rc APIs change between
# candidates; move to the stable when it ships. (russh 0.63 pins =0.7.0-rc.11 too.)
ssh-key = { version = "=0.7.0-rc.11", default-features = false, features = ["ed25519", "std", "rand_core"] }
rand_core = "0.10"
# rand_core 0.10 dropped OsRng and its `getrandom` feature; the OS CSPRNG is now
# getrandom's `SysRng` (2026-10-02, W3 crypto bump).
getrandom = { version = "0.4", features = ["sys_rng"] }
# OPTIONAL, riding `porcelain` (see the feature block). Off, gix and its ~57
# `gix-*` crates plus the reqwest/rustls/icu tail leave the graph entirely and
# what is left is russh + ssh-key: the transport half.
# `worktree-mutation` is what exposes `gix::worktree::state::checkout`, the ONE
# worktree writer this crate has (`gitio::materialize_worktree`). It was already
# in the graph on oden by feature unification with a sibling member, which is
# exactly the way a dependency comes to look satisfied in a workspace and is
# absent for a consumer who takes this crate alone — so it is asked for here.
# `default-features = false` (2026-10-12): the set below is what `gitio`,
# `gitops`, `gitclean` and `fleet` call; `https` / `gix-defaults` add the rest.
gix = { version = "0.90", default-features = false, features = ["max-performance", "sha1", "sha256", "revision", "status", "worktree-mutation", "blocking-network-client"], optional = true }
# 0.61 pinned `ecdsa =0.17.0-rc.18`, which cannot coexist with the stable
# ecdsa 0.17 the rest of the RustCrypto set now resolves to — 0.63 is the first
# russh on the stable set (ssh-key =0.7.0-rc.11). 2026-10-02, W3 crypto bump.
russh = "0.65"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "io-util", "sync", "fs", "time"] }
# UNUSED since 2026-10-06 and kept ON PURPOSE for now: the bridge it supplied
# (`SyncIoBridge`) could not see the session end, so a writer parked on the SSH
# window slept forever; the reader/writer are this crate's own now
# (`ssh::SshReader` / `ssh::SshWriter`). Dropping the line changes this crate's
# entry in EVERY sibling's Cargo.lock (gunnar, the ops kits, ...), which breaks
# their `--locked` builds on a release night. Delete it with the next lock wave.
tokio-util = { version = "0.7", features = ["io-util"] }
nornir-testmatrix = { version = "0.2.7", path = "../nornir-testmatrix", optional = true }
[dev-dependencies]
tempfile = "3"
# `tests/ssh_exec.rs` runs a real russh SERVER on loopback and `exec`s stock
# `git upload-pack` / `git receive-pack` under it, so the interop assertions are
# against the other implementation rather than against a fake. That needs tokio's
# process driver — dev-only, so nothing is added to what this crate ships.
tokio = { version = "1", features = ["process", "rt-multi-thread", "macros", "io-util", "net", "sync", "time"] }