use anyhow::Result;
use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct RuleSet {
root: PathBuf,
pub(crate) rules: Vec<Rule>,
tester: GlobSet,
}
impl RuleSet {
pub fn new(root: &PathBuf, raw_rules: Vec<&str>) -> Result<RuleSet> {
let cleaned_root = Self::strip_prefix(root, Path::new("./"));
let lines = raw_rules
.into_iter()
.map(RuleSet::parse_line)
.collect::<Result<Vec<ParsedLine>>>()?;
let rules: Vec<Rule> = lines
.iter()
.filter_map(|parsed_line| {
match parsed_line {
&ParsedLine::WithRule(ref rule) => Some(rule.clone()),
_ => None,
}
})
.collect();
let mut tester_builder = GlobSetBuilder::new();
for rule in rules.iter() {
let mut glob_builder = GlobBuilder::new(&rule.pattern);
glob_builder.literal_separator(rule.anchored);
let glob = glob_builder.build()?;
tester_builder.add(glob);
}
let tester = tester_builder.build()?;
Ok(RuleSet {
root: cleaned_root,
rules,
tester,
})
}
pub fn is_ignored<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> bool {
let mut cleaned_path = Self::strip_prefix(path.as_ref(), Path::new("./"));
cleaned_path = Self::strip_prefix(cleaned_path.as_path(), &self.root);
let candidate = Candidate::new(&cleaned_path);
let results = self.tester.matches_candidate(&candidate);
for idx in results.iter().rev() {
let ref rule = self.rules[*idx];
if rule.dir_only && !is_dir {
continue;
}
return !rule.negation;
}
false
}
fn parse_line<R: AsRef<str>>(raw_rule: R) -> Result<ParsedLine> {
let mut pattern = raw_rule.as_ref().trim();
if pattern.is_empty() {
return Ok(ParsedLine::Empty);
}
if pattern.starts_with('#') {
return Ok(ParsedLine::Comment);
}
let negation = pattern.starts_with('!');
if negation {
pattern = pattern.trim_start_matches('!').trim();
}
let dir_only = pattern.ends_with('/');
if dir_only {
pattern = pattern.trim_end_matches('/').trim();
}
let absolute = pattern.starts_with('/');
if absolute {
pattern = pattern.trim_start_matches('/');
}
let anchored = absolute || pattern.contains('/');
let mut cleaned_pattern = if !absolute && !pattern.starts_with("**/") {
format!("**/{}", pattern.replace(r"\", ""))
} else {
pattern.replace(r"\", "")
};
if cleaned_pattern.ends_with("/**") {
cleaned_pattern = format!("{}/*", cleaned_pattern);
}
Ok(ParsedLine::WithRule(Rule {
pattern: cleaned_pattern, anchored,
dir_only,
negation,
}))
}
fn strip_prefix<P: AsRef<Path>, PR: AsRef<Path>>(path: P, prefix: PR) -> PathBuf {
path.as_ref()
.strip_prefix(prefix.as_ref())
.unwrap_or(path.as_ref())
.to_path_buf()
}
}
impl fmt::Debug for RuleSet {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?} gitignore RULES", self.rules.len())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Rule {
pub pattern: String,
pub anchored: bool,
pub dir_only: bool,
pub negation: bool,
}
enum ParsedLine {
Empty,
Comment,
WithRule(Rule),
}
pub fn load_str(root: &PathBuf, content: &str) -> Result<RuleSet> {
let split = content.split("\n");
let lines = split.collect::<Vec<&str>>();
let rule_set = RuleSet::new(root, lines.clone())?;
Ok(rule_set)
}