Skip to content

Commit

Permalink
feat(cache): add commands for listing entries and purging all entries
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Apr 10, 2024
1 parent 6565664 commit 4b0b146
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 164 deletions.
317 changes: 174 additions & 143 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::fs;
use std::io;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
use crossterm::style::Stylize;
use miette::Diagnostic;
use thiserror::Error;
Expand Down Expand Up @@ -34,49 +34,52 @@ pub struct AppState {
pub cleanup_path: Option<PathBuf>,
}

#[derive(Parser, Debug)]
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: BaseCommand,
pub enum Cli {
/// Scaffold from a remote repository.
#[command(visible_alias = "r")]
Remote(RepositoryArgs),
/// Scaffold from a local repository.
#[command(visible_alias = "l")]
Local(RepositoryArgs),
/// Commands for interacting with the cache.
#[command(visible_alias = "c")]
Cache {
#[command(subcommand)]
command: CacheCommand,
},
}

#[derive(Clone, Debug, Args)]
pub struct RepositoryArgs {
/// Repository to use for scaffolding.
src: String,
/// Directory to scaffold to.
path: Option<String>,
/// Scaffold from a specified ref (branch, tag, or commit).
#[arg(name = "REF", short = 'r', long = "ref")]
meta: Option<String>,
/// Clean up on failure. No-op if failed because target directory already exists.
#[arg(global = true, short = 'C', long)]
#[arg(short = 'C', long)]
cleanup: bool,
/// Delete arx config after scaffolding is complete.
#[arg(global = true, short, long)]
/// Delete config after scaffolding is complete.
#[arg(short, long)]
delete: Option<bool>,
/// Skip reading config and running actions.
#[arg(short, long)]
skip: bool,
/// Use cached template if available.
#[arg(global = true, short = 'c', long, default_value = "true")]
#[arg(short = 'c', long, default_value = "true")]
cache: bool,
/// Skip running actions.
#[arg(global = true, short, long)]
skip: bool,
}

#[derive(Clone, Debug, Subcommand)]
pub enum BaseCommand {
/// Scaffold from a remote repository.
#[command(visible_alias = "r")]
Remote {
/// Repository to use for scaffolding.
src: String,
/// Directory to scaffold to.
path: Option<String>,
/// Scaffold from a specified ref (branch, tag, or commit).
#[arg(name = "REF", short = 'r', long = "ref")]
meta: Option<String>,
},
/// Scaffold from a local repository.
#[command(visible_alias = "l")]
Local {
/// Repository to use for scaffolding.
src: String,
/// Directory to scaffold to.
path: Option<String>,
/// Scaffold from a specified ref (branch, tag, or commit).
#[arg(name = "REF", short = 'r', long = "ref")]
meta: Option<String>,
},
pub enum CacheCommand {
/// List cache entries.
List,
/// Remove all cache entries.
Clear,
}

#[derive(Debug)]
Expand Down Expand Up @@ -118,142 +121,161 @@ impl App {

/// Kicks of the scaffolding process.
pub async fn scaffold(&mut self) -> miette::Result<()> {
// Build override options.
let overrides = ConfigOptionsOverrides { delete: self.cli.delete };

// Cleanup on failure.
self.state.cleanup = self.cli.cleanup;
match self.cli.clone() {
| Cli::Remote(args) => self.scaffold_remote(args).await,
| Cli::Local(args) => self.scaffold_local(args).await,
| Cli::Cache { command } => self.handle_cache(command),
}
}

// Retrieve the template.
let destination = match self.cli.command.clone() {
| BaseCommand::Remote { src, path, meta } => {
let mut remote = RemoteRepository::new(src, meta)?;
async fn scaffold_remote(&mut self, args: RepositoryArgs) -> miette::Result<()> {
let mut remote = RemoteRepository::new(args.src, args.meta)?;

// Try to fetch refs early. If we can't get them, there's no point in continuing.
remote.fetch_refs()?;
// Try to fetch refs early. If we can't get them, there's no point in continuing.
remote.fetch_refs()?;

// Try to resolve a ref to specific hash.
let hash = remote.resolve_hash()?;
// Try to resolve a ref to specific hash.
let hash = remote.resolve_hash()?;

let name = path.as_ref().unwrap_or(&remote.repo);
let destination = PathBuf::from(name);
let name = args.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());
// Cleanup on failure.
self.state.cleanup = args.cleanup;
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()
);
}

// 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;
let mut cache = Cache::init()?;
let mut bytes = None;

miette::bail!(
"Failed to scaffold: '{}' already exists.",
destination.display()
);
}
let source = remote.get_source();
let mut should_fetch = !args.cache;

let mut cache = Cache::init()?;
let mut bytes = None;
if args.cache {
println!("{}", "~ Attempting to read from cache".dim());

let source = remote.get_source();
let mut should_fetch = !self.cli.cache;
if let Some(cached) = cache.read(&source, &hash)? {
println!("{}", "~ Found in cache, reading".dim());
bytes = Some(cached);
} else {
println!("{}", "~ Nothing found in cache, fetching".dim());
should_fetch = true;
}
}

if self.cli.cache {
println!("{}", "~ Attempting to read from cache".dim());
if should_fetch {
bytes = Some(remote.fetch().await?);
}

if let Some(cached) = cache.read(&source, &hash)? {
println!("{}", "~ Found in cache, reading".dim());
bytes = Some(cached);
} else {
println!("{}", "~ Nothing found in cache, fetching".dim());
should_fetch = true;
}
}
// Decompress and unpack the tarball. If somehow the tarball is empty, bail.
if let Some(bytes) = bytes {
if should_fetch {
cache.write(&source, &remote.meta.to_string(), &hash, &bytes)?;
}

if should_fetch {
bytes = Some(remote.fetch().await?);
}
let unpacker = Unpacker::new(bytes);
unpacker.unpack_to(&destination)?;
} else {
miette::bail!("Failed to scaffold: zero bytes.");
}

// Decompress and unpack the tarball. If somehow the tarball is empty, bail.
if let Some(bytes) = bytes {
if should_fetch {
cache.write(&source, &hash, &bytes)?;
}
self
.scaffold_execute(
&destination,
args.skip,
ConfigOptionsOverrides { delete: args.delete },
)
.await
}

let unpacker = Unpacker::new(bytes);
unpacker.unpack_to(&destination)?;
} else {
miette::bail!("Failed to scaffold: zero bytes.");
}
async fn scaffold_local(&mut self, args: RepositoryArgs) -> miette::Result<()> {
let local = LocalRepository::new(args.src, args.meta);

destination
},
| 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()
);
}
let destination = if let Some(destination) = args.path {
PathBuf::from(destination)
} else {
local
.source
.file_name()
.map(PathBuf::from)
.unwrap_or_default()
};

// Copy the directory.
local.copy(&destination)?;
// Cleanup on failure.
self.state.cleanup = args.cleanup;
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()
);
}

// .git directory path.
let inner_git = destination.join(".git");
// Copy the directory.
local.copy(&destination)?;

// If we copied a repository, we also need to checkout the ref.
if let Ok(true) = inner_git.try_exists() {
println!("{}", "~ Cloned repository".dim());
// .git directory path.
let inner_git = destination.join(".git");

// Checkout the ref.
local.checkout(&destination)?;
// If we copied a repository, we also need to checkout the ref.
if let Ok(true) = inner_git.try_exists() {
println!("{}", "~ Cloned repository".dim());

println!("{} {}", "~ Checked out ref:".dim(), local.meta.0.dim());
// Checkout the ref.
local.checkout(&destination)?;

// 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!("{} {}", "~ Checked out ref:".dim(), local.meta.0.dim());

println!("{}", "~ Removed inner .git directory".dim());
} else {
println!("{}", "~ Copied directory".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,
}
})?;

destination
},
};
println!("{}", "~ Removed inner .git directory".dim());
} else {
println!("{}", "~ Copied directory".dim());
}

self
.scaffold_execute(
&destination,
args.skip,
ConfigOptionsOverrides { delete: args.delete },
)
.await
}

if self.cli.skip {
async fn scaffold_execute(
&mut self,
destination: &Path,
should_skip: bool,
overrides: ConfigOptionsOverrides,
) -> miette::Result<()> {
if should_skip {
println!("{}", "~ Skipping running actions".dim());
return Ok(());
}

// Read the config (if it is present).
let mut config = Config::new(&destination);
let mut config = Config::new(destination);

if config.load()? {
println!();
Expand All @@ -269,8 +291,17 @@ impl App {
}
}

fn handle_cache(&mut self, command: CacheCommand) -> miette::Result<()> {
let mut cache = Cache::init()?;

match command {
| CacheCommand::List => Ok(cache.list()?),
| CacheCommand::Clear => Ok(cache.clear()?),
}
}

/// Clean up on failure.
pub fn cleanup(&self) -> miette::Result<()> {
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| {
Expand Down
Loading

0 comments on commit 4b0b146

Please sign in to comment.