From 2364cadfd5070c146ade07290652e1ad4f738c54 Mon Sep 17 00:00:00 2001 From: grimhilt Date: Sat, 31 Aug 2024 19:42:44 +0200 Subject: [PATCH] feat(nsignore): implement nsignore --- src/store/nsignore.rs | 144 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/store/nsignore.rs diff --git a/src/store/nsignore.rs b/src/store/nsignore.rs new file mode 100644 index 0000000..d0e64b6 --- /dev/null +++ b/src/store/nsignore.rs @@ -0,0 +1,144 @@ +use regex::Regex; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +/// Search .nsignore or .nextsyncignore in this path level +/// +/// * `level_path`: the dir we want to find the ignore file in +pub fn get_nsignore_file(level_path: &PathBuf) -> Option { + let mut path = level_path.to_path_buf(); + + path.push(".nextsyncignore"); + if path.exists() { + return Some(path); + } + + path.pop(); + path.push(".nsignore"); + if path.exists() { + return Some(path); + } + + None +} + +pub fn get_nsignore_rules(nsignore_path: &PathBuf) -> Vec { + let file = File::open(nsignore_path).expect(&format!( + "fatal: failed to open nextsyncignore file in '{}'", + nsignore_path.display() + )); + + let reader = BufReader::new(file); + reader + .lines() + .map(|rule| normalize_rule(rule.unwrap())) + .collect() +} + +fn normalize_rule(l: String) -> String { + let mut line = l; + + // define / as root + let re = Regex::new("^(!)?/").unwrap(); + line = re.replace_all(&line, "$1^").to_string(); + + // escape . + let re = Regex::new(r"\.").unwrap(); + line = re.replace_all(&line, r"\.").to_string(); + + // add . before * + let re = Regex::new(r"\*").unwrap(); + line = re.replace_all(&line, r".*").to_string(); + + // add optional .* at the end of / + let re = Regex::new(r"/$").unwrap(); + line = re.replace_all(&line, r"(/.*)?").to_string(); + line +} + +pub fn is_file_ignored(path: &str, rules: &Vec) -> bool { + let mut ignored = false; + for rule in rules { + if rule.starts_with("!") { + if !ignored { + continue; + } + + let strip_rule = rule.strip_prefix("!").unwrap(); + let re = Regex::new(&strip_rule).unwrap(); + if re.is_match(path) { + ignored = false; + } + } else if !ignored { + let re = Regex::new(&rule).unwrap(); + if re.is_match(path) { + ignored = true; + } + } + } + ignored +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn test_multiple(rules: Vec<&str>, tests: Vec<(&str, bool)>) { + let rules = rules + .iter() + .map(|rule| normalize_rule(rule.to_string())) + .collect(); + for (file, res) in tests.into_iter() { + assert_eq!(is_file_ignored(file, &rules), res, "{}", file); + } + } + + #[test] + fn test_ignore_files() { + let rules = vec![ + "*.log", + "exclude", + "/logs/", + "/build/target/", + "**/*.swp", + "secret/", + ]; + + let tests = vec![ + ("error.log", true), + ("./error.log", true), + ("dir/error.log", true), + ("exclude", true), + ("dir/exclude", true), + ("logs/dir/file2", true), + ("dir/logs/dir/file2", false), + ("build/target/dir/file1", true), + ("build", false), + ("build/target", true), + ("dir/build/target", false), + ("dir/file.swp", true), + (".swp", false), + ("secret", true), + ("dir/secret", true), + ("dir/secret/file", true), + ]; + + test_multiple(rules, tests); + } + + #[test] + fn test_ignore_files_negation() { + let rules = vec!["*", "!*.log", "!*log.*"]; + let tests = vec![ + ("file", true), + ("dir/file", true), + ("file.log", false), + ("log.file", false), + ("dir/file.log", false), + ("dir/log.file", false), + ]; + test_multiple(rules, tests); + } +}