///|
priv struct IgnoreRuleSet {
base_dir : String
patterns : Array[String]
}
// Minimal traversal-only matcher until this package can use bit_ignore.
// Supported today: comments, blank lines, literal directory patterns, and `*` within a path segment.
///|
let char_star : UInt16 = {
let c : Int = '*'.to_int()
c.to_uint16()
}
///|
fn parse_ignore_patterns(content : String) -> Array[String] {
content
.split("\n")
.map(fn(raw_line) { raw_line.to_owned().trim().to_owned() })
.filter(fn(line) {
line != "" && !line.has_prefix("#") && !line.has_prefix("!")
})
.map(fn(line) { line.trim_start(chars="/").trim_end(chars="/").to_owned() })
.collect()
}
///|
async fn read_ignore_rules(dir : String) -> IgnoreRuleSet? {
let path = @path.Path::join(dir, ".gitignore").to_string()
let content = @fs.read_file(path).text() catch {
error if @async.is_being_cancelled() => raise error
_ => ""
}
if content.trim() == "" {
None
} else {
Some({ base_dir: dir, patterns: parse_ignore_patterns(content) })
}
}
///|
fn path_matches_pattern(relative : String, pattern : String) -> Bool {
let pattern_segments = non_empty_path_segments(pattern)
let relative_segments = non_empty_path_segments(relative)
if pattern_segments.length() == 0 || relative_segments.length() == 0 {
return false
}
if pattern_segments.length() == 1 {
return relative_segments.any(fn(segment) {
glob_segment_matches(pattern_segments[0], segment)
})
}
contains_contiguous_segments(relative_segments, pattern_segments)
}
///|
fn non_empty_path_segments(path : String) -> Array[String] {
path
.split("/")
.map(fn(segment) { segment.to_owned() })
.filter(fn(segment) { segment != "" })
.collect()
}
///|
fn contains_contiguous_segments(
path_segments : Array[String],
pattern_segments : Array[String],
) -> Bool {
if pattern_segments.length() > path_segments.length() {
return false
}
(0)
.until(path_segments.length() - pattern_segments.length() + 1)
.any(fn(start) {
(0)
.until(pattern_segments.length())
.all(fn(offset) {
glob_segment_matches(
pattern_segments[offset],
path_segments[start + offset],
)
})
})
}
///|
fn glob_segment_matches(pattern : String, text : String) -> Bool {
let pattern_length = pattern.length()
let text_length = text.length()
let mut pattern_index = 0
let mut text_index = 0
let mut star_index = -1
let mut match_index = 0
while text_index < text_length {
if pattern_index < pattern_length &&
pattern.unsafe_get(pattern_index) == text.unsafe_get(text_index) {
pattern_index += 1
text_index += 1
} else if pattern_index < pattern_length &&
pattern.unsafe_get(pattern_index) == char_star {
star_index = pattern_index
match_index = text_index
pattern_index += 1
} else if star_index != -1 {
pattern_index = star_index + 1
match_index += 1
text_index = match_index
} else {
return false
}
}
while pattern_index < pattern_length &&
pattern.unsafe_get(pattern_index) == char_star {
pattern_index += 1
}
pattern_index == pattern_length
}
///|
fn is_ignored_directory(
target_path : String,
ignore_rules : Array[IgnoreRuleSet],
) -> Bool {
ignore_rules
.iter()
.any(fn(rules) {
let relative = @path.Path::relative(
base=normalize_path(rules.base_dir),
normalize_path(target_path),
).to_string()
rules.patterns
.iter()
.any(fn(pattern) { path_matches_pattern(relative, pattern) })
})
}