///|
priv struct Rule {
pattern : Pattern
base : String
negative : Bool
source : String
line : Int32
}
///|
// Git removes only unescaped trailing spaces; leading spaces are significant.
fn trim_rule_line(line : String) -> String {
let mut end = line.length()
if end > 0 && line[end - 1] == '\r' {
end -= 1
}
while end > 0 && line[end - 1] == ' ' {
let mut slashes = 0
let mut pos = end - 1
while pos > 0 && line[pos - 1] == '\\' {
slashes += 1
pos -= 1
}
if slashes % 2 == 1 {
break
}
end -= 1
}
line[:end].to_owned()
}
///|
async fn rule_text(path : String) -> String? {
match optional_kind(path) {
None => None
Some(Regular) => Some(@utf8.decode(@fs.read_file(path).binary()))
Some(_) => raise SelectionError::InvalidRuleFile(path)
}
}
///|
async fn load_rules(base : String, name : String, rules : Array[Rule]) -> Unit {
let source = join(base, name)
if rule_text(source) is Some(text) {
for index, raw in text.split("\n").collect() {
let line = trim_rule_line(raw.to_owned())
if line == "" || line.has_prefix("#") {
continue
}
let negative = line.has_prefix("!")
let pattern = if negative { line[1:].to_owned() } else { line }
rules.push({
pattern: compile_pattern(pattern, source, index + 1, false),
base,
negative,
source,
line: index + 1,
})
}
}
}
///|
fn ignored_by(rules : Array[Rule], path : String, is_dir : Bool) -> Rule? {
let mut matched : Rule? = None
for rule in rules {
if within(path, rule.base) is Some(relative) &&
relative != "" &&
rule.pattern.matches(relative, is_dir) {
matched = if rule.negative { None } else { Some(rule) }
}
}
matched
}
///|
// Every metadata component is checked before reading through it; rules never
// follow symlinks (including linked info directories and linked gitdirs).
async fn checked_metadata_dir(path : String) -> Bool {
let up = parent(path)
if up != path && !checked_metadata_dir(up) {
return false
}
match optional_kind(path) {
None => false
Some(Directory) => true
_ => raise SelectionError::InvalidGitMetadata(path)
}
}
///|
async fn git_exclude(
base : String,
marker : @fs.FileKind,
rules : Array[Rule],
) -> Unit {
let dotgit = join(base, ".git")
let gitdir = match marker {
Directory => dotgit
Regular => {
let text = rule_text(dotgit).unwrap().trim_end(chars="\r\n").to_owned()
if !text.has_prefix("gitdir: ") {
raise SelectionError::InvalidGitMetadata(dotgit)
}
let target = text[8:].to_owned()
if target == "" || target.contains("\n") {
raise SelectionError::InvalidGitMetadata(dotgit)
}
normalize(
if absolute(separators(target)) {
target
} else {
join(base, target)
},
)
}
_ => raise SelectionError::InvalidGitMetadata(dotgit)
}
if !checked_metadata_dir(gitdir) {
raise SelectionError::InvalidGitMetadata(gitdir)
}
let common = match rule_text(join(gitdir, "commondir")) {
None => gitdir
Some(text) => {
let target = text.trim_end(chars="\r\n").to_owned()
if target == "" || target.contains("\n") {
raise SelectionError::InvalidGitMetadata(join(gitdir, "commondir"))
}
normalize(
if absolute(separators(target)) {
target
} else {
join(gitdir, target)
},
)
}
}
if !checked_metadata_dir(common) {
raise SelectionError::InvalidGitMetadata(common)
}
let info = join(common, "info")
if checked_metadata_dir(info) {
// The physical source is in Git metadata, but its patterns are based at
// the worktree root, not at .git/info.
let loaded : Array[Rule] = []
load_rules(info, "exclude", loaded)
for rule in loaded {
rules.push({ ..rule, base, })
}
}
}
///|
async fn inherited_rules(
root : String,
options : SelectionOptions,
) -> (Array[Rule], Rule?) {
let rules : Array[Rule] = []
if !options.use_gitignore {
return (rules, None)
}
let ancestors : Array[String] = []
let mut current = root
while true {
ancestors.push(current)
if optional_kind(join(current, ".git")) is Some(marker) {
git_exclude(current, marker, rules)
break
}
let up = parent(current)
if up == current {
break
}
current = up
}
// Root's files are loaded by walk, together with .ignore/.repomixignore.
for i = ancestors.length() - 1; i >= 0; i = i - 1 {
if ignored_by(rules, ancestors[i], true) is Some(rule) {
return (rules, Some(rule))
}
if i > 0 {
load_rules(ancestors[i], ".gitignore", rules)
}
}
(rules, None)
}
///|
async fn local_rules(
base : String,
inherited : Array[Rule],
options : SelectionOptions,
) -> Array[Rule] {
let rules = inherited.copy()
if options.use_gitignore {
load_rules(base, ".gitignore", rules)
}
if options.use_dot_ignore {
load_rules(base, ".ignore", rules)
load_rules(base, ".repomixignore", rules)
}
rules
}