From 5c77d8db769d86ab8dc64b54c2f529b685898c13 Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:19:53 +0100 Subject: [PATCH] refactor(cli): :art: use static variables --- Cargo.lock | 57 +++--- Cargo.toml | 3 +- registry/src/endpoints/packages.rs | 5 +- src/cli/api_token.rs | 147 +++++++++++++++ src/cli/auth.rs | 25 ++- src/cli/config.rs | 20 +- src/cli/mod.rs | 274 +++++++++++++++++++++++++++ src/cli/root.rs | 75 ++++---- src/dependencies/git.rs | 2 +- src/dependencies/mod.rs | 4 +- src/main.rs | 288 +---------------------------- src/multithread.rs | 7 +- 12 files changed, 520 insertions(+), 387 deletions(-) create mode 100644 src/cli/api_token.rs diff --git a/Cargo.lock b/Cargo.lock index 3fdf260..5509e06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,7 +92,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -130,7 +130,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -243,7 +243,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -571,7 +571,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -600,13 +600,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -925,7 +925,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1146,7 +1146,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1157,7 +1157,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1363,7 +1363,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1711,7 +1711,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -2604,9 +2604,9 @@ dependencies = [ [[package]] name = "luau0-src" -version = "0.8.4+luau616" +version = "0.8.5+luau617" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f34c6c8e52606273c5d689b722e03383e58fed85840868885301fe9038fbd9" +checksum = "652e36b8c35d807ec76a4931fe7c62883c62cc93311fb49bf5b76084647078ea" dependencies = [ "cc", ] @@ -3103,7 +3103,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3263,6 +3263,7 @@ dependencies = [ "keyring", "log", "lune", + "once_cell", "pathdiff", "pretty_env_logger", "relative-path", @@ -3280,7 +3281,7 @@ dependencies = [ [[package]] name = "pesde-registry" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix-cors", "actix-governor", @@ -3325,7 +3326,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3470,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4320,7 +4321,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4352,7 +4353,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4653,9 +4654,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -4888,7 +4889,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -5022,7 +5023,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -5180,7 +5181,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -5474,7 +5475,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-shared", ] @@ -5508,7 +5509,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5872,7 +5873,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6a72a95..7de702c 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", "inquire"] +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", "once_cell"] [[bin]] name = "pesde" @@ -48,6 +48,7 @@ indicatif = { version = "0.17.8", optional = true } auth-git2 = { version = "0.5.4", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true } inquire = { version = "0.7.1", optional = true } +once_cell = { version = "1.19.0", optional = true } [dev-dependencies] tempfile = "3.10.1" diff --git a/registry/src/endpoints/packages.rs b/registry/src/endpoints/packages.rs index 0d8311c..e4d301f 100644 --- a/registry/src/endpoints/packages.rs +++ b/registry/src/endpoints/packages.rs @@ -174,10 +174,7 @@ pub async fn get_package_version( match index.package(&package_name)? { Some(package) => { if version == "latest" { - version = package - .last() - .map(|v| v.version.to_string()) - .unwrap(); + version = package.last().map(|v| v.version.to_string()).unwrap(); } else if !package.iter().any(|v| v.version.to_string() == version) { return Ok(HttpResponse::NotFound().finish()); } diff --git a/src/cli/api_token.rs b/src/cli/api_token.rs new file mode 100644 index 0000000..f5ec99d --- /dev/null +++ b/src/cli/api_token.rs @@ -0,0 +1,147 @@ +use std::path::PathBuf; + +use keyring::Entry; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +use crate::cli::INDEX_DIR; + +pub trait ApiTokenSource: Send + Sync { + fn get_api_token(&self) -> anyhow::Result>; + fn set_api_token(&self, api_token: &str) -> anyhow::Result<()>; + fn delete_api_token(&self) -> anyhow::Result<()>; + fn persists(&self) -> bool { + true + } +} + +pub struct EnvVarApiTokenSource; + +const API_TOKEN_ENV_VAR: &str = "PESDE_API_TOKEN"; + +impl ApiTokenSource for EnvVarApiTokenSource { + fn get_api_token(&self) -> anyhow::Result> { + match std::env::var(API_TOKEN_ENV_VAR) { + Ok(token) => Ok(Some(token)), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(e) => Err(e.into()), + } + } + + // don't need to implement set_api_token or delete_api_token + fn set_api_token(&self, _api_token: &str) -> anyhow::Result<()> { + Ok(()) + } + + fn delete_api_token(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn persists(&self) -> bool { + false + } +} + +static KEYRING_ENTRY: Lazy = + Lazy::new(|| Entry::new(env!("CARGO_BIN_NAME"), "api_token").unwrap()); + +pub struct KeyringApiTokenSource; + +impl ApiTokenSource for KeyringApiTokenSource { + fn get_api_token(&self) -> anyhow::Result> { + match KEYRING_ENTRY.get_password() { + Ok(api_token) => Ok(Some(api_token)), + Err(err) => match err { + keyring::Error::NoEntry | keyring::Error::PlatformFailure(_) => Ok(None), + _ => Err(err.into()), + }, + } + } + + fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> { + KEYRING_ENTRY.set_password(api_token)?; + + Ok(()) + } + + fn delete_api_token(&self) -> anyhow::Result<()> { + KEYRING_ENTRY.delete_password()?; + + Ok(()) + } +} + +static AUTH_FILE_PATH: Lazy = Lazy::new(|| INDEX_DIR.join("auth.yaml")); +static AUTH_FILE: Lazy = + Lazy::new( + || match std::fs::read_to_string(AUTH_FILE_PATH.to_path_buf()) { + Ok(config) => serde_yaml::from_str(&config).unwrap(), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => AuthFile::default(), + Err(e) => panic!("{:?}", e), + }, + ); + +#[derive(Serialize, Deserialize, Default, Clone)] +struct AuthFile { + #[serde(default)] + api_token: Option, +} + +pub struct ConfigFileApiTokenSource; + +impl ApiTokenSource for ConfigFileApiTokenSource { + fn get_api_token(&self) -> anyhow::Result> { + Ok(AUTH_FILE.api_token.clone()) + } + + fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> { + let mut config = AUTH_FILE.clone(); + config.api_token = Some(api_token.to_string()); + + serde_yaml::to_writer( + &mut std::fs::File::create(AUTH_FILE_PATH.to_path_buf())?, + &config, + )?; + + Ok(()) + } + + fn delete_api_token(&self) -> anyhow::Result<()> { + let mut config = AUTH_FILE.clone(); + + config.api_token = None; + + serde_yaml::to_writer( + &mut std::fs::File::create(AUTH_FILE_PATH.to_path_buf())?, + &config, + )?; + + Ok(()) + } +} + +pub static API_TOKEN_SOURCE: Lazy> = Lazy::new(|| { + let sources: Vec> = vec![ + Box::new(EnvVarApiTokenSource), + Box::new(KeyringApiTokenSource), + Box::new(ConfigFileApiTokenSource), + ]; + + let mut valid_sources = vec![]; + + for source in sources { + match source.get_api_token() { + Ok(Some(_)) => return source, + Ok(None) => { + if source.persists() { + valid_sources.push(source); + } + } + Err(e) => { + log::error!("error getting api token: {e}"); + } + } + } + + valid_sources.pop().unwrap() +}); diff --git a/src/cli/auth.rs b/src/cli/auth.rs index 543578b..470dcaa 100644 --- a/src/cli/auth.rs +++ b/src/cli/auth.rs @@ -2,9 +2,9 @@ use clap::Subcommand; use pesde::index::Index; use reqwest::{header::AUTHORIZATION, Url}; -use crate::{send_request, CliParams}; +use crate::cli::{api_token::API_TOKEN_SOURCE, send_request, INDEX, REQWEST_CLIENT}; -#[derive(Subcommand)] +#[derive(Subcommand, Clone)] pub enum AuthCommand { /// Logs in to the registry Login, @@ -12,14 +12,14 @@ pub enum AuthCommand { Logout, } -pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { - let index_config = params.index.config()?; - +pub fn auth_command(cmd: AuthCommand) -> anyhow::Result<()> { match cmd { AuthCommand::Login => { - let response = send_request(params.reqwest_client.post(Url::parse_with_params( + let github_oauth_client_id = INDEX.config()?.github_oauth_client_id; + + let response = send_request(REQWEST_CLIENT.post(Url::parse_with_params( "https://github.com/login/device/code", - &[("client_id", index_config.github_oauth_client_id.as_str())], + &[("client_id", &github_oauth_client_id)], )?))? .json::()?; @@ -43,10 +43,10 @@ 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(REQWEST_CLIENT.post(Url::parse_with_params( "https://github.com/login/oauth/access_token", &[ - ("client_id", index_config.github_oauth_client_id.as_str()), + ("client_id", github_oauth_client_id.as_str()), ("device_code", device_code), ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ], @@ -80,11 +80,10 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { .as_str() .ok_or(anyhow::anyhow!("couldn't get access_token"))?; - params.api_token_entry.set_password(access_token)?; + API_TOKEN_SOURCE.set_api_token(access_token)?; let response = send_request( - params - .reqwest_client + REQWEST_CLIENT .get("https://api.github.com/user") .header(AUTHORIZATION, format!("Bearer {access_token}")), )? @@ -102,7 +101,7 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> { anyhow::bail!("code expired, please re-run the login command"); } AuthCommand::Logout => { - params.api_token_entry.delete_password()?; + API_TOKEN_SOURCE.delete_api_token()?; println!("you're now logged out"); } diff --git a/src/cli/config.rs b/src/cli/config.rs index 5c64e59..1eb98c0 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -2,9 +2,9 @@ use std::path::PathBuf; use clap::Subcommand; -use crate::{CliConfig, CliParams}; +use crate::{cli::CLI_CONFIG, CliConfig}; -#[derive(Subcommand)] +#[derive(Subcommand, Clone)] pub enum ConfigCommand { /// Sets the index repository URL SetIndexRepo { @@ -25,41 +25,41 @@ pub enum ConfigCommand { GetCacheDir, } -pub fn config_command(cmd: ConfigCommand, params: CliParams) -> anyhow::Result<()> { +pub fn config_command(cmd: ConfigCommand) -> anyhow::Result<()> { match cmd { ConfigCommand::SetIndexRepo { url } => { let cli_config = CliConfig { index_repo_url: url.clone(), - ..params.cli_config + ..CLI_CONFIG.clone() }; - cli_config.write(¶ms.directories)?; + cli_config.write()?; println!("index repository url set to: `{url}`"); } ConfigCommand::GetIndexRepo => { println!( "current index repository url: `{}`", - params.cli_config.index_repo_url + CLI_CONFIG.index_repo_url ); } ConfigCommand::SetCacheDir { directory } => { let cli_config = CliConfig { cache_dir: directory, - ..params.cli_config + ..CLI_CONFIG.clone() }; - cli_config.write(¶ms.directories)?; + cli_config.write()?; println!( "cache directory set to: `{}`", - cli_config.cache_dir(¶ms.directories).display() + cli_config.cache_dir().display() ); } ConfigCommand::GetCacheDir => { println!( "current cache directory: `{}`", - params.cli_config.cache_dir(¶ms.directories).display() + CLI_CONFIG.cache_dir().display() ); } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 7a5ccd6..8261112 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,277 @@ +use crate::cli::{api_token::API_TOKEN_SOURCE, auth::AuthCommand, config::ConfigCommand}; +use auth_git2::GitAuthenticator; +use clap::{Parser, Subcommand}; +use directories::ProjectDirs; +use indicatif::MultiProgress; +use indicatif_log_bridge::LogWrapper; +use log::error; +use once_cell::sync::Lazy; +use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName}; +use pretty_env_logger::env_logger::Env; +use reqwest::{ + blocking::{RequestBuilder, Response}, + header::ACCEPT, +}; +use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + path::PathBuf, + str::FromStr, +}; + +pub mod api_token; pub mod auth; pub mod config; pub mod root; + +#[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, Clone)] +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: 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, + }, + + /// Publishes the project to the registry + Publish, + + /// Converts a `wally.toml` file to a `pesde.yaml` file + Convert, + + /// Begins a new patch + Patch { + /// The package to patch + #[clap(value_name = "PACKAGE")] + package: VersionedPackageName, + }, + + /// Commits (finishes) the patch + PatchCommit { + /// The package's changed directory + #[clap(value_name = "DIRECTORY")] + dir: PathBuf, + }, + + /// Auth-related commands + Auth { + #[clap(subcommand)] + command: AuthCommand, + }, + + /// Config-related commands + Config { + #[clap(subcommand)] + command: ConfigCommand, + }, +} + +#[derive(Parser, Clone)] +pub struct Cli { + #[clap(subcommand)] + pub command: Command, + + /// The directory to run the command in + #[arg(short, long, value_name = "DIRECTORY")] + pub directory: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct CliConfig { + pub index_repo_url: String, + pub cache_dir: Option, +} + +impl Default for CliConfig { + fn default() -> Self { + Self { + index_repo_url: "https://github.com/daimond113/pesde-index".to_string(), + cache_dir: None, + } + } +} + +impl CliConfig { + pub fn cache_dir(&self) -> PathBuf { + self.cache_dir + .clone() + .unwrap_or_else(|| DIRS.cache_dir().to_path_buf()) + } + + pub fn open() -> anyhow::Result { + let cli_config_path = DIRS.config_dir().join("config.yaml"); + + if cli_config_path.exists() { + Ok(serde_yaml::from_slice(&std::fs::read(cli_config_path)?)?) + } else { + let config = CliConfig::default(); + config.write()?; + Ok(config) + } + } + + pub fn write(&self) -> anyhow::Result<()> { + let cli_config_path = DIRS.config_dir().join("config.yaml"); + serde_yaml::to_writer( + &mut std::fs::File::create(cli_config_path.as_path())?, + &self, + )?; + + Ok(()) + } +} + +pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result { + let res = request_builder.send()?; + + match res.error_for_status_ref() { + Ok(_) => Ok(res), + Err(e) => { + error!("request failed: {e}\nbody: {}", res.text()?); + Err(e.into()) + } + } +} + +pub static CLI: Lazy = Lazy::new(Cli::parse); + +pub static DIRS: Lazy = Lazy::new(|| { + ProjectDirs::from("com", env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME")) + .expect("couldn't get home directory") +}); + +pub static CLI_CONFIG: Lazy = Lazy::new(|| CliConfig::open().unwrap()); + +pub static INDEX_DIR: Lazy = Lazy::new(|| { + let mut hasher = DefaultHasher::new(); + CLI_CONFIG.index_repo_url.hash(&mut hasher); + let hash = hasher.finish().to_string(); + + CLI_CONFIG.cache_dir().join("indices").join(hash) +}); + +pub static INDEX: Lazy = Lazy::new(|| { + let index = GitIndex::new( + INDEX_DIR.join("index"), + &CLI_CONFIG.index_repo_url, + Some(Box::new(|| { + Box::new(|a, b, c| { + let git_authenticator = GitAuthenticator::new(); + let config = git2::Config::open_default().unwrap(); + let mut cred = git_authenticator.credentials(&config); + + cred(a, b, c) + }) + })), + ); + + index.refresh().unwrap(); + + index +}); + +pub static CWD: Lazy = Lazy::new(|| { + CLI.directory + .clone() + .or(std::env::current_dir().ok()) + .expect("couldn't get current directory") +}); + +pub static REQWEST_CLIENT: Lazy = Lazy::new(|| { + let mut header_map = reqwest::header::HeaderMap::new(); + header_map.insert(ACCEPT, "application/json".parse().unwrap()); + header_map.insert("X-GitHub-Api-Version", "2022-11-28".parse().unwrap()); + + if let Ok(Some(token)) = API_TOKEN_SOURCE.get_api_token() { + header_map.insert( + reqwest::header::AUTHORIZATION, + format!("Bearer {token}").parse().unwrap(), + ); + } + + reqwest::blocking::Client::builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .default_headers(header_map) + .build() + .unwrap() +}); + +pub static MULTI: Lazy = Lazy::new(|| { + let logger = pretty_env_logger::formatted_builder() + .parse_env(Env::default().default_filter_or("info")) + .build(); + let multi = MultiProgress::new(); + + LogWrapper::new(multi.clone(), logger).try_init().unwrap(); + + multi +}); diff --git a/src/cli/root.rs b/src/cli/root.rs index 43b82e8..41b6344 100644 --- a/src/cli/root.rs +++ b/src/cli/root.rs @@ -10,6 +10,7 @@ use ignore::{overrides::OverrideBuilder, WalkBuilder}; use inquire::{validator::Validation, Select, Text}; use log::debug; use lune::Runtime; +use once_cell::sync::Lazy; use reqwest::{header::AUTHORIZATION, Url}; use semver::Version; use serde_json::Value; @@ -27,27 +28,19 @@ use pesde::{ SERVER_PACKAGES_FOLDER, }; -use crate::{send_request, CliParams, Command}; +use crate::cli::{ + api_token::API_TOKEN_SOURCE, send_request, Command, CLI_CONFIG, CWD, DIRS, INDEX, MULTI, + REQWEST_CLIENT, +}; pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024; -fn get_project(params: &CliParams) -> anyhow::Result> { - Project::from_path( - ¶ms.cwd, - params.cli_config.cache_dir(¶ms.directories), - params.index.clone(), - params.api_token_entry.get_password().ok(), - ) - .map_err(Into::into) -} - fn multithreaded_bar + 'static>( - params: &CliParams, job: MultithreadedJob, len: u64, message: String, ) -> Result<(), anyhow::Error> { - let bar = params.multi.add( + let bar = MULTI.add( indicatif::ProgressBar::new(len) .with_style( indicatif::ProgressStyle::default_bar() @@ -77,13 +70,21 @@ macro_rules! none_if_empty { }; } -pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { +pub fn root_command(cmd: Command) -> anyhow::Result<()> { + let project: Lazy> = Lazy::new(|| { + Project::from_path( + CWD.to_path_buf(), + CLI_CONFIG.cache_dir(), + INDEX.clone(), + API_TOKEN_SOURCE.get_api_token().ok().flatten(), + ) + .unwrap() + }); + match cmd { Command::Install { locked } => { - let project = get_project(¶ms)?; - for packages_folder in &[PACKAGES_FOLDER, DEV_PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER] { - if let Err(e) = remove_dir_all(¶ms.cwd.join(packages_folder)) { + if let Err(e) = remove_dir_all(CWD.join(packages_folder)) { if e.kind() != std::io::ErrorKind::NotFound { return Err(e.into()); } else { @@ -97,7 +98,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { let download_job = project.download(&resolved_versions_map)?; multithreaded_bar( - ¶ms, download_job, resolved_versions_map.len() as u64, "Downloading packages".to_string(), @@ -111,8 +111,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { )?; } Command::Run { package, args } => { - let project = get_project(¶ms)?; - let lockfile = project .lockfile()? .ok_or(anyhow::anyhow!("lockfile not found"))?; @@ -145,10 +143,10 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { ))?; } Command::Search { query } => { - let config = params.index.config()?; + let config = INDEX.config()?; let api_url = config.api(); - let response = send_request(params.reqwest_client.get(Url::parse_with_params( + let response = send_request(REQWEST_CLIENT.get(Url::parse_with_params( &format!("{}/v0/search", api_url), &query.map(|q| vec![("query", q)]).unwrap_or_default(), )?))? @@ -171,8 +169,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { } } Command::Publish => { - let project = get_project(¶ms)?; - if project.manifest().private { anyhow::bail!("package is private, cannot publish"); } @@ -180,9 +176,11 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { let encoder = GzEncoder::new(vec![], Compression::default()); let mut archive = TarBuilder::new(encoder); - let mut walk_builder = WalkBuilder::new(¶ms.cwd); + let cwd = &CWD.to_path_buf(); + + let mut walk_builder = WalkBuilder::new(cwd); walk_builder.add_custom_ignore_filename(".pesdeignore"); - let mut overrides = OverrideBuilder::new(¶ms.cwd); + let mut overrides = OverrideBuilder::new(cwd); for packages_folder in IGNORED_FOLDERS { overrides.add(&format!("!{}", packages_folder))?; @@ -193,7 +191,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { for entry in walk_builder.build() { let entry = entry?; let path = entry.path(); - let relative_path = path.strip_prefix(¶ms.cwd)?; + let relative_path = path.strip_prefix(cwd)?; let entry_type = entry .file_type() .ok_or(anyhow::anyhow!("failed to get file type"))?; @@ -222,8 +220,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { .file_name("tarball.tar.gz") .mime_str("application/gzip")?; - let mut request = params - .reqwest_client + let mut request = REQWEST_CLIENT .post(format!("{}/v0/packages", project.index().config()?.api())) .multipart(reqwest::blocking::multipart::Form::new().part("tarball", part)); @@ -236,8 +233,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { println!("{}", send_request(request)?.text()?); } Command::Patch { package } => { - let project = get_project(¶ms)?; - let lockfile = project .lockfile()? .ok_or(anyhow::anyhow!("lockfile not found"))?; @@ -247,7 +242,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { .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!( + let dir = DIRS.data_dir().join("patches").join(format!( "{}_{}", package.0.escaped(), package.1 @@ -273,8 +268,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { println!("done! modify the files in {} and run `{} patch-commit ` to commit the changes", dir.display(), env!("CARGO_BIN_NAME")); } Command::PatchCommit { dir } => { - let project = get_project(¶ms)?; - let manifest = Manifest::from_path(&dir)?; let patch_path = project.path().join(PATCHES_FOLDER).join(format!( "{}@{}.patch", @@ -301,13 +294,13 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { ); } Command::Init => { - let manifest_path = params.cwd.join(MANIFEST_FILE_NAME); + let manifest_path = CWD.join(MANIFEST_FILE_NAME); if manifest_path.exists() { anyhow::bail!("manifest already exists"); } - let default_name = params.cwd.file_name().and_then(|s| s.to_str()); + let default_name = CWD.file_name().and_then(|s| s.to_str()); let mut name = Text::new("What is the name of the package?").with_validator(|name: &str| { @@ -380,8 +373,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { realm, peer, } => { - let project = get_project(¶ms)?; - let mut manifest = project.manifest().clone(); let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier { @@ -402,8 +393,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { )?; } Command::Remove { package } => { - let project = get_project(¶ms)?; - let mut manifest = project.manifest().clone(); for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] { @@ -422,8 +411,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { )?; } Command::Outdated => { - let project = get_project(¶ms)?; - let manifest = project.manifest(); let dependency_tree = manifest.dependency_tree(&project, false)?; @@ -434,7 +421,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { } if let PackageRef::Registry(registry) = resolved_pkg.pkg_ref { - let latest_version = send_request(params.reqwest_client.get(format!( + let latest_version = send_request(REQWEST_CLIENT.get(format!( "{}/v0/packages/{}/{}/versions", project.index().config()?.api(), registry.name.scope(), @@ -459,7 +446,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { } } Command::Convert => { - Manifest::from_path_or_convert(¶ms.cwd)?; + Manifest::from_path_or_convert(CWD.to_path_buf())?; } _ => unreachable!(), } diff --git a/src/dependencies/git.rs b/src/dependencies/git.rs index a7e83ea..735702e 100644 --- a/src/dependencies/git.rs +++ b/src/dependencies/git.rs @@ -152,7 +152,7 @@ impl GitPackageRef { } repo.reset(&obj, git2::ResetType::Hard, None)?; - + Manifest::from_path_or_convert(dest)?; Ok(()) diff --git a/src/dependencies/mod.rs b/src/dependencies/mod.rs index a5ff9fc..a7bd914 100644 --- a/src/dependencies/mod.rs +++ b/src/dependencies/mod.rs @@ -163,7 +163,9 @@ impl Project { let project = self.clone(); - job.execute(&tx, move || resolved_package.pkg_ref.download(&project, source)); + job.execute(&tx, move || { + resolved_package.pkg_ref.download(&project, source) + }); } } diff --git a/src/main.rs b/src/main.rs index fb83c44..48aca38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,289 +1,17 @@ -use anyhow::bail; -use std::{ - fs::{create_dir_all, read}, - hash::{DefaultHasher, Hash, Hasher}, - path::PathBuf, - str::FromStr, -}; +use once_cell::sync::Lazy; -use auth_git2::GitAuthenticator; -use clap::{Parser, Subcommand}; -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::{ - blocking::{RequestBuilder, Response}, - header::{ACCEPT, AUTHORIZATION}, -}; -use semver::{Version, VersionReq}; -use serde::{Deserialize, Serialize}; +use cli::{auth::auth_command, config::config_command, root::root_command}; -use cli::{ - auth::{auth_command, AuthCommand}, - config::{config_command, ConfigCommand}, - root::root_command, -}; -use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName}; +use crate::cli::{CliConfig, Command, CLI, MULTI}; 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: 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, - }, - - /// Publishes the project to the registry - Publish, - - /// Converts a `wally.toml` file to a `pesde.yaml` file - Convert, - - /// Begins a new patch - Patch { - /// The package to patch - #[clap(value_name = "PACKAGE")] - package: VersionedPackageName, - }, - - /// Commits (finishes) the patch - PatchCommit { - /// The package's changed directory - #[clap(value_name = "DIRECTORY")] - dir: PathBuf, - }, - - /// Auth-related commands - Auth { - #[clap(subcommand)] - command: AuthCommand, - }, - - /// Config-related commands - Config { - #[clap(subcommand)] - command: ConfigCommand, - }, -} - -#[derive(Parser)] -struct Cli { - #[clap(subcommand)] - command: Command, - - /// The directory to run the command in - #[arg(short, long, value_name = "DIRECTORY")] - directory: Option, -} - -#[derive(Serialize, Deserialize, Clone)] -struct CliConfig { - index_repo_url: String, - cache_dir: Option, -} - -impl CliConfig { - fn cache_dir(&self, directories: &ProjectDirs) -> PathBuf { - self.cache_dir - .clone() - .unwrap_or_else(|| directories.cache_dir().to_path_buf()) - } -} - -struct CliParams { - index: GitIndex, - api_token_entry: Entry, - reqwest_client: reqwest::blocking::Client, - cli_config: CliConfig, - cwd: PathBuf, - multi: MultiProgress, - directories: ProjectDirs, -} - -impl CliConfig { - fn write(&self, directories: &ProjectDirs) -> anyhow::Result<()> { - let cli_config_path = directories.config_dir().join("config.yaml"); - serde_yaml::to_writer( - &mut std::fs::File::create(cli_config_path.as_path())?, - &self, - )?; - - Ok(()) - } -} - -pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result { - let res = request_builder.send()?; - - 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<()> { - let logger = pretty_env_logger::formatted_builder() - .parse_env(Env::default().default_filter_or("info")) - .build(); - let multi = MultiProgress::new(); + Lazy::force(&MULTI); - LogWrapper::new(multi.clone(), logger).try_init().unwrap(); - - let cli = Cli::parse(); - - let directories = ProjectDirs::from("com", env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME")) - .expect("couldn't get home directory"); - - let cli_config_path = directories.config_dir().join("config.yaml"); - let cli_config = if cli_config_path.exists() { - serde_yaml::from_slice(&read(cli_config_path.as_path())?)? - } else { - let config = CliConfig { - index_repo_url: "https://github.com/daimond113/pesde-index".to_string(), - cache_dir: None, - }; - create_dir_all(directories.config_dir())?; - config.write(&directories)?; - config - }; - - let cwd_buf = cli - .directory - .or(std::env::current_dir().ok()) - .ok_or(anyhow::anyhow!("couldn't get current directory"))?; - - let api_token_entry = Entry::new(env!("CARGO_BIN_NAME"), "api_token")?; - - let mut hasher = DefaultHasher::new(); - cli_config.index_repo_url.hash(&mut hasher); - let hash = hasher.finish().to_string(); - - let index = GitIndex::new( - cli_config.cache_dir(&directories).join("index").join(hash), - &cli_config.index_repo_url, - Some(Box::new(|| { - Box::new(|a, b, c| { - let git_authenticator = GitAuthenticator::new(); - let config = git2::Config::open_default().unwrap(); - let mut cred = git_authenticator.credentials(&config); - - cred(a, b, c) - }) - })), - ); - index.refresh()?; - - let mut header_map = reqwest::header::HeaderMap::new(); - header_map.insert(ACCEPT, "application/json".parse()?); - header_map.insert("X-GitHub-Api-Version", "2022-11-28".parse()?); - - match api_token_entry.get_password() { - Ok(api_token) => { - header_map.insert(AUTHORIZATION, format!("Bearer {api_token}").parse()?); - } - Err(err) => match err { - keyring::Error::NoEntry => {} - _ => { - bail!("error getting api token from keyring: {err}") - } - }, - }; - - let reqwest_client = reqwest::blocking::Client::builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .default_headers(header_map) - .build()?; - - let params = CliParams { - index, - api_token_entry, - reqwest_client, - cli_config, - cwd: cwd_buf, - multi, - directories, - }; - - match cli.command { - Command::Auth { command } => auth_command(command, params), - Command::Config { command } => config_command(command, params), - cmd => root_command(cmd, params), + match CLI.command.clone() { + Command::Auth { command } => auth_command(command), + Command::Config { command } => config_command(command), + cmd => root_command(cmd), } } diff --git a/src/multithread.rs b/src/multithread.rs index 7273594..1ab75c3 100644 --- a/src/multithread.rs +++ b/src/multithread.rs @@ -13,10 +13,7 @@ impl MultithreadedJob { let (tx, rx) = std::sync::mpsc::channel(); let pool = ThreadPool::new(6); - (Self { - progress: rx, - pool, - }, tx) + (Self { progress: rx, pool }, tx) } /// Returns the progress of the job @@ -41,7 +38,7 @@ impl MultithreadedJob { F: (FnOnce() -> Result<(), E>) + Send + 'static, { let sender = tx.clone(); - + self.pool.execute(move || { let result = f(); sender.send(result).unwrap();