Skip to main content

inherit_core/
pipeline.rs

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/// The context of the loaded template. Used by the CLI to understand which variables to request.
13#[derive(Debug)]
14pub struct TemplateContext {
15    pub manifest: Manifest,
16    pub required_vars: HashSet<String>,
17    /// Mapping: var_name -> desc (from manifest)
18    pub var_descriptions: HashMap<String, String>,
19}
20
21/// Loads the template, parses the manifest, and scans the files to collect all required variables.
22pub 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    // Adding variables from the manifest
31    for (k, v) in &manifest.variables {
32        required_vars.insert(k.clone());
33        var_descriptions.insert(k.clone(), v.clone());
34    }
35
36    // Add variables found in files (even if they are not in the manifest)
37    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
69/// Applies replacements and generates the final project.
70/// `final_vars` must contain ALL required variables (CLI is responsible for collection and prompts).
71pub 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    // Validating the names of the passed variables
80    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    // We check that all required variables are filled (not empty)
87    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}