Skip to main content

Crate vcs_core

Crate vcs_core 

Source
Expand description

vcs-core — write code against “the repository” without caring whether it’s git or jj.

You hold one handle, Repo, that auto-detects whether a directory is a git or a jj checkout and runs whatever operations both tools support — handing back plain result types (RepoSnapshot, FileChange, MergeProbe, …) that don’t mention the backend (whether the repo is git or jj). Async, structured errors, and every subprocess inherits the underlying client’s OS-job containment (an OS-level container that kills the whole process tree if your program exits, via processkit) so no git/jj tree is orphaned.

§What you can do

From one Repo handle: read the current branch and a batched status snapshot · list & diff changed files · commit paths · fetch / push / checkout / rebase · probe a merge for conflicts (try_merge) · drive in-progress merge/rebase state · manage worktrees. Open one and read a prompt line:

use vcs_core::Repo;
let repo = Repo::open(".")?;            // detects git vs jj
let s = repo.snapshot().await?;         // a few spawns, not a call per field
let branch = s.branch.as_deref().unwrap_or("(detached)");
println!("{branch} {}", if s.dirty { "*" } else { "" });

It’s a thin common layer, not a god-object. The shared surface carries only what unifies without lying; the few operations the two tools model too differently (a full merge, jj’s op restore, range/revset queries) stay on the raw git/jj handle rather than being faked (see below). Reach for the unified handle when code must work on both backends; drop to the raw client when you need power only one of them offers.

§Mental model (engineering reference)

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

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::testing::{Reply, ScriptedRunner};
use vcs_core::{vcs_git::Git, Repo};
let runner = ScriptedRunner::new().on(["git", "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;
pub use processkit;

Modules§

guide
vcs-core — backend-agnostic facade guide

Structs§

BranchDelete
Options for Repo::delete_branch.
CancellationToken
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 a small fixed number of process spawns instead of a call per field. The data a prompt, status line, or TUI refresh needs. See Repo::snapshot.
UpstreamTracking
Upstream tracking for the current branch: the upstream ref and how far the branch is ahead/behind it. RepoSnapshot carries it as one Option<UpstreamTracking>None when no upstream is configured at all.
WorktreeInfo
One attached worktree (git) / workspace (jj).
WorktreeRemove
Options for Repo::remove_worktree.

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.