use std::env;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str;
use glob::Pattern;
use tera::Context;
use walkdir::WalkDir;
use crate::definition::TemplateDefinition;
use crate::errors::{map_io_err, new_error, ErrorKind, Result};
use crate::utils::{
create_directory, get_source, is_binary, read_file, render_one_off_template, write_file, Source,
};
#[derive(Debug, PartialEq)]
pub struct Template {
path: PathBuf,
}
impl Template {
pub fn from_input(input: &str, sub_dir: Option<&str>) -> Result<Template> {
match get_source(input) {
Source::Git(remote) => Template::from_git(&remote, sub_dir),
Source::Local(path) => Ok(Template::from_local(&path, sub_dir)),
}
}
pub fn from_git(remote: &str, sub_dir: Option<&str>) -> Result<Template> {
let mut tmp = env::temp_dir();
println!("Tmp dir: {:?}", tmp);
tmp.push(remote.split('/').last().unwrap_or("kickstart"));
if tmp.exists() {
fs::remove_dir_all(&tmp)?;
}
Command::new("git")
.args(["clone", "--recurse-submodules", remote, &format!("{}", tmp.display())])
.output()
.map_err(|err| new_error(ErrorKind::Git { err }))?;
Ok(Template::from_local(&tmp, sub_dir))
}
pub fn from_local(path: &Path, sub_dir: Option<&str>) -> Template {
let mut buf = path.to_path_buf();
if let Some(dir) = sub_dir {
buf.push(dir);
}
Template { path: buf }
}
pub fn generate(&self, output_dir: &Path, no_input: bool) -> Result<()> {
let conf_path = self.path.join("template.toml");
if !conf_path.exists() {
return Err(new_error(ErrorKind::MissingTemplateDefinition));
}
let definition: TemplateDefinition = toml::from_str(&read_file(&conf_path)?)
.map_err(|err| new_error(ErrorKind::Toml { err }))?;
let variables = definition.ask_questions(no_input)?;
let mut context = Context::new();
for (key, val) in &variables {
context.insert(key, val);
}
if !output_dir.exists() {
println!("Creating {:?}", output_dir);
create_directory(output_dir)?;
}
let patterns: Vec<Pattern> =
definition.copy_without_render.iter().map(|s| Pattern::new(s).unwrap()).collect();
let start_path = if let Some(ref directory) = definition.directory {
self.path.join(directory)
} else {
self.path.clone()
};
let walker = WalkDir::new(&start_path)
.into_iter()
.filter_entry(|e| {
let relative_path = e.path().strip_prefix(&start_path).expect("Stripping prefix");
if relative_path.starts_with(".git/")
|| (relative_path.is_dir() && relative_path.starts_with(".git"))
{
return false;
}
true
})
.filter_map(|e| e.ok());
'outer: for entry in walker {
if entry.path() == self.path || entry.path() == conf_path {
continue;
}
let path = entry.path().strip_prefix(&self.path).unwrap();
if path.starts_with(output_dir) {
continue;
}
let path_str = format!("{}", path.display());
for ignored in &definition.ignore {
if ignored == &path_str || path_str.starts_with(ignored) {
continue 'outer;
}
}
let path_str = path_str.replace("$$", "|");
let tpl = render_one_off_template(&path_str, &context, None)?;
let real_path = output_dir.join(Path::new(&tpl));
if entry.path().is_dir() {
create_directory(&real_path)?;
continue;
}
let mut f = File::open(entry.path())?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
let no_render = patterns.iter().map(|p| p.matches_path(&real_path)).any(|x| x);
if no_render || is_binary(&buffer) {
map_io_err(fs::copy(entry.path(), &real_path), entry.path())?;
continue;
}
let contents = render_one_off_template(
str::from_utf8(&buffer).unwrap(),
&context,
Some(entry.path().to_path_buf()),
)?;
write_file(&real_path, &contents)?;
}
for cleanup in &definition.cleanup {
if let Some(val) = variables.get(&cleanup.name) {
if *val == cleanup.value {
for p in &cleanup.paths {
let actual_path = render_one_off_template(p, &context, None)?;
let path_to_delete = output_dir.join(actual_path);
if !path_to_delete.exists() {
continue;
}
if path_to_delete.is_dir() {
map_io_err(fs::remove_dir_all(&path_to_delete), &path_to_delete)?;
} else {
map_io_err(fs::remove_file(&path_to_delete), &path_to_delete)?;
}
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn can_generate_from_local_path() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/complex", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
}
#[test]
fn can_generate_from_local_path_with_directory() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/with-directory", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
assert!(res.is_ok());
assert!(dir.path().join("Hello").join("Howdy.py").exists());
}
#[test]
fn can_generate_from_local_path_with_subdir() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("./", Some("examples/complex")).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
}
#[test]
fn can_generate_from_remote_repo() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("https://github.com/Keats/rust-cli-template", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
println!("{:?}", res);
assert!(res.is_ok());
assert!(!dir.path().join("My-CLI").join("template.toml").exists());
assert!(dir.path().join("My-CLI").join(".travis.yml").exists());
}
#[test]
fn can_generate_from_remote_repo_with_subdir() {
let dir = tempdir().unwrap();
let tpl =
Template::from_input("https://github.com/Keats/kickstart", Some("examples/complex"))
.unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
println!("{:?}", res);
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
}
#[test]
fn can_generate_handling_slugify() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/slugify", None).unwrap();
let res = tpl.generate(&dir.path().to_path_buf(), true);
assert!(res.is_ok());
assert!(!dir.path().join("template.toml").exists());
assert!(dir.path().join("hello.md").exists());
}
}