aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart-registry/src/lockfile.rs84
-rw-r--r--crates/mozart-semver/src/lib.rs7
-rw-r--r--crates/mozart/src/commands/update.rs79
-rw-r--r--crates/mozart/tests/installer.rs2
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);