Skip to content

Commit

Permalink
feat(app): add opt-in cleanup on failures
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Mar 11, 2024
1 parent 3c54d4e commit f68baf1
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 120 deletions.
239 changes: 139 additions & 100 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<PathBuf>,
}

#[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<bool>,
}

#[derive(Debug, Subcommand)]
#[derive(Clone, Debug, Subcommand)]
pub enum BaseCommand {
/// Scaffold from a remote repository.
#[command(visible_alias = "r")]
Expand Down Expand Up @@ -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(
Expand All @@ -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<String>,
meta: Option<String>,
overrides: ConfigOptionsOverrides,
) -> miette::Result<Config> {
// 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<String>,
meta: Option<String>,
overrides: ConfigOptionsOverrides,
) -> miette::Result<Config> {
// 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(())
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
21 changes: 1 addition & 20 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions src/report.rs
Original file line number Diff line number Diff line change
@@ -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<T>(fallible: miette::Result<T>) {
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:?}");
}
}
}

0 comments on commit f68baf1

Please sign in to comment.