Skip to content

Commit

Permalink
refactor: redesign cli and make it more shell-friendly
Browse files Browse the repository at this point in the history
I decided to switch to command-based design and get rid of `file:` prefix for
local repositories. This allows:

- Get rid of programmatic shell expansion and simply leverage shell
capabilities.
- Make use of proper tab completions since now there's no prefix that'd been
breaking such functionality, which was rather annoying.

Also refactored code a bit and slightly simplified/reorganized it.
  • Loading branch information
norskeld committed Mar 1, 2024
1 parent c09115b commit 2bedfce
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 262 deletions.
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ flate2 = { version = "1.0.28" }
git2 = { version = "0.18.1", features = ["vendored-libgit2"] }
glob-match = { version = "0.2.1" }
indoc = "2.0.4"
itertools = { version = "0.12.0" }
kdl = { version = "4.6.0" }
reqwest = { version = "0.11.22", features = ["json"] }
run_script = { version = "0.10.1" }
shellexpand = { version = "3.1.0", features = ["full"] }
tar = { version = "0.4.40" }
thiserror = { version = "1.0.51" }
tokio = { version = "1.35.0", features = ["macros", "fs", "rt-multi-thread"] }
Expand Down
172 changes: 102 additions & 70 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,101 +1,133 @@
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

use clap::Parser;
use clap::{Parser, Subcommand};

use crate::manifest::Manifest;
use crate::path::PathUtils;
use crate::repository::{Repository, RepositoryMeta};
use crate::repository::{LocalRepository, RemoteRepository};
use crate::unpacker::Unpacker;

#[derive(Parser, Debug)]
#[clap(version, about, long_about = None)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Repository to download.
#[clap(name = "target")]
pub target: String,
#[command(subcommand)]
pub command: BaseCommands,

/// Directory to download to.
#[clap(name = "path")]
pub path: Option<String>,

/// Delete arx config after download.
#[clap(short, long, display_order = 1)]
/// Delete arx config after scaffolding.
#[arg(short, long)]
pub delete: bool,
}

/// Download using specified ref (branch, tag, commit).
#[clap(short, long, display_order = 3)]
pub meta: Option<String>,
#[derive(Debug, Subcommand)]
pub enum BaseCommands {
/// Scaffold from a remote repository.
Remote {
/// Template 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.
Local {
/// Template 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 struct App;
impl BaseCommands {
pub fn path(&self) -> Option<PathBuf> {
match self {
| BaseCommands::Remote { path, .. } | BaseCommands::Local { path, .. } => {
path.as_ref().map(PathBuf::from)
},
}
}
}

#[derive(Debug)]
pub struct App {
cli: Cli,
}

impl App {
pub fn new() -> Self {
Self
Self { cli: Cli::parse() }
}

pub async fn run(&mut self) -> anyhow::Result<()> {
// Parse CLI options.
let options = Cli::parse();
pub async fn run(self) -> anyhow::Result<()> {
// TODO: For `Remote` and `Local` variants check if destination already exists before
// downloading or performing local clone.
if let Some(path) = &self.cli.command.path() {
todo!("Check if destination {path:?} already exists");
}

match self.cli.command {
| BaseCommands::Remote { src, path, meta } => Self::remote(src, path, meta).await,
| BaseCommands::Local { src, path, meta } => Self::local(src, path, meta).await,
}
}

// Parse repository information from the CLI argument.
let repository = Repository::from_str(&options.target)?;
/// Preparation flow for remote repositories.
async fn remote(src: String, path: Option<String>, meta: Option<String>) -> anyhow::Result<()> {
// Parse repository.
let remote = RemoteRepository::new(src, meta)?;

// Check if any specific meta (ref) was passed, if so, then use it; otherwise use parsed meta.
let meta = options.meta.map_or(repository.meta(), RepositoryMeta);
let repository = repository.with_meta(meta);
let name = path.unwrap_or(remote.repo.clone());
let destination = PathBuf::from(name);

// TODO: Check if destination already exists before downloading or performing local clone.
// Fetch the tarball as bytes (compressed).
let tarball = remote.fetch().await?;

// Depending on the repository type, either download and unpack or make a local clone.
let destination = match repository {
| Repository::Remote(remote) => {
let name = options.path.unwrap_or(remote.repo.clone());
let destination = PathBuf::from(name);
// Decompress and unpack the tarball.
let unpacker = Unpacker::new(tarball);
unpacker.unpack_to(&destination)?;

// Fetch the tarball as bytes (compressed).
let tarball = remote.fetch().await?;
// Now we need to read the manifest (if it is present).
let mut manifest = Manifest::with_options(&destination);
manifest.load()?;

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

destination
},
| Repository::Local(local) => {
// TODO: Check if source exists and valid.
let source = local.source.clone().expand();

let destination = if let Some(destination) = options.path {
PathBuf::from(destination).expand()
} else {
source
.file_name()
.map(|name| name.into())
.unwrap_or_default()
};

// Copy the directory.
local.copy(&destination)?;
local.checkout(&destination)?;

// Delete inner .git.
let inner_git = destination.join(".git");

if inner_git.exists() {
println!("Removing {}", inner_git.display());
fs::remove_dir_all(inner_git)?;
}

// TODO: Check if source is a plain directory or git repo. If the latter, then we should
// also do a checkout.

destination
},
/// Preparation flow for local repositories.
async fn local(src: String, path: Option<String>, meta: Option<String>) -> anyhow::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(|name| name.into())
.unwrap_or_default()
};

// Copy the directory.
local.copy(&destination)?;
local.checkout(&destination)?;

// Delete inner .git.
let inner_git = destination.join(".git");

if inner_git.exists() {
println!("Removing {}", inner_git.display());
fs::remove_dir_all(inner_git)?;
}

// Now we need to read the manifest (if it is present).
let mut manifest = Manifest::with_options(&destination);
manifest.load()?;
Expand Down
3 changes: 1 addition & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ use arx::app::App;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut app = App::new();
app.run().await
App::new().run().await
}
24 changes: 0 additions & 24 deletions src/path/expand.rs

This file was deleted.

2 changes: 0 additions & 2 deletions src/path/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
pub use expand::*;
pub use utils::*;

mod expand;
mod utils;
12 changes: 0 additions & 12 deletions src/path/utils.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
use std::path::{Path, PathBuf};

use crate::path::expand;

pub trait PathUtils {
/// Given `root`, returns `root` if `self` is `.`, otherwise returns `self`.
fn to_root<P: AsRef<Path>>(&self, root: P) -> PathBuf;

/// Expands tilde and environment variables in given `path`.
fn expand(&self) -> PathBuf;
}

impl PathUtils for Path {
Expand All @@ -18,11 +13,4 @@ impl PathUtils for Path {
self.to_path_buf()
}
}

fn expand(&self) -> PathBuf {
let path = self.display().to_string();
let expanded = expand(&path);

PathBuf::from(expanded)
}
}
Loading

0 comments on commit 2bedfce

Please sign in to comment.