Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Summarise plugins list #2662

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 145 additions & 2 deletions src/commands/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ fn list_installed_plugins() -> Result<Vec<PluginDescriptor>> {
installed: true,
compatibility: PluginCompatibility::for_current(&m),
manifest: m,
installed_version: None,
})
.collect();
Ok(descriptors)
Expand All @@ -458,6 +459,7 @@ async fn list_catalogue_plugins() -> Result<Vec<PluginDescriptor>> {
installed: m.is_installed_in(store),
compatibility: PluginCompatibility::for_current(&m),
manifest: m,
installed_version: None,
})
.collect();
Ok(descriptors)
Expand All @@ -469,13 +471,81 @@ async fn list_catalogue_and_installed_plugins() -> Result<Vec<PluginDescriptor>>
Ok(merge_plugin_lists(catalogue, installed))
}

fn summarise(all_plugins: Vec<PluginDescriptor>) -> Vec<PluginDescriptor> {
use itertools::Itertools;

let names_to_versions = all_plugins
.into_iter()
.into_group_map_by(|pd| pd.name.clone());
names_to_versions
.into_values()
.flat_map(|versions| {
let (latest, rest) = latest_and_rest(versions);
let Some(mut latest) = latest else {
// We can't parse things well enough to summarise: return all versions.
return rest;
};
if latest.installed {
// The installed is the latest: return it.
return vec![latest];
}

let installed = rest.into_iter().find(|pd| pd.installed);
let Some(installed) = installed else {
// No installed version: return the latest.
return vec![latest];
};

// If we get here then there is an installed version which is not the latest.
// Mark the latest as installed (representing, in this case, that the plugin
// is installed, even though this version isn't), and record what version _is_
// installed.
latest.installed = true;
latest.installed_version = Some(installed.version);
vec![latest]
})
.collect()
}

/// Given a list of plugin descriptors, this looks for the one with the latest version.
/// If it can determine a latest version, it returns a tuple where the first element is
/// the latest version, and the second is the remaining versions (order not preserved).
/// Otherwise it returns None and the original list.
fn latest_and_rest(
mut plugins: Vec<PluginDescriptor>,
) -> (Option<PluginDescriptor>, Vec<PluginDescriptor>) {
// `versions` is the parsed version of each plugin in the vector, in the same order.
// We rely on this 1-1 order-preserving behaviour as we are going to calculate
// an index from `versions` and use it to index into `plugins`.
let Ok(versions) = plugins
.iter()
.map(|pd| semver::Version::parse(&pd.version))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd hate to make this more complicated, but perhaps we want to treat unparseable versions as not possibly being the latest but still try with any versions that do successfully parse.

.collect::<Result<Vec<_>, _>>()
else {
return (None, plugins);
};
let Some((latest_index, _)) = versions.iter().enumerate().max_by_key(|(_, v)| *v) else {
return (None, plugins);
};
let pd = plugins.swap_remove(latest_index);
(Some(pd), plugins)
}

/// List available or installed plugins.
#[derive(Parser, Debug)]
pub struct List {
/// List only installed plugins.
#[clap(long = "installed", takes_value = false)]
#[clap(long = "installed", takes_value = false, group = "which")]
pub installed: bool,

/// List all versions of plugins. This is the default behaviour.
#[clap(long = "all", takes_value = false, group = "which")]
pub all: bool,

/// List latest and installed versions of plugins.
#[clap(long = "summary", takes_value = false, group = "which")]
pub summary: bool,

/// Filter the list to plugins containing this string.
#[clap(long = "filter")]
pub filter: Option<String>,
Expand All @@ -489,6 +559,10 @@ impl List {
list_catalogue_and_installed_plugins().await
}?;

if self.summary {
plugins = summarise(plugins);
}

plugins.sort_by(|p, q| p.cmp(q));

if let Some(filter) = self.filter.as_ref() {
Expand All @@ -504,7 +578,15 @@ impl List {
println!("No plugins found");
} else {
for p in plugins {
let installed = if p.installed { " [installed]" } else { "" };
let installed = if p.installed {
if let Some(installed) = p.installed_version.as_ref() {
format!(" [installed version: {installed}]")
} else {
" [installed]".to_string()
}
} else {
"".to_string()
};
let compat = match &p.compatibility {
PluginCompatibility::Compatible => String::new(),
PluginCompatibility::IncompatibleSpin(v) => format!(" [requires Spin {v}]"),
Expand All @@ -527,6 +609,8 @@ impl Search {
async fn run(&self) -> anyhow::Result<()> {
let list_cmd = List {
installed: false,
all: true,
summary: false,
filter: self.filter.clone(),
};

Expand Down Expand Up @@ -563,6 +647,7 @@ struct PluginDescriptor {
compatibility: PluginCompatibility,
installed: bool,
manifest: PluginManifest,
installed_version: Option<String>, // only in "latest" mode and if installed version is not latest
}

impl PluginDescriptor {
Expand Down Expand Up @@ -701,3 +786,61 @@ async fn try_install(
Ok(false)
}
}

#[cfg(test)]
mod test {
use super::*;

fn dummy_descriptor(version: &str) -> PluginDescriptor {
use serde::Deserialize;
PluginDescriptor {
name: "dummy".into(),
version: version.into(),
compatibility: PluginCompatibility::Compatible,
installed: false,
manifest: PluginManifest::deserialize(serde_json::json!({
"name": "dummy",
"version": version,
"spinCompatibility": ">= 0.1",
"license": "dummy",
"packages": []
}))
.unwrap(),
installed_version: None,
}
}

#[test]
fn latest_and_rest_if_empty_returns_no_latest_rest_empty() {
let (latest, rest) = latest_and_rest(vec![]);
assert!(latest.is_none());
assert_eq!(0, rest.len());
}

#[test]
fn latest_and_rest_if_invalid_ver_returns_no_latest_all_rest() {
let (latest, rest) = latest_and_rest(vec![
dummy_descriptor("1.2.3"),
dummy_descriptor("spork"),
dummy_descriptor("1.3.5"),
]);
assert!(latest.is_none());
assert_eq!(3, rest.len());
}

#[test]
fn latest_and_rest_if_valid_ver_returns_latest_and_rest() {
let (latest, rest) = latest_and_rest(vec![
dummy_descriptor("1.2.3"),
dummy_descriptor("2.4.6"),
dummy_descriptor("1.3.5"),
]);
let latest = latest.expect("should have found a latest");
assert_eq!("2.4.6", latest.version);

assert_eq!(2, rest.len());
let rest_vers: std::collections::HashSet<_> = rest.into_iter().map(|p| p.version).collect();
assert!(rest_vers.contains("1.2.3"));
assert!(rest_vers.contains("1.3.5"));
}
}
Loading