aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 22:11:09 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 22:11:09 +0900
commit6d036ca5b0e6cb21e5b12a02d544a27e33a97a14 (patch)
tree7bb13b6db63a6c1ef75b1bb90345b30a52758ca8 /crates/mozart/src
parent38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc (diff)
downloadphp-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.rs60
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).