update_project.rs
1005 lignes · 31370 octets
use std::fs; use std::path::{Path, PathBuf}; use console::style; use dialoguer::{Confirm, Input}; use crate::version; /// Fichiers et dossiers du framework qui sont remplaces lors de la mise a jour. /// `rustwarden-core` est toujours synchronise. Les autres crates ne sont /// synchronisees que si elles sont presentes dans le projet consommateur /// (detection en debut de `run()`). const FRAMEWORK_CRATES: &[&str] = &[ "rustwarden-core", "rustwarden-ui", "rustwarden-fediverse", "rustwarden-fediverse-ui", "rustwarden-fediverse-guard", ]; /// Fichiers du projet consommateur qu'on ne touche JAMAIS. const PROTECTED_FILES: &[&str] = &[ "Cargo.toml", "src/main.rs", ".env", ".env.example", "README.md", "DEVELOPER.md", ]; /// Lance la commande update-project. pub fn run() -> Result<(), Box<dyn std::error::Error>> { println!( "\n{}", style(" Rustwarden - Mise a jour du framework ") .bold() .on_cyan() .black(), ); println!(); let rustwarden_root = find_rustwarden_root()?; let project_dir = ask_project_dir()?; validate_consumer_project(&project_dir)?; let current_version = version::read_version(&project_dir); let new_version = version::compute_version(&rustwarden_root)?; print_version_info(¤t_version, &new_version); let has_ui = project_dir.join("crates/rustwarden-ui").exists(); let has_fediverse = project_dir.join("crates/rustwarden-fediverse").exists(); let has_fediverse_ui = project_dir.join("crates/rustwarden-fediverse-ui").exists(); let has_fediverse_guard = project_dir .join("crates/rustwarden-fediverse-guard") .exists(); let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui); // Si la fédération (ou une de ses crates) manque, proposer de l'ajouter. let mut newly_added: Vec<&'static str> = Vec::new(); let (has_fediverse, has_fediverse_ui, has_fediverse_guard) = prompt_fediverse_addition( has_fediverse, has_fediverse_ui, has_fediverse_guard, &mut newly_added, )?; print_update_plan( &project_dir, has_ui, has_fediverse, has_fediverse_ui, has_fediverse_guard, &newly_added, &custom_templates, ); if !Confirm::new() .with_prompt(format!("{}", style("Proceder a la mise a jour ?").cyan())) .default(false) .interact()? { println!(" {} Operation annulee.", style("✗").red()); return Ok(()); } // --- Backup --- let backup_dir = create_backup(&project_dir)?; // --- Mise a jour des crates --- let stats = update_crates( &rustwarden_root, &project_dir, has_ui, has_fediverse, has_fediverse_ui, has_fediverse_guard, &custom_templates, )?; // --- Resync des docs framework-owned (SERVICES_REFERENCE, MIGRATION_GUIDE, …) --- refresh_docs(&rustwarden_root, &project_dir, has_fediverse)?; // --- Reecrire le fichier de version --- version::write_version(&project_dir, &new_version)?; // --- Resume --- print_summary( &project_dir, &backup_dir, &custom_templates, &newly_added, &stats, ); Ok(()) } /// Quand le projet n'inclut pas (encore) la fédération ActivityPub, /// propose à l'utilisateur de l'ajouter pendant l'update. Retourne le /// nouveau triplet `(has_fediverse, has_fediverse_ui, has_fediverse_guard)` /// et pousse dans `newly_added` les noms de crates effectivement ajoutées. fn prompt_fediverse_addition( mut has_fediverse: bool, mut has_fediverse_ui: bool, mut has_fediverse_guard: bool, newly_added: &mut Vec<&'static str>, ) -> Result<(bool, bool, bool), Box<dyn std::error::Error>> { // Cas 1 : la fédération est absente — proposer le bundle complet. if !has_fediverse { println!("\n{}", style("── Fédération ActivityPub ──").cyan().bold()); println!( " Ce projet n'inclut pas la fédération (rustwarden-fediverse).\n \ Périmètre, features Cargo et câblage : docs/fediverse-consumer-guide.md" ); if !Confirm::new() .with_prompt(format!( "{}", style("Ajouter le support fédération à ce projet ?").cyan() )) .default(false) .interact()? { return Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard)); } has_fediverse = true; newly_added.push("rustwarden-fediverse"); } // Cas 2 / extension : -ui absent. if !has_fediverse_ui && Confirm::new() .with_prompt(format!( "{}", style("Inclure aussi l'UI HTML (rustwarden-fediverse-ui) ?").cyan() )) .default(false) .interact()? { has_fediverse_ui = true; newly_added.push("rustwarden-fediverse-ui"); } // Cas 3 / extension : -guard absent. if !has_fediverse_guard && Confirm::new() .with_prompt(format!( "{}", style("Inclure aussi le guard anti-abus (rustwarden-fediverse-guard) ?").cyan() )) .default(false) .interact()? { has_fediverse_guard = true; newly_added.push("rustwarden-fediverse-guard"); } Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard)) } // --------------------------------------------------------------------------- // Detection du projet consommateur // --------------------------------------------------------------------------- /// Demande le chemin du projet consommateur. fn ask_project_dir() -> Result<PathBuf, Box<dyn std::error::Error>> { let dir: String = Input::new() .with_prompt(format!( "{}", style("Chemin du projet consommateur a mettre a jour").cyan() )) .default(".".into()) .interact_text()?; let path = PathBuf::from(&dir); let path = if path.is_absolute() { path } else { std::env::current_dir()?.join(path) }; Ok(path.canonicalize().unwrap_or(path)) } /// Verifie que le dossier est bien un projet consommateur Rustwarden. fn validate_consumer_project(project_dir: &Path) -> Result<(), Box<dyn std::error::Error>> { if !project_dir.join("crates/rustwarden-core").exists() { return Err(format!( "Le dossier {} ne semble pas etre un projet Rustwarden.\n\ (crates/rustwarden-core/ introuvable)", project_dir.display() ) .into()); } if !project_dir.join("Cargo.toml").exists() { return Err(format!( "Pas de Cargo.toml dans {}. Est-ce bien la racine du projet ?", project_dir.display() ) .into()); } Ok(()) } // --------------------------------------------------------------------------- // Detection des templates personnalises // --------------------------------------------------------------------------- /// Compare les templates du projet avec ceux du framework pour detecter les /// fichiers modifies par le consommateur. fn detect_custom_templates( project_dir: &Path, rustwarden_root: &Path, has_ui: bool, ) -> Vec<String> { let mut custom = Vec::new(); if !has_ui { return custom; } let project_templates = project_dir.join("crates/rustwarden-ui/templates"); let framework_templates = rustwarden_root.join("crates/rustwarden-ui/templates"); if !project_templates.exists() || !framework_templates.exists() { return custom; } compare_dirs_recursive( &project_templates, &framework_templates, &project_templates, &mut custom, ); custom } /// Compare recursivement deux dossiers et enregistre les fichiers differents. fn compare_dirs_recursive( project_dir: &Path, framework_dir: &Path, base: &Path, custom: &mut Vec<String>, ) { let entries = match fs::read_dir(project_dir) { Ok(e) => e, Err(_) => return, }; for entry in entries.flatten() { let path = entry.path(); let relative = path.strip_prefix(base).unwrap_or(&path); let framework_path = framework_dir.join(relative); if path.is_dir() { compare_dirs_recursive(&path, &framework_dir.join(entry.file_name()), base, custom); } else if path.extension().and_then(|e| e.to_str()) == Some("html") { if !framework_path.exists() { // Fichier qui n'existe pas dans le framework = fichier custom custom.push(relative.to_string_lossy().to_string()); } else { // Comparer le contenu let Ok(project_content) = fs::read(&path) else { continue; }; let Ok(framework_content) = fs::read(&framework_path) else { continue; }; if project_content != framework_content { custom.push(relative.to_string_lossy().to_string()); } } } } } // --------------------------------------------------------------------------- // Affichage // --------------------------------------------------------------------------- /// Affiche les informations de version. fn print_version_info(current: &Option<String>, new: &str) { println!("\n{}", style("── Versions ──").cyan().bold()); match current { Some(v) => println!(" {} {}", style("Actuelle :").dim(), style(v).white()), None => println!( " {} {}", style("Actuelle :").dim(), style("inconnue (pas de .rustwarden-version)").yellow() ), } println!(" {} {}", style("Nouvelle :").dim(), style(new).green()); } /// Affiche le plan de mise a jour. fn print_update_plan( project_dir: &Path, has_ui: bool, has_fediverse: bool, has_fediverse_ui: bool, has_fediverse_guard: bool, newly_added: &[&'static str], custom_templates: &[String], ) { println!("\n{}", style("── Plan de mise a jour ──").cyan().bold()); println!(" {} {}", style("Projet :").dim(), project_dir.display()); println!("\n {} :", style("Sera mis a jour").green().bold()); println!(" - crates/rustwarden-core/"); if has_ui { println!(" - crates/rustwarden-ui/"); } let added = |name: &str| newly_added.contains(&name); if has_fediverse { let tag = if added("rustwarden-fediverse") { style(" (NOUVEAU)").green().bold().to_string() } else { String::new() }; println!(" - crates/rustwarden-fediverse/{tag}"); } if has_fediverse_ui { let tag = if added("rustwarden-fediverse-ui") { style(" (NOUVEAU)").green().bold().to_string() } else { String::new() }; println!(" - crates/rustwarden-fediverse-ui/{tag}"); } if has_fediverse_guard { let tag = if added("rustwarden-fediverse-guard") { style(" (NOUVEAU)").green().bold().to_string() } else { String::new() }; println!(" - crates/rustwarden-fediverse-guard/{tag}"); } println!( "\n {} :", style("Preserve (jamais touche)").yellow().bold() ); for f in PROTECTED_FILES { if project_dir.join(f).exists() { println!(" - {f}"); } } if !custom_templates.is_empty() { println!( "\n {} {} template(s) personnalise(s) detecte(s) :", style("⚠").yellow().bold(), custom_templates.len() ); for t in custom_templates { println!(" - templates/{t}"); } println!( " {}", style(" Ces fichiers seront preserves (non ecrases).").green() ); } println!( "\n {}", style("Seuls les fichiers modifies dans le framework seront mis a jour.").dim() ); println!( " {}", style("Un backup complet des crates sera cree avant la mise a jour.").dim() ); } /// Statistiques de synchronisation. #[derive(Default)] struct SyncStats { updated: Vec<String>, added: Vec<String>, skipped_custom: Vec<String>, unchanged: u32, removed: Vec<String>, } /// Affiche le resume final. fn print_summary( project_dir: &Path, backup_dir: &Path, custom_templates: &[String], newly_added: &[&'static str], stats: &SyncStats, ) { let divider = style("─".repeat(55)).dim(); println!("\n{divider}"); println!(" {}", style("Mise a jour terminee !").green().bold()); println!("{divider}\n"); println!(" {} {}", style("Backup :").bold(), backup_dir.display()); if !stats.added.is_empty() { println!( "\n {} {} fichier(s) ajoute(s) :", style("+").green().bold(), stats.added.len() ); for f in &stats.added { println!(" {}", style(f).green()); } } if !stats.updated.is_empty() { println!( "\n {} {} fichier(s) mis a jour :", style("~").cyan().bold(), stats.updated.len() ); for f in &stats.updated { println!(" {}", style(f).cyan()); } } if !stats.removed.is_empty() { println!( "\n {} {} fichier(s) supprime(s) (n'existent plus dans le framework) :", style("-").red().bold(), stats.removed.len() ); for f in &stats.removed { println!(" {}", style(f).red()); } } if !stats.skipped_custom.is_empty() { println!( "\n {} {} template(s) personnalise(s) preserve(s) :", style("⚠").yellow().bold(), stats.skipped_custom.len() ); for f in &stats.skipped_custom { println!(" {}", style(f).yellow()); } } if stats.unchanged > 0 { println!( "\n {} {} fichier(s) identiques (non modifies)", style("=").dim(), stats.unchanged ); } let _ = (project_dir, custom_templates); // utilises dans le plan println!("\n{divider}"); println!(" {}", style("Prochaines etapes :").bold()); println!("{divider}\n"); println!(" {} Verifier la compilation :", style("1.").cyan().bold()); println!( " {}", style(format!("cd {} && cargo check", project_dir.display())).green() ); println!( " {} Corriger les eventuels breaking changes :", style("2.").cyan().bold() ); println!(" {}", style("cargo clippy -- -D warnings").green()); println!(" {} Verifier les migrations :", style("3.").cyan().bold()); println!( " {}", style("cargo run -p rustwarden-core --bin migrate").green() ); println!(" {} Lancer les tests :", style("4.").cyan().bold()); println!(" {}", style("cargo test").green()); println!(); println!( " • Nouveaux écrans éventuels : {} (puis {})", style("cargo run -p rustwarden-core --bin screen-select").green(), style("-- --check").green() ); println!(); if !newly_added.is_empty() { let divider = style("─".repeat(55)).dim(); println!("{divider}"); println!( " {}", style("Etapes supplementaires pour la federation :") .yellow() .bold() ); println!("{divider}\n"); println!(" {} crate(s) ajoutee(s) :", style("+").green().bold()); for c in newly_added { println!(" - crates/{c}/"); } println!( "\n {} Ces crates ont ete copiees mais ne sont pas encore", style("!").yellow().bold() ); println!(" cablees. Pour les activer dans votre app :"); println!(); println!( " {} Ajouter dans {} :", style("a)").cyan(), style("Cargo.toml").white() ); if newly_added.contains(&"rustwarden-fediverse") { println!( " {}", style( "[features]\n \ fediverse = [\"dep:rustwarden-fediverse\"]" ) .green() ); } if newly_added.contains(&"rustwarden-fediverse-ui") { println!( " {}", style("fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]").green() ); } if newly_added.contains(&"rustwarden-fediverse-guard") { println!( " {}", style("fediverse-guard = [\"fediverse\", \"dep:rustwarden-fediverse-guard\"]") .green() ); } println!(); println!( " {} Reproduire le cablage de reference dans src/main.rs", style("b)").cyan() ); println!(" (FediverseConfig::from_env, LocalUserResolver,"); println!(" SignatureVerifier, register_revocation_checker,"); println!(" merge_routes, OutboundWorker dans tokio::spawn)."); println!(); println!( " {} Configurer les variables d'environnement requises :", style("c)").cyan() ); println!( " {}", style("FEDIVERSE_DOMAIN, ENCRYPTION_KEY, JWT_SECRET").green() ); println!(); println!( " {} Appliquer les migrations fediverse :", style("d)").cyan() ); println!( " {}", style("cargo run -p rustwarden-core --bin migrate").green() ); println!(); println!( " {} Guide complet : {}", style("→").dim(), style("docs/fediverse-consumer-guide.md") .cyan() .underlined() ); println!( " {} Reference API : {}", style("→").dim(), style("docs/SERVICES_REFERENCE.md §21").cyan().underlined() ); println!(); } } // --------------------------------------------------------------------------- // Backup // --------------------------------------------------------------------------- /// Cree un backup des crates du framework dans le projet. fn create_backup(project_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> { let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); let backup_dir = project_dir.join(format!(".rustwarden-backup-{timestamp}")); fs::create_dir_all(&backup_dir)?; println!( "\n {} {}", style("Backup").dim(), style(backup_dir.display().to_string()).white() ); for crate_name in FRAMEWORK_CRATES { let src = project_dir.join("crates").join(crate_name); if src.exists() { let dst = backup_dir.join("crates").join(crate_name); copy_dir_recursive(&src, &dst)?; println!(" {} crates/{crate_name}/", style("→").dim(),); } } Ok(backup_dir) } // --------------------------------------------------------------------------- // Mise a jour // --------------------------------------------------------------------------- /// Met a jour les crates du framework via sync fichier par fichier. fn update_crates( rustwarden_root: &Path, project_dir: &Path, has_ui: bool, has_fediverse: bool, has_fediverse_ui: bool, has_fediverse_guard: bool, custom_templates: &[String], ) -> Result<SyncStats, Box<dyn std::error::Error>> { println!("\n{}", style("── Mise a jour des crates ──").cyan().bold()); let mut stats = SyncStats::default(); // rustwarden-core : toujours mis a jour sync_crate( rustwarden_root, project_dir, "rustwarden-core", &[], &mut stats, )?; // rustwarden-ui : uniquement si le projet l'utilise if has_ui { sync_crate( rustwarden_root, project_dir, "rustwarden-ui", custom_templates, &mut stats, )?; } // rustwarden-fediverse : uniquement si le projet l'utilise if has_fediverse { sync_crate( rustwarden_root, project_dir, "rustwarden-fediverse", &[], &mut stats, )?; } // rustwarden-fediverse-ui : uniquement si le projet l'utilise if has_fediverse_ui { sync_crate( rustwarden_root, project_dir, "rustwarden-fediverse-ui", &[], &mut stats, )?; } // rustwarden-fediverse-guard : uniquement si le projet l'utilise if has_fediverse_guard { sync_crate( rustwarden_root, project_dir, "rustwarden-fediverse-guard", &[], &mut stats, )?; } Ok(stats) } /// Resynchronise les documents framework-owned dans `<project>/docs/`. /// /// Ces fichiers ne sont jamais customisés par le consommateur ; ils sont /// systématiquement réécrits à l'identique de l'upstream à chaque update, /// afin que la documentation de référence reste alignée avec la version /// installée des crates. Le contenu fediverse n'est copié que si le projet /// utilise effectivement la fédération. fn refresh_docs( rustwarden_root: &Path, project_dir: &Path, has_fediverse: bool, ) -> Result<(), Box<dyn std::error::Error>> { let mut docs_to_copy: Vec<&str> = vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"]; if has_fediverse { docs_to_copy.push("docs/fediverse-consumer-guide.md"); } let dst_dir = project_dir.join("docs"); fs::create_dir_all(&dst_dir)?; println!( "\n{}", style("── Resync des docs framework ──").cyan().bold() ); for doc in &docs_to_copy { let src = rustwarden_root.join(doc); let Some(filename) = Path::new(doc).file_name() else { continue; }; let dst = dst_dir.join(filename); if !src.exists() { println!( " {} {} (source manquante, ignoré)", style("!").yellow(), style(format!("docs/{}", filename.to_string_lossy())).dim() ); continue; } // Si le fichier existe et a le même contenu, ne rien faire ; // sinon overwrite. Pas de notion de "custom" : ces docs sont // framework-owned, jamais éditées par le consommateur. let need_write = match (fs::read(&src), fs::read(&dst)) { (Ok(s), Ok(d)) => s != d, (Ok(_), Err(_)) => true, // dst missing _ => false, }; if need_write { fs::copy(&src, &dst)?; println!( " {} docs/{}", style("✓").green().bold(), style(filename.to_string_lossy()).cyan() ); } else { println!( " {} docs/{} (déjà à jour)", style("=").dim(), style(filename.to_string_lossy()).dim() ); } } Ok(()) } /// Synchronise un crate du framework fichier par fichier. /// - Fichiers identiques : ignores /// - Fichiers modifies dans le framework : mis a jour /// - Fichiers nouveaux dans le framework : ajoutes /// - Fichiers supprimes du framework : supprimes (sauf custom) /// - Templates personnalises : preserves fn sync_crate( rustwarden_root: &Path, project_dir: &Path, crate_name: &str, custom_templates: &[String], stats: &mut SyncStats, ) -> Result<(), Box<dyn std::error::Error>> { let src = rustwarden_root.join("crates").join(crate_name); let dst = project_dir.join("crates").join(crate_name); if !src.exists() { return Err(format!("Crate source introuvable : {}", src.display()).into()); } let prefix = format!("crates/{crate_name}"); // 1. Copier/mettre a jour depuis la source sync_dir_recursive(&src, &dst, &prefix, crate_name, custom_templates, stats)?; // 2. Supprimer les fichiers du projet qui n'existent plus dans le framework cleanup_removed_files(&src, &dst, &prefix, crate_name, custom_templates, stats)?; println!(" {} crates/{crate_name}/", style("✓").green().bold()); Ok(()) } /// Synchronise recursivement un dossier source vers un dossier destination. fn sync_dir_recursive( src: &Path, dst: &Path, prefix: &str, crate_name: &str, custom_templates: &[String], stats: &mut SyncStats, ) -> Result<(), Box<dyn std::error::Error>> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let file_type = entry.file_type()?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); let name = entry.file_name().to_string_lossy().to_string(); // Exclure target/, .git/, data/ if name == "target" || name == ".git" || name == "data" { continue; } let relative = format!("{prefix}/{name}"); if file_type.is_dir() { sync_dir_recursive( &src_path, &dst_path, &relative, crate_name, custom_templates, stats, )?; } else { // Verifier si c'est un template personnalise a preserver if is_custom_template(&dst_path, crate_name, custom_templates) { stats.skipped_custom.push(relative.clone()); continue; } if dst_path.exists() { // Comparer le contenu let src_content = fs::read(&src_path)?; let dst_content = fs::read(&dst_path)?; if src_content == dst_content { stats.unchanged += 1; } else { fs::copy(&src_path, &dst_path)?; stats.updated.push(relative); } } else { // Nouveau fichier if let Some(parent) = dst_path.parent() { fs::create_dir_all(parent)?; } fs::copy(&src_path, &dst_path)?; stats.added.push(relative); } } } Ok(()) } /// Supprime les fichiers du projet qui n'existent plus dans le framework. fn cleanup_removed_files( src: &Path, dst: &Path, prefix: &str, crate_name: &str, custom_templates: &[String], stats: &mut SyncStats, ) -> Result<(), Box<dyn std::error::Error>> { if !dst.exists() { return Ok(()); } for entry in fs::read_dir(dst)? { let entry = entry?; let file_type = entry.file_type()?; let src_path = src.join(entry.file_name()); let dst_path = entry.path(); let name = entry.file_name().to_string_lossy().to_string(); if name == "target" || name == ".git" || name == "data" { continue; } let relative = format!("{prefix}/{name}"); if file_type.is_dir() { if src_path.exists() { // Le dossier existe dans le framework, verifier son contenu cleanup_removed_files( &src_path, &dst_path, &relative, crate_name, custom_templates, stats, )?; } // On ne supprime pas les dossiers entiers qui n'existent pas // dans le framework — ils peuvent contenir des fichiers custom } else if !src_path.exists() { // Le fichier n'existe plus dans le framework if is_custom_template(&dst_path, crate_name, custom_templates) { // Template custom, on le garde continue; } fs::remove_file(&dst_path)?; stats.removed.push(relative); } } Ok(()) } /// Verifie si un fichier du projet est un template personnalise. fn is_custom_template(file_path: &Path, crate_name: &str, custom_templates: &[String]) -> bool { if crate_name != "rustwarden-ui" || custom_templates.is_empty() { return false; } // Extraire le chemin relatif depuis templates/ let path_str = file_path.to_string_lossy(); let Some(idx) = path_str.find("templates/") else { return false; }; let relative = &path_str[idx + "templates/".len()..]; custom_templates.iter().any(|t| t == relative) } // --------------------------------------------------------------------------- // Decouverte du workspace Rustwarden // --------------------------------------------------------------------------- /// Cherche la racine du workspace Rustwarden (le framework source). fn find_rustwarden_root() -> Result<PathBuf, Box<dyn std::error::Error>> { let start_dirs = [ std::env::current_exe() .ok() .and_then(|p| p.parent().map(|p| p.to_path_buf())), std::env::current_dir().ok(), ]; for start in start_dirs.into_iter().flatten() { let mut dir = start; loop { let cargo_toml = dir.join("Cargo.toml"); if cargo_toml.exists() { let content = fs::read_to_string(&cargo_toml)?; if content.contains("[workspace]") && dir.join("crates/rustwarden-core").exists() && dir.join("tools/rustwarden-cli").exists() { return Ok(dir); } } if !dir.pop() { break; } } } Err("Impossible de trouver la racine du framework Rustwarden.\n\ Lancez cette commande depuis le repertoire Rustwarden \ ou son sous-dossier." .into()) } // --------------------------------------------------------------------------- // Copie recursive // --------------------------------------------------------------------------- /// Copie un dossier recursivement en excluant target/, .git/, data/. fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let file_type = entry.file_type()?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); let name = entry.file_name().to_string_lossy().to_string(); if name == "target" || name == ".git" || name == "data" { continue; } if file_type.is_dir() { copy_dir_recursive(&src_path, &dst_path)?; } else { fs::copy(&src_path, &dst_path)?; } } Ok(()) }
GitRust