From f664a25070b38c5b73995874e0ee15dad23bc5ef Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 20:06:49 +0900 Subject: fix(update): union lock and repo requires when expanding --with-deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously requires_for_name returned the lock entry's requires when the package was already locked, falling back to repo requires only when not. That missed the case where the resolver would pick a *newer* version of the locked package that added a new requirement on another locked package — the new dependency stayed pinned and the upgrade was silently suppressed. Union both sources so every candidate version's requires contribute to the unlock cascade. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart/src/commands/update.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) (limited to 'crates/mozart/src/commands') diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 5e83104..5cf05c4 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -506,20 +506,42 @@ fn is_platform_dep(dep_name: &str) -> bool { || dep_name == "php-debug" } -/// Look up the require-list for `name`: prefer the lock entry (the version -/// that will stay if not unlocked) and fall back to the union of repo -/// requires for not-yet-locked packages. Lowercase names returned. +/// Look up the require-list for `name`, unioning the lock entry's +/// requires with every available version's requires from inline / +/// composer-repo entries. Lowercase names returned, deduped. +/// +/// Composer's `PoolBuilder::loadPackage` dynamically unlocks any locked +/// dependency referenced by an allow-listed package's *new* (about-to-be- +/// resolved) version. Mozart pre-computes the unlock set, so it has to +/// consider not only the lock-pinned version's requires but also every +/// candidate version the resolver might pick — otherwise upgrading a +/// locked package whose new version added a requirement on another +/// locked package leaves that other package pinned, and the resolver +/// silently keeps the old version. fn requires_for_name( name: &str, lock_map: &IndexMap, repo_requires: &IndexMap>, ) -> Option> { + let mut deps: IndexSet = IndexSet::new(); + let mut seen = false; if let Some(pkg) = lock_map.get(name) { - return Some(pkg.require.keys().map(|k| k.to_lowercase()).collect()); + seen = true; + for k in pkg.require.keys() { + deps.insert(k.to_lowercase()); + } + } + if let Some(set) = repo_requires.get(name) { + seen = true; + for k in set { + deps.insert(k.clone()); + } + } + if seen { + Some(deps.into_iter().collect()) + } else { + None } - repo_requires - .get(name) - .map(|set| set.iter().cloned().collect()) } /// Expand the allow-list with transitive `require` dependencies, stopping at -- cgit v1.3.1