2022-11-26 23:57:50 +00:00
|
|
|
// Copyright 2021 The Jujutsu Authors
|
2021-05-12 16:43:58 +00:00
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2022-03-10 07:15:02 +00:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::Read;
|
|
|
|
use std::path::PathBuf;
|
2021-05-12 16:43:58 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
2021-06-09 20:57:48 +00:00
|
|
|
use itertools::Itertools;
|
2021-05-12 16:43:58 +00:00
|
|
|
use regex::{escape as regex_escape, Regex};
|
|
|
|
|
2022-01-12 17:24:33 +00:00
|
|
|
#[derive(Debug)]
|
2021-05-12 16:43:58 +00:00
|
|
|
struct GitIgnoreLine {
|
|
|
|
is_negative: bool,
|
|
|
|
regex: Regex,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl GitIgnoreLine {
|
2023-01-14 17:56:33 +00:00
|
|
|
// Remove trailing spaces (unless backslash-escaped). Any character
|
|
|
|
// can be backslash-escaped as well.
|
2021-05-12 16:43:58 +00:00
|
|
|
fn remove_trailing_space(input: &str) -> &str {
|
2022-01-12 17:37:58 +00:00
|
|
|
let input = input.strip_suffix('\r').unwrap_or(input);
|
2023-01-14 17:56:33 +00:00
|
|
|
let mut it = input.char_indices().rev().peekable();
|
|
|
|
while let Some((i, c)) = it.next() {
|
|
|
|
if c != ' ' {
|
|
|
|
return &input[..i + c.len_utf8()];
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
2023-01-14 17:56:33 +00:00
|
|
|
if matches!(it.peek(), Some((_, '\\'))) {
|
|
|
|
if it.skip(1).take_while(|(_, b)| *b == '\\').count() % 2 == 1 {
|
|
|
|
return &input[..i];
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
2023-01-14 17:56:33 +00:00
|
|
|
return &input[..i + 1];
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-14 17:56:33 +00:00
|
|
|
""
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-03-10 07:07:23 +00:00
|
|
|
fn parse(prefix: &str, input: &str) -> Option<GitIgnoreLine> {
|
2021-05-17 05:40:19 +00:00
|
|
|
assert!(prefix.is_empty() || prefix.ends_with('/'));
|
2021-05-12 16:43:58 +00:00
|
|
|
if input.starts_with('#') {
|
2022-03-10 07:07:23 +00:00
|
|
|
return None;
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let input = GitIgnoreLine::remove_trailing_space(input);
|
|
|
|
// Remove leading "!" before checking for empty to match git's implementation
|
|
|
|
// (i.e. just "!" matching nothing, not everything).
|
|
|
|
let (is_negative, input) = match input.strip_prefix('!') {
|
|
|
|
None => (false, input),
|
|
|
|
Some(rest) => (true, rest),
|
|
|
|
};
|
|
|
|
if input.is_empty() {
|
2022-03-10 07:07:23 +00:00
|
|
|
return None;
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let (matches_only_directory, input) = match input.strip_suffix('/') {
|
|
|
|
None => (false, input),
|
|
|
|
Some(rest) => (true, rest),
|
|
|
|
};
|
|
|
|
let (mut is_rooted, input) = match input.strip_prefix('/') {
|
|
|
|
None => (false, input),
|
|
|
|
Some(rest) => (true, rest),
|
|
|
|
};
|
|
|
|
is_rooted |= input.contains('/');
|
|
|
|
|
|
|
|
let mut regex = String::new();
|
2022-01-12 19:01:14 +00:00
|
|
|
regex.push('^');
|
|
|
|
regex.push_str(prefix);
|
|
|
|
if !is_rooted {
|
|
|
|
regex.push_str("(.*/)?");
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
2021-06-09 20:57:48 +00:00
|
|
|
let components = input.split('/').collect_vec();
|
2021-05-12 16:43:58 +00:00
|
|
|
for (i, component) in components.iter().enumerate() {
|
|
|
|
if *component == "**" {
|
|
|
|
if i == components.len() - 1 {
|
|
|
|
regex.push_str(".*");
|
|
|
|
} else {
|
|
|
|
regex.push_str("(.*/)?");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let mut in_escape = false;
|
|
|
|
let mut character_class: Option<String> = None;
|
|
|
|
for c in component.chars() {
|
|
|
|
if in_escape {
|
|
|
|
in_escape = false;
|
|
|
|
if !matches!(c, ' ' | '#' | '!' | '?' | '\\' | '*') {
|
|
|
|
regex.push_str(®ex_escape("\\"));
|
|
|
|
}
|
|
|
|
regex.push_str(®ex_escape(&c.to_string()));
|
|
|
|
} else if c == '\\' {
|
|
|
|
in_escape = true;
|
|
|
|
} else if let Some(characters) = &mut character_class {
|
|
|
|
if c == ']' {
|
|
|
|
regex.push('[');
|
2021-06-14 07:18:38 +00:00
|
|
|
regex.push_str(characters);
|
2021-05-12 16:43:58 +00:00
|
|
|
regex.push(']');
|
|
|
|
character_class = None;
|
|
|
|
} else {
|
|
|
|
characters.push(c);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
in_escape = false;
|
|
|
|
if c == '?' {
|
|
|
|
regex.push_str("[^/]");
|
|
|
|
} else if c == '*' {
|
|
|
|
regex.push_str("[^/]*");
|
|
|
|
} else if c == '[' {
|
|
|
|
character_class = Some(String::new());
|
|
|
|
} else {
|
|
|
|
regex.push_str(®ex_escape(&c.to_string()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if in_escape {
|
|
|
|
regex.push_str(®ex_escape("\\"));
|
|
|
|
}
|
|
|
|
if i < components.len() - 1 {
|
|
|
|
regex.push('/');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if matches_only_directory {
|
|
|
|
regex.push_str("/.*");
|
|
|
|
} else {
|
|
|
|
regex.push_str("(/.*|$)");
|
|
|
|
}
|
|
|
|
let regex = Regex::new(®ex).unwrap();
|
|
|
|
|
2022-03-10 07:07:23 +00:00
|
|
|
Some(GitIgnoreLine { is_negative, regex })
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
2021-05-14 04:26:45 +00:00
|
|
|
fn matches(&self, path: &str) -> bool {
|
2021-05-12 16:43:58 +00:00
|
|
|
self.regex.is_match(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-12 17:24:33 +00:00
|
|
|
#[derive(Debug)]
|
2021-05-12 16:43:58 +00:00
|
|
|
pub struct GitIgnoreFile {
|
|
|
|
parent: Option<Arc<GitIgnoreFile>>,
|
|
|
|
lines: Vec<GitIgnoreLine>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl GitIgnoreFile {
|
|
|
|
pub fn empty() -> Arc<GitIgnoreFile> {
|
|
|
|
Arc::new(GitIgnoreFile {
|
|
|
|
parent: None,
|
|
|
|
lines: vec![],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-10 07:07:23 +00:00
|
|
|
pub fn chain(self: &Arc<GitIgnoreFile>, prefix: &str, input: &[u8]) -> Arc<GitIgnoreFile> {
|
2021-05-12 16:43:58 +00:00
|
|
|
let mut lines = vec![];
|
|
|
|
for input_line in input.split(|b| *b == b'\n') {
|
|
|
|
// Skip non-utf8 lines
|
|
|
|
if let Ok(line_string) = String::from_utf8(input_line.to_vec()) {
|
2022-03-10 07:07:23 +00:00
|
|
|
if let Some(line) = GitIgnoreLine::parse(prefix, &line_string) {
|
2021-05-12 16:43:58 +00:00
|
|
|
lines.push(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-10 07:07:23 +00:00
|
|
|
Arc::new(GitIgnoreFile {
|
2021-05-12 16:43:58 +00:00
|
|
|
parent: Some(self.clone()),
|
|
|
|
lines,
|
2022-03-10 07:07:23 +00:00
|
|
|
})
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-03-10 07:15:02 +00:00
|
|
|
pub fn chain_with_file(
|
|
|
|
self: &Arc<GitIgnoreFile>,
|
|
|
|
prefix: &str,
|
|
|
|
file: PathBuf,
|
|
|
|
) -> Arc<GitIgnoreFile> {
|
|
|
|
if file.is_file() {
|
|
|
|
let mut file = File::open(file).unwrap();
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
file.read_to_end(&mut buf).unwrap();
|
|
|
|
self.chain(prefix, &buf)
|
|
|
|
} else {
|
|
|
|
self.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-12 16:43:58 +00:00
|
|
|
fn all_lines_reversed<'a>(&'a self) -> Box<dyn Iterator<Item = &GitIgnoreLine> + 'a> {
|
|
|
|
if let Some(parent) = &self.parent {
|
|
|
|
Box::new(self.lines.iter().rev().chain(parent.all_lines_reversed()))
|
|
|
|
} else {
|
|
|
|
Box::new(self.lines.iter().rev())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn matches_file(&self, path: &str) -> bool {
|
|
|
|
// Later lines take precedence, so check them in reverse
|
|
|
|
for line in self.all_lines_reversed() {
|
2021-05-14 04:26:45 +00:00
|
|
|
if line.matches(path) {
|
2021-05-12 16:43:58 +00:00
|
|
|
return !line.is_negative;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
2021-05-14 04:26:45 +00:00
|
|
|
|
|
|
|
pub fn matches_all_files_in(&self, dir: &str) -> bool {
|
|
|
|
// Later lines take precedence, so check them in reverse
|
|
|
|
assert!(dir.is_empty() || dir.ends_with('/'));
|
|
|
|
for line in self.all_lines_reversed() {
|
|
|
|
// Let's say there's a "/target/" pattern and then a "!interesting" pattern
|
|
|
|
// after it, then we can't say for sure that all files in target/ match.
|
|
|
|
// TODO: This can be smarter. For example, if there's a pattern "/foo/" followed
|
|
|
|
// by "!/bar/", then we can answer "true" for "foo/". A more complex
|
|
|
|
// case is if a pattern "/foo/" is followed "!/foo/bar/", then we
|
|
|
|
// can say "false" for "foo/" and "true" for "foo/baz/".
|
|
|
|
if line.is_negative {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if line.matches(dir) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn matches_file(input: &[u8], path: &str) -> bool {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", input);
|
2021-05-12 16:43:58 +00:00
|
|
|
file.matches_file(path)
|
|
|
|
}
|
|
|
|
|
2021-05-14 04:26:45 +00:00
|
|
|
fn matches_all_files_in(input: &[u8], path: &str) -> bool {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", input);
|
2021-05-14 04:26:45 +00:00
|
|
|
file.matches_all_files_in(path)
|
|
|
|
}
|
|
|
|
|
2021-05-12 16:43:58 +00:00
|
|
|
#[test]
|
|
|
|
fn test_gitignore_empty_file() {
|
|
|
|
let file = GitIgnoreFile::empty();
|
|
|
|
assert!(!file.matches_file("foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_empty_file_with_prefix() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("dir/", b"");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(!file.matches_file("dir/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_literal() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", b"foo\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(file.matches_file("foo"));
|
|
|
|
assert!(file.matches_file("dir/foo"));
|
|
|
|
assert!(file.matches_file("dir/subdir/foo"));
|
|
|
|
assert!(!file.matches_file("food"));
|
|
|
|
assert!(!file.matches_file("dir/food"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_literal_with_prefix() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("dir/", b"foo\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
// I consider it undefined whether a file in a parent directory matches, but
|
|
|
|
// let's test it anyway
|
2022-01-12 19:01:14 +00:00
|
|
|
assert!(!file.matches_file("foo"));
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(file.matches_file("dir/foo"));
|
|
|
|
assert!(file.matches_file("dir/subdir/foo"));
|
|
|
|
}
|
|
|
|
|
2022-01-12 19:01:14 +00:00
|
|
|
#[test]
|
|
|
|
fn test_gitignore_pattern_same_as_prefix() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("dir/", b"dir\n");
|
2022-01-12 19:01:14 +00:00
|
|
|
assert!(file.matches_file("dir/dir"));
|
|
|
|
// We don't want the "dir" pattern to apply to the parent directory
|
|
|
|
assert!(!file.matches_file("dir/foo"));
|
|
|
|
}
|
|
|
|
|
2021-05-12 16:43:58 +00:00
|
|
|
#[test]
|
|
|
|
fn test_gitignore_rooted_literal() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", b"/foo\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(file.matches_file("foo"));
|
|
|
|
assert!(!file.matches_file("dir/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_rooted_literal_with_prefix() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("dir/", b"/foo\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
// I consider it undefined whether a file in a parent directory matches, but
|
|
|
|
// let's test it anyway
|
|
|
|
assert!(!file.matches_file("foo"));
|
|
|
|
assert!(file.matches_file("dir/foo"));
|
|
|
|
assert!(!file.matches_file("dir/subdir/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_deep_dir() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", b"/dir1/dir2/dir3\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(!file.matches_file("foo"));
|
|
|
|
assert!(!file.matches_file("dir1/foo"));
|
|
|
|
assert!(!file.matches_file("dir1/dir2/foo"));
|
|
|
|
assert!(file.matches_file("dir1/dir2/dir3/foo"));
|
|
|
|
assert!(file.matches_file("dir1/dir2/dir3/dir4/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_match_only_dir() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("", b"/dir/\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(!file.matches_file("dir"));
|
|
|
|
assert!(file.matches_file("dir/foo"));
|
|
|
|
assert!(file.matches_file("dir/subdir/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_unusual_symbols() {
|
|
|
|
assert!(matches_file(b"\\*\n", "*"));
|
|
|
|
assert!(!matches_file(b"\\*\n", "foo"));
|
|
|
|
assert!(matches_file(b"\\\n", "\\"));
|
|
|
|
assert!(matches_file(b"\\!\n", "!"));
|
|
|
|
assert!(matches_file(b"\\?\n", "?"));
|
|
|
|
assert!(!matches_file(b"\\?\n", "x"));
|
|
|
|
// Invalid escapes are treated like literal backslashes
|
|
|
|
assert!(matches_file(b"\\w\n", "\\w"));
|
|
|
|
assert!(!matches_file(b"\\w\n", "w"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_whitespace() {
|
|
|
|
assert!(!matches_file(b" \n", " "));
|
|
|
|
assert!(matches_file(b"\\ \n", " "));
|
2023-01-14 17:56:33 +00:00
|
|
|
assert!(matches_file(b"\\\\ \n", "\\"));
|
|
|
|
assert!(!matches_file(b"\\\\ \n", " "));
|
|
|
|
assert!(matches_file(b"\\\\\\ \n", "\\ "));
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(matches_file(b" a\n", " a"));
|
|
|
|
assert!(matches_file(b"a b\n", "a b"));
|
|
|
|
assert!(matches_file(b"a b \n", "a b"));
|
|
|
|
assert!(!matches_file(b"a b \n", "a b "));
|
|
|
|
assert!(matches_file(b"a b\\ \\ \n", "a b "));
|
|
|
|
// It's unclear how this should be interpreted, but we count spaces before
|
|
|
|
// escaped spaces
|
|
|
|
assert!(matches_file(b"a b \\ \n", "a b "));
|
2022-01-12 17:37:58 +00:00
|
|
|
// A single CR at EOL is ignored
|
|
|
|
assert!(matches_file(b"a\r\n", "a"));
|
|
|
|
assert!(!matches_file(b"a\r\n", "a\r"));
|
|
|
|
assert!(matches_file(b"a\r\r\n", "a\r"));
|
|
|
|
assert!(!matches_file(b"a\r\r\n", "a\r\r"));
|
|
|
|
assert!(matches_file(b"\ra\n", "\ra"));
|
|
|
|
assert!(!matches_file(b"\ra\n", "a"));
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_glob() {
|
|
|
|
assert!(!matches_file(b"*.o\n", "foo"));
|
|
|
|
assert!(matches_file(b"*.o\n", "foo.o"));
|
|
|
|
assert!(!matches_file(b"foo.?\n", "foo"));
|
|
|
|
assert!(!matches_file(b"foo.?\n", "foo."));
|
|
|
|
assert!(matches_file(b"foo.?\n", "foo.o"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_range() {
|
|
|
|
assert!(!matches_file(b"foo.[az]\n", "foo"));
|
|
|
|
assert!(matches_file(b"foo.[az]\n", "foo.a"));
|
|
|
|
assert!(!matches_file(b"foo.[az]\n", "foo.g"));
|
|
|
|
assert!(matches_file(b"foo.[az]\n", "foo.z"));
|
|
|
|
assert!(!matches_file(b"foo.[a-z]\n", "foo"));
|
|
|
|
assert!(matches_file(b"foo.[a-z]\n", "foo.a"));
|
|
|
|
assert!(matches_file(b"foo.[a-z]\n", "foo.g"));
|
|
|
|
assert!(matches_file(b"foo.[a-z]\n", "foo.z"));
|
|
|
|
assert!(matches_file(b"foo.[0-9a-fA-F]\n", "foo.5"));
|
|
|
|
assert!(matches_file(b"foo.[0-9a-fA-F]\n", "foo.c"));
|
|
|
|
assert!(matches_file(b"foo.[0-9a-fA-F]\n", "foo.E"));
|
|
|
|
assert!(!matches_file(b"foo.[0-9a-fA-F]\n", "foo._"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_leading_dir_glob() {
|
|
|
|
assert!(matches_file(b"**/foo\n", "foo"));
|
|
|
|
assert!(matches_file(b"**/foo\n", "dir1/dir2/foo"));
|
|
|
|
assert!(matches_file(b"**/foo\n", "foo/file"));
|
|
|
|
assert!(matches_file(b"**/dir/foo\n", "dir/foo"));
|
|
|
|
assert!(matches_file(b"**/dir/foo\n", "dir1/dir2/dir/foo"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_leading_dir_glob_with_prefix() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file = GitIgnoreFile::empty().chain("dir1/dir2/", b"**/foo\n");
|
2021-05-12 16:43:58 +00:00
|
|
|
// I consider it undefined whether a file in a parent directory matches, but
|
|
|
|
// let's test it anyway
|
|
|
|
assert!(!file.matches_file("foo"));
|
|
|
|
assert!(file.matches_file("dir1/dir2/foo"));
|
|
|
|
assert!(!file.matches_file("dir1/dir2/bar"));
|
|
|
|
assert!(file.matches_file("dir1/dir2/sub1/sub2/foo"));
|
|
|
|
assert!(!file.matches_file("dir1/dir2/sub1/sub2/bar"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_trailing_dir_glob() {
|
|
|
|
assert!(!matches_file(b"abc/**\n", "abc"));
|
|
|
|
assert!(matches_file(b"abc/**\n", "abc/file"));
|
|
|
|
assert!(matches_file(b"abc/**\n", "abc/dir/file"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_internal_dir_glob() {
|
|
|
|
assert!(matches_file(b"a/**/b\n", "a/b"));
|
|
|
|
assert!(matches_file(b"a/**/b\n", "a/x/b"));
|
|
|
|
assert!(matches_file(b"a/**/b\n", "a/x/y/b"));
|
|
|
|
assert!(!matches_file(b"a/**/b\n", "ax/y/b"));
|
|
|
|
assert!(!matches_file(b"a/**/b\n", "a/x/yb"));
|
|
|
|
assert!(!matches_file(b"a/**/b\n", "ab"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_internal_dir_glob_not_really() {
|
|
|
|
assert!(!matches_file(b"a/x**y/b\n", "a/b"));
|
|
|
|
assert!(matches_file(b"a/x**y/b\n", "a/xy/b"));
|
|
|
|
assert!(matches_file(b"a/x**y/b\n", "a/xzzzy/b"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_line_ordering() {
|
|
|
|
assert!(matches_file(b"foo\n!foo/bar\n", "foo"));
|
|
|
|
assert!(!matches_file(b"foo\n!foo/bar\n", "foo/bar"));
|
|
|
|
assert!(matches_file(b"foo\n!foo/bar\n", "foo/baz"));
|
|
|
|
assert!(matches_file(b"foo\n!foo/bar\nfoo/bar/baz", "foo"));
|
|
|
|
assert!(!matches_file(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar"));
|
|
|
|
assert!(matches_file(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/baz"));
|
|
|
|
assert!(!matches_file(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/quux"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_file_ordering() {
|
2022-03-10 07:07:23 +00:00
|
|
|
let file1 = GitIgnoreFile::empty().chain("", b"foo\n");
|
|
|
|
let file2 = file1.chain("foo/", b"!bar");
|
|
|
|
let file3 = file2.chain("foo/bar/", b"baz");
|
2021-05-12 16:43:58 +00:00
|
|
|
assert!(file1.matches_file("foo"));
|
|
|
|
assert!(file1.matches_file("foo/bar"));
|
|
|
|
assert!(!file2.matches_file("foo/bar"));
|
|
|
|
assert!(file2.matches_file("foo/baz"));
|
|
|
|
assert!(file3.matches_file("foo/bar/baz"));
|
|
|
|
assert!(!file3.matches_file("foo/bar/qux"));
|
|
|
|
}
|
2021-05-14 04:26:45 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_gitignore_match_dir() {
|
|
|
|
assert!(matches_all_files_in(b"foo\n", "foo/"));
|
|
|
|
assert!(matches_all_files_in(b"foo\nbar\n", "foo/"));
|
|
|
|
assert!(matches_all_files_in(b"!foo\nbar\n", "bar/"));
|
|
|
|
assert!(!matches_all_files_in(b"foo\n!bar\n", "foo/"));
|
|
|
|
// This one could return true, but it doesn't currently
|
|
|
|
assert!(!matches_all_files_in(b"foo\n!/bar\n", "foo/"));
|
|
|
|
}
|
2021-05-12 16:43:58 +00:00
|
|
|
}
|