From 0be8a520c38504481e0d357a9af7010696b7c273 Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:03:12 +0100 Subject: [PATCH] feat(cli): :sparkles: add init, add, remove, and outdated commands --- Cargo.lock | 131 ++++++++++++++++--- Cargo.toml | 3 +- src/cli/auth.rs | 20 +-- src/cli/config.rs | 2 + src/cli/root.rs | 240 +++++++++++++++++++++++++++++------ src/dependencies/git.rs | 31 ++--- src/dependencies/mod.rs | 8 +- src/dependencies/registry.rs | 1 + src/index.rs | 9 -- src/main.rs | 90 ++++++++++--- src/manifest.rs | 19 +++ src/multithread.rs | 39 +++++- 12 files changed, 476 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f7b73d..d002288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,9 +509,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ "async-lock 3.3.0", "cfg-if", @@ -580,7 +580,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ - "async-io 2.3.1", + "async-io 2.3.2", "async-lock 2.8.0", "atomic-waker", "cfg-if", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecount" @@ -829,9 +829,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -1077,6 +1077,31 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1280,6 +1305,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.10.0" @@ -1349,9 +1380,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" +checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" dependencies = [ "serde", ] @@ -1689,6 +1720,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder 1.5.0", +] + [[package]] name = "generator" version = "0.7.5" @@ -2119,6 +2168,23 @@ dependencies = [ "log", ] +[[package]] +name = "inquire" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd05e4e63529f3c9c5f5c668c398217f72756ffe48c85266b49692c55accd1f7" +dependencies = [ + "bitflags 2.4.2", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.12" @@ -2664,6 +2730,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -3034,6 +3109,7 @@ dependencies = [ "ignore", "indicatif", "indicatif-log-bridge", + "inquire", "keyring", "log", "lune", @@ -3053,7 +3129,7 @@ dependencies = [ [[package]] name = "pesde-registry" -version = "0.2.0" +version = "0.3.0" dependencies = [ "actix-cors", "actix-governor", @@ -3551,9 +3627,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" dependencies = [ "base64 0.21.7", "bytes", @@ -4183,6 +4259,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -4391,20 +4488,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", diff --git a/Cargo.toml b/Cargo.toml index 1021a77..af2ed39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://pesde.daimond113.com" include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHANGELOG.md"] [features] -bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge"] +bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge", "inquire"] [[bin]] name = "pesde" @@ -46,6 +46,7 @@ futures-executor = { version = "0.3.30", optional = true } indicatif = { version = "0.17.8", optional = true } auth-git2 = { version = "0.5.3", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true } +inquire = { version = "0.7.0", optional = true } [dev-dependencies] tempfile = "3.10.1" diff --git a/src/cli/auth.rs b/src/cli/auth.rs index 419ed21..543578b 100644 --- a/src/cli/auth.rs +++ b/src/cli/auth.rs @@ -17,10 +17,10 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { match cmd { AuthCommand::Login => { - let response = send_request!(params.reqwest_client.post(Url::parse_with_params( + let response = send_request(params.reqwest_client.post(Url::parse_with_params( "https://github.com/login/device/code", &[("client_id", index_config.github_oauth_client_id.as_str())], - )?)) + )?))? .json::()?; println!( @@ -43,14 +43,14 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { while time_left > 0 { std::thread::sleep(interval); time_left -= interval.as_secs() as i64; - let response = send_request!(params.reqwest_client.post(Url::parse_with_params( + let response = send_request(params.reqwest_client.post(Url::parse_with_params( "https://github.com/login/oauth/access_token", &[ ("client_id", index_config.github_oauth_client_id.as_str()), ("device_code", device_code), - ("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ], - )?)) + )?))? .json::()?; match response @@ -82,10 +82,12 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { params.api_token_entry.set_password(access_token)?; - let response = send_request!(params - .reqwest_client - .get("https://api.github.com/user") - .header(AUTHORIZATION, format!("Bearer {access_token}"))) + let response = send_request( + params + .reqwest_client + .get("https://api.github.com/user") + .header(AUTHORIZATION, format!("Bearer {access_token}")), + )? .json::()?; let login = response["login"] diff --git a/src/cli/config.rs b/src/cli/config.rs index c09689d..5c64e59 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -8,6 +8,7 @@ use crate::{CliConfig, CliParams}; pub enum ConfigCommand { /// Sets the index repository URL SetIndexRepo { + /// The URL of the index repository #[clap(value_name = "URL")] url: String, }, @@ -16,6 +17,7 @@ pub enum ConfigCommand { /// Sets the cache directory SetCacheDir { + /// The directory to use as the cache directory #[clap(value_name = "DIRECTORY")] directory: Option, }, diff --git a/src/cli/root.rs b/src/cli/root.rs index 386cd41..21e5211 100644 --- a/src/cli/root.rs +++ b/src/cli/root.rs @@ -1,11 +1,13 @@ use std::{ - fs::{create_dir_all, read, remove_dir_all, write}, + fs::{create_dir_all, read, remove_dir_all, write, File}, + str::FromStr, time::Duration, }; use flate2::{write::GzEncoder, Compression}; use futures_executor::block_on; use ignore::{overrides::OverrideBuilder, WalkBuilder}; +use inquire::{validator::Validation, Select, Text}; use log::debug; use lune::Runtime; use reqwest::{header::AUTHORIZATION, Url}; @@ -14,13 +16,15 @@ use serde_json::Value; use tar::Builder as TarBuilder; use pesde::{ - dependencies::PackageRef, + dependencies::{registry::RegistryDependencySpecifier, DependencySpecifier, PackageRef}, index::{GitIndex, Index}, - manifest::Manifest, + manifest::{Manifest, PathStyle, Realm}, + multithread::MultithreadedJob, package_name::PackageName, patches::{create_patch, setup_patches_repo}, project::{InstallOptions, Project}, - DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, PACKAGES_FOLDER, PATCHES_FOLDER, SERVER_PACKAGES_FOLDER, + DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, MANIFEST_FILE_NAME, PACKAGES_FOLDER, PATCHES_FOLDER, + SERVER_PACKAGES_FOLDER, }; use crate::{send_request, CliParams, Command}; @@ -37,6 +41,32 @@ fn get_project(params: &CliParams) -> anyhow::Result> { .map_err(Into::into) } +fn multithreaded_bar + 'static>( + params: &CliParams, + job: MultithreadedJob, + len: u64, + message: String, +) -> Result<(), anyhow::Error> { + let bar = params.multi.add( + indicatif::ProgressBar::new(len) + .with_style( + indicatif::ProgressStyle::default_bar() + .template("{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}")?, + ) + .with_message(message), + ); + bar.enable_steady_tick(Duration::from_millis(100)); + + while let Ok(result) = job.progress().recv() { + result.map_err(Into::into)?; + bar.inc(1); + } + + bar.finish_with_message("done"); + + Ok(()) +} + pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { match cmd { Command::Install { locked } => { @@ -55,21 +85,13 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { let resolved_versions_map = project.manifest().dependency_tree(&project, locked)?; let download_job = project.download(&resolved_versions_map)?; - let bar = params.multi.add( - indicatif::ProgressBar::new(resolved_versions_map.len() as u64) - .with_style(indicatif::ProgressStyle::default_bar().template( - "{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}", - )?) - .with_message("Downloading packages"), - ); - bar.enable_steady_tick(Duration::from_millis(100)); - while let Ok(result) = download_job.progress().recv() { - result?; - bar.inc(1); - } - - bar.finish_with_message("done"); + multithreaded_bar( + ¶ms, + download_job, + resolved_versions_map.len() as u64, + "Downloading packages".to_string(), + )?; project.install( InstallOptions::new() @@ -81,16 +103,16 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { Command::Run { package, args } => { let project = get_project(¶ms)?; - let name: PackageName = package.parse()?; - let lockfile = project .lockfile()? .ok_or(anyhow::anyhow!("lockfile not found"))?; let (_, resolved_pkg) = lockfile - .get(&name) + .get(&package) .and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root)) - .ok_or(anyhow::anyhow!("package not found in lockfile"))?; + .ok_or(anyhow::anyhow!( + "package not found in lockfile (or isn't root)" + ))?; if !resolved_pkg.is_root { anyhow::bail!("package is not a root package"); @@ -116,10 +138,10 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { let config = params.index.config()?; let api_url = config.api(); - let response = send_request!(params.reqwest_client.get(Url::parse_with_params( + let response = send_request(params.reqwest_client.get(Url::parse_with_params( &format!("{}/v0/search", api_url), - &query.map_or_else(Vec::new, |q| vec![("query", q)]) - )?)) + &query.map_or_else(Vec::new, |q| vec![("query", q)]), + )?))? .json::()?; for package in response.as_array().unwrap() { @@ -201,30 +223,24 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { request = request.header(AUTHORIZATION, ""); } - println!("{}", send_request!(request).text()?); + println!("{}", send_request(request)?.text()?); } Command::Patch { package } => { let project = get_project(¶ms)?; - let (name, version) = package - .split_once('@') - .ok_or(anyhow::anyhow!("Malformed package name"))?; - let name: PackageName = name.parse()?; - let version = Version::parse(version)?; - let lockfile = project .lockfile()? .ok_or(anyhow::anyhow!("lockfile not found"))?; let resolved_pkg = lockfile - .get(&name) - .and_then(|versions| versions.get(&version)) + .get(&package.0) + .and_then(|versions| versions.get(&package.1)) .ok_or(anyhow::anyhow!("package not found in lockfile"))?; let dir = params.directories.data_dir().join("patches").join(format!( "{}_{}", - name.escaped(), - version + package.0.escaped(), + package.1 )); if dir.exists() { @@ -274,6 +290,158 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { env!("CARGO_BIN_NAME") ); } + Command::Init => { + let manifest_path = params.cwd.join(MANIFEST_FILE_NAME); + + if manifest_path.exists() { + anyhow::bail!("manifest already exists"); + } + + let default_name = params.cwd.file_name().unwrap().to_str().unwrap(); + + let name = Text::new("What is the name of the package?") + .with_initial_value(default_name) + .with_validator(|name: &str| { + Ok(match PackageName::from_str(name) { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.into()), + }) + }) + .prompt()?; + + let path_style = + Select::new("What style of paths do you want to use?", vec!["roblox"]).prompt()?; + let path_style = match path_style { + "roblox" => PathStyle::Roblox { + place: Default::default(), + }, + _ => unreachable!(), + }; + + let description = Text::new("What is the description of the package?").prompt()?; + let license = Text::new("What is the license of the package?").prompt()?; + let authors = Text::new("Who are the authors of the package? (split using ;)") + .prompt()? + .split(';') + .map(|s| s.trim().to_string()) + .collect::>(); + + let private = Select::new("Is this package private?", vec!["yes", "no"]).prompt()?; + let private = private == "yes"; + + let realm = Select::new( + "What is the realm of the package?", + vec!["shared", "server", "dev"], + ) + .prompt()?; + + let realm = match realm { + "shared" => Realm::Shared, + "server" => Realm::Server, + "dev" => Realm::Development, + _ => unreachable!(), + }; + + let manifest = Manifest { + name: name.parse()?, + version: Version::parse("0.1.0")?, + exports: Default::default(), + path_style, + private, + realm: Some(realm), + dependencies: Default::default(), + peer_dependencies: Default::default(), + description: Some(description), + license: Some(license), + authors: Some(authors), + }; + + serde_yaml::to_writer(File::create(manifest_path)?, &manifest)?; + } + Command::Add { + package, + realm, + peer, + } => { + let project = get_project(¶ms)?; + + let mut manifest = project.manifest().clone(); + + let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier { + name: package.0, + version: package.1, + realm, + }); + + if peer { + manifest.peer_dependencies.push(specifier); + } else { + manifest.dependencies.push(specifier); + } + + serde_yaml::to_writer( + File::create(project.path().join(MANIFEST_FILE_NAME))?, + &manifest, + )?; + } + Command::Remove { package } => { + let project = get_project(¶ms)?; + + let mut manifest = project.manifest().clone(); + + for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] { + dependencies.retain(|d| { + if let DependencySpecifier::Registry(registry) = d { + registry.name != package + } else { + true + } + }); + } + + serde_yaml::to_writer( + File::create(project.path().join(MANIFEST_FILE_NAME))?, + &manifest, + )?; + } + Command::Outdated => { + let project = get_project(¶ms)?; + + let manifest = project.manifest(); + let dependency_tree = manifest.dependency_tree(&project, false)?; + + for (name, versions) in dependency_tree { + for (version, resolved_pkg) in versions { + if !resolved_pkg.is_root { + continue; + } + + if let PackageRef::Registry(registry) = resolved_pkg.pkg_ref { + let latest_version = send_request(params.reqwest_client.get(format!( + "{}/v0/packages/{}/{}/versions", + project.index().config()?.api(), + registry.name.scope(), + registry.name.name() + )))? + .json::()? + .as_array() + .unwrap() + .iter() + .map(|v| Version::parse(v.as_str().unwrap())) + .collect::, semver::Error>>()? + .into_iter() + .max() + .unwrap(); + + if latest_version > version { + println!( + "{name}@{version} is outdated. latest version: {latest_version}" + ); + } + } + } + } + } _ => unreachable!(), } diff --git a/src/dependencies/git.rs b/src/dependencies/git.rs index 4415eea..cb4f2e5 100644 --- a/src/dependencies/git.rs +++ b/src/dependencies/git.rs @@ -22,6 +22,7 @@ pub struct GitDependencySpecifier { /// The revision of the git repository to use pub rev: String, /// The realm of the package + #[serde(skip_serializing_if = "Option::is_none")] pub realm: Option, } @@ -65,17 +66,15 @@ impl GitDependencySpecifier { // should also work with ssh urls let is_url = self.repo.contains(':'); - let repo_name = { - if !is_url { - self.repo.to_string() - } else { - let parts: Vec<&str> = self.repo.split('/').collect(); - format!( - "{}/{}", - parts[parts.len() - 2], - parts[parts.len() - 1].trim_end_matches(".git") - ) - } + let repo_name = if !is_url { + self.repo.to_string() + } else { + let parts: Vec<&str> = self.repo.split('/').collect(); + format!( + "{}/{}", + parts[parts.len() - 2], + parts[parts.len() - 1].trim_end_matches(".git") + ) }; if is_url { @@ -84,12 +83,10 @@ impl GitDependencySpecifier { debug!("assuming git repository is a name: {}", &repo_name); } - let repo_url = { - if !is_url { - format!("https://github.com/{}.git", &self.repo) - } else { - self.repo.to_string() - } + let repo_url = if !is_url { + format!("https://github.com/{}.git", &self.repo) + } else { + self.repo.to_string() }; if is_url { diff --git a/src/dependencies/mod.rs b/src/dependencies/mod.rs index 5adc5cb..4b5e327 100644 --- a/src/dependencies/mod.rs +++ b/src/dependencies/mod.rs @@ -143,7 +143,7 @@ impl Project { &self, map: &ResolvedVersionsMap, ) -> Result, InstallProjectError> { - let (job, tx) = MultithreadedJob::new(); + let job = MultithreadedJob::new(); for (name, versions) in map.clone() { for (version, resolved_package) in versions { @@ -162,12 +162,8 @@ impl Project { create_dir_all(&source)?; let project = self.clone(); - let tx = tx.clone(); - job.pool.execute(move || { - let result = resolved_package.pkg_ref.download(&project, source); - tx.send(result).unwrap(); - }); + job.execute(move || resolved_package.pkg_ref.download(&project, source)); } } diff --git a/src/dependencies/registry.rs b/src/dependencies/registry.rs index 7dbb323..37101ae 100644 --- a/src/dependencies/registry.rs +++ b/src/dependencies/registry.rs @@ -22,6 +22,7 @@ pub struct RegistryDependencySpecifier { // #[serde(skip_serializing_if = "Option::is_none")] // pub registry: Option, /// The realm of the package + #[serde(skip_serializing_if = "Option::is_none")] pub realm: Option, } diff --git a/src/index.rs b/src/index.rs index efbf9af..47df56f 100644 --- a/src/index.rs +++ b/src/index.rs @@ -515,7 +515,6 @@ impl IndexConfig { /// An entry in the index file #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] pub struct IndexFileEntry { /// The version of the package pub version: Version, @@ -525,12 +524,6 @@ pub struct IndexFileEntry { /// A description of the package #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - /// The license of the package - #[serde(default, skip_serializing_if = "Option::is_none")] - pub license: Option, - /// The authors of the package - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authors: Option>, /// The dependencies of the package #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -546,8 +539,6 @@ impl From for IndexFileEntry { realm: manifest.realm, description: manifest.description, - license: manifest.license, - authors: manifest.authors, dependencies, } diff --git a/src/main.rs b/src/main.rs index e1c8c4b..acd5912 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::{ fs::{create_dir_all, read}, hash::{DefaultHasher, Hash, Hasher}, path::PathBuf, + str::FromStr, }; use auth_git2::GitAuthenticator; @@ -11,8 +12,13 @@ use directories::ProjectDirs; use indicatif::MultiProgress; use indicatif_log_bridge::LogWrapper; use keyring::Entry; +use log::error; use pretty_env_logger::env_logger::Env; -use reqwest::header::{ACCEPT, AUTHORIZATION}; +use reqwest::{ + blocking::{RequestBuilder, Response}, + header::{ACCEPT, AUTHORIZATION}, +}; +use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use cli::{ @@ -20,29 +26,79 @@ use cli::{ config::{config_command, ConfigCommand}, root::root_command, }; -use pesde::index::GitIndex; +use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName}; mod cli; +#[derive(Debug, Clone)] +pub struct VersionedPackageName>(PackageName, V); + +impl> FromStr for VersionedPackageName { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (name, version) = s.split_once('@').ok_or_else(|| { + anyhow::anyhow!("invalid package name: {s}; expected format: name@version") + })?; + + Ok(VersionedPackageName( + name.to_string().parse()?, + version.parse()?, + )) + } +} + #[derive(Subcommand)] pub enum Command { + /// Initializes a manifest file + Init, + + /// Adds a package to the manifest + Add { + /// The package to add + #[clap(value_name = "PACKAGE")] + package: VersionedPackageName, + + /// Whether the package is a peer dependency + #[clap(long, short)] + peer: bool, + + /// The realm of the package + #[clap(long, short)] + realm: Option, + }, + + /// Removes a package from the manifest + Remove { + /// The package to remove + #[clap(value_name = "PACKAGE")] + package: PackageName, + }, + + /// Lists outdated packages + Outdated, + /// Installs the dependencies of the project Install { + /// Whether to use the lockfile for resolving dependencies #[clap(long, short)] locked: bool, }, /// Runs the `bin` export of the specified package Run { + /// The package to run #[clap(value_name = "PACKAGE")] - package: String, + package: PackageName, + /// The arguments to pass to the package #[clap(last = true)] args: Vec, }, /// Searches for a package on the registry Search { + /// The query to search for #[clap(value_name = "QUERY")] query: Option, }, @@ -51,10 +107,15 @@ pub enum Command { Publish, /// Begins a new patch - Patch { package: String }, + Patch { + /// The package to patch + #[clap(value_name = "PACKAGE")] + package: VersionedPackageName, + }, - /// Commits (finished) the patch + /// Commits (finishes) the patch PatchCommit { + /// The package's changed directory #[clap(value_name = "DIRECTORY")] dir: PathBuf, }, @@ -77,6 +138,7 @@ struct Cli { #[clap(subcommand)] command: Command, + /// The directory to run the command in #[arg(short, long, value_name = "DIRECTORY")] directory: Option, } @@ -117,18 +179,16 @@ impl CliConfig { } } -#[macro_export] -macro_rules! send_request { - ($req:expr) => {{ - let res = $req.send()?; +pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result { + let res = request_builder.send()?; - match res.error_for_status_ref() { - Ok(_) => res, - Err(e) => { - panic!("request failed: {e}\nbody: {}", res.text()?); - } + match res.error_for_status_ref() { + Ok(_) => Ok(res), + Err(e) => { + error!("request failed: {e}\nbody: {}", res.text()?); + Err(e.into()) } - }}; + } } fn main() -> anyhow::Result<()> { diff --git a/src/manifest.rs b/src/manifest.rs index 50d1b76..59289f5 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::{collections::BTreeMap, fmt::Display, fs::read}; use relative_path::RelativePathBuf; @@ -87,6 +88,24 @@ impl Display for Realm { } } +/// An error that occurred while parsing a realm from a string +#[derive(Debug, Error)] +#[error("invalid realm {0}")] +pub struct FromStrRealmError(String); + +impl FromStr for Realm { + type Err = FromStrRealmError; + + fn from_str(s: &str) -> Result { + match s { + "shared" => Ok(Realm::Shared), + "server" => Ok(Realm::Server), + "development" => Ok(Realm::Development), + _ => Err(FromStrRealmError(s.to_string())), + } + } +} + /// The manifest of a package #[derive(Serialize, Deserialize, Debug, Clone)] // #[serde(deny_unknown_fields)] diff --git a/src/multithread.rs b/src/multithread.rs index d1f7e1e..61b8bf8 100644 --- a/src/multithread.rs +++ b/src/multithread.rs @@ -1,18 +1,30 @@ -use std::sync::mpsc::Receiver; +use std::sync::mpsc::{Receiver, Sender}; use threadpool::ThreadPool; /// A multithreaded job -pub struct MultithreadedJob { - pub(crate) progress: Receiver>, - pub(crate) pool: ThreadPool, +pub struct MultithreadedJob { + progress: Receiver>, + sender: Sender>, + pool: ThreadPool, } -impl MultithreadedJob { - pub(crate) fn new() -> (Self, std::sync::mpsc::Sender>) { +impl Default for MultithreadedJob { + fn default() -> Self { let (tx, rx) = std::sync::mpsc::channel(); let pool = ThreadPool::new(6); - (Self { progress: rx, pool }, tx) + Self { + progress: rx, + pool, + sender: tx.clone(), + } + } +} + +impl MultithreadedJob { + /// Creates a new multithreaded job + pub fn new() -> Self { + Self::default() } /// Returns the progress of the job @@ -30,4 +42,17 @@ impl MultithreadedJob { Ok(()) } + + /// Executes a function on the thread pool + pub fn execute(&self, f: F) + where + F: (FnOnce() -> Result<(), E>) + Send + 'static, + { + let sender = self.sender.clone(); + + self.pool.execute(move || { + let result = f(); + sender.send(result).unwrap(); + }); + } }