aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-04 00:49:40 +0900
committernsfisis <nsfisis@gmail.com>2026-05-04 00:49:40 +0900
commit74c188162886755c380a4696d8b684cb28687402 (patch)
tree4a33b20c6ceef51d3720f169918c077c1cb06376 /crates/mozart
parent6449a15de90fe8252fb288bd5eacb99dc2cd699a (diff)
downloadphp-mozart-74c188162886755c380a4696d8b684cb28687402.tar.gz
php-mozart-74c188162886755c380a4696d8b684cb28687402.tar.zst
php-mozart-74c188162886755c380a4696d8b684cb28687402.zip
fix(update): preserve locked refs and aliases on partial update
Partial update of a non-allow-listed dev package now resolves and emits the locked-repo entry verbatim, mirroring Composer's `PoolBuilder`. Three coordinated changes: - resolver: `lock_filter_allows` accepts the locked package's branch- alias normalized versions, not just the base. Without this, root constraints like `~2.1` against a `dev-master` locked package whose branch alias is `2.1.x-dev` failed with "no matching package found". - lockfile: new `lock_pinned_names` field on `LockFileGenerationRequest` routes non-allow-listed packages through `previous_lock_lookup` before `inline_lookup`, so the lock's source/dist references survive even when the inline metadata has moved to a newer commit. - update: `apply_partial_update` skips alias entries — re-pinning their pretty `version` to the base would collapse the alias label and emit a self-referential entry in the new lock's `aliases[]` block. Unblocks partial_update_forces_dev_reference_from_lock_for_non_updated_packages. 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/remove.rs4
-rw-r--r--crates/mozart/src/commands/require.rs3
-rw-r--r--crates/mozart/src/commands/update.rs35
-rw-r--r--crates/mozart/tests/installer.rs5
5 files changed, 44 insertions, 4 deletions
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index 61cb886..6674ccc 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -468,6 +468,7 @@ pub async fn execute(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
previous_lock: None,
+ lock_pinned_names: indexmap::IndexSet::new(),
})
.await?;
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index d4b3aef..6498e01 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -380,6 +380,7 @@ pub async fn execute(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
previous_lock: old_lock.clone(),
+ lock_pinned_names: indexmap::IndexSet::new(),
})
.await?;
@@ -627,6 +628,7 @@ async fn remove_unused(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
previous_lock: Some(old_lock.clone()),
+ lock_pinned_names: indexmap::IndexSet::new(),
})
.await?;
@@ -946,6 +948,7 @@ mod tests {
),
),
previous_lock: None,
+ lock_pinned_names: IndexSet::new(),
})
.await
.expect("initial lock file generation should succeed");
@@ -1010,6 +1013,7 @@ mod tests {
),
),
previous_lock: Some(initial_lock.clone()),
+ lock_pinned_names: IndexSet::new(),
})
.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 b302ed9..02e5d8e 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -769,6 +769,7 @@ pub async fn execute(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
previous_lock: old_lock.clone(),
+ lock_pinned_names: indexmap::IndexSet::new(),
})
.await?;
@@ -1103,6 +1104,7 @@ mod tests {
),
),
previous_lock: None,
+ lock_pinned_names: IndexSet::new(),
})
.await
.expect("Lock file generation should succeed");
@@ -1180,6 +1182,7 @@ mod tests {
),
),
previous_lock: None,
+ lock_pinned_names: IndexSet::new(),
})
.await
.expect("Lock file generation should succeed");
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 5210a34..2a0aa88 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -410,6 +410,15 @@ pub fn apply_partial_update(
.into_iter()
.map(|mut pkg| {
let name_lower = pkg.name.to_lowercase();
+ // Alias entries already carry their post-swap shape: the resolver
+ // picked them from the locked-repo branch-alias surface, which is
+ // exactly where the previous lock would have put them. Re-pinning
+ // their `version` to the base's locked pretty would collapse the
+ // alias label into the base, leaving a self-referential entry in
+ // the new lock's `aliases[]` block.
+ if pkg.alias_of_normalized.is_some() {
+ return pkg;
+ }
// If this package is NOT in the update set and we have an old locked version,
// swap it back to the old version to prevent unintended changes.
//
@@ -1230,6 +1239,10 @@ pub async fn run(
continue;
}
names.insert(name_lower.clone());
+ let branch_aliases = lockfile::locked_package_branch_aliases(p)
+ .into_iter()
+ .map(|a| (a.alias, a.alias_normalized))
+ .collect();
infos.push(LockedPackageInfo {
name: name_lower,
pretty_version: p.version.clone(),
@@ -1254,6 +1267,7 @@ pub async fn run(
.iter()
.map(|(k, v)| (k.to_lowercase(), v.clone()))
.collect(),
+ branch_aliases,
});
}
(names, infos)
@@ -1599,6 +1613,25 @@ pub async fn run(
// Step 9: Generate new lock file. `include_dev: true` matches Composer:
// `update --no-dev` still writes a complete lock file with packages-dev
// populated, so a later `install` (with dev_mode) sees them.
+ //
+ // For partial updates, names NOT in the CLI allow list keep their
+ // locked-repo metadata (source/dist references in particular). Computed
+ // here from the same `update_packages` list `apply_partial_update` used
+ // to swap the resolved versions back. Empty for full updates.
+ let lock_pinned_names: IndexSet<String> = if update_packages.is_empty() {
+ IndexSet::new()
+ } else if let Some(lock) = &old_lock {
+ let update_set: IndexSet<String> =
+ update_packages.iter().map(|s| s.to_lowercase()).collect();
+ lock.packages
+ .iter()
+ .chain(lock.packages_dev.iter().flatten())
+ .map(|p| p.name.to_lowercase())
+ .filter(|n| !update_set.contains(n))
+ .collect()
+ } else {
+ IndexSet::new()
+ };
let mut new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest {
resolved_packages: resolved,
composer_json_content: composer_json_content.clone(),
@@ -1606,6 +1639,7 @@ pub async fn run(
include_dev: true,
repositories: repositories.clone(),
previous_lock: old_lock.clone(),
+ lock_pinned_names,
})
.await?;
@@ -2504,6 +2538,7 @@ mod tests {
),
),
previous_lock: None,
+ lock_pinned_names: IndexSet::new(),
})
.await
.expect("Lock file generation should succeed");
diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs
index 177decf..955a1ad 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -311,10 +311,7 @@ installer_fixture!(outdated_lock_file_fails_install);
installer_fixture!(outdated_lock_file_with_new_platform_reqs_fails);
installer_fixture!(partial_update_always_updates_symlinked_path_repos);
installer_fixture!(partial_update_downgrades_non_allow_listed_unstable);
-installer_fixture!(
- partial_update_forces_dev_reference_from_lock_for_non_updated_packages,
- ignore
-);
+installer_fixture!(partial_update_forces_dev_reference_from_lock_for_non_updated_packages);
installer_fixture!(partial_update_from_lock);
installer_fixture!(partial_update_from_lock_with_root_alias);
installer_fixture!(partial_update_installs_from_lock_even_missing, ignore);