use anyhow::{Result};
use std::fs;
use std::path::Path;
use toml_edit::{value, DocumentMut};
use colored::Colorize;
use dialoguer::MultiSelect;
use crate::commands::test_mode::is_test_mode;
use super::utils::update_root_file_references;
use ferrisup_common::fs::copy_directory;
use super::ui::{create_root_readme, create_root_gitignore};
pub fn categorize_files(
project_dir: &Path,
component_name: &str,
files_to_keep_at_root: &[String]
) -> Result<(Vec<String>, Vec<String>, Vec<String>, Vec<String>)> {
let always_skip_filenames = vec![
"Cargo.toml".to_string(), "Cargo.lock".to_string(), ".git".to_string(), ".ferrisup".to_string(), component_name.to_string(), ];
let critical_component_files = vec![
"src".to_string(), "build.rs".to_string(), "benches".to_string(), "examples".to_string(), "bin".to_string() ];
let mut critical_files_to_move = Vec::new();
let mut other_files_to_move = Vec::new();
let mut files_kept_at_root = Vec::new();
let mut workspace_files = Vec::new();
let entries = fs::read_dir(project_dir)?; for entry in entries {
let entry = entry?;
let src_path = entry.path();
let file_name = src_path.file_name().unwrap().to_string_lossy().to_string();
if always_skip_filenames.contains(&file_name) {
workspace_files.push(file_name);
} else if files_to_keep_at_root.contains(&file_name) {
files_kept_at_root.push(file_name);
} else if critical_component_files.contains(&file_name) {
critical_files_to_move.push(file_name);
} else {
other_files_to_move.push(file_name);
}
}
Ok((critical_files_to_move, other_files_to_move, files_kept_at_root, workspace_files))
}
pub fn select_files_to_keep_at_root(project_dir: &Path, component_name: &str) -> Result<Vec<String>> {
let all_root_entries: Vec<String> = fs::read_dir(project_dir)?
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
let always_skip_filenames = vec![
"Cargo.toml".to_string(), "Cargo.lock".to_string(), ".git".to_string(), ".ferrisup".to_string(), component_name.to_string(), ];
let critical_component_files = vec![
"src".to_string(), "build.rs".to_string(), "benches".to_string(), "examples".to_string(), "bin".to_string() ];
let build_artifacts_and_temp_files = vec![
"target".to_string(), ".idea".to_string(), ".vscode".to_string(), ".DS_Store".to_string(), "Cargo.lock".to_string(), "*.log".to_string(), "*.tmp".to_string(), "*.swp".to_string(), "*.bak".to_string(), ];
let selectable_entries_for_prompt: Vec<String> = all_root_entries
.iter()
.filter(|name| {
!always_skip_filenames.contains(name) &&
!critical_component_files.contains(name) &&
build_artifacts_and_temp_files.iter().any(|pattern| {
if pattern.contains('*') {
let pattern_parts: Vec<&str> = pattern.split('*').collect();
if pattern_parts.len() == 2 {
let prefix = pattern_parts[0];
let suffix = pattern_parts[1];
name.starts_with(prefix) && name.ends_with(suffix)
} else {
pattern == *name
}
} else {
pattern == *name
}
})
})
.cloned()
.collect();
if !critical_component_files.is_empty() {
println!(
"{}",
"\nIMPORTANT: The following critical files/directories will automatically be moved to the component:".bold().yellow()
);
for file in &critical_component_files {
if all_root_entries.contains(file) {
println!(" - {}", file.cyan());
}
}
println!();
}
let files_to_keep_at_root: Vec<String> = if !selectable_entries_for_prompt.is_empty() {
println!(
"{}",
"The following files/directories are in your project root:".yellow()
);
let default_selections: Vec<bool> = selectable_entries_for_prompt
.iter()
.map(|entry_name| {
build_artifacts_and_temp_files.contains(entry_name)
})
.collect();
println!(
"{}",
"\nSAFE DEFAULTS: Only build artifacts and temporary files are pre-selected to stay at root.".bold().green()
);
println!(
"{}",
"All source code, documentation, and project-specific files will be moved to the component.".green()
);
println!();
if is_test_mode() {
selectable_entries_for_prompt
.iter()
.zip(default_selections.iter())
.filter_map(|(name, &selected)| if selected { Some(name.clone()) } else { None })
.collect()
} else {
let selections = MultiSelect::new()
.items(&selectable_entries_for_prompt)
.with_prompt("Select files/directories to KEEP at the project root (they will NOT be moved to the new component). Use Space to select/deselect, Enter to confirm.")
.defaults(&default_selections)
.interact()?;
selections
.into_iter()
.map(|index| selectable_entries_for_prompt[index].clone())
.collect()
}
} else {
println!("{}", "No movable files found in the project root to select for keeping.".yellow());
Vec::new()
};
if !files_to_keep_at_root.is_empty() {
println!(
"{} {:?}",
"Files selected to keep at root (will not be moved):".cyan(),
files_to_keep_at_root
);
}
Ok(files_to_keep_at_root)
}
pub fn move_files_to_component(
project_dir: &Path,
component_dir: &Path,
files_to_keep_at_root: &[String],
always_skip_filenames: &[String]
) -> Result<()> {
println!("{}", "Moving files to component directory...".blue());
let entries = fs::read_dir(project_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
if always_skip_filenames.contains(&file_name) || files_to_keep_at_root.contains(&file_name) {
continue;
}
let target_path = component_dir.join(&file_name);
if path.is_dir() {
copy_directory(&path, &target_path)?;
fs::remove_dir_all(&path)?;
} else {
fs::copy(&path, &target_path)?;
fs::remove_file(&path)?;
}
}
Ok(())
}
pub fn update_component_cargo_toml(component_dir: &Path, component_name: &str) -> Result<()> {
let component_cargo_path = component_dir.join("Cargo.toml");
if component_cargo_path.exists() {
let component_cargo_content = fs::read_to_string(&component_cargo_path)?;
let mut component_doc = component_cargo_content.parse::<DocumentMut>()?;
if let Some(package) = component_doc.get_mut("package") {
if let Some(table) = package.as_table_mut() {
table.insert("name", value(component_name.to_lowercase()));
}
}
fs::write(component_cargo_path, component_doc.to_string())?;
}
Ok(())
}
pub fn create_workspace_cargo_toml(project_dir: &Path, component_name: &str) -> Result<()> {
let workspace_cargo_toml = format!(
r#"[workspace]
members = [
"{}"
]
[workspace.package]
version = "0.1.0"
edition = "2021"
resolver = "2"
"#,
component_name
);
fs::write(project_dir.join("Cargo.toml"), workspace_cargo_toml)?;
Ok(())
}
pub fn finalize_workspace_setup(
project_dir: &Path,
_component_dir: &Path,
component_name: &str,
files_to_keep_at_root: &[String]
) -> Result<()> {
update_root_file_references(project_dir, component_name, files_to_keep_at_root)?;
create_root_readme(project_dir, component_name)?;
create_root_gitignore(project_dir)?;
println!("{}", "Project successfully converted to workspace!".green());
Ok(())
}