From 2a2674c83f4e991a579b0a192d0d4e744f65712c Mon Sep 17 00:00:00 2001 From: karajan1001 Date: Sat, 1 Aug 2020 17:55:08 +0800 Subject: [PATCH] Two new features -all --stdin for dvc check-ignore Fix #3736 1. Introduce new arguments `--all` for `dvc check-ignore` 2. Add tests for `dvc check-ignore --all` 3. Introduce an interactive mode for `dvc check-ignore` with arguments `--stdin` on --- dvc/command/check_ignore.py | 75 ++++++++++++++++++++++++++++----- dvc/prompt.py | 9 ++++ tests/func/test_check_ignore.py | 21 ++++----- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/dvc/command/check_ignore.py b/dvc/command/check_ignore.py index ef3f1250bb..c611bdbd34 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 path_input logger = logging.getLogger(__name__) @@ -14,25 +15,62 @@ 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 + + if self.args.details: + patterns = result.patterns + if not self.args.all: + patterns = patterns[-1:] + + 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 = path_input() + 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: + if not self._check_one_file(target): + ret = 0 + return ret 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.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 both --details and --quiet") - ret = 1 - for target in self.args.targets: - result = self.ignore_filter.check_ignore(target) - self._show_results(result) - if result.match: - ret = 0 + if self.args.stdin: + ret = self._interactive_mode() + else: + ret = self._normal_mode() + return ret @@ -61,6 +99,21 @@ 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="+", diff --git a/dvc/prompt.py b/dvc/prompt.py index 285f6f62dc..8af245473d 100644 --- a/dvc/prompt.py +++ b/dvc/prompt.py @@ -54,3 +54,12 @@ def password(statement): """ logger.info(f"{statement}: ") return getpass("") + + +def path_input(): + """Ask the user for a path. + + Returns: + str: path entered by the user. + """ + return _ask("") diff --git a/tests/func/test_check_ignore.py b/tests/func/test_check_ignore.py index b53f72f531..214cab629a 100644 --- a/tests/func/test_check_ignore.py +++ b/tests/func/test_check_ignore.py @@ -49,16 +49,9 @@ def test_check_ignore_non_matching(tmp_dir, dvc, non_matching, output, caplog): assert output in caplog.text -def test_check_ignore_non_matching_without_details(tmp_dir, dvc): - tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "other") - - assert main(["check-ignore", "-n", "file"]) == 255 - - -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"], ["-a"], ["-q", "-d"]]) +def test_check_ignore_error_args_cases(tmp_dir, dvc, args): + assert main(["check-ignore"] + args + ["file"]) == 255 @pytest.mark.parametrize("path,ret", [({"dir": {}}, 0), ({"dir": "files"}, 1)]) @@ -107,3 +100,11 @@ 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