aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/mozart/src/commands/install.rs31
-rw-r--r--crates/mozart/tests/installer.rs2
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);