aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src/repository
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
committernsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
commit8cc1ba8a02c0318b65658f1634de378c780392b9 (patch)
treefdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-registry/src/repository
parent72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff)
downloadphp-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz
php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst
php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry, mozart-sat-resolver, and mozart-vcs into mozart-core to align the source layout with Composer's structure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/repository')
-rw-r--r--crates/mozart-registry/src/repository/inline_package_repo.rs63
-rw-r--r--crates/mozart-registry/src/repository/mod.rs319
-rw-r--r--crates/mozart-registry/src/repository/packagist_repo.rs121
-rw-r--r--crates/mozart-registry/src/repository/vcs_repo.rs63
4 files changed, 0 insertions, 566 deletions
diff --git a/crates/mozart-registry/src/repository/inline_package_repo.rs b/crates/mozart-registry/src/repository/inline_package_repo.rs
deleted file mode 100644
index 1043559..0000000
--- a/crates/mozart-registry/src/repository/inline_package_repo.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-//! [`Repository`] for inline `type: package` repositories.
-//!
-//! Wraps [`crate::inline_package::collect_inline_packages`]. The data is
-//! embedded in `composer.json` so there's no I/O — the repo just filters
-//! its in-memory list by queried name.
-//!
-//! Mirrors `Composer\Repository\PackageRepository` (which extends
-//! `ArrayRepository`). Only the package's own `name` is matched against
-//! queries — `replace`/`provide` targets are NOT advertised here, exactly
-//! like Composer's `ArrayRepository::loadPackages` checks `getName()` only.
-//! Replacement satisfaction happens later in the solver once the replacing
-//! package is loaded transitively.
-
-use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository};
-use crate::inline_package::{InlinePackage, collect_inline_packages};
-use mozart_core::package::RawRepository;
-
-pub struct InlinePackageRepository {
- id: String,
- packages: Vec<InlinePackage>,
-}
-
-impl InlinePackageRepository {
- /// Build from the raw `repositories` array of a `composer.json`. Non-
- /// `package` entries are ignored.
- pub fn from_repositories(repositories: &[RawRepository]) -> Self {
- Self {
- id: "package".to_string(),
- packages: collect_inline_packages(repositories),
- }
- }
-
- pub fn package_count(&self) -> usize {
- self.packages.len()
- }
-}
-
-#[async_trait::async_trait]
-impl Repository for InlinePackageRepository {
- fn id(&self) -> &str {
- &self.id
- }
-
- async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> {
- let mut result = LoadResult::default();
- for query in queries {
- let mut found_any = false;
- for ipkg in &self.packages {
- if ipkg.name == query.name {
- found_any = true;
- result.packages.push(NamedPackagistVersion {
- name: ipkg.name.clone(),
- version: ipkg.version.clone(),
- });
- }
- }
- if found_any {
- result.names_found.push(query.name.to_string());
- }
- }
- Ok(result)
- }
-}
diff --git a/crates/mozart-registry/src/repository/mod.rs b/crates/mozart-registry/src/repository/mod.rs
deleted file mode 100644
index 46f62f0..0000000
--- a/crates/mozart-registry/src/repository/mod.rs
+++ /dev/null
@@ -1,319 +0,0 @@
-//! Repository abstraction over package metadata sources.
-//!
-//! Mirrors Composer's `Composer\Repository\RepositoryInterface::loadPackages`
-//! and `Composer\Repository\RepositoryManager`. The resolver and lockfile
-//! generator query a [`RepositorySet`] instead of calling Packagist directly,
-//! so test code can substitute a set without `PackagistRepository` (mirroring
-//! Composer's `FactoryMock` injecting `repositories: ['packagist' => false]`).
-//!
-//! Concrete implementations live in sibling modules: [`packagist_repo`] for
-//! the live Packagist HTTP repo, [`inline_package_repo`] for `type: package`
-//! entries embedded in `composer.json`, and [`vcs_repo`] for VCS repositories.
-
-use std::collections::BTreeMap;
-
-use crate::advisory::{MatchedAdvisory, PackageInfo};
-use crate::packagist::{PackagistVersion, SearchResult};
-
-pub mod inline_package_repo;
-pub mod packagist_repo;
-pub mod vcs_repo;
-
-/// Search modes for [`Repository::search`].
-///
-/// Mirrors Composer's `RepositoryInterface::SEARCH_FULLTEXT|SEARCH_NAME|SEARCH_VENDOR`
-/// constants (`composer/src/Composer/Repository/RepositoryInterface.php`).
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum SearchMode {
- /// Full-text search over name, description, and keywords (Packagist's
- /// `search.json` API).
- Fulltext,
- /// Match the regex against package names. Tokens are split on whitespace
- /// and joined as `(?:t1|t2|...)`; callers must pre-quote regex metachars.
- Name,
- /// Match the regex against vendor names. Result rows have only `name`
- /// populated (the vendor part).
- Vendor,
-}
-
-/// One name-keyed lookup against a repository.
-///
-/// Matches the `$packageNameMap` argument of Composer's `loadPackages`. The
-/// constraint is informational — repositories may use it to skip versions
-/// that obviously can't match (an optimization), but the resolver still
-/// re-checks every returned version when generating rules.
-#[derive(Debug, Clone)]
-pub struct PackageQuery<'a> {
- pub name: &'a str,
- /// Raw constraint string from `composer.json`, e.g. `"^1.2"`. `None`
- /// when the caller wants every version (transitive exploration).
- pub constraint: Option<&'a str>,
-}
-
-/// Result of a single [`Repository::load_packages`] call.
-///
-/// Mirrors Composer's `['packages' => ..., 'namesFound' => ...]` tuple.
-/// `names_found` lets [`RepositorySet`] short-circuit lower-priority repos
-/// once an upstream repo has authoritatively answered for a name (Composer's
-/// "first repo wins" semantics).
-#[derive(Debug, Default)]
-pub struct LoadResult {
- pub packages: Vec<NamedPackagistVersion>,
- pub names_found: Vec<String>,
-}
-
-/// A `PackagistVersion` paired with the canonical package name it answers
-/// for. Inline `type: package` repos can return packages whose own `name`
-/// field differs from the queried name when they declare `replace`/`provide`,
-/// so callers need both.
-#[derive(Debug, Clone)]
-pub struct NamedPackagistVersion {
- pub name: String,
- pub version: PackagistVersion,
-}
-
-/// A source of package metadata. Mirrors Composer's `RepositoryInterface`.
-///
-/// Implementations should return an empty [`LoadResult`] (not an error) when
-/// they simply don't know a queried name — [`RepositorySet`] uses that to
-/// fall through to the next repo. Reserve `Err` for genuine I/O failures
-/// the caller cannot route around.
-#[async_trait::async_trait]
-pub trait Repository: Send + Sync {
- /// Identifier for diagnostics (`"packagist.org"`, `"package"`, `"vcs:<url>"`).
- fn id(&self) -> &str;
-
- /// Look up every version of every queried name this repo knows about.
- async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult>;
-
- /// Search this repository.
- ///
- /// The default returns an empty result so repositories that don't
- /// participate in search (e.g. inline / VCS repos that only resolve
- /// known names) can opt out. Mirrors Composer's
- /// `RepositoryInterface::search` whose default behavior on
- /// `ArrayRepository` walks the in-memory list.
- async fn search(
- &self,
- _query: &str,
- _mode: SearchMode,
- _package_type: Option<&str>,
- ) -> anyhow::Result<Vec<SearchResult>> {
- Ok(Vec::new())
- }
-}
-
-/// Ordered list of repositories. Mirrors `Composer\Repository\RepositoryManager`.
-///
-/// `load_packages` queries each repo in order. Once a repo authoritatively
-/// answers for a name (i.e. lists it in `names_found`), later repos are not
-/// asked about that name — matching Composer's first-repo-wins priority.
-pub struct RepositorySet {
- repos: Vec<Box<dyn Repository>>,
-}
-
-impl RepositorySet {
- pub fn new(repos: Vec<Box<dyn Repository>>) -> Self {
- Self { repos }
- }
-
- /// Production default: a single [`packagist_repo::PackagistRepository`]
- /// backed by the given on-disk cache. Mirrors what Composer does when
- /// no `'packagist' => false` entry appears in the merged config.
- pub fn with_packagist(repo_cache: crate::cache::Cache) -> Self {
- Self::new(vec![Box::new(packagist_repo::PackagistRepository::new(
- repo_cache,
- ))])
- }
-
- /// An empty set. Mirrors Composer's `'packagist' => false` test config:
- /// resolution proceeds entirely from packages already in the pool
- /// (eager VCS scan, inline `type: package` repos, the locked repository).
- pub fn empty() -> Self {
- Self::new(Vec::new())
- }
-
- pub fn is_empty(&self) -> bool {
- self.repos.is_empty()
- }
-
- pub fn len(&self) -> usize {
- self.repos.len()
- }
-
- /// Iterate over repositories in priority order.
- pub fn repos(&self) -> impl Iterator<Item = &dyn Repository> {
- self.repos.iter().map(|b| b.as_ref())
- }
-
- /// Query every repo, accumulating packages and tracking which names have
- /// been authoritatively answered. Names already covered by an earlier
- /// repo are dropped from the query passed to later repos.
- pub async fn load_packages(
- &self,
- queries: &[PackageQuery<'_>],
- ) -> anyhow::Result<Vec<NamedPackagistVersion>> {
- use indexmap::IndexSet;
-
- let mut packages: Vec<NamedPackagistVersion> = Vec::new();
- let mut answered: IndexSet<String> = IndexSet::new();
-
- for repo in &self.repos {
- let pending: Vec<PackageQuery<'_>> = queries
- .iter()
- .filter(|q| !answered.contains(q.name))
- .cloned()
- .collect();
- if pending.is_empty() {
- break;
- }
- let result = repo.load_packages(&pending).await?;
- for name in result.names_found {
- answered.insert(name);
- }
- packages.extend(result.packages);
- }
-
- Ok(packages)
- }
-
- /// Fan-out search across every repository, concatenating results in
- /// priority order. Mirrors Composer's
- /// `CompositeRepository::search` which `array_merge`s per-repo results
- /// without de-duplication.
- pub async fn search(
- &self,
- query: &str,
- mode: SearchMode,
- package_type: Option<&str>,
- ) -> anyhow::Result<Vec<SearchResult>> {
- let mut all = Vec::new();
- for repo in &self.repos {
- let mut hits = repo.search(query, mode, package_type).await?;
- all.append(&mut hits);
- }
- Ok(all)
- }
-
- /// Fetch security advisories matching the installed packages, with version filtering.
- ///
- /// Mirrors `Composer\Repository\RepositorySet::getMatchingSecurityAdvisories()`.
- /// Returns the matched advisories (already filtered by installed version) and a list
- /// of unreachable repository URLs. When `ignore_unreachable` is false and a repository
- /// is unreachable, the error is propagated instead.
- pub async fn get_matching_security_advisories(
- &self,
- packages: &[PackageInfo],
- _allow_partial: bool,
- ignore_unreachable: bool,
- ) -> anyhow::Result<(BTreeMap<String, Vec<MatchedAdvisory>>, Vec<String>)> {
- let names: Vec<&str> = packages.iter().map(|p| p.name.as_str()).collect();
-
- let (raw_advisories, unreachable_repos) =
- match crate::packagist::fetch_security_advisories(&names).await {
- Ok(a) => (a, vec![]),
- Err(e) if ignore_unreachable => {
- tracing::warn!("Packagist advisory fetch failed (ignored): {e}");
- let unreachable = vec!["https://packagist.org".to_string()];
- (BTreeMap::new(), unreachable)
- }
- Err(e) => return Err(e),
- };
-
- let matched = version_filter_advisories(&raw_advisories, packages);
-
- Ok((matched, unreachable_repos))
- }
-}
-
-/// Normalize single-pipe OR separators (`|`) in a version constraint string to
-/// double-pipe (`||`) so the constraint parser can handle both forms.
-///
-/// The Packagist security advisories API may return constraints with single `|`
-/// as the OR separator (e.g. `>=1.0,<1.5|>=2.0,<2.3`), but Mozart's
-/// `VersionConstraint::parse` expects `||`.
-///
-/// TODO: fix `mozart_semver::VersionConstraint::parse` to accept single `|` and remove this.
-fn normalize_or_separator(constraint: &str) -> String {
- let bytes = constraint.as_bytes();
- let mut result = String::with_capacity(constraint.len() + 4);
- let mut i = 0;
- while i < bytes.len() {
- if bytes[i] == b'|' {
- if i + 1 < bytes.len() && bytes[i + 1] == b'|' {
- result.push_str("||");
- i += 2;
- } else {
- result.push_str("||");
- i += 1;
- }
- } else {
- result.push(bytes[i] as char);
- i += 1;
- }
- }
- result
-}
-
-/// Filter raw advisories by installed package versions.
-///
-/// Mirrors the version-matching step inside Composer's repository advisory fetch.
-fn version_filter_advisories(
- all_advisories: &BTreeMap<String, Vec<crate::packagist::SecurityAdvisory>>,
- packages: &[PackageInfo],
-) -> BTreeMap<String, Vec<MatchedAdvisory>> {
- let mut result: BTreeMap<String, Vec<MatchedAdvisory>> = BTreeMap::new();
-
- for pkg in packages {
- let Some(advisories) = all_advisories.get(&pkg.name) else {
- continue;
- };
-
- let version_str = pkg
- .version_normalized
- .as_deref()
- .unwrap_or(pkg.version.as_str());
-
- let installed_ver = match mozart_semver::Version::parse(version_str) {
- Ok(v) => v,
- Err(_) => {
- tracing::warn!(
- "Could not parse version {:?} for package {:?}, skipping advisory matching",
- version_str,
- pkg.name
- );
- continue;
- }
- };
-
- let mut matched: Vec<MatchedAdvisory> = Vec::new();
-
- for advisory in advisories {
- let normalized = normalize_or_separator(&advisory.affected_versions);
- let constraint = match mozart_semver::VersionConstraint::parse(&normalized) {
- Ok(c) => c,
- Err(_) => {
- tracing::warn!(
- "Could not parse affected versions {:?} for advisory {:?}, skipping",
- advisory.affected_versions,
- advisory.advisory_id
- );
- continue;
- }
- };
-
- if constraint.matches(&installed_ver) {
- matched.push(MatchedAdvisory {
- advisory: advisory.clone(),
- installed_version: pkg.version.clone(),
- });
- }
- }
-
- if !matched.is_empty() {
- result.insert(pkg.name.clone(), matched);
- }
- }
-
- result
-}
diff --git a/crates/mozart-registry/src/repository/packagist_repo.rs b/crates/mozart-registry/src/repository/packagist_repo.rs
deleted file mode 100644
index fa656b7..0000000
--- a/crates/mozart-registry/src/repository/packagist_repo.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-//! [`Repository`] backed by the live Packagist HTTP API.
-//!
-//! Wraps the existing [`crate::packagist::fetch_package_versions`] so the
-//! resolver sees the same data either through this trait or via the legacy
-//! direct call. Construction takes ownership of the [`Cache`] handle so
-//! callers no longer thread it through `ResolveRequest` / `LockFileGenerationRequest`.
-
-use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository, SearchMode};
-use crate::cache::Cache;
-use crate::packagist;
-use crate::packagist::SearchResult;
-
-pub struct PackagistRepository {
- id: String,
- cache: Cache,
-}
-
-impl PackagistRepository {
- pub fn new(cache: Cache) -> Self {
- Self {
- id: "packagist.org".to_string(),
- cache,
- }
- }
-}
-
-#[async_trait::async_trait]
-impl Repository for PackagistRepository {
- fn id(&self) -> &str {
- &self.id
- }
-
- async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> {
- let mut result = LoadResult::default();
- for query in queries {
- // Errors propagate to the caller. Composer's
- // `ComposerRepository::loadAsyncPackages` distinguishes 404
- // (empty result, no error) from transport failures (exception);
- // Mozart's underlying `fetch_package_versions` doesn't yet make
- // that distinction, so for now both surface as `Err` and the
- // caller decides whether the loop wants to continue (transitive
- // exploration) or abort (seed-time fetch failure).
- let versions = packagist::fetch_package_versions(query.name, &self.cache).await?;
- // A successful fetch counts as "this repo authoritatively knows
- // the name", even if the version list is empty — mirrors
- // Composer's `ArrayRepository::loadPackages` which adds the
- // name to `namesFound` regardless of constraint match.
- result.names_found.push(query.name.to_string());
- for version in versions {
- result.packages.push(NamedPackagistVersion {
- name: query.name.to_string(),
- version,
- });
- }
- }
- Ok(result)
- }
-
- async fn search(
- &self,
- query: &str,
- mode: SearchMode,
- package_type: Option<&str>,
- ) -> anyhow::Result<Vec<SearchResult>> {
- match mode {
- SearchMode::Fulltext => {
- let (results, _total) = packagist::search_packages(query, package_type).await?;
- Ok(results)
- }
- SearchMode::Name => {
- let pattern = build_name_regex(query)?;
- let names = packagist::fetch_package_names(package_type, &self.cache).await?;
- Ok(names
- .into_iter()
- .filter(|name| pattern.is_match(name))
- .map(empty_search_result)
- .collect())
- }
- SearchMode::Vendor => {
- let pattern = build_name_regex(query)?;
- let vendors = packagist::fetch_vendor_names(&self.cache).await?;
- Ok(vendors
- .into_iter()
- .filter(|name| pattern.is_match(name))
- .map(empty_search_result)
- .collect())
- }
- }
- }
-}
-
-/// Build the case-insensitive `(?:t1|t2|...)` regex from whitespace-split
-/// tokens, mirroring Composer's `'{(?:'.implode('|', $matches).')}i'`.
-///
-/// Tokens are joined as-is — callers are expected to have already escaped
-/// regex metacharacters (`SearchCommand` calls `preg_quote`; Mozart calls
-/// `regex::escape` before reaching this point).
-fn build_name_regex(query: &str) -> anyhow::Result<regex::Regex> {
- let tokens: Vec<&str> = query.split_whitespace().collect();
- let body = if tokens.is_empty() {
- String::new()
- } else {
- tokens.join("|")
- };
- Ok(regex::Regex::new(&format!("(?i)(?:{body})"))?)
-}
-
-/// Build a [`SearchResult`] with only `name` populated, mirroring the shape
-/// Composer returns for `SEARCH_NAME` / `SEARCH_VENDOR` modes
-/// (`['name' => $name]`, all other fields `null`).
-fn empty_search_result(name: String) -> SearchResult {
- SearchResult {
- name,
- description: String::new(),
- url: String::new(),
- repository: None,
- downloads: 0,
- favers: 0,
- abandoned: None,
- }
-}
diff --git a/crates/mozart-registry/src/repository/vcs_repo.rs b/crates/mozart-registry/src/repository/vcs_repo.rs
deleted file mode 100644
index fff5f6f..0000000
--- a/crates/mozart-registry/src/repository/vcs_repo.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-//! [`Repository`] for VCS-type repositories.
-//!
-//! Wraps [`crate::vcs_bridge::scan_vcs_repositories`] + [`crate::vcs_bridge::vcs_to_packagist_version`].
-//! Scanning is expensive (clones / fetches), so we do it once at construction
-//! and serve subsequent queries from the in-memory cache. Mirrors
-//! `Composer\Repository\Vcs\VcsRepository`'s lazy-then-memoized behavior.
-
-use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository};
-use crate::packagist::PackagistVersion;
-use crate::vcs_bridge::{scan_vcs_repositories, vcs_to_packagist_version};
-use mozart_core::package::RawRepository;
-
-pub struct VcsRepository {
- id: String,
- versions: Vec<(String, PackagistVersion)>,
-}
-
-impl VcsRepository {
- /// Scan every VCS-type entry in `repositories` and cache the resulting
- /// versions. Non-VCS entries are ignored. This performs network I/O.
- pub async fn from_repositories(repositories: &[RawRepository]) -> Self {
- let scanned = scan_vcs_repositories(repositories).await;
- let versions = scanned
- .iter()
- .map(|v| (v.name.clone(), vcs_to_packagist_version(v)))
- .collect();
- Self {
- id: "vcs".to_string(),
- versions,
- }
- }
-
- pub fn version_count(&self) -> usize {
- self.versions.len()
- }
-}
-
-#[async_trait::async_trait]
-impl Repository for VcsRepository {
- fn id(&self) -> &str {
- &self.id
- }
-
- async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> {
- let mut result = LoadResult::default();
- for query in queries {
- let mut found_any = false;
- for (name, version) in &self.versions {
- if name == query.name {
- found_any = true;
- result.packages.push(NamedPackagistVersion {
- name: name.clone(),
- version: version.clone(),
- });
- }
- }
- if found_any {
- result.names_found.push(query.name.to_string());
- }
- }
- Ok(result)
- }
-}