git-spawn 0.1.0

Async wrapper around the git CLI: builder commands, typed parsers, high-level workflow helpers
Documentation

git-spawn

Crates.io Docs.rs CI License

An async Rust wrapper around the git CLI. Each git subcommand is a builder-style struct; .execute().await spawns git as a subprocess and returns typed output.

use git_spawn::{GitCommand, Repository};

#[tokio::main]
async fn main() -> git_spawn::Result<()> {
    let repo = Repository::open("/path/to/repo")?;

    repo.add().all().execute().await?;
    repo.commit().message("snapshot").execute().await?;
    repo.push().remote("origin").refspec("main").execute().await?;

    Ok(())
}

Install

[dependencies]
git-spawn = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

MSRV: 1.85 (Rust 2024 edition).

Capabilities

  • Porcelain: init, clone, add, commit, status, log, diff, show, branch, checkout, switch, merge, rebase, pull, push, fetch, remote, tag, stash, reset, restore, rm, mv
  • Plumbing: rev-parse, ls-files, ls-tree, cat-file, hash-object, update-ref, for-each-ref
  • Advanced: worktree, submodule, bisect, cherry-pick, grep, config, reflog
  • Typed parsers (behind the parse feature, on by default) for status --porcelain=v1 -z, log with a fixed token format, and diff --name-status -z
  • Higher-level workflow helpers (behind the workflow feature, off by default) — repo.info(), repo.branches(), repo.tags(), repo.history(), and repo.workflow() for one-call repo state, typed branch / tag / commit listings, and common compositions like feature_branch, commit_all, sync, squash_merge
  • Escape hatches on every command (.arg, .args, .flag, .option) so flags the typed API hasn't surfaced are still reachable

Choosing a git library for Rust

This crate is one of three realistic options. Pick based on what you're building, not which is "best."

Project What it is You need git installed Async-native Honors local git config, hooks, credential helpers
git-spawn Async subprocess wrapper around the git CLI yes yes yes
git2 Rust bindings to libgit2 (C) no no partial
gix Pure-Rust (gitoxide) no some partial

When to reach for git-spawn

  • You're automating workflows a human would script in bash: commits, pushes, rebases, cherry-picks, worktree setup, release tagging.
  • You want behavior to match exactly what the user's git does on the host, including ~/.gitconfig, core.* settings, pre-commit hooks, SSH/HTTPS credential helpers, and safe.directory.
  • You're already in a tokio program and want to run several git operations concurrently (fetching multiple remotes, building repos in parallel).
  • You need a feature that libgit2 / gix haven't implemented yet. Any git flag works via the escape hatches.

Trade-offs:

  • A git binary must be on PATH at runtime.
  • Each operation has process-spawn overhead (low hundreds of microseconds to a few milliseconds). Fine for workflow automation; not fine for tight loops over thousands of objects.
  • Output parsing is on you (or the parse feature).

When to reach for git2

  • You need in-process object database access (walking trees, reading blobs, creating commits) without spawning a subprocess per call.
  • You're building a tool that should work without requiring users to have git installed (GUIs, IDE plugins, CI containers).
  • You're comfortable with a C dependency: git2 links libgit2, which means a C compiler / CMake (or the vendored build) at build time.
  • Your program is sync or you're OK running libgit2 calls on a blocking thread pool.

Trade-offs:

  • No first-class async. You'll use spawn_blocking if you're in tokio.
  • libgit2 doesn't invoke the user's git hooks or credential helpers by default; you implement credential callbacks yourself.
  • Some newer git features lag behind the CLI (partial clone variants, SHA-256, sparse checkout modes).

When to reach for gix

  • You want a pure-Rust stack: no C toolchain, deterministic builds, easy cross-compilation, no libgit2 CVEs to track.
  • You need high-throughput object access or want to build sophisticated tooling on top of git's data model. gix is split into many focused crates so you can take only what you need.
  • You're willing to accept a still-evolving API in some areas. The read paths are solid; some write/network paths are newer.

Trade-offs:

  • Like git2, doesn't run git hooks or credential helpers for you.
  • Not every git feature is implemented yet; check the gitoxide project board for status of what you need.

Quick decision guide

  • "I'm calling git push / git rebase / git clone on behalf of a user." -> git-spawn.
  • "I'm walking commit history to generate a report, or reading blobs, and I can't require git to be installed." -> git2 if you need maturity and don't mind C; gix if you want pure Rust.
  • "I'm building a merge engine, a git server, or a CAS-backed fetcher." -> gix.
  • "I want to let a user pick a commit and cherry-pick it onto another branch, respecting their hooks." -> git-spawn.

Usage

Repository handle

use git_spawn::{GitCommand, Repository};

async fn demo() -> git_spawn::Result<()> {
    // Open an existing repo (cheap, no process spawn).
    let repo = Repository::open("/path/to/repo")?;

    // Or initialize a new one.
    let fresh = Repository::init("/tmp/new-repo").await?;

    // Or clone.
    let cloned = Repository::clone(
        "https://github.com/octocat/Hello-World.git",
        "/tmp/hello",
    ).await?;

    Ok(())
}

Repository is cheap and cloneable; the accessor methods (.add(), .commit(), .log(), ...) return commands pre-scoped to the repo's working directory.

Typed parsers

use git_spawn::{GitCommand, Repository};
use git_spawn::command::status::StatusFormat;
use git_spawn::parse::{parse_status, StatusKind};

async fn modified_paths() -> git_spawn::Result<()> {
    let repo = Repository::open("/path/to/repo")?;
    let out = repo.status()
        .format(StatusFormat::PorcelainV1)
        .null_terminate()
        .execute()
        .await?;

    for entry in parse_status(&out.stdout)? {
        if entry.worktree == StatusKind::Modified {
            println!("modified: {}", entry.path);
        }
    }
    Ok(())
}

The parse feature (on by default) also provides parse_log (paired with LOG_FORMAT) and parse_diff_name_status. Enable the serde feature to get Serialize / Deserialize on the parsed types.

Workflow helpers (opt-in)

Enable the workflow feature for one-call repo state, typed listings, and common compositions:

[dependencies]
git-spawn = { version = "0.1", features = ["workflow"] }
use git_spawn::Repository;

async fn quick_status() -> git_spawn::Result<()> {
    let repo = Repository::open("/repo")?;

    let info = repo.info().await?;
    println!("{} (dirty: {}, ahead {} / behind {})",
        info.branch.as_deref().unwrap_or("(detached)"),
        info.dirty, info.ahead, info.behind);

    for b in repo.branches().list().await? {
        println!("  {}{}", if b.current { "* " } else { "  " }, b.name);
    }

    for c in repo.history().max_count(5).execute().await? {
        println!("  {} {}", c.short_sha, c.subject);
    }

    // Multi-step shortcuts.
    repo.workflow().feature_branch("feature/x", "main").await?;
    repo.workflow().commit_all("wip").await?;
    Ok(())
}

See the module docs for info, branches, tags, history, and workflow for the full surface.

Escape hatches

Every command supports .arg, .args, .flag, and .option for flags that don't yet have a typed builder method:

use git_spawn::{GitCommand, Repository};

async fn shortstat() -> git_spawn::Result<()> {
    let repo = Repository::open("/repo")?;
    // `--shortstat` isn't a typed method on DiffCommand, but this still works:
    let out = repo.diff().cached().arg("--shortstat").execute().await?;
    println!("{}", out.stdout);
    Ok(())
}

Timeouts, env, working dir

use std::time::Duration;
use git_spawn::{GitCommand, Repository};

async fn careful_fetch() -> git_spawn::Result<()> {
    let repo = Repository::open("/repo")?;
    let mut cmd = repo.fetch();
    cmd.remote("origin")
        .with_timeout(Duration::from_secs(30))
        .env("GIT_TERMINAL_PROMPT", "0");
    cmd.execute().await?;
    Ok(())
}

Feature flags

Flag Default Purpose
parse on Typed parsers for status / log / diff output
serde off Serialize / Deserialize on parsed types
workflow off Higher-level helpers: info, branches, tags, history, workflow compositions (implies parse)

Contributing

PRs welcome. Please run before submitting:

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo doc --all-features --no-deps

License

Licensed under either of

at your option.