1use crate::{
39 config::Config,
40 error::ForeheadError,
41 header::{apply_header_to_file, check_header_on_file, remove_header_from_file, FileStatus},
42};
43use std::{fs, path::Path};
44use walkdir::WalkDir;
45
46pub mod comment;
47pub mod config;
48pub mod error;
49pub mod header;
50pub mod template;
51
52const SKIP_DIRS: &[&str] = &[".git", "target", "node_modules"];
53const SKIP_FILES: &[&str] = &["Cargo.lock", "forehead.toml"];
54
55pub struct Forehead {
56 config: Config,
57 root: std::path::PathBuf,
58}
59
60impl Forehead {
61 pub fn new(config: Config) -> Self {
62 let root = config.project_dir.clone();
63 Forehead { config, root }
64 }
65
66 pub fn from_config_path(path: &Path) -> Result<Self, ForeheadError> {
67 let config = Config::from_path(path)?;
68 Ok(Forehead::new(config))
69 }
70
71 fn walk_entries(&self) -> impl Iterator<Item = walkdir::DirEntry> {
72 WalkDir::new(&self.root)
73 .into_iter()
74 .filter_entry(|e| {
75 let fname = e.file_name().to_str().unwrap_or("");
76 !SKIP_DIRS.contains(&fname)
77 })
78 .filter_map(|e| e.ok())
79 .filter(|e| e.path().is_file())
80 .filter(|e| {
81 let fname = e.file_name().to_str().unwrap_or("");
82 !SKIP_FILES.contains(&fname)
83 })
84 }
85
86 pub fn apply(&self, dry_run: bool) -> Result<ApplyReport, ForeheadError> {
87 let mut report = ApplyReport::new();
88 let indicators = self.config.header.all_indicators();
89 let greetings = &self.config.header.greetings;
90
91 for entry in self.walk_entries() {
92 let path = entry.path().to_path_buf();
93 let fname = entry.file_name().to_str().unwrap_or("").to_string();
94 let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
95
96 if fname == "Cargo.toml" {
97 self.apply_cargo_toml_license(&path, &rel, dry_run, &mut report);
98 continue;
99 }
100
101 let comment_style = match comment::comment_style_for(&path) {
102 Some(s) => s,
103 None => continue,
104 };
105
106 let template = match self.config.template_for(&path, &self.root) {
107 Some(t) => t,
108 None => continue,
109 };
110
111 let subst = self.config.substitution_for(&path, &self.root);
112
113 match apply_header_to_file(
114 &path,
115 &template,
116 &comment_style,
117 &subst,
118 dry_run,
119 &indicators,
120 greetings,
121 ) {
122 Ok(true) => report.applied.push(rel),
123 Ok(false) => {}
124 Err(e) => report.errors.push((rel, e.to_string())),
125 }
126 }
127
128 Ok(report)
129 }
130
131 pub fn check(&self) -> Result<CheckReport, ForeheadError> {
132 let mut report = CheckReport::new();
133 let indicators = self.config.header.all_indicators();
134 let greetings = &self.config.header.greetings;
135
136 for entry in self.walk_entries() {
137 let path = entry.path().to_path_buf();
138 let fname = entry.file_name().to_str().unwrap_or("").to_string();
139 let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
140
141 if fname == "Cargo.toml" {
142 if !self.check_cargo_toml_license(&path, &rel) {
143 report.missing.push(rel);
144 }
145 continue;
146 }
147
148 let comment_style = match comment::comment_style_for(&path) {
149 Some(s) => s,
150 None => continue,
151 };
152
153 let template = match self.config.template_for(&path, &self.root) {
154 Some(t) => t,
155 None => continue,
156 };
157
158 let subst = self.config.substitution_for(&path, &self.root);
159
160 match check_header_on_file(
161 &path,
162 &template,
163 &comment_style,
164 &subst,
165 &indicators,
166 greetings,
167 ) {
168 Ok(FileStatus::Correct) => {}
169 Ok(FileStatus::Missing | FileStatus::Wrong) => {
170 report.missing.push(rel);
171 }
172 Err(e) => report.errors.push((rel, e.to_string())),
173 }
174 }
175
176 Ok(report)
177 }
178
179 pub fn list(&self) -> Result<Vec<(std::path::PathBuf, FileStatus)>, ForeheadError> {
180 let mut results = Vec::new();
181 let indicators = self.config.header.all_indicators();
182 let greetings = &self.config.header.greetings;
183
184 for entry in self.walk_entries() {
185 let path = entry.path().to_path_buf();
186 let fname = entry.file_name().to_str().unwrap_or("").to_string();
187 let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
188
189 if fname == "Cargo.toml" {
190 let status = if self.check_cargo_toml_license(&path, &rel) {
191 FileStatus::Correct
192 } else {
193 FileStatus::Missing
194 };
195 results.push((rel, status));
196 continue;
197 }
198
199 let comment_style = match comment::comment_style_for(&path) {
200 Some(s) => s,
201 None => continue,
202 };
203
204 let template = match self.config.template_for(&path, &self.root) {
205 Some(t) => t,
206 None => continue,
207 };
208
209 let subst = self.config.substitution_for(&path, &self.root);
210
211 match check_header_on_file(
212 &path,
213 &template,
214 &comment_style,
215 &subst,
216 &indicators,
217 greetings,
218 ) {
219 Ok(status) => results.push((rel, status)),
220 Err(_) => results.push((rel, FileStatus::Wrong)),
221 }
222 }
223
224 Ok(results)
225 }
226
227 pub fn config(&self) -> &Config {
228 &self.config
229 }
230
231 pub fn remove(&self, dry_run: bool) -> Result<ApplyReport, ForeheadError> {
232 let mut report = ApplyReport::new();
233 let indicators = self.config.header.all_indicators();
234
235 for entry in self.walk_entries() {
236 let path = entry.path().to_path_buf();
237 let fname = entry.file_name().to_str().unwrap_or("").to_string();
238 let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
239
240 if fname == "Cargo.toml" || fname == "Cargo.lock" {
241 continue;
242 }
243
244 let comment_style = match comment::comment_style_for(&path) {
245 Some(s) => s,
246 None => continue,
247 };
248
249 match remove_header_from_file(&path, &comment_style, &indicators, dry_run) {
250 Ok(true) => report.applied.push(rel),
251 Ok(false) => {}
252 Err(e) => report.errors.push((rel, e.to_string())),
253 }
254 }
255
256 Ok(report)
257 }
258
259 fn apply_cargo_toml_license(
260 &self,
261 path: &Path,
262 rel: &Path,
263 _dry_run: bool,
264 report: &mut ApplyReport,
265 ) {
266 let license_str = self.config.license_for(path, &self.root);
267
268 let content = match fs::read_to_string(path) {
269 Ok(c) => c,
270 Err(e) => {
271 report.errors.push((rel.to_path_buf(), e.to_string()));
272 return;
273 }
274 };
275
276 let new_content = update_cargo_toml_license(&content, &license_str);
277
278 if new_content != content {
279 if let Err(e) = fs::write(path, &new_content) {
280 report.errors.push((rel.to_path_buf(), e.to_string()));
281 } else {
282 report.applied.push(rel.to_path_buf());
283 }
284 }
285 }
286
287 fn check_cargo_toml_license(&self, path: &Path, _rel: &Path) -> bool {
288 let license_str = self.config.license_for(path, &self.root);
289
290 let content = match fs::read_to_string(path) {
291 Ok(c) => c,
292 Err(_) => return false,
293 };
294
295 let new_content = update_cargo_toml_license(&content, &license_str);
296 new_content == content
297 }
298}
299
300fn update_cargo_toml_license(content: &str, license_str: &str) -> String {
301 let mut new_lines: Vec<String> = Vec::new();
302 let mut in_package = false;
303 let mut found_license = false;
304
305 for line in content.lines() {
306 let trimmed = line.trim();
307 if trimmed == "[package]" {
308 in_package = true;
309 new_lines.push(line.to_string());
310 } else if in_package
311 && !found_license
312 && (trimmed.starts_with("license") || trimmed.starts_with("license.workspace"))
313 {
314 new_lines.push(format!("license = \"{license_str}\""));
315 found_license = true;
316 in_package = false;
317 } else if in_package && !found_license && trimmed.starts_with('[') {
318 new_lines.push(format!("license = \"{license_str}\""));
319 new_lines.push(String::new());
320 new_lines.push(line.to_string());
321 found_license = true;
322 in_package = false;
323 } else {
324 new_lines.push(line.to_string());
325 }
326 }
327
328 new_lines.join("\n")
329}
330
331#[derive(Debug, Default)]
332pub struct ApplyReport {
333 pub applied: Vec<std::path::PathBuf>,
334 pub errors: Vec<(std::path::PathBuf, String)>,
335}
336
337impl ApplyReport {
338 pub fn new() -> Self {
339 ApplyReport::default()
340 }
341 pub fn is_empty(&self) -> bool {
342 self.applied.is_empty() && self.errors.is_empty()
343 }
344}
345
346#[derive(Debug, Default)]
347pub struct CheckReport {
348 pub missing: Vec<std::path::PathBuf>,
349 pub errors: Vec<(std::path::PathBuf, String)>,
350}
351
352impl CheckReport {
353 pub fn new() -> Self {
354 CheckReport::default()
355 }
356 pub fn is_clean(&self) -> bool {
357 self.missing.is_empty() && self.errors.is_empty()
358 }
359}