1use crate::error::{InheritError, Result};
2use crate::ignore::{InheritIgnore, ALWAYS_IGNORE};
3use crate::manifest::Manifest;
4use crate::scanner;
5use ignore::Walk;
6use kissreplace::{KissReplace, Variables};
7use std::collections::{HashMap, HashSet};
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12#[derive(Debug)]
14pub struct TemplateContext {
15 pub manifest: Manifest,
16 pub required_vars: HashSet<String>,
17 pub var_descriptions: HashMap<String, String>,
19}
20
21pub fn load_template(source_dir: &Path) -> Result<TemplateContext> {
23 let manifest = Manifest::load(source_dir)?;
24 let ignore = InheritIgnore::load(source_dir);
25 let scanned_vars = scanner::collect_variables(source_dir, &ignore)?;
26
27 let mut required_vars = HashSet::new();
28 let mut var_descriptions = HashMap::new();
29
30 for (k, v) in &manifest.variables {
32 required_vars.insert(k.clone());
33 var_descriptions.insert(k.clone(), v.clone());
34 }
35
36 for k in scanned_vars {
38 required_vars.insert(k);
39 }
40
41 Ok(TemplateContext {
42 manifest,
43 required_vars,
44 var_descriptions,
45 })
46}
47
48#[derive(Debug, Default)]
49pub struct ProcessResult {
50 pub processed_files: usize,
51 pub binary_files: usize,
52}
53
54#[derive(Debug, Clone)]
55pub struct ProcessOptions {
56 pub init_git: bool,
57 pub run_hooks: bool,
58}
59
60impl Default for ProcessOptions {
61 fn default() -> Self {
62 Self {
63 init_git: true,
64 run_hooks: true,
65 }
66 }
67}
68
69pub fn process_template(
72 source_dir: &Path,
73 target_dir: &Path,
74 final_vars: &Variables,
75 opts: ProcessOptions,
76) -> Result<ProcessResult> {
77 let ctx = load_template(source_dir)?;
78
79 for key in final_vars.keys() {
81 if !kissreplace::valid::is_valid_var_name(key) {
82 return Err(InheritError::InvalidVariable(key.clone()));
83 }
84 }
85
86 let missing: Vec<String> = ctx
88 .required_vars
89 .iter()
90 .filter(|v| final_vars.get(*v).map(|s| s.is_empty()).unwrap_or(true))
91 .cloned()
92 .collect();
93
94 if !missing.is_empty() {
95 return Err(InheritError::MissingVariables(missing));
96 }
97
98 if target_dir.exists() {
99 return Err(InheritError::Io(std::io::Error::new(
100 std::io::ErrorKind::AlreadyExists,
101 format!("Target directory already exists: {}", target_dir.display()),
102 )));
103 }
104 fs::create_dir_all(target_dir)?;
105
106 let ignore = InheritIgnore::load(source_dir);
107 let mut result = ProcessResult::default();
108
109 for entry in Walk::new(source_dir) {
110 let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
111 let path = entry.path();
112 let rel = match path.strip_prefix(source_dir) {
113 Ok(r) if r.as_os_str().is_empty() => continue,
114 Ok(r) => r,
115 Err(_) => continue,
116 };
117
118 let rel_str = rel.to_string_lossy();
119 if ALWAYS_IGNORE
120 .iter()
121 .any(|&x| rel_str == x || rel_str.starts_with(&format!("{x}/")))
122 {
123 continue;
124 }
125
126 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
127 if ignore.is_ignored(rel, is_dir) {
128 continue;
129 }
130
131 let new_rel_str = final_vars.replace_str(&rel_str);
132 let new_rel = PathBuf::from(new_rel_str);
133 let new_abs = target_dir.join(&new_rel);
134
135 if is_dir {
136 fs::create_dir_all(&new_abs)?;
137 continue;
138 }
139
140 if !entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
141 continue;
142 }
143
144 if let Some(parent) = new_abs.parent() {
145 fs::create_dir_all(parent)?;
146 }
147
148 match fs::read_to_string(path) {
149 Ok(text) => {
150 let replaced = final_vars.replace_str(&text);
151 fs::write(&new_abs, replaced)?;
152 result.processed_files += 1;
153 }
154 Err(_) => {
155 fs::copy(path, &new_abs)?;
156 result.binary_files += 1;
157 }
158 }
159 }
160
161 if opts.init_git {
162 let git_dir = target_dir.join(".git");
163 if git_dir.exists() {
164 fs::remove_dir_all(&git_dir)?;
165 }
166 let status = Command::new("git")
167 .arg("init")
168 .arg("-q")
169 .current_dir(target_dir)
170 .status()?;
171 if !status.success() {
172 return Err(InheritError::CommandFailed {
173 cmd: "git init".into(),
174 status,
175 });
176 }
177 }
178
179 if opts.run_hooks {
180 for cmd in &ctx.manifest.hooks.post_create {
181 let status = if cfg!(target_os = "windows") {
182 Command::new("cmd")
183 .args(["/C", cmd])
184 .current_dir(target_dir)
185 .status()
186 } else {
187 Command::new("sh")
188 .args(["-c", cmd])
189 .current_dir(target_dir)
190 .status()
191 }?;
192 if !status.success() {
193 return Err(InheritError::CommandFailed {
194 cmd: cmd.clone(),
195 status,
196 });
197 }
198 }
199 }
200
201 Ok(result)
202}