Skip to main content

Crate vcs_jj

Crate vcs_jj 

Source
Expand description

vcs-jj — automate Jujutsu (jj) from Rust by driving the jj CLI.

You call typed async methods; vcs-jj runs the real jj, parses its templated output, and hands you structured values — so you get jj’s own behaviour and config, not a reimplementation of the operation log or backend. Async, structured errors, mockable. Every command runs inside an OS job (an OS-level container that kills the whole process tree if your program exits, via processkit) so a jj subprocess is never orphaned, with an optional per-client timeout.

§What you can do

Working-copy status & the change log · describe / new change · bookmarks · the operation log (restore / undo — jj’s safety net) · workspaces · squash / split / absorb / duplicate / abandon · diff & template queries · git sync (fetch / push / clone / import) · parse & resolve jj’s native conflict markers · transactions that roll the op log back on error. One tiny call to start:

use std::path::Path;
use vcs_jj::{Jj, JjApi};
let jj = Jj::new();
// the working-copy change `@`:
println!("{}", jj.current_change(Path::new(".")).await?.change_id);

§The surface (engineering reference)

There is deliberately no Jj::hardened() counterpart to vcs-git’s untrusted-repo profile: jj has no repo-local hooks, and its config comes from the user/repo TOML files jj itself trusts. In a colocated repo the risk lives on the git side — git hooks fire when git commands run there, so harden the Git client you point at it.

§Recipes

Read state — depend on the trait so the same code takes a real client or a mock:

use std::path::Path;
use vcs_jj::{Jj, JjApi};
let jj = Jj::new();
let dir = Path::new(".");
let current = jj.current_change(dir).await?;       // the working-copy change `@`
let dirty = !jj.status(dir).await?.is_empty();     // any working-copy edit?

Mutate inside a transaction — an Err rolls the op log back (safely: the cleanup survives a cancelled closure and refuses to clobber a concurrent process’s work — see TransactionError / Rollback):

use std::path::Path;
use vcs_jj::Jj;
let dir = Path::new(".");
jj.transaction(dir, |tx| async move {
    tx.describe("wip").await?;
    tx.new_change("next").await        // an Err here undoes the describe
})
.await?;

A binding (or any caller that can’t pass a Rust closure) drives the same rollback imperatively with the primitives transaction wraps — op_head to capture a savepoint and rollback_to to roll back to it on failure with the same cancellation-safe, divergence-checked protocol.

§Testing

Two seams: enable the mock feature for a mockall-generated MockJjApi (stub whole methods), or inject a ScriptedRunner with Jj::with_runner to exercise the real argv-building and parsing against canned output. The cross-cutting testing patterns live in vcs-testkit’s guide.

§Safety

Every caller value placed in a bare positional argv slot (bookmark name, revset, operation id, merge parent, …) is refused before spawning if it is empty or starts with - (jj would parse it as a flag); flag-value slots (-r <revset>, -m <msg>) and the run/run_raw escape hatches are not guarded. For eager validation at an input boundary, RevsetExpr validates up front. Paths go through the exact-path JjFileset form.

A concrete instance of the flag-value-slot rule: DiffSpec::Rev on diff_text/diff (diff_text_budgeted) is a bare String from the shared vcs-diff crate, passed verbatim into -r <revset> — unguarded here, same as any other flag-value slot, and rejected by jj itself if it starts with -.

§In-depth guide

Beyond this page, this crate ships a full how-to guide — rendered on docs.rs from docs/. See the guide module. The conflict model is covered by vcs-git’s conflicts guide, which spans both backends.

Modules§

__mock_MockJjApi
__mock_MockJjApi_JjApi
blocking
Synchronous, best-effort helpers for contexts that cannot .await — chiefly a Drop guard. They shell out through std::process directly (no async, no job-containment), so reserve them for short-lived cleanup.
conflict
Typed model of jj’s materialized conflict markers — parse a conflicted file’s content into structured regions and write a chosen resolution back. Pure functions (no subprocess), so everything here is hermetic.
guide
vcs-jj — Jujutsu CLI guide

Structs§

AnnotationLine
One line of jj file annotate output: which change last touched it.
Bookmark
A jj bookmark, parsed from jj bookmark list output.
BookmarkMove
Options for JjApi::bookmark_move (jj bookmark move <name> --to <rev>).
BookmarkName
A validated jj bookmark name (jj’s equivalent of a git branch). Every JjApi operation that names a bookmark to create, move, rename, delete, track, fetch, or push takes a BookmarkName, so a name from untrusted input is validated once, at construction. jj bookmark names are permissive, so the guarantee is the load-bearing one: non-empty and not flag-shaped (no leading -), matching the injection guard these operations applied internally before. The typed methods additionally wrap the name in jj’s exact: string pattern so a */? in a name can never fan the operation out across every bookmark. A rejected name is an vcs_cli_support::is_invalid_input failure.
BookmarkRef
A bookmark from jj bookmark list -a — local or remote-tracking.
CancellationToken
A token which can be used to signal a cancellation request to one or more tasks.
Change
A jj change, parsed from a \t-delimited template row.
ChangedPath
One entry from jj diff --summary: a single-letter status (M/A/D/…) and the (forward-slash-normalised) path it applies to — the new path for a rename/copy, with the original on old_path.
DiffStat
Aggregate line/file counts from a diff stat (git diff --shortstat, jj diff --stat).
FileDiff
One file’s entry in a parsed git-format unified diff (git diff or jj diff --git).
GitClone
Colocation choice for JjApi::git_clone (jj git clone --colocate|--no-colocate).
Hunk
A single @@ … @@ hunk within a FileDiff.
Jj
The real jj client. Generic over the ProcessRunner so tests can inject a fake process executor; Jj::new uses the real job-backed runner.
JjAt
A Jj client with a working directory bound, so calls drop the leading dir argument — jj.at(dir).status() is jj.status(dir). Construct one with Jj::at (or, through the facade, vcs_core::Repo::jj_at). Cheap to copy: it only borrows the client and the path.
JjCapabilities
What the installed jj binary supports, probed via JjApi::capabilities. A value type — the client holds no state, so probe once and keep the result (callers cache it).
JjFileset
An exact-path jj fileset (root-file:"<path>"), so path metacharacters like (, ), |, * are treated literally rather than as fileset operators.
JjVersion
A parsed CLI version (major.minor.patch). Ord compares numerically, so a caller can gate a feature on a minimum version; Hash lets it key a map (e.g. a per-version capability cache).
JobRunner
The default runner: every run gets a fresh, private ProcessGroup owned by the run, so its tree is torn down when the run finishes (or its handle drops).
MockJjApi
The jj operations this crate exposes — the interface consumers code against and mock in tests.
Operation
One entry of jj op log (an operation-log row).
OutputBudget
A configurable ceiling on how much output a potentially large content operation may buffer before it is refused — a diff (diff_text/diff), a file’s bytes at a revision (show_file/file_show), a forge PR/MR diff (pr_diff), and the diagnostic (error/progress) output of clone/fetch.
ProcessResult
The captured result of running a process to completion.
RetryPolicy
A bounded retry strategy: how many attempts, the (exponential) backoff between them, and whether to add full jitter. Used by ManagedClient to retry is_lock_contention failures. The Default is none (no retry) — retry is opt-in.
RevsetExpr
A validated revset expression. Every JjApi operation that resolves a revision/revset takes a RevsetExpr (directly or inside its options struct), so a revset from untrusted input (UIs, bots, agents) is validated once, at construction, and the type is the flag-injection barrier from then on. Deliberately minimal — jj’s revset grammar is too rich to validate here — it only guarantees the expression is non-empty and cannot be parsed as a flag (no leading -). A rejected expression is an vcs_cli_support::is_invalid_input failure. For a value that must be a bookmark name (create/move/delete a bookmark) use BookmarkName.
SquashInto
Options for JjApi::squash_into (jj squash --into <rev>).
SquashPaths
Options for JjApi::squash_paths (jj squash --from <from> --into <into> [--use-destination-message] <filesets>).
TransactionError
The error Jj::transaction returns when its closure fails. It preserves the closure’s own error in cause — the same value the previous (rollback-swallowing) Result<T> contract returned — and additionally records what the concurrency-safe rollback did in rollback, so a failed or refused rollback is visible to the caller instead of silently dropped (the earlier let _ = op_restore(..) discarded it).
Workspace
A workspace from jj workspace list (rendered with WORKSPACE_TEMPLATE).
WorkspaceAdd
Options for JjApi::workspace_add (jj workspace add).

Enums§

ChangeKind
How a file changed in a unified diff.
DiffLine
One line inside a Hunk, tagged by its role. The stored text excludes the leading /+/- marker and the line terminator — a CRLF-origin diff’s trailing \r is stripped along with the \n, so reconstruct exact bytes from FileDiff::raw, not from these lines.
DiffSpec
What a diff call compares — the working tree/copy, or a specific revision/revset (or range).
Error
Errors produced when launching or running a child process.
Rollback
What the concurrency-safe op-log rollback did after a mutation failed — the outcome Jj::rollback_to returns and Jj::transaction reports on its TransactionError. It lets a caller tell a completed rollback apart from one that was deliberately refused (a concurrent process’s work would have been clobbered) or one that failed, instead of guessing by re-probing the op log.
SparseMode
How a new workspace inherits sparse patterns (jj workspace add --sparse-patterns <mode>).

Constants§

BINARY
Name of the underlying CLI binary this crate drives.

Traits§

JjApi
The jj operations this crate exposes — the interface consumers code against and mock in tests.
ProcessRunner
Runs a Command — to a captured result (output_string / output_bytes) or a live handle (start).

Functions§

is_lock_contention
Whether err is a whole-repository lock-contention failure — another process held git’s index.lock or jj’s working-copy / op-heads lock, so the command couldn’t even start. Such a failure is pre-execution and therefore safe to retry even on a mutating operation (the repo was never modified). Per-ref lock failures (cannot lock ref, <ref>.lock) are deliberately not classified here — they can occur mid-way through a multi-ref push/fetch, where a retry would not be idempotent. Conflict, “nothing to commit”, a real non-zero exit, a timeout, a signal, or a missing binary are also not lock contention and must not be retried this way.
is_transient_fetch_error
Whether a failed fetch/fetch_branch/remote_branch_exists looks transient (DNS, a dropped connection, a fast network blip) and is worth retrying.
normalize_workspace_root
Normalise a path for comparison against jj’s workspace root output: canonicalize (resolve symlinks / macOS case) and strip the Windows verbatim prefix (\\?\…, which canonicalize adds but jj never emits). A path that doesn’t exist (or otherwise fails to canonicalize — e.g. a worktree directory already removed) falls back to its own literal form.
parse_diff
Parse a git-format unified diff into one FileDiff per file. Works on git diff and jj diff --git output alike. Public so a consumer can parse diff text it obtained by other means.
workspace_root_matches
Whether a workspace whose jj workspace root resolved to root is the workspace requested at path. The single comparison set shared by both jj-workspace-by-path resolvers in this workspace — vcs-core’s async Repo::remove_worktree path (jj_backend::workspace_name_for_path) and this crate’s synchronous blocking::workspace_name_for_path (the Drop path) — so “does this path resolve to a workspace” answers the same question on both sides. The two used to carry independently-maintained, already-diverged comparison sets (T-080); this is their union, so a path either side used to resolve still resolves:

Type Aliases§

Result
Crate result alias.