diff options
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 31 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 2 |
2 files changed, 31 insertions, 2 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index 52e82ec..b41e91a 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -196,8 +196,11 @@ pub fn compute_operations<'a>( 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`). + // root require pinned a new commit via `dev-main#abcd`), or + // when the `abandoned` flag / replacement target drifted (so + // installed.json picks up the fresh metadata). Some(entry) if !installed_refs_match_locked(entry, pkg) => Action::Update, + Some(entry) if !installed_abandoned_matches_locked(entry, pkg) => Action::Update, Some(_) => Action::Skip, }; ops.push((pkg, action)); @@ -412,6 +415,32 @@ fn installed_refs_match_locked( installed_source_ref == locked_source_ref && installed_dist_ref == locked_dist_ref } +/// Reduce a serialized `abandoned` value to the (isAbandoned, replacement) +/// pair Composer compares in `Transaction::calculateOperations`: +/// `isAbandoned()` is the truthy cast of the field, and +/// `getReplacementPackage()` is the field itself when it's a string, else +/// null. Missing / `false` / `null` collapse to "not abandoned"; `true` is +/// abandoned with no replacement; a string is both. +fn abandoned_state(v: Option<&serde_json::Value>) -> (bool, Option<&str>) { + match v { + Some(serde_json::Value::Bool(b)) => (*b, None), + Some(serde_json::Value::String(s)) => (true, Some(s.as_str())), + _ => (false, None), + } +} + +/// Mirror the `isAbandoned()` / `getReplacementPackage()` leg of Composer's +/// same-version update check: when an installed package's `abandoned` flag +/// (or its replacement target) drifts from the lock, fire an UpdateOperation +/// so vendor/composer/installed.json is rewritten with the fresh value. +fn installed_abandoned_matches_locked( + entry: &installed::InstalledPackageEntry, + locked: &lockfile::LockedPackage, +) -> bool { + abandoned_state(entry.extra_fields.get("abandoned")) + == abandoned_state(locked.extra_fields.get("abandoned")) +} + /// Convert a LockedPackage to an InstalledPackageEntry. /// /// `LockedPackage::extra_fields` is forwarded verbatim so flags like diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index cb7f430..b5e4026 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -259,7 +259,7 @@ installer_fixture!(install_aliased_alias); installer_fixture!(install_branch_alias_composer_repo); installer_fixture!(install_dev); installer_fixture!(install_dev_using_dist); -installer_fixture!(install_forces_reinstall_if_abandon_changes, ignore); +installer_fixture!(install_forces_reinstall_if_abandon_changes); installer_fixture!(install_from_incomplete_lock); installer_fixture!(install_from_incomplete_lock_with_ignore, ignore); installer_fixture!(install_from_lock_removes_package); |
