vcs-git 0.1.0

Automate the Git CLI from Rust through process execution.
Documentation

vcs-git

Automate the Git CLI from Rust through process execution. Part of the vcs-toolkit-rs workspace.

Typed, repo-scoped, async commands over the git binary, behind a mockable interface. Commands run inside an OS job (via processkit) so no git subprocess is ever orphaned, return the structured Error, and honour an optional timeout.

Inside an async context (every method is async):

use vcs_git::{Git, GitApi};
use std::path::Path;

let git = Git::new();
let repo = Path::new(".");

let branch = git.current_branch(repo).await?;   // String, e.g. "main"
let status = git.status(repo).await?;           // Vec<StatusEntry>
let log    = git.log(repo, 10).await?;          // Vec<Commit>, newest first

Stage, commit, inspect

use vcs_git::{Git, GitApi};
use std::path::{Path, PathBuf};

# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let git = Git::new();

git.add(repo, &[PathBuf::from("src/lib.rs")]).await?;   // `git add -- src/lib.rs`
git.commit(repo, "feat: tidy lib").await?;              // `git commit -m …`

// `diff_is_empty` is the exit-code answer of `git diff --quiet`:
if !git.diff_is_empty(repo).await? {
    println!("working tree still has unstaged changes");
}

for c in git.log(repo, 5).await? {
    println!("{} {}{} <{}>", c.short_hash, c.subject, c.author, c.date);
}
# Ok(()) }

Renames come back structured

status runs git status --porcelain=v1 -z, so a rename carries both paths:

# use vcs_git::{Git, GitApi};
# use std::path::Path;
# async fn demo(git: &Git, repo: &Path) -> Result<(), processkit::Error> {
for entry in git.status(repo).await? {
    match entry.orig_path {
        Some(from) => println!("rename {from} -> {}", entry.path),
        None => println!("{} {}", entry.code, entry.path),
    }
}
# Ok(()) }

Distinguish failures structurally

# use vcs_git::{Git, GitApi};
# use std::path::Path;
# async fn demo(git: &Git, repo: &Path) {
match git.checkout(repo, "nope").await {
    Ok(()) => {}
    Err(processkit::Error::Exit { code, stderr, .. }) => eprintln!("git exited {code}: {stderr}"),
    Err(processkit::Error::Timeout { .. }) => eprintln!("timed out"),
    Err(e) => eprintln!("{e}"),
}
# }

Consumers depend on the GitApi trait and substitute a fake in tests — enable the mock feature for a mockall-generated MockGitApi, or inject a fake process runner with Git::with_runner(processkit::ScriptedRunner::new()…):

use vcs_git::{Git, GitApi};
use processkit::{Reply, ScriptedRunner};
use std::path::Path;

# async fn demo() {
let git = Git::with_runner(
    ScriptedRunner::new().on(["rev-parse"], Reply::ok("feature\n")),
);
assert_eq!(git.current_branch(Path::new(".")).await.unwrap(), "feature");
# }

Requires the git binary on PATH.

License

MIT