vcs-jj 0.3.1

Automate the Jujutsu (jj) CLI from Rust through process execution.
Documentation

vcs-jj

Automate the Jujutsu (jj) CLI from Rust through process execution. Part of the vcs-toolkit-rs workspace.

Typed, repo-scoped, async commands over the jj binary, behind a mockable interface. Commands run inside an OS job (via processkit) so no jj subprocess is ever orphaned, return the structured Error, and honour an optional timeout.

Inside an async context (every method is async):

use std::path::Path;
use vcs_jj::{Jj, JjApi};

let jj = Jj::new();
let head = jj.current_change(Path::new(".")).await?; // Change
jj.describe(Path::new("."), "feat: new thing").await?; // set @ description

A change workflow

use std::path::Path;
use vcs_jj::{Jj, JjApi};

# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
    let jj = Jj::new();

    // Describe the working-copy change, then start a fresh one on top.
    jj.describe(repo, "feat: parser").await?;
    jj.new_change(repo, "wip: follow-up").await?;

    let head = jj.current_change(repo).await?; // Change { change_id, commit_id, empty, description }
    println!("@ = {} ({})", head.change_id, head.description);

    // Everything reachable from @, newest first.
    for c in jj.log(repo, "::@", 10).await? {
        println!(
            "{} {}{}",
            c.change_id,
            if c.empty { "(empty) " } else { "" },
            c.description
        );
    }
# Ok(()) }

Bookmarks and syncing the git remote

# use std::path::Path;
# use vcs_jj::{Jj, JjApi};
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
    let jj = Jj::new();

    jj.git_fetch(repo).await?; // `jj git fetch`
    jj.bookmark_set(repo, "main", "@").await?; // point `main` at @
    for b in jj.bookmarks(repo).await? {
        println!("{} -> {}", b.name, b.target);
    }
    jj.git_push(repo, Some("main".to_string())).await?; // `jj git push -b main`
# Ok(()) }

Workspaces

Manage workspaces (jj's worktrees) with structured results:

use vcs_jj::{Jj, JjApi, WorkspaceAdd};
use std::path::Path;

# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let jj = Jj::new();

jj.workspace_add(repo, WorkspaceAdd::new("feature", "@", "/tmp/feature"))
    .await?;

for ws in jj.workspace_list(repo).await? {            // Vec<Workspace>
    println!("{} @ {} {:?}", ws.name, ws.commit, ws.bookmarks);
}

jj.workspace_forget(repo, "feature").await?;
# Ok(()) }

Timeouts

# use vcs_jj::Jj;
use std::time::Duration;
let jj = Jj::new().default_timeout(Duration::from_secs(10));
// every command now fails with `processkit::Error::Timeout` if it outruns 10s
# let _ = jj;

Consumers depend on the JjApi trait and substitute a fake in tests — enable the mock feature for a mockall-generated MockJjApi, or inject a fake process runner with Jj::with_runner(processkit::ScriptedRunner::new()…):

use processkit::{Reply, ScriptedRunner};
use std::path::Path;
use vcs_jj::{Jj, JjApi};

# async fn demo() {
    let jj = Jj::with_runner(
        ScriptedRunner::new().on(["log"], Reply::ok("kztuxlro\t38e00654\tfalse\thello\n")),
    );
    assert_eq!(
        jj.current_change(Path::new(".")).await.unwrap().description,
        "hello"
    );
# }

Requires the jj binary on PATH.

License

MIT