use crate::generate::{
any_msg,
config::{RenameConfig, TemplateConfig},
emoji, ParamMap,
};
use anyhow::{anyhow, Context, Result};
use console::style;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use path_absolutize::Absolutize;
use std::{
convert::AsRef,
fs,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
use weld_codegen::render::Renderer;
struct Matcher {
exclude: Option<Gitignore>,
raw: Option<Gitignore>,
rename: Vec<RenameConfig>,
}
impl Matcher {
fn new<P: AsRef<Path>>(project_dir: P, template_config: &TemplateConfig) -> Result<Self> {
let exclude = if !template_config.exclude.is_empty() {
Some(create_matcher(
project_dir.as_ref(),
&template_config.exclude,
)?)
} else {
None
};
let raw = if !template_config.raw.is_empty() {
Some(create_matcher(project_dir.as_ref(), &template_config.raw)?)
} else {
None
};
let rename = template_config.rename.clone();
Ok(Self {
exclude,
raw,
rename,
})
}
fn should_include(&self, rel_path: &Path) -> bool {
if let Some(exclude) = &self.exclude {
!exclude
.matched_path_or_any_parents(rel_path, false)
.is_ignore()
} else {
true
}
}
fn rename_path(&self, rel_path: &Path) -> Option<&str> {
let ren = self
.rename
.iter()
.find(|rc| rc.from == rel_path)
.map(|rc| rc.to.as_str());
match ren {
None => {
println!("DBG: ren: {}: no", &rel_path.display());
}
Some(p) => {
println!("DBG: ren: {}: {}", &rel_path.display(), p);
}
}
ren
}
fn is_raw(&self, rel_path: &Path) -> bool {
if let Some(raw) = &self.raw {
raw.matched_path_or_any_parents(rel_path, false)
.is_ignore()
} else {
false
}
}
}
fn create_matcher<P: AsRef<Path>>(project_dir: P, patterns: &[String]) -> Result<Gitignore> {
let mut builder = GitignoreBuilder::new(project_dir);
for rule in patterns {
builder.add_line(None, rule)?;
}
Ok(builder.build()?)
}
pub(crate) fn spinner() -> ProgressStyle {
ProgressStyle::default_spinner()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
.template("{prefix:.bold.dim} {spinner} {wide_msg}")
}
pub(crate) fn process_template_dir(
source_dir: &Path,
project_dir: &Path,
template_config: &TemplateConfig,
renderer: &Renderer,
values: &ParamMap,
mp: &mut MultiProgress,
) -> Result<()> {
fn is_git_metadata(entry: &Path) -> bool {
entry
.components()
.any(|c| c == std::path::Component::Normal(".git".as_ref()))
}
let matcher = Matcher::new(source_dir, template_config)?;
let spinner_style = spinner();
let files = WalkDir::new(source_dir)
.sort_by_file_name() .contents_first(true) .follow_links(false) .into_iter()
.filter_map(Result::ok)
.filter(|e| !is_git_metadata(e.path()))
.filter(|e| e.path() != source_dir)
.map(|e| e.into_path())
.collect::<Vec<PathBuf>>();
let total = files.len().to_string();
for (progress, entry) in files.into_iter().enumerate() {
let pb = mp.add(ProgressBar::new(50));
pb.set_style(spinner_style.clone());
pb.set_prefix(format!(
"[{:width$}/{}]",
progress + 1,
total,
width = total.len()
));
let filename = entry.as_path();
let src_relative = filename.strip_prefix(source_dir)?;
let f = src_relative.display();
pb.set_message(format!("Processing: {}", f));
if matcher.should_include(src_relative) {
if entry.is_file() {
let dest_rel_path = if let Some(rename_path) = matcher.rename_path(src_relative) {
PathBuf::from(renderer.render_template(rename_path, values).with_context(
|| {
format!(
"Error processing template filename '{}'. Project variables: {:?}",
rename_path, &values
)
},
)?)
} else {
src_relative.to_path_buf()
};
let dest_path = project_dir.join(&dest_rel_path);
let dest_path = dest_path.absolutize().with_context(|| {
format!(
"Invalid file destination path: {}",
&dest_rel_path.display()
)
})?;
if !dest_path.starts_with(project_dir) {
return Err(anyhow!(
"Invalid destination: {} is not within project dir",
&dest_path.display()
));
}
if dest_path.exists() {
return Err(anyhow!(
"Destination file '{}' exists: quitting!",
&dest_path.display()
));
}
fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
if matcher.is_raw(src_relative) {
fs::copy(&entry, &dest_path)?;
} else {
let contents = fs::read_to_string(&entry).with_context(|| {
format!(
"{} {} `{}` {}",
emoji::ERROR,
style("Error reading template file.").bold().red(),
style(&entry.display()).bold(),
"If this is not a text file, you may want to add the path to the 'template.raw' list in project-generate.toml"
)
})?;
let rendered = renderer.render_template(&contents, values).map_err(|e| {
any_msg(
&format!("rendering template file {}", &src_relative.display()),
&e.to_string(),
)
})?;
fs::write(&dest_path, rendered.as_bytes()).with_context(|| {
format!(
"{} {} `{}`",
emoji::ERROR,
style("Error saving rendered file:").bold().red(),
style(dest_path.display()).bold()
)
})?;
let f = &dest_rel_path.display();
pb.inc(50);
pb.finish_with_message(format!("Done: {}", f));
}
} } else {
pb.finish_with_message(format!("Skipped: {}", f));
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn match_rename() {
let template_config = TemplateConfig {
exclude: Vec::new(),
raw: Vec::new(),
rename: vec![RenameConfig {
from: "a.txt".into(),
to: "b.txt".into(),
}],
};
let matcher = Matcher::new("/target", &template_config).unwrap();
assert_eq!(matcher.rename_path(&PathBuf::from("README.txt")), None);
assert_eq!(matcher.rename_path(&PathBuf::from("a.txt")), Some("b.txt"));
}
#[test]
fn match_exclude() {
let template_config = TemplateConfig {
exclude: vec!["*.txt".into(), ".gitignore".into()],
raw: Vec::new(),
rename: Vec::new(),
};
let matcher = Matcher::new("/target", &template_config).unwrap();
assert_eq!(matcher.should_include(&PathBuf::from("a.txt")), false);
assert_eq!(matcher.should_include(&PathBuf::from("a.txt.html")), true);
}
#[test]
fn match_raw() {
let template_config = TemplateConfig {
exclude: Vec::new(),
raw: vec!["*.bin".into(), "b.dat".into()],
rename: vec![RenameConfig {
from: "a.bin".into(),
to: "b.bin".into(),
}],
};
let matcher = Matcher::new("/target", &template_config).unwrap();
assert_eq!(matcher.is_raw(&PathBuf::from("a.bin")), true);
assert_eq!(matcher.is_raw(&PathBuf::from("x.bin")), true);
assert_eq!(matcher.is_raw(&PathBuf::from("b.dat")), true);
}
}