diff --git a/dvc/command/check_ignore.py b/dvc/command/check_ignore.py index ef3f1250bb..e28f4f19b4 100644 --- a/dvc/command/check_ignore.py +++ b/dvc/command/check_ignore.py @@ -4,6 +4,7 @@ from dvc.command import completion from dvc.command.base import CmdBase, append_doc_link from dvc.exceptions import DvcException +from dvc.prompt import ask logger = logging.getLogger(__name__) @@ -14,27 +15,71 @@ def __init__(self, args): self.ignore_filter = self.repo.tree.dvcignore def _show_results(self, result): - if result.match or self.args.non_matching: - if self.args.details: - logger.info("{}\t{}".format(result.patterns[-1], result.file)) - else: - logger.info(result.file) + if not result.match and not self.args.non_matching: + return - def run(self): - if self.args.non_matching and not self.args.details: - raise DvcException("--non-matching is only valid with --details") + if self.args.details: + patterns = result.patterns + if not self.args.all: + patterns = patterns[-1:] - if self.args.quiet and self.args.details: - raise DvcException("cannot both --details and --quiet") + for pattern in patterns: + logger.info("{}\t{}".format(pattern, result.file)) + else: + logger.info(result.file) + + def _check_one_file(self, target): + result = self.ignore_filter.check_ignore(target) + self._show_results(result) + if result.match: + return 0 + return 1 + def _interactive_mode(self): + ret = 1 + while True: + target = ask("") + if target == "": + logger.info( + "Empty string is not a valid pathspec. Please use . " + "instead if you meant to match all paths." + ) + break + if not self._check_one_file(target): + ret = 0 + return ret + + def _normal_mode(self): ret = 1 for target in self.args.targets: - result = self.ignore_filter.check_ignore(target) - self._show_results(result) - if result.match: + if not self._check_one_file(target): ret = 0 return ret + def _check_args(self): + if not self.args.stdin and not self.args.targets: + raise DvcException("`targets` or `--stdin` needed") + + if self.args.stdin and self.args.targets: + raise DvcException("cannot have both `targets` and `--stdin`") + + if self.args.non_matching and not self.args.details: + raise DvcException( + "`--non-matching` is only valid with `--details`" + ) + + if self.args.all and not self.args.details: + raise DvcException("`--all` is only valid with `--details`") + + if self.args.quiet and self.args.details: + raise DvcException("cannot use both `--details` and `--quiet`") + + def run(self): + self._check_args() + if self.args.stdin: + return self._interactive_mode() + return self._normal_mode() + def add_parser(subparsers, parent_parser): ADD_HELP = "Debug DVC ignore/exclude files" @@ -61,9 +106,24 @@ def add_parser(subparsers, parent_parser): help="Show the target paths which don’t match any pattern. " "Only usable when `--details` is also employed", ) + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read pathnames from the standard input, one per line, " + "instead of from the command-line.", + ) + parser.add_argument( + "-a", + "--all", + action="store_true", + default=False, + help="Show all of the patterns match the target paths. " + "Only usable when `--details` is also employed", + ) parser.add_argument( "targets", - nargs="+", + nargs="*", help="Exact or wildcard paths of files or directories to check " "ignore patterns.", ).complete = completion.FILE diff --git a/dvc/prompt.py b/dvc/prompt.py index 285f6f62dc..cb12a73b93 100644 --- a/dvc/prompt.py +++ b/dvc/prompt.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -def _ask(prompt, limited_to=None): +def ask(prompt, limited_to=None): if not sys.stdout.isatty(): return None @@ -39,7 +39,7 @@ def confirm(statement): bool: whether or not specified statement was confirmed. """ prompt = f"{statement} [y/n]" - answer = _ask(prompt, limited_to=["yes", "no", "y", "n"]) + answer = ask(prompt, limited_to=["yes", "no", "y", "n"]) return answer and answer.startswith("y") diff --git a/tests/func/test_check_ignore.py b/tests/func/test_check_ignore.py index b53f72f531..eba86bc98f 100644 --- a/tests/func/test_check_ignore.py +++ b/tests/func/test_check_ignore.py @@ -7,13 +7,13 @@ @pytest.mark.parametrize( - "file,ret,output", [("ignored", 0, "ignored\n"), ("not_ignored", 1, "")] + "file,ret,output", [("ignored", 0, True), ("not_ignored", 1, False)] ) def test_check_ignore(tmp_dir, dvc, file, ret, output, caplog): tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "ignored") assert main(["check-ignore", file]) == ret - assert output in caplog.text + assert (file in caplog.text) is output @pytest.mark.parametrize( @@ -39,26 +39,29 @@ def test_check_ignore_details(tmp_dir, dvc, file, ret, output, caplog): assert output in caplog.text -@pytest.mark.parametrize( - "non_matching,output", [(["-n"], "::\tfile\n"), ([], "")] -) -def test_check_ignore_non_matching(tmp_dir, dvc, non_matching, output, caplog): - tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "other") - - assert main(["check-ignore", "-d"] + non_matching + ["file"]) == 1 - assert output in caplog.text - - -def test_check_ignore_non_matching_without_details(tmp_dir, dvc): +@pytest.mark.parametrize("non_matching", [True, False]) +def test_check_ignore_non_matching(tmp_dir, dvc, non_matching, caplog): tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "other") + if non_matching: + assert main(["check-ignore", "-d", "-n", "file"]) == 1 + else: + assert main(["check-ignore", "-d", "file"]) == 1 - assert main(["check-ignore", "-n", "file"]) == 255 - + assert ("::\tfile\n" in caplog.text) is non_matching -def test_check_ignore_details_with_quiet(tmp_dir, dvc): - tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "other") - assert main(["check-ignore", "-d", "-q", "file"]) == 255 +@pytest.mark.parametrize( + "args", + [ + ["-n", "file"], + ["-a", "file"], + ["-q", "-d", "file"], + ["--stdin", "file"], + [], + ], +) +def test_check_ignore_error_args_cases(tmp_dir, dvc, args): + assert main(["check-ignore"] + args) == 255 @pytest.mark.parametrize("path,ret", [({"dir": {}}, 0), ({"dir": "files"}, 1)]) @@ -107,3 +110,26 @@ def test_check_sub_dir_ignore_file(tmp_dir, dvc, caplog): with sub_dir.chdir(): assert main(["check-ignore", "-d", "foo"]) == 0 assert ".dvcignore:2:foo\tfoo" in caplog.text + + +def test_check_ignore_details_all(tmp_dir, dvc, caplog): + tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "f*\n!foo") + + assert main(["check-ignore", "-d", "-a", "foo"]) == 0 + assert "{}:1:f*\tfoo\n".format(DvcIgnore.DVCIGNORE_FILE) in caplog.text + assert "{}:2:!foo\tfoo\n".format(DvcIgnore.DVCIGNORE_FILE) in caplog.text + + +@pytest.mark.parametrize( + "file,ret,output", [("ignored", 0, True), ("not_ignored", 1, False)] +) +def test_check_ignore_stdin_mode( + tmp_dir, dvc, file, ret, output, caplog, mocker +): + tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "ignored") + mocker.patch("builtins.input", side_effect=[file, ""]) + stdout_mock = mocker.patch("sys.stdout") + stdout_mock.isatty.return_value = True + + assert main(["check-ignore", "--stdin"]) == ret + assert (file in caplog.text) is output