check-ignore: add -n / --non-matching option

If `-n` or `--non-matching` are specified, non-matching pathnames will also be output, in which case all fields in each output record except for <pathname> will be empty. This can be useful when running check-ignore as a background process, so that files can be incrementally streamed to STDIN, and for each of these files, STDOUT will indicate whether that file matched a pattern or not. (Without this option, it would be impossible to tell whether the absence of output for a given file meant that it didn't match any pattern, or that the result simply hadn't been flushed to STDOUT yet.) Signed-off-by: Adam Spiers <git@adamspiers.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adam Spiers committed Apr 11, 2013 at 13:05 UTC ae3caf4c912429c8e9755f2e10d2f0a7f6279c36
3 files changed +134 -49
Documentation/git-check-ignore.txt
+15
@@ -39,6 +39,12 @@ OPTIONS
39 below). If `--stdin` is also given, input paths are separated
40 with a NUL character instead of a linefeed character.
41
42 +-n, --non-matching::
43 + Show given paths which don't match any pattern. This only
44 + makes sense when `--verbose` is enabled, otherwise it would
45 + not be possible to distinguish between paths which match a
46 + pattern and those which don't.
47 +
48 OUTPUT
49 ------
50
@@ -65,6 +71,15 @@ are also used instead of colons and hard tabs:
71
72 <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL>
73
74 +If `-n` or `--non-matching` are specified, non-matching pathnames will
75 +also be output, in which case all fields in each output record except
76 +for <pathname> will be empty. This can be useful when running
77 +non-interactively, so that files can be incrementally streamed to
78 +STDIN of a long-running check-ignore process, and for each of these
79 +files, STDOUT will indicate whether that file matched a pattern or
80 +not. (Without this option, it would be impossible to tell whether the
81 +absence of output for a given file meant that it didn't match any
82 +pattern, or that the output hadn't been generated yet.)
83
84 EXIT STATUS
85 -----------
builtin/check-ignore.c
+29 -17
@@ -5,7 +5,7 @@
5 #include "pathspec.h"
6 #include "parse-options.h"
7
8 -static int quiet, verbose, stdin_paths;
8 +static int quiet, verbose, stdin_paths, show_non_matching;
9 static const char * const check_ignore_usage[] = {
10 "git check-ignore [options] pathname...",
11 "git check-ignore [options] --stdin < <list-of-paths>",
@@ -22,21 +22,28 @@ static const struct option check_ignore_options[] = {
22 N_("read file names from stdin")),
23 OPT_BOOLEAN('z', NULL, &null_term_line,
24 N_("input paths are terminated by a null character")),
25 + OPT_BOOLEAN('n', "non-matching", &show_non_matching,
26 + N_("show non-matching input paths")),
27 OPT_END()
28 };
29
30 static void output_exclude(const char *path, struct exclude *exclude)
31 {
30 - char *bang = exclude->flags & EXC_FLAG_NEGATIVE ? "!" : "";
31 - char *slash = exclude->flags & EXC_FLAG_MUSTBEDIR ? "/" : "";
32 + char *bang = (exclude && exclude->flags & EXC_FLAG_NEGATIVE) ? "!" : "";
33 + char *slash = (exclude && exclude->flags & EXC_FLAG_MUSTBEDIR) ? "/" : "";
34 if (!null_term_line) {
35 if (!verbose) {
36 write_name_quoted(path, stdout, '\n');
37 } else {
36 - quote_c_style(exclude->el->src, NULL, stdout, 0);
37 - printf(":%d:%s%s%s\t",
38 - exclude->srcpos,
39 - bang, exclude->pattern, slash);
38 + if (exclude) {
39 + quote_c_style(exclude->el->src, NULL, stdout, 0);
40 + printf(":%d:%s%s%s\t",
41 + exclude->srcpos,
42 + bang, exclude->pattern, slash);
43 + }
44 + else {
45 + printf("::\t");
46 + }
47 quote_c_style(path, NULL, stdout, 0);
48 fputc('\n', stdout);
49 }
@@ -44,11 +51,14 @@ static void output_exclude(const char *path, struct exclude *exclude)
51 if (!verbose) {
52 printf("%s%c", path, '\0');
53 } else {
47 - printf("%s%c%d%c%s%s%s%c%s%c",
48 - exclude->el->src, '\0',
49 - exclude->srcpos, '\0',
50 - bang, exclude->pattern, slash, '\0',
51 - path, '\0');
54 + if (exclude)
55 + printf("%s%c%d%c%s%s%s%c%s%c",
56 + exclude->el->src, '\0',
57 + exclude->srcpos, '\0',
58 + bang, exclude->pattern, slash, '\0',
59 + path, '\0');
60 + else
61 + printf("%c%c%c%s%c", '\0', '\0', '\0', path, '\0');
62 }
63 }
64 }
@@ -89,15 +99,15 @@ static int check_ignore(const char *prefix, const char **pathspec)
99 ? strlen(prefix) : 0, path);
100 full_path = check_path_for_gitlink(full_path);
101 die_if_path_beyond_symlink(full_path, prefix);
102 + exclude = NULL;
103 if (!seen[i]) {
104 exclude = last_exclude_matching_path(&check, full_path,
105 -1, &dtype);
95 - if (exclude) {
96 - if (!quiet)
97 - output_exclude(path, exclude);
98 - num_ignored++;
99 - }
106 }
107 + if (!quiet && (exclude || show_non_matching))
108 + output_exclude(path, exclude);
109 + if (exclude)
110 + num_ignored++;
111 }
112 free(seen);
113 clear_directory(&dir);
@@ -161,6 +171,8 @@ int cmd_check_ignore(int argc, const char **argv, const char *prefix)
171 if (verbose)
172 die(_("cannot have both --quiet and --verbose"));
173 }
174 + if (show_non_matching && !verbose)
175 + die(_("--non-matching is only valid with --verbose"));
176
177 if (stdin_paths) {
178 num_ignored = check_ignore_stdin_paths(prefix);
t/t0008-ignores.sh
+90 -32
@@ -66,16 +66,23 @@ test_check_ignore () {
66
67 init_vars &&
68 rm -f "$HOME/stdout" "$HOME/stderr" "$HOME/cmd" &&
69 - echo git $global_args check-ignore $quiet_opt $verbose_opt $args \
69 + echo git $global_args check-ignore $quiet_opt $verbose_opt $non_matching_opt $args \
70 >"$HOME/cmd" &&
71 + echo "$expect_code" >"$HOME/expected-exit-code" &&
72 test_expect_code "$expect_code" \
72 - git $global_args check-ignore $quiet_opt $verbose_opt $args \
73 + git $global_args check-ignore $quiet_opt $verbose_opt $non_matching_opt $args \
74 >"$HOME/stdout" 2>"$HOME/stderr" &&
75 test_cmp "$HOME/expected-stdout" "$HOME/stdout" &&
76 stderr_empty_on_success "$expect_code"
77 }
78
78 -# Runs the same code with 3 different levels of output verbosity,
79 +# Runs the same code with 4 different levels of output verbosity:
80 +#
81 +# 1. with -q / --quiet
82 +# 2. with default verbosity
83 +# 3. with -v / --verbose
84 +# 4. with -v / --verbose, *and* -n / --non-matching
85 +#
86 # expecting success each time. Takes advantage of the fact that
87 # check-ignore --verbose output is the same as normal output except
88 # for the extra first column.
@@ -83,7 +90,9 @@ test_check_ignore () {
90 # Arguments:
91 # - (optional) prereqs for this test, e.g. 'SYMLINKS'
92 # - test name
86 -# - output to expect from -v / --verbose mode
93 +# - output to expect from the fourth verbosity mode (the output
94 +# from the other verbosity modes is automatically inferred
95 +# from this value)
96 # - code to run (should invoke test_check_ignore)
97 test_expect_success_multi () {
98 prereq=
@@ -92,8 +101,9 @@ test_expect_success_multi () {
101 prereq=$1
102 shift
103 fi
95 - testname="$1" expect_verbose="$2" code="$3"
104 + testname="$1" expect_all="$2" code="$3"
105
106 + expect_verbose=$( echo "$expect_all" | grep -v '^:: ' )
107 expect=$( echo "$expect_verbose" | sed -e 's/.* //' )
108
109 test_expect_success $prereq "$testname" '
@@ -101,23 +111,40 @@ test_expect_success_multi () {
111 eval "$code"
112 '
113
104 - for quiet_opt in '-q' '--quiet'
105 - do
106 - test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
114 + # --quiet is only valid when a single pattern is passed
115 + if test $( echo "$expect_all" | wc -l ) = 1
116 + then
117 + for quiet_opt in '-q' '--quiet'
118 + do
119 + test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
120 expect '' &&
121 $code
122 "
110 - done
111 - quiet_opt=
123 + done
124 + quiet_opt=
125 + fi
126
127 for verbose_opt in '-v' '--verbose'
128 do
115 - test_expect_success $prereq "$testname${verbose_opt:+ with $verbose_opt}" "
116 - expect '$expect_verbose' &&
117 - $code
118 - "
129 + for non_matching_opt in '' ' -n' ' --non-matching'
130 + do
131 + if test -n "$non_matching_opt"
132 + then
133 + my_expect="$expect_all"
134 + else
135 + my_expect="$expect_verbose"
136 + fi
137 +
138 + test_code="
139 + expect '$my_expect' &&
140 + $code
141 + "
142 + opts="$verbose_opt$non_matching_opt"
143 + test_expect_success $prereq "$testname${opts:+ with $opts}" "$test_code"
144 + done
145 done
146 verbose_opt=
147 + non_matching_opt=
148 }
149
150 test_expect_success 'setup' '
@@ -178,7 +205,7 @@ test_expect_success 'setup' '
205 #
206 # test invalid inputs
207
181 -test_expect_success_multi '. corner-case' '' '
208 +test_expect_success_multi '. corner-case' ':: .' '
209 test_check_ignore . 1
210 '
211
@@ -276,27 +303,39 @@ do
303 where="in subdir $subdir"
304 fi
305
279 - test_expect_success_multi "non-existent file $where not ignored" '' "
280 - test_check_ignore '${subdir}non-existent' 1
281 - "
306 + test_expect_success_multi "non-existent file $where not ignored" \
307 + ":: ${subdir}non-existent" \
308 + "test_check_ignore '${subdir}non-existent' 1"
309
310 test_expect_success_multi "non-existent file $where ignored" \
284 - ".gitignore:1:one ${subdir}one" "
285 - test_check_ignore '${subdir}one'
286 - "
311 + ".gitignore:1:one ${subdir}one" \
312 + "test_check_ignore '${subdir}one'"
313
288 - test_expect_success_multi "existing untracked file $where not ignored" '' "
289 - test_check_ignore '${subdir}not-ignored' 1
290 - "
314 + test_expect_success_multi "existing untracked file $where not ignored" \
315 + ":: ${subdir}not-ignored" \
316 + "test_check_ignore '${subdir}not-ignored' 1"
317
292 - test_expect_success_multi "existing tracked file $where not ignored" '' "
293 - test_check_ignore '${subdir}ignored-but-in-index' 1
294 - "
318 + test_expect_success_multi "existing tracked file $where not ignored" \
319 + ":: ${subdir}ignored-but-in-index" \
320 + "test_check_ignore '${subdir}ignored-but-in-index' 1"
321
322 test_expect_success_multi "existing untracked file $where ignored" \
297 - ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" "
298 - test_check_ignore '${subdir}ignored-and-untracked'
299 - "
323 + ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" \
324 + "test_check_ignore '${subdir}ignored-and-untracked'"
325 +
326 + test_expect_success_multi "mix of file types $where" \
327 +":: ${subdir}non-existent
328 +.gitignore:1:one ${subdir}one
329 +:: ${subdir}not-ignored
330 +:: ${subdir}ignored-but-in-index
331 +.gitignore:2:ignored-* ${subdir}ignored-and-untracked" \
332 + "test_check_ignore '
333 + ${subdir}non-existent
334 + ${subdir}one
335 + ${subdir}not-ignored
336 + ${subdir}ignored-but-in-index
337 + ${subdir}ignored-and-untracked'
338 + "
339 done
340
341 # Having established the above, from now on we mostly test against
@@ -391,7 +430,7 @@ test_expect_success 'cd to ignored sub-directory with -v' '
430 #
431 # test handling of symlinks
432
394 -test_expect_success_multi SYMLINKS 'symlink' '' '
433 +test_expect_success_multi SYMLINKS 'symlink' ':: a/symlink' '
434 test_check_ignore "a/symlink" 1
435 '
436
@@ -574,22 +613,33 @@ cat <<-\EOF >stdin
613 globaltwo
614 b/globaltwo
615 ../b/globaltwo
616 + c/not-ignored
617 EOF
578 -cat <<-EOF >expected-verbose
618 +# N.B. we deliberately end STDIN with a non-matching pattern in order
619 +# to test that the exit code indicates that one or more of the
620 +# provided paths is ignored - in other words, that it represents an
621 +# aggregation of all the results, not just the final result.
622 +
623 +cat <<-EOF >expected-all
624 .gitignore:1:one ../one
625 + :: ../not-ignored
626 .gitignore:1:one one
627 + :: not-ignored
628 a/b/.gitignore:8:!on* b/on
629 a/b/.gitignore:8:!on* b/one
630 a/b/.gitignore:8:!on* b/one one
631 a/b/.gitignore:8:!on* b/one two
632 a/b/.gitignore:8:!on* "b/one\"three"
633 a/b/.gitignore:9:!two b/two
634 + :: b/not-ignored
635 a/.gitignore:1:two* b/twooo
636 $global_excludes:2:!globaltwo ../globaltwo
637 $global_excludes:2:!globaltwo globaltwo
638 $global_excludes:2:!globaltwo b/globaltwo
639 $global_excludes:2:!globaltwo ../b/globaltwo
640 + :: c/not-ignored
641 EOF
642 +grep -v '^:: ' expected-all >expected-verbose
643 sed -e 's/.* //' expected-verbose >expected-default
644
645 sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \
@@ -615,6 +665,14 @@ test_expect_success '--stdin from subdirectory with -v' '
665 )
666 '
667
668 +test_expect_success '--stdin from subdirectory with -v -n' '
669 + expect_from_stdin <expected-all &&
670 + (
671 + cd a &&
672 + test_check_ignore "--stdin -v -n" <../stdin
673 + )
674 +'
675 +
676 for opts in '--stdin -z' '-z --stdin'
677 do
678 test_expect_success "$opts from subdirectory" '