diff --git a/src/commands/install.rs b/src/commands/install.rs index 2fde3fb0..603c5902 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -1,22 +1,22 @@ use std::collections::BTreeSet; + use std::path::PathBuf; use std::time::Duration; -use anyhow::bail; -use crossterm::style::{Color, SetForegroundColor}; +use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor}; use indicatif::{ProgressBar, ProgressStyle}; -use indoc::indoc; + +use std::io::Write; use structopt::StructOpt; use crate::installation::InstallationContext; -use crate::lockfile::{LockPackage, Lockfile}; +use crate::lockfile::Lockfile; use crate::manifest::Manifest; use crate::package_id::PackageId; -use crate::package_source::{ - PackageSource, PackageSourceMap, Registry, TestRegistry, -}; +use crate::package_source::{PackageSource, PackageSourceMap, Registry, TestRegistry}; use crate::resolution::resolve; +use super::utils::{generate_depedency_changes, render_update_difference}; use super::GlobalOptions; /// Install all of the dependencies of this project. @@ -51,29 +51,64 @@ impl InstallSubcommand { let mut package_sources = PackageSourceMap::new(default_registry); package_sources.add_fallbacks()?; - let mut try_to_use = BTreeSet::new(); - for package in lockfile.packages { - match package { - LockPackage::Registry(registry_package) => { - try_to_use.insert(PackageId::new( - registry_package.name, - registry_package.version, - )); - } - LockPackage::Git(_) => {} + let try_to_use = lockfile.as_ids().collect(); + + let progress = ProgressBar::new(0).with_style( + ProgressStyle::with_template("{spinner:.cyan}{wide_msg}")?.tick_chars("⠁⠈⠐⠠⠄⠂ "), + ); + + progress.enable_steady_tick(Duration::from_millis(100)); + + if self.locked { + progress.println(format!( + "{} Verifying {}lockfile is up-to-date...", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + let latest_graph = resolve(&manifest, &BTreeSet::new(), &package_sources)?; + + if try_to_use != latest_graph.activated { + progress.finish_and_clear(); + + let old_dependencies = &try_to_use; + + let changes = generate_depedency_changes(old_dependencies, &latest_graph.activated); + let mut error_output = Vec::new(); + + writeln!( + error_output, + "{} The Lockfile is out of date and wasn't changed due to --locked{}", + SetForegroundColor(Color::Yellow), + SetForegroundColor(Color::Reset) + )?; + render_update_difference(&changes, &mut error_output)?; + writeln!( + error_output, + "{}{} Suggestion{}{} try running wally update", + SetAttribute(Attribute::Bold), + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset), + SetAttribute(Attribute::Reset) + )?; + + // Should be safe since we're only getting valid utf-8. + anyhow::bail!(String::from_utf8(error_output).unwrap()); } - } - let progress = ProgressBar::new(0) - .with_style( - ProgressStyle::with_template("{spinner:.cyan}{wide_msg}")?.tick_chars("⠁⠈⠐⠠⠄⠂ "), - ) - .with_message(format!( - "{} Resolving {}packages...", + progress.println(format!( + "{} Verified {}lockfile is up-to-date...{}", SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Green), SetForegroundColor(Color::Reset) )); - progress.enable_steady_tick(Duration::from_millis(100)); + } + + progress.println(format!( + "{} Resolving {}packages...", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); let resolved = resolve(&manifest, &try_to_use, &package_sources)?; @@ -84,77 +119,6 @@ impl InstallSubcommand { resolved.activated.len() - 1 )); - if self.locked && resolved.activated != try_to_use { - // We know at this point that these are not equal sets. - // Meaning, at least there was a new dependency being used or an old dependency that is no longer being used. - // We'll find either by taking the difference of the latest set of dependencies and the old set of dependencies. - let old_dependencies: Vec<_> = try_to_use.difference(&resolved.activated).collect(); - let latest_dependencies: Vec<_> = resolved.activated.difference(&try_to_use).collect(); - - // If a dependency name is present in both sets, then it was updated. - let updated_dependencies = { - let mut updated_ids = Vec::new(); - - for old_id in old_dependencies.iter() { - for new_id in latest_dependencies.iter() { - if old_id.name() == new_id.name() { - updated_ids.push((old_id, new_id)); - break; - } - } - } - - updated_ids - }; - - // If there is a dependency in the latest set, but not in the old set, then it is a new dependency. - let gained_dependencies: Vec<_> = latest_dependencies - .iter() - .filter(|new_id| { - !old_dependencies - .iter() - .any(|old_id| old_id.name() == new_id.name()) - }) - .collect(); - - // If there is a dependency in the old set, but not in the latest, then it's a dependency no longer used. - let lost_dependencies: Vec<_> = old_dependencies - .iter() - .filter(|old_id| { - !latest_dependencies - .iter() - .any(|new_id| new_id.name() == old_id.name()) - }) - .collect(); - - let mut formatted_result: String = updated_dependencies - .iter() - .map(|(old, new)| format!("Updated {} to {}\n", old, new)) - .chain( - gained_dependencies - .iter() - .map(|new| format!("Added {}\n", new)), - ) - .chain( - lost_dependencies - .iter() - .map(|old| format!("Removed {}\n", old)), - ) - .collect(); - - // There'll be an extra new-line at the end, we can pop it off for comestic reasons. - formatted_result.pop(); - - bail!( - indoc! {r#" - The dependencies and their versions being installed do not match with the lockfile! These are the conflicts: - - {} - "#}, - formatted_result - ) - } - let new_lockfile = Lockfile::from_resolve(&resolved); new_lockfile.save(&self.project_path)?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d16ae071..1af65781 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -7,6 +7,7 @@ mod package; mod publish; mod search; mod update; +mod utils; pub use init::InitSubcommand; pub use install::InstallSubcommand; diff --git a/src/commands/update.rs b/src/commands/update.rs index e23e0078..2c7b2fb9 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,5 +1,4 @@ use std::collections::BTreeSet; -use std::convert::TryInto; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -16,6 +15,8 @@ use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor}; use indicatif::{ProgressBar, ProgressStyle}; use structopt::StructOpt; +use super::utils::{generate_depedency_changes, render_update_difference}; + /// Update all of the dependencies of this project. #[derive(Debug, StructOpt)] pub struct UpdateSubcommand { @@ -100,7 +101,7 @@ impl UpdateSubcommand { progress.suspend(|| { let dependency_changes = generate_depedency_changes(&lockfile.as_ids().collect(), &resolved_graph.activated); - render_update_difference(&dependency_changes); + render_update_difference(&dependency_changes, &mut std::io::stdout()).unwrap(); }); Lockfile::from_resolve(&resolved_graph).save(&self.project_path)?; @@ -178,131 +179,3 @@ impl FromStr for PackageSpec { } } } - -enum DependencyChange { - Added(PackageId), - Removed(PackageId), - Updated { from: PackageId, to: PackageId }, - Downgrade { from: PackageId, to: PackageId }, -} - -fn generate_depedency_changes( - old_dependencies: &BTreeSet, - new_dependencies: &BTreeSet, -) -> Vec { - let added_packages = new_dependencies.difference(old_dependencies); - let removed_packages = old_dependencies.difference(new_dependencies); - let changed_dependencies: BTreeSet<&PackageName> = added_packages - .clone() - .chain(removed_packages.clone()) - .map(|package| package.name()) - .collect(); - - let mut dependency = Vec::new(); - - for dependency_name in changed_dependencies { - let matching_packages_removed = removed_packages - .clone() - .filter(|x| *x.name() == *dependency_name); - let matching_packages_added = added_packages - .clone() - .filter(|x| *x.name() == *dependency_name); - - match ( - matching_packages_added.clone().count(), - matching_packages_removed.clone().count(), - ) { - (1, 1) => { - // We know for certain that there is only one item in the iterator. - let package_added = matching_packages_added.last().unwrap(); - let package_removed = matching_packages_removed.last().unwrap(); - - if package_added.gt(package_removed) { - dependency.push(DependencyChange::Updated { - from: package_removed.clone(), - to: package_added.clone(), - }); - } else { - dependency.push(DependencyChange::Downgrade { - from: package_added.clone(), - to: package_removed.clone(), - }); - } - } - (0, 1) => { - // We know for certain that there is only one item in the iterator. - let package_removed = matching_packages_removed.last().unwrap(); - dependency.push(DependencyChange::Removed(package_removed.clone())); - } - (1, 0) => { - // We know for certain that there is only one item in the iterator. - let package_added = matching_packages_added.last().unwrap(); - dependency.push(DependencyChange::Added(package_added.clone())); - } - (0, 0) => panic!("Impossible for the package name {} to not be removed or added if found in earlier.", dependency_name), - (_, _) => { - dependency.extend(matching_packages_added.map(|package| DependencyChange::Added(package.clone()))); - dependency.extend(matching_packages_removed.map(|package| DependencyChange::Removed(package.clone()))); - } - } - } - - dependency -} - -fn render_update_difference(dependency_changes: &[DependencyChange]) { - if dependency_changes.is_empty() { - return println!( - "{} No Dependency changes{}", - SetForegroundColor(Color::DarkGreen), - SetForegroundColor(Color::Reset) - ); - } - - println!( - "{} Dependency changes{}", - SetForegroundColor(Color::DarkGreen), - SetForegroundColor(Color::Reset) - ); - - for dependency_change in dependency_changes { - match dependency_change { - DependencyChange::Added(package_id) => { - println!( - "{} Added {}{} v{}", - SetForegroundColor(Color::DarkGreen), - SetForegroundColor(Color::Reset), - package_id.name(), - package_id.version() - ); - } - DependencyChange::Removed(package_id) => { - println!( - "{} Removed {}{} v{}", - SetForegroundColor(Color::DarkRed), - SetForegroundColor(Color::Reset), - package_id.name(), - package_id.version() - ); - } - DependencyChange::Updated { from, to } => { - println!( - "{} Updated {}{} from v{} to v{}", - SetForegroundColor(Color::DarkCyan), - SetForegroundColor(Color::Reset), - from.name(), - from.version(), - to.version() - ); - } - DependencyChange::Downgrade { from, to } => println!( - "{} Downgraded {}{} from v{} to v{}", - SetForegroundColor(Color::DarkYellow), - SetForegroundColor(Color::Reset), - from.name(), - from.version(), - to.version() - ), - } - } -} diff --git a/src/commands/utils.rs b/src/commands/utils.rs new file mode 100644 index 00000000..c99a20ae --- /dev/null +++ b/src/commands/utils.rs @@ -0,0 +1,136 @@ +use crate::{package_id::PackageId, package_name::PackageName}; +use crossterm::style::{Color, SetForegroundColor}; +use std::{collections::BTreeSet, io::Write}; + +pub(crate) enum DependencyChange { + Added(PackageId), + Removed(PackageId), + Updated { from: PackageId, to: PackageId }, + Downgrade { from: PackageId, to: PackageId }, +} + +pub(crate) fn generate_depedency_changes( + old_dependencies: &BTreeSet, + new_dependencies: &BTreeSet, +) -> Vec { + let added_packages = new_dependencies.difference(old_dependencies); + let removed_packages = old_dependencies.difference(new_dependencies); + let changed_dependencies: BTreeSet<&PackageName> = added_packages + .clone() + .chain(removed_packages.clone()) + .map(|package| package.name()) + .collect(); + + let mut dependency = Vec::new(); + + for dependency_name in changed_dependencies { + let matching_packages_removed = removed_packages + .clone() + .filter(|x| *x.name() == *dependency_name); + let matching_packages_added = added_packages + .clone() + .filter(|x| *x.name() == *dependency_name); + + match ( + matching_packages_added.clone().count(), + matching_packages_removed.clone().count(), + ) { + (1, 1) => { + // We know for certain that there is only one item in the iterator. + let package_added = matching_packages_added.last().unwrap(); + let package_removed = matching_packages_removed.last().unwrap(); + + if package_added.gt(package_removed) { + dependency.push(DependencyChange::Updated { + from: package_removed.clone(), + to: package_added.clone(), + }); + } else { + dependency.push(DependencyChange::Downgrade { + from: package_added.clone(), + to: package_removed.clone(), + }); + } + } + (0, 1) => { + // We know for certain that there is only one item in the iterator. + let package_removed = matching_packages_removed.last().unwrap(); + dependency.push(DependencyChange::Removed(package_removed.clone())); + } + (1, 0) => { + // We know for certain that there is only one item in the iterator. + let package_added = matching_packages_added.last().unwrap(); + dependency.push(DependencyChange::Added(package_added.clone())); + } + (0, 0) => panic!("Impossible for the package name {} to not be removed or added if found in earlier.", dependency_name), + (_, _) => { + dependency.extend(matching_packages_added.map(|package| DependencyChange::Added(package.clone()))); + dependency.extend(matching_packages_removed.map(|package| DependencyChange::Removed(package.clone()))); + } + } + } + + dependency +} + +pub(crate) fn render_update_difference( + dependency_changes: &[DependencyChange], + writer: &mut impl Write, +) -> anyhow::Result<()> { + if dependency_changes.is_empty() { + writeln!( + writer, + "{} No Dependency changes{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )?; + } + + writeln!( + writer, + "{} Dependency changes{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )?; + + for dependency_change in dependency_changes { + match dependency_change { + DependencyChange::Added(package_id) => writeln!( + writer, + "{} Added {}{} v{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset), + package_id.name(), + package_id.version() + ), + DependencyChange::Removed(package_id) => writeln!( + writer, + "{} Removed {}{} v{}", + SetForegroundColor(Color::DarkRed), + SetForegroundColor(Color::Reset), + package_id.name(), + package_id.version() + ), + DependencyChange::Updated { from, to } => writeln!( + writer, + "{} Updated {}{} from v{} to v{}", + SetForegroundColor(Color::DarkCyan), + SetForegroundColor(Color::Reset), + from.name(), + from.version(), + to.version() + ), + DependencyChange::Downgrade { from, to } => writeln!( + writer, + "{} Downgraded {}{} from v{} to v{}", + SetForegroundColor(Color::DarkYellow), + SetForegroundColor(Color::Reset), + from.name(), + from.version(), + to.version() + ), + }? + } + + Ok(()) +} diff --git a/test-projects/diamond-graph/root/latest/default.project.json b/test-projects/diamond-graph/root/latest/default.project.json new file mode 100644 index 00000000..a66ff578 --- /dev/null +++ b/test-projects/diamond-graph/root/latest/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "root", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/root/latest/src/init.lua b/test-projects/diamond-graph/root/latest/src/init.lua new file mode 100644 index 00000000..fc684338 --- /dev/null +++ b/test-projects/diamond-graph/root/latest/src/init.lua @@ -0,0 +1,4 @@ +local A = require(script.Parent.A) +local B = require(script.Parent.B) + +return `{A} {B}` \ No newline at end of file diff --git a/test-projects/diamond-graph/root/latest/wally.lock b/test-projects/diamond-graph/root/latest/wally.lock new file mode 100644 index 00000000..630b4047 --- /dev/null +++ b/test-projects/diamond-graph/root/latest/wally.lock @@ -0,0 +1,28 @@ +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.1" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.1"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.1"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.1"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] diff --git a/test-projects/diamond-graph/root/latest/wally.toml b/test-projects/diamond-graph/root/latest/wally.toml new file mode 100644 index 00000000..9b77c47d --- /dev/null +++ b/test-projects/diamond-graph/root/latest/wally.toml @@ -0,0 +1,10 @@ +[package] +name = "diamond-graph/root" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +A = "diamond-graph/direct-dependency-a@0.1.0" +B = "diamond-graph/direct-dependency-b@0.1.0" \ No newline at end of file diff --git a/tests/integration/install.rs b/tests/integration/install.rs index 6e309dbb..a9f699bd 100644 --- a/tests/integration/install.rs +++ b/tests/integration/install.rs @@ -4,45 +4,77 @@ use std::path::Path; #[test] fn minimal() { - run_test("minimal"); + run_install_test("minimal"); } #[test] fn one_dependency() { - run_test("one-dependency"); + run_install_test("one-dependency"); } #[test] fn transitive_dependency() { - run_test("transitive-dependency"); + run_install_test("transitive-dependency"); } #[test] fn private_with_public_dependency() { - run_test("private-with-public-dependency"); + run_install_test("private-with-public-dependency"); } #[test] fn dev_dependency() { - run_test("dev-dependency"); + run_install_test("dev-dependency"); } #[test] fn dev_dependency_also_required_as_non_dev() { - run_test("dev-dependency-also-required-as-non-dev"); + run_install_test("dev-dependency-also-required-as-non-dev"); } #[test] fn cross_realm_dependency() { - run_test("cross-realm-dependency"); + run_install_test("cross-realm-dependency"); } #[test] fn cross_realm_explicit_dependency() { - run_test("cross-realm-explicit-dependency"); + run_install_test("cross-realm-explicit-dependency"); } -fn run_test(name: &str) -> TempProject { +#[test] +fn locked_pass() { + let result = run_locked_install("diamond-graph/root/latest"); + + assert!(result.is_ok(), "Should pass without any problems"); +} + +#[test] +fn locked_catches_dated_packages() { + let result = run_locked_install("diamond-graph/root/dated"); + assert!(result.is_err(), "Should fail!"); +} + +fn run_locked_install(name: &str) -> Result<(), anyhow::Error> { + let source_project = + Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/test-projects",)).join(name); + + let project = TempProject::new(&source_project).unwrap(); + + Args { + global: GlobalOptions { + test_registry: true, + ..Default::default() + }, + subcommand: Subcommand::Install(InstallSubcommand { + project_path: project.path().to_owned(), + locked: true, + }), + } + .run() +} + +fn run_install_test(name: &str) -> TempProject { let source_project = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/test-projects",)).join(name);