diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 84 | ||||
| -rw-r--r-- | crates/mozart-semver/src/lib.rs | 7 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 79 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 2 |
4 files changed, 143 insertions, 29 deletions
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 98c0fdc..94983e3 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -494,6 +494,85 @@ impl LockFileGenerationRequest { .find(|cpkg| cpkg.name == name && cpkg.version.version_normalized == version_normalized) .map(|cpkg| cpkg.version) } + + /// Reuse `previous_lock` as a metadata source when no repository can + /// answer for `(name, version_normalized)`. Mirrors the slice of + /// Composer's `PoolBuilder` flow that re-loads locked-only packages + /// straight off the lock: a partial update keeping a package at its + /// locked version doesn't need to re-fetch its metadata, and the + /// repositories may no longer carry that version (e.g. an inline + /// `type: package` repo only listing the new release). + fn previous_lock_lookup( + &self, + name: &str, + version_normalized: &str, + ) -> Option<PackagistVersion> { + let prev = self.previous_lock.as_ref()?; + prev.packages + .iter() + .chain(prev.packages_dev.iter().flatten()) + .find(|p| { + p.name.eq_ignore_ascii_case(name) + && p.version_normalized + .as_deref() + .map(|v| v == version_normalized) + .unwrap_or_else(|| { + mozart_semver::Version::parse(&p.version) + .map(|v| v.to_string() == version_normalized) + .unwrap_or(false) + }) + }) + .map(locked_package_to_packagist_version) + } +} + +/// Synthesize a `PackagistVersion` from a `LockedPackage`. Used by +/// `previous_lock_lookup` so the metadata loop has a complete view even +/// when the surrounding repositories have moved on from a locked version. +fn locked_package_to_packagist_version(pkg: &LockedPackage) -> PackagistVersion { + PackagistVersion { + version: pkg.version.clone(), + version_normalized: pkg + .version_normalized + .clone() + .unwrap_or_else(|| pkg.version.clone()), + require: pkg.require.clone(), + replace: pkg.replace.clone(), + provide: pkg.provide.clone(), + conflict: pkg.conflict.clone(), + dist: pkg.dist.as_ref().map(|d| PackagistDist { + dist_type: d.dist_type.clone(), + url: d.url.clone(), + reference: d.reference.clone(), + shasum: d.shasum.clone(), + }), + source: pkg.source.as_ref().map(|s| PackagistSource { + source_type: s.source_type.clone(), + url: s.url.clone(), + reference: s.reference.clone(), + }), + require_dev: pkg.require_dev.clone(), + suggest: pkg.suggest.clone(), + package_type: pkg.package_type.clone(), + autoload: pkg.autoload.clone(), + autoload_dev: pkg.autoload_dev.clone(), + license: pkg.license.clone(), + description: pkg.description.clone(), + homepage: pkg.homepage.clone(), + keywords: pkg.keywords.clone(), + authors: pkg.authors.clone(), + support: None, + funding: None, + time: pkg.time.clone(), + extra: pkg.extra_fields.get("extra").cloned(), + notification_url: pkg + .extra_fields + .get("notification-url") + .and_then(|v| v.as_str()) + .map(String::from), + default_branch: false, + abandoned: pkg.extra_fields.get("abandoned").cloned(), + } } /// Convert a `PackagistSource` to a `LockedSource`. @@ -857,6 +936,11 @@ pub async fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow:: continue; } + if let Some(prev) = request.previous_lock_lookup(&pkg.name, &pkg.version_normalized) { + package_metadata.insert(pkg.name.clone(), prev); + continue; + } + let queries = [crate::repository::PackageQuery { name: pkg.name.as_str(), constraint: None, diff --git a/crates/mozart-semver/src/lib.rs b/crates/mozart-semver/src/lib.rs index 5f6b5fe..0ceaf5a 100644 --- a/crates/mozart-semver/src/lib.rs +++ b/crates/mozart-semver/src/lib.rs @@ -846,7 +846,12 @@ fn parse_wildcard(s: &str) -> Result<VersionConstraint, String> { // Strip trailing .* let base = s.trim_end_matches(".*"); - if base.is_empty() { + // `*.*` (and `*.*.*` etc.) collapse to plain `*` after stripping every + // trailing `.*` segment — the major slot is itself a wildcard, so the + // whole constraint is unconstrained. Composer's `parseConstraint` + // reaches the same conclusion via its `xRange` step (any `x` anchor in + // a position after a `*` is dropped). + if base.is_empty() || base == "*" { return Ok(VersionConstraint::Single(Constraint::Any)); } diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 0439cfa..f065c2a 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -471,10 +471,18 @@ fn glob_segment_matches_inner(pattern: &[u8], text: &[u8]) -> bool { pub fn expand_wildcards( specifiers: &[String], lock: &lockfile::LockFile, + root_requires: &IndexSet<String>, console: &mozart_core::console::Console, ) -> Vec<String> { - // Collect all locked package names (prod + dev) - let all_names: Vec<String> = lock + // Collect all locked package names (prod + dev) plus the current root + // require names. Mirrors Composer's + // `PoolBuilder::warnAboutNonMatchingUpdateAllowList`, which accepts a + // pattern as soon as it matches *either* a locked package or a root + // require (so `update new/pkg` works even when `new/pkg` was just + // added to composer.json and isn't in the lock yet). Names appear in + // declaration order; deduplication happens implicitly via `seen` + // below. + let mut all_names: Vec<String> = lock .packages .iter() .map(|p| p.name.to_lowercase()) @@ -485,32 +493,41 @@ pub fn expand_wildcards( .map(|p| p.name.to_lowercase()), ) .collect(); + for name in root_requires { + let lower = name.to_lowercase(); + if !all_names.contains(&lower) { + all_names.push(lower); + } + } let mut result: Vec<String> = Vec::new(); let mut seen: IndexSet<String> = IndexSet::new(); for spec in specifiers { - if spec.contains('*') { - // Expand the wildcard against the lock - let mut matched = false; - for name in &all_names { - if glob_matches(spec, name) && seen.insert(name.clone()) { - result.push(name.clone()); - matched = true; - } - } - if !matched { - console.info(&console::warning(&format!( - "No locked packages matched the pattern '{}'. Pattern will be ignored.", - spec - ))); - } - } else { - let lower = spec.to_lowercase(); - if seen.insert(lower.clone()) { - result.push(lower); + // Mirror Composer's `BasePackage::packageNameToRegexp` + the + // `isUpdateAllowed` walk over locked packages: the pattern is + // matched case-insensitively against each locked name, with `*` + // expanded to `.*` and every other character treated literally. + // Specs that match no locked package are warned about and dropped + // — for a non-wildcard spec like `notexact/Test` that's typo'd + // against `notexact/testpackage`, this prevents Mozart from + // forwarding the bogus name into the resolver (which would then + // fail looking it up). Genuinely new packages are still picked up + // by the resolver via `composer.json` root requires regardless of + // whether they appear in `update_packages`. + let mut matched = false; + for name in &all_names { + if glob_matches(spec, name) && seen.insert(name.clone()) { + result.push(name.clone()); + matched = true; } } + if !matched { + console.info(&console::warning(&format!( + "Package '{}' listed for update is not in the lock file. Specifier will be ignored.", + spec + ))); + } } result @@ -754,7 +771,7 @@ pub fn expand_packages( console: &mozart_core::console::Console, ) -> Vec<String> { let mut packages: Vec<String> = if let Some(lock) = lock { - expand_wildcards(specifiers, lock, console) + expand_wildcards(specifiers, lock, root_requires, console) } else { // No lock file: pass through as-is (no wildcards can be resolved) specifiers.iter().map(|s| s.to_lowercase()).collect() @@ -2240,8 +2257,12 @@ mod tests { #[test] fn test_expand_wildcards_no_wildcard_passthrough() { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); + let root_requires: IndexSet<String> = ["psr/log", "nonexistent/pkg"] + .into_iter() + .map(String::from) + .collect(); let specs = vec!["psr/log".to_string(), "nonexistent/pkg".to_string()]; - let result = expand_wildcards(&specs, &lock, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); assert_eq!(result, vec!["psr/log", "nonexistent/pkg"]); } @@ -2253,7 +2274,8 @@ mod tests { make_locked_package("monolog/monolog", "3.8.0"), ]); let specs = vec!["symfony/*".to_string()]; - let mut result = expand_wildcards(&specs, &lock, &test_console()); + let root_requires: IndexSet<String> = IndexSet::new(); + let mut result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); result.sort(); assert_eq!(result, vec!["symfony/console", "symfony/http-kernel"]); } @@ -2262,8 +2284,9 @@ mod tests { fn test_expand_wildcards_no_match_emits_warning() { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); let specs = vec!["unknown/*".to_string()]; + let root_requires: IndexSet<String> = IndexSet::new(); // Should return empty (no match), no panic - let result = expand_wildcards(&specs, &lock, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); assert!(result.is_empty()); } @@ -2271,7 +2294,8 @@ mod tests { fn test_expand_wildcards_deduplication() { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); let specs = vec!["psr/log".to_string(), "psr/log".to_string()]; - let result = expand_wildcards(&specs, &lock, &test_console()); + let root_requires: IndexSet<String> = IndexSet::new(); + let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); assert_eq!(result.len(), 1); assert_eq!(result[0], "psr/log"); } @@ -2281,7 +2305,8 @@ mod tests { let mut lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); let specs = vec!["phpunit/*".to_string()]; - let result = expand_wildcards(&specs, &lock, &test_console()); + let root_requires: IndexSet<String> = IndexSet::new(); + let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); assert_eq!(result, vec!["phpunit/phpunit"]); } diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 198dd9f..197b00f 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -356,7 +356,7 @@ installer_fixture!(update_all_dry_run); installer_fixture!(update_allow_list); installer_fixture!(update_allow_list_locked_require); installer_fixture!(update_allow_list_minimal_changes); -installer_fixture!(update_allow_list_patterns, ignore); +installer_fixture!(update_allow_list_patterns); installer_fixture!(update_allow_list_patterns_with_all_dependencies); installer_fixture!(update_allow_list_patterns_with_dependencies); installer_fixture!(update_allow_list_patterns_with_root_dependencies); |
