diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:32:10 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:32:10 +0900 |
| commit | 2af684c6397001944c9d9aac20ca59677d6a9650 (patch) | |
| tree | 054f40875ca547223c36f19e876269baadb1bf6f /crates/mozart-registry/src | |
| parent | ab2772b8c85139df7d5e625ac5262d385e5ab4c0 (diff) | |
| download | php-mozart-2af684c6397001944c9d9aac20ca59677d6a9650.tar.gz php-mozart-2af684c6397001944c9d9aac20ca59677d6a9650.tar.zst php-mozart-2af684c6397001944c9d9aac20ca59677d6a9650.zip | |
fix(install): emit MarkAliasUninstalled and fix dev-branch upgrade direction
Two pieces of Composer's update-trace machinery were missing:
1. VersionParser::isUpgrade in Composer\Package\Version (which overrides
the upstream Semver one) substitutes dev-master / dev-trunk /
dev-default with the 9999999-dev default-branch alias, then returns
true whenever either side starts with `dev-`. Mozart's is_upgrade
compared via the generic version order, so dev-master → dev-foo came
out as Downgrading. Port the override.
2. Transaction::calculateOperations seeds removeAliasMap from the
currently-installed AliasPackages and emits MarkAliasUninstalled for
every entry not covered by the new lock. Mozart never emitted those,
so updating away from a branch-aliased package produced no trace
line for the alias retirement. Walk installed.json's
`extra.branch-alias` map, compare against the new lock's aliases[]
block, and emit a MarkAliasUninstalled PackageOperation (a new
variant on the executor surface — no filesystem effects, only the
trace recorder cares).
Unblocks update_alias, update_alias_lock2, and update_no_dev_still_resolves_dev
installer fixtures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src')
3 files changed, 88 insertions, 16 deletions
diff --git a/crates/mozart-registry/src/installer_executor/filesystem.rs b/crates/mozart-registry/src/installer_executor/filesystem.rs index cceb5da..cb1a2cc 100644 --- a/crates/mozart-registry/src/installer_executor/filesystem.rs +++ b/crates/mozart-registry/src/installer_executor/filesystem.rs @@ -29,10 +29,11 @@ impl InstallerExecutor for FilesystemExecutor { op: PackageOperation<'_>, ctx: &ExecuteContext, ) -> anyhow::Result<()> { - // Marking an alias as installed has no filesystem side effects — - // the target package's files are already in vendor/. Mirrors - // Composer's `MarkAliasInstalledOperation` which the installation - // manager only uses to update the in-memory installed repository. + // Marking an alias as installed/uninstalled has no filesystem side + // effects — the target package's files are already in vendor/. + // Mirrors Composer's `MarkAlias{,Un}installedOperation` which the + // installation manager only uses to update the in-memory installed + // repository. let Some(pkg) = op.package() else { return Ok(()); }; diff --git a/crates/mozart-registry/src/installer_executor/mod.rs b/crates/mozart-registry/src/installer_executor/mod.rs index a774490..c344ba4 100644 --- a/crates/mozart-registry/src/installer_executor/mod.rs +++ b/crates/mozart-registry/src/installer_executor/mod.rs @@ -54,6 +54,23 @@ pub enum PackageOperation<'a> { /// reference suffix for the trace line. target: &'a LockedPackage, }, + /// Mark a previously-installed alias as uninstalled. No filesystem + /// effects — only the trace recorder cares. Mirrors Composer's + /// `MarkAliasUninstalledOperation`. Composer derives the AliasPackage + /// from the previous installed.json entries (via `extra.branch-alias`), + /// then emits this when the alias is no longer in the result. Caller + /// pre-renders the display strings so this variant doesn't need to know + /// how to spelunk the entry. + MarkAliasUninstalled { + /// Package name (e.g. `a/a`) used as both the alias's name and the + /// target's name on the trace line. + name: &'a str, + /// Alias's full-pretty form (alias pretty version plus reference + /// suffix), e.g. `1.0.x-dev master`. + alias_full: &'a str, + /// Target's full-pretty form, e.g. `dev-master master`. + target_full: &'a str, + }, } impl<'a> PackageOperation<'a> { @@ -62,7 +79,8 @@ impl<'a> PackageOperation<'a> { PackageOperation::Install { package } | PackageOperation::Update { package, .. } => { Some(package) } - PackageOperation::MarkAliasInstalled { .. } => None, + PackageOperation::MarkAliasInstalled { .. } + | PackageOperation::MarkAliasUninstalled { .. } => None, } } } @@ -92,11 +110,14 @@ pub fn format_full_pretty_with_pretty(pretty_version: &str, pkg: &LockedPackage) ) } -/// 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 { +/// Same as [`format_full_pretty_version_for_installed`] but lets the caller +/// supply an alternate pretty version. Used when emitting +/// `MarkAliasUninstalled`: the alias's `1.0.x-dev` text needs to be rendered +/// with the *target installed entry's* reference suffix. +pub fn format_full_pretty_with_pretty_for_installed( + pretty_version: &str, + entry: &InstalledPackageEntry, +) -> String { let source_ref = entry .source .as_ref() @@ -113,7 +134,7 @@ pub fn format_full_pretty_version_for_installed(entry: &InstalledPackageEntry) - .and_then(|v| v.get("type")) .and_then(|v| v.as_str()); format_full_pretty_with_refs( - &entry.version, + pretty_version, &entry.version, source_ref, dist_ref, @@ -121,6 +142,14 @@ pub fn format_full_pretty_version_for_installed(entry: &InstalledPackageEntry) - ) } +/// 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 { + format_full_pretty_with_pretty_for_installed(&entry.version, entry) +} + /// Render the from/to display strings for an update trace line, mirroring /// Composer's `UpdateOperation::format`. Defaults to `DISPLAY_SOURCE_REF_IF_DEV`, /// then if both sides render identically: diff --git a/crates/mozart-registry/src/installer_executor/trace_recorder.rs b/crates/mozart-registry/src/installer_executor/trace_recorder.rs index 5c49160..159d854 100644 --- a/crates/mozart-registry/src/installer_executor/trace_recorder.rs +++ b/crates/mozart-registry/src/installer_executor/trace_recorder.rs @@ -91,6 +91,16 @@ impl InstallerExecutor for TraceRecorderExecutor { alias.package, alias_full, alias.package, target_full )); } + PackageOperation::MarkAliasUninstalled { + name, + alias_full, + target_full, + } => { + self.trace.push(format!( + "Marking {} ({}) as uninstalled, alias of {} ({})", + name, alias_full, name, target_full + )); + } } Ok(()) } @@ -106,12 +116,44 @@ impl InstallerExecutor for TraceRecorderExecutor { } } -/// Mirrors `Composer\Package\Version\VersionParser::isUpgrade` — returns -/// true when `to` is a strictly higher version than `from`. Both unparseable -/// or both equal means treat as upgrade (Composer's behavior on edge cases). +/// Mirrors `Composer\Package\Version\VersionParser::isUpgrade`. Returns true +/// when `to` should be treated as an upgrade from `from` for the purpose of +/// the trace verb (`Upgrading` vs `Downgrading`). +/// +/// The rules: +/// 1. Same string → upgrade. +/// 2. `dev-master` / `dev-trunk` / `dev-default` substitute to the +/// `9999999-dev` default-branch alias before further checks (they are +/// not literal dev-* names; they are the conventional "latest" branch). +/// 3. After that substitution, if either side starts with `dev-` (i.e. is +/// a dev branch other than the defaults) → upgrade. Composer treats +/// hopping between dev branches as a forward move regardless of order. +/// 4. Otherwise sort numerically and check the original `from` ended up +/// first (= the smaller value). fn is_upgrade(from: &str, to: &str) -> bool { - match (Version::parse(from), Version::parse(to)) { + if from == to { + return true; + } + let original_from = from; + let normalize_default = |s: &str| -> String { + if matches!(s, "dev-master" | "dev-trunk" | "dev-default") { + "9999999-dev".to_string() + } else { + s.to_string() + } + }; + let from_norm = normalize_default(from); + let to_norm = normalize_default(to); + if from_norm.starts_with("dev-") || to_norm.starts_with("dev-") { + return true; + } + match (Version::parse(&from_norm), Version::parse(&to_norm)) { (Ok(a), Ok(b)) => b >= a, - _ => true, + _ => { + // Mirror Composer's fall-through: with two unparseable strings + // there is nothing to compare, treat the move as an upgrade. + let _ = original_from; + true + } } } |
