Skip to content

Commit

Permalink
feat/add search command (fix #230) (#244)
Browse files Browse the repository at this point in the history
* feat: add search command

* fix: remove duplicates packages and sorting of repodatarecords

* fix: add docs

* fix: search looks for substr before fuzzy find

* chore: fix clippy lint

* fix: reduce similarity for fuzzy search, prettify output

* feat: use channels from current pixi project, add flag to provide manifest path

* feat: add limit flag

* fix: make output line smaller, add desc latest package fetch logic

* fix: ignore project if channel passed explicitly
  • Loading branch information
Wackyator authored Aug 12, 2023
1 parent 6fcd570 commit 8a73d86
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ serde_spanned = "0.6.3"
serde_with = { version = "3.1.0", features = ["indexmap"] }
shlex = "1.1.0"
spdx = "0.10.2"
strsim = "0.10.0"
tempfile = "3.6.0"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "signal"] }
tokio-util = "0.7.8"
Expand Down
3 changes: 3 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod info;
pub mod init;
pub mod install;
pub mod run;
pub mod search;
pub mod shell;
pub mod task;
pub mod upload;
Expand Down Expand Up @@ -63,6 +64,7 @@ pub enum Command {
Task(task::Args),
Info(info::Args),
Upload(upload::Args),
Search(search::Args),
}

fn completion(args: CompletionCommand) -> miette::Result<()> {
Expand Down Expand Up @@ -165,6 +167,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> {
Command::Task(cmd) => task::execute(cmd),
Command::Info(cmd) => info::execute(cmd).await,
Command::Upload(cmd) => upload::execute(cmd).await,
Command::Search(cmd) => search::execute(cmd).await,
}
}

Expand Down
165 changes: 165 additions & 0 deletions src/cli/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use std::{cmp::Ordering, path::PathBuf};

use clap::Parser;
use itertools::Itertools;
use miette::IntoDiagnostic;
use rattler_conda_types::{Channel, ChannelConfig, Platform, RepoDataRecord};
use rattler_repodata_gateway::sparse::SparseRepoData;
use strsim::jaro;
use tokio::task::spawn_blocking;

use crate::{progress::await_in_progress, repodata::fetch_sparse_repodata, Project};

/// Search a package, output will list the latest version of package
#[derive(Debug, Parser)]
#[clap(arg_required_else_help = true)]
pub struct Args {
/// Name of package to search
#[arg(required = true)]
pub package: String,

/// Channel to specifically search package, defaults to
/// project channels or conda-forge
#[clap(short, long)]
channel: Option<Vec<String>>,

/// The path to 'pixi.toml'
#[arg(long)]
pub manifest_path: Option<PathBuf>,

/// Limit the number of search results
#[clap(short, long, default_value_t = 15)]
limit: usize,
}

/// fetch packages from `repo_data` based on `filter_func`
fn search_package_by_filter<F>(
package: &str,
repo_data: &[SparseRepoData],
filter_func: F,
) -> miette::Result<Vec<RepoDataRecord>>
where
F: Fn(&str, &str) -> bool,
{
let similar_packages = repo_data
.iter()
.flat_map(|repo| {
repo.package_names()
.filter(|&name| filter_func(name, package))
})
.collect::<Vec<&str>>();

let mut latest_packages = Vec::new();

// search for `similar_packages` in all platform's repodata
// add the latest version of the fetched package to latest_packages vector
for repo in repo_data {
for package in &similar_packages {
let mut records = repo.load_records(package).into_diagnostic()?;
// sort records by version, get the latest one
records.sort_by(|a, b| a.package_record.version.cmp(&b.package_record.version));
let latest_package = records.last().cloned();
if let Some(latest_package) = latest_package {
latest_packages.push(latest_package);
}
}
}

latest_packages = latest_packages
.into_iter()
.unique_by(|record| record.package_record.name.clone())
.collect::<Vec<_>>();

Ok(latest_packages)
}

pub async fn execute(args: Args) -> miette::Result<()> {
let project = Project::load_or_else_discover(args.manifest_path.as_deref()).ok();

let channel_config = ChannelConfig::default();

let channels = match (args.channel, project) {
// if user passes channels through the channel flag
(Some(c), _) => c
.iter()
.map(|c| Channel::from_str(c, &channel_config))
.collect::<Result<Vec<Channel>, _>>()
.into_diagnostic()?,
// if user doesn't pass channels and we are in a project
(None, Some(p)) => p.channels().to_owned(),
// if user doesn't pass channels and we are not in project
(None, None) => vec![Channel::from_str("conda-forge", &channel_config).into_diagnostic()?],
};

let limit = args.limit;
let package_name = args.package;
let platforms = [Platform::current()];
let repo_data = fetch_sparse_repodata(&channels, &platforms).await?;

let p = package_name.clone();
let mut packages = await_in_progress(
"searching packages",
spawn_blocking(move || {
let packages = search_package_by_filter(&p, &repo_data, |pn, n| pn.contains(n));
match packages {
Ok(packages) => {
if packages.is_empty() {
let similarity = 0.6;
return search_package_by_filter(&p, &repo_data, |pn, n| {
jaro(pn, n) > similarity
});
}
Ok(packages)
}
Err(e) => Err(e),
}
}),
)
.await
.into_diagnostic()??;

packages.sort_by(|a, b| {
let ord = jaro(&b.package_record.name, &package_name)
.partial_cmp(&jaro(&a.package_record.name, &package_name));
if let Some(ord) = ord {
ord
} else {
Ordering::Equal
}
});

if packages.is_empty() {
return Err(miette::miette!("Could not find {package_name}"));
}

// split off at `limit`, discard the second half
if packages.len() > limit {
let _ = packages.split_off(limit);
}

println!(
"{:40} {:19} {:19}",
console::style("Package").bold(),
console::style("Version").bold(),
console::style("Channel").bold(),
);
for package in packages {
// TODO: change channel fetch logic to be more robust
// currently it relies on channel field being a url with trailing slash
// https://github.com/mamba-org/rattler/issues/146
let channel = package.channel.split('/').collect::<Vec<_>>();
let channel_name = channel[channel.len() - 2];

let package_name = package.package_record.name;
let version = package.package_record.version.as_str();

println!(
"{:40} {:19} {:19}",
console::style(package_name).cyan().bright(),
console::style(version),
console::style(channel_name),
);
}

Ok(())
}

0 comments on commit 8a73d86

Please sign in to comment.