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 | |
| 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')
| -rw-r--r-- | crates/mozart-registry/src/installer_executor/mod.rs | 65 | ||||
| -rw-r--r-- | crates/mozart-registry/src/installer_executor/trace_recorder.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 67 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 6 |
4 files changed, 118 insertions, 23 deletions
diff --git a/crates/mozart-registry/src/installer_executor/mod.rs b/crates/mozart-registry/src/installer_executor/mod.rs index ff3d7a8..704031f 100644 --- a/crates/mozart-registry/src/installer_executor/mod.rs +++ b/crates/mozart-registry/src/installer_executor/mod.rs @@ -15,6 +15,7 @@ use std::path::PathBuf; +use crate::installed::InstalledPackageEntry; use crate::lockfile::{LockAlias, LockedPackage}; pub mod filesystem; @@ -30,9 +31,13 @@ pub enum PackageOperation<'a> { /// `package.dist`/`package.source`. Install { package: &'a LockedPackage }, /// Replace an existing install with a new version. `from_version` is the - /// pretty version that was installed before. + /// pretty version that was installed before (no reference suffix — + /// drives the upgrade-vs-downgrade direction). `from_full_pretty` is the + /// formatted display string (`dev-master abc123`) used verbatim in the + /// trace output. Update { from_version: &'a str, + from_full_pretty: &'a str, package: &'a LockedPackage, }, /// Mark an alias of a real package as installed. No filesystem effects — @@ -72,15 +77,65 @@ pub fn format_full_pretty_version(pkg: &LockedPackage) -> String { /// alternate pretty version (used by `MarkAliasInstalled` so the alias's /// `3.2.x-dev` text is rendered with the *target's* reference). pub fn format_full_pretty_with_pretty(pretty_version: &str, pkg: &LockedPackage) -> String { - let is_dev = mozart_semver::Version::parse(&pkg.version) + let source_ref = pkg.source.as_ref().and_then(|s| s.reference.as_deref()); + let dist_ref = pkg.dist.as_ref().and_then(|d| d.reference.as_deref()); + let source_type = pkg.source.as_ref().map(|s| s.source_type.as_str()); + format_full_pretty_with_refs( + pretty_version, + &pkg.version, + source_ref, + dist_ref, + source_type, + ) +} + +/// Mirror Composer's `BasePackage::getFullPrettyVersion()` for an +/// `InstalledPackageEntry`. Same display rules as +/// [`format_full_pretty_version`] but pulls source/dist info out of the +/// installed.json `source`/`dist` JSON values. +pub fn format_full_pretty_version_for_installed(entry: &InstalledPackageEntry) -> String { + let source_ref = entry + .source + .as_ref() + .and_then(|v| v.get("reference")) + .and_then(|v| v.as_str()); + let dist_ref = entry + .dist + .as_ref() + .and_then(|v| v.get("reference")) + .and_then(|v| v.as_str()); + let source_type = entry + .source + .as_ref() + .and_then(|v| v.get("type")) + .and_then(|v| v.as_str()); + format_full_pretty_with_refs( + &entry.version, + &entry.version, + source_ref, + dist_ref, + source_type, + ) +} + +/// Core of `BasePackage::getFullPrettyVersion()` factored over raw +/// fields so both [`LockedPackage`] and [`InstalledPackageEntry`] can share +/// the rendering logic. `version` drives the dev-stability check; the result +/// is `pretty_version` plus a reference suffix when the package is a dev +/// branch backed by git/hg (with sha1 references truncated to 7 chars). +fn format_full_pretty_with_refs( + pretty_version: &str, + version: &str, + source_ref: Option<&str>, + dist_ref: Option<&str>, + source_type: Option<&str>, +) -> String { + let is_dev = mozart_semver::Version::parse(version) .map(|v| matches!(v.pre_release.as_deref(), Some("dev")) || v.is_dev_branch) .unwrap_or(false); if !is_dev { return pretty_version.to_string(); } - let source_ref = pkg.source.as_ref().and_then(|s| s.reference.as_deref()); - let dist_ref = pkg.dist.as_ref().and_then(|d| d.reference.as_deref()); - let source_type = pkg.source.as_ref().map(|s| s.source_type.as_str()); // Composer falls back to dist reference only when no source type is set // (or the package isn't git/hg — in which case the dev display is skipped // entirely above). diff --git a/crates/mozart-registry/src/installer_executor/trace_recorder.rs b/crates/mozart-registry/src/installer_executor/trace_recorder.rs index 44fceea..785d161 100644 --- a/crates/mozart-registry/src/installer_executor/trace_recorder.rs +++ b/crates/mozart-registry/src/installer_executor/trace_recorder.rs @@ -69,6 +69,7 @@ impl InstallerExecutor for TraceRecorderExecutor { } PackageOperation::Update { from_version, + from_full_pretty, package, } => { let action = if is_upgrade(from_version, &package.version) { @@ -80,7 +81,7 @@ impl InstallerExecutor for TraceRecorderExecutor { "{} {} ({} => {})", action, package.name, - from_version, + from_full_pretty, format_full_pretty_version(package) )); } 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); |
