// Ignore/allow rule evaluation for source trees, build outputs, and caches.
///|
/// The action encoded by one ignore-file rule.
pub enum RuleAction {
Include
Exclude
} derive(Debug, Eq)
///|
/// The result of evaluating all rules for one path.
pub enum RuleDecision {
Included
Excluded
Unmatched
} derive(Debug, Eq)
///|
/// One parsed rule with the original source retained for diagnostics.
pub(all) struct GlobRule {
source : String
pattern : CompiledPattern
action : RuleAction
anchored : Bool
directory_only : Bool
has_separator : Bool
} derive(Debug, Eq)
///|
/// An ordered rule set using last-match-wins evaluation.
pub(all) struct GlobRules {
rules : Array[GlobRule]
} derive(Debug, Eq)
///|
fn trim_rule_line(line : String) -> String {
line.trim().to_owned()
}
///|
fn strip_rule_suffix(value : String, suffix : String) -> (String, Bool) {
if value.has_suffix(suffix) {
(value[:value.length() - suffix.length()].to_owned(), true)
} else {
(value, false)
}
}
///|
fn parse_rule_line(line : String) -> Result[GlobRule?, GlobError] {
let trimmed = trim_rule_line(line)
if trimmed.is_empty() || trimmed.has_prefix("#") {
return Ok(None)
}
let mut action = RuleAction::Exclude
let mut body = trimmed
if body.has_prefix("!") {
action = RuleAction::Include
body = body[1:].to_owned()
}
let (without_suffix, directory_only) = strip_rule_suffix(body, "/")
body = without_suffix
let anchored = body.has_prefix("/")
if anchored {
body = body[1:].to_owned()
}
if body.is_empty() {
return Err(GlobError::EmptyPattern)
}
let has_separator = match separator_count(body) {
Ok(count) => count > 0
Err(_) => false
}
match compile_pattern(body) {
Err(err) => Err(err)
Ok(pattern) =>
Ok(
Some({
source: trimmed,
pattern,
action,
anchored,
directory_only,
has_separator,
}),
)
}
}
///|
/// Parses lines using common .gitignore-style comments and blank lines.
pub fn GlobRules::from_lines(
lines : Array[String],
) -> Result[GlobRules, GlobError] {
let rules : Array[GlobRule] = []
for line in lines {
match parse_rule_line(line) {
Err(err) => return Err(err)
Ok(None) => ()
Ok(Some(rule)) => rules.push(rule)
}
}
Ok({ rules, })
}
///|
/// Parses a newline-separated rule document.
pub fn GlobRules::from_text(text : String) -> Result[GlobRules, GlobError] {
let lines : Array[String] = []
let mut start = 0
let mut i = 0
while i <= text.length() {
if i == text.length() || text[i].to_int().unsafe_to_char() == '\n' {
let mut line = text[start:i].to_owned()
if line.has_suffix("\r") {
line = line[:line.length() - 1].to_owned()
}
lines.push(line)
start = i + 1
}
i = i + 1
}
GlobRules::from_lines(lines)
}
///|
pub fn GlobRules::rule_count(self : GlobRules) -> Int {
self.rules.length()
}
///|
pub fn GlobRules::is_empty(self : GlobRules) -> Bool {
self.rules.is_empty()
}
///|
fn rule_matches(rule : GlobRule, path : String, _is_directory : Bool) -> Bool {
let normalized = normalize_path(path)
if rule.directory_only {
let directory_name = normalize_path(rule.pattern.literal_prefix())
if normalized != directory_name &&
!normalized.has_prefix(directory_name + "/") {
return false
}
if rule.anchored && !normalized.has_prefix(directory_name) {
return false
}
return true
}
if rule.has_separator || rule.anchored {
return rule.pattern.matches(normalized)
}
rule.pattern.matches(basename(normalized))
}
///|
/// Evaluates a path with last-match-wins semantics.
pub fn GlobRules::decision(
self : GlobRules,
path : String,
is_directory : Bool,
) -> RuleDecision {
let mut decision = RuleDecision::Unmatched
for rule in self.rules {
if rule_matches(rule, path, is_directory) {
decision = match rule.action {
RuleAction::Include => RuleDecision::Included
RuleAction::Exclude => RuleDecision::Excluded
}
}
}
decision
}
///|
/// Returns whether a path should remain visible after rule evaluation.
pub fn GlobRules::allows(
self : GlobRules,
path : String,
is_directory : Bool,
) -> Bool {
match self.decision(path, is_directory) {
RuleDecision::Excluded => false
RuleDecision::Included => true
RuleDecision::Unmatched => true
}
}
///|
/// Filters a path list, treating every entry as a file.
pub fn GlobRules::filter(
self : GlobRules,
paths : Array[String],
) -> Array[String] {
let result : Array[String] = []
for path in paths {
if self.allows(path, false) {
let normalized = normalize_path(path)
if !result.contains(normalized) {
result.push(normalized)
}
}
}
result
}
///|
/// Filters paths when the caller knows which entries are directories.
pub fn GlobRules::filter_with_kinds(
self : GlobRules,
entries : Array[(String, Bool)],
) -> Array[String] {
let result : Array[String] = []
for entry in entries {
let (path, is_directory) = entry
if self.allows(path, is_directory) {
let normalized = normalize_path(path)
if !result.contains(normalized) {
result.push(normalized)
}
}
}
result
}
///|
/// Describes the last rule that affects a path.
pub fn GlobRules::explain(
self : GlobRules,
path : String,
is_directory : Bool,
) -> String {
let normalized = normalize_path(path)
let mut matched_source : String? = None
let mut decision = RuleDecision::Unmatched
for rule in self.rules {
if rule_matches(rule, normalized, is_directory) {
matched_source = Some(rule.source)
decision = match rule.action {
RuleAction::Include => RuleDecision::Included
RuleAction::Exclude => RuleDecision::Excluded
}
}
}
let decision_text = match decision {
RuleDecision::Included => "Included"
RuleDecision::Excluded => "Excluded"
RuleDecision::Unmatched => "Unmatched"
}
match matched_source {
None => normalized + ":" + decision_text
Some(source) => normalized + ":" + decision_text + " by " + source
}
}
///|
/// Returns all rules in their source order for tooling integrations.
pub fn GlobRules::to_array(self : GlobRules) -> Array[GlobRule] {
self.rules
}
///|
/// Appends another rule set while preserving evaluation order.
pub fn GlobRules::append(self : GlobRules, other : GlobRules) -> GlobRules {
let rules = self.rules.copy()
for rule in other.rules {
rules.push(rule)
}
{ rules, }
}
///|
/// Returns the count of include rules in this set.
pub fn GlobRules::include_count(self : GlobRules) -> Int {
let mut count = 0
for rule in self.rules {
if rule.action == RuleAction::Include {
count = count + 1
}
}
count
}
///|
/// Returns the count of exclude rules in this set.
pub fn GlobRules::exclude_count(self : GlobRules) -> Int {
self.rule_count() - self.include_count()
}