use crate::composer::Composer;
use clap::Args;
use mozart_core::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_core::vcs::version_guesser::{VersionGuesser, VersionParser};
#[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 composer = Composer::require(cli.working_dir()?)?;
let installed_repo = composer.repository_manager().local_repository();
let dm = composer.download_manager();
let im = composer.installation_manager();
let mut errors = Vec::new();
let mut unpushed_changes = Vec::new();
let mut vcs_version_changes = Vec::new();
let parser = VersionParser::new();
let guesser = VersionGuesser::new(parser);
let dumper = ArrayDumper::new();
for package in installed_repo.get_canonical_packages() {
let Some(downloader) = dm.get_downloader_for_package(package) else {
continue;
};
let Some(target_dir) = im.get_install_path(package) else {
continue;
};
let target_dir_key = target_dir.display().to_string();
if downloader.is_change_report() {
if std::fs::symlink_metadata(&target_dir)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
{
errors.push((
target_dir_key.clone(),
format!("{target_dir_key} is a symbolic link."),
));
}
if let Some(changes) = downloader.get_local_changes(&target_dir)? {
errors.push((target_dir_key.clone(), changes));
}
}
if downloader.is_vcs_capable_downloader()
&& 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 current_version = guesser.guess_version(&dumper.dump(package), &target_dir);
if let (Some(previous_ref), Some(current_version)) = (previous_ref, current_version) {
let current_commit = current_version.commit.as_deref().unwrap_or("");
let current_pretty_version =
current_version.pretty_version.as_deref().unwrap_or("");
if current_commit != previous_ref && current_pretty_version != previous_ref {
vcs_version_changes.push((
target_dir_key.clone(),
VcsVerChange {
previous: VerRef {
version: package.pretty_version().to_string(),
reference: previous_ref.to_string(),
},
current: VerRef {
version: current_pretty_version.to_string(),
reference: current_commit.to_string(),
},
},
));
}
}
}
if downloader.is_dvcs_downloader()
&& let Some(unpushed) = downloader.unpushed_changes(&target_dir)?
{
unpushed_changes.push((target_dir_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(());
}
if !errors.is_empty() {
console_writeln_error!(
console,
"You have changes in the following dependencies:"
);
for (path, changes) in &errors {
if cli.is_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 cli.is_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 cli.is_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 console.is_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 !cli.is_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")
}
/// Mirrors `Composer\Package\Dumper\ArrayDumper`. Serialises a `LocalPackage`
/// into the JSON shape that `VersionGuesser::guess_version` expects.
struct ArrayDumper;
impl ArrayDumper {
fn new() -> Self {
Self
}
fn dump(&self, package: &LocalPackage) -> serde_json::Value {
build_package_config(package)
}
}
/// Serialises a `LocalPackage` to the JSON shape consumed by
/// `VersionGuesser::guess_version`. Mirrors `ArrayDumper::dump($package)` —
/// we include all fields that `VersionGuesser` inspects.
fn build_package_config(package: &LocalPackage) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("name".into(), package.pretty_name().into());
obj.insert("version".into(), package.pretty_version().into());
if let Some(t) = package.package_type() {
obj.insert("type".into(), t.into());
}
obj.insert("extra".into(), package.extra().clone());
if let Some(src) = package.source() {
let mut s = serde_json::Map::new();
s.insert("type".into(), src.kind.clone().into());
s.insert("url".into(), src.url.clone().into());
if let Some(r) = &src.reference {
s.insert("reference".into(), r.clone().into());
}
obj.insert("source".into(), serde_json::Value::Object(s));
}
if let Some(dist) = package.dist() {
let mut d = serde_json::Map::new();
d.insert("type".into(), dist.kind.clone().into());
d.insert("url".into(), dist.url.clone().into());
if let Some(r) = &dist.reference {
d.insert("reference".into(), r.clone().into());
}
obj.insert("dist".into(), serde_json::Value::Object(d));
}
if let Some(is) = package.installation_source() {
let s = match is {
InstallationSource::Source => "source",
InstallationSource::Dist => "dist",
};
obj.insert("installation-source".into(), s.into());
}
serde_json::Value::Object(obj)
}