mod git;
mod sapling;
pub use git::GitVcs;
pub use sapling::SaplingVcs;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::VcsError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsType {
Git,
Sapling,
}
impl fmt::Display for VcsType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VcsType::Git => write!(f, "Git"),
VcsType::Sapling => write!(f, "Sapling"),
}
}
}
#[derive(Debug, Clone)]
pub struct VcsDetection {
pub vcs_type: VcsType,
pub root: PathBuf,
}
pub trait Vcs: Send + Sync {
fn vcs_type(&self) -> VcsType;
fn has_changes(&self) -> Result<bool, VcsError>;
fn stage_all(&self) -> Result<(), VcsError>;
fn commit(&self, message: &str) -> Result<String, VcsError>;
fn commit_all(&self, message: &str) -> Result<Option<String>, VcsError> {
if !self.has_changes()? {
return Ok(None);
}
self.stage_all()?;
let hash = self.commit(message)?;
Ok(Some(hash))
}
}
pub fn detect_vcs(start_path: &Path) -> Result<Option<VcsDetection>, VcsError> {
let search_path = if start_path.is_file() {
start_path.parent().unwrap_or(start_path)
} else {
start_path
};
let canonical = search_path
.canonicalize()
.map_err(|e| VcsError::Detection(format!("Failed to canonicalize path: {}", e)))?;
if let Ok(output) = Command::new("sl")
.args(["root"])
.current_dir(&canonical)
.output()
{
if output.status.success() {
let root_str = String::from_utf8_lossy(&output.stdout);
let root = PathBuf::from(root_str.trim());
return Ok(Some(VcsDetection {
vcs_type: VcsType::Sapling,
root,
}));
}
}
let mut current = canonical.as_path();
loop {
let git_dir = current.join(".git");
if git_dir.exists() {
if Command::new("git")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Ok(Some(VcsDetection {
vcs_type: VcsType::Git,
root: current.to_path_buf(),
}));
}
}
match current.parent() {
Some(parent) => current = parent,
None => break,
}
}
Ok(None)
}
pub fn create_vcs(working_dir: &Path) -> Option<Box<dyn Vcs>> {
match detect_vcs(working_dir) {
Ok(Some(detection)) => match detection.vcs_type {
VcsType::Git => Some(Box::new(GitVcs::new(detection.root))),
VcsType::Sapling => Some(Box::new(SaplingVcs::new(detection.root))),
},
Ok(None) => None,
Err(e) => {
eprintln!("[VCS] Warning: Detection failed: {}", e);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_vcs_type_display() {
assert_eq!(VcsType::Git.to_string(), "Git");
assert_eq!(VcsType::Sapling.to_string(), "Sapling");
}
#[test]
fn test_detect_vcs_in_git_repo() {
let dir = TempDir::new().expect("temp dir");
Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.expect("git init");
let detection = detect_vcs(dir.path()).expect("detect");
assert!(detection.is_some());
let det = detection.unwrap();
assert!(det.vcs_type == VcsType::Git || det.vcs_type == VcsType::Sapling);
}
#[test]
fn test_detect_vcs_no_repo() {
let dir = TempDir::new().expect("temp dir");
let detection = detect_vcs(dir.path()).expect("detect");
let _ = detection;
}
#[test]
fn test_create_vcs_returns_implementation() {
let dir = TempDir::new().expect("temp dir");
Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.expect("git init");
let vcs = create_vcs(dir.path());
assert!(vcs.is_some());
}
}