use std::{
collections::HashMap,
env,
path::{Path, PathBuf},
};
use crate::absolute_path::AbsolutePathExt;
use console::style;
use regex::Regex;
use crate::{app_config::AppConfig, template_variables::CrateType, GenerateArgs, Vcs};
use log::warn;
#[derive(Debug)]
#[cfg(test)]
pub struct UserParsedInputBuilder {
subject: UserParsedInput,
}
#[cfg(test)]
impl UserParsedInputBuilder {
#[cfg(test)]
pub(crate) fn for_testing() -> Self {
use crate::TemplatePath;
Self {
subject: UserParsedInput::try_from_args_and_config(
AppConfig::default(),
&GenerateArgs {
destination: Some(Path::new("/tmp/dest/").to_path_buf()),
template_path: TemplatePath {
path: Some("/tmp".to_string()),
..TemplatePath::default()
},
..GenerateArgs::default()
},
),
}
}
pub const fn with_force(mut self) -> Self {
self.subject.force = true;
self
}
pub fn build(self) -> UserParsedInput {
self.subject
}
}
#[derive(Debug)]
pub struct UserParsedInput {
name: Option<String>,
template_location: TemplateLocation,
destination: PathBuf,
subfolder: Option<String>,
template_values: HashMap<String, toml::Value>,
vcs: Vcs,
pub init: bool,
overwrite: bool,
crate_type: CrateType,
allow_commands: bool,
silent: bool,
force: bool,
test: bool,
force_git_init: bool,
}
impl UserParsedInput {
pub fn try_from_args_and_config(app_config: AppConfig, args: &GenerateArgs) -> Self {
const DEFAULT_VCS: Vcs = Vcs::Git;
let destination = args
.destination
.as_ref()
.map(|p| {
p.as_absolute()
.expect("cannot get the absolute path of the destination folder")
.to_path_buf()
})
.unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| ".".into()));
let mut default_values = app_config.values.clone().unwrap_or_default();
let ssh_identity = app_config
.defaults
.as_ref()
.and_then(|dcfg| dcfg.ssh_identity.clone())
.or_else(|| {
args.ssh_identity.as_ref().cloned().or_else(|| {
app_config
.defaults
.as_ref()
.and_then(|defaults| defaults.ssh_identity.clone())
})
});
if let Some(git_url) = args.template_path.git() {
let resolved_url = abbreviated_git_url_to_full_remote(git_url.as_ref())
.or_else(|| {
if local_path(git_url.as_ref()).is_none() {
abbreviated_github(git_url.as_ref())
} else {
None
}
})
.unwrap_or_else(|| git_url.as_ref().to_owned());
let git_user_in = GitUserInput::new(
&resolved_url,
args.template_path.branch(),
args.template_path.tag(),
args.template_path.revision(),
ssh_identity,
args.gitconfig.clone(),
args.force_git_init,
args.skip_submodules,
);
return Self {
name: args.name.clone(),
template_location: git_user_in.into(),
subfolder: args
.template_path
.subfolder()
.map(|s| s.as_ref().to_owned()),
template_values: default_values,
vcs: args.vcs.unwrap_or(DEFAULT_VCS),
init: args.init,
overwrite: args.overwrite,
crate_type: CrateType::from(args),
allow_commands: args.allow_commands,
silent: args.silent,
destination,
force: args.force,
test: args.template_path.test,
force_git_init: args.force_git_init,
};
}
if let Some(path) = args.template_path.path() {
return Self {
name: args.name.clone(),
template_location: path.as_ref().into(),
subfolder: args
.template_path
.subfolder()
.map(|s| s.as_ref().to_owned()),
template_values: default_values,
vcs: args.vcs.unwrap_or(DEFAULT_VCS),
init: args.init,
overwrite: args.overwrite,
crate_type: CrateType::from(args),
allow_commands: args.allow_commands,
silent: args.silent,
destination,
force: args.force,
test: args.template_path.test,
force_git_init: args.force_git_init,
};
}
let fav_name = args.template_path.any_path();
if let Some(fav_cfg) = app_config.get_favorite_cfg(fav_name) {
assert!(fav_cfg.git.is_none() || fav_cfg.path.is_none());
let temp_location = fav_cfg.git.as_ref().map_or_else(
|| fav_cfg.path.as_ref().map(TemplateLocation::from).unwrap(),
|git_url| {
let branch = args
.template_path
.branch()
.map(|s| s.as_ref().to_owned())
.or_else(|| fav_cfg.branch.clone());
let tag = args
.template_path
.tag()
.map(|s| s.as_ref().to_owned())
.or_else(|| fav_cfg.tag.clone());
let revision = args
.template_path
.revision()
.map(|s| s.as_ref().to_owned())
.or_else(|| fav_cfg.revision.clone());
let git_user_input = GitUserInput::new(
git_url,
branch.as_ref(),
tag.as_ref(),
revision.as_ref(),
ssh_identity,
None,
args.force_git_init,
args.skip_submodules,
);
TemplateLocation::from(git_user_input)
},
);
if let Some(fav_default_values) = &fav_cfg.values {
default_values.extend(fav_default_values.clone());
}
return Self {
name: args.name.clone(),
template_location: temp_location,
subfolder: args
.template_path
.subfolder()
.map(|s| s.as_ref().to_owned())
.or_else(|| fav_cfg.subfolder.clone()),
template_values: default_values,
vcs: args.vcs.or(fav_cfg.vcs).unwrap_or(DEFAULT_VCS),
init: args
.init
.then_some(true)
.or(fav_cfg.init)
.unwrap_or_default(),
overwrite: args
.overwrite
.then_some(true)
.or(fav_cfg.overwrite)
.unwrap_or_default(),
crate_type: CrateType::from(args),
allow_commands: args.allow_commands,
silent: args.silent,
destination,
force: args.force,
test: args.template_path.test,
force_git_init: args.force_git_init,
};
}
let temp_location = abbreviated_git_url_to_full_remote(fav_name).map(|git_url| {
let git_user_in = GitUserInput::with_git_url_and_args(&git_url, args);
TemplateLocation::from(git_user_in)
});
let temp_location =
temp_location.or_else(|| local_path(fav_name).map(TemplateLocation::from));
let temp_location = temp_location.or_else(|| {
abbreviated_github(fav_name).map(|git_url| {
let git_user_in = GitUserInput::with_git_url_and_args(&git_url, args);
TemplateLocation::from(git_user_in)
})
});
let temp_location = temp_location.unwrap_or_else(|| {
let git_user_in = GitUserInput::new(
&fav_name,
args.template_path.branch(),
args.template_path.tag(),
args.template_path.revision(),
ssh_identity,
args.gitconfig.clone(),
args.force_git_init,
args.skip_submodules,
);
TemplateLocation::from(git_user_in)
});
let location_msg = match &temp_location {
TemplateLocation::Git(git_user_input) => {
format!("git repository: {}", style(git_user_input.url()).bold())
}
TemplateLocation::Path(path) => {
format!("local path: {}", style(path.display()).bold())
}
};
warn!(
"Favorite `{}` not found in config, using it as a {}",
style(&fav_name).bold(),
location_msg
);
Self {
name: args.name.clone(),
template_location: temp_location,
subfolder: args
.template_path
.subfolder()
.map(|s| s.as_ref().to_owned()),
template_values: default_values,
vcs: args.vcs.unwrap_or(DEFAULT_VCS),
init: args.init,
overwrite: args.overwrite,
crate_type: CrateType::from(args),
allow_commands: args.allow_commands,
silent: args.silent,
destination,
force: args.force,
test: args.template_path.test,
force_git_init: args.force_git_init,
}
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub const fn location(&self) -> &TemplateLocation {
&self.template_location
}
pub fn subfolder(&self) -> Option<&str> {
self.subfolder.as_deref()
}
pub const fn template_values(&self) -> &HashMap<String, toml::Value> {
&self.template_values
}
pub const fn template_values_mut(&mut self) -> &mut HashMap<String, toml::Value> {
&mut self.template_values
}
pub const fn vcs(&self) -> Vcs {
self.vcs
}
pub const fn init(&self) -> bool {
self.init
}
pub const fn overwrite(&self) -> bool {
self.overwrite
}
pub const fn crate_type(&self) -> CrateType {
self.crate_type
}
pub const fn allow_commands(&self) -> bool {
self.allow_commands
}
pub const fn silent(&self) -> bool {
self.silent
}
pub fn destination(&self) -> &Path {
self.destination.as_path()
}
pub const fn force(&self) -> bool {
self.force
}
pub const fn test(&self) -> bool {
self.test
}
pub const fn force_git_init(&self) -> bool {
self.force_git_init
}
}
pub fn abbreviated_git_url_to_full_remote(git: impl AsRef<str>) -> Option<String> {
let git = git.as_ref();
if git.len() >= 3 {
match &git[..3] {
"gl:" => Some(format!("https://gitlab.com/{}.git", &git[3..])),
"bb:" => Some(format!("https://bitbucket.org/{}.git", &git[3..])),
"gh:" => Some(format!("https://github.com/{}.git", &git[3..])),
"sr:" => Some(format!("https://git.sr.ht/~{}", &git[3..])),
short_cut_maybe if is_abbreviated_github(short_cut_maybe) => {
Some(format!("https://github.com/{short_cut_maybe}.git"))
}
_ => None,
}
} else {
None
}
}
fn is_abbreviated_github(fav: &str) -> bool {
let org_repo_regex = Regex::new(r"^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_%-]+$").unwrap();
org_repo_regex.is_match(fav)
}
pub fn abbreviated_github(fav: &str) -> Option<String> {
is_abbreviated_github(fav).then(|| format!("https://github.com/{fav}.git"))
}
pub fn local_path(fav: &str) -> Option<PathBuf> {
let path = PathBuf::from(fav);
(path.exists() && path.is_dir()).then_some(path)
}
#[derive(Debug)]
pub struct GitUserInput {
url: String,
branch: Option<String>,
tag: Option<String>,
revision: Option<String>,
identity: Option<PathBuf>,
gitconfig: Option<PathBuf>,
_force_init: bool,
pub skip_submodules: bool,
}
impl GitUserInput {
#[allow(clippy::too_many_arguments)]
fn new(
url: &impl AsRef<str>,
branch: Option<&impl AsRef<str>>,
tag: Option<&impl AsRef<str>>,
revision: Option<&impl AsRef<str>>,
identity: Option<PathBuf>,
gitconfig: Option<PathBuf>,
force_init: bool,
skip_submodules: bool,
) -> Self {
Self {
url: url.as_ref().to_owned(),
branch: branch.map(|s| s.as_ref().to_owned()),
tag: tag.map(|s| s.as_ref().to_owned()),
revision: revision.map(|s| s.as_ref().to_owned()),
identity,
gitconfig,
_force_init: force_init,
skip_submodules,
}
}
fn with_git_url_and_args(url: &impl AsRef<str>, args: &GenerateArgs) -> Self {
Self::new(
url,
args.template_path.branch(),
args.template_path.tag(),
args.template_path.revision(),
args.ssh_identity.clone(),
args.gitconfig.clone(),
args.force_git_init,
args.skip_submodules,
)
}
pub fn url(&self) -> &str {
self.url.as_ref()
}
pub fn branch(&self) -> Option<&str> {
self.branch.as_deref()
}
pub fn tag(&self) -> Option<&str> {
self.tag.as_deref()
}
pub fn revision(&self) -> Option<&str> {
self.revision.as_deref()
}
pub fn identity(&self) -> Option<&Path> {
self.identity.as_deref()
}
pub fn gitconfig(&self) -> Option<&Path> {
self.gitconfig.as_deref()
}
}
#[derive(Debug)]
pub enum TemplateLocation {
Git(GitUserInput),
Path(PathBuf),
}
impl From<GitUserInput> for TemplateLocation {
fn from(source: GitUserInput) -> Self {
Self::Git(source)
}
}
impl<T> From<T> for TemplateLocation
where
T: AsRef<Path>,
{
fn from(source: T) -> Self {
Self::Path(PathBuf::from(source.as_ref()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_support_colon_abbreviations() {
assert_eq!(
&abbreviated_git_url_to_full_remote("gh:foo/bar").unwrap(),
"https://github.com/foo/bar.git"
);
assert_eq!(
&abbreviated_git_url_to_full_remote("bb:foo/bar").unwrap(),
"https://bitbucket.org/foo/bar.git"
);
assert_eq!(
&abbreviated_git_url_to_full_remote("gl:foo/bar").unwrap(),
"https://gitlab.com/foo/bar.git"
);
assert_eq!(
&abbreviated_git_url_to_full_remote("sr:foo/bar").unwrap(),
"https://git.sr.ht/~foo/bar"
);
assert!(&abbreviated_git_url_to_full_remote("foo/bar").is_none());
}
#[test]
fn should_appreviation_org_repo_to_github() {
assert_eq!(
&abbreviated_github("org/repo").unwrap(),
"https://github.com/org/repo.git"
);
assert!(&abbreviated_github("path/to/a/sth").is_none());
}
fn resolve_git_flag(git_value: &str) -> String {
let args = GenerateArgs {
destination: Some(std::path::PathBuf::from("/tmp")),
template_path: crate::TemplatePath {
git: Some(git_value.to_owned()),
..crate::TemplatePath::default()
},
..GenerateArgs::default()
};
let parsed = UserParsedInput::try_from_args_and_config(AppConfig::default(), &args);
match parsed.location() {
TemplateLocation::Git(git) => git.url().to_owned(),
TemplateLocation::Path(p) => panic!("expected Git location, got Path: {p:?}"),
}
}
#[test]
fn git_flag_full_url() {
assert_eq!(
resolve_git_flag("https://github.com/username-on-github/mytemplate.git"),
"https://github.com/username-on-github/mytemplate.git"
);
}
#[test]
fn git_flag_org_repo_shorthand() {
assert_eq!(
resolve_git_flag("username-on-github/mytemplate"),
"https://github.com/username-on-github/mytemplate.git"
);
}
#[test]
fn git_flag_gh_prefix() {
assert_eq!(
resolve_git_flag("gh:username-on-github/mytemplate"),
"https://github.com/username-on-github/mytemplate.git"
);
}
#[test]
fn git_flag_gl_prefix() {
assert_eq!(
resolve_git_flag("gl:username-on-gitlab/mytemplate"),
"https://gitlab.com/username-on-gitlab/mytemplate.git"
);
}
#[test]
fn git_flag_bb_prefix() {
assert_eq!(
resolve_git_flag("bb:username-on-bitbucket/mytemplate"),
"https://bitbucket.org/username-on-bitbucket/mytemplate.git"
);
}
#[test]
fn git_flag_sr_prefix() {
assert_eq!(
resolve_git_flag("sr:username-on-sourcehut/mytemplate"),
"https://git.sr.ht/~username-on-sourcehut/mytemplate"
);
}
#[test]
fn git_flag_local_path_takes_precedence_over_org_repo() {
let workspace = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(workspace.path().join("myorg/myrepo")).unwrap();
let args = GenerateArgs {
destination: Some(workspace.path().to_path_buf()),
template_path: crate::TemplatePath {
git: Some("myorg/myrepo".to_owned()),
..crate::TemplatePath::default()
},
..GenerateArgs::default()
};
std::env::set_current_dir(workspace.path()).unwrap();
let parsed = UserParsedInput::try_from_args_and_config(AppConfig::default(), &args);
std::env::set_current_dir(std::env::temp_dir()).unwrap();
match parsed.location() {
TemplateLocation::Git(git) => assert_eq!(git.url(), "myorg/myrepo"),
TemplateLocation::Path(p) => panic!("expected Git location, got Path: {p:?}"),
}
}
}