Skip to content

Commit 41ec2a8

Browse files
committed
Summarise plugins list
Signed-off-by: itowlson <[email protected]>
1 parent 4f4693e commit 41ec2a8

File tree

1 file changed

+145
-2
lines changed

1 file changed

+145
-2
lines changed

src/commands/plugins.rs

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ fn list_installed_plugins() -> Result<Vec<PluginDescriptor>> {
437437
installed: true,
438438
compatibility: PluginCompatibility::for_current(&m),
439439
manifest: m,
440+
installed_version: None,
440441
})
441442
.collect();
442443
Ok(descriptors)
@@ -458,6 +459,7 @@ async fn list_catalogue_plugins() -> Result<Vec<PluginDescriptor>> {
458459
installed: m.is_installed_in(store),
459460
compatibility: PluginCompatibility::for_current(&m),
460461
manifest: m,
462+
installed_version: None,
461463
})
462464
.collect();
463465
Ok(descriptors)
@@ -469,13 +471,81 @@ async fn list_catalogue_and_installed_plugins() -> Result<Vec<PluginDescriptor>>
469471
Ok(merge_plugin_lists(catalogue, installed))
470472
}
471473

474+
fn summarise(all_plugins: Vec<PluginDescriptor>) -> Vec<PluginDescriptor> {
475+
use itertools::Itertools;
476+
477+
let names_to_versions = all_plugins
478+
.into_iter()
479+
.into_group_map_by(|pd| pd.name.clone());
480+
names_to_versions
481+
.into_values()
482+
.flat_map(|versions| {
483+
let (latest, rest) = latest_and_rest(versions);
484+
let Some(mut latest) = latest else {
485+
// We can't parse things well enough to summarise: return all versions.
486+
return rest;
487+
};
488+
if latest.installed {
489+
// The installed is the latest: return it.
490+
return vec![latest];
491+
}
492+
493+
let installed = rest.into_iter().find(|pd| pd.installed);
494+
let Some(installed) = installed else {
495+
// No installed version: return the latest.
496+
return vec![latest];
497+
};
498+
499+
// If we get here then there is an installed version which is not the latest.
500+
// Mark the latest as installed (representing, in this case, that the plugin
501+
// is installed, even though this version isn't), and record what version _is_
502+
// installed.
503+
latest.installed = true;
504+
latest.installed_version = Some(installed.version);
505+
vec![latest]
506+
})
507+
.collect()
508+
}
509+
510+
/// Given a list of plugin descriptors, this looks for the one with the latest version.
511+
/// If it can determine a latest version, it returns a tuple where the first element is
512+
/// the latest version, and the second is the remaining versions (order not preserved).
513+
/// Otherwise it returns None and the original list.
514+
fn latest_and_rest(
515+
mut plugins: Vec<PluginDescriptor>,
516+
) -> (Option<PluginDescriptor>, Vec<PluginDescriptor>) {
517+
// `versions` is the parsed version of each plugin in the vector, in the same order.
518+
// We rely on this 1-1 order-preserving behaviour as we are going to calculate
519+
// an index from `versions` and use it to index into `plugins`.
520+
let Ok(versions) = plugins
521+
.iter()
522+
.map(|pd| semver::Version::parse(&pd.version))
523+
.collect::<Result<Vec<_>, _>>()
524+
else {
525+
return (None, plugins);
526+
};
527+
let Some((latest_index, _)) = versions.iter().enumerate().max_by_key(|(_, v)| *v) else {
528+
return (None, plugins);
529+
};
530+
let pd = plugins.swap_remove(latest_index);
531+
(Some(pd), plugins)
532+
}
533+
472534
/// List available or installed plugins.
473535
#[derive(Parser, Debug)]
474536
pub struct List {
475537
/// List only installed plugins.
476-
#[clap(long = "installed", takes_value = false)]
538+
#[clap(long = "installed", takes_value = false, group = "which")]
477539
pub installed: bool,
478540

541+
/// List all versions of plugins. This is the default behaviour.
542+
#[clap(long = "all", takes_value = false, group = "which")]
543+
pub all: bool,
544+
545+
/// List latest and installed versions of plugins.
546+
#[clap(long = "summary", takes_value = false, group = "which")]
547+
pub summary: bool,
548+
479549
/// Filter the list to plugins containing this string.
480550
#[clap(long = "filter")]
481551
pub filter: Option<String>,
@@ -489,6 +559,10 @@ impl List {
489559
list_catalogue_and_installed_plugins().await
490560
}?;
491561

562+
if self.summary {
563+
plugins = summarise(plugins);
564+
}
565+
492566
plugins.sort_by(|p, q| p.cmp(q));
493567

494568
if let Some(filter) = self.filter.as_ref() {
@@ -504,7 +578,15 @@ impl List {
504578
println!("No plugins found");
505579
} else {
506580
for p in plugins {
507-
let installed = if p.installed { " [installed]" } else { "" };
581+
let installed = if p.installed {
582+
if let Some(installed) = p.installed_version.as_ref() {
583+
format!(" [installed version: {installed}]")
584+
} else {
585+
" [installed]".to_string()
586+
}
587+
} else {
588+
"".to_string()
589+
};
508590
let compat = match &p.compatibility {
509591
PluginCompatibility::Compatible => String::new(),
510592
PluginCompatibility::IncompatibleSpin(v) => format!(" [requires Spin {v}]"),
@@ -527,6 +609,8 @@ impl Search {
527609
async fn run(&self) -> anyhow::Result<()> {
528610
let list_cmd = List {
529611
installed: false,
612+
all: true,
613+
summary: false,
530614
filter: self.filter.clone(),
531615
};
532616

@@ -563,6 +647,7 @@ struct PluginDescriptor {
563647
compatibility: PluginCompatibility,
564648
installed: bool,
565649
manifest: PluginManifest,
650+
installed_version: Option<String>, // only in "latest" mode and if installed version is not latest
566651
}
567652

568653
impl PluginDescriptor {
@@ -701,3 +786,61 @@ async fn try_install(
701786
Ok(false)
702787
}
703788
}
789+
790+
#[cfg(test)]
791+
mod test {
792+
use super::*;
793+
794+
fn dummy_descriptor(version: &str) -> PluginDescriptor {
795+
use serde::Deserialize;
796+
PluginDescriptor {
797+
name: "dummy".into(),
798+
version: version.into(),
799+
compatibility: PluginCompatibility::Compatible,
800+
installed: false,
801+
manifest: PluginManifest::deserialize(serde_json::json!({
802+
"name": "dummy",
803+
"version": version,
804+
"spinCompatibility": ">= 0.1",
805+
"license": "dummy",
806+
"packages": []
807+
}))
808+
.unwrap(),
809+
installed_version: None,
810+
}
811+
}
812+
813+
#[test]
814+
fn latest_and_rest_if_empty_returns_no_latest_rest_empty() {
815+
let (latest, rest) = latest_and_rest(vec![]);
816+
assert!(latest.is_none());
817+
assert_eq!(0, rest.len());
818+
}
819+
820+
#[test]
821+
fn latest_and_rest_if_invalid_ver_returns_no_latest_all_rest() {
822+
let (latest, rest) = latest_and_rest(vec![
823+
dummy_descriptor("1.2.3"),
824+
dummy_descriptor("spork"),
825+
dummy_descriptor("1.3.5"),
826+
]);
827+
assert!(latest.is_none());
828+
assert_eq!(3, rest.len());
829+
}
830+
831+
#[test]
832+
fn latest_and_rest_if_valid_ver_returns_latest_and_rest() {
833+
let (latest, rest) = latest_and_rest(vec![
834+
dummy_descriptor("1.2.3"),
835+
dummy_descriptor("2.4.6"),
836+
dummy_descriptor("1.3.5"),
837+
]);
838+
let latest = latest.expect("should have found a latest");
839+
assert_eq!("2.4.6", latest.version);
840+
841+
assert_eq!(2, rest.len());
842+
let rest_vers: std::collections::HashSet<_> = rest.into_iter().map(|p| p.version).collect();
843+
assert!(rest_vers.contains("1.2.3"));
844+
assert!(rest_vers.contains("1.3.5"));
845+
}
846+
}

0 commit comments

Comments
 (0)