use rust_i18n::t;
use crate::core::{VirtualenvService, list_installed_packages};
use crate::error::{Result, ScoopError};
use crate::output::{CloneData, Output};
use crate::paths::abbreviate_home;
use crate::validate;
pub fn execute(
output: &Output,
src: &str,
dst: &str,
no_packages: bool,
force: bool,
) -> Result<()> {
validate::validate_env_name(dst)?;
if src == dst {
return Err(ScoopError::InvalidArgument {
message: t!("clone.self_clone_error").to_string(),
});
}
let service = VirtualenvService::auto()?;
if !service.exists(src)? {
return Err(ScoopError::VirtualenvNotFound {
name: src.to_string(),
});
}
if service.exists(dst)? {
if force {
service.delete(dst)?;
} else {
return Err(ScoopError::VirtualenvExists {
name: dst.to_string(),
});
}
}
let src_path = service.get_path(src)?;
let metadata = service.read_metadata(&src_path);
let python = metadata
.as_ref()
.map(|m| m.python_version.clone())
.ok_or_else(|| ScoopError::CorruptedEnvironment {
name: src.to_string(),
reason: "missing metadata".to_string(),
})?;
output.info(&t!("clone.cloning", src = src, dst = dst, python = python));
let dst_path = service.create(dst, &python)?;
let packages = if no_packages {
output.info(&t!("clone.no_packages_skipped"));
Vec::new()
} else {
let pkgs = list_installed_packages(&src_path);
if pkgs.is_empty() {
Vec::new()
} else {
output.info(&t!("clone.copying_packages", count = pkgs.len()));
let specs: Vec<String> = pkgs.iter().map(|(n, v)| format!("{n}=={v}")).collect();
service.pip_install(&dst_path, &specs)?;
pkgs
}
};
if output.is_json() {
output.json_success(
"clone",
CloneData {
src: src.to_string(),
dst: dst.to_string(),
python: python.clone(),
path: dst_path.display().to_string(),
packages_copied: packages.len(),
packages_skipped: no_packages,
},
);
return Ok(());
}
output.success(&t!("clone.success", src = src, dst = dst));
output.info(&format!(" Path: {}", abbreviate_home(&dst_path)));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::with_temp_scoop_home;
use serial_test::serial;
#[test]
#[serial]
fn execute_rejects_invalid_dst_name() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(temp_dir.path().join("virtualenvs")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "src", "list", false, false).unwrap_err();
assert!(matches!(err, ScoopError::InvalidEnvName { .. }));
});
}
#[test]
#[serial]
fn execute_rejects_self_clone() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(temp_dir.path().join("virtualenvs")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "same", "same", false, false).unwrap_err();
assert!(matches!(err, ScoopError::InvalidArgument { .. }));
});
}
#[test]
#[serial]
fn execute_returns_not_found_when_src_missing() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(temp_dir.path().join("virtualenvs")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "ghost", "newcopy", false, false).unwrap_err();
assert!(matches!(err, ScoopError::VirtualenvNotFound { .. }));
});
}
#[test]
#[serial]
fn execute_rejects_existing_dst_without_force() {
with_temp_scoop_home(|temp_dir| {
let src = temp_dir.path().join("virtualenvs").join("source");
std::fs::create_dir_all(src.join("bin")).unwrap();
let meta = crate::core::Metadata {
name: "source".to_string(),
python_version: "3.12.0".to_string(),
created_at: chrono::Utc::now(),
created_by: "test".to_string(),
uv_version: None,
python_path: None,
};
std::fs::write(
src.join(".scoop-metadata.json"),
serde_json::to_string(&meta).unwrap(),
)
.unwrap();
std::fs::create_dir_all(temp_dir.path().join("virtualenvs").join("dupe")).unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "source", "dupe", false, false ).unwrap_err();
assert!(matches!(err, ScoopError::VirtualenvExists { .. }));
});
}
#[test]
#[serial]
fn execute_surfaces_corrupted_when_metadata_missing() {
with_temp_scoop_home(|temp_dir| {
std::fs::create_dir_all(
temp_dir
.path()
.join("virtualenvs")
.join("nometa")
.join("bin"),
)
.unwrap();
let output = Output::new(0, true, true, false);
let err = execute(&output, "nometa", "newone", false, false).unwrap_err();
assert!(matches!(err, ScoopError::CorruptedEnvironment { .. }));
});
}
}