aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-registry/src')
-rw-r--r--crates/mozart-registry/src/lib.rs1
-rw-r--r--crates/mozart-registry/src/lockfile.rs1
-rw-r--r--crates/mozart-registry/src/resolver.rs38
-rw-r--r--crates/mozart-registry/src/vcs_bridge.rs204
4 files changed, 241 insertions, 3 deletions
diff --git a/crates/mozart-registry/src/lib.rs b/crates/mozart-registry/src/lib.rs
index 9fd9aff..4c26c1e 100644
--- a/crates/mozart-registry/src/lib.rs
+++ b/crates/mozart-registry/src/lib.rs
@@ -4,4 +4,5 @@ pub mod installed;
pub mod lockfile;
pub mod packagist;
pub mod resolver;
+pub mod vcs_bridge;
pub mod version;
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs
index bfae4ee..8f27fbf 100644
--- a/crates/mozart-registry/src/lockfile.rs
+++ b/crates/mozart-registry/src/lockfile.rs
@@ -1032,6 +1032,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved = resolve(&resolve_request)
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index 898a91c..4930b3a 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -9,7 +9,8 @@ use std::fmt;
use crate::cache::Cache;
use crate::packagist;
-use mozart_core::package::Stability;
+use crate::vcs_bridge;
+use mozart_core::package::{RawRepository, Stability};
use mozart_sat_resolver::{
DefaultPolicy, PoolBuilder, PoolPackageInput, RuleSetGenerator, Solver, make_pool_links,
};
@@ -20,7 +21,7 @@ use mozart_semver::Version;
// ─────────────────────────────────────────────────────────────────────────────
/// Determine the `Stability` of a `Version` from its pre_release string.
-fn version_stability(v: &Version) -> Stability {
+pub(crate) fn version_stability(v: &Version) -> Stability {
match &v.pre_release {
None => Stability::Stable,
Some(pre) => {
@@ -43,7 +44,7 @@ fn version_stability(v: &Version) -> Stability {
/// Parse a Packagist normalized version string like "1.2.3.0", "1.0.0.0-beta1".
/// Returns `None` for dev branches (dev-master, dev-*, *.x-dev).
-fn parse_normalized(normalized: &str) -> Option<Version> {
+pub(crate) fn parse_normalized(normalized: &str) -> Option<Version> {
let s = normalized.trim();
// Reject dev branches
@@ -348,6 +349,9 @@ pub struct ResolveRequest {
/// Temporary version constraint overrides (from --with flag).
/// Maps package name (lowercase) to constraint string.
pub temporary_constraints: HashMap<String, String>,
+ /// VCS repositories from composer.json "repositories" section.
+ /// Used to fetch packages from VCS before falling back to Packagist.
+ pub repositories: Vec<RawRepository>,
}
/// A single package in the resolution output.
@@ -413,6 +417,7 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
let prefer_lowest = request.prefer_lowest;
let ignore_platform_reqs = request.ignore_platform_reqs;
let ignore_platform_req_list = request.ignore_platform_req_list.clone();
+ let vcs_repositories = request.repositories.clone();
// 2. Build pool, generate rules, and solve on a blocking thread
tokio::task::spawn_blocking(move || -> Result<Vec<ResolvedPackage>, ResolveError> {
@@ -447,12 +452,33 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
builder.add_package(input);
}
+ // Scan VCS repositories and collect packages from them
+ let vcs_repos = &vcs_repositories;
+ let vcs_packages = vcs_bridge::scan_vcs_repositories(vcs_repos);
+ let mut vcs_package_names: HashSet<String> = HashSet::new();
+ for vpkg in &vcs_packages {
+ vcs_package_names.insert(vpkg.name.clone());
+ }
+
+ // Add VCS packages to the pool
+ for vpkg in &vcs_packages {
+ let inputs = vcs_bridge::vcs_to_pool_inputs(vpkg, minimum_stability, &stability_flags);
+ for input in inputs {
+ builder.add_package(input);
+ }
+ }
+
// Seed the builder with packages for root requirements
for name in root_requires.keys() {
if PackageName(name.clone()).is_platform() {
continue; // platform packages already added
}
+ // Skip packages already provided by VCS repositories
+ if vcs_package_names.contains(name) {
+ continue;
+ }
+
// Fetch available versions from Packagist
let versions = handle
.block_on(packagist::fetch_package_versions(name, repo_cache.as_ref()))
@@ -476,6 +502,11 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
continue;
}
+ // Skip packages already provided by VCS repositories
+ if vcs_package_names.contains(&name) {
+ continue;
+ }
+
let versions = match handle.block_on(packagist::fetch_package_versions(
&name,
repo_cache.as_ref(),
@@ -938,6 +969,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let result = resolve(&request).await;
diff --git a/crates/mozart-registry/src/vcs_bridge.rs b/crates/mozart-registry/src/vcs_bridge.rs
new file mode 100644
index 0000000..d81cae8
--- /dev/null
+++ b/crates/mozart-registry/src/vcs_bridge.rs
@@ -0,0 +1,204 @@
+//! Bridge between `mozart-vcs` and `mozart-registry`.
+//!
+//! Scans VCS repositories defined in composer.json and converts
+//! discovered package versions into pool inputs for the SAT resolver.
+
+use std::collections::{BTreeMap, HashMap};
+
+use mozart_core::package::{RawRepository, Stability};
+use mozart_sat_resolver::{PoolPackageInput, make_pool_links};
+use mozart_vcs::driver::DriverConfig;
+use mozart_vcs::repository::{VcsPackageVersion, VcsRepository};
+
+use crate::packagist::PackagistVersion;
+use crate::resolver::{parse_normalized, version_stability};
+
+/// Scan all VCS-type repositories and collect package versions.
+///
+/// Non-VCS repos (e.g. "composer", "package") are silently skipped.
+pub fn scan_vcs_repositories(repositories: &[RawRepository]) -> Vec<VcsPackageVersion> {
+ let config = DriverConfig::default();
+ let mut all_versions = Vec::new();
+
+ for repo in repositories {
+ let repo_type = repo.repo_type.as_str();
+ match repo_type {
+ "vcs" | "git" | "svn" | "hg" | "github" | "gitlab" | "bitbucket" | "forgejo" => {}
+ _ => continue,
+ }
+
+ let forced_type = match repo_type {
+ "vcs" => None,
+ other => Some(other),
+ };
+
+ let vcs_repo = VcsRepository::new(repo.url.clone(), forced_type, config.clone());
+
+ match vcs_repo.scan() {
+ Ok(versions) => {
+ all_versions.extend(versions);
+ }
+ Err(e) => {
+ eprintln!("Warning: Failed to scan VCS repository {}: {}", repo.url, e,);
+ }
+ }
+ }
+
+ all_versions
+}
+
+/// Convert a VCS package version to SAT pool inputs.
+pub fn vcs_to_pool_inputs(
+ vpkg: &VcsPackageVersion,
+ minimum_stability: Stability,
+ stability_flags: &HashMap<String, Stability>,
+) -> Vec<PoolPackageInput> {
+ let mut results = Vec::new();
+
+ // Extract dependency links from composer.json
+ let require = extract_dep_map(&vpkg.composer_json, "require");
+ let replace = extract_dep_map(&vpkg.composer_json, "replace");
+ let provide = extract_dep_map(&vpkg.composer_json, "provide");
+ let conflict = extract_dep_map(&vpkg.composer_json, "conflict");
+
+ let input = PoolPackageInput {
+ name: vpkg.name.clone(),
+ version: vpkg.version_normalized.clone(),
+ pretty_version: vpkg.version.clone(),
+ requires: make_pool_links(
+ &vpkg.name,
+ &require
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ replaces: make_pool_links(
+ &vpkg.name,
+ &replace
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ provides: make_pool_links(
+ &vpkg.name,
+ &provide
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ conflicts: make_pool_links(
+ &vpkg.name,
+ &conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ is_fixed: false,
+ };
+
+ // Apply stability filtering
+ if let Some(v) = parse_normalized(&vpkg.version_normalized) {
+ if passes_vcs_stability_filter(&vpkg.name, &v, minimum_stability, stability_flags) {
+ results.push(input);
+ }
+ } else {
+ // Dev version: always include (dev stability)
+ let pkg_flag = stability_flags.get(&vpkg.name.to_lowercase());
+ let allowed = pkg_flag.copied().unwrap_or(minimum_stability);
+ if allowed >= Stability::Dev {
+ results.push(input);
+ }
+ }
+
+ results
+}
+
+/// Convert a `VcsPackageVersion` into a `PackagistVersion` for lockfile generation.
+pub fn vcs_to_packagist_version(vpkg: &VcsPackageVersion) -> PackagistVersion {
+ PackagistVersion {
+ version: vpkg.version.clone(),
+ version_normalized: vpkg.version_normalized.clone(),
+ require: extract_dep_map(&vpkg.composer_json, "require"),
+ replace: extract_dep_map(&vpkg.composer_json, "replace"),
+ provide: extract_dep_map(&vpkg.composer_json, "provide"),
+ conflict: extract_dep_map(&vpkg.composer_json, "conflict"),
+ dist: vpkg.dist.as_ref().map(|d| crate::packagist::PackagistDist {
+ dist_type: d.dist_type.clone(),
+ url: d.url.clone(),
+ reference: Some(d.reference.clone()),
+ shasum: d.shasum.clone(),
+ }),
+ source: Some(crate::packagist::PackagistSource {
+ source_type: vpkg.source.source_type.clone(),
+ url: vpkg.source.url.clone(),
+ reference: Some(vpkg.source.reference.clone()),
+ }),
+ require_dev: extract_dep_map(&vpkg.composer_json, "require-dev"),
+ suggest: vpkg
+ .composer_json
+ .get("suggest")
+ .and_then(|v| serde_json::from_value(v.clone()).ok()),
+ package_type: vpkg
+ .composer_json
+ .get("type")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string()),
+ autoload: vpkg.composer_json.get("autoload").cloned(),
+ autoload_dev: vpkg.composer_json.get("autoload-dev").cloned(),
+ license: vpkg
+ .composer_json
+ .get("license")
+ .and_then(|v| serde_json::from_value(v.clone()).ok()),
+ description: vpkg
+ .composer_json
+ .get("description")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string()),
+ homepage: vpkg
+ .composer_json
+ .get("homepage")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string()),
+ keywords: vpkg
+ .composer_json
+ .get("keywords")
+ .and_then(|v| serde_json::from_value(v.clone()).ok()),
+ authors: vpkg
+ .composer_json
+ .get("authors")
+ .and_then(|v| serde_json::from_value(v.clone()).ok()),
+ support: vpkg.composer_json.get("support").cloned(),
+ funding: vpkg
+ .composer_json
+ .get("funding")
+ .and_then(|v| serde_json::from_value(v.clone()).ok()),
+ time: vpkg.time.clone(),
+ extra: vpkg.composer_json.get("extra").cloned(),
+ notification_url: None,
+ }
+}
+
+/// Extract a dependency map from composer.json JSON.
+fn extract_dep_map(json: &serde_json::Value, key: &str) -> BTreeMap<String, String> {
+ json.get(key)
+ .and_then(|v| v.as_object())
+ .map(|obj| {
+ obj.iter()
+ .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
+/// Stability filter for VCS packages (mirrors resolver logic).
+fn passes_vcs_stability_filter(
+ package_name: &str,
+ version: &mozart_semver::Version,
+ minimum_stability: Stability,
+ stability_flags: &HashMap<String, Stability>,
+) -> bool {
+ let stability = version_stability(version);
+ let pkg_flag = stability_flags.get(&package_name.to_lowercase());
+ let allowed = pkg_flag.copied().unwrap_or(minimum_stability);
+ stability <= allowed
+}