import os import pytest from dvc.ignore import DvcIgnorePatterns @pytest.mark.parametrize( "file_to_ignore_relpath, patterns, expected_match", [ # all rules from https://git-scm.com/docs/gitignore ("to_ignore", ["to_ignore"], True), ("dont_ignore.txt", ["dont_ignore"], False), # A blank line matches no files, so it can serve as a separator for # readability. ("to_ignore", ["", "to_ignore"], True), # A line starting with # serves as a comment. # Put a backslash ("\") in front of the first hash for patterns # that begin with a hash. ("#to_ignore", ["\\#to_ignore"], True), ("#to_ignore", ["#to_ignore"], False), # Trailing spaces are ignored unless they are quoted with # backslash ("\"). (" to_ignore", [" to_ignore"], False), (" to_ignore", ["\\ to_ignore"], True), # An optional prefix "!" which negates the pattern; any matching file # excluded by a previous pattern will become included again. ("to_ignore.txt", ["to_ignore*"], True), ("to_ignore.txt", ["to_ignore*", "!to_ignore.txt"], False), ("to_ignore.txt", ["!to_ignore.txt", "to_ignore*"], True), # It is not possible to re-include a file if a parent directory of # that file is excluded. # Git doesn't list excluded directories for performance reasons, # so any patterns on contained files have no effect, # no matter where they are defined. # see (`tests/func/test_ignore.py::test_ignore_parent_path`) # Put a backslash ("\") in front of the first "!" # for patterns that begin with a literal "!", # for example, "\!important!.txt". ("!to_ignore.txt", ["\\!to_ignore.txt"], True), # The slash / is used as the directory separator. # Separators may occur at the beginning, middle or end of the # .gitignore search pattern. # If there is a separator at the beginning or middle (or both) # of the pattern, then the pattern is relative to the directory # level of the particular .gitignore file itself. # Otherwise the pattern may also match at any level below # the .gitignore level. ("file", ["/file"], True), (os.path.join("data", "file"), ["/file"], False), (os.path.join("data", "file"), ["data/file"], True), (os.path.join("other", "data", "file"), ["data/file"], False), ( os.path.join( os.path.sep, "full", "path", "to", "ignore", "file", "to_ignore", ), ["to_ignore"], True, ), # If there is a separator at the end of the pattern then the pattern # will only match directories, # otherwise the pattern can match both files and directories. # For example, a pattern doc/frotz/ matches doc/frotz directory, # but not a/doc/frotz directory; # see (`tests/func/test_ignore.py::test_ignore_sub_directory`) # however frotz/ matches frotz and a/frotz that is a directory # (all paths are relative from the .gitignore file). # see (`tests/func/test_ignore.py::test_ignore_directory`) # An asterisk "*" matches anything except a slash. ("to_ignore.txt", ["/*.txt"], True), (os.path.join("path", "to_ignore.txt"), ["/*.txt"], False), (os.path.join("data", "file.txt"), ["data/*"], True), (os.path.join("data", "subdir", "file.txt"), ["data/*"], True), (os.path.join("data", "file.txt"), ["data/"], True), (os.path.join("data", "subdir", "file.txt"), ["data/"], True), (os.path.join("data", "subdir", "file.txt"), ["subdir/"], True), (os.path.join("data", "subdir", "file.txt"), ["/subdir/"], False), (os.path.join("data", "path"), ["path/"], False), (os.path.join(".git", "file.txt"), [".git/"], True), (os.path.join("data", ".dvc", "file.txt"), [".dvc/"], True), # wait for Git # (os.path.join("data", "sub", "file.txt"), ["data/*"], True), ( os.path.join("rel", "path", "path2", "to_ignore"), ["rel/*/to_ignore"], False, ), ("file.txt", ["file.*"], True), # The character "?" matches any one character except "/". ("file.txt", ["fi?e.t?t"], True), ("fi/e.txt", ["fi?e.t?t"], False), # The range notation, e.g. [a-zA-Z], can be used # to match one of the characters in a range. See fnmatch(3) and # the FNM_PATHNAME flag for a more detailed description. ("file.txt", ["[a-zA-Z]ile.txt"], True), ("2ile.txt", ["[a-zA-Z]ile.txt"], False), # Two consecutive asterisks ("**") in patterns matched against # full pathname may have special meaning: # A leading "**" followed by a slash means match in all directories. # For example, "**/foo" matches file or directory "foo" anywhere, the # same as pattern "foo". # "**/foo/bar" matches file or directory "bar" anywhere that is # directly under directory "foo". (os.path.join("rel", "p", "p2", "to_ignore"), ["**/to_ignore"], True), ( os.path.join("rel", "p", "p2", "to_ignore"), ["**/p2/to_ignore"], True, ), ( os.path.join("rel", "path", "path2", "dont_ignore"), ["**/to_ignore"], False, ), # A trailing "/**" matches everything inside. # For example, "abc/**" matches all files inside directory "abc", # relative to the location of the .gitignore file, with infinite depth. (os.path.join("rel", "p", "p2", "to_ignore"), ["rel/**"], True), (os.path.join("rel", "p", "p2", "to_ignore"), ["p/**"], False), ( os.path.join("rel", "path", "path2", "dont_ignore"), ["rel/**"], True, ), # A slash followed by two consecutive asterisks then a slash matches # zero or more directories. # For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on. (os.path.join("rel", "p", "to_ignore"), ["rel/**/to_ignore"], True), ( os.path.join("rel", "p", "p2", "to_ignore"), ["rel/**/to_ignore"], True, ), ( os.path.join("rel", "path", "path2", "dont_ignore"), ["rel/**/to_ignore"], False, ), ( os.path.join("rel", "path", "path2", "dont_ignore"), ["path/**/dont_ignore"], False, ), # Other consecutive asterisks are considered regular asterisks # and will match according to the previous rules. ("to_ignore.txt", ["/***.txt"], True), (os.path.join("path", "to_ignore.txt"), ["/****.txt"], False), (os.path.join("path", "to_ignore.txt"), ["****.txt"], True), (os.path.join("data", "file.txt"), ["data/***"], True), # bug from PathSpec # (os.path.join("data", "p", "file.txt"), ["data/***"], False), (os.path.join("data", "p", "file.txt"), ["***/file.txt"], False), ( os.path.join("rel", "path", "path2", "to_ignore"), ["rel/***/to_ignore"], False, ), ], ) def test_match_ignore_from_file( file_to_ignore_relpath, patterns, expected_match, mocker ): from dvc.fs import localfs dvcignore_path = os.path.join( os.path.sep, "full", "path", "to", "ignore", "file", ".dvcignore" ) dvcignore_dirname = os.path.dirname(dvcignore_path) mocker.patch.object( localfs, "open", mocker.mock_open(read_data="\n".join(patterns)) ) ignore_file = DvcIgnorePatterns.from_file(dvcignore_path, localfs, "mocked") assert ( ignore_file.matches(dvcignore_dirname, file_to_ignore_relpath) == expected_match ) @pytest.mark.parametrize("sub_dir", ["", "dir"]) @pytest.mark.parametrize("omit_dir", [".git", ".hg", ".dvc"]) def test_should_ignore_dir(omit_dir, sub_dir): root = os.path.join(os.path.sep, "walk", "dir", "root") ignore = DvcIgnorePatterns([".git/", ".hg/", ".dvc/"], root, os.sep) dirs = [omit_dir, "dir1", "dir2"] files = [omit_dir, "file1", "file2"] if sub_dir: current = os.path.join(root, sub_dir) else: current = root new_dirs, new_files = ignore(current, dirs, files) assert set(new_dirs) == {"dir1", "dir2"} assert set(new_files) == {"file1", "file2", omit_dir}