aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 20:40:56 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 20:40:56 +0900
commit6ec10b18cfe2e473d71f8d786ae0d6a9877864ab (patch)
treeaa3b92efa514c228fb6c8864789d0853731c542f /crates/mozart
parent2bb4f62d0a7b98ea4b3195fbfefdd7b5f0aff19c (diff)
downloadphp-mozart-6ec10b18cfe2e473d71f8d786ae0d6a9877864ab.tar.gz
php-mozart-6ec10b18cfe2e473d71f8d786ae0d6a9877864ab.tar.zst
php-mozart-6ec10b18cfe2e473d71f8d786ae0d6a9877864ab.zip
fix(install): align partial-update operation order with Composer
Three coordinated changes to make `update --with-dependencies` produce the same operation trace Composer emits: - LockFileGenerationRequest gains a previous_lock field. When a resolved package matches an entry in the old lock at the same name + version_normalized, its relationship-shaped fields (require / require-dev / conflict / replace / provide / suggest) are carried over verbatim. Source/dist refs and version-shaped fields still refresh from upstream metadata so dev packages can still pick up new commits. Without this carry-over, partial updates regenerated lock entries from upstream COMPOSER repo definitions, which can declare different requires than the lock — and topological_sort then sees a graph Composer's transaction never built. - Transaction's topological_sort and get_root_packages now expand replace/provide targets when matching `require` links to result packages, mirroring Composer's getProvidersInResult. Previously a package was only treated as required when matched by its own name, so packages reached only via replace/provide were mis-classified as roots and the DFS stack visited deps in the wrong order. - compute_operations iterates installed.json in reverse when emitting removals, mirroring Composer's array_unshift onto operations. Two co-orphaned packages otherwise emit removals in the wrong order vs Composer's trace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart')
-rw-r--r--crates/mozart/src/commands/create_project.rs1
-rw-r--r--crates/mozart/src/commands/install.rs9
-rw-r--r--crates/mozart/src/commands/remove.rs4
-rw-r--r--crates/mozart/src/commands/require.rs3
-rw-r--r--crates/mozart/src/commands/update.rs2
-rw-r--r--crates/mozart/tests/installer.rs7
6 files changed, 20 insertions, 6 deletions
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index 13a2bb2..fc0efee 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -464,6 +464,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
+ previous_lock: None,
})
.await?;
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index 58a01b4..017c08e 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -206,12 +206,19 @@ pub fn compute_operations<'a>(
ops.push((pkg, action));
}
- // Compute removals: packages in installed but not in locked
+ // Compute removals: packages in installed but not in locked. Iterate
+ // installed.json in reverse, mirroring Composer's
+ // `Transaction::calculateOperations`, which seeds `removeMap` from
+ // `presentPackages` in order and then `array_unshift`s each entry onto
+ // `operations` — flipping the iteration order. Without the flip, two
+ // co-orphaned packages emit removals in the wrong order vs Composer's
+ // trace.
let locked_names: IndexSet<String> = locked.iter().map(|p| p.name.to_lowercase()).collect();
let removals: Vec<String> = installed
.packages
.iter()
+ .rev()
.filter(|p| !locked_names.contains(&p.name.to_lowercase()))
.map(|p| p.name.clone())
.collect();
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index 3ffe04a..eb6ee4a 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -376,6 +376,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
+ previous_lock: old_lock.clone(),
})
.await?;
@@ -619,6 +620,7 @@ async fn remove_unused(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
+ previous_lock: Some(old_lock.clone()),
})
.await?;
@@ -934,6 +936,7 @@ mod tests {
),
),
),
+ previous_lock: None,
})
.await
.expect("initial lock file generation should succeed");
@@ -994,6 +997,7 @@ mod tests {
),
),
),
+ previous_lock: Some(initial_lock.clone()),
})
.await
.expect("post-remove lock file generation should succeed");
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 45ad759..110bd1a 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -765,6 +765,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
+ previous_lock: old_lock.clone(),
})
.await?;
@@ -1095,6 +1096,7 @@ mod tests {
),
),
),
+ previous_lock: None,
})
.await
.expect("Lock file generation should succeed");
@@ -1168,6 +1170,7 @@ mod tests {
),
),
),
+ previous_lock: None,
})
.await
.expect("Lock file generation should succeed");
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 5cf05c4..43825f2 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -1290,6 +1290,7 @@ pub async fn run(
composer_json: composer_json.clone(),
include_dev: dev_mode,
repositories: repositories.clone(),
+ previous_lock: old_lock.clone(),
})
.await?;
@@ -2285,6 +2286,7 @@ mod tests {
),
),
),
+ previous_lock: None,
})
.await
.expect("Lock file generation should succeed");
diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs
index f6324b6..06c7581 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -373,10 +373,7 @@ installer_fixture!(update_allow_list_with_dependencies_alias, ignore);
installer_fixture!(update_allow_list_with_dependencies_new_requirement);
installer_fixture!(update_allow_list_with_dependencies_require_new);
installer_fixture!(update_allow_list_with_dependencies_require_new_replace);
-installer_fixture!(
- update_allow_list_with_dependencies_require_new_replace_mutual,
- ignore
-);
+installer_fixture!(update_allow_list_with_dependencies_require_new_replace_mutual);
installer_fixture!(update_allow_list_with_dependency_conflict);
installer_fixture!(update_changes_url, ignore);
installer_fixture!(update_dev_ignores_providers);
@@ -403,7 +400,7 @@ installer_fixture!(update_picks_up_change_of_vcs_type);
installer_fixture!(update_prefer_lowest_stable);
installer_fixture!(update_reference);
installer_fixture!(update_reference_picks_latest);
-installer_fixture!(update_removes_unused_locked_dep, ignore);
+installer_fixture!(update_removes_unused_locked_dep);
installer_fixture!(update_requiring_decision_reverts_and_learning_positive_literals);
installer_fixture!(update_security_advisory_matching_direct_dependency, ignore);
installer_fixture!(