diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 22:11:09 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 22:11:09 +0900 |
| commit | 6d036ca5b0e6cb21e5b12a02d544a27e33a97a14 (patch) | |
| tree | 7bb13b6db63a6c1ef75b1bb90345b30a52758ca8 /crates/mozart/src | |
| parent | 38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc (diff) | |
| download | php-mozart-6d036ca5b0e6cb21e5b12a02d544a27e33a97a14.tar.gz php-mozart-6d036ca5b0e6cb21e5b12a02d544a27e33a97a14.tar.zst php-mozart-6d036ca5b0e6cb21e5b12a02d544a27e33a97a14.zip | |
fix(update): unlock replacer when --with-deps walks a replaced require
`expand_with_(direct|all)_dependencies` only looked up dependencies by
their literal name in the lock. When a transitive require pointed at a
virtual / replaced name (e.g. `replaced/pkg1`) and the lock owned it
through another package's `replace` map (e.g. `dep/pkg1` replaces
`replaced/pkg1`), the replacer never entered the unlock set. The
partial-update resolver then left it pinned at its lock version and
silently kept the user on the old release. Mirror Composer's replace
branch in `PoolBuilder::loadPackage`: build a `replaced → replacers`
index over the lock and route every dep walked during expansion
through it before recursing.
Diffstat (limited to 'crates/mozart/src')
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 60 |
1 files changed, 54 insertions, 6 deletions
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index bb99e26..185d3be 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -587,6 +587,7 @@ pub fn expand_with_direct_dependencies( repo_requires: &IndexMap<String, IndexSet<String>>, ) -> Vec<String> { let lock_map = build_lock_map(lock); + let replace_map = build_lock_replace_map(lock); let mut result_set: IndexSet<String> = packages.iter().cloned().collect(); let mut queue: Vec<String> = packages.clone(); let mut result: Vec<String> = packages; @@ -603,9 +604,11 @@ pub fn expand_with_direct_dependencies( if root_requires.contains(&dep_name) { continue; } - if result_set.insert(dep_name.clone()) { - result.push(dep_name.clone()); - queue.push(dep_name); + for actual in resolve_dep_via_replace(&dep_name, &lock_map, &replace_map) { + if result_set.insert(actual.clone()) { + result.push(actual.clone()); + queue.push(actual); + } } } } @@ -622,6 +625,7 @@ pub fn expand_with_all_dependencies( repo_requires: &IndexMap<String, IndexSet<String>>, ) -> Vec<String> { let lock_map = build_lock_map(lock); + let replace_map = build_lock_replace_map(lock); let mut result_set: IndexSet<String> = packages.iter().cloned().collect(); let mut queue: Vec<String> = packages.clone(); let mut result: Vec<String> = packages; @@ -634,9 +638,11 @@ pub fn expand_with_all_dependencies( if is_platform_dep(&dep_name) { continue; } - if result_set.insert(dep_name.clone()) { - result.push(dep_name.clone()); - queue.push(dep_name); + for actual in resolve_dep_via_replace(&dep_name, &lock_map, &replace_map) { + if result_set.insert(actual.clone()) { + result.push(actual.clone()); + queue.push(actual); + } } } } @@ -644,6 +650,48 @@ pub fn expand_with_all_dependencies( result } +/// Build a `replaced_name → list of replacing package names` index over the +/// lock, so a dependency on a virtual / replaced name reaches the actual +/// locked package that owns it. Mirrors the replace branch of Composer's +/// `PoolBuilder::loadPackage`: a partial update with `--with-dependencies` +/// must unlock the replacer when a transitive require points at the +/// replaced name, otherwise the resolver leaves the replacer pinned at +/// its lock version and silently fails to upgrade. +fn build_lock_replace_map(lock: &lockfile::LockFile) -> IndexMap<String, Vec<String>> { + let mut map: IndexMap<String, Vec<String>> = IndexMap::new(); + for pkg in lock + .packages + .iter() + .chain(lock.packages_dev.iter().flatten()) + { + for replaced in pkg.replace.keys() { + map.entry(replaced.to_lowercase()) + .or_default() + .push(pkg.name.to_lowercase()); + } + } + map +} + +/// Translate a dependency name into the list of locked package names that +/// effectively own it: either the package directly named (the common case) +/// or, when the name is virtual / replaced, every locked package whose +/// `replace` map covers it. The result is what should enter the unlock set +/// during `--with-(all-)dependencies` expansion. +fn resolve_dep_via_replace( + dep_name: &str, + lock_map: &IndexMap<String, &lockfile::LockedPackage>, + replace_map: &IndexMap<String, Vec<String>>, +) -> Vec<String> { + if lock_map.contains_key(dep_name) { + vec![dep_name.to_string()] + } else if let Some(replacers) = replace_map.get(dep_name) { + replacers.clone() + } else { + vec![dep_name.to_string()] + } +} + /// Expand the package list applying wildcard matching and optional dependency expansion. /// /// Returns the final list of package names to update (concrete, lowercase, deduplicated). |
