use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use colored::Colorize;
pub fn extract_repo_name(url: &str) -> Option<String> {
let mut cleaned = url.trim();
if cleaned.is_empty() {
return None;
}
if let Some(pos) = cleaned.find('?') {
cleaned = &cleaned[..pos];
}
if let Some(pos) = cleaned.find('#') {
cleaned = &cleaned[..pos];
}
while cleaned.ends_with('/') {
cleaned = &cleaned[..cleaned.len() - 1];
}
if cleaned.to_lowercase().ends_with(".git") {
cleaned = &cleaned[..cleaned.len() - 4];
}
while cleaned.ends_with('/') {
cleaned = &cleaned[..cleaned.len() - 1];
}
let last_sep = cleaned.rfind('/').or_else(|| cleaned.rfind(':'));
let name = match last_sep {
Some(pos) => &cleaned[pos + 1..],
None => cleaned,
};
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn check_target_exists(base: &Path, name: &str) -> Option<PathBuf> {
for cat in crate::classify::Category::all() {
let cat_dir = base.join(cat.dir_name());
if !cat_dir.exists() {
continue;
}
let standalone = cat_dir.join(name);
if standalone.exists() {
return Some(standalone);
}
if let Ok(rd) = std::fs::read_dir(&cat_dir) {
for entry in rd.filter_map(|e| e.ok()) {
if entry.path().is_dir() {
let grouped = entry.path().join(name);
if grouped.exists() {
return Some(grouped);
}
}
}
}
}
None
}
fn check_git_installed() -> Result<()> {
Command::new("git")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("git command not found on $PATH. Please install Git to use the clone command.")?;
Ok(())
}
fn open_editor(path: &Path) -> Result<()> {
let installed = crate::editors::detect_installed();
if installed.is_empty() {
eprintln!(" {} No supported editors found on $PATH.", "warning:".yellow().bold());
return Ok(());
}
let mut prefs = crate::prefs::Prefs::load()?;
let editor = prefs
.last_editor_for(path)
.map(|s| s.to_string())
.or_else(|| installed.iter().map(|e| e.binary.to_string()).next())
.unwrap_or_else(|| installed[0].binary.to_string());
println!(" Opening in {}...", editor.bold());
let mut cmd = Command::new(&editor);
cmd.arg(path);
cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null());
cmd.spawn().with_context(|| format!("Failed to spawn editor '{}'", editor))?;
prefs.set_last_editor(path, &editor)?;
Ok(())
}
pub fn run(url: &str, name: Option<String>, branch: Option<String>, open: bool) -> Result<()> {
check_git_installed()?;
let base = crate::config::load().base;
if !base.exists() {
std::fs::create_dir_all(&base)
.with_context(|| format!("Failed to create base directory: {}", base.display()))?;
}
let target_name = match name {
Some(n) => n.trim().to_string(),
None => extract_repo_name(url).ok_or_else(|| {
anyhow::anyhow!(
"Could not extract project name from URL. Please specify a custom name:\n \
projm clone <url> <name>"
)
})?,
};
if target_name.is_empty() {
anyhow::bail!("Project name cannot be empty.");
}
if let Some(existing_path) = check_target_exists(&base, &target_name) {
anyhow::bail!(
"A project named '{}' already exists in your organized directory at:\n {}",
target_name.bold(),
existing_path.display().to_string().dimmed()
);
}
println!(
" Cloning {} into temporary staging...",
url.cyan()
);
let staging_root = tempfile::Builder::new()
.prefix(".tmp_clone_")
.tempdir_in(&base)
.context("Failed to create temporary staging directory")?;
let staging_dir = staging_root.path().join(&target_name);
let mut cmd = Command::new("git");
cmd.arg("clone");
if let Some(ref b) = branch {
cmd.arg("--branch").arg(b);
}
cmd.arg(url).arg(&staging_dir);
let status = cmd.status().context("Failed to execute git clone command")?;
if !status.success() {
anyhow::bail!("git clone failed with exit status: {}", status);
}
println!(" {} Successfully cloned. Organizing project...", "✓".green().bold());
let dest_path = crate::organize::organize_single(&staging_dir)
.context("Failed to auto-organize cloned project")?;
println!(
" {} Organized under category: {}",
"✓".green().bold(),
dest_path.display().to_string().green()
);
if open {
open_editor(&dest_path)?;
}
Ok(())
}