aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands/outdated.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart/src/commands/outdated.rs')
-rw-r--r--crates/mozart/src/commands/outdated.rs111
1 files changed, 47 insertions, 64 deletions
diff --git a/crates/mozart/src/commands/outdated.rs b/crates/mozart/src/commands/outdated.rs
index 5c7b691..9da8e0e 100644
--- a/crates/mozart/src/commands/outdated.rs
+++ b/crates/mozart/src/commands/outdated.rs
@@ -1,6 +1,7 @@
use clap::Args;
+use mozart_core::matches_wildcard;
use std::cmp::Ordering;
-use std::collections::{BTreeMap, HashSet};
+use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -142,33 +143,15 @@ pub async fn execute(
HashSet::new()
};
- // Build constraint map from root composer.json
- let root_constraints: BTreeMap<String, String> = if let Some(ref root) = root_package {
- let mut map: BTreeMap<String, String> = root
- .require
- .iter()
- .map(|(k, v)| (k.to_lowercase(), v.clone()))
- .collect();
- if !args.no_dev {
- map.extend(
- root.require_dev
- .iter()
- .map(|(k, v)| (k.to_lowercase(), v.clone())),
- );
- }
- map
- } else {
- BTreeMap::new()
- };
-
- // Build ignore set
- let ignore_set: HashSet<String> = args.ignore.iter().map(|n| n.to_lowercase()).collect();
-
// Process each package
let mut entries: Vec<OutdatedEntry> = Vec::new();
for pkg in &packages {
// Skip ignored packages
- if ignore_set.contains(&pkg.name.to_lowercase()) {
+ if args
+ .ignore
+ .iter()
+ .any(|pattern| matches_wildcard(&pkg.name, pattern))
+ {
continue;
}
@@ -178,10 +161,14 @@ pub async fn execute(
}
// --package filter
- if let Some(ref filter) = args.package
- && !pkg.name.eq_ignore_ascii_case(filter)
- {
- continue;
+ if let Some(ref filter) = args.package {
+ if filter.contains('*') {
+ if !matches_wildcard(&pkg.name, filter) {
+ continue;
+ }
+ } else if !pkg.name.eq_ignore_ascii_case(filter) {
+ continue;
+ }
}
// Fetch latest version from Packagist
@@ -194,12 +181,7 @@ pub async fn execute(
};
// Classify the update
- let root_constraint = root_constraints.get(&pkg.name.to_lowercase()).cloned();
- let category = classify_update(
- &pkg.version_normalized,
- &latest.version_normalized,
- root_constraint.as_deref(),
- );
+ let category = classify_update(&pkg.version_normalized, &latest.version_normalized);
// If showing all, include up-to-date; otherwise only show outdated
if !args.all && category == UpdateCategory::UpToDate {
@@ -242,7 +224,9 @@ pub async fn execute(
.iter()
.any(|e| e.category != UpdateCategory::UpToDate);
if has_outdated {
- std::process::exit(1);
+ return Err(mozart_core::exit_code::bail_silent(
+ mozart_core::exit_code::GENERAL_ERROR,
+ ));
}
}
@@ -359,15 +343,13 @@ async fn fetch_latest_version(name: &str) -> anyhow::Result<PackageInfo> {
/// Determine the update category for a package.
///
+/// Mirrors Composer's logic: constructs `^<installed_version>` and checks if the
+/// latest version satisfies it.
+///
/// - If latest <= current → UpToDate
-/// - If root constraint exists and latest matches it → SemverCompatible (red)
-/// - If root constraint exists but latest doesn't match → SemverIncompatible (yellow)
-/// - Fallback (no constraint): same major = compatible, different major = incompatible
-fn classify_update(
- current_normalized: &str,
- latest_normalized: &str,
- root_constraint: Option<&str>,
-) -> UpdateCategory {
+/// - If latest satisfies `^<current>` → SemverCompatible (semver-safe update)
+/// - Otherwise → SemverIncompatible (update-possible, may require constraint change)
+fn classify_update(current_normalized: &str, latest_normalized: &str) -> UpdateCategory {
use mozart_registry::version::compare_normalized_versions;
// If latest is not newer than current, it's up-to-date
@@ -375,9 +357,10 @@ fn classify_update(
return UpdateCategory::UpToDate;
}
- // We have an update available — classify it
- if let Some(constraint_str) = root_constraint
- && let Ok(constraint) = mozart_semver::VersionConstraint::parse(constraint_str)
+ // Build ^<current_normalized> constraint and check if latest satisfies it.
+ // This mirrors Composer's approach of checking semver safety.
+ let caret_constraint = format!("^{current_normalized}");
+ if let Ok(constraint) = mozart_semver::VersionConstraint::parse(&caret_constraint)
&& let Ok(latest_ver) = mozart_semver::Version::parse(latest_normalized)
{
if constraint.matches(&latest_ver) {
@@ -387,7 +370,7 @@ fn classify_update(
}
}
- // Fallback: no constraint or parse failed — compare major versions
+ // Fallback: parse failed — compare major versions
let current_major = extract_major(current_normalized);
let latest_major = extract_major(latest_normalized);
if current_major == latest_major {
@@ -590,48 +573,48 @@ mod tests {
#[test]
fn test_classify_up_to_date_equal() {
- let cat = classify_update("1.2.3.0", "1.2.3.0", None);
+ let cat = classify_update("1.2.3.0", "1.2.3.0");
assert_eq!(cat, UpdateCategory::UpToDate);
}
#[test]
fn test_classify_up_to_date_latest_older() {
- let cat = classify_update("2.0.0.0", "1.5.0.0", None);
+ let cat = classify_update("2.0.0.0", "1.5.0.0");
assert_eq!(cat, UpdateCategory::UpToDate);
}
#[test]
- fn test_classify_semver_compatible_with_constraint() {
- // Current 1.2.0, latest 1.3.0, constraint ^1.0 — latest matches constraint
- let cat = classify_update("1.2.0.0", "1.3.0.0", Some("^1.0"));
+ fn test_classify_semver_compatible_minor_update() {
+ // Current 1.2.0, latest 1.3.0 — ^1.2.0 allows 1.3.0
+ let cat = classify_update("1.2.0.0", "1.3.0.0");
assert_eq!(cat, UpdateCategory::SemverCompatible);
}
#[test]
- fn test_classify_semver_incompatible_with_constraint() {
- // Current 1.2.0, latest 2.0.0, constraint ^1.0 — latest doesn't match
- let cat = classify_update("1.2.0.0", "2.0.0.0", Some("^1.0"));
+ fn test_classify_semver_incompatible_major_bump() {
+ // Current 1.2.0, latest 2.0.0 — ^1.2.0 does not allow 2.0.0
+ let cat = classify_update("1.2.0.0", "2.0.0.0");
assert_eq!(cat, UpdateCategory::SemverIncompatible);
}
#[test]
- fn test_classify_no_constraint_same_major() {
- // No constraint, same major → SemverCompatible
- let cat = classify_update("1.2.0.0", "1.5.0.0", None);
+ fn test_classify_semver_compatible_same_major() {
+ // Same major, minor bump → semver-safe under ^
+ let cat = classify_update("1.2.0.0", "1.5.0.0");
assert_eq!(cat, UpdateCategory::SemverCompatible);
}
#[test]
- fn test_classify_no_constraint_different_major() {
- // No constraint, different major → SemverIncompatible
- let cat = classify_update("1.9.0.0", "2.0.0.0", None);
+ fn test_classify_semver_incompatible_different_major() {
+ // Different major → not semver-safe under ^
+ let cat = classify_update("1.9.0.0", "2.0.0.0");
assert_eq!(cat, UpdateCategory::SemverIncompatible);
}
#[test]
- fn test_classify_no_constraint_patch_update() {
- // No constraint, same major.minor, patch bump → SemverCompatible
- let cat = classify_update("1.2.3.0", "1.2.4.0", None);
+ fn test_classify_semver_compatible_patch_update() {
+ // Patch bump → SemverCompatible
+ let cat = classify_update("1.2.3.0", "1.2.4.0");
assert_eq!(cat, UpdateCategory::SemverCompatible);
}