Skip to content

Commit

Permalink
Update subcommand (#140)
Browse files Browse the repository at this point in the history
* Refactored TempProject into its own module and started on tests

* Created test projects with a diamond-graph
This will allow us to test the update tool in all expected situations
- One dependency added/removed
- One dependency updated
- Transitive dependencies changed

It's also a nice addition for testing any other commands as well.

* Added to primary-registry
Metadata and zipped up

* Built the update subcommand.

* Lockfiles need to be written!

* Fixed misspelling of dependency_changes

* Reverted test_registry arg back to invisible

* Changed target_packages to package_specs
Clarity?

* Downgrading was added as a concept

* Added lockfile method to view packages as packageIds

* Installs the new packages now, forgor.

* No longer throws if missing lockfile
It will generate a new one instead based on the
manifest.

* Clarity on the filtering process for try_to_use

* Added needed imports
-- they got stashed by accident

* Made the clippy happy and fixed grammar mistakes

* Delete root from registry
They're not packages to be consumed.

* Add new tests, almost done with them all so far.

* Added the rest of snapshots missing

* Testing now works, yay!

* Deleted testing code by mistake and forgot snap
They say to run your tests before push..
This is why.

* Appleasing the formatter
Import in the wrong order...
Extra newline at the end...

* Made it look... *pretty* and cleaned messes

* another blood sarciface for rust fmt

* Doing final cleanups per comments upstream

* The gods demand it, another formatter sacrifice

Co-authored-by: magnalite <[email protected]>

* The coolness must be toned down.

* A little silly mistake indeed
Seemingly forgot to filter out packages to update for try_to_use.
instead it kept them and got rid of everything else.

---------

Co-authored-by: magnalite <[email protected]>
  • Loading branch information
u-train and magnalite authored Jun 13, 2023
1 parent 911a035 commit 84a81e8
Show file tree
Hide file tree
Showing 49 changed files with 860 additions and 61 deletions.
4 changes: 2 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub use manifest_to_json::ManifestToJsonSubcommand;
pub use package::PackageSubcommand;
pub use publish::PublishSubcommand;
pub use search::SearchSubcommand;
pub use update::UpdateSubcommand;
pub use update::{PackageSpec, UpdateSubcommand};

use structopt::StructOpt;

Expand All @@ -37,7 +37,7 @@ impl Args {
Subcommand::Init(subcommand) => subcommand.run(),
Subcommand::Login(subcommand) => subcommand.run(),
Subcommand::Logout(subcommand) => subcommand.run(),
Subcommand::Update(subcommand) => subcommand.run(),
Subcommand::Update(subcommand) => subcommand.run(self.global),
Subcommand::Search(subcommand) => subcommand.run(),
Subcommand::Package(subcommand) => subcommand.run(),
Subcommand::Install(subcommand) => subcommand.run(self.global),
Expand Down
303 changes: 300 additions & 3 deletions src/commands/update.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,308 @@
use std::collections::BTreeSet;
use std::convert::TryInto;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;

use crate::installation::InstallationContext;
use crate::lockfile::Lockfile;
use crate::manifest::Manifest;
use crate::package_id::PackageId;
use crate::package_name::PackageName;
use crate::package_req::PackageReq;
use crate::package_source::{PackageSource, PackageSourceMap, Registry, TestRegistry};
use crate::{resolution, GlobalOptions};
use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor};
use indicatif::{ProgressBar, ProgressStyle};
use structopt::StructOpt;

/// Update all of the dependencies of this project.
#[derive(Debug, StructOpt)]
pub struct UpdateSubcommand {}
pub struct UpdateSubcommand {
/// Path to the project to publish.
#[structopt(long = "project-path", default_value = ".")]
pub project_path: PathBuf,

/// An optional list of dependencies to update.
/// They must be valid package name with an optional version requirement.
pub package_specs: Vec<PackageSpec>,
}

impl UpdateSubcommand {
pub fn run(self) -> anyhow::Result<()> {
todo!("The 'update' subcommand")
pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> {
let manifest = Manifest::load(&self.project_path)?;

let lockfile = match Lockfile::load(&self.project_path)? {
Some(lockfile) => lockfile,
None => Lockfile::from_manifest(&manifest),
};

let default_registry: Box<PackageSource> = if global.test_registry {
Box::new(PackageSource::TestRegistry(TestRegistry::new(
&manifest.package.registry,
)))
} else {
Box::new(PackageSource::Registry(Registry::from_registry_spec(
&manifest.package.registry,
)?))
};

let mut package_sources = PackageSourceMap::new(default_registry);
package_sources.add_fallbacks()?;

// If the user didn't specify any targets, then update all of the packages.
// Otherwise, find the target packages to update.
let try_to_use = if self.package_specs.is_empty() {
println!(
"{} Selected {} all dependencies to try update",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
);

BTreeSet::new()
} else {
let try_to_use: BTreeSet<PackageId> = lockfile
.as_ids()
// We update the target packages by removing the package from the list of packages to try to keep.
.filter(|package_id| !self.given_package_id_satisifies_targets(package_id))
.collect();

println!(
"{} Selected {}{} dependencies to try update",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset),
lockfile.packages.len() - try_to_use.len(),
);

try_to_use
};

let progress = ProgressBar::new(0)
.with_style(
ProgressStyle::with_template("{spinner:.cyan}{wide_msg}")?.tick_chars("⠁⠈⠐⠠⠄⠂ "),
)
.with_message(format!(
"{} Resolving {}new dependencies...",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
));

let resolved_graph = resolution::resolve(&manifest, &try_to_use, &package_sources)?;

progress.println(format!(
"{} Resolved {}{} total dependencies",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset),
resolved_graph.activated.len() - 1
));

progress.enable_steady_tick(Duration::from_millis(100));
progress.suspend(|| {
let dependency_changes =
generate_depedency_changes(&lockfile.as_ids().collect(), &resolved_graph.activated);
render_update_difference(&dependency_changes);
});

Lockfile::from_resolve(&resolved_graph).save(&self.project_path)?;

progress.println(format!(
"{} Updated {}lockfile",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
));

let root_package_id = manifest.package_id();
let installation_context = InstallationContext::new(
&self.project_path,
manifest.place.shared_packages,
manifest.place.server_packages,
);

progress.set_message(format!(
"{} Cleaning {}package destination...",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
));

installation_context.clean()?;

progress.println(format!(
"{} Cleaned {}package destination",
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
));

progress.finish_with_message(format!(
"{}{} Starting installation {}",
SetAttribute(Attribute::Bold),
SetForegroundColor(Color::DarkGreen),
SetForegroundColor(Color::Reset)
));

installation_context.install(package_sources, root_package_id, resolved_graph)?;

Ok(())
}

fn given_package_id_satisifies_targets(&self, package_id: &PackageId) -> bool {
self.package_specs
.iter()
.any(|target_package| match target_package {
PackageSpec::Named(named_target) => package_id.name() == named_target,
PackageSpec::Required(required_target) => {
required_target.matches(package_id.name(), package_id.version())
}
})
}
}

#[derive(Debug, PartialEq, Eq, Hash)]
pub enum PackageSpec {
Named(PackageName),
Required(PackageReq),
}

impl FromStr for PackageSpec {
type Err = anyhow::Error;

fn from_str(value: &str) -> anyhow::Result<Self> {
if let Ok(package_req) = value.parse() {
Ok(PackageSpec::Required(package_req))
} else if let Ok(package_name) = value.parse() {
Ok(PackageSpec::Named(package_name))
} else {
anyhow::bail!(
"Was unable to parse {} into a package requirement or a package name!",
value
)
}
}
}

enum DependencyChange {
Added(PackageId),
Removed(PackageId),
Updated { from: PackageId, to: PackageId },
Downgrade { from: PackageId, to: PackageId },
}

fn generate_depedency_changes(
old_dependencies: &BTreeSet<PackageId>,
new_dependencies: &BTreeSet<PackageId>,
) -> Vec<DependencyChange> {
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()
),
}
}
}
9 changes: 9 additions & 0 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ impl Lockfile {

Ok(())
}

pub fn as_ids(&self) -> impl Iterator<Item = PackageId> + '_ {
self.packages.iter().map(|lock_package| match lock_package {
LockPackage::Registry(lock_package) => {
PackageId::new(lock_package.name.clone(), lock_package.version.clone())
}
LockPackage::Git(_) => todo!(),
})
}
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "direct-dependency-a",
"tree": {
"Packages": {
"$path": "Packages"
},
"$path": "src"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
local Indirect = require(script.Parent.Indirect)

return if Indirect == 420 then "Bingo!" else "Naw..."
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "diamond-graph/direct-dependency-a"
version = "0.1.0"
license = "MIT"
realm = "server"
registry = "test-registries/primary-registry"

[server-dependencies]
Indirect = "diamond-graph/[email protected]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "direct-dependency-a",
"tree": {
"Packages": {
"$path": "Packages"
},
"$path": "src"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
local Indirect = require(script.Parent.Indirect)

return if Indirect == 420 then "Bingo!" else "Naw..."
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "diamond-graph/direct-dependency-a"
version = "0.1.1"
license = "MIT"
realm = "server"
registry = "test-registries/primary-registry"

[server-dependencies]
Indirect = "diamond-graph/[email protected]"
Loading

0 comments on commit 84a81e8

Please sign in to comment.