// Copyright 2021 The Jujutsu Authors//// 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. #![allow(missing_docs)] use std::fs;use std::io;use std::iter;use std::path::Path;use std::path::PathBuf;use std::sync::Arc; use ignore::gitignore;use thiserror::Error; #[derive(Debug, Error)]pub enum GitIgnoreError { #[error("Failed to read ignore patterns from file {path}")] ReadFile { path: PathBuf, source: io::Error }, #[error("Invalid UTF-8 for ignore pattern in {path} on line #{line_num_for_display}: {line}")] InvalidUtf8 { path: PathBuf, line_num_for_display: usize, line: String, source: std::str::Utf8Error, }, #[error("Failed to parse ignore patterns from file {path}")] Underlying { path: PathBuf, source: ignore::Error, },} /// Models the effective contents of multiple .gitignore files.#[derive(Debug)]pub struct GitIgnoreFile { parent: Option<Arc<GitIgnoreFile>>, matcher: gitignore::Gitignore,} impl GitIgnoreFile { pub fn empty() -> Arc<GitIgnoreFile> { Arc::new(GitIgnoreFile { parent: None, matcher: gitignore::Gitignore::empty(), }) } /// Concatenates new `.gitignore` content at the `prefix` directory. /// /// The `prefix` should be a slash-separated path relative to the workspace /// root. pub fn chain( self: &Arc<GitIgnoreFile>, prefix: &str, ignore_path: &Path, input: &[u8], ) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> { let mut builder = gitignore::GitignoreBuilder::new(prefix); for (i, input_line) in input.split(|b| *b == b'\n').enumerate() { let line = std::str::from_utf8(input_line).map_err(|err| GitIgnoreError::InvalidUtf8 { path: ignore_path.to_path_buf(), line_num_for_display: i + 1, line: String::from_utf8_lossy(input_line).to_string(), source: err, })?; // The `from` argument doesn't provide any diagnostics or correctness, so it is // not required. It only allows retrieving the path from the `Glob` later, which // we never do. builder .add_line(None, line) .map_err(|err| GitIgnoreError::Underlying { path: ignore_path.to_path_buf(), source: err, })?; } let matcher = builder.build().map_err(|err| GitIgnoreError::Underlying { path: ignore_path.to_path_buf(), source: err, })?; let parent = if self.matcher.is_empty() { self.parent.clone() // omit the empty root } else { Some(self.clone()) }; Ok(Arc::new(GitIgnoreFile { parent, matcher })) } /// Concatenates new `.gitignore` file at the `prefix` directory. /// /// The `prefix` should be a slash-separated path relative to the workspace /// root. pub fn chain_with_file( self: &Arc<GitIgnoreFile>, prefix: &str, file: PathBuf, ) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> { if file.is_file() { let buf = fs::read(&file).map_err(|err| GitIgnoreError::ReadFile { path: file.clone(), source: err, })?; self.chain(prefix, &file, &buf) } else { Ok(self.clone()) } } fn matches_helper(&self, path: &str, is_dir: bool) -> bool { iter::successors(Some(self), |file| file.parent.as_deref()) .find_map(|file| { // TODO: the documentation warns that // `matched_path_or_any_parents` is slower than `matched`; // ideally, we would switch to that. match file.matcher.matched_path_or_any_parents(path, is_dir) { ignore::Match::None => None, ignore::Match::Ignore(_) => Some(true), ignore::Match::Whitelist(_) => Some(false), } }) .unwrap_or_default() } /// Returns whether specified path (not just file!) should be ignored. This /// method does not directly define which files should not be tracked in /// the repository. Instead, it performs a simple matching against the /// last applicable .gitignore line. The effective set of paths /// ignored in the repository should take into account that all (untracked) /// files within a ignored directory should be ignored unconditionally. /// The code in this file does not take that into account. pub fn matches(&self, path: &str) -> bool { //If path ends with slash, consider it as a directory. let (path, is_dir) = match path.strip_suffix('/') { Some(path) => (path, true), None => (path, false), }; self.matches_helper(path, is_dir) }} #[cfg(test)]mod tests { use super::*; fn matches(input: &[u8], path: &str) -> bool { let file = GitIgnoreFile::empty() .chain("", Path::new(""), input) .unwrap(); file.matches(path) } #[test] fn test_gitignore_empty_file() { let file = GitIgnoreFile::empty(); assert!(!file.matches("foo")); } #[test] fn test_gitignore_empty_file_with_prefix() { let file = GitIgnoreFile::empty() .chain("dir/", Path::new(""), b"") .unwrap(); assert!(!file.matches("dir/foo")); } #[test] fn test_gitignore_literal() { let file = GitIgnoreFile::empty() .chain("", Path::new(""), b"foo\n") .unwrap(); assert!(file.matches("foo")); assert!(file.matches("dir/foo")); assert!(file.matches("dir/subdir/foo")); assert!(!file.matches("food")); assert!(!file.matches("dir/food")); } #[test] fn test_gitignore_literal_with_prefix() { let file = GitIgnoreFile::empty() .chain("./dir/", Path::new(""), b"foo\n") .unwrap(); assert!(file.matches("dir/foo")); assert!(file.matches("dir/subdir/foo")); } #[test] fn test_gitignore_pattern_same_as_prefix() { let file = GitIgnoreFile::empty() .chain("dir/", Path::new(""), b"dir\n") .unwrap(); assert!(file.matches("dir/dir")); // We don't want the "dir" pattern to apply to the parent directory assert!(!file.matches("dir/foo")); } #[test] fn test_gitignore_rooted_literal() { let file = GitIgnoreFile::empty() .chain("", Path::new(""), b"/foo\n") .unwrap(); assert!(file.matches("foo")); assert!(!file.matches("dir/foo")); } #[test] fn test_gitignore_rooted_literal_with_prefix() { let file = GitIgnoreFile::empty() .chain("dir/", Path::new(""), b"/foo\n") .unwrap(); assert!(file.matches("dir/foo")); assert!(!file.matches("dir/subdir/foo")); } #[test] fn test_gitignore_deep_dir() { let file = GitIgnoreFile::empty() .chain("", Path::new(""), b"/dir1/dir2/dir3\n") .unwrap(); assert!(!file.matches("foo")); assert!(!file.matches("dir1/foo")); assert!(!file.matches("dir1/dir2/foo")); assert!(file.matches("dir1/dir2/dir3/foo")); assert!(file.matches("dir1/dir2/dir3/dir4/foo")); } #[test] fn test_gitignore_deep_dir_chained() { // Prefix is relative to root, not to parent file let file = GitIgnoreFile::empty() .chain("", Path::new(""), b"/dummy\n") .unwrap() .chain("dir1/", Path::new(""), b"/dummy\n") .unwrap() .chain("dir1/dir2/", Path::new(""), b"/dir3\n") .unwrap(); assert!(!file.matches("foo")); assert!(!file.matches("dir1/foo")); assert!(!file.matches("dir1/dir2/foo")); assert!(file.matches("dir1/dir2/dir3/foo")); assert!(file.matches("dir1/dir2/dir3/dir4/foo")); } #[test] fn test_gitignore_match_only_dir() { let file = GitIgnoreFile::empty() .chain("", Path::new(""), b"/dir/\n") .unwrap(); assert!(!file.matches("dir")); assert!(file.matches("dir/foo")); assert!(file.matches("dir/subdir/foo")); } #[test] fn test_gitignore_unusual_symbols() { assert!(matches(b"\\*\n", "*")); assert!(!matches(b"\\*\n", "foo")); assert!(matches(b"\\!\n", "!")); assert!(matches(b"\\?\n", "?")); assert!(!matches(b"\\?\n", "x")); assert!(matches(b"\\w\n", "w")); assert!(GitIgnoreFile::empty() .chain("", Path::new(""), b"\\\n") .is_err()); } #[test] #[cfg(not(target_os = "windows"))] fn test_gitignore_backslash_path() { assert!(!matches(b"/foo/bar", "/foo\\bar")); assert!(!matches(b"/foo/bar", "/foo/bar\\")); assert!(!matches(b"/foo/bar/", "/foo\\bar/")); assert!(!matches(b"/foo/bar/", "/foo\\bar\\/")); // Invalid escapes are treated like literal backslashes assert!(!matches(b"\\w\n", "\\w")); assert!(matches(b"\\\\ \n", "\\ ")); assert!(matches(b"\\\\\\ \n", "\\ ")); } #[test] #[cfg(target_os = "windows")] /// ignore crate consider backslashes as a directory divider only on /// Windows. fn test_gitignore_backslash_path() { assert!(matches(b"/foo/bar", "/foo\\bar")); assert!(matches(b"/foo/bar", "/foo/bar\\")); assert!(matches(b"/foo/bar/", "/foo\\bar/")); assert!(matches(b"/foo/bar/", "/foo\\bar\\/")); assert!(matches(b"\\w\n", "\\w")); assert!(!matches(b"\\\\ \n", "\\ ")); assert!(!matches(b"\\\\\\ \n", "\\ ")); } #[test] fn test_gitignore_whitespace() { assert!(!matches(b" \n", " ")); assert!(matches(b"\\ \n", " ")); assert!(!matches(b"\\\\ \n", " ")); assert!(matches(b" a\n", " a")); assert!(matches(b"a b\n", "a b")); assert!(matches(b"a b \n", "a b")); assert!(!matches(b"a b \n", "a b ")); assert!(matches(b"a b\\ \\ \n", "a b ")); // Trail CRs at EOL is ignored assert!(matches(b"a\r\n", "a")); assert!(!matches(b"a\r\n", "a\r")); assert!(!matches(b"a\r\r\n", "a\r")); assert!(matches(b"a\r\r\n", "a")); assert!(!matches(b"a\r\r\n", "a\r\r")); assert!(matches(b"a\r\r\n", "a")); assert!(matches(b"\ra\n", "\ra")); assert!(!matches(b"\ra\n", "a")); assert!(GitIgnoreFile::empty() .chain("", Path::new(""), b"a b \\ \n") .is_err()); } #[test] fn test_gitignore_glob() { assert!(!matches(b"*.o\n", "foo")); assert!(matches(b"*.o\n", "foo.o")); assert!(!matches(b"foo.?\n", "foo")); assert!(!matches(b"foo.?\n", "foo.")); assert!(matches(b"foo.?\n", "foo.o")); } #[test] fn test_gitignore_range() { assert!(!matches(b"foo.[az]\n", "foo")); assert!(matches(b"foo.[az]\n", "foo.a")); assert!(!matches(b"foo.[az]\n", "foo.g")); assert!(matches(b"foo.[az]\n", "foo.z")); assert!(!matches(b"foo.[a-z]\n", "foo")); assert!(matches(b"foo.[a-z]\n", "foo.a")); assert!(matches(b"foo.[a-z]\n", "foo.g")); assert!(matches(b"foo.[a-z]\n", "foo.z")); assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.5")); assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.c")); assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.E")); assert!(!matches(b"foo.[0-9a-fA-F]\n", "foo._")); } #[test] fn test_gitignore_leading_dir_glob() { assert!(matches(b"**/foo\n", "foo")); assert!(matches(b"**/foo\n", "dir1/dir2/foo")); assert!(matches(b"**/foo\n", "foo/file")); assert!(matches(b"**/dir/foo\n", "dir/foo")); assert!(matches(b"**/dir/foo\n", "dir1/dir2/dir/foo")); } #[test] fn test_gitignore_leading_dir_glob_with_prefix() { let file = GitIgnoreFile::empty() .chain("dir1/dir2/", Path::new(""), b"**/foo\n") .unwrap(); assert!(file.matches("dir1/dir2/foo")); assert!(!file.matches("dir1/dir2/bar")); assert!(file.matches("dir1/dir2/sub1/sub2/foo")); assert!(!file.matches("dir1/dir2/sub1/sub2/bar")); } #[test] fn test_gitignore_trailing_dir_glob() { assert!(!matches(b"abc/**\n", "abc")); assert!(matches(b"abc/**\n", "abc/file")); assert!(matches(b"abc/**\n", "abc/dir/file")); } #[test] fn test_gitignore_internal_dir_glob() { assert!(matches(b"a/**/b\n", "a/b")); assert!(matches(b"a/**/b\n", "a/x/b")); assert!(matches(b"a/**/b\n", "a/x/y/b")); assert!(!matches(b"a/**/b\n", "ax/y/b")); assert!(!matches(b"a/**/b\n", "a/x/yb")); assert!(!matches(b"a/**/b\n", "ab")); } #[test] fn test_gitignore_internal_dir_glob_not_really() { assert!(!matches(b"a/x**y/b\n", "a/b")); assert!(matches(b"a/x**y/b\n", "a/xy/b")); assert!(matches(b"a/x**y/b\n", "a/xzzzy/b")); } #[test] fn test_gitignore_line_ordering() { assert!(matches(b"foo\n!foo/bar\n", "foo")); assert!(!matches(b"foo\n!foo/bar\n", "foo/bar")); assert!(matches(b"foo\n!foo/bar\n", "foo/baz")); assert!(matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo")); assert!(!matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar")); assert!(matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/baz")); assert!(!matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/quux")); assert!(!matches(b"foo/*\n!foo/bar", "foo/bar")); } #[test] fn test_gitignore_file_ordering() { let file1 = GitIgnoreFile::empty() .chain("", Path::new(""), b"/foo\n") .unwrap(); let file2 = file1.chain("foo/", Path::new(""), b"!/bar").unwrap(); let file3 = file2.chain("foo/bar/", Path::new(""), b"/baz").unwrap(); assert!(file1.matches("foo")); assert!(file1.matches("foo/bar")); assert!(!file2.matches("foo/bar")); assert!(!file2.matches("foo/bar/baz")); assert!(file2.matches("foo/baz")); assert!(file3.matches("foo/bar/baz")); assert!(!file3.matches("foo/bar/qux")); } #[test] fn test_gitignore_negative_parent_directory() { // The following script shows that Git ignores the file: // // ```bash // $ rm -rf test-repo && \ // git init test-repo &>/dev/null && \ // cd test-repo && \ // printf 'A/B.*\n!/A/\n' >.gitignore && \ // mkdir A && \ // touch A/B.ext && \ // git check-ignore A/B.ext // A/B.ext // ``` let ignore = GitIgnoreFile::empty() .chain("", Path::new(""), b"foo/bar.*\n!/foo/\n") .unwrap(); assert!(ignore.matches("foo/bar.ext")); let ignore = GitIgnoreFile::empty() .chain("", Path::new(""), b"!/foo/\nfoo/bar.*\n") .unwrap(); assert!(ignore.matches("foo/bar.ext")); }}
// Copyright 2021 The Jujutsu Authors
//
// 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.
#![allow(missing_docs)]
use std::fs;
use std::io;
use std::iter;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use ignore::gitignore;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GitIgnoreError {
#[error("Failed to read ignore patterns from file {path}")]
ReadFile { path: PathBuf, source: io::Error },
#[error("Invalid UTF-8 for ignore pattern in {path} on line #{line_num_for_display}: {line}")]
InvalidUtf8 {
path: PathBuf,
line_num_for_display: usize,
line: String,
source: std::str::Utf8Error,
},
#[error("Failed to parse ignore patterns from file {path}")]
Underlying {
path: PathBuf,
source: ignore::Error,
},
}
/// Models the effective contents of multiple .gitignore files.
#[derive(Debug)]
pub struct GitIgnoreFile {
parent: Option<Arc<GitIgnoreFile>>,
matcher: gitignore::Gitignore,
}
impl GitIgnoreFile {
pub fn empty() -> Arc<GitIgnoreFile> {
Arc::new(GitIgnoreFile {
parent: None,
matcher: gitignore::Gitignore::empty(),
})
}
/// Concatenates new `.gitignore` content at the `prefix` directory.
///
/// The `prefix` should be a slash-separated path relative to the workspace
/// root.
pub fn chain(
self: &Arc<GitIgnoreFile>,
prefix: &str,
ignore_path: &Path,
input: &[u8],
) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
let mut builder = gitignore::GitignoreBuilder::new(prefix);
for (i, input_line) in input.split(|b| *b == b'\n').enumerate() {
let line =
std::str::from_utf8(input_line).map_err(|err| GitIgnoreError::InvalidUtf8 {
path: ignore_path.to_path_buf(),
line_num_for_display: i + 1,
line: String::from_utf8_lossy(input_line).to_string(),
source: err,
})?;
// The `from` argument doesn't provide any diagnostics or correctness, so it is
// not required. It only allows retrieving the path from the `Glob` later, which
// we never do.
builder
.add_line(None, line)
.map_err(|err| GitIgnoreError::Underlying {
path: ignore_path.to_path_buf(),
source: err,
})?;
}
let matcher = builder.build().map_err(|err| GitIgnoreError::Underlying {
path: ignore_path.to_path_buf(),
source: err,
})?;
let parent = if self.matcher.is_empty() {
self.parent.clone() // omit the empty root
} else {
Some(self.clone())
};
Ok(Arc::new(GitIgnoreFile { parent, matcher }))
}
/// Concatenates new `.gitignore` file at the `prefix` directory.
///
/// The `prefix` should be a slash-separated path relative to the workspace
/// root.
pub fn chain_with_file(
self: &Arc<GitIgnoreFile>,
prefix: &str,
file: PathBuf,
) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
if file.is_file() {
let buf = fs::read(&file).map_err(|err| GitIgnoreError::ReadFile {
path: file.clone(),
source: err,
})?;
self.chain(prefix, &file, &buf)
} else {
Ok(self.clone())
}
}
fn matches_helper(&self, path: &str, is_dir: bool) -> bool {
iter::successors(Some(self), |file| file.parent.as_deref())
.find_map(|file| {
// TODO: the documentation warns that
// `matched_path_or_any_parents` is slower than `matched`;
// ideally, we would switch to that.
match file.matcher.matched_path_or_any_parents(path, is_dir) {
ignore::Match::None => None,
ignore::Match::Ignore(_) => Some(true),
ignore::Match::Whitelist(_) => Some(false),
}
})
.unwrap_or_default()
}
/// Returns whether specified path (not just file!) should be ignored. This
/// method does not directly define which files should not be tracked in
/// the repository. Instead, it performs a simple matching against the
/// last applicable .gitignore line. The effective set of paths
/// ignored in the repository should take into account that all (untracked)
/// files within a ignored directory should be ignored unconditionally.
/// The code in this file does not take that into account.
pub fn matches(&self, path: &str) -> bool {
//If path ends with slash, consider it as a directory.
let (path, is_dir) = match path.strip_suffix('/') {
Some(path) => (path, true),
None => (path, false),
};
self.matches_helper(path, is_dir)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn matches(input: &[u8], path: &str) -> bool {
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), input)
.unwrap();
file.matches(path)
}
#[test]
fn test_gitignore_empty_file() {
let file = GitIgnoreFile::empty();
assert!(!file.matches("foo"));
}
#[test]
fn test_gitignore_empty_file_with_prefix() {
let file = GitIgnoreFile::empty()
.chain("dir/", Path::new(""), b"")
.unwrap();
assert!(!file.matches("dir/foo"));
}
#[test]
fn test_gitignore_literal() {
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), b"foo\n")
.unwrap();
assert!(file.matches("foo"));
assert!(file.matches("dir/foo"));
assert!(file.matches("dir/subdir/foo"));
assert!(!file.matches("food"));
assert!(!file.matches("dir/food"));
}
#[test]
fn test_gitignore_literal_with_prefix() {
let file = GitIgnoreFile::empty()
.chain("./dir/", Path::new(""), b"foo\n")
.unwrap();
assert!(file.matches("dir/foo"));
assert!(file.matches("dir/subdir/foo"));
}
#[test]
fn test_gitignore_pattern_same_as_prefix() {
let file = GitIgnoreFile::empty()
.chain("dir/", Path::new(""), b"dir\n")
.unwrap();
assert!(file.matches("dir/dir"));
// We don't want the "dir" pattern to apply to the parent directory
assert!(!file.matches("dir/foo"));
}
#[test]
fn test_gitignore_rooted_literal() {
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), b"/foo\n")
.unwrap();
assert!(file.matches("foo"));
assert!(!file.matches("dir/foo"));
}
#[test]
fn test_gitignore_rooted_literal_with_prefix() {
let file = GitIgnoreFile::empty()
.chain("dir/", Path::new(""), b"/foo\n")
.unwrap();
assert!(file.matches("dir/foo"));
assert!(!file.matches("dir/subdir/foo"));
}
#[test]
fn test_gitignore_deep_dir() {
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), b"/dir1/dir2/dir3\n")
.unwrap();
assert!(!file.matches("foo"));
assert!(!file.matches("dir1/foo"));
assert!(!file.matches("dir1/dir2/foo"));
assert!(file.matches("dir1/dir2/dir3/foo"));
assert!(file.matches("dir1/dir2/dir3/dir4/foo"));
}
#[test]
fn test_gitignore_deep_dir_chained() {
// Prefix is relative to root, not to parent file
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), b"/dummy\n")
.unwrap()
.chain("dir1/", Path::new(""), b"/dummy\n")
.unwrap()
.chain("dir1/dir2/", Path::new(""), b"/dir3\n")
.unwrap();
assert!(!file.matches("foo"));
assert!(!file.matches("dir1/foo"));
assert!(!file.matches("dir1/dir2/foo"));
assert!(file.matches("dir1/dir2/dir3/foo"));
assert!(file.matches("dir1/dir2/dir3/dir4/foo"));
}
#[test]
fn test_gitignore_match_only_dir() {
let file = GitIgnoreFile::empty()
.chain("", Path::new(""), b"/dir/\n")
.unwrap();
assert!(!file.matches("dir"));
assert!(file.matches("dir/foo"));
assert!(file.matches("dir/subdir/foo"));
}
#[test]
fn test_gitignore_unusual_symbols() {
assert!(matches(b"\\*\n", "*"));
assert!(!matches(b"\\*\n", "foo"));
assert!(matches(b"\\!\n", "!"));
assert!(matches(b"\\?\n", "?"));
assert!(!matches(b"\\?\n", "x"));
assert!(matches(b"\\w\n", "w"));
assert!(GitIgnoreFile::empty()
.chain("", Path::new(""), b"\\\n")
.is_err());
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_gitignore_backslash_path() {
assert!(!matches(b"/foo/bar", "/foo\\bar"));
assert!(!matches(b"/foo/bar", "/foo/bar\\"));
assert!(!matches(b"/foo/bar/", "/foo\\bar/"));
assert!(!matches(b"/foo/bar/", "/foo\\bar\\/"));
// Invalid escapes are treated like literal backslashes
assert!(!matches(b"\\w\n", "\\w"));
assert!(matches(b"\\\\ \n", "\\ "));
assert!(matches(b"\\\\\\ \n", "\\ "));
}
#[test]
#[cfg(target_os = "windows")]
/// ignore crate consider backslashes as a directory divider only on
/// Windows.
fn test_gitignore_backslash_path() {
assert!(matches(b"/foo/bar", "/foo\\bar"));
assert!(matches(b"/foo/bar", "/foo/bar\\"));
assert!(matches(b"/foo/bar/", "/foo\\bar/"));
assert!(matches(b"/foo/bar/", "/foo\\bar\\/"));
assert!(matches(b"\\w\n", "\\w"));
assert!(!matches(b"\\\\ \n", "\\ "));
assert!(!matches(b"\\\\\\ \n", "\\ "));
}
#[test]
fn test_gitignore_whitespace() {
assert!(!matches(b" \n", " "));
assert!(matches(b"\\ \n", " "));
assert!(!matches(b"\\\\ \n", " "));
assert!(matches(b" a\n", " a"));
assert!(matches(b"a b\n", "a b"));
assert!(matches(b"a b \n", "a b"));
assert!(!matches(b"a b \n", "a b "));
assert!(matches(b"a b\\ \\ \n", "a b "));
// Trail CRs at EOL is ignored
assert!(matches(b"a\r\n", "a"));
assert!(!matches(b"a\r\n", "a\r"));
assert!(!matches(b"a\r\r\n", "a\r"));
assert!(matches(b"a\r\r\n", "a"));
assert!(!matches(b"a\r\r\n", "a\r\r"));
assert!(matches(b"a\r\r\n", "a"));
assert!(matches(b"\ra\n", "\ra"));
assert!(!matches(b"\ra\n", "a"));
assert!(GitIgnoreFile::empty()
.chain("", Path::new(""), b"a b \\ \n")
.is_err());
}
#[test]
fn test_gitignore_glob() {
assert!(!matches(b"*.o\n", "foo"));
assert!(matches(b"*.o\n", "foo.o"));
assert!(!matches(b"foo.?\n", "foo"));
assert!(!matches(b"foo.?\n", "foo."));
assert!(matches(b"foo.?\n", "foo.o"));
}
#[test]
fn test_gitignore_range() {
assert!(!matches(b"foo.[az]\n", "foo"));
assert!(matches(b"foo.[az]\n", "foo.a"));
assert!(!matches(b"foo.[az]\n", "foo.g"));
assert!(matches(b"foo.[az]\n", "foo.z"));
assert!(!matches(b"foo.[a-z]\n", "foo"));
assert!(matches(b"foo.[a-z]\n", "foo.a"));
assert!(matches(b"foo.[a-z]\n", "foo.g"));
assert!(matches(b"foo.[a-z]\n", "foo.z"));
assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.5"));
assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.c"));
assert!(matches(b"foo.[0-9a-fA-F]\n", "foo.E"));
assert!(!matches(b"foo.[0-9a-fA-F]\n", "foo._"));
}
#[test]
fn test_gitignore_leading_dir_glob() {
assert!(matches(b"**/foo\n", "foo"));
assert!(matches(b"**/foo\n", "dir1/dir2/foo"));
assert!(matches(b"**/foo\n", "foo/file"));
assert!(matches(b"**/dir/foo\n", "dir/foo"));
assert!(matches(b"**/dir/foo\n", "dir1/dir2/dir/foo"));
}
#[test]
fn test_gitignore_leading_dir_glob_with_prefix() {
let file = GitIgnoreFile::empty()
.chain("dir1/dir2/", Path::new(""), b"**/foo\n")
.unwrap();
assert!(file.matches("dir1/dir2/foo"));
assert!(!file.matches("dir1/dir2/bar"));
assert!(file.matches("dir1/dir2/sub1/sub2/foo"));
assert!(!file.matches("dir1/dir2/sub1/sub2/bar"));
}
#[test]
fn test_gitignore_trailing_dir_glob() {
assert!(!matches(b"abc/**\n", "abc"));
assert!(matches(b"abc/**\n", "abc/file"));
assert!(matches(b"abc/**\n", "abc/dir/file"));
}
#[test]
fn test_gitignore_internal_dir_glob() {
assert!(matches(b"a/**/b\n", "a/b"));
assert!(matches(b"a/**/b\n", "a/x/b"));
assert!(matches(b"a/**/b\n", "a/x/y/b"));
assert!(!matches(b"a/**/b\n", "ax/y/b"));
assert!(!matches(b"a/**/b\n", "a/x/yb"));
assert!(!matches(b"a/**/b\n", "ab"));
}
#[test]
fn test_gitignore_internal_dir_glob_not_really() {
assert!(!matches(b"a/x**y/b\n", "a/b"));
assert!(matches(b"a/x**y/b\n", "a/xy/b"));
assert!(matches(b"a/x**y/b\n", "a/xzzzy/b"));
}
#[test]
fn test_gitignore_line_ordering() {
assert!(matches(b"foo\n!foo/bar\n", "foo"));
assert!(!matches(b"foo\n!foo/bar\n", "foo/bar"));
assert!(matches(b"foo\n!foo/bar\n", "foo/baz"));
assert!(matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo"));
assert!(!matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar"));
assert!(matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/baz"));
assert!(!matches(b"foo\n!foo/bar\nfoo/bar/baz", "foo/bar/quux"));
assert!(!matches(b"foo/*\n!foo/bar", "foo/bar"));
}
#[test]
fn test_gitignore_file_ordering() {
let file1 = GitIgnoreFile::empty()
.chain("", Path::new(""), b"/foo\n")
.unwrap();
let file2 = file1.chain("foo/", Path::new(""), b"!/bar").unwrap();
let file3 = file2.chain("foo/bar/", Path::new(""), b"/baz").unwrap();
assert!(file1.matches("foo"));
assert!(file1.matches("foo/bar"));
assert!(!file2.matches("foo/bar"));
assert!(!file2.matches("foo/bar/baz"));
assert!(file2.matches("foo/baz"));
assert!(file3.matches("foo/bar/baz"));
assert!(!file3.matches("foo/bar/qux"));
}
#[test]
fn test_gitignore_negative_parent_directory() {
// The following script shows that Git ignores the file:
//
// ```bash
// $ rm -rf test-repo && \
// git init test-repo &>/dev/null && \
// cd test-repo && \
// printf 'A/B.*\n!/A/\n' >.gitignore && \
// mkdir A && \
// touch A/B.ext && \
// git check-ignore A/B.ext
// A/B.ext
// ```
let ignore = GitIgnoreFile::empty()
.chain("", Path::new(""), b"foo/bar.*\n!/foo/\n")
.unwrap();
assert!(ignore.matches("foo/bar.ext"));
let ignore = GitIgnoreFile::empty()
.chain("", Path::new(""), b"!/foo/\nfoo/bar.*\n")
.unwrap();
assert!(ignore.matches("foo/bar.ext"));
}
}