Skip to main content

Crate vcs_core

Crate vcs_core 

Source
Expand description

vcs-core — one backend-agnostic facade over vcs-git and vcs-jj.

It detects whether a directory is a git or a jj checkout, then dispatches the operations both tools share to whichever backend is present — returning backend-agnostic DTOs (RepoSnapshot, FileChange, MergeProbe, …), so a caller codes against “the repository” instead of against git or jj. Async throughout, structured errors, and every subprocess inherits the underlying client’s OS-job containment (via processkit) so no git/jj tree is ever orphaned.

It is the honest least-common-denominator, not a god-object. The common surface carries only what unifies without lying; operations the backends model too differently are deliberately left on the bound per-tool handles rather than faked (see below). Reach for the facade when code must work on both backends; drop to the raw client the moment you need power only one of them offers.

§Mental model

The surface is three layers, narrowing from “which tool is this?” to “do the thing”:

  • detect — walk up from a directory to the filesystem root for a .git/.jj repo (jj wins when colocated — it’s the tool driving the working copy). Pure filesystem probing, no subprocess; yields a Located (BackendKind + worktree root).
  • Repo — the cwd-bound facade handle, the thing you hold. Open one with Repo::open (real job-backed runner) or build it over an explicit client with Repo::from_git / Repo::from_jj (the test seam). Re-anchor it to another directory cheaply with Repo::at — the backend is shared behind an Arc, so threading work across worktrees never re-detects or rebuilds the client. Inspect it with kind / root / cwd.
  • VcsRepo — the same common surface as an object-safe trait, so a consumer can hold a Box<dyn VcsRepo> / &dyn VcsRepo without naming the ProcessRunner generic. Every method mirrors the like-named inherent method on Repo; it adds nothing but the abstraction boundary.

§The common operations

All on Repo (and VcsRepo), dir-free, dispatched per backend:

Because the backends genuinely diverge in places, several common methods carry a documented asymmetry (e.g. upstream/ahead/behind are always None on jj; diff_stat excludes untracked files on git but not jj; in_progress_state never returns Conflict on git). The method docs spell each one out — the facade unifies the shape, not away the truth.

§The escape hatches

Tool-specific work reaches the underlying typed clients without adding vcs-git/vcs-jj as separate dependencies (both are re-exported): git_at / jj_at hand out a cwd-bound view (GitAt / JjAt, dir dropped); the raw git / jj hand out a borrow of the client itself. Each returns None for the other backend.

§What’s deliberately not unified

Three families stay off the common surface because no honest single shape exists — reach them through the bound handles:

  • Full merge — jj composes new + squash + bookmark moves; git runs a single command. Only the conflict probe unifies, as try_merge.
  • Operation rollback — jj’s op restore has no faithful git analogue; use Jj::transaction on the jj client.
  • Range / revset queries — commit counts and diff stats over a range: git’s a..b and jj’s revsets aren’t interchangeable, so neither is forced onto a shared signature.

§Recipes

Open a repo and read a snapshot for a one-line prompt:

use vcs_core::Repo;
let repo = Repo::open(".")?;            // detects git vs jj
let s = repo.snapshot().await?;         // one or two spawns, not a call per field
let branch = s.branch.as_deref().unwrap_or("(detached)");
let head = s.head.as_deref().map(|h| &h[..7.min(h.len())]).unwrap_or("-");
println!("{}@{head} {}", branch, if s.dirty { "*" } else { "" });

Probe a merge for conflicts (trace-free), or spin up a worktree:

use std::path::Path;
use vcs_core::{MergeProbe, Repo};
match repo.try_merge("feature").await? {
    MergeProbe::Clean            => println!("merges cleanly"),
    MergeProbe::Conflicts(paths) => println!("would conflict in {paths:?}"),
    _                            => {} // #[non_exhaustive]
}
let wt = repo.create_worktree(Path::new("/tmp/feat"), "feature", "main").await?;

§Testing

There is no mock feature on the facade traits — the runner is the seam. Build a Repo over a fake ProcessRunner with Repo::from_git / Repo::from_jj (e.g. a ScriptedRunner replying to canned argv), so the real per-backend dispatch, argv-building and parsing run against canned output — exactly what a mocked VcsRepo would skip. The cross-cutting patterns live in vcs-testkit’s guide.

use processkit::{Reply, ScriptedRunner};
use vcs_core::{vcs_git::Git, Repo};
let runner = ScriptedRunner::new().on(["status"], Reply::ok(" M a.rs\0"));
let repo = Repo::from_git("/repo", "/repo", Git::with_runner(runner));
assert!(repo.has_uncommitted_changes().await?);

§In-depth guide

Beyond this page, this crate ships a full how-to guide — rendered on docs.rs from docs/. See the guide module, which walks every operation in depth and hosts the cross-cutting sub-guides: a cookbook of end-to-end flows, the process_model (job containment, errors, cancellation), positioning (facade-vs-raw-client and the three call shapes), and the stability contract.

Re-exports§

pub use vcs_git;
pub use vcs_jj;

Modules§

guide
vcs-core — backend-agnostic facade guide

Structs§

CancellationTokencancellation
A token which can be used to signal a cancellation request to one or more tasks.
DiffStat
Aggregate insertion/deletion counts for the working copy — the shared vcs_diff::DiffStat, returned by the backends directly (no remapping). Aggregate line/file counts from a diff stat (git diff --shortstat, jj diff --stat).
FileChange
One changed path in the working copy, unified across git status / jj diff --summary.
Located
The result of detect: which backend, and the repository root it was found at.
Repo
A cwd-bound, backend-agnostic VCS handle. Operations run against the bound directory (cwd); use at to get a sibling handle bound elsewhere.
RepoSnapshot
A one-shot snapshot of the common repository state — branch, upstream tracking, ahead/behind, dirtiness, and operation state — gathered in one or two process spawns instead of a call per field. The data a prompt, status line, or TUI refresh needs. See Repo::snapshot.
WorktreeInfo
One attached worktree (git) / workspace (jj).

Enums§

BackendKind
Which version-control tool backs a Repo.
ChangeKind
How a file changed in the working copy — the shared vcs_diff::ChangeKind (one type across the wrappers and the facade, no remapping). The status-code mappers in the backends turn git’s XY codes / jj’s letters into it. How a file changed in a unified diff.
CreateOutcome
How a worktree was materialised. The facade always reports Plain; the CowCloned variant exists so a consumer that layers a copy-on-write strategy on top can reuse this type.
Error
An error from a Repo operation.
MergeProbe
The outcome of a try_merge probe. The probe itself is rolled back before it returns, whatever the outcome — this only reports what a real merge would do.
OperationState
Whether the working copy is mid-operation, unified across the backends’ different models: git exposes an in-progress merge or rebase as on-disk state (MERGE_HEAD / a rebase-* dir), while jj has no multi-step operations — it records a conflict directly on the working-copy change.

Traits§

VcsRepo
The backend-agnostic common surface of Repo, as a trait — so a consumer can hold a Box<dyn VcsRepo> / &dyn VcsRepo and code against the operations without naming the ProcessRunner generic or wrapping Repo themselves.

Functions§

detect
Walk up from start to the filesystem root looking for a repository. A .jj directory wins over .git (colocated repos are driven through jj); .git may be a directory or a gitlink file (a linked worktree/submodule). Pure filesystem probing — no subprocess.

Type Aliases§

Result
Result specialised to the facade Error.