use std::{collections::HashMap, convert::TryFrom, env, fs};
use anyhow::{anyhow, bail, ensure, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_hatch::{
cargo,
cli::{self, Command, CreationFlags},
dirs::Utf8ProjectDirs,
repo,
settings::{self, DefaultSetting},
templates,
};
use inquire::Confirm;
fn main() -> Result<()> {
let cmd = cli::parse();
let dirs = Utf8ProjectDirs::new()?;
match cmd {
Command::Init { name } => {
let mut cwd = Utf8PathBuf::try_from(env::current_dir()?)?;
if let Some(name) = name {
cwd.push(name);
}
println!("TODO! init at {cwd}");
}
Command::List => {
let settings = settings::load_global(&dirs)?;
let width = settings
.bookmarks
.keys()
.map(|name| name.len())
.max()
.unwrap_or_default();
for (name, info) in settings.bookmarks {
println!(
"{name:width$} - {}",
info.description.as_deref().unwrap_or_default(),
);
}
}
Command::New { bookmark, flags } => {
let mut settings = settings::load_global(&dirs)?;
let bookmark = settings
.bookmarks
.remove(&bookmark)
.ok_or_else(|| anyhow!("bookmark with name `{bookmark}` unknown"))?;
let mut path = if bookmark.repository.starts_with("git@")
|| bookmark.repository.starts_with("http:")
|| bookmark.repository.starts_with("https:")
{
let path = {
let base = dirs.cache_dir();
let repo_name = repo::find_repo_name(&bookmark.repository)
.context("can't determine repo name from git URL")?;
base.join(repo_name)
};
fs::create_dir_all(&path)?;
repo::clone_or_update(&bookmark.repository, &path).context("failed cloning")?;
path
} else if fs::metadata(&bookmark.repository)
.map(|meta| meta.is_dir())
.unwrap_or_default()
{
Utf8PathBuf::from(&bookmark.repository)
} else {
bail!(
"configured bookmark repository doesn't seem to be remote git repo URL nor a \
local machine folder"
);
};
if let Some(folder) = &bookmark.folder {
path.push(folder);
}
generate_project(&path, flags, bookmark.defaults)?;
println!("done!");
}
Command::Git { folder, url, flags } => {
let mut path = {
let base = dirs.cache_dir();
let repo_name =
repo::find_repo_name(&url).context("can't determine repo name from git URL")?;
base.join(repo_name)
};
fs::create_dir_all(&path)?;
repo::clone_or_update(&url, &path).context("failed cloning")?;
if let Some(folder) = folder {
path.push(folder);
}
generate_project(&path, flags, HashMap::new())?;
println!("done!");
}
Command::Local { path, flags } => {
generate_project(&path, flags, HashMap::new())?;
println!("done!");
}
Command::Completions { shell } => cli::completions(shell),
Command::Manpages { dir } => cli::manpages(&dir)?,
}
Ok(())
}
fn generate_project(
path: &Utf8Path,
flags: CreationFlags,
defaults: HashMap<String, DefaultSetting>,
) -> Result<()> {
let (name, target) = get_target_dir(flags.name).context("failed preparing target directory")?;
let files = templates::collect_files(path).context("failed collecting files")?;
let repo_settings = settings::load_repo(path).context("failed loading hatch config")?;
let mut context =
settings::new_context(&repo_settings, &name).context("failed creating context")?;
settings::fill_context(&mut context, repo_settings.args, defaults)
.context("failed filling context")?;
let files = templates::filter_ignored(files, &context, repo_settings.ignore)?;
templates::render(&files, &context, &target).context("failed rendering templates")?;
if flags.update_deps {
cargo::update_all_cargo_tomls(&target, &files)?;
}
repo::init(&target).context("failed initializing git repository")?;
Ok(())
}
fn get_target_dir(name: Option<Utf8PathBuf>) -> Result<(String, Utf8PathBuf)> {
let out = env::current_dir().context("failed getting current directory")?;
let mut out = Utf8PathBuf::try_from(out).context("current directory is not valid UTF-8")?;
if let Some(name) = name {
out.push(name);
}
let name = out
.file_name()
.context("directory can't be used as project name")?
.to_owned();
ensure!(
!out.exists() || out.is_dir(),
"target diretory appears to be an existing file"
);
if out.exists() && !is_dir_empty(&out)? {
let mut prompt = Confirm::new("target directory already exists. Do you want to continue?");
prompt.default = Some(false);
prompt.help_message = Some("if you continue, the directory will be cleared beforehand");
if prompt.prompt().context("failed to prompt for user input")? {
fs::remove_dir_all(&out).context("failed clearing output directory")?;
} else {
bail!("generation cancelled by user");
}
}
Ok((name, out))
}
fn is_dir_empty(path: &Utf8Path) -> Result<bool> {
Ok(path
.read_dir()
.context("failed listing directory entries")?
.next()
.is_none())
}