use crate::tools::ensure_user_profile_bin_paths;
use crate::workspace::WorkspaceError;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JjWorkspaceEntry {
pub name: String,
pub root: PathBuf,
}
#[derive(Debug, Default, Clone)]
pub struct JjCommand {
current_dir: Option<PathBuf>,
}
impl JjCommand {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn current_dir(mut self, dir: impl AsRef<Path>) -> Self {
self.current_dir = Some(dir.as_ref().to_path_buf());
self
}
fn build_cmd(&self, args: &[&str]) -> Command {
let mut cmd = Command::new("jj");
cmd.arg("--no-pager");
cmd.args(args);
if let Some(ref dir) = self.current_dir {
cmd.current_dir(dir);
}
ensure_user_profile_bin_paths(&mut cmd);
cmd
}
fn exec(&self, args: &[&str]) -> Result<std::process::Output, WorkspaceError> {
self.build_cmd(args).output().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
WorkspaceError::NotInJjRepo
} else {
WorkspaceError::Io(err)
}
})
}
pub fn workspace_root(&self, name: Option<&str>) -> Result<PathBuf, WorkspaceError> {
let mut args = vec!["workspace", "root"];
if let Some(n) = name {
args.extend(["--name", n]);
}
let output = self.exec(&args)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr_lower = stderr.to_lowercase();
if stderr_lower.contains("no jj repo") || stderr_lower.contains("there is no jj repo") {
return Err(WorkspaceError::NotInJjRepo);
}
return Err(WorkspaceError::JjCommandFailed(stderr.trim().to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
if trimmed.is_empty() {
return Err(WorkspaceError::JjCommandFailed(
"Empty output from jj workspace root".to_string(),
));
}
let path = PathBuf::from(trimmed);
Ok(path.canonicalize().unwrap_or(path))
}
pub fn workspace_list(&self) -> Result<Vec<JjWorkspaceEntry>, WorkspaceError> {
let templated =
self.exec(&["workspace", "list", "-T", r#"name ++ "\t" ++ root ++ "\n""#])?;
if templated.status.success() {
let stdout = String::from_utf8_lossy(&templated.stdout);
let mut entries = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some((name, root_str)) = line.split_once('\t') {
let root = PathBuf::from(root_str.trim());
let canonical = root.canonicalize().unwrap_or(root);
entries.push(JjWorkspaceEntry {
name: name.trim().to_string(),
root: canonical,
});
}
}
return Ok(entries);
}
let stderr = String::from_utf8_lossy(&templated.stderr);
let stderr_lower = stderr.to_lowercase();
if stderr_lower.contains("no jj repo") || stderr_lower.contains("there is no jj repo") {
return Err(WorkspaceError::NotInJjRepo);
}
let fallback = self.exec(&["workspace", "list"])?;
if !fallback.status.success() {
let fb_stderr = String::from_utf8_lossy(&fallback.stderr);
let fb_lower = fb_stderr.to_lowercase();
if fb_lower.contains("no jj repo") || fb_lower.contains("there is no jj repo") {
return Err(WorkspaceError::NotInJjRepo);
}
return Err(WorkspaceError::JjCommandFailed(
fb_stderr.trim().to_string(),
));
}
let stdout = String::from_utf8_lossy(&fallback.stdout);
let mut entries = Vec::new();
let base_dir = self
.current_dir
.clone()
.unwrap_or_else(|| PathBuf::from("."));
for line in stdout.lines() {
if let Some((name, _)) = line.split_once(':') {
let name = name.trim().to_string();
let root = base_dir.join(".workspaces").join(&name);
entries.push(JjWorkspaceEntry { name, root });
}
}
Ok(entries)
}
pub fn workspace_add(&self, path: &Path, name: &str) -> Result<(), WorkspaceError> {
let path_str = path.to_string_lossy();
let output = self.exec(&["workspace", "add", &path_str, "--name", name])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr_lower = stderr.to_lowercase();
if stderr_lower.contains("no jj repo") || stderr_lower.contains("there is no jj repo") {
return Err(WorkspaceError::NotInJjRepo);
}
return Err(WorkspaceError::JjCommandFailed(stderr.trim().to_string()));
}
Ok(())
}
pub fn workspace_forget(&self, name: &str) -> Result<(), WorkspaceError> {
let output = self.exec(&["workspace", "forget", name])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr_lower = stderr.to_lowercase();
if stderr_lower.contains("no jj repo") || stderr_lower.contains("there is no jj repo") {
return Err(WorkspaceError::NotInJjRepo);
}
return Err(WorkspaceError::JjCommandFailed(stderr.trim().to_string()));
}
Ok(())
}
}