From ad568fc631d53315acc01b3b41792a4035b52eb2 Mon Sep 17 00:00:00 2001 From: Dmitrii Kovanikov Date: Sun, 11 Sep 2022 16:01:37 +0100 Subject: [PATCH] [#33] Better end-of-sync output Resolves #33 --- src/config/schema.rs | 6 +- src/infra.rs | 1 + src/infra/client.rs | 103 ++++++++++++++++++++++++++++ src/lib.rs | 1 + src/model/release.rs | 10 +++ src/model/tool.rs | 46 ++++++++++++- src/sync.rs | 64 ++++++++++++------ src/sync/download.rs | 128 +++-------------------------------- src/sync/install.rs | 96 +++++++++++--------------- src/sync/prefetch.rs | 157 +++++++++++++++++++++++++++++++++++++++++++ src/sync/progress.rs | 89 ++++++++++++++---------- 11 files changed, 467 insertions(+), 234 deletions(-) create mode 100644 src/infra.rs create mode 100644 src/infra/client.rs create mode 100644 src/sync/prefetch.rs diff --git a/src/config/schema.rs b/src/config/schema.rs index 137bacf..6293b34 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -31,12 +31,12 @@ pub struct ConfigAsset { /// Defaults to `repo` if not specified pub exe_name: Option, - /// Name of the specific asset to download - pub asset_name: AssetName, - /// Release tag to download /// Defaults to the latest release pub tag: Option, + + /// Name of the specific asset to download + pub asset_name: AssetName, } impl Config { diff --git a/src/infra.rs b/src/infra.rs new file mode 100644 index 0000000..b9babe5 --- /dev/null +++ b/src/infra.rs @@ -0,0 +1 @@ +pub mod client; diff --git a/src/infra/client.rs b/src/infra/client.rs new file mode 100644 index 0000000..a7eb78e --- /dev/null +++ b/src/infra/client.rs @@ -0,0 +1,103 @@ +use std::env; +use std::error::Error; +use std::io::Read; + +use crate::model::release::{Asset, Release}; + +/// GitHub API client to handle all API requests +pub struct Client { + pub owner: String, + pub repo: String, + pub version: String, +} + +impl Client { + fn release_url(&self) -> String { + format!( + "https://api.github.com/repos/{owner}/{repo}/releases/{version}", + owner = self.owner, + repo = self.repo, + version = self.version, + ) + } + + fn asset_url(&self, asset_id: u32) -> String { + format!( + "https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}", + owner = self.owner, + repo = self.repo, + asset_id = asset_id + ) + } + + pub fn fetch_release_info(&self) -> Result> { + let release_url = self.release_url(); + + let req = add_auth_header( + ureq::get(&release_url) + .set("Accept", "application/vnd.github+json") + .set("User-Agent", "chshersh/tool-sync-0.2.0"), + ); + + let release: Release = req.call()?.into_json()?; + + Ok(release) + } + + pub fn get_asset_stream( + &self, + asset: &Asset, + ) -> Result, ureq::Error> { + let asset_url = self.asset_url(asset.id); + + let req = add_auth_header( + ureq::get(&asset_url) + .set("Accept", "application/octet-stream") + .set("User-Agent", "chshersh/tool-sync-0.2.0"), + ); + + Ok(req.call()?.into_reader()) + } +} + +fn add_auth_header(req: ureq::Request) -> ureq::Request { + match env::var("GITHUB_TOKEN") { + Err(_) => req, + Ok(token) => req.set("Authorization", &format!("token {}", token)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::model::tool::ToolInfoTag; + + #[test] + fn release_url_with_latest_tag_is_correct() { + let client = Client { + owner: String::from("OWNER"), + repo: String::from("REPO"), + version: ToolInfoTag::Latest.to_str_version(), + }; + + assert_eq!( + client.release_url(), + "https://api.github.com/repos/OWNER/REPO/releases/latest" + ); + } + + #[test] + fn release_url_with_specific_tag_is_correct() { + let client = Client { + owner: String::from("OWNER"), + repo: String::from("REPO"), + version: ToolInfoTag::Specific(String::from("SPECIFIC_TAG")).to_str_version(), + }; + + assert_eq!( + client.release_url(), + "https://api.github.com/repos/OWNER/REPO/releases/tags/SPECIFIC_TAG" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 88d0544..1626c33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ mod config; mod err; +mod infra; mod model; mod sync; diff --git a/src/model/release.rs b/src/model/release.rs index 7fd39b8..d279181 100644 --- a/src/model/release.rs +++ b/src/model/release.rs @@ -12,3 +12,13 @@ pub struct Asset { pub name: String, pub size: u64, } + +impl From<&Asset> for Asset { + fn from(other: &Asset) -> Self { + Asset { + id: other.id, + name: other.name.clone(), + size: other.size, + } + } +} diff --git a/src/model/tool.rs b/src/model/tool.rs index d45d5f8..89aed8e 100644 --- a/src/model/tool.rs +++ b/src/model/tool.rs @@ -1,3 +1,5 @@ +use super::release::Asset; +use crate::infra::client::Client; use crate::model::asset_name::AssetName; #[derive(Debug, PartialEq, Eq)] @@ -59,9 +61,49 @@ pub struct ToolInfo { /// Executable name inside the .tar.gz or .zip archive pub exe_name: String, + /// Version tag + pub tag: ToolInfoTag, + /// Asset name depending on the OS pub asset_name: AssetName, +} - /// Version tag - pub tag: ToolInfoTag, +impl ToolInfo { + pub fn select_asset(&self, assets: &[Asset]) -> Result { + match self.asset_name.get_name_by_os() { + None => Err(String::from( + "Don't know the asset name for this OS: specify it explicitly in the config", + )), + Some(asset_name) => { + let asset = assets.iter().find(|&asset| asset.name.contains(asset_name)); + + match asset { + None => Err(format!("No asset matching name: {}", asset_name)), + Some(asset) => Ok(Asset::from(asset)), + } + } + } + } +} + +/// All information about the tool, needed to download its asset after fetching +/// the release and asset info. Values of this type are created in +/// `src/sync/prefetch.rs` from `ToolInfo`. +pub struct ToolAsset { + /// Name of the tool (e.g. "ripgrep" or "exa") + pub tool_name: String, + + /// Specific git tag (e.g. "v3.4.2") + /// This value is the result of `ToolInfoTag::to_str_version` so "latest" + /// **can't** be here. + pub tag: String, + + /// Executable name inside the .tar.gz or .zip archive + pub exe_name: String, + + /// The selected asset + pub asset: Asset, + + /// GitHub API client that produces the stream for downloading the asset + pub client: Client, } diff --git a/src/sync.rs b/src/sync.rs index 924b175..936a975 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -3,16 +3,29 @@ mod configure; mod db; mod download; mod install; +mod prefetch; mod progress; +use console::Emoji; + use crate::config::schema::Config; -use crate::sync::install::Installer; -use crate::sync::progress::SyncProgress; + +use self::install::Installer; +use self::prefetch::prefetch; +use self::progress::SyncProgress; +use self::progress::ToolPair; pub fn sync(config: Config) { if config.tools.is_empty() { - eprintln!( - r#"No tools to sync. Have you configured 'tool-sync'? + no_tools_message() + } else { + sync_tools(config) + } +} + +fn no_tools_message() { + eprintln!( + r#"No tools to sync. Have you configured 'tool-sync'? Put the following into the $HOME/.tool.toml file for the simplest configuration: @@ -27,21 +40,34 @@ Put the following into the $HOME/.tool.toml file for the simplest configuration: For more details, refer to the official documentation: * https://github.com/chshersh/tool-sync#tool-sync"# - ); - } else { - let store_directory = config.ensure_store_directory(); - - let tools: Vec = config.tools.keys().cloned().collect(); - let tags: Vec = config - .tools - .values() - .map(|config_asset| config_asset.tag.clone().unwrap_or_else(|| "latest".into())) - .collect(); - let sync_progress = SyncProgress::new(tools, tags); - let installer = Installer::mk(store_directory, sync_progress); - - for (tool_name, config_asset) in config.tools.iter() { - installer.install(tool_name, config_asset); + ); +} + +const DONE: Emoji<'_, '_> = Emoji("✨ ", "* "); + +fn sync_tools(config: Config) { + let store_directory = config.ensure_store_directory(); + let tool_assets = prefetch(config.tools); + + let tool_pairs = tool_assets + .iter() + .map(|ta| ToolPair { + name: &ta.tool_name, + tag: &ta.tag, + }) + .collect(); + + let sync_progress = SyncProgress::new(tool_pairs); + let installer = Installer::mk(store_directory, sync_progress); + + let mut installed_tools: u64 = 0; + + for tool_asset in tool_assets { + let is_successs = installer.install(tool_asset); + if is_successs { + installed_tools += 1 } } + + eprintln!("{} Successfully installed {} tools!", DONE, installed_tools); } diff --git a/src/sync/download.rs b/src/sync/download.rs index 2307b17..0309519 100644 --- a/src/sync/download.rs +++ b/src/sync/download.rs @@ -1,18 +1,16 @@ use indicatif::ProgressBar; -use std::env; use std::error::Error; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; -use crate::model::release::{Asset, Release}; +use crate::infra::client::Client; +use crate::model::release::Asset; use crate::sync::progress::SyncProgress; pub struct Downloader<'a> { - pub owner: &'a str, - pub repo: &'a str, - pub asset_name: &'a str, - pub version: &'a str, + pub asset: &'a Asset, + pub client: &'a Client, pub pb_msg: &'a ProgressBar, pub sync_progress: &'a SyncProgress, } @@ -20,59 +18,17 @@ pub struct Downloader<'a> { /// Info about the downloaded asset pub struct DownloadInfo { pub archive_path: PathBuf, - pub asset_name: String, - pub tag_name: String, } impl<'a> Downloader<'a> { - fn release_url(&self) -> String { - format!( - "https://api.github.com/repos/{owner}/{repo}/releases/{version}", - owner = self.owner, - repo = self.repo, - version = self.version, - ) - } - - fn asset_url(&self, asset_id: u32) -> String { - format!( - "https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}", - owner = self.owner, - repo = self.repo, - asset_id = asset_id - ) - } - - fn download_release(&self) -> Result> { - let release_url = self.release_url(); - - let req = add_auth_header( - ureq::get(&release_url) - .set("Accept", "application/vnd.github+json") - .set("User-Agent", "chshersh/tool-sync-0.1.0"), - ); - - let release: Release = req.call()?.into_json()?; - - Ok(release) - } - - fn download_asset(&self, tmp_dir: &Path, asset: &Asset) -> Result> { - let asset_url = self.asset_url(asset.id); - - let req = add_auth_header( - ureq::get(&asset_url) - .set("Accept", "application/octet-stream") - .set("User-Agent", "chshersh/tool-sync-0.1.0"), - ); - - let mut stream = req.call()?.into_reader(); + fn download_asset(&self, tmp_dir: &Path) -> Result> { + let mut stream = self.client.get_asset_stream(self.asset)?; - let download_path = tmp_dir.join(&asset.name); + let download_path = tmp_dir.join(&self.asset.name); let mut destination = File::create(&download_path)?; self.pb_msg.set_message("Downloading..."); - let pb_downloading = self.sync_progress.create_progress_bar(asset.size); + let pb_downloading = self.sync_progress.create_progress_bar(self.asset.size); let mut buffer = [0; 4096]; while let Ok(bytes_read) = stream.read(&mut buffer) { @@ -94,72 +50,8 @@ impl<'a> Downloader<'a> { pub fn download(&self, tmp_dir: &Path) -> Result> { self.pb_msg.set_message("Fetching info..."); - let release = self.download_release()?; - - let asset = release - .assets - .iter() - .find(|&asset| asset.name.contains(self.asset_name)); - - match asset { - None => Err(format!("No asset matching name: {}", self.asset_name).into()), - Some(asset) => { - let archive_path = self.download_asset(tmp_dir, asset)?; - - Ok(DownloadInfo { - archive_path, - asset_name: asset.name.clone(), - tag_name: release.tag_name, - }) - } - } - } -} - -pub fn add_auth_header(req: ureq::Request) -> ureq::Request { - match env::var("GITHUB_TOKEN") { - Err(_) => req, - Ok(token) => req.set("Authorization", &format!("token {}", token)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::model::tool::ToolInfoTag; - - #[test] - fn release_url_with_latest_tag_is_correct() { - let downloader = Downloader { - owner: "OWNER", - repo: "REPO", - asset_name: "ASSET_NAME", - version: &ToolInfoTag::Latest.to_str_version(), - pb_msg: &ProgressBar::hidden(), - sync_progress: &SyncProgress::new(vec!["tool".to_string()], vec!["latest".to_string()]), - }; - - assert_eq!( - downloader.release_url(), - "https://api.github.com/repos/OWNER/REPO/releases/latest" - ); - } - - #[test] - fn release_url_with_specific_tag_is_correct() { - let downloader = Downloader { - owner: "OWNER", - repo: "REPO", - asset_name: "ASSET_NAME", - version: &ToolInfoTag::Specific("SPECIFIC_TAG".to_string()).to_str_version(), - pb_msg: &ProgressBar::hidden(), - sync_progress: &SyncProgress::new(vec!["tool".to_string()], vec!["latest".to_string()]), - }; + let archive_path = self.download_asset(tmp_dir)?; - assert_eq!( - downloader.release_url(), - "https://api.github.com/repos/OWNER/REPO/releases/tags/SPECIFIC_TAG" - ); + Ok(DownloadInfo { archive_path }) } } diff --git a/src/sync/install.rs b/src/sync/install.rs index 7714441..795f059 100644 --- a/src/sync/install.rs +++ b/src/sync/install.rs @@ -7,13 +7,11 @@ use tempdir::TempDir; #[cfg(target_family = "unix")] use std::os::unix::fs::PermissionsExt; -use crate::config::schema::ConfigAsset; use crate::err; use crate::model::asset_name::mk_exe_name; -use crate::model::tool::{Tool, ToolInfo}; +use crate::model::tool::ToolAsset; use super::archive::Archive; -use super::configure::configure_tool; use super::download::Downloader; use super::progress::SyncProgress; @@ -40,68 +38,56 @@ impl Installer { } } - pub fn install(&self, tool_name: &str, config_asset: &ConfigAsset) { - let tag: String = config_asset.tag.clone().unwrap_or_else(|| "latest".into()); - let pb_msg = self.sync_progress.create_message_bar(tool_name, &tag); + /// Returns `true` if the installation was successful + pub fn install(&self, tool_asset: ToolAsset) -> bool { + let tool_name = &tool_asset.tool_name; + let tag = &tool_asset.tag; - match configure_tool(tool_name, config_asset) { - Tool::Known(tool_info) => match self.sync_single_tool(&tool_info, &pb_msg) { - Ok(tag_name) => { - self.sync_progress.success(pb_msg, tool_name, &tag_name); - } - Err(e) => { - self.sync_progress - .failure(pb_msg, tool_name, &tag, format!("[error] {}", e)); - } - }, - Tool::Error(e) => { + let pb_msg = self.sync_progress.create_message_bar(tool_name, tag); + + match self.sync_single_tool(&tool_asset, &pb_msg) { + Ok(_) => { + self.sync_progress.success(pb_msg, tool_name, tag); + true + } + Err(e) => { self.sync_progress - .failure(pb_msg, tool_name, &tag, e.display()); + .failure(pb_msg, tool_name, tag, format!("[error] {}", e)); + false } } } fn sync_single_tool( &self, - tool_info: &ToolInfo, + tool_asset: &ToolAsset, pb_msg: &ProgressBar, - ) -> Result> { - match tool_info.asset_name.get_name_by_os() { - None => Err( - "Don't know the asset name for this OS: specify it explicitly in the config".into(), - ), - Some(asset_name) => { - let downloader = Downloader { - owner: &tool_info.owner, - repo: &tool_info.repo, - version: &tool_info.tag.to_str_version(), - sync_progress: &self.sync_progress, - pb_msg, - asset_name, - }; - - let download_info = downloader.download(self.tmp_dir.path())?; - - let archive = Archive::from( - &download_info.archive_path, - self.tmp_dir.path(), - &tool_info.exe_name, - &download_info.asset_name, - ); - - match archive { - None => { - Err(format!("Unsupported asset type: {}", download_info.asset_name).into()) - } - Some(archive) => match archive.unpack() { - Err(unpack_err) => Err(unpack_err.display().into()), - Ok(tool_path) => { - copy_file(tool_path, &self.store_directory, &tool_info.exe_name)?; - Ok(download_info.tag_name) - } - }, + ) -> Result<(), Box> { + let downloader = Downloader { + asset: &tool_asset.asset, + client: &tool_asset.client, + sync_progress: &self.sync_progress, + pb_msg, + }; + + let download_info = downloader.download(self.tmp_dir.path())?; + + let archive = Archive::from( + &download_info.archive_path, + self.tmp_dir.path(), + &tool_asset.exe_name, + &tool_asset.asset.name, + ); + + match archive { + None => Err(format!("Unsupported asset type: {}", tool_asset.asset.name).into()), + Some(archive) => match archive.unpack() { + Err(unpack_err) => Err(unpack_err.display().into()), + Ok(tool_path) => { + copy_file(tool_path, &self.store_directory, &tool_asset.exe_name)?; + Ok(()) } - } + }, } } } diff --git a/src/sync/prefetch.rs b/src/sync/prefetch.rs new file mode 100644 index 0000000..bd71ec3 --- /dev/null +++ b/src/sync/prefetch.rs @@ -0,0 +1,157 @@ +use console::{style, Emoji}; +use indicatif::{HumanBytes, ProgressBar, ProgressStyle}; +use std::collections::BTreeMap; + +use super::configure::configure_tool; +use crate::config::schema::ConfigAsset; +use crate::infra::client::Client; +use crate::model::tool::{Tool, ToolAsset}; + +const PREFETCH: Emoji<'_, '_> = Emoji("🔄 ", "-> "); +const ERROR: Emoji<'_, '_> = Emoji("❌ ", "x "); +const PACKAGE: Emoji<'_, '_> = Emoji("📦 ", "# "); + +struct PrefetchProgress { + pb: ProgressBar, + total_count: usize, +} + +impl PrefetchProgress { + fn new(total_count: usize) -> PrefetchProgress { + let pb = create_prefetch_progress_bar(); + PrefetchProgress { pb, total_count } + } + + fn update_message(&self, already_completed: usize) { + let remaining_count = self.total_count - already_completed; + + if remaining_count == 0 { + self.pb.set_message("All done!"); + self.pb.finish() + } else { + self.pb.set_message(format!( + "Fetching info about {} tools (this may take a few seconds)...", + remaining_count + )) + } + } + + fn expected_err_msg(&self, tool_name: &str, msg: &str) { + let tool = format!("{}", style(tool_name).cyan().bold()); + self.pb.println(format!("{} {} {}", ERROR, tool, msg)) + } + + fn unexpected_err_msg(&self, tool_name: &str, msg: &str) { + let tool = format!("{}", style(tool_name).cyan().bold()); + let err_msg = format!( + r#"{emoji} {tool} {msg} + +If you think you see this error by a 'tool-sync' mistake, +don't hesitate to open an issue: + + * https://github.com/chshersh/tool-sync/issues/new"#, + emoji = ERROR, + tool = tool, + msg = msg, + ); + + self.pb.println(err_msg); + } + + fn finish(&self) { + self.pb.finish() + } +} + +/// Fetch information about all the tool from the configuration. This function +/// combines two steps: +/// +/// 1. Resolving all the required fields from `ConfigAsset`. +/// 2. Fetching release and asset info from GitHub. +pub fn prefetch(tools: BTreeMap) -> Vec { + let total_count = tools.len(); + + let prefetch_progress = PrefetchProgress::new(total_count); + prefetch_progress.update_message(0); + + let tool_assets: Vec = tools + .iter() + .enumerate() + .filter_map(|(index, (tool_name, config_asset))| { + prefetch_tool(tool_name, config_asset, &prefetch_progress, index) + }) + .collect(); + + prefetch_progress.finish(); + + let estimated_download_size: u64 = tool_assets.iter().map(|ta| ta.asset.size).sum(); + let size = HumanBytes(estimated_download_size); + eprintln!( + "{emoji} Estimated download size: {size}", + emoji = PACKAGE, + size = size + ); + + tool_assets +} + +fn prefetch_tool( + tool_name: &str, + config_asset: &ConfigAsset, + prefetch_progress: &PrefetchProgress, + current_index: usize, +) -> Option { + // indexes start with 0 so we add 1 to calculate already fetched tools + let already_completed = current_index + 1; + + match configure_tool(tool_name, config_asset) { + Tool::Error(e) => { + prefetch_progress.expected_err_msg(tool_name, &e.display()); + prefetch_progress.update_message(already_completed); + None + } + Tool::Known(tool_info) => { + let client = Client { + owner: tool_info.owner.clone(), + repo: tool_info.repo.clone(), + version: tool_info.tag.to_str_version(), + }; + + match client.fetch_release_info() { + Err(e) => { + prefetch_progress.unexpected_err_msg(tool_name, &format!("{}", e)); + prefetch_progress.update_message(already_completed); + None + } + Ok(release) => match tool_info.select_asset(&release.assets) { + Err(msg) => { + prefetch_progress.unexpected_err_msg(tool_name, &msg); + prefetch_progress.update_message(already_completed); + None + } + Ok(asset) => { + let tool_asset = ToolAsset { + tool_name: String::from(tool_name), + tag: release.tag_name, + exe_name: tool_info.exe_name, + asset, + client, + }; + + prefetch_progress.update_message(already_completed); + + Some(tool_asset) + } + }, + } + } + } +} + +fn create_prefetch_progress_bar() -> ProgressBar { + let message_style = ProgressStyle::with_template("{prefix} {msg}").unwrap(); + + ProgressBar::new(100) + .with_style(message_style) + .with_prefix(format!("{}", PREFETCH)) +} diff --git a/src/sync/progress.rs b/src/sync/progress.rs index be28e8f..ec6e623 100644 --- a/src/sync/progress.rs +++ b/src/sync/progress.rs @@ -10,19 +10,26 @@ pub struct SyncProgress { const SUCCESS: Emoji<'_, '_> = Emoji("✅ ", "OK "); const FAILURE: Emoji<'_, '_> = Emoji("⛔ ", "NO "); const PROCESS: Emoji<'_, '_> = Emoji("📥 ", ".. "); + const MIN_TAG_SIZE: usize = 8; +/// A data type with minimal information required to create `SyncProgress` +pub struct ToolPair<'a> { + pub name: &'a str, + pub tag: &'a str, +} + impl SyncProgress { /// Creates new `SyncProgress` from a list of tools. /// !!! The given `Vec` must be non-empty !!! - pub fn new(tools: Vec, tags: Vec) -> SyncProgress { + pub fn new(tool_assets: Vec) -> SyncProgress { // unwrap is safe here because 'new' is called with a non-empty vector - let max_tool_size = tools.iter().map(|tool| tool.len()).max().unwrap(); + let max_tool_size = tool_assets.iter().map(|tp| tp.name.len()).max().unwrap(); // putting a default of 8 here since tags like v0.10.10 is already 8 - let max_tag_size = tags + let max_tag_size = tool_assets .iter() - .map(|tag| std::cmp::max(tag.len(), MIN_TAG_SIZE)) + .map(|tp| std::cmp::max(tp.tag.len(), MIN_TAG_SIZE)) .max() .unwrap_or(MIN_TAG_SIZE); @@ -35,11 +42,11 @@ impl SyncProgress { } } - fn fmt_prefix(&self, emoji: Emoji, tool_name: &str, tag_name: &str) -> String { + fn fmt_prefix(&self, emoji: Emoji, tool_name: &str, tag: &str) -> String { let aligned_tool = format!( "{:tool_width$} {:tag_width$}", tool_name, - tag_name, + tag, tool_width = self.max_tool_size, tag_width = self.max_tag_size, ); @@ -47,13 +54,13 @@ impl SyncProgress { format!("{}{}", emoji, aligned_tool) } - pub fn create_message_bar(&self, tool_name: &str, tag_name: &str) -> ProgressBar { + pub fn create_message_bar(&self, tool_name: &str, tag: &str) -> ProgressBar { let message_style = ProgressStyle::with_template("{prefix:.bold.dim} {msg}").unwrap(); self.multi_progress.add( ProgressBar::new(100) .with_style(message_style) - .with_prefix(self.fmt_prefix(PROCESS, tool_name, tag_name)), + .with_prefix(self.fmt_prefix(PROCESS, tool_name, tag)), ) } @@ -69,16 +76,16 @@ impl SyncProgress { pb.finish_and_clear() } - pub fn success(&self, pb: ProgressBar, tool_name: &str, tag_name: &str) { - pb.set_prefix(self.fmt_prefix(SUCCESS, tool_name, tag_name)); + pub fn success(&self, pb: ProgressBar, tool_name: &str, tag: &str) { + pb.set_prefix(self.fmt_prefix(SUCCESS, tool_name, tag)); let success_msg = format!("{}", style("Completed!").bold().green()); pb.set_message(success_msg); pb.finish(); } - pub fn failure(&self, pb: ProgressBar, tool_name: &str, tag_name: &str, err_msg: String) { - pb.set_prefix(self.fmt_prefix(FAILURE, tool_name, tag_name)); + pub fn failure(&self, pb: ProgressBar, tool_name: &str, tag: &str, err_msg: String) { + pb.set_prefix(self.fmt_prefix(FAILURE, tool_name, tag)); let failure_msg = format!("{}", style(err_msg).red()); pb.set_message(failure_msg); @@ -88,47 +95,55 @@ impl SyncProgress { #[cfg(test)] mod tests { - use super::SyncProgress; + use super::*; #[test] fn test_max_tag_size_specific() { - let tags: Vec = vec![ - String::from("v10.10.100"), - String::from("latest"), - String::from("latest"), - ]; - let tools: Vec = vec![ - String::from("ripgrep"), - String::from("bat"), - String::from("exa"), + let tool_pairs = vec![ + ToolPair { + name: "ripgrep", + tag: "v10.10.100", + }, + ToolPair { + name: "bat", + tag: "latest", + }, + ToolPair { + name: "exa", + tag: "latest", + }, ]; - let progres = SyncProgress::new(tools, tags); + let progress = SyncProgress::new(tool_pairs); // v10.10.100 is 10 characters - assert_eq!(progres.max_tag_size, 10); + assert_eq!(progress.max_tag_size, 10); // ripgrep is 7 characters - assert_eq!(progres.max_tool_size, 7); + assert_eq!(progress.max_tool_size, 7); } #[test] fn test_max_tag_size_latest() { - let tags: Vec = vec![ - String::from("latest"), - String::from("latest"), - String::from("latest"), - ]; - let tools: Vec = vec![ - String::from("ripgrep"), - String::from("bat"), - String::from("exa"), + let tool_pairs = vec![ + ToolPair { + name: "ripgrep", + tag: "latest", + }, + ToolPair { + name: "bat", + tag: "latest", + }, + ToolPair { + name: "exa", + tag: "latest", + }, ]; - let progres = SyncProgress::new(tools, tags); + let progress = SyncProgress::new(tool_pairs); // latest is 6 characters so it should default to 8 - assert_eq!(progres.max_tag_size, 8); + assert_eq!(progress.max_tag_size, MIN_TAG_SIZE); // ripgrep is 7 characters - assert_eq!(progres.max_tool_size, 7); + assert_eq!(progress.max_tool_size, 7); } }