use crate::{
config::Config,
error::ForeheadError,
header::{apply_header_to_file, check_header_on_file, remove_header_from_file, FileStatus},
};
use std::{fs, path::Path};
use walkdir::WalkDir;
pub mod comment;
pub mod config;
pub mod error;
pub mod header;
pub mod template;
const SKIP_DIRS: &[&str] = &[".git", "target", "node_modules"];
const SKIP_FILES: &[&str] = &["Cargo.lock", "forehead.toml"];
pub struct Forehead {
config: Config,
root: std::path::PathBuf,
}
impl Forehead {
pub fn new(config: Config) -> Self {
let root = config.project_dir.clone();
Forehead { config, root }
}
pub fn from_config_path(path: &Path) -> Result<Self, ForeheadError> {
let config = Config::from_path(path)?;
Ok(Forehead::new(config))
}
fn walk_entries(&self) -> impl Iterator<Item = walkdir::DirEntry> {
WalkDir::new(&self.root)
.into_iter()
.filter_entry(|e| {
let fname = e.file_name().to_str().unwrap_or("");
!SKIP_DIRS.contains(&fname)
})
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter(|e| {
let fname = e.file_name().to_str().unwrap_or("");
!SKIP_FILES.contains(&fname)
})
}
pub fn apply(&self, dry_run: bool) -> Result<ApplyReport, ForeheadError> {
let mut report = ApplyReport::new();
let indicators = self.config.header.all_indicators();
let greetings = &self.config.header.greetings;
for entry in self.walk_entries() {
let path = entry.path().to_path_buf();
let fname = entry.file_name().to_str().unwrap_or("").to_string();
let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
if fname == "Cargo.toml" {
self.apply_cargo_toml_license(&path, &rel, dry_run, &mut report);
continue;
}
let comment_style = match comment::comment_style_for(&path) {
Some(s) => s,
None => continue,
};
let template = match self.config.template_for(&path, &self.root) {
Some(t) => t,
None => continue,
};
let subst = self.config.substitution_for(&path, &self.root);
match apply_header_to_file(
&path,
&template,
&comment_style,
&subst,
dry_run,
&indicators,
greetings,
) {
Ok(true) => report.applied.push(rel),
Ok(false) => {}
Err(e) => report.errors.push((rel, e.to_string())),
}
}
Ok(report)
}
pub fn check(&self) -> Result<CheckReport, ForeheadError> {
let mut report = CheckReport::new();
let indicators = self.config.header.all_indicators();
let greetings = &self.config.header.greetings;
for entry in self.walk_entries() {
let path = entry.path().to_path_buf();
let fname = entry.file_name().to_str().unwrap_or("").to_string();
let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
if fname == "Cargo.toml" {
if !self.check_cargo_toml_license(&path, &rel) {
report.missing.push(rel);
}
continue;
}
let comment_style = match comment::comment_style_for(&path) {
Some(s) => s,
None => continue,
};
let template = match self.config.template_for(&path, &self.root) {
Some(t) => t,
None => continue,
};
let subst = self.config.substitution_for(&path, &self.root);
match check_header_on_file(
&path,
&template,
&comment_style,
&subst,
&indicators,
greetings,
) {
Ok(FileStatus::Correct) => {}
Ok(FileStatus::Missing | FileStatus::Wrong) => {
report.missing.push(rel);
}
Err(e) => report.errors.push((rel, e.to_string())),
}
}
Ok(report)
}
pub fn list(&self) -> Result<Vec<(std::path::PathBuf, FileStatus)>, ForeheadError> {
let mut results = Vec::new();
let indicators = self.config.header.all_indicators();
let greetings = &self.config.header.greetings;
for entry in self.walk_entries() {
let path = entry.path().to_path_buf();
let fname = entry.file_name().to_str().unwrap_or("").to_string();
let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
if fname == "Cargo.toml" {
let status = if self.check_cargo_toml_license(&path, &rel) {
FileStatus::Correct
} else {
FileStatus::Missing
};
results.push((rel, status));
continue;
}
let comment_style = match comment::comment_style_for(&path) {
Some(s) => s,
None => continue,
};
let template = match self.config.template_for(&path, &self.root) {
Some(t) => t,
None => continue,
};
let subst = self.config.substitution_for(&path, &self.root);
match check_header_on_file(
&path,
&template,
&comment_style,
&subst,
&indicators,
greetings,
) {
Ok(status) => results.push((rel, status)),
Err(_) => results.push((rel, FileStatus::Wrong)),
}
}
Ok(results)
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn remove(&self, dry_run: bool) -> Result<ApplyReport, ForeheadError> {
let mut report = ApplyReport::new();
let indicators = self.config.header.all_indicators();
for entry in self.walk_entries() {
let path = entry.path().to_path_buf();
let fname = entry.file_name().to_str().unwrap_or("").to_string();
let rel = path.strip_prefix(&self.root).unwrap_or(&path).to_path_buf();
if fname == "Cargo.toml" || fname == "Cargo.lock" {
continue;
}
let comment_style = match comment::comment_style_for(&path) {
Some(s) => s,
None => continue,
};
match remove_header_from_file(&path, &comment_style, &indicators, dry_run) {
Ok(true) => report.applied.push(rel),
Ok(false) => {}
Err(e) => report.errors.push((rel, e.to_string())),
}
}
Ok(report)
}
fn apply_cargo_toml_license(
&self,
path: &Path,
rel: &Path,
_dry_run: bool,
report: &mut ApplyReport,
) {
let license_str = self.config.license_for(path, &self.root);
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
report.errors.push((rel.to_path_buf(), e.to_string()));
return;
}
};
let new_content = update_cargo_toml_license(&content, &license_str);
if new_content != content {
if let Err(e) = fs::write(path, &new_content) {
report.errors.push((rel.to_path_buf(), e.to_string()));
} else {
report.applied.push(rel.to_path_buf());
}
}
}
fn check_cargo_toml_license(&self, path: &Path, _rel: &Path) -> bool {
let license_str = self.config.license_for(path, &self.root);
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
let new_content = update_cargo_toml_license(&content, &license_str);
new_content == content
}
}
fn update_cargo_toml_license(content: &str, license_str: &str) -> String {
let mut new_lines: Vec<String> = Vec::new();
let mut in_package = false;
let mut found_license = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
new_lines.push(line.to_string());
} else if in_package
&& !found_license
&& (trimmed.starts_with("license") || trimmed.starts_with("license.workspace"))
{
new_lines.push(format!("license = \"{license_str}\""));
found_license = true;
in_package = false;
} else if in_package && !found_license && trimmed.starts_with('[') {
new_lines.push(format!("license = \"{license_str}\""));
new_lines.push(String::new());
new_lines.push(line.to_string());
found_license = true;
in_package = false;
} else {
new_lines.push(line.to_string());
}
}
new_lines.join("\n")
}
#[derive(Debug, Default)]
pub struct ApplyReport {
pub applied: Vec<std::path::PathBuf>,
pub errors: Vec<(std::path::PathBuf, String)>,
}
impl ApplyReport {
pub fn new() -> Self {
ApplyReport::default()
}
pub fn is_empty(&self) -> bool {
self.applied.is_empty() && self.errors.is_empty()
}
}
#[derive(Debug, Default)]
pub struct CheckReport {
pub missing: Vec<std::path::PathBuf>,
pub errors: Vec<(std::path::PathBuf, String)>,
}
impl CheckReport {
pub fn new() -> Self {
CheckReport::default()
}
pub fn is_clean(&self) -> bool {
self.missing.is_empty() && self.errors.is_empty()
}
}