Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cleanup on failures #24

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 139 additions & 108 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,14 +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,

/// 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 All @@ -44,10 +63,6 @@ pub enum BaseCommand {
/// Scaffold from a specified ref (branch, tag, or commit).
#[arg(name = "REF", short = 'r', long = "ref")]
meta: Option<String>,

/// Delete arx config after scaffolding.
#[arg(short, long)]
delete: Option<bool>,
},
/// Scaffold from a local repository.
#[command(visible_alias = "l")]
Expand All @@ -61,24 +76,35 @@ pub enum BaseCommand {
/// Scaffold from a specified ref (branch, tag, or commit).
#[arg(name = "REF", short = 'r', long = "ref")]
meta: Option<String>,

/// Delete arx config after scaffolding.
#[arg(short, long)]
delete: Option<bool>,
},
}

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

pub async fn run(self) -> miette::Result<()> {
/// 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());
}
}

/// 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 @@ -90,125 +116,130 @@ impl App {
)
}))?;

// Load the config.
let config = match self.cli.command {
| BaseCommand::Remote { src, path, meta, delete } => {
let options = ConfigOptionsOverrides { delete };
Self::remote(src, path, meta, options).await?
},
| BaseCommand::Local { src, path, meta, delete } => {
let options = ConfigOptionsOverrides { delete };
Self::local(src, path, meta, options).await?
},
};
// Build override options.
let overrides = ConfigOptionsOverrides { delete: self.cli.delete };

// Create executor and kick off execution.
let executor = Executor::new(config);
executor.execute().await?;
// Cleanup on failure.
self.state.cleanup = self.cli.cleanup;

Ok(())
}
// Load the config.
let destination = match self.cli.command.clone() {
// Preparation flow for remote repositories.
| BaseCommand::Remote { src, path, meta } => {
let remote = RemoteRepository::new(src, meta)?;

/// 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()
);
}
let name = path.as_ref().unwrap_or(&remote.repo);
let destination = PathBuf::from(name);

// Fetch the tarball as bytes (compressed).
let tarball = remote.fetch().await?;
// Set cleanup path to the destination.
self.state.cleanup_path = Some(destination.clone());

// Decompress and unpack the tarball.
let unpacker = Unpacker::new(tarball);
unpacker.unpack_to(&destination)?;
// 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;

// Now we need to read the config (if it is present).
let mut config = Config::new(&destination);
miette::bail!(
"Failed to scaffold: '{}' already exists.",
destination.display()
);
}

config.load()?;
config.override_with(overrides);
// Fetch the tarball as bytes (compressed).
let tarball = remote.fetch().await?;

Ok(config)
}
// Decompress and unpack the tarball.
let unpacker = Unpacker::new(tarball);
unpacker.unpack_to(&destination)?;

/// 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()
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
},
};

// 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");
// Read the config (if it is present).
let mut config = Config::new(&destination);

if let Ok(true) = inner_git.try_exists() {
println!("{}", "~ Cloned repository".dim());
config.load()?;
config.override_with(overrides);

// 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:?}");
}
}
}
Loading