diff options
Diffstat (limited to 'crates/mozart/src')
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 156 |
1 files changed, 144 insertions, 12 deletions
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index eb8d501..7b19453 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -137,18 +137,11 @@ pub async fn execute( let mut raw = package::read_from_file(&composer_path)?; - // Step 4: Handle --unused flag (deferred implementation) - if args.unused { - console.info(&format!( - "{}", - console::warning( - "--unused is not yet fully implemented. The resolver will naturally prune unreachable packages." - ) - )); - // Fall through: if no explicit packages were named, nothing to remove. - if args.packages.is_empty() { - return Ok(()); - } + // Step 4: Handle --unused flag + // When --unused is set with no explicit packages, we re-resolve to detect + // packages in the lock file that are no longer reachable from root requirements. + if args.unused && args.packages.is_empty() { + return remove_unused(&raw, &working_dir, args, console).await; } // Step 5: Determine which packages to remove and remove them @@ -477,6 +470,145 @@ pub async fn execute( Ok(()) } +/// Remove unused packages by re-resolving and comparing with the current lock file. +async fn remove_unused( + raw: &package::RawPackageData, + working_dir: &std::path::Path, + args: &RemoveArgs, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { + let lock_path = working_dir.join("composer.lock"); + + if !lock_path.exists() { + console.info("No lock file found. Nothing to prune."); + return Ok(()); + } + + let old_lock = lockfile::LockFile::read_from_file(&lock_path)?; + + let dev_mode = !args.update_no_dev; + + let require: Vec<(String, String)> = raw + .require + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let require_dev: Vec<(String, String)> = raw + .require_dev + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + let minimum_stability_str = raw.minimum_stability.as_deref().unwrap_or("stable"); + let minimum_stability = package::Stability::parse(minimum_stability_str); + let composer_prefer_stable = raw + .extra_fields + .get("prefer-stable") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let request = ResolveRequest { + root_name: raw.name.clone(), + require, + require_dev, + include_dev: dev_mode, + minimum_stability, + stability_flags: HashMap::new(), + prefer_stable: composer_prefer_stable, + prefer_lowest: false, + platform: PlatformConfig::new(), + ignore_platform_reqs: args.ignore_platform_reqs, + ignore_platform_req_list: args.ignore_platform_req.clone(), + repo_cache: None, + }; + + console.info("Resolving dependencies to detect unused packages..."); + + let resolved = resolver::resolve(&request).await.map_err(|e| { + mozart_core::exit_code::bail( + mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED, + e.to_string(), + ) + })?; + + // Build set of resolved package names + let resolved_names: std::collections::HashSet<String> = resolved + .iter() + .map(|p| p.name.to_lowercase()) + .collect(); + + // Find packages in the old lock that are not in the new resolution + let mut unused: Vec<String> = Vec::new(); + for pkg in &old_lock.packages { + if !resolved_names.contains(&pkg.name.to_lowercase()) { + unused.push(pkg.name.clone()); + } + } + if let Some(ref dev_pkgs) = old_lock.packages_dev { + for pkg in dev_pkgs { + if !resolved_names.contains(&pkg.name.to_lowercase()) { + unused.push(pkg.name.clone()); + } + } + } + + if unused.is_empty() { + console.info("No unused packages found."); + return Ok(()); + } + + for name in &unused { + console.info(&format!(" - Removing unused package: {name}")); + } + console.info(&format!("Found {} unused package(s).", unused.len())); + + if args.dry_run { + console.info(&console::comment( + "Dry run: lock file not modified.", + )); + return Ok(()); + } + + // Re-generate lock file without unused packages + let composer_json_content = std::fs::read_to_string(&working_dir.join("composer.json"))?; + let new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest { + resolved_packages: resolved, + composer_json_content, + composer_json: raw.clone(), + include_dev: dev_mode, + repo_cache: None, + }) + .await?; + + console.info("Writing lock file"); + new_lock.write_to_file(&lock_path)?; + + // Install + if !args.no_install { + let vendor_dir = working_dir.join("vendor"); + super::install::install_from_lock( + &new_lock, + working_dir, + &vendor_dir, + &super::install::InstallConfig { + dev_mode, + dry_run: false, + no_autoloader: false, + no_progress: args.no_progress, + ignore_platform_reqs: args.ignore_platform_reqs, + ignore_platform_req: args.ignore_platform_req.clone(), + optimize_autoloader: args.optimize_autoloader, + classmap_authoritative: args.classmap_authoritative, + apcu_autoloader: false, + apcu_autoloader_prefix: None, + }, + ) + .await?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; |
