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 std::path::Path;
use vcs_git::{Git, GitApi};
let git = Git::new();
let repo = Path::new(".");
let branch = git.current_branch(repo).await?; let status = git.status(repo).await?; let log = git.log(repo, 10).await?;
Stage, commit, inspect
use std::path::{Path, PathBuf};
use vcs_git::{Git, GitApi};
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let git = Git::new();
git.add(repo, &[PathBuf::from("src/lib.rs")]).await?; git.commit(repo, "feat: tidy lib").await?;
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 std::path::Path;
# use vcs_git::{Git, GitApi};
# 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(()) }
Worktrees
Manage linked worktrees with structured results:
use vcs_git::{Git, GitApi, WorktreeAdd};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let git = Git::new();
git.worktree_add(repo, WorktreeAdd::create_branch("/tmp/feature", "feature", "HEAD"))
.await?;
for wt in git.worktree_list(repo).await? { println!("{} -> {:?}", wt.path.display(), wt.branch);
}
git.worktree_remove(repo, Path::new("/tmp/feature"), false).await?;
# Ok(()) }
Distinguish failures structurally
# use std::path::Path;
# use vcs_git::{Git, GitApi};
# 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 processkit::{Reply, ScriptedRunner};
use std::path::Path;
use vcs_git::{Git, GitApi};
# 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