use crate::commands::Outcome;
use crate::manifest::{Manifest, ManifestKind};
use crate::package::Package;
use crate::registry::Registries;
use crate::workspace::{PackageGroups, Workspace};
use anyhow::Result;
use console::{Style, measure_text_width};
use semver::Version;
use std::cmp::Ordering;
use std::collections::HashMap;
use tokio::task::JoinSet;
pub async fn status(workspace: &Workspace) -> Result<Outcome> {
let fixed = workspace.fixed_groups()?;
let linked = workspace.linked_groups()?;
let registries = Registries::new(&workspace.config.registry, &workspace.config.http)?;
let mut latest = latest_versions(workspace, ®istries).await;
let packages = workspace
.packages
.iter()
.map(|package| PackageStatus {
name: package.name().to_string(),
scope: scope(package, &fixed, &linked),
manifests: package
.manifests()
.iter()
.map(|manifest| {
let lookup = latest.remove(&key(manifest));
ManifestStatus::new(package, manifest, ®istries, lookup)
})
.collect(),
})
.collect();
let report = Report {
title: workspace_title(workspace, &fixed, &linked),
packages,
};
print!("{}", report.render(console::colors_enabled()));
Ok(Outcome::Done)
}
type Key = (ManifestKind, String);
type Lookup = Result<Option<Version>>;
fn key(manifest: &Manifest) -> Key {
(manifest.kind(), manifest.name().to_string())
}
async fn latest_versions(workspace: &Workspace, registries: &Registries) -> HashMap<Key, Lookup> {
let mut lookups = JoinSet::new();
for package in &workspace.packages {
for manifest in package.publishable_manifests() {
let registries = registries.clone();
let key = key(manifest);
lookups.spawn(async move {
let latest = registries.latest(key.0, &key.1).await;
(key, latest)
});
}
}
let mut found = HashMap::new();
while let Some(lookup) = lookups.join_next().await {
if let Ok((key, latest)) = lookup {
found.insert(key, latest);
}
}
found
}
fn scope(package: &Package, fixed: &PackageGroups, linked: &PackageGroups) -> Option<String> {
let groups = [("fixed", fixed), ("linked", linked)];
let labels = groups
.iter()
.flat_map(|(kind, groups)| {
groups
.iter()
.enumerate()
.filter(|(_, members)| members.contains(package))
.map(move |(index, _)| format!("{kind} {}", index + 1))
})
.collect::<Vec<_>>();
(!labels.is_empty()).then(|| labels.join(", "))
}
fn workspace_title(workspace: &Workspace, fixed: &PackageGroups, linked: &PackageGroups) -> String {
let name = workspace.root.file_name().map_or_else(
|| workspace.root.display().to_string(),
|name| name.to_string_lossy().to_string(),
);
let mut parts = vec![name, count(workspace.packages.len(), "package")];
for (len, noun) in [(fixed.len(), "fixed group"), (linked.len(), "linked group")] {
if len > 0 {
parts.push(count(len, noun));
}
}
parts.join(" ยท ")
}
fn count(len: usize, noun: &str) -> String {
match len {
1 => format!("1 {noun}"),
_ => format!("{len} {noun}s"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Standing {
UpToDate,
Unreleased,
BehindRegistry,
NeverPublished,
CouldNotLoad,
Private,
}
impl Standing {
const ALL: [Self; 6] = [
Self::UpToDate,
Self::Unreleased,
Self::BehindRegistry,
Self::NeverPublished,
Self::CouldNotLoad,
Self::Private,
];
fn label(self) -> &'static str {
match self {
Self::UpToDate => "up to date",
Self::Unreleased => "unreleased",
Self::BehindRegistry => "behind registry",
Self::NeverPublished => "never published",
Self::CouldNotLoad => "could not load",
Self::Private => "private",
}
}
fn symbol(self) -> &'static str {
match self {
Self::UpToDate => "โ",
Self::Unreleased => "โ",
Self::BehindRegistry => "โ",
Self::NeverPublished => "+",
Self::CouldNotLoad => "โ",
Self::Private => "ยท",
}
}
fn style(self) -> Style {
let style = Style::new();
match self {
Self::UpToDate => style.green(),
Self::Unreleased => style.yellow(),
Self::BehindRegistry => style.magenta(),
Self::NeverPublished => style.cyan(),
Self::CouldNotLoad => style.red(),
Self::Private => style.dim(),
}
}
}
struct ManifestStatus {
name: String,
registry: &'static str,
version: Version,
latest: Option<Version>,
standing: Standing,
error: Option<String>,
}
impl ManifestStatus {
fn new(
package: &Package,
manifest: &Manifest,
registries: &Registries,
lookup: Option<Lookup>,
) -> Self {
let version = manifest.version();
let (latest, standing, error) = match lookup {
_ if !package.publishes(manifest) => (None, Standing::Private, None),
None => (
None,
Standing::CouldNotLoad,
Some("lookup did not finish".into()),
),
Some(Err(error)) => (None, Standing::CouldNotLoad, Some(format!("{error:#}"))),
Some(Ok(None)) => (None, Standing::NeverPublished, None),
Some(Ok(Some(latest))) => {
let standing = match version.cmp_precedence(&latest) {
Ordering::Equal => Standing::UpToDate,
Ordering::Greater => Standing::Unreleased,
Ordering::Less => Standing::BehindRegistry,
};
(Some(latest), standing, None)
}
};
Self {
name: manifest.name().to_string(),
registry: registries.name(manifest.kind()),
version: version.clone(),
latest,
standing,
error,
}
}
}
struct PackageStatus {
name: String,
scope: Option<String>,
manifests: Vec<ManifestStatus>,
}
struct Report {
title: String,
packages: Vec<PackageStatus>,
}
impl Report {
fn render(&self, colors: bool) -> String {
let paint = |text: &str, style: Style| style.force_styling(colors).apply_to(text).to_string();
let dim = Style::new().dim();
let mut table = Table::new(
[
"PACKAGE", "SCOPE", "MANIFEST", "REGISTRY", "VERSION", "LATEST", "STATUS",
],
Style::new().bold(),
);
let mut failures = vec![];
for status in &self.packages {
for (index, manifest) in status.manifests.iter().enumerate() {
let (package, scope) = match index {
0 => (
Cell::new(&status.name, Style::new().bold()),
match &status.scope {
Some(scope) => Cell::new(scope, Style::new().cyan()),
None => Cell::new("-", dim.clone()),
},
),
_ => (Cell::plain(""), Cell::plain("")),
};
let latest = match (&manifest.latest, manifest.standing) {
(Some(latest), _) => Cell::plain(&latest.to_string()),
(None, Standing::CouldNotLoad) => Cell::new("?", dim.clone()),
(None, _) => Cell::new("-", dim.clone()),
};
let standing = manifest.standing;
let mut label = format!("{} {}", standing.symbol(), standing.label());
if let Some(error) = &manifest.error {
failures.push((&manifest.name, error));
label.push_str(&format!(" [{}]", failures.len()));
}
table.push([
package,
scope,
Cell::plain(&manifest.name),
Cell::new(manifest.registry, dim.clone()),
Cell::plain(&manifest.version.to_string()),
latest,
Cell::new(&label, standing.style()),
]);
}
}
let mut out = format!("{}\n\n", paint(&self.title, Style::new().bold()));
out.push_str(&table.render(colors));
if !failures.is_empty() {
let red = Style::new().red();
out.push_str(&format!(
"\n{}\n",
paint("Could not load", red.clone().bold())
));
for (index, (name, error)) in failures.iter().enumerate() {
let reference = paint(&format!("[{}]", index + 1), red.clone());
out.push_str(&format!(" {reference} {name}: {error}\n"));
}
}
let summary = Standing::ALL
.iter()
.filter_map(|&standing| {
let count = self
.packages
.iter()
.flat_map(|status| &status.manifests)
.filter(|manifest| manifest.standing == standing)
.count();
(count > 0).then(|| paint(&format!("{count} {}", standing.label()), standing.style()))
})
.collect::<Vec<_>>();
out.push_str(&format!("\n{}\n", summary.join(&paint(" ยท ", dim))));
out
}
}
struct Cell {
text: String,
style: Style,
}
impl Cell {
fn new(text: &str, style: Style) -> Self {
Self {
text: text.to_string(),
style,
}
}
fn plain(text: &str) -> Self {
Self::new(text, Style::new())
}
}
struct Table<const N: usize> {
rows: Vec<[Cell; N]>,
}
impl<const N: usize> Table<N> {
fn new(header: [&str; N], style: Style) -> Self {
Self {
rows: vec![header.map(|title| Cell::new(title, style.clone()))],
}
}
fn push(&mut self, row: [Cell; N]) {
self.rows.push(row);
}
fn render(&self, colors: bool) -> String {
let mut widths = [0; N];
for row in &self.rows {
for (width, cell) in widths.iter_mut().zip(row) {
*width = (*width).max(measure_text_width(&cell.text));
}
}
let mut out = String::new();
for row in &self.rows {
let mut line = String::new();
for (column, (cell, width)) in row.iter().zip(widths).enumerate() {
line.push_str(
&cell
.style
.apply_to(&cell.text)
.force_styling(colors)
.to_string(),
);
if column + 1 < N {
let padding = width - measure_text_width(&cell.text) + 2;
line.push_str(&" ".repeat(padding));
}
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}
}