diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:10:38 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:10:38 +0900 |
| commit | dffb6244ebb432477b83631d68584bbc7186dd94 (patch) | |
| tree | 4be972cb21de544c53108a244ba4288f4467d544 /crates/mozart | |
| parent | ed77ff97d6c137ef58f0464b7a9b08bc2b875bd2 (diff) | |
| download | php-mozart-dffb6244ebb432477b83631d68584bbc7186dd94.tar.gz php-mozart-dffb6244ebb432477b83631d68584bbc7186dd94.tar.zst php-mozart-dffb6244ebb432477b83631d68584bbc7186dd94.zip | |
fix(install): treat dev-reference shifts as upgrades
Composer's Transaction fires an UpdateOperation when an installed
package's source/dist reference moved, even if the version string is
unchanged — that is how a `dev-main#abcd` root require pinning a new
commit propagates through `composer install`. Mozart was checking only
(name, version) and short-circuiting to Skip, so the package stayed
pinned to whatever reference installed.json carried.
Compare references in compute_operations and route mismatches into
Action::Update. The trace recorder needs the from-side display string
to include the reference suffix (`dev-master abc123`) so the EXPECT
output matches Composer's UpdateOperation::format; thread that through
PackageOperation::Update as a separate from_full_pretty field while
keeping from_version (sans suffix) for the upgrade-vs-downgrade
direction check, which has to compare normalized versions like
Composer's VersionParser::isUpgrade does.
Unblocks update_reference, update_reference_picks_latest, and
updating_dev_updates_url_and_reference installer fixtures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart')
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 67 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 6 |
2 files changed, 56 insertions, 17 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index e53b6d1..f809a86 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -5,6 +5,7 @@ use mozart_core::console_format; use mozart_registry::installed; use mozart_registry::installer_executor::{ ExecuteContext, FilesystemExecutor, InstallerExecutor, PackageOperation, + format_full_pretty_version_for_installed, }; use mozart_registry::lockfile; use std::collections::BTreeMap; @@ -185,17 +186,20 @@ pub fn compute_operations<'a>( let mut ops: Vec<(&'a lockfile::LockedPackage, Action)> = Vec::new(); for pkg in ordered { - if installed.is_installed(&pkg.name, &pkg.version) { - ops.push((pkg, Action::Skip)); - } else if installed + let installed_entry = installed .packages .iter() - .any(|p| p.name.eq_ignore_ascii_case(&pkg.name)) - { - ops.push((pkg, Action::Update)); - } else { - ops.push((pkg, Action::Install)); - } + .find(|p| p.name.eq_ignore_ascii_case(&pkg.name)); + let action = match installed_entry { + None => Action::Install, + Some(entry) if entry.version != pkg.version => Action::Update, + // Same version present — Composer's Transaction also fires an + // UpdateOperation when the source/dist reference moved (e.g. a + // root require pinned a new commit via `dev-main#abcd`). + Some(entry) if !installed_refs_match_locked(entry, pkg) => Action::Update, + Some(_) => Action::Skip, + }; + ops.push((pkg, action)); } // Compute removals: packages in installed but not in locked @@ -325,6 +329,30 @@ fn topological_sort<'a>( ordered } +/// Compare an installed-package entry's source/dist references with a +/// locked package's. Mirrors the reference-equality leg of Composer's +/// `Transaction::calculateOperations` update-detection: a same-version +/// install is upgraded (or downgraded) when either reference has shifted, +/// so users who pinned a new commit via `dev-main#abcd` see the move. +fn installed_refs_match_locked( + entry: &installed::InstalledPackageEntry, + locked: &lockfile::LockedPackage, +) -> bool { + let installed_source_ref = entry + .source + .as_ref() + .and_then(|v| v.get("reference")) + .and_then(|v| v.as_str()); + let installed_dist_ref = entry + .dist + .as_ref() + .and_then(|v| v.get("reference")) + .and_then(|v| v.as_str()); + let locked_source_ref = locked.source.as_ref().and_then(|s| s.reference.as_deref()); + let locked_dist_ref = locked.dist.as_ref().and_then(|d| d.reference.as_deref()); + installed_source_ref == locked_source_ref && installed_dist_ref == locked_dist_ref +} + /// Convert a LockedPackage to an InstalledPackageEntry. /// /// `LockedPackage::extra_fields` is forwarded verbatim so flags like @@ -647,6 +675,10 @@ pub async fn install_from_lock( } for (pkg, action) in &ops { + // Owned scratch buffer the Update branch borrows for + // `PackageOperation::Update::from_version`. Declared at loop + // scope so the borrow outlives the await call. + let from_version_buf; let op = match action { Action::Skip => continue, Action::Install => { @@ -665,15 +697,22 @@ pub async fn install_from_lock( )); // Pull the previously-installed version from installed.json // so the trace recorder can format - // `Upgrading pkg (oldVersion => newVersion)`. - let from_version = installed + // `Upgrading pkg (oldVersion => newVersion)`. The plain + // version drives the upgrade/downgrade direction; the + // full-pretty form (with the dev reference suffix) is + // what shows up in the trace, mirroring Composer's + // `UpdateOperation::format`. + let from_entry = installed .packages .iter() - .find(|p| p.name.eq_ignore_ascii_case(&pkg.name)) - .map(|p| p.version.as_str()) - .unwrap_or(""); + .find(|p| p.name.eq_ignore_ascii_case(&pkg.name)); + let from_version = from_entry.map(|p| p.version.as_str()).unwrap_or(""); + from_version_buf = from_entry + .map(format_full_pretty_version_for_installed) + .unwrap_or_default(); PackageOperation::Update { from_version, + from_full_pretty: &from_version_buf, package: pkg, } } diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index a0eaab0..93c3496 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -416,8 +416,8 @@ installer_fixture!(update_package_present_in_lock_but_not_in_remote_due_to_min_s installer_fixture!(update_package_present_in_lower_repo_prio_but_not_main_due_to_min_stability); installer_fixture!(update_picks_up_change_of_vcs_type, ignore); installer_fixture!(update_prefer_lowest_stable); -installer_fixture!(update_reference, ignore); -installer_fixture!(update_reference_picks_latest, ignore); +installer_fixture!(update_reference); +installer_fixture!(update_reference_picks_latest); installer_fixture!(update_removes_unused_locked_dep, ignore); installer_fixture!(update_requiring_decision_reverts_and_learning_positive_literals); installer_fixture!(update_security_advisory_matching_direct_dependency, ignore); @@ -431,4 +431,4 @@ installer_fixture!(update_to_empty_from_locked, ignore); installer_fixture!(update_with_all_dependencies); installer_fixture!(update_without_lock); installer_fixture!(updating_dev_from_lock_removes_old_deps, ignore); -installer_fixture!(updating_dev_updates_url_and_reference, ignore); +installer_fixture!(updating_dev_updates_url_and_reference); |
