diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-23 11:38:42 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-23 11:38:42 +0900 |
| commit | 0080efea9386d46f65d1862fcb90eb44999d9761 (patch) | |
| tree | e9f7e17b3f12ff9b09b3df0848fd55e91003cd23 /crates/mozart-registry | |
| parent | eb1e21c059d83f0af9786e4d3cace80afe8456a2 (diff) | |
| download | php-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.tar.gz php-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.tar.zst php-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.zip | |
feat(vcs): add mozart-vcs crate for VCS repository support
Implement VCS driver/downloader infrastructure mirroring Composer's VCS
subsystem. Includes drivers for GitHub, GitLab, Bitbucket, Forgejo, Git,
Hg, and SVN with API-based metadata resolution, plus source downloaders
for Git/Hg/SVN. Integrates into mozart-registry via vcs_bridge module to
scan VCS repositories and feed discovered packages into the SAT resolver.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry')
| -rw-r--r-- | crates/mozart-registry/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 38 | ||||
| -rw-r--r-- | crates/mozart-registry/src/vcs_bridge.rs | 204 |
5 files changed, 242 insertions, 3 deletions
diff --git a/crates/mozart-registry/Cargo.toml b/crates/mozart-registry/Cargo.toml index 9d089e5..87d3c69 100644 --- a/crates/mozart-registry/Cargo.toml +++ b/crates/mozart-registry/Cargo.toml @@ -8,6 +8,7 @@ mozart-core.workspace = true mozart-metadata-minifier.workspace = true mozart-sat-resolver.workspace = true mozart-semver.workspace = true +mozart-vcs.workspace = true anyhow.workspace = true filetime.workspace = true flate2.workspace = true 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 +} |
