diff --git a/src/app.rs b/src/app.rs index 26a190b..15508e0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,6 +9,7 @@ use thiserror::Error; use crate::actions::Executor; use crate::config::{Config, ConfigOptionsOverrides}; +use crate::report; use crate::repository::{LocalRepository, RemoteRepository}; use crate::unpacker::Unpacker; @@ -23,18 +24,32 @@ pub enum AppError { }, } +#[derive(Debug, Default)] +pub struct AppState { + /// Whether to cleanup on failure or not. + pub cleanup: bool, + /// Cleanup path, will be set to the destination acquired after creating [RemoteRepository] or + /// [LocalRepository]. + pub cleanup_path: Option, +} + #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Cli { #[command(subcommand)] pub command: BaseCommand, - /// Delete arx config after scaffolding. + /// Cleanup on failure, i.e. delete target directory. No-op if failed because target directory + /// does not exist. + #[arg(global = true, short, long)] + cleanup: bool, + + /// Delete arx config after scaffolding is complete. #[arg(global = true, short, long)] delete: Option, } -#[derive(Debug, Subcommand)] +#[derive(Clone, Debug, Subcommand)] pub enum BaseCommand { /// Scaffold from a remote repository. #[command(visible_alias = "r")] @@ -67,14 +82,29 @@ pub enum BaseCommand { #[derive(Debug)] pub struct App { cli: Cli, + state: AppState, } impl App { pub fn new() -> Self { - Self { cli: Cli::parse() } + Self { + cli: Cli::parse(), + state: AppState::default(), + } + } + + /// Runs the app and prints any errors. + pub async fn run(&mut self) { + let scaffold_res = self.scaffold().await; + + if scaffold_res.is_err() { + report::try_report(scaffold_res); + report::try_report(self.cleanup()); + } } - pub async fn run(self) -> miette::Result<()> { + /// Kicks of the scaffolding process. + pub async fn scaffold(&mut self) -> miette::Result<()> { // Slightly tweak miette. miette::set_hook(Box::new(|_| { Box::new( @@ -86,121 +116,130 @@ impl App { ) }))?; + // Build override options. let overrides = ConfigOptionsOverrides { delete: self.cli.delete }; + // Cleanup on failure. + self.state.cleanup = self.cli.cleanup; + // Load the config. - let config = match self.cli.command { - | BaseCommand::Remote { src, path, meta } => Self::remote(src, path, meta, overrides).await?, - | BaseCommand::Local { src, path, meta } => Self::local(src, path, meta, overrides).await?, + let destination = match self.cli.command.clone() { + // Preparation flow for remote repositories. + | BaseCommand::Remote { src, path, meta } => { + let remote = RemoteRepository::new(src, meta)?; + + let name = path.as_ref().unwrap_or(&remote.repo); + let destination = PathBuf::from(name); + + // Set cleanup path to the destination. + self.state.cleanup_path = Some(destination.clone()); + + // Check if destination already exists before downloading. + if let Ok(true) = &destination.try_exists() { + // We do not want to remove already existing directory. + self.state.cleanup = false; + + miette::bail!( + "Failed to scaffold: '{}' already exists.", + destination.display() + ); + } + + // Fetch the tarball as bytes (compressed). + let tarball = remote.fetch().await?; + + // Decompress and unpack the tarball. + let unpacker = Unpacker::new(tarball); + unpacker.unpack_to(&destination)?; + + destination + }, + // Preparation flow for local repositories. + | BaseCommand::Local { src, path, meta } => { + let local = LocalRepository::new(src, meta); + + let destination = if let Some(destination) = path { + PathBuf::from(destination) + } else { + local + .source + .file_name() + .map(PathBuf::from) + .unwrap_or_default() + }; + + // Set cleanup path to the destination. + self.state.cleanup_path = Some(destination.clone()); + + // Check if destination already exists before performing local clone. + if let Ok(true) = &destination.try_exists() { + // We do not want to remove already existing directory. + self.state.cleanup = false; + + miette::bail!( + "Failed to scaffold: '{}' already exists.", + destination.display() + ); + } + + // Copy the directory. + local.copy(&destination)?; + + // .git directory path. + let inner_git = destination.join(".git"); + + // If we copied a repository, we also need to checkout the ref. + if let Ok(true) = inner_git.try_exists() { + println!("{}", "~ Cloned repository".dim()); + + // Checkout the ref. + local.checkout(&destination)?; + + println!("{} {}", "~ Checked out ref:".dim(), local.meta.0.dim()); + + // At last, remove the inner .git directory. + fs::remove_dir_all(inner_git).map_err(|source| { + AppError::Io { + message: "Failed to remove inner .git directory.".to_string(), + source, + } + })?; + + println!("{}", "~ Removed inner .git directory\n".dim()); + } else { + println!("{}", "~ Copied directory\n".dim()); + } + + destination + }, }; - // Create executor and kick off execution. - let executor = Executor::new(config); - executor.execute().await?; - - Ok(()) - } - - /// Preparation flow for remote repositories. - async fn remote( - src: String, - path: Option, - meta: Option, - overrides: ConfigOptionsOverrides, - ) -> miette::Result { - // Parse repository. - let remote = RemoteRepository::new(src, meta)?; - - let name = path.unwrap_or(remote.repo.clone()); - let destination = PathBuf::from(name); - - // Check if destination already exists before downloading. - if let Ok(true) = &destination.try_exists() { - miette::bail!( - "Failed to scaffold: '{}' already exists.", - destination.display() - ); - } - - // Fetch the tarball as bytes (compressed). - let tarball = remote.fetch().await?; - - // Decompress and unpack the tarball. - let unpacker = Unpacker::new(tarball); - unpacker.unpack_to(&destination)?; - - // Now we need to read the config (if it is present). + // Read the config (if it is present). let mut config = Config::new(&destination); config.load()?; config.override_with(overrides); - Ok(config) - } - - /// Preparation flow for local repositories. - async fn local( - src: String, - path: Option, - meta: Option, - overrides: ConfigOptionsOverrides, - ) -> miette::Result { - // Create repository. - let local = LocalRepository::new(src, meta); - - let destination = if let Some(destination) = path { - PathBuf::from(destination) - } else { - local - .source - .file_name() - .map(PathBuf::from) - .unwrap_or_default() - }; - - // Check if destination already exists before performing local clone. - if let Ok(true) = &destination.try_exists() { - miette::bail!( - "Failed to scaffold: '{}' already exists.", - destination.display() - ); - } - - // Copy the directory. - local.copy(&destination)?; - - // .git directory path. - let inner_git = destination.join(".git"); - - if let Ok(true) = inner_git.try_exists() { - println!("{}", "~ Cloned repository".dim()); - - // Checkout the ref. - local.checkout(&destination)?; + // Create executor and kick off execution. + let executor = Executor::new(config); - println!("{} {}", "~ Checked out ref:".dim(), local.meta.0.dim()); + executor.execute().await + } - if let Ok(true) = inner_git.try_exists() { - fs::remove_dir_all(inner_git).map_err(|source| { + /// Cleanup on failure. + pub fn cleanup(&self) -> miette::Result<()> { + if self.state.cleanup { + if let Some(destination) = &self.state.cleanup_path { + fs::remove_dir_all(destination).map_err(|source| { AppError::Io { - message: "Failed to remove inner .git directory.".to_string(), + message: format!("Failed to remove directory: '{}'.", destination.display()), source, } })?; - - println!("{}", "~ Removed inner .git directory\n".dim()); } - } else { - println!("{}", "~ Copied directory\n".dim()); } - // Now we need to read the config (if it is present). - let mut config = Config::new(&destination); - - config.load()?; - config.override_with(overrides); - - Ok(config) + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index b585c68..fff5b60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub(crate) mod actions; pub mod app; pub(crate) mod config; pub(crate) mod path; +pub(crate) mod report; pub(crate) mod repository; pub(crate) mod spinner; pub(crate) mod unpacker; diff --git a/src/main.rs b/src/main.rs index b8de888..0ec0b7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,6 @@ use arx::app::App; -use crossterm::style::Stylize; -use miette::Severity; #[tokio::main] async fn main() { - let app = App::new(); - - if let Err(err) = app.run().await { - let severity = match err.severity().unwrap_or(Severity::Error) { - | Severity::Advice => "Advice:".cyan(), - | Severity::Warning => "Warning:".yellow(), - | Severity::Error => "Error:".red(), - }; - - if err.code().is_some() { - eprintln!("{severity} {err:?}"); - } else { - eprintln!("{severity}\n"); - eprintln!("{err:?}"); - } - - std::process::exit(1); - } + App::new().run().await } diff --git a/src/report.rs b/src/report.rs new file mode 100644 index 0000000..84579e5 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,20 @@ +use crossterm::style::Stylize; +use miette::Severity; + +/// Prints an error message and exits the program if given an error. +pub fn try_report(fallible: miette::Result) { + if let Err(err) = fallible { + let severity = match err.severity().unwrap_or(Severity::Error) { + | Severity::Advice => "Advice:".cyan(), + | Severity::Warning => "Warning:".yellow(), + | Severity::Error => "Error:".red(), + }; + + if err.code().is_some() { + eprintln!("{severity} {err:?}"); + } else { + eprintln!("{severity}\n"); + eprintln!("{err:?}"); + } + } +}