use {
git2,
glob,
id_arena::{
Arena,
Id,
},
regex::Regex,
std::{
fs::File,
io::{BufRead, BufReader, Result},
path::Path,
},
};
pub fn is_repo(root: &Path) -> bool {
root.join(".git").exists()
}
struct GitIgnoreRule {
ok: bool, directory: bool, filename: bool, pattern: glob::Pattern,
pattern_options: glob::MatchOptions,
}
impl GitIgnoreRule {
fn from(line: &str, dir: &Path) -> Option<GitIgnoreRule> {
if line.starts_with('#') {
return None; }
let r = regex!(
r"(?x)
^\s*
(!)? # 1 : negation
(.+?) # 2 : pattern
(/)? # 3 : directory
\s*$
"
);
if let Some(c) = r.captures(line) {
if let Some(p) = c.get(2) {
let mut p = p.as_str().to_string();
let has_separator = p.contains('/');
if has_separator && p.starts_with('/') {
p = dir.to_string_lossy().to_string() + &p;
}
if let Ok(pattern) = glob::Pattern::new(&p) {
let pattern_options = glob::MatchOptions {
case_sensitive: true, require_literal_leading_dot: false,
require_literal_separator: has_separator,
};
return Some(GitIgnoreRule {
ok: c.get(1).is_some(), pattern,
directory: c.get(3).is_some(),
filename: !has_separator,
pattern_options,
});
}
}
}
None
}
}
pub struct GitIgnoreFile {
rules: Vec<GitIgnoreRule>,
}
impl GitIgnoreFile {
pub fn new(path: &Path) -> Result<GitIgnoreFile> {
let f = File::open(path)?;
let parent = path.parent().unwrap();
let mut rules: Vec<GitIgnoreRule> = Vec::new();
for line in BufReader::new(f).lines() {
if let Some(rule) = GitIgnoreRule::from(&line?, &parent) {
rules.push(rule);
}
}
rules.reverse();
debug!(
"loaded .gitignore file {:?} with {} rules",
path,
rules.len(),
);
Ok(GitIgnoreFile { rules })
}
}
pub fn find_global_ignore() -> Option<GitIgnoreFile> {
let global_conf = match git2::Config::open_default() {
Ok(conf) => conf,
Err(e) => {
debug!("open default git config failed : {:?}", e);
return None;
}
};
global_conf.get_path("core.excludesfile")
.ok()
.as_ref()
.and_then(
|path| time!(
Debug,
"GitIgnoreFile::new",
GitIgnoreFile::new(path)
).ok()
)
}
#[derive(Debug, Clone, Default)]
pub struct GitIgnoreChain {
file_ids: Vec<Id<GitIgnoreFile>>,
}
impl GitIgnoreChain {
pub fn push(&mut self, id: Id<GitIgnoreFile>) {
self.file_ids.push(id);
}
}
pub struct GitIgnorer {
files: Arena<GitIgnoreFile>,
global_chain: GitIgnoreChain,
}
impl GitIgnorer {
pub fn new() -> Self {
let mut files = Arena::new();
let mut global_chain = GitIgnoreChain::default();
if let Some(gif) = find_global_ignore() {
global_chain.push(files.alloc(gif));
}
Self {
files,
global_chain,
}
}
pub fn root_chain(&mut self, mut dir: &Path) -> GitIgnoreChain {
debug!("searching applicable gifs for {:?}", dir);
let mut chain = self.global_chain.clone();
loop {
debug!(" looking in {:?}", dir);
let ignore_file = dir.join(".gitignore");
if let Ok(gif) = GitIgnoreFile::new(&ignore_file) {
debug!(" adding {:?}", &ignore_file);
chain.push(self.files.alloc(gif));
}
if is_repo(dir) {
debug!(" break because git repo");
break;
}
if let Some(parent) = dir.parent() {
dir = parent;
} else {
break;
}
}
chain
}
pub fn deeper_chain(
&mut self,
parent_chain: &GitIgnoreChain,
dir: &Path,
) -> GitIgnoreChain {
let mut chain = if is_repo(dir) {
self.global_chain.clone()
} else {
parent_chain.clone()
};
let ignore_file = dir.join(".gitignore");
if let Ok(gif) = GitIgnoreFile::new(&ignore_file) {
chain.push(self.files.alloc(gif));
}
chain
}
pub fn accepts(
&self,
chain: &GitIgnoreChain,
path: &Path,
filename: &str,
directory: bool,
) -> bool {
for id in chain.file_ids.iter().rev() {
let file = &self.files[*id];
for rule in &file.rules {
if rule.directory && !directory {
continue;
}
let ok = if rule.filename {
rule.pattern.matches_with(filename, rule.pattern_options)
} else {
rule.pattern.matches_path_with(path, rule.pattern_options)
};
if ok {
return rule.ok;
}
}
}
true
}
}