Refactor: Introduce resolve_packages for wildcard handling and enhance repository package resolution

- Added `resolve_packages` to centralize wildcard pattern processing and match the latest package versions consistently.
- Updated both `FileBackend` and `RestBackend` to support wildcard patterns via `glob_to_regex` logic.
- Made `glob_to_regex` public to facilitate regex conversion for glob patterns.
- Improved `catalog_manager` handling in `RestBackend` by explicitly loading catalog parts to ensure accurate package matching.
- Replaced redundant FMRI parsing logic with `resolve_packages` for cleaner and more maintainable code.
This commit is contained in:
Till Wegmueller 2026-02-06 00:04:23 +01:00
parent f8068364bc
commit e4f49cd7c8
No known key found for this signature in database
3 changed files with 74 additions and 20 deletions

View file

@ -120,7 +120,7 @@ fn parse_query(query: &str) -> SearchQuery {
} }
} }
fn glob_to_regex(pattern: &str) -> String { pub fn glob_to_regex(pattern: &str) -> String {
let mut regex = String::from("^"); let mut regex = String::from("^");
for c in pattern.chars() { for c in pattern.chars() {
match c { match c {
@ -1000,7 +1000,7 @@ impl ReadableRepository for FileBackend {
self.find_manifests_recursive( self.find_manifests_recursive(
&publisher_pkg_dir, &publisher_pkg_dir,
&pub_name, &pub_name,
pattern, pattern.map(glob_to_regex).as_deref(),
&mut packages, &mut packages,
)?; )?;
} }

View file

@ -1218,7 +1218,7 @@ impl RestBackend {
.join(&pub_name) .join(&pub_name)
}; };
let catalog_manager = self.get_catalog_manager(&pub_name)?; let mut catalog_manager = self.get_catalog_manager(&pub_name)?;
let attrs_path = cache_path.join("catalog.attrs"); let attrs_path = cache_path.join("catalog.attrs");
let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| { let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| {
@ -1238,6 +1238,11 @@ impl RestBackend {
let mut seen_fmris = HashSet::new(); let mut seen_fmris = HashSet::new();
for part_name in parts.keys() { for part_name in parts.keys() {
// Load part explicitly because CatalogManager doesn't load them automatically
catalog_manager.load_part(part_name).map_err(|e| {
RepositoryError::Other(format!("Failed to load catalog part {}: {}", part_name, e))
})?;
if let Some(part) = catalog_manager.get_part(part_name) { if let Some(part) = catalog_manager.get_part(part_name) {
// Match stems against pattern // Match stems against pattern
for (publisher_in_catalog, stems) in &part.packages { for (publisher_in_catalog, stems) in &part.packages {
@ -1248,16 +1253,13 @@ impl RestBackend {
for (stem, versions) in stems { for (stem, versions) in stems {
let matches = if pattern == "*" { let matches = if pattern == "*" {
true true
} else if pattern.contains('*') { } else {
// Basic glob matching (stem matching pattern) let re_str = super::file_backend::glob_to_regex(pattern);
let re_pattern = pattern.replace('*', ".*"); if let Ok(re) = regex::Regex::new(&re_str) {
if let Ok(re) = regex::Regex::new(&format!("^{}$", re_pattern)) {
re.is_match(stem) re.is_match(stem)
} else { } else {
stem == pattern stem == pattern
} }
} else {
stem == pattern
}; };
if matches { if matches {

View file

@ -6,7 +6,7 @@ use libips::repository::{
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use std::path::PathBuf; use std::path::PathBuf;
use tracing::info; use tracing::{info, warn};
use tracing_subscriber::{EnvFilter, fmt}; use tracing_subscriber::{EnvFilter, fmt};
struct ConsoleProgressReporter; struct ConsoleProgressReporter;
@ -55,22 +55,15 @@ fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
// Open destination repository
// We'll open it inside each branch to avoid borrow checker issues with moves
let fmris: Vec<Fmri> = cli
.packages
.iter()
.map(|s| Fmri::parse(s))
.collect::<std::result::Result<Vec<_>, _>>()
.into_diagnostic()?;
let progress = ConsoleProgressReporter; let progress = ConsoleProgressReporter;
// Determine if source is a URL or a path and receive packages // Determine if source is a URL or a path and receive packages
if cli.source.starts_with("http://") || cli.source.starts_with("https://") { if cli.source.starts_with("http://") || cli.source.starts_with("https://") {
let source_repo = RestBackend::open(&cli.source).into_diagnostic()?; let source_repo = RestBackend::open(&cli.source).into_diagnostic()?;
let dest_repo = FileBackend::open(&cli.dest).into_diagnostic()?; let dest_repo = FileBackend::open(&cli.dest).into_diagnostic()?;
let fmris = resolve_packages(&source_repo, cli.publisher.as_deref(), &cli.packages)?;
let mut receiver = PackageReceiver::new(&source_repo, dest_repo); let mut receiver = PackageReceiver::new(&source_repo, dest_repo);
receiver = receiver.with_progress(&progress); receiver = receiver.with_progress(&progress);
receiver receiver
@ -79,6 +72,9 @@ fn main() -> Result<()> {
} else { } else {
let source_repo = FileBackend::open(&cli.source).into_diagnostic()?; let source_repo = FileBackend::open(&cli.source).into_diagnostic()?;
let dest_repo = FileBackend::open(&cli.dest).into_diagnostic()?; let dest_repo = FileBackend::open(&cli.dest).into_diagnostic()?;
let fmris = resolve_packages(&source_repo, cli.publisher.as_deref(), &cli.packages)?;
let mut receiver = PackageReceiver::new(&source_repo, dest_repo); let mut receiver = PackageReceiver::new(&source_repo, dest_repo);
receiver = receiver.with_progress(&progress); receiver = receiver.with_progress(&progress);
receiver receiver
@ -90,3 +86,59 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
fn resolve_packages<R: ReadableRepository>(
repo: &R,
default_publisher: Option<&str>,
packages: &[String],
) -> Result<Vec<Fmri>> {
let mut resolved_fmris = Vec::new();
for pkg_str in packages {
if pkg_str.contains('*') || pkg_str.contains('?') {
// It's a pattern, resolve it
info!("Resolving wildcard pattern: {}", pkg_str);
let matched = repo.list_packages(default_publisher, Some(pkg_str)).into_diagnostic()?;
if matched.is_empty() {
warn!("No packages matched pattern: {}", pkg_str);
}
// For each matched stem, we probably want the newest version if not specified.
// list_packages returns all versions. PackageReceiver::receive also handles
// FMRIs without versions by picking the newest.
// But list_packages returns full FMRIs. If the pattern matched multiple packages,
// we get all versions of all of them.
// To be consistent with IPS, if someone says "text/*", they usually want
// the latest version of everything that matches.
let mut latest_versions: std::collections::HashMap<String, Fmri> = std::collections::HashMap::new();
for pi in matched {
let entry = latest_versions.entry(pi.fmri.name.clone());
match entry {
std::collections::hash_map::Entry::Occupied(mut oe) => {
if pi.fmri.version() > oe.get().version() {
oe.insert(pi.fmri);
}
}
std::collections::hash_map::Entry::Vacant(ve) => {
ve.insert(pi.fmri);
}
}
}
for (_, fmri) in latest_versions {
info!("Found package: {}", fmri);
resolved_fmris.push(fmri);
}
} else {
// It's a regular FMRI or package name
let fmri = Fmri::parse(pkg_str).into_diagnostic()?;
resolved_fmris.push(fmri);
}
}
Ok(resolved_fmris)
}