mod git;
mod jj;
use std::path::Path;
use anyhow::{bail, Result};
use vcs_runner::{jj_available, run_git_utf8};
pub use self::git::GitBackend;
pub use self::jj::JjBackend;
pub trait Vcs: Send + Sync {
fn detect_trunk(&self, project_dir: &Path) -> Result<String>;
fn create_workspace(&self, project_dir: &Path, ws_dir: &Path, ws_id: &str, trunk: &str) -> Result<()>;
fn pre_copy_sync(&self, project_dir: &Path);
fn changed_files(&self, ws_id: &str, project_dir: &Path, ws_dir: &Path) -> Vec<String>;
fn save_work(&self, ws_id: &str, project_dir: &Path, ws_dir: &Path) -> Result<()>;
fn forget_workspace(&self, ws_id: &str, project_dir: &Path, ws_dir: &Path);
fn ignore_generated_file(&self, project_dir: &Path, ws_dir: &Path, relpath: &str);
}
pub fn detect(project_dir: &Path) -> Result<Box<dyn Vcs>> {
if project_dir.join(".jj").is_dir() {
return Ok(Box::new(JjBackend));
}
let has_git = project_dir.join(".git").is_dir();
if !has_git {
bail!("not a git or jj repository");
}
if jj_available() {
jj::init_jj(project_dir)?;
return Ok(Box::new(JjBackend));
}
Ok(Box::new(GitBackend))
}
pub(crate) fn path_str(p: &Path) -> String {
p.to_string_lossy().into_owned()
}
pub(crate) fn append_git_exclude(project_dir: &Path, pattern: &str) -> Result<()> {
let git_dir = run_git_utf8(project_dir, &["rev-parse", "--absolute-git-dir"])
.map_err(|e| anyhow::anyhow!("could not locate .git directory: {e}"))?;
let exclude = Path::new(git_dir.trim()).join("info").join("exclude");
let existing = std::fs::read_to_string(&exclude).unwrap_or_default();
if existing.lines().any(|l| l.trim() == pattern) {
return Ok(());
}
if let Some(parent) = exclude.parent() {
std::fs::create_dir_all(parent)?;
}
let mut content = existing;
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str(pattern);
content.push('\n');
std::fs::write(&exclude, content)?;
Ok(())
}
pub(crate) fn detect_git_remote(project_dir: &Path) -> String {
run_git_utf8(project_dir, &["remote"])
.ok()
.and_then(|s| s.lines().next().map(|l| l.to_string()))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "origin".into())
}