Skip to main content

Module guide

Module guide 

Source
Expand description

§vcs-git — Git CLI guide

Typed, repo-scoped, async commands over the git binary, behind a mockable interface. Every method runs git inside an OS job (via processkit) so a subprocess is never orphaned, returns the structured processkit::Error, and honours an optional timeout. Consumers code against the GitApi trait and swap in a fake in tests.

Caller-supplied names, revisions, ranges, remotes, and URLs that land in a bare positional argv slot are guarded automatically — a value that is empty or begins with - is rejected with an Error::Spawn before anything spawns, so it can’t be smuggled in as a flag.

§Construction & configuration

use vcs_git::Git;

let git = Git::new();                                         // real, job-backed runner
let git = Git::new().default_timeout(Duration::from_secs(30)); // every cmd → Error::Timeout past 30s
  • Git::new() — the production client over the real job-backed runner.
  • Git::with_runner(runner) — inject a fake ProcessRunner (e.g. processkit::ScriptedRunner) for hermetic tests; see Testing & mocking.
  • default_timeout(Duration) — builder; arms a per-command timeout.

new, with_runner, and default_timeout all come from the processkit::cli_client! macro that defines Git.

§Hardening (Git::hardened())

Running git inside an untrusted checkout executes that repository’s hooks and honours its config — arbitrary code execution by default. Git::hardened() (equivalently Git::new().harden()) applies a containment profile to every command the client runs:

  • Disables hookscore.hooksPath is pinned to /dev/null via git’s env-based config (GIT_CONFIG_COUNT/KEY_n/VALUE_n) — and core.fsmonitor is forced false (a config-driven daemon launch).
  • Scrubs inherited GIT_* redirectors so a poisoned parent environment can’t point commands at another repo: GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_NAMESPACE, GIT_CEILING_DIRECTORIES, GIT_CONFIG_PARAMETERS, GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM.
  • Skips system config (GIT_CONFIG_NOSYSTEM=1) and keeps terminal prompts off everywhere (GIT_TERMINAL_PROMPT=0).

It does not sandbox the git binary or vet the repo’s content. harden() is chainable on any runner — Git::with_runner(rec).harden() works in tests — but Git::hardened() is the shorthand for the common case. See Security & hardening.

use vcs_git::Git;

let git = Git::hardened();   // drive a repo you didn't create — hooks/config neutered

§The cwd-bound view (GitAt)

Most GitApi methods take a leading dir: &Path. When you drive one directory repeatedly, bind it once with git.at(&path) — the returned GitAt drops that argument:

let git = Git::new();
let at = git.at(repo);            // GitAt — Copy, borrows the client + path
let branch = at.current_branch().await?;   // == git.current_branch(repo)
at.commit("feat: thing").await?;           // == git.commit(repo, "…")

GitAt is Copy for every runner (it holds only two references). The dir-taking GitApi methods stay on Git so one client can drive many directories — e.g. linked worktrees. Through the facade, vcs_core::Repo::git_at yields the same handle.

§Inherent run_args / run_raw_args

The object-safe GitApi trait can’t take &[&str], so two inherent helpers do — no Vec<String> allocation:

let out = git.run_args(&["status", "-s"]).await?;   // String, errors on non-zero exit
let res = git.run_raw_args(&["rev-parse", "HEAD"]).await?; // ProcessResult<String>, never errors on exit

§Status & staging

Working-tree inspection and the index.

async fn status(&self, dir: &Path) -> Result<Vec<StatusEntry>>;
async fn status_text(&self, dir: &Path) -> Result<String>;
async fn status_tracked(&self, dir: &Path) -> Result<Vec<StatusEntry>>;
async fn branch_status(&self, dir: &Path) -> Result<BranchStatus>;
async fn add(&self, dir: &Path, paths: &[PathBuf]) -> Result<()>;
async fn staged_is_empty(&self, dir: &Path) -> Result<bool>;
async fn conflicted_files(&self, dir: &Path) -> Result<Vec<String>>;
  • statusgit status --porcelain=v1 -z, parsed. Renames carry both paths.
  • status_text — the raw porcelain text (--porcelain=v1), unparsed.
  • status_trackedstatus ignoring untracked files (--untracked-files=no); “is the tracked tree dirty”, staged or not.
  • branch_status — a combined branch + working-tree snapshot in one spawn (status --porcelain=v2 --branch -z): HEAD, branch, upstream, ahead/behind, and change counts (BranchStatus) — the cheap primitive behind the facade’s Repo::snapshot. Use it for a prompt/status-bar line without N round-trips.
  • addgit add -- <paths> (the -- keeps a path from being read as a flag).
  • staged_is_emptygit diff --cached --quiet, exit-code mapped: true = nothing staged.
  • conflicted_filesgit diff --name-only --diff-filter=U -z; repo-relative paths with / separators, empty when there are none.
git.add(repo, &[PathBuf::from("src/lib.rs")]).await?;       // `git add -- src/lib.rs`

for entry in git.status(repo).await? {                       // Vec<StatusEntry>
    match entry.orig_path {
        Some(from) => println!("rename {from} -> {}", entry.path),
        None => println!("{} {}", entry.code, entry.path),
    }
}

if !git.staged_is_empty(repo).await? {                       // bool
    println!("index has staged changes");
}
for path in git.conflicted_files(repo).await? {             // Vec<String>
    println!("conflict: {path}");
}

§Commits & log

async fn log(&self, dir: &Path, max: usize) -> Result<Vec<Commit>>;
async fn log_range(&self, dir: &Path, range: &str, max: usize) -> Result<Vec<Commit>>;
async fn commit(&self, dir: &Path, message: &str) -> Result<()>;
async fn commit_paths(&self, dir: &Path, spec: CommitPaths) -> Result<()>;
async fn last_commit_message(&self, dir: &Path) -> Result<String>;
async fn rev_list_count(&self, dir: &Path, range: &str) -> Result<usize>;
  • log — the latest max commits, newest first.
  • log_range — commits in range (e.g. main..HEAD), newest first, up to max.
  • commitgit commit -m <message> of the staged index.
  • commit_paths — commit exactly the spec’s paths’ working-tree content, ignoring the index (commit [--amend] -m <message> --only -- <paths>); built through CommitPaths.
  • last_commit_message — the full last message (log -1 --format=%B), e.g. to pre-fill an amend.
  • rev_list_count — how many commits a range spans (rev-list --count <range>), e.g. how far ahead of the upstream you are — cheaper than fetching and counting log_range.
git.commit(repo, "feat: tidy lib").await?;
for c in git.log(repo, 5).await? {                          // Vec<Commit>, newest first
    println!("{} {} — {} <{}>", c.short_hash, c.subject, c.author, c.date);
}
let ahead = git.log_range(repo, "origin/main..HEAD", 50).await?; // Vec<Commit>
let n = git.rev_list_count(repo, "origin/main..HEAD").await?;    // usize — # commits ahead
let _ = (ahead, n);

A commit that finds nothing to record fails; classify it with is_nothing_to_commit rather than treating it as a real error.

§Branches

async fn branches(&self, dir: &Path) -> Result<Vec<Branch>>;
async fn create_branch(&self, dir: &Path, name: &str) -> Result<()>;
async fn branch_exists(&self, dir: &Path, name: &str) -> Result<bool>;
async fn delete_branch(&self, dir: &Path, name: &str, force: bool) -> Result<()>;
async fn rename_branch(&self, dir: &Path, old: &str, new: &str) -> Result<()>;
async fn is_merged(&self, dir: &Path, branch: &str, target: &str) -> Result<bool>;
async fn set_upstream(&self, dir: &Path, branch: &str, upstream: &str) -> Result<()>;
async fn current_branch(&self, dir: &Path) -> Result<String>;
  • branches — local branches (git branch), current one flagged.
  • create_branchgit branch <name>, without switching to it.
  • branch_existsshow-ref --verify --quiet refs/heads/<name>, exit-code mapped.
  • delete_branchbranch -d, or -D when force.
  • rename_branchbranch -m <old> <new>.
  • is_merged — whether branch is fully merged into target (branch --merged <target>).
  • set_upstreambranch --set-upstream-to=<upstream> <branch>.
  • current_branchrev-parse --abbrev-ref HEAD (e.g. "main"; "HEAD" when detached).
if !git.branch_exists(repo, "feature").await? {            // bool
    git.create_branch(repo, "feature").await?;
}
git.set_upstream(repo, "feature", "origin/feature").await?;
if git.is_merged(repo, "feature", "main").await? {         // bool
    git.delete_branch(repo, "feature", false).await?;      // `branch -d feature`
}
for b in git.branches(repo).await? {                       // Vec<Branch>
    println!("{}{}", if b.current { "* " } else { "  " }, b.name);
}

§Revisions

async fn rev_parse(&self, dir: &Path, rev: &str) -> Result<String>;
async fn rev_parse_short(&self, dir: &Path, rev: &str) -> Result<String>;
async fn resolve_commit(&self, dir: &Path, rev: &str) -> Result<String>;
async fn is_unborn(&self, dir: &Path) -> Result<bool>;
async fn checkout(&self, dir: &Path, reference: &str) -> Result<()>;
async fn checkout_detach(&self, dir: &Path, commit: &str) -> Result<()>;
  • rev_parse — resolve a revision to its full hash (rev-parse <rev>).
  • rev_parse_short — the abbreviated hash (rev-parse --short <rev>), e.g. to label a detached HEAD.
  • resolve_commit — resolve to a commit hash, peeling annotated tags (rev-parse --verify <rev>^{commit}).
  • is_unborn — whether HEAD is unborn — a fresh repo with no commits (rev-parse --verify -q HEAD, exit-code mapped).
  • checkout — switch to a branch or revision (git checkout <reference>).
  • checkout_detach — check out a commit as a detached HEAD (checkout --detach <commit>).
if git.is_unborn(repo).await? {                            // bool
    println!("no commits yet");
}
let hash = git.rev_parse(repo, "HEAD").await?;             // String — full 40-hex sha
let short = git.rev_parse_short(repo, "HEAD").await?;      // String — abbreviated
let _ = (hash, short);
git.checkout(repo, "main").await?;

To carry uncommitted changes across a switch, see the composed inherent helper switch_with_stash.

§Worktrees

async fn worktree_list(&self, dir: &Path) -> Result<Vec<Worktree>>;
async fn worktree_add(&self, dir: &Path, spec: WorktreeAdd) -> Result<()>;
async fn worktree_remove(&self, dir: &Path, path: &Path, force: bool) -> Result<()>;
async fn worktree_move(&self, dir: &Path, from: &Path, to: &Path) -> Result<()>;
async fn worktree_prune(&self, dir: &Path) -> Result<()>;
  • worktree_listworktree list --porcelain, parsed into Vec<Worktree>.
  • worktree_addworktree add [-b <branch>] [--no-checkout] <path> [<commitish>]; built through WorktreeAdd.
  • worktree_removeworktree remove [--force] <path>.
  • worktree_moveworktree move <from> <to>.
  • worktree_pruneworktree prune, dropping stale admin entries.
git.worktree_add(repo, WorktreeAdd::create_branch("/tmp/feature", "feature", "HEAD"))
    .await?;                                                 // `worktree add -b feature /tmp/feature HEAD`

for wt in git.worktree_list(repo).await? {                  // Vec<Worktree>
    println!("{} -> {:?}", wt.path.display(), wt.branch);
}

git.worktree_remove(repo, Path::new("/tmp/feature"), false).await?;

For a synchronous best-effort removal in a Drop guard, see blocking::worktree_remove.

§Diff

async fn diff(&self, dir: &Path, spec: DiffSpec) -> Result<Vec<FileDiff>>;
async fn diff_text(&self, dir: &Path, spec: DiffSpec) -> Result<String>;
async fn diff_is_empty(&self, dir: &Path) -> Result<bool>;
async fn diff_range_is_empty(&self, dir: &Path, range: &str) -> Result<bool>;
async fn diff_stat(&self, dir: &Path, range: &str) -> Result<DiffStat>;
  • diff — parsed per-file unified diff for spec, layered on diff_text.
  • diff_text — raw git-format unified diff for spec (diff <spec> --no-color --no-ext-diff -M) — stable machine output. On an unborn repo, DiffSpec::WorkingTree diffs against the empty tree rather than failing.
  • diff_is_emptygit diff --quiet, exit-code mapped: are there unstaged modifications to tracked files? Untracked files are not counted — not a full “is the working tree clean?” check; use status for that.
  • diff_range_is_emptydiff --quiet <range>, exit-code mapped.
  • diff_stat — aggregate DiffStat for a range (diff --shortstat <range>).

DiffSpec selects what is compared: WorkingTree (vs HEAD) or Rev(String) (a revision or range).

if !git.diff_is_empty(repo).await? {
    println!("working tree has unstaged tracked changes");
}
for file in git.diff(repo, DiffSpec::WorkingTree).await? {  // Vec<FileDiff>
    println!("{:?} {}", file.change, file.path);
}
let raw = git.diff_text(repo, DiffSpec::Rev("main..HEAD".into())).await?; // String
let stat = git.diff_stat(repo, "main..HEAD").await?;        // DiffStat
println!("{} files, +{} -{}", stat.files_changed, stat.insertions, stat.deletions);
let _ = raw;

§Blame

async fn blame(&self, dir: &Path, path: &str, rev: Option<String>) -> Result<Vec<BlameLine>>;

Per-line authorship of path (blame --line-porcelain [<rev>] -- <path>); None blames the working tree’s HEAD.

for line in git.blame(repo, "src/lib.rs", None).await? {    // Vec<BlameLine>
    println!("{} {} {}", &line.commit[..8], line.author, line.content);
}

§Remotes & upstream

async fn remote_url(&self, dir: &Path, remote: &str) -> Result<String>;
async fn remote_add(&self, dir: &Path, name: &str, url: &str) -> Result<()>;
async fn remote_set_url(&self, dir: &Path, name: &str, url: &str) -> Result<()>;
async fn remote_branches(&self, dir: &Path, remote: &str) -> Result<Vec<String>>;
async fn remote_branch_exists(&self, dir: &Path, name: &str) -> Result<bool>;
async fn remote_head_branch(&self, dir: &Path) -> Result<Option<String>>;
async fn upstream(&self, dir: &Path) -> Result<Option<String>>;
  • remote_url — a remote’s URL (remote get-url <remote>).
  • remote_add / remote_set_urlremote add / remote set-url.
  • remote_branches — branch names on remote, without fetching (ls-remote --heads <remote>), with GIT_TERMINAL_PROMPT=0.
  • remote_branch_exists — whether origin has name without fetching, querying the fully-qualified ref so foo can’t tail-match bar/foo. Runs prompt-off with a 10s timeout; an unreachable remote reads as false, not an error.
  • remote_head_branchorigin’s default branch (short name) from symbolic-ref refs/remotes/origin/HEAD; None when unset.
  • upstream — the current branch’s upstream, e.g. Some("origin/main"); None when unset.
if let Some(up) = git.upstream(repo).await? {              // Option<String>
    println!("tracking {up}");
}
if let Some(default) = git.remote_head_branch(repo).await? { // Option<String>
    println!("origin default: {default}");
}
let exists = git.remote_branch_exists(repo, "main").await?;  // bool — best-effort
let _ = exists;

§Fetch / push / merge

async fn fetch(&self, dir: &Path) -> Result<()>;
async fn fetch_from(&self, dir: &Path, remote: &str) -> Result<()>;
async fn fetch_remote_branch(&self, dir: &Path, branch: &str) -> Result<()>;
async fn push(&self, dir: &Path, spec: GitPush) -> Result<()>;
async fn merge_squash(&self, dir: &Path, branch: &str) -> Result<()>;
async fn merge_commit(&self, dir: &Path, spec: MergeCommit) -> Result<()>;
async fn merge_no_commit(&self, dir: &Path, spec: MergeNoCommit) -> Result<()>;
async fn merge_abort(&self, dir: &Path) -> Result<()>;
async fn merge_continue(&self, dir: &Path) -> Result<()>;
async fn reset_merge(&self, dir: &Path) -> Result<()>;
async fn reset_hard(&self, dir: &Path, rev: &str) -> Result<()>;
  • fetchfetch --quiet from the default remote, prompt-off, retried on transient failures (3 attempts, 500 ms backoff).
  • fetch_from — fetch from a named remote; same containment and retry.
  • fetch_remote_branch — fetch one branch into its remote-tracking ref (fetch --quiet origin refs/heads/<b>:refs/remotes/origin/<b>); same retry.
  • pushpush [-u] <remote> <refspec>, prompt-off; built through GitPush.
  • merge_squash — stage a branch’s changes without committing (merge --squash).
  • merge_commitmerge [--no-ff] [-m <msg> | --no-edit] <branch>; with no message it takes the default merge message non-interactively. Built through MergeCommit.
  • merge_no_commit — merge without committing, for a dry run (merge --no-commit [--squash | --no-ff] <branch>). Built through MergeNoCommit.
  • merge_abortmerge --abort.
  • merge_continue — finish a merge after resolving conflicts (commit --no-edit, editor suppressed).
  • reset_merge — clear merge state, squash-safe (reset --merge).
  • reset_hard — move HEAD and the working tree to rev, discarding all staged and unstaged changes (reset --hard <rev>) — destructive; there is no undo for uncommitted work.
git.fetch(repo).await?;                                      // retried on transient failure
git.push(repo, GitPush::branch("feature").set_upstream()).await?; // `push -u origin feature`

match git.merge_commit(repo, MergeCommit::branch("feature").no_ff()).await {  // --no-ff, default message
    Ok(()) => {}
    Err(e) if is_merge_conflict(&e) => {
        // resolve conflicts, then:
        git.merge_continue(repo).await?;
    }
    Err(e) => return Err(e),
}

§Rebase & sequencer

async fn rebase(&self, dir: &Path, onto: &str) -> Result<()>;
async fn rebase_abort(&self, dir: &Path) -> Result<()>;
async fn rebase_continue(&self, dir: &Path) -> Result<()>;
async fn rebase_skip(&self, dir: &Path) -> Result<()>;
async fn cherry_pick(&self, dir: &Path, rev: &str) -> Result<()>;
async fn revert(&self, dir: &Path, rev: &str) -> Result<()>;

Every command here suppresses the editor (GIT_EDITOR=true, GIT_SEQUENCE_EDITOR=true) so it never hangs a headless caller.

  • rebase — rebase the current branch onto onto (rebase <onto>).
  • rebase_abort / rebase_continuerebase --abort / --continue.
  • rebase_skiprebase --skip; mainly for the apply backend’s “nothing to commit” stop (the default merge backend auto-drops emptied patches on --continue).
  • cherry_pick — apply a commit onto the current branch (cherry-pick <rev>); a conflict surfaces as an error classifiable by is_merge_conflict.
  • revert — revert a commit with the default message (revert --no-edit <rev>).
match git.cherry_pick(repo, "abc123").await {
    Ok(()) => {}
    Err(e) if is_merge_conflict(&e) => {
        // resolve, then continue or abort
        git.rebase_abort(repo).await.ok();
    }
    Err(e) => return Err(e),
}

§Stash

async fn stash_push(&self, dir: &Path, include_untracked: bool) -> Result<()>;
async fn stash_pop(&self, dir: &Path) -> Result<()>;
  • stash_pushstash push (--include-untracked when asked), e.g. to save state before a copy-on-write restore.
  • stash_pop — restore the most recent stash and drop it (stash pop).
git.stash_push(repo, true).await?;   // include untracked
// … do work on a clean tree …
git.stash_pop(repo).await?;

§In-progress state

async fn is_rebase_in_progress(&self, dir: &Path) -> Result<bool>;
async fn is_merge_in_progress(&self, dir: &Path) -> Result<bool>;
  • is_rebase_in_progresstrue when a rebase-merge/rebase-apply dir exists under the git dir.
  • is_merge_in_progresstrue when MERGE_HEAD exists under the git dir.
if git.is_rebase_in_progress(repo).await? || git.is_merge_in_progress(repo).await? {
    println!("repo is mid-operation");
}

§Clone / tags / config / show

async fn clone_repo(&self, url: &str, dest: &Path, spec: CloneSpec) -> Result<()>;
async fn tag_create(&self, dir: &Path, name: &str, rev: Option<String>) -> Result<()>;
async fn tag_create_annotated(&self, dir: &Path, spec: AnnotatedTag) -> Result<()>;
async fn tag_list(&self, dir: &Path) -> Result<Vec<String>>;
async fn tag_delete(&self, dir: &Path, name: &str) -> Result<()>;
async fn show_file(&self, dir: &Path, rev: &str, path: &str) -> Result<String>;
async fn config_get(&self, dir: &Path, key: &str) -> Result<Option<String>>;
async fn config_set(&self, dir: &Path, key: &str, value: &str) -> Result<()>;
  • clone_repogit clone <url> <dest> plus CloneSpec flags. Runs without a working directory — pass an absolute dest. Prompt-off.
  • tag_create — a lightweight tag at rev (tag <name> [<rev>]; None = HEAD).
  • tag_create_annotatedtag -a <name> -m <message> [<rev>]; built through AnnotatedTag.
  • tag_list — tag names in git’s default ordering (tag --list).
  • tag_deletetag -d <name>.
  • show_file — a file’s content at a revision (show <rev>:<path>). path is repo-relative; backslashes are normalised to /. Decoded lossily — binary files come back mangled rather than erroring.
  • config_get — a config key’s value, or None when unset (config --get <key>). A multi-valued key errors; read those via run.
  • config_set — set a key in the repo’s local config (config <key> <value>).
git.clone_repo(
    "https://example.com/repo.git",
    Path::new("/abs/dest"),
    CloneSpec::new().branch("main").depth(1),
).await?;                                                    // shallow, single branch

let repo = Path::new("/abs/dest");
git.tag_create_annotated(repo, AnnotatedTag::new("v1.0.0", "first release")).await?;
if let Some(name) = git.config_get(repo, "user.name").await? { // Option<String>
    println!("user.name = {name}");
}
let readme = git.show_file(repo, "HEAD", "README.md").await?;  // String (lossy)
let _ = readme;

§Discovery

async fn version(&self) -> Result<String>;
async fn capabilities(&self) -> Result<GitCapabilities>;
async fn common_dir(&self, dir: &Path) -> Result<PathBuf>;
async fn git_dir(&self, dir: &Path) -> Result<PathBuf>;
async fn init(&self, dir: &Path) -> Result<()>;
  • versiongit --version text.
  • capabilities — the parsed version as GitCapabilities. A value type — probe once and keep it; an unrecognisable version string is an Error::Parse.
  • common_dir — the repository’s common git directory (rev-parse --git-common-dir), stable across linked worktrees.
  • git_dir — this worktree’s git directory (rev-parse --git-dir).
  • init — initialise a repository (git init).
let caps = git.capabilities().await?;                       // GitCapabilities
caps.ensure_supported()?;                                    // clear error if git < 2
println!("git {}", caps.version);
let common = git.common_dir(repo).await?;                    // PathBuf
let _ = common;

§Raw escape hatches

async fn run(&self, args: &[String]) -> Result<String>;
async fn run_raw(&self, args: &[String]) -> Result<ProcessResult<String>>;
  • rungit <args> in the current directory, returning trimmed stdout (errors on a non-zero exit). For unmodelled commands.
  • run_raw — like run but never errors on a non-zero exit — returns the captured ProcessResult.

These are not flag-guarded — the caller owns the argv. The inherent run_args / run_raw_args take &[&str] to skip the Vec<String> allocation.

let out = git.run(&["describe".into(), "--tags".into()]).await?; // String
let res = git.run_raw(&["status".into(), "-s".into()]).await?;   // ProcessResult<String>
println!("exited {:?}", res.code());
let _ = out;

§Composed inherent helpers

These live on Git (and GitAt), not the object-safe GitApi trait — they are multi-step operations, not 1:1 CLI verbs, so mock their underlying calls instead.

  • switch_with_stash(dir, branch) — switch to branch, carrying uncommitted changes (tracked and untracked) across via the stash: stash push -ucheckoutstash pop. A clean tree skips the stash round-trip. On a failed checkout the stash is popped back to restore the original state; on a conflicting pop the target branch stays checked out with the stash entry preserved.
git.switch_with_stash(repo, "feature").await?;  // dirty tree comes along

§Blocking helpers

pub fn blocking::worktree_remove(dir: &Path, path: &Path, force: bool) -> std::io::Result<()>;

A synchronous, best-effort git worktree remove [--force] <path> for contexts that cannot .await — chiefly a Drop guard. It shells out through std::process directly (no async, no job-containment), so reserve it for short-lived cleanup.

§Result types

All result structs/enums are #[non_exhaustive] (except GitVersion) — match with a trailing .. and construct via the crate, not struct literals.

The diff types (ChangeKind, DiffLine, Hunk, FileDiff, DiffStat, parse_diff) and GitVersion actually live in the shared vcs-diff crate — git diff and jj diff --git are byte-identical, so vcs-git and vcs-jj share one parser. They’re re-exported here, so vcs_git::FileDiff etc. still resolve (GitVersion is an alias of vcs_diff::Version).

§StatusEntry

One entry from git status --porcelain=v1 -z.

fieldtypemeaning
codeStringtwo-character status code, e.g. " M", "??", "A ", "R "
pathStringthe path (the new path for a rename/copy); raw, unquoted
orig_pathOption<String>the original path for a rename/copy; None otherwise

§BranchStatus

The combined snapshot from branch_status (status --porcelain=v2 --branch -z). is_dirty() returns whether there’s any change (tracked or untracked).

fieldtypemeaning
headOption<String>HEAD commit’s full oid; None on an unborn repo (truncate for display)
branchOption<String>current branch; None when detached
upstreamOption<String>upstream tracking branch; None when unset
aheadOption<usize>commits ahead of upstream; None with no upstream
behindOption<usize>commits behind upstream; None with no upstream
tracked_changesusizechanged tracked entries (1/2/u records)
untrackedusizeuntracked files (? records)
conflictsusizeunmerged entries (u records; also in tracked_changes)

§Commit

A commit parsed from a \x1f-delimited git log line.

fieldtypemeaning
hashStringfull commit hash (%H)
short_hashStringabbreviated hash (%h)
authorStringauthor name (%an)
dateStringauthor date, strict ISO-8601 (%aI)
subjectStringsubject line (%s)

§Branch

fieldtypemeaning
nameStringbranch name
currentboolwhether this is the checked-out branch (the * marker)

§Worktree

fieldtypemeaning
pathPathBufabsolute path to the worktree
branchOption<String>short branch name (refs/heads/ stripped); None when detached or bare
headOption<String>the checked-out commit; None for a bare entry
bareboolthe main worktree of a bare repository
detachedboolchecked out at a detached HEAD
lockedboollocked against pruning

§DiffStat

Copy. Aggregate counts from git diff --shortstat.

fieldtypemeaning
files_changedusizenumber of files changed
insertionsusizelines added
deletionsusizelines removed

§FileDiff

One file’s entry in a parsed git-format unified diff.

fieldtypemeaning
changeChangeKindhow the file changed
pathStringthe file’s path (the new path for a rename), /-normalised
old_pathOption<String>the original path for a rename, /-normalised; None otherwise
hunksVec<Hunk>the @@ hunks; empty for a binary file or a pure rename
rawStringthe verbatim diff --git … block, for callers that display raw text
§ChangeKind

Copy enum: Added, Modified, Deleted, Renamed.

§Hunk

A single @@ … @@ hunk within a FileDiff.

fieldtypemeaning
old_startusizestart line in the old file
old_linesusizeline count in the old file (defaults to 1 when the ,count is omitted)
new_startusizestart line in the new file
new_linesusizeline count in the new file (defaults to 1 when omitted)
sectionStringtext after the closing @@ (the function/section heading); empty when none
linesVec<DiffLine>the hunk body, one entry per line
§DiffLine

Enum, one variant per line role; the stored text excludes the leading marker: Context(String) ( ), Added(String) (+), Removed(String) (-).

§BlameLine

One line of git blame --line-porcelain output.

fieldtypemeaning
commitStringfull hash of the commit that last changed the line
orig_lineu32line number in that commit’s version (1-based)
final_lineu32line number in the blamed version (1-based)
authorStringauthor name of that commit
author_timei64author timestamp as a unix epoch (seconds)
author_tzStringauthor timezone offset, e.g. +0200
contentStringthe line’s content (no trailing newline)

§GitVersion

Copy, and Ord (so versions compare directly). Not #[non_exhaustive].

fieldtypemeaning
majoru64major component (2 in 2.54.0)
minoru64minor component
patchu64patch component (0 when the binary reports only major.minor)

Displays as major.minor.patch.

§GitCapabilities

Copy. What the installed git supports, probed via capabilities().

fieldtypemeaning
versionGitVersionthe binary’s parsed version

Methods: is_supported(&self) -> bool (major ≥ 2) and ensure_supported(&self) -> Result<()> (a clear “needs git ≥ 2” error otherwise).

§Config & builder types

§DiffSpec

#[non_exhaustive] enum selecting what diff / diff_text compares:

  • DiffSpec::WorkingTree — all tracked working-tree changes vs the last commit (git diff HEAD), staged or not, excluding untracked files.
  • DiffSpec::Rev(String) — a specific revision or range, e.g. main..HEAD or HEAD~1 (git diff <rev>).

§WorktreeAdd

Options for worktree_add. #[non_exhaustive] — build it through the constructors, not a struct literal.

pub fn checkout(path: impl Into<PathBuf>, commitish: impl Into<String>) -> Self;
pub fn create_branch(path: impl Into<PathBuf>, name: impl Into<String>, commitish: impl Into<String>) -> Self;
pub fn no_checkout(self) -> Self;   // chainable: register without populating files (--no-checkout)
  • checkout — a worktree at path checking out an existing commitish: worktree add <path> <commitish>.
  • create_branch — create a new branch name based on commitish: worktree add -b <name> <path> <commitish>.
  • no_checkout — register the worktree without populating its files (--no-checkout), for a caller (e.g. a copy-on-write clone) that fills the working tree itself.

Fields: path: PathBuf, new_branch: Option<String>, commitish: Option<String>, no_checkout: bool.

let a = WorktreeAdd::checkout("/wt", "main");                       // existing branch
let b = WorktreeAdd::create_branch("/wt", "feature", "HEAD");       // new branch off HEAD
let c = WorktreeAdd::checkout("/wt", "main").no_checkout();         // skeleton only

§GitPush

Options for push. #[non_exhaustive] — build it through the constructors.

pub fn branch(name: impl Into<String>) -> Self;                          // push origin <name>
pub fn refspec(local: impl AsRef<str>, remote_branch: impl AsRef<str>) -> Self; // push origin <local>:<remote_branch>
pub fn remote(self, remote: impl Into<String>) -> Self;                  // chainable: non-default remote
pub fn set_upstream(self) -> Self;                                       // chainable: record upstream (-u)

Fields: remote: String (defaults to origin), refspec: String, set_upstream: bool.

let p = GitPush::branch("feature").set_upstream();           // push -u origin feature
let q = GitPush::refspec("local", "remote_branch").remote("upstream");

§CloneSpec

Options for clone_repo. #[non_exhaustive], Default — build through new and the chained setters.

pub fn new() -> Self;                          // a plain full clone of the default branch
pub fn branch(self, branch: impl Into<String>) -> Self; // --branch
pub fn depth(self, depth: u32) -> Self;        // --depth (see local-path caveat below)
pub fn bare(self) -> Self;                     // --bare

Fields: branch: Option<String>, depth: Option<u32>, bare: bool.

depth is silently ignored by git for a plain local-path source (it warns and clones fully); use a file:// URL to shallow-clone locally.

let spec = CloneSpec::new().branch("main").depth(1);
let bare = CloneSpec::new().bare();

§CommitPaths

Options for commit_paths. #[non_exhaustive] — build through new and the chained setter.

pub fn new(paths: impl IntoIterator<Item = impl Into<PathBuf>>, message: impl Into<String>) -> Self;
pub fn amend(self) -> Self;                    // chainable: amend the previous commit (--amend)

Fields: paths: Vec<PathBuf> (--only -- <paths>), message: String (-m), amend: bool (--amend).

let c = CommitPaths::new(["src/lib.rs"], "feat: thing");
let a = CommitPaths::new(["src/lib.rs"], "feat: thing").amend();

§MergeCommit

Options for merge_commit. #[non_exhaustive] — build through branch and the chained setters.

pub fn branch(name: impl Into<String>) -> Self; // merge --no-edit <name> (default message)
pub fn no_ff(self) -> Self;                     // chainable: always create a merge commit (--no-ff)
pub fn message(self, m: impl Into<String>) -> Self; // chainable: merge message (-m)

Fields: branch: String, no_ff: bool (--no-ff), message: Option<String> (-m; None takes the default message non-interactively via --no-edit).

let m = MergeCommit::branch("feature").no_ff();             // --no-ff, default message
let n = MergeCommit::branch("feature").message("merge it"); // -m "merge it"

§MergeNoCommit

Options for merge_no_commit. #[non_exhaustive] — build through branch and the chained setters.

pub fn branch(name: impl Into<String>) -> Self; // merge --no-commit <name>
pub fn squash(self) -> Self;                    // chainable: stage the squashed result (--squash)
pub fn no_ff(self) -> Self;                     // chainable: record a real abortable merge (--no-ff)

Fields: branch: String, squash: bool (--squash; takes precedence over no_ff), no_ff: bool (--no-ff). With no_ff (and not squash) git records MERGE_HEAD, so the merge is abortable via merge_abort; with squash no MERGE_HEAD is recorded — undo via reset_merge / reset_hard.

let dry = MergeNoCommit::branch("feature").no_ff();   // abortable dry-run merge
let sq = MergeNoCommit::branch("feature").squash();   // stage squashed, no MERGE_HEAD

§AnnotatedTag

Options for tag_create_annotated. #[non_exhaustive] — build through new and the chained setter.

pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self; // tag -a <name> -m <message> at HEAD
pub fn rev(self, r: impl Into<String>) -> Self;  // chainable: tag <rev> instead of HEAD

Fields: name: String, message: String (-m), rev: Option<String> (<rev>; None tags HEAD).

let t = AnnotatedTag::new("v1.0.0", "first release");
let u = AnnotatedTag::new("v1.0.0", "first release").rev("abc123");

§Validating newtypes

Optional up-front validation for callers that accept names/revisions from untrusted input (UIs, bots, agents) and want to fail early with a clear error at the input boundary. They are not required wrappers — the dir-taking methods stay &str and apply the same flag-injection guard internally on every call, regardless of whether you used these.

§RefName

A pre-validated reference name (branch/tag/remote), following the load-bearing core of git check-ref-format. Rejects a name that is:

  • empty,
  • has a leading - or .,
  • contains ..,
  • contains a control character or space, or any of ~ ^ : ? * [ \,
  • ends with / or .lock.
pub fn new(name: impl Into<String>) -> Result<Self>;
pub fn as_str(&self) -> &str;

§RevSpec

A pre-validated revision/range expression (HEAD~2, main..feature). Deliberately minimal — git’s revision grammar is too rich to validate here — it only guarantees the expression is non-empty and cannot be parsed as a flag (no leading -), matching the internal guard.

pub fn new(rev: impl Into<String>) -> Result<Self>;
pub fn as_str(&self) -> &str;
let name = RefName::new("feature/login")?;   // Ok
let rev = RevSpec::new("main..HEAD")?;        // Ok
assert!(RefName::new("-evil").is_err());      // leading '-'
assert!(RefName::new("bad..name").is_err());  // contains '..'
let _ = (name, rev);

Both implement Display and yield the validated string via as_str().

§Error classification

git writes load-bearing diagnostics to either stream on failure, so these free functions probe both stdout and stderr of an Error::Exit — call them instead of re-implementing the string-scraping yourself.

pub fn is_merge_conflict(err: &Error) -> bool;        // a merge/cherry-pick stopped on conflicts
pub fn is_nothing_to_commit(err: &Error) -> bool;     // a commit found a clean tree
pub fn is_transient_fetch_error(err: &Error) -> bool; // DNS/timeout/dropped connection — retryable

is_transient_fetch_error also treats a processkit-level Error::Timeout as retryable. See Process model & errors for the Error shape.

§See also

Modules§

conflicts
Conflict resolution guide
security
Security & hardening guide