vcs-runner
Subprocess runner for jj and git CLI tools, with automatic retry on transient errors, repository detection, and structured jj output parsing.
Why not std::process::Command?
- Typed errors — distinguishes "couldn't spawn the binary" from "command ran and exited non-zero," so callers can handle the second as a legitimate in-band signal
- Retry with backoff on lock contention and stale working copy errors
- Binary-safe output (
Vec<u8>) with convenient.stdout_lossy()for text - Repo detection that walks parent directories and distinguishes git, jj, and colocated repos
- Structured jj parsing (optional) turns
jj logandjj bookmark listoutput into typed Rust structs
Usage
[]
= "0.7"
Git-only consumers can skip the jj parsing types and their serde dependencies:
[]
= { = "0.7", = false }
Running commands
use ;
// Run a jj command, get captured output
let output = run_jj?;
let log_text = output.stdout_lossy;
// Binary content: access raw bytes directly (e.g., for image diffs)
let output = run_jj?;
let image_bytes: = output.stdout;
// With retry on lock contention / stale working copy
let output = run_jj_with_retry?;
// Custom retry predicate receives a typed RunError
let output = run_jj_with_retry?;
// Git works the same way
let output = run_git?;
Handling "command ran and said no"
run_jj and run_git return Result<RunOutput, RunError>. The RunError enum distinguishes infrastructure failure (binary missing, fork failed) from non-zero exits (the command ran and reported failure via exit code):
use ;
match run_git
RunError implements std::error::Error, so ? into anyhow::Result works when you don't care about the distinction.
Inspection methods on RunError:
err.is_non_zero_exit()/err.is_spawn_failure()— check the varianterr.stderr()— captured stderr onNonZeroExit,NoneonSpawnerr.exit_status()— exit status onNonZeroExit,NoneonSpawnerr.program()— the program name that failed
Commands other than jj/git
use ;
// Captured output
let output = run_cmd?;
// In a specific directory
let output = run_cmd_in?;
// With environment variables (e.g., for GIT_INDEX_FILE)
let output = run_cmd_in_with_env?;
// Inherited I/O (user sees output directly)
run_cmd_inherited?;
Repository detection
use ;
let = detect_vcs?;
if backend.is_jj
if backend.has_git
Detection walks parent directories automatically (e.g., /repo/src/lib/ finds /repo/.jj).
Parsing jj output
Requires the jj-parse feature (on by default). Pre-built templates produce line-delimited JSON; parse functions handle malformed output gracefully.
use ;
use ;
let output = run_jj?;
let result = parse_log_output;
for entry in &result.entries
// Skipped entries (malformed JSON from stale bookmarks, etc.)
for name in &result.skipped
Binary availability
use ;
if jj_available
// Generic: works with any binary that supports --version
if binary_available
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.