use std::sync::Arc;
use anyhow::Result;
use clap::ArgMatches;
use console::style;
use indicatif::ProgressBar;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use tokio::time::Instant;
use crate::app;
use crate::constants::MAX_CONCURRENT_REQUESTS;
use crate::filter::Filter;
use crate::json::UpdateJsonResult;
use crate::model::{CheckResult, CommandResult, Ecosystem, UpdateResult};
use crate::output;
use crate::progress;
pub async fn update(matches: &ArgMatches) -> Result<bool> {
let path = app::path_argument(matches);
let json = app::is_json(matches);
let dry_run = matches.get_flag("dry-run");
let filter = Filter::from_matches(matches);
let instant = Instant::now();
let root = path.canonicalize().unwrap_or_else(|_| path.clone());
let (do_maven, do_npm) = super::pipeline::detect_ecosystems(&filter, &root);
let (check_results, npm_projects) =
crate::command::pipeline::resolve_versions(&root, do_maven, do_npm, filter.stable, json)
.await?;
let outdated: Vec<CheckResult> = check_results
.into_iter()
.filter(|r| r.is_outdated() && filter.matches(r))
.collect();
if outdated.is_empty() {
if json {
println!("[]");
} else {
println!("{}", style("All dependencies are up to date.").green());
}
return Ok(false);
}
if dry_run {
if json {
let json_results: Vec<UpdateJsonResult> = outdated
.iter()
.map(UpdateJsonResult::would_update)
.collect();
output::print_json(&json_results);
} else {
println!();
println!("{}", style("Dry run \u{2014} no changes made:").bold());
let preview: Vec<UpdateResult> = outdated
.iter()
.map(|r| UpdateResult::updated(r, r.latest_version().unwrap_or("?").to_string()))
.collect();
output::print_table(&preview, "", output::update_summary);
progress::done(instant);
}
return Ok(false);
}
let mut all_results: Vec<UpdateResult> = Vec::new();
let maven_outdated: Vec<CheckResult> = outdated
.iter()
.filter(|r| r.ecosystem() == Ecosystem::Maven)
.cloned()
.collect();
let npm_outdated: Vec<&CheckResult> = outdated
.iter()
.filter(|r| r.ecosystem() == Ecosystem::Npm)
.collect();
let maven_pom_count = maven_outdated
.iter()
.map(|r| r.source())
.collect::<std::collections::HashSet<_>>()
.len();
let npm_project_count = count_npm_projects_with_outdated(&npm_projects, &root, &npm_outdated);
let total = maven_pom_count + npm_project_count;
let bar = if json || total == 0 {
ProgressBar::hidden()
} else {
progress::bar(total as u64)
};
if !maven_outdated.is_empty() {
let maven_results = crate::maven::updater::apply_updates(&root, &maven_outdated, &bar)?;
all_results.extend(maven_results);
}
if !npm_outdated.is_empty() {
let npm_results = run_npm_updates(&npm_projects, &root, &npm_outdated, &bar).await;
all_results.extend(npm_results);
}
bar.finish_and_clear();
if json {
let json_results: Vec<UpdateJsonResult> =
all_results.iter().map(UpdateJsonResult::from).collect();
output::print_json(&json_results);
} else {
println!();
output::print_table(&all_results, "", output::update_summary);
progress::done(instant);
}
Ok(all_results.iter().any(|r| r.is_error()))
}
fn match_npm_projects<'a>(
projects: &'a [crate::npm::discovery::NpmProject],
root: &std::path::Path,
outdated: &[&CheckResult],
) -> Vec<(&'a crate::npm::discovery::NpmProject, Vec<CheckResult>)> {
projects
.iter()
.filter_map(|p| {
let project_source = p
.path
.strip_prefix(root)
.unwrap_or(&p.path)
.join("package.json")
.display()
.to_string();
let project_results: Vec<CheckResult> = outdated
.iter()
.filter(|r| r.source() == project_source)
.map(|r| (*r).clone())
.collect();
if project_results.is_empty() {
None
} else {
Some((p, project_results))
}
})
.collect()
}
fn count_npm_projects_with_outdated(
projects: &[crate::npm::discovery::NpmProject],
root: &std::path::Path,
outdated: &[&CheckResult],
) -> usize {
match_npm_projects(projects, root, outdated).len()
}
async fn run_npm_updates(
projects: &[crate::npm::discovery::NpmProject],
root: &std::path::Path,
outdated: &[&CheckResult],
bar: &ProgressBar,
) -> Vec<UpdateResult> {
let projects_with_outdated = match_npm_projects(projects, root, outdated);
if projects_with_outdated.is_empty() {
return Vec::new();
}
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));
let mut join_set = JoinSet::new();
for (project, project_results) in projects_with_outdated {
let project = project.clone();
let root = root.to_path_buf();
let semaphore = Arc::clone(&semaphore);
let bar = bar.clone();
join_set.spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
bar.set_message(format!("{} ({})", project.name, project.package_manager));
let result =
crate::npm::updater::update_project(&project, &root, &project_results).await;
bar.inc(1);
result
});
}
join_set.join_all().await.into_iter().flatten().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Dependency, DependencyKind, Ecosystem};
use crate::npm::PackageManager;
use crate::npm::discovery::NpmProject;
use std::path::PathBuf;
fn npm_result(name: &str, source: &str) -> CheckResult {
CheckResult::checked(
Dependency::new(
Ecosystem::Npm,
DependencyKind::NpmDep,
name.into(),
None,
source.into(),
),
"1.0.0".into(),
"2.0.0".into(),
true,
)
}
fn npm_project(root: &std::path::Path, subdir: &str) -> NpmProject {
NpmProject {
name: subdir.to_string(),
path: root.join(subdir),
package_manager: PackageManager::Npm,
pm_version: None,
}
}
#[test]
fn matches_outdated_to_correct_project() {
let root = PathBuf::from("/repo");
let projects = vec![npm_project(&root, "app-a"), npm_project(&root, "app-b")];
let r1 = npm_result("react", "app-a/package.json");
let r2 = npm_result("lodash", "app-b/package.json");
let outdated: Vec<&CheckResult> = vec![&r1, &r2];
let matched = match_npm_projects(&projects, &root, &outdated);
assert_eq!(matched.len(), 2);
assert_eq!(matched[0].0.name, "app-a");
assert_eq!(matched[0].1.len(), 1);
assert_eq!(matched[1].0.name, "app-b");
assert_eq!(matched[1].1.len(), 1);
}
#[test]
fn skips_projects_without_outdated() {
let root = PathBuf::from("/repo");
let projects = vec![npm_project(&root, "app-a"), npm_project(&root, "app-b")];
let r1 = npm_result("react", "app-a/package.json");
let outdated: Vec<&CheckResult> = vec![&r1];
let matched = match_npm_projects(&projects, &root, &outdated);
assert_eq!(matched.len(), 1);
assert_eq!(matched[0].0.name, "app-a");
}
#[test]
fn empty_outdated_returns_empty() {
let root = PathBuf::from("/repo");
let projects = vec![npm_project(&root, "app-a")];
let outdated: Vec<&CheckResult> = vec![];
let matched = match_npm_projects(&projects, &root, &outdated);
assert!(matched.is_empty());
}
#[test]
fn count_matches_len() {
let root = PathBuf::from("/repo");
let projects = vec![npm_project(&root, "app-a"), npm_project(&root, "app-b")];
let r1 = npm_result("react", "app-a/package.json");
let outdated: Vec<&CheckResult> = vec![&r1];
assert_eq!(
count_npm_projects_with_outdated(&projects, &root, &outdated),
1
);
}
}