aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 23:02:32 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 23:02:32 +0900
commit26af378d81da76c50593674fa86ed4911aa0e46f (patch)
tree913066dc3a65eb5cac92adf60d59d5b0eb5aa5df /crates/mozart-registry
parent2b48ae7bcf857bc35de95968513750c2d6e6de7b (diff)
downloadphp-mozart-26af378d81da76c50593674fa86ed4911aa0e46f.tar.gz
php-mozart-26af378d81da76c50593674fa86ed4911aa0e46f.tar.zst
php-mozart-26af378d81da76c50593674fa86ed4911aa0e46f.zip
fix(update): pattern-match allow-list specifiers and reuse locked metadata
Three related parity gaps surfaced by the `update-allow-list-patterns` fixture: 1. `mozart-semver`'s wildcard parser turned `*.*` into `>=0 <1` (a single-major range) because stripping the trailing `.*` left `*` in the major slot, which `parse()` quietly read as `0`. Composer reduces such patterns to a plain `*` (unconstrained) — match that and short-circuit when the stripped base is `*`. 2. `expand_wildcards` passed any non-wildcard specifier straight through, so a typo like `notexact/Test` (lock has `notexact/testpackage`) entered the resolver as a real package name and failed lookup. Mirror Composer's regex-based `isUpdateAllowed`/`warnAboutNonMatchingUpdateAllowList`: every specifier — wildcard or not — is matched against locked names *and* current root-require names, with `*` expanded to `.*`, and unmatched specs are warned and dropped instead of forwarded. 3. The lockfile generator's metadata loop hit the empty test repo set when a partial update kept a non-allow-listed package at its locked version that the inline repo no longer advertised, and bailed with "Could not find version". Add a `previous_lock` fallback that synthesizes a `PackagistVersion` straight off the `LockedPackage` so the lock entry's own metadata stays authoritative for packages that aren't moving.
Diffstat (limited to 'crates/mozart-registry')
-rw-r--r--crates/mozart-registry/src/lockfile.rs84
1 files changed, 84 insertions, 0 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,