use std::path::Path;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use crate::Error;
pub(crate) const FILE_NAME: &str = ".refquiem.toml";
const DEFAULT_EXCLUDES: [&str; 8] = [
"CHANGELOG*",
"changelog/",
"RELEASES*",
"RELEASE-NOTES*",
"RELEASE_NOTES*",
"NEWS*",
"HISTORY*",
"CHANGES*",
];
#[derive(Debug, Default, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct Config {
#[serde(default)]
pub(crate) exclude: Vec<String>,
#[serde(default)]
pub(crate) allow: Vec<String>,
#[serde(default)]
pub(crate) generated_markers: Vec<String>,
#[serde(default)]
pub(crate) scan_generated: bool,
#[serde(default)]
pub(crate) scan_commented_code: bool,
pub(crate) since: Option<String>,
}
pub(crate) fn load(path: &Path, required: bool) -> Result<Config, Error> {
if !required && !path.exists() {
return Ok(Config::default());
}
let text = std::fs::read_to_string(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
toml::from_str(&text).map_err(|source| Error::Config {
path: path.to_path_buf(),
source: Box::new(source),
})
}
pub(crate) struct Filter {
excludes: Gitignore,
workspace: String,
generated_markers: Vec<String>,
scan_generated: bool,
pub(crate) scan_commented_code: bool,
package_dir: Option<String>,
}
impl Filter {
pub(crate) fn new(
config: &Config,
workspace_dir: &Path,
workspace: &str,
package_dir: Option<String>,
) -> Result<Self, Error> {
let mut builder = GitignoreBuilder::new(workspace_dir);
for glob in DEFAULT_EXCLUDES
.iter()
.copied()
.chain(config.exclude.iter().map(String::as_str))
{
builder.add_line(None, glob).map_err(Error::Exclude)?;
}
let excludes = builder.build().map_err(Error::Exclude)?;
Ok(Self {
excludes,
workspace: workspace.to_owned(),
generated_markers: config.generated_markers.clone(),
scan_generated: config.scan_generated,
scan_commented_code: config.scan_commented_code,
package_dir,
})
}
pub(crate) fn skips_path(&self, file: &str) -> bool {
if let Some(dir) = &self.package_dir
&& !below(dir, file)
{
return true;
}
let relative = if self.workspace.is_empty() {
Some(file)
} else {
file.strip_prefix(&self.workspace)
.and_then(|rest| rest.strip_prefix('/'))
};
relative.is_some_and(|relative| {
self.excludes
.matched_path_or_any_parents(relative, false)
.is_ignore()
})
}
pub(crate) fn skips_text(&self, text: &str) -> bool {
!self.scan_generated && is_generated(text, &self.generated_markers)
}
}
fn below(dir: &str, file: &str) -> bool {
dir.is_empty()
|| file
.strip_prefix(dir)
.is_some_and(|rest| rest.starts_with('/'))
}
fn is_generated(text: &str, markers: &[String]) -> bool {
text.lines().take(5).any(|line| {
line.contains("@generated")
|| (line.starts_with("// Code generated ") && line.ends_with(" DO NOT EDIT."))
|| markers.iter().any(|marker| line.contains(marker.as_str()))
})
}