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/.jjrepo (jj wins when colocated — it’s the tool driving the working copy). Pure filesystem probing, no subprocess; yields aLocated(BackendKind+ worktree root).Repo— the cwd-bound facade handle, the thing you hold. Open one withRepo::open(real job-backed runner) or build it over an explicit client withRepo::from_git/Repo::from_jj(the test seam). Re-anchor it to another directory cheaply withRepo::at— the backend is shared behind anArc, so threading work across worktrees never re-detects or rebuilds the client. Inspect it withkind/root/cwd.VcsRepo— the same common surface as an object-safe trait, so a consumer can hold aBox<dyn VcsRepo>/&dyn VcsRepowithout naming theProcessRunnergeneric. Every method mirrors the like-named inherent method onRepo; it adds nothing but the abstraction boundary.
§The common operations
All on Repo (and VcsRepo), dir-free, dispatched per backend:
- Refs —
current_branch,trunk,local_branches,branch_exists,delete_branch,rename_branch(branch on git, bookmark on jj). - Status —
changed_files,diff_stat,has_uncommitted_changes,has_tracked_changes,conflicted_files, andsnapshot— a batched prompt/status-bar read of the lot in one or two spawns. - Mutations —
commit_paths(partial commit),fetch/fetch_from/fetch_branch/push,checkout,rebase. - Merge & operation state —
try_merge(a trace-free conflict probe →MergeProbe),in_progress_state/abort_in_progress/continue_in_progress→OperationState. - Worktrees / workspaces —
list_worktrees,create_worktree,remove_worktree, and the synchronouscleanup_worktree_blockingfor aDropguard that cannot.await.
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 composesnew+squash+ bookmark moves; git runs a single command. Only the conflict probe unifies, astry_merge. - Operation rollback — jj’s
op restorehas no faithful git analogue; useJj::transactionon the jj client. - Range / revset queries — commit counts and diff stats over a range: git’s
a..band 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, WorktreeCreate};
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(WorktreeCreate::new(Path::new("/tmp/feat"), "feature").base("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§
- Branch
Delete - Options for
Repo::delete_branch. - Cancellation
Token - A token which can be used to signal a cancellation request to one or more tasks.
- Diff
Stat - 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). - File
Change - 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); useatto get a sibling handle bound elsewhere. - Repo
Snapshot - 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. - Upstream
Tracking - Upstream tracking for the current branch: the upstream ref and how far the
branch is ahead/behind it.
RepoSnapshotcarries it as oneOption<UpstreamTracking>—Nonewhen no upstream is configured at all. - Worktree
Create - Options for
Repo::create_worktree. - Worktree
Create Partial - Partial
WorktreeCreate— carries the path and new-branch name; chainbaseto name the ref it forks from. - Worktree
Info - One attached worktree (git) / workspace (jj).
- Worktree
Remove - Options for
Repo::remove_worktree.
Enums§
- Backend
Kind - Which version-control tool backs a
Repo. - Change
Kind - 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’sXYcodes / jj’s letters into it. How a file changed in a unified diff. - Create
Outcome - How a worktree was materialised. The facade always reports
Plain; theCowClonedvariant exists so a consumer that layers a copy-on-write strategy on top can reuse this type. - Error
- An error from a
Repooperation. - Merge
Probe - The outcome of a
try_mergeprobe. The probe itself is rolled back before it returns, whatever the outcome — this only reports what a real merge would do. - Operation
State - 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/ arebase-*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 aBox<dyn VcsRepo>/&dyn VcsRepoand code against the operations without naming theProcessRunnergeneric or wrappingRepothemselves.
Functions§
- detect
- Walk up from
startto the filesystem root looking for a repository. A.jjdirectory wins over.git(colocated repos are driven through jj);.gitmay be a directory or a gitlink file (a linked worktree/submodule). Pure filesystem probing — no subprocess.