use std::path::Path;
use processkit::ProcessRunner;
pub use processkit::{Error, ProcessResult, Result};
mod parse;
pub use parse::{Bookmark, Change};
pub const BINARY: &str = "jj";
#[cfg_attr(feature = "mock", mockall::automock)]
#[async_trait::async_trait]
pub trait JjApi: Send + Sync {
async fn run(&self, args: &[String]) -> Result<String>;
async fn run_raw(&self, args: &[String]) -> Result<ProcessResult<String>>;
async fn version(&self) -> Result<String>;
async fn status(&self, dir: &Path) -> Result<String>;
async fn log(&self, dir: &Path, revset: &str, max: usize) -> Result<Vec<Change>>;
async fn current_change(&self, dir: &Path) -> Result<Change>;
async fn describe(&self, dir: &Path, message: &str) -> Result<()>;
async fn new_change(&self, dir: &Path, message: &str) -> Result<()>;
async fn bookmarks(&self, dir: &Path) -> Result<Vec<Bookmark>>;
async fn bookmark_set(&self, dir: &Path, name: &str, revision: &str) -> Result<()>;
async fn git_fetch(&self, dir: &Path) -> Result<()>;
async fn git_push(&self, dir: &Path, bookmark: Option<String>) -> Result<()>;
}
processkit::cli_client!(
pub struct Jj => BINARY
);
#[async_trait::async_trait]
impl<R: ProcessRunner> JjApi for Jj<R> {
async fn run(&self, args: &[String]) -> Result<String> {
self.core.text(self.core.command(args)).await
}
async fn run_raw(&self, args: &[String]) -> Result<ProcessResult<String>> {
self.core.capture(self.core.command(args)).await
}
async fn version(&self) -> Result<String> {
self.core.text(self.core.command(["--version"])).await
}
async fn status(&self, dir: &Path) -> Result<String> {
self.core.text(self.core.command_in(dir, ["status"])).await
}
async fn log(&self, dir: &Path, revset: &str, max: usize) -> Result<Vec<Change>> {
let n = format!("-n{max}");
self.core
.parse(
self.core.command_in(
dir,
[
"log",
"-r",
revset,
n.as_str(),
"--no-graph",
"-T",
parse::CHANGE_TEMPLATE,
],
),
parse::parse_changes,
)
.await
}
async fn current_change(&self, dir: &Path) -> Result<Change> {
let mut changes = self.log(dir, "@", 1).await?;
changes.pop().ok_or_else(|| Error::Parse {
program: BINARY.to_string(),
message: "no working-copy change found".to_string(),
})
}
async fn describe(&self, dir: &Path, message: &str) -> Result<()> {
self.core
.unit(self.core.command_in(dir, ["describe", "-m", message]))
.await
}
async fn new_change(&self, dir: &Path, message: &str) -> Result<()> {
self.core
.unit(self.core.command_in(dir, ["new", "-m", message]))
.await
}
async fn bookmarks(&self, dir: &Path) -> Result<Vec<Bookmark>> {
self.core
.parse(
self.core.command_in(dir, ["bookmark", "list"]),
parse::parse_bookmarks,
)
.await
}
async fn bookmark_set(&self, dir: &Path, name: &str, revision: &str) -> Result<()> {
self.core
.unit(
self.core
.command_in(dir, ["bookmark", "set", name, "-r", revision]),
)
.await
}
async fn git_fetch(&self, dir: &Path) -> Result<()> {
self.core
.unit(self.core.command_in(dir, ["git", "fetch"]))
.await
}
async fn git_push(&self, dir: &Path, bookmark: Option<String>) -> Result<()> {
let mut args = vec!["git", "push"];
if let Some(name) = bookmark.as_deref() {
args.push("-b");
args.push(name);
}
self.core.unit(self.core.command_in(dir, args)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use processkit::{Reply, ScriptedRunner};
#[test]
fn binary_name_is_jj() {
assert_eq!(BINARY, "jj");
}
#[tokio::test]
async fn current_change_parses_scripted_output() {
let jj = Jj::with_runner(
ScriptedRunner::new().on(["log"], Reply::ok("kztuxlro\t38e00654\tfalse\thello jj\n")),
);
let change = jj
.current_change(Path::new("."))
.await
.expect("current_change");
assert_eq!(change.change_id, "kztuxlro");
assert!(!change.empty);
assert_eq!(change.description, "hello jj");
}
#[tokio::test]
async fn git_push_appends_bookmark_flag() {
let jj = Jj::with_runner(
ScriptedRunner::new().on(["git", "push", "-b", "feature"], Reply::ok("")),
);
jj.git_push(Path::new("."), Some("feature".to_string()))
.await
.expect("should build `git push -b feature`");
}
#[tokio::test]
async fn git_push_without_bookmark_is_bare() {
let jj = Jj::with_runner(ScriptedRunner::new().on(["git", "push"], Reply::ok("")));
jj.git_push(Path::new("."), None).await.expect("bare push");
}
#[cfg(feature = "mock")]
#[tokio::test]
async fn consumer_mocks_the_interface() {
let mut mock = MockJjApi::new();
mock.expect_describe().returning(|_, _| Ok(()));
assert!(mock.describe(Path::new("."), "msg").await.is_ok());
}
}