diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-04 00:49:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-04 00:49:40 +0900 |
| commit | 74c188162886755c380a4696d8b684cb28687402 (patch) | |
| tree | 4a33b20c6ceef51d3720f169918c077c1cb06376 /crates | |
| parent | 6449a15de90fe8252fb288bd5eacb99dc2cd699a (diff) | |
| download | php-mozart-74c188162886755c380a4696d8b684cb28687402.tar.gz php-mozart-74c188162886755c380a4696d8b684cb28687402.tar.zst php-mozart-74c188162886755c380a4696d8b684cb28687402.zip | |
fix(update): preserve locked refs and aliases on partial update
Partial update of a non-allow-listed dev package now resolves and emits
the locked-repo entry verbatim, mirroring Composer's `PoolBuilder`.
Three coordinated changes:
- resolver: `lock_filter_allows` accepts the locked package's branch-
alias normalized versions, not just the base. Without this, root
constraints like `~2.1` against a `dev-master` locked package whose
branch alias is `2.1.x-dev` failed with "no matching package found".
- lockfile: new `lock_pinned_names` field on `LockFileGenerationRequest`
routes non-allow-listed packages through `previous_lock_lookup`
before `inline_lookup`, so the lock's source/dist references survive
even when the inline metadata has moved to a newer commit.
- update: `apply_partial_update` skips alias entries — re-pinning their
pretty `version` to the base would collapse the alias label and
emit a self-referential entry in the new lock's `aliases[]` block.
Unblocks partial_update_forces_dev_reference_from_lock_for_non_updated_packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 27 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 30 | ||||
| -rw-r--r-- | crates/mozart/src/commands/create_project.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 35 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 5 |
7 files changed, 96 insertions, 9 deletions
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 94983e3..5a293fe 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -467,6 +467,16 @@ pub struct LockFileGenerationRequest { /// during partial updates: lock entries are stable across updates that /// don't touch the package, even if the upstream metadata has drifted. pub previous_lock: Option<LockFile>, + /// Lowercase package names that were held back to their locked version + /// on a partial update — i.e. they were NOT in the CLI's allow list and + /// were re-pinned by `apply_partial_update`. For these names the lock + /// entry's metadata (source/dist references in particular) is canonical: + /// inline / composer-repo metadata may have drifted to a newer commit + /// that the partial update is explicitly choosing not to take. Mirrors + /// Composer's `PoolBuilder`, which keeps non-allow-listed packages at + /// the locked-repo entry rather than re-loading them from the inline / + /// VCS sources. + pub lock_pinned_names: indexmap::IndexSet<String>, } impl LockFileGenerationRequest { @@ -926,6 +936,21 @@ pub async fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow:: let mut package_metadata: IndexMap<String, PackagistVersion> = IndexMap::new(); let repo_set = &request.repositories; for pkg in &real_resolved { + // For packages held back to the locked version on a partial update, + // the lock entry is the canonical metadata source. Inline / composer- + // repo / VCS sources may have moved to a newer commit that this + // partial update is explicitly choosing NOT to take, so consulting + // them first would silently bump the source/dist reference. Mirrors + // Composer's `PoolBuilder` behaviour: non-allow-listed packages keep + // the locked-repo entry rather than re-loading from upstream. + let pinned = request.lock_pinned_names.contains(&pkg.name.to_lowercase()); + if pinned + && let Some(prev) = request.previous_lock_lookup(&pkg.name, &pkg.version_normalized) + { + package_metadata.insert(pkg.name.clone(), prev); + continue; + } + if let Some(inline) = request.inline_lookup(&pkg.name, &pkg.version_normalized) { package_metadata.insert(pkg.name.clone(), inline); continue; @@ -1640,6 +1665,7 @@ mod tests { crate::cache::Cache::new(std::env::temp_dir().join("mozart-test-cache"), false), )), previous_lock: None, + lock_pinned_names: IndexSet::new(), }; let lock = generate_lock_file(&request).await.unwrap(); @@ -1787,6 +1813,7 @@ mod tests { false, ))), previous_lock: None, + lock_pinned_names: IndexSet::new(), }; let lock = generate_lock_file(&gen_request) diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index c592e01..35856c3 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -800,6 +800,13 @@ pub struct LockedPackageInfo { pub replaces: Vec<(String, String)>, pub provides: Vec<(String, String)>, pub conflicts: Vec<(String, String)>, + /// Branch-alias entries to surface alongside the base locked package, as + /// `(pretty, normalized)` pairs. Mirrors what + /// `Composer\Package\Locker::getLockedRepository` constructs from + /// `extra.branch-alias`: a `dev-master` locked package with branch alias + /// `2.1.x-dev` needs to expose itself under both versions so root + /// constraints like `~2.1` still resolve on a partial update. + pub branch_aliases: Vec<(String, String)>, } /// A single package in the resolution output. @@ -1098,15 +1105,28 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R } // Build a map first so the filter below knows which (name, version) - // pairs are the only allowed entries for locked names. - let locked_name_to_version: IndexMap<String, String> = request + // pairs are the only allowed entries for locked names. Each entry holds + // the locked normalized version plus any branch-alias normalized + // versions Composer's `Locker::getLockedRepository` would expose + // alongside the base. Without the alias entries, an inline-package or + // VCS source providing the same `dev-master` + alias as the lock would + // have its alias filtered out, leaving root constraints like `~2.1` — + // which can only match the alias version, not the raw `dev-master` — + // unsatisfiable on a partial update. + let locked_name_to_versions: IndexMap<String, Vec<String>> = request .locked_packages .iter() - .map(|p| (p.name.to_lowercase(), p.version_normalized.clone())) + .map(|p| { + let mut versions = vec![p.version_normalized.clone()]; + for (_, alias_normalized) in &p.branch_aliases { + versions.push(alias_normalized.clone()); + } + (p.name.to_lowercase(), versions) + }) .collect(); let lock_filter_allows = |name: &str, version: &str| -> bool { - match locked_name_to_version.get(&name.to_lowercase()) { - Some(locked_version) => locked_version == version, + match locked_name_to_versions.get(&name.to_lowercase()) { + Some(locked_versions) => locked_versions.iter().any(|v| v == version), None => true, } }; diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 61cb886..6674ccc 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -468,6 +468,7 @@ pub async fn execute( mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()), ), previous_lock: None, + lock_pinned_names: indexmap::IndexSet::new(), }) .await?; diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index d4b3aef..6498e01 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -380,6 +380,7 @@ pub async fn execute( mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()), ), previous_lock: old_lock.clone(), + lock_pinned_names: indexmap::IndexSet::new(), }) .await?; @@ -627,6 +628,7 @@ async fn remove_unused( mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()), ), previous_lock: Some(old_lock.clone()), + lock_pinned_names: indexmap::IndexSet::new(), }) .await?; @@ -946,6 +948,7 @@ mod tests { ), ), previous_lock: None, + lock_pinned_names: IndexSet::new(), }) .await .expect("initial lock file generation should succeed"); @@ -1010,6 +1013,7 @@ mod tests { ), ), previous_lock: Some(initial_lock.clone()), + lock_pinned_names: IndexSet::new(), }) .await .expect("post-remove lock file generation should succeed"); diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index b302ed9..02e5d8e 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -769,6 +769,7 @@ pub async fn execute( mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()), ), previous_lock: old_lock.clone(), + lock_pinned_names: indexmap::IndexSet::new(), }) .await?; @@ -1103,6 +1104,7 @@ mod tests { ), ), previous_lock: None, + lock_pinned_names: IndexSet::new(), }) .await .expect("Lock file generation should succeed"); @@ -1180,6 +1182,7 @@ mod tests { ), ), previous_lock: None, + lock_pinned_names: IndexSet::new(), }) .await .expect("Lock file generation should succeed"); diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 5210a34..2a0aa88 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -410,6 +410,15 @@ pub fn apply_partial_update( .into_iter() .map(|mut pkg| { let name_lower = pkg.name.to_lowercase(); + // Alias entries already carry their post-swap shape: the resolver + // picked them from the locked-repo branch-alias surface, which is + // exactly where the previous lock would have put them. Re-pinning + // their `version` to the base's locked pretty would collapse the + // alias label into the base, leaving a self-referential entry in + // the new lock's `aliases[]` block. + if pkg.alias_of_normalized.is_some() { + return pkg; + } // If this package is NOT in the update set and we have an old locked version, // swap it back to the old version to prevent unintended changes. // @@ -1230,6 +1239,10 @@ pub async fn run( continue; } names.insert(name_lower.clone()); + let branch_aliases = lockfile::locked_package_branch_aliases(p) + .into_iter() + .map(|a| (a.alias, a.alias_normalized)) + .collect(); infos.push(LockedPackageInfo { name: name_lower, pretty_version: p.version.clone(), @@ -1254,6 +1267,7 @@ pub async fn run( .iter() .map(|(k, v)| (k.to_lowercase(), v.clone())) .collect(), + branch_aliases, }); } (names, infos) @@ -1599,6 +1613,25 @@ pub async fn run( // Step 9: Generate new lock file. `include_dev: true` matches Composer: // `update --no-dev` still writes a complete lock file with packages-dev // populated, so a later `install` (with dev_mode) sees them. + // + // For partial updates, names NOT in the CLI allow list keep their + // locked-repo metadata (source/dist references in particular). Computed + // here from the same `update_packages` list `apply_partial_update` used + // to swap the resolved versions back. Empty for full updates. + let lock_pinned_names: IndexSet<String> = if update_packages.is_empty() { + IndexSet::new() + } else if let Some(lock) = &old_lock { + let update_set: IndexSet<String> = + update_packages.iter().map(|s| s.to_lowercase()).collect(); + lock.packages + .iter() + .chain(lock.packages_dev.iter().flatten()) + .map(|p| p.name.to_lowercase()) + .filter(|n| !update_set.contains(n)) + .collect() + } else { + IndexSet::new() + }; let mut new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest { resolved_packages: resolved, composer_json_content: composer_json_content.clone(), @@ -1606,6 +1639,7 @@ pub async fn run( include_dev: true, repositories: repositories.clone(), previous_lock: old_lock.clone(), + lock_pinned_names, }) .await?; @@ -2504,6 +2538,7 @@ mod tests { ), ), previous_lock: None, + lock_pinned_names: IndexSet::new(), }) .await .expect("Lock file generation should succeed"); diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 177decf..955a1ad 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -311,10 +311,7 @@ installer_fixture!(outdated_lock_file_fails_install); installer_fixture!(outdated_lock_file_with_new_platform_reqs_fails); installer_fixture!(partial_update_always_updates_symlinked_path_repos); installer_fixture!(partial_update_downgrades_non_allow_listed_unstable); -installer_fixture!( - partial_update_forces_dev_reference_from_lock_for_non_updated_packages, - ignore -); +installer_fixture!(partial_update_forces_dev_reference_from_lock_for_non_updated_packages); installer_fixture!(partial_update_from_lock); installer_fixture!(partial_update_from_lock_with_root_alias); installer_fixture!(partial_update_installs_from_lock_even_missing, ignore); |
