diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9f6ba8fce..8dfb6584d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,44 @@ when "Cmd/Ctrl + K", "s" or "/" is pressed. ([Sambit Sahoo](https://github.com/soulsam480)) +- `gleam deps` now supports `tree` operation that lists the dependency tree. + + ```markdown + Usage: gleam deps tree [OPTIONS] + + Options: + -p, --package Package to be used as the root of the tree + -i, --invert Invert the tree direction and focus on the given package + -h, --help Print help + ``` + + For example, if the root project (`project_a`) depends on `package_b` and `package_c`, and `package_c` also depends on `package_b`, the output will be: + + + ```markdown + $ gleam deps tree + + project_a v1.0.0 + ├── package_b v0.52.0 + └── package_c v1.2.0 + └── package_b v0.52.0 + + $ gleam deps tree --package package_c + + package_c v1.2.0 + └── package_b v0.52.0 + + $ gleam deps tree --invert package_b + + package_b v0.52.0 + ├── package_c v1.2.0 + │ └── project_a v1.0.0 + └── project_a v1.0.0 + + ``` + + ([Ramkarthik Krishnamurthy](https://github.com/ramkarthik)) + ### Language server - The language server can now generate the definition of functions that do not diff --git a/compiler-cli/src/dependencies.rs b/compiler-cli/src/dependencies.rs index 5c0b00c7932..94d2f8f0e79 100644 --- a/compiler-cli/src/dependencies.rs +++ b/compiler-cli/src/dependencies.rs @@ -4,7 +4,7 @@ use std::{ }; use camino::{Utf8Path, Utf8PathBuf}; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use flate2::read::GzDecoder; use futures::future; use gleam_core::{ @@ -32,12 +32,54 @@ use crate::{ cli, fs::{self, ProjectIO}, http::HttpClient, + TreeOptions, +}; + +struct Symbols { + down: &'static str, + tee: &'static str, + ell: &'static str, + right: &'static str, +} + +static UTF8_SYMBOLS: Symbols = Symbols { + down: "│", + tee: "├", + ell: "└", + right: "─", }; pub fn list() -> Result<()> { + let (_, _, manifest) = get_manifest_details()?; + list_manifest_packages(std::io::stdout(), manifest) +} + +pub fn tree(options: TreeOptions) -> Result<()> { + let (project, config, manifest) = get_manifest_details()?; + + // Initialize the root package since it is not part of the manifest + let root_package = ManifestPackage { + build_tools: vec![], + name: config.name.clone(), + requirements: config.all_direct_dependencies()?.keys().cloned().collect(), + version: config.version.clone(), + source: ManifestPackageSource::Local { + path: project.clone(), + }, + otp_app: None, + }; + + // Get the manifest packages and add the root package to the vec + let mut packages = manifest.packages.iter().cloned().collect_vec(); + packages.append(&mut vec![root_package.clone()]); + + list_package_and_dependencies_tree(std::io::stdout(), options, packages.clone(), config.name) +} + +fn get_manifest_details() -> Result<(Utf8PathBuf, PackageConfig, Manifest)> { let runtime = tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime"); let project = fs::get_project_root(fs::get_current_directory()?)?; - let paths = ProjectPaths::new(project); + let paths = ProjectPaths::new(project.clone()); let config = crate::config::root_config()?; let (_, manifest) = get_manifest( &paths, @@ -48,7 +90,7 @@ pub fn list() -> Result<()> { UseManifest::Yes, Vec::new(), )?; - list_manifest_packages(std::io::stdout(), manifest) + Ok((project, config, manifest)) } fn list_manifest_packages(mut buffer: W, manifest: Manifest) -> Result<()> { @@ -62,6 +104,99 @@ fn list_manifest_packages(mut buffer: W, manifest: Manifest) }) } +fn list_package_and_dependencies_tree( + mut buffer: W, + options: TreeOptions, + packages: Vec, + root_package_name: EcoString, +) -> Result<()> { + let mut invert = false; + + let package: Option<&ManifestPackage> = if let Some(input_package_name) = options.package { + packages.iter().find(|p| p.name == input_package_name) + } else if let Some(input_package_name) = options.invert { + invert = true; + packages.iter().find(|p| p.name == input_package_name) + } else { + packages.iter().find(|p| p.name == root_package_name) + }; + + if let Some(package) = package { + let tree = Vec::from([eco_format!("{0} v{1}", package.name, package.version)]); + let tree = list_dependencies_tree( + tree.clone(), + package.clone(), + packages, + EcoString::new(), + invert, + ); + + tree.iter() + .try_for_each(|line| writeln!(buffer, "{}", line)) + .map_err(|e| Error::StandardIo { + action: StandardIoAction::Write, + err: Some(e.kind()), + }) + } else { + writeln!(buffer, "Package not found. Please check the package name.").map_err(|e| { + Error::StandardIo { + action: StandardIoAction::Write, + err: Some(e.kind()), + } + }) + } +} + +fn list_dependencies_tree( + mut tree: Vec, + package: ManifestPackage, + packages: Vec, + accum: EcoString, + invert: bool, +) -> Vec { + let dependencies = packages + .iter() + .filter(|p| { + (invert && p.requirements.contains(&package.name)) + || (!invert && package.requirements.contains(&p.name)) + }) + .cloned() + .collect_vec(); + + let dependencies = dependencies.iter().sorted().enumerate(); + + let deps_length = dependencies.len(); + for (index, dependency) in dependencies { + let is_last = index == deps_length - 1; + let prefix = if is_last { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + + tree.push(eco_format!( + "{0}{1}{2}{2} {3} v{4}", + accum.clone(), + prefix, + UTF8_SYMBOLS.right, + dependency.name, + dependency.version + )); + + let accum = accum.clone() + (if !is_last { UTF8_SYMBOLS.down } else { " " }) + " "; + + tree = list_dependencies_tree( + tree.clone(), + dependency.clone(), + packages.clone(), + accum.clone(), + invert, + ); + } + + tree +} + #[derive(Debug, Clone, Copy)] pub enum UseManifest { Yes, diff --git a/compiler-cli/src/dependencies/tests.rs b/compiler-cli/src/dependencies/tests.rs index c7efc895266..1d1bf8fa9aa 100644 --- a/compiler-cli/src/dependencies/tests.rs +++ b/compiler-cli/src/dependencies/tests.rs @@ -63,6 +63,250 @@ zzz 0.4.0 ) } +#[test] +fn tree_format() { + let mut buffer = vec![]; + let manifest = Manifest { + requirements: HashMap::new(), + packages: vec![ + ManifestPackage { + name: "deps_proj".into(), + version: Version::parse("1.0.0").unwrap(), + build_tools: [].into(), + otp_app: None, + requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), + }, + }, + ManifestPackage { + name: "gleam_stdlib".into(), + version: Version::new(0, 52, 0), + build_tools: ["rebar3".into(), "make".into()].into(), + otp_app: Some("aaa_app".into()), + requirements: vec![], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ManifestPackage { + name: "gleam_regexp".into(), + version: Version::new(1, 0, 0), + build_tools: ["mix".into()].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ], + }; + + let options = TreeOptions { + package: None, + invert: None, + }; + + let root_package_name = EcoString::from("deps_proj"); + + list_package_and_dependencies_tree( + &mut buffer, + options, + manifest.packages.clone(), + root_package_name, + ) + .unwrap(); + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#"deps_proj v1.0.0 +├── gleam_regexp v1.0.0 +│ └── gleam_stdlib v0.52.0 +└── gleam_stdlib v0.52.0 +"# + ) +} + +#[test] +fn tree_package_format() { + let mut buffer = vec![]; + let manifest = Manifest { + requirements: HashMap::new(), + packages: vec![ + ManifestPackage { + name: "gleam_stdlib".into(), + version: Version::new(0, 52, 0), + build_tools: ["rebar3".into(), "make".into()].into(), + otp_app: Some("aaa_app".into()), + requirements: vec![], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ManifestPackage { + name: "deps_proj".into(), + version: Version::parse("1.0.0").unwrap(), + build_tools: [].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), + }, + }, + ManifestPackage { + name: "gleam_regexp".into(), + version: Version::new(1, 0, 0), + build_tools: ["mix".into()].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ], + }; + let options = TreeOptions { + package: Some("gleam_regexp".to_string()), + invert: None, + }; + + let root_package_name = EcoString::from("deps_proj"); + + list_package_and_dependencies_tree( + &mut buffer, + options, + manifest.packages.clone(), + root_package_name, + ) + .unwrap(); + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#"gleam_regexp v1.0.0 +└── gleam_stdlib v0.52.0 +"# + ) +} + +#[test] +fn tree_invert_format() { + let mut buffer = vec![]; + let manifest = Manifest { + requirements: HashMap::new(), + packages: vec![ + ManifestPackage { + name: "gleam_stdlib".into(), + version: Version::new(0, 52, 0), + build_tools: ["rebar3".into(), "make".into()].into(), + otp_app: Some("aaa_app".into()), + requirements: vec![], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ManifestPackage { + name: "deps_proj".into(), + version: Version::parse("1.0.0").unwrap(), + build_tools: [].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), + }, + }, + ManifestPackage { + name: "gleam_regexp".into(), + version: Version::new(1, 0, 0), + build_tools: ["mix".into()].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ], + }; + let options = TreeOptions { + package: None, + invert: Some("gleam_stdlib".to_string()), + }; + + let root_package_name = EcoString::from("deps_proj"); + + list_package_and_dependencies_tree( + &mut buffer, + options, + manifest.packages.clone(), + root_package_name, + ) + .unwrap(); + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#"gleam_stdlib v0.52.0 +├── deps_proj v1.0.0 +└── gleam_regexp v1.0.0 + └── deps_proj v1.0.0 +"# + ) +} + +#[test] +fn list_tree_invalid_package_format() { + let mut buffer = vec![]; + let manifest = Manifest { + requirements: HashMap::new(), + packages: vec![ + ManifestPackage { + name: "gleam_stdlib".into(), + version: Version::new(0, 52, 0), + build_tools: ["rebar3".into(), "make".into()].into(), + otp_app: Some("aaa_app".into()), + requirements: vec![], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ManifestPackage { + name: "gleam_regexp".into(), + version: Version::new(1, 0, 0), + build_tools: ["mix".into()].into(), + otp_app: None, + requirements: vec!["gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![3, 22]), + }, + }, + ManifestPackage { + name: "root".into(), + version: Version::parse("1.0.0").unwrap(), + build_tools: [].into(), + otp_app: None, + requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()], + source: ManifestPackageSource::Hex { + outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), + }, + }, + ], + }; + let options = TreeOptions { + package: Some("zzzzzz".to_string()), + invert: None, + }; + + let root_package_name = EcoString::from("deps_proj"); + + list_package_and_dependencies_tree( + &mut buffer, + options, + manifest.packages.clone(), + root_package_name, + ) + .unwrap(); + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#"Package not found. Please check the package name. +"# + ) +} + #[test] fn parse_gleam_add_specifier_invalid_semver() { assert!(parse_gleam_add_specifier("some_package@1.2.3.4").is_err()); diff --git a/compiler-cli/src/main.rs b/compiler-cli/src/main.rs index 8be439e0476..f0219ff541d 100644 --- a/compiler-cli/src/main.rs +++ b/compiler-cli/src/main.rs @@ -101,6 +101,27 @@ struct UpdateOptions { packages: Vec, } +#[derive(Args, Debug, Clone)] +struct TreeOptions { + /// Name of the package to get the dependency tree for + #[arg( + short, + long, + ignore_case = true, + help = "Package to be used as the root of the tree" + )] + package: Option, + /// Name of the package to get the inverted dependency tree for + #[arg( + short, + long, + ignore_case = true, + help = "Invert the tree direction and focus on the given package", + value_name = "PACKAGE" + )] + invert: Option, +} + #[derive(Parser, Debug)] #[command( version, @@ -355,6 +376,9 @@ enum Dependencies { /// Update dependency packages to their latest versions Update(UpdateOptions), + + /// Tree of all the dependency packages + Tree(TreeOptions), } #[derive(Subcommand, Debug)] @@ -485,6 +509,8 @@ fn main() { Command::Deps(Dependencies::Update(options)) => dependencies::update(options.packages), + Command::Deps(Dependencies::Tree(options)) => dependencies::tree(options), + Command::Hex(Hex::Authenticate) => hex::authenticate(), Command::New(options) => new::create(options, COMPILER_VERSION),