use clap::Args; use indexmap::IndexMap; use mozart_core::composer::{Composer, InstallationSource, LocalPackage}; use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; use mozart_registry::download_manager::DownloadManager; use mozart_vcs::version_guesser::VersionGuesser; #[derive(Args)] pub struct StatusArgs {} struct VcsVerChange { previous: VerRef, current: VerRef, } struct VerRef { version: String, reference: String, } pub async fn execute( _args: &StatusArgs, cli: &super::Cli, console: &Console, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let composer = Composer::require(&working_dir)?; let installed_repo = composer.repository_manager().local_repository(); let im = composer.installation_manager(); let dm = DownloadManager::new(im.vendor_dir().join(".cache").join("git")); let guesser = VersionGuesser::new(); let mut errors: IndexMap = IndexMap::new(); let mut unpushed_changes: IndexMap = IndexMap::new(); let mut vcs_version_changes: IndexMap = IndexMap::new(); for package in installed_repo.canonical_packages() { let Some(downloader) = dm.for_package(package) else { continue; }; let Some(target_dir) = im.get_install_path(package) else { continue; }; let target_key = target_dir.display().to_string(); // ChangeReportInterface — Composer mirrors the symlink branch and // the local-changes branch unconditionally; the latter overrides // the former when both fire. if std::fs::symlink_metadata(&target_dir) .map(|m| m.file_type().is_symlink()) .unwrap_or(false) { errors.insert( target_key.clone(), format!("{target_key} is a symbolic link."), ); } if let Some(changes) = downloader.local_changes(&target_dir)? { errors.insert(target_key.clone(), changes); } // VcsCapableDownloaderInterface if downloader.vcs_reference(&target_dir)?.is_some() { let previous_ref = match package.installation_source() { Some(InstallationSource::Source) => package.source_reference(), Some(InstallationSource::Dist) => package.dist_reference(), _ => None, }; let pkg_config = build_package_config(package); let current_version = guesser.guess_version(&pkg_config, &target_dir); if let (Some(previous_ref), Some(current_version)) = (previous_ref, current_version) { let cur_commit = current_version.commit.as_deref().unwrap_or(""); let cur_pretty = current_version.pretty_version.as_deref().unwrap_or(""); if cur_commit != previous_ref && cur_pretty != previous_ref { vcs_version_changes.insert( target_key.clone(), VcsVerChange { previous: VerRef { version: package.pretty_version().to_string(), reference: previous_ref.to_string(), }, current: VerRef { version: cur_pretty.to_string(), reference: cur_commit.to_string(), }, }, ); } } } // DvcsDownloaderInterface if let Some(unpushed) = downloader.unpushed_changes(&target_dir)? { unpushed_changes.insert(target_key.clone(), unpushed); } } if errors.is_empty() && unpushed_changes.is_empty() && vcs_version_changes.is_empty() { console_writeln_error!(console, "No local changes"); return Ok(()); } let verbose = cli.verbose > 0; let very_verbose = cli.verbose >= 2; if !errors.is_empty() { console_writeln_error!( console, "You have changes in the following dependencies:" ); for (path, changes) in &errors { if verbose { console_writeln!(console, "{path}:"); console_writeln!(console, "{}", &indent_block(changes)); } else { console_writeln!(console, "{}", path); } } } if !unpushed_changes.is_empty() { console_writeln_error!( console, "You have unpushed changes on the current branch in the following dependencies:" ); for (path, changes) in &unpushed_changes { if verbose { console_writeln!(console, "{path}:"); console_writeln!(console, "{}", &indent_block(changes)); } else { console_writeln!(console, "{}", path); } } } if !vcs_version_changes.is_empty() { console_writeln_error!( console, "You have version variations in the following dependencies:" ); for (path, change) in &vcs_version_changes { if verbose { let mut prev = if change.previous.version.is_empty() { change.previous.reference.clone() } else { change.previous.version.clone() }; let mut curr = if change.current.version.is_empty() { change.current.reference.clone() } else { change.current.version.clone() }; if very_verbose { prev.push_str(&format!(" ({})", change.previous.reference)); curr.push_str(&format!(" ({})", change.current.reference)); } console_writeln!(console, "{path}:"); console_writeln!( console, " From {prev} to {curr}" ); } else { console_writeln!(console, "{}", path); } } } if !verbose { console_writeln_error!(console, "Use --verbose (-v) to see a list of files"); } let code = (if !errors.is_empty() { 1 } else { 0 }) | (if !unpushed_changes.is_empty() { 2 } else { 0 }) | (if !vcs_version_changes.is_empty() { 4 } else { 0 }); if code != 0 { return Err(exit_code::bail_silent(code)); } Ok(()) } fn indent_block(s: &str) -> String { s.split('\n') .map(|line| format!(" {}", line.trim_start())) .collect::>() .join("\n") } /// Build the `package_config` shape that `VersionGuesser` reads. The PHP /// equivalent is `ArrayDumper::dump($package)`; we only need the fields /// that `VersionGuesser` actually inspects. fn build_package_config(package: &LocalPackage) -> serde_json::Value { serde_json::json!({ "extra": package.extra(), }) }