Merge branch 'as/check-ignore'
authorJunio C Hamano <gitster@pobox.com>
Wed, 29 May 2013 21:23:39 +0000 (14:23 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 29 May 2013 21:23:40 +0000 (14:23 -0700)
Enhance "check-ignore" (1.8.2 update) to work more like "check-attr"
over bidi-pipes.

* as/check-ignore:
t0008: use named pipe (FIFO) to test check-ignore streaming
Documentation: add caveats about I/O buffering for check-{attr,ignore}
check-ignore: allow incremental streaming of queries via --stdin
check-ignore: move setup into cmd_check_ignore()
check-ignore: add -n / --non-matching option
t0008: remove duplicated test fixture data

Documentation/git-check-attr.txt
Documentation/git-check-ignore.txt
Documentation/git.txt
builtin/check-ignore.c
t/t0008-ignores.sh
index 5abdbaa51cf58e216bbc63d28039a5abfba01669..a7be80d48bf6fd6928186116f00f3b48cf564041 100644 (file)
@@ -56,6 +56,11 @@ being queried and <info> can be either:
 'set';;                when the attribute is defined as true.
 <value>;;      when a value has been assigned to the attribute.
 
+Buffering happens as documented under the `GIT_FLUSH` option in
+linkgit:git[1].  The caller is responsible for avoiding deadlocks
+caused by overfilling an input buffer or reading from an empty output
+buffer.
+
 EXAMPLES
 --------
 
index 854e4d0c425a9396bdb71dc84093f43156b04cd5..8e1f7ab7ea93af4b7c496efc2251469ed553a55f 100644 (file)
@@ -39,6 +39,12 @@ OPTIONS
        below).  If `--stdin` is also given, input paths are separated
        with a NUL character instead of a linefeed character.
 
+-n, --non-matching::
+       Show given paths which don't match any pattern.  This only
+       makes sense when `--verbose` is enabled, otherwise it would
+       not be possible to distinguish between paths which match a
+       pattern and those which don't.
+
 OUTPUT
 ------
 
@@ -65,6 +71,20 @@ are also used instead of colons and hard tabs:
 
 <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL>
 
+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
+non-interactively, so that files can be incrementally streamed to
+STDIN of a long-running check-ignore process, 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 output hadn't been generated yet.)
+
+Buffering happens as documented under the `GIT_FLUSH` option in
+linkgit:git[1].  The caller is responsible for avoiding deadlocks
+caused by overfilling an input buffer or reading from an empty output
+buffer.
 
 EXIT STATUS
 -----------
index 9e302b0a60552c4297cb94018dad49169505bd35..65de534e43ab4e83b2ba1c8cfc4df7c52ee2cb84 100644 (file)
@@ -811,8 +811,9 @@ for further details.
 'GIT_FLUSH'::
        If this environment variable is set to "1", then commands such
        as 'git blame' (in incremental mode), 'git rev-list', 'git log',
-       and 'git whatchanged' will force a flush of the output stream
-       after each commit-oriented record have been flushed.   If this
+       'git check-attr', 'git check-ignore', and 'git whatchanged' will
+       force a flush of the output stream after each record have been
+       flushed. If this
        variable is set to "0", the output of these commands will be done
        using completely buffered I/O.   If this environment variable is
        not set, Git will choose buffered or record-oriented flushing
index 854a88a0568e2f0226d26a2d9ebc99d24765053b..4a8fc707c747596e31dcc6f57abf5f965cdf612f 100644 (file)
@@ -5,7 +5,7 @@
 #include "pathspec.h"
 #include "parse-options.h"
 
-static int quiet, verbose, stdin_paths;
+static int quiet, verbose, stdin_paths, show_non_matching;
 static const char * const check_ignore_usage[] = {
 "git check-ignore [options] pathname...",
 "git check-ignore [options] --stdin < <list-of-paths>",
@@ -22,21 +22,28 @@ static const struct option check_ignore_options[] = {
                    N_("read file names from stdin")),
        OPT_BOOLEAN('z', NULL, &null_term_line,
                    N_("input paths are terminated by a null character")),
+       OPT_BOOLEAN('n', "non-matching", &show_non_matching,
+                   N_("show non-matching input paths")),
        OPT_END()
 };
 
 static void output_exclude(const char *path, struct exclude *exclude)
 {
-       char *bang  = exclude->flags & EXC_FLAG_NEGATIVE  ? "!" : "";
-       char *slash = exclude->flags & EXC_FLAG_MUSTBEDIR ? "/" : "";
+       char *bang  = (exclude && exclude->flags & EXC_FLAG_NEGATIVE)  ? "!" : "";
+       char *slash = (exclude && exclude->flags & EXC_FLAG_MUSTBEDIR) ? "/" : "";
        if (!null_term_line) {
                if (!verbose) {
                        write_name_quoted(path, stdout, '\n');
                } else {
-                       quote_c_style(exclude->el->src, NULL, stdout, 0);
-                       printf(":%d:%s%s%s\t",
-                              exclude->srcpos,
-                              bang, exclude->pattern, slash);
+                       if (exclude) {
+                               quote_c_style(exclude->el->src, NULL, stdout, 0);
+                               printf(":%d:%s%s%s\t",
+                                      exclude->srcpos,
+                                      bang, exclude->pattern, slash);
+                       }
+                       else {
+                               printf("::\t");
+                       }
                        quote_c_style(path, NULL, stdout, 0);
                        fputc('\n', stdout);
                }
@@ -44,30 +51,26 @@ static void output_exclude(const char *path, struct exclude *exclude)
                if (!verbose) {
                        printf("%s%c", path, '\0');
                } else {
-                       printf("%s%c%d%c%s%s%s%c%s%c",
-                              exclude->el->src, '\0',
-                              exclude->srcpos, '\0',
-                              bang, exclude->pattern, slash, '\0',
-                              path, '\0');
+                       if (exclude)
+                               printf("%s%c%d%c%s%s%s%c%s%c",
+                                      exclude->el->src, '\0',
+                                      exclude->srcpos, '\0',
+                                      bang, exclude->pattern, slash, '\0',
+                                      path, '\0');
+                       else
+                               printf("%c%c%c%s%c", '\0', '\0', '\0', path, '\0');
                }
        }
 }
 
-static int check_ignore(const char *prefix, const char **pathspec)
+static int check_ignore(struct dir_struct *dir,
+                       const char *prefix, const char **pathspec)
 {
-       struct dir_struct dir;
        const char *path, *full_path;
        char *seen;
        int num_ignored = 0, dtype = DT_UNKNOWN, i;
        struct exclude *exclude;
 
-       /* read_cache() is only necessary so we can watch out for submodules. */
-       if (read_cache() < 0)
-               die(_("index file corrupt"));
-
-       memset(&dir, 0, sizeof(dir));
-       setup_standard_excludes(&dir);
-
        if (!pathspec || !*pathspec) {
                if (!quiet)
                        fprintf(stderr, "no pathspec given.\n");
@@ -86,28 +89,26 @@ static int check_ignore(const char *prefix, const char **pathspec)
                                        ? strlen(prefix) : 0, path);
                full_path = check_path_for_gitlink(full_path);
                die_if_path_beyond_symlink(full_path, prefix);
+               exclude = NULL;
                if (!seen[i]) {
-                       exclude = last_exclude_matching(&dir, full_path, &dtype);
-                       if (exclude) {
-                               if (!quiet)
-                                       output_exclude(path, exclude);
-                               num_ignored++;
-                       }
+                       exclude = last_exclude_matching(dir, full_path, &dtype);
                }
+               if (!quiet && (exclude || show_non_matching))
+                       output_exclude(path, exclude);
+               if (exclude)
+                       num_ignored++;
        }
        free(seen);
-       clear_directory(&dir);
 
        return num_ignored;
 }
 
-static int check_ignore_stdin_paths(const char *prefix)
+static int check_ignore_stdin_paths(struct dir_struct *dir, const char *prefix)
 {
        struct strbuf buf, nbuf;
-       char **pathspec = NULL;
-       size_t nr = 0, alloc = 0;
+       char *pathspec[2] = { NULL, NULL };
        int line_termination = null_term_line ? 0 : '\n';
-       int num_ignored;
+       int num_ignored = 0;
 
        strbuf_init(&buf, 0);
        strbuf_init(&nbuf, 0);
@@ -118,23 +119,19 @@ static int check_ignore_stdin_paths(const char *prefix)
                                die("line is badly quoted");
                        strbuf_swap(&buf, &nbuf);
                }
-               ALLOC_GROW(pathspec, nr + 1, alloc);
-               pathspec[nr] = xcalloc(strlen(buf.buf) + 1, sizeof(*buf.buf));
-               strcpy(pathspec[nr++], buf.buf);
+               pathspec[0] = buf.buf;
+               num_ignored += check_ignore(dir, prefix, (const char **)pathspec);
+               maybe_flush_or_die(stdout, "check-ignore to stdout");
        }
-       ALLOC_GROW(pathspec, nr + 1, alloc);
-       pathspec[nr] = NULL;
-       num_ignored = check_ignore(prefix, (const char **)pathspec);
-       maybe_flush_or_die(stdout, "attribute to stdout");
        strbuf_release(&buf);
        strbuf_release(&nbuf);
-       free(pathspec);
        return num_ignored;
 }
 
 int cmd_check_ignore(int argc, const char **argv, const char *prefix)
 {
        int num_ignored;
+       struct dir_struct dir;
 
        git_config(git_default_config, NULL);
 
@@ -156,13 +153,24 @@ int cmd_check_ignore(int argc, const char **argv, const char *prefix)
                if (verbose)
                        die(_("cannot have both --quiet and --verbose"));
        }
+       if (show_non_matching && !verbose)
+               die(_("--non-matching is only valid with --verbose"));
+
+       /* read_cache() is only necessary so we can watch out for submodules. */
+       if (read_cache() < 0)
+               die(_("index file corrupt"));
+
+       memset(&dir, 0, sizeof(dir));
+       setup_standard_excludes(&dir);
 
        if (stdin_paths) {
-               num_ignored = check_ignore_stdin_paths(prefix);
+               num_ignored = check_ignore_stdin_paths(&dir, prefix);
        } else {
-               num_ignored = check_ignore(prefix, argv);
+               num_ignored = check_ignore(&dir, prefix, argv);
                maybe_flush_or_die(stdout, "ignore to stdout");
        }
 
+       clear_directory(&dir);
+
        return !num_ignored;
 }
index 9c1bde1fd6e6424f7af732556656a9daf9be350b..a56db804cbe502b664864cbf2d1550d9ec815ebc 100755 (executable)
@@ -66,16 +66,23 @@ test_check_ignore () {
 
        init_vars &&
        rm -f "$HOME/stdout" "$HOME/stderr" "$HOME/cmd" &&
-       echo git $global_args check-ignore $quiet_opt $verbose_opt $args \
+       echo git $global_args check-ignore $quiet_opt $verbose_opt $non_matching_opt $args \
                >"$HOME/cmd" &&
+       echo "$expect_code" >"$HOME/expected-exit-code" &&
        test_expect_code "$expect_code" \
-               git $global_args check-ignore $quiet_opt $verbose_opt $args \
+               git $global_args check-ignore $quiet_opt $verbose_opt $non_matching_opt $args \
                >"$HOME/stdout" 2>"$HOME/stderr" &&
        test_cmp "$HOME/expected-stdout" "$HOME/stdout" &&
        stderr_empty_on_success "$expect_code"
 }
 
-# Runs the same code with 3 different levels of output verbosity,
+# Runs the same code with 4 different levels of output verbosity:
+#
+#   1. with -q / --quiet
+#   2. with default verbosity
+#   3. with -v / --verbose
+#   4. with -v / --verbose, *and* -n / --non-matching
+#
 # expecting success each time.  Takes advantage of the fact that
 # check-ignore --verbose output is the same as normal output except
 # for the extra first column.
@@ -83,7 +90,9 @@ test_check_ignore () {
 # Arguments:
 #   - (optional) prereqs for this test, e.g. 'SYMLINKS'
 #   - test name
-#   - output to expect from -v / --verbose mode
+#   - output to expect from the fourth verbosity mode (the output
+#     from the other verbosity modes is automatically inferred
+#     from this value)
 #   - code to run (should invoke test_check_ignore)
 test_expect_success_multi () {
        prereq=
@@ -92,8 +101,9 @@ test_expect_success_multi () {
                prereq=$1
                shift
        fi
-       testname="$1" expect_verbose="$2" code="$3"
+       testname="$1" expect_all="$2" code="$3"
 
+       expect_verbose=$( echo "$expect_all" | grep -v '^::     ' )
        expect=$( echo "$expect_verbose" | sed -e 's/.* //' )
 
        test_expect_success $prereq "$testname" '
@@ -101,23 +111,40 @@ test_expect_success_multi () {
                eval "$code"
        '
 
-       for quiet_opt in '-q' '--quiet'
-       do
-               test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
+       # --quiet is only valid when a single pattern is passed
+       if test $( echo "$expect_all" | wc -l ) = 1
+       then
+               for quiet_opt in '-q' '--quiet'
+               do
+                       test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
                        expect '' &&
                        $code
                "
-       done
-       quiet_opt=
+               done
+               quiet_opt=
+       fi
 
        for verbose_opt in '-v' '--verbose'
        do
-               test_expect_success $prereq "$testname${verbose_opt:+ with $verbose_opt}" "
-                       expect '$expect_verbose' &&
-                       $code
-               "
+               for non_matching_opt in '' ' -n' ' --non-matching'
+               do
+                       if test -n "$non_matching_opt"
+                       then
+                               my_expect="$expect_all"
+                       else
+                               my_expect="$expect_verbose"
+                       fi
+
+                       test_code="
+                               expect '$my_expect' &&
+                               $code
+                       "
+                       opts="$verbose_opt$non_matching_opt"
+                       test_expect_success $prereq "$testname${opts:+ with $opts}" "$test_code"
+               done
        done
        verbose_opt=
+       non_matching_opt=
 }
 
 test_expect_success 'setup' '
@@ -178,7 +205,7 @@ test_expect_success 'setup' '
 #
 # test invalid inputs
 
-test_expect_success_multi '. corner-case' '' '
+test_expect_success_multi '. corner-case' '::  .' '
        test_check_ignore . 1
 '
 
@@ -189,11 +216,7 @@ test_expect_success_multi 'empty command line' '' '
 
 test_expect_success_multi '--stdin with empty STDIN' '' '
        test_check_ignore "--stdin" 1 </dev/null &&
-       if test -n "$quiet_opt"; then
-               test_stderr ""
-       else
-               test_stderr "no pathspec given."
-       fi
+       test_stderr ""
 '
 
 test_expect_success '-q with multiple args' '
@@ -276,27 +299,39 @@ do
                where="in subdir $subdir"
        fi
 
-       test_expect_success_multi "non-existent file $where not ignored" '' "
-               test_check_ignore '${subdir}non-existent' 1
-       "
+       test_expect_success_multi "non-existent file $where not ignored" \
+               "::     ${subdir}non-existent" \
+               "test_check_ignore '${subdir}non-existent' 1"
 
        test_expect_success_multi "non-existent file $where ignored" \
-               ".gitignore:1:one       ${subdir}one" "
-               test_check_ignore '${subdir}one'
-       "
+               ".gitignore:1:one       ${subdir}one" \
+               "test_check_ignore '${subdir}one'"
 
-       test_expect_success_multi "existing untracked file $where not ignored" '' "
-               test_check_ignore '${subdir}not-ignored' 1
-       "
+       test_expect_success_multi "existing untracked file $where not ignored" \
+               "::     ${subdir}not-ignored" \
+               "test_check_ignore '${subdir}not-ignored' 1"
 
-       test_expect_success_multi "existing tracked file $where not ignored" '' "
-               test_check_ignore '${subdir}ignored-but-in-index' 1
-       "
+       test_expect_success_multi "existing tracked file $where not ignored" \
+               "::     ${subdir}ignored-but-in-index" \
+               "test_check_ignore '${subdir}ignored-but-in-index' 1"
 
        test_expect_success_multi "existing untracked file $where ignored" \
-               ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" "
-               test_check_ignore '${subdir}ignored-and-untracked'
-       "
+               ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" \
+               "test_check_ignore '${subdir}ignored-and-untracked'"
+
+       test_expect_success_multi "mix of file types $where" \
+"::    ${subdir}non-existent
+.gitignore:1:one       ${subdir}one
+::     ${subdir}not-ignored
+::     ${subdir}ignored-but-in-index
+.gitignore:2:ignored-* ${subdir}ignored-and-untracked" \
+               "test_check_ignore '
+                       ${subdir}non-existent
+                       ${subdir}one
+                       ${subdir}not-ignored
+                       ${subdir}ignored-but-in-index
+                       ${subdir}ignored-and-untracked'
+               "
 done
 
 # Having established the above, from now on we mostly test against
@@ -391,7 +426,7 @@ test_expect_success 'cd to ignored sub-directory with -v' '
 #
 # test handling of symlinks
 
-test_expect_success_multi SYMLINKS 'symlink' '' '
+test_expect_success_multi SYMLINKS 'symlink' '::       a/symlink' '
        test_check_ignore "a/symlink" 1
 '
 
@@ -574,37 +609,34 @@ cat <<-\EOF >stdin
        globaltwo
        b/globaltwo
        ../b/globaltwo
+       c/not-ignored
 EOF
-cat <<-\EOF >expected-default
-       ../one
-       one
-       b/on
-       b/one
-       b/one one
-       b/one two
-       "b/one\"three"
-       b/two
-       b/twooo
-       ../globaltwo
-       globaltwo
-       b/globaltwo
-       ../b/globaltwo
-EOF
-cat <<-EOF >expected-verbose
+# N.B. we deliberately end STDIN with a non-matching pattern in order
+# to test that the exit code indicates that one or more of the
+# provided paths is ignored - in other words, that it represents an
+# aggregation of all the results, not just the final result.
+
+cat <<-EOF >expected-all
        .gitignore:1:one        ../one
+       ::      ../not-ignored
        .gitignore:1:one        one
+       ::      not-ignored
        a/b/.gitignore:8:!on*   b/on
        a/b/.gitignore:8:!on*   b/one
        a/b/.gitignore:8:!on*   b/one one
        a/b/.gitignore:8:!on*   b/one two
        a/b/.gitignore:8:!on*   "b/one\"three"
        a/b/.gitignore:9:!two   b/two
+       ::      b/not-ignored
        a/.gitignore:1:two*     b/twooo
        $global_excludes:2:!globaltwo   ../globaltwo
        $global_excludes:2:!globaltwo   globaltwo
        $global_excludes:2:!globaltwo   b/globaltwo
        $global_excludes:2:!globaltwo   ../b/globaltwo
+       ::      c/not-ignored
 EOF
+grep -v '^::   ' expected-all >expected-verbose
+sed -e 's/.*   //' expected-verbose >expected-default
 
 sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \
        tr "\n" "\0" >stdin0
@@ -629,6 +661,14 @@ test_expect_success '--stdin from subdirectory with -v' '
        )
 '
 
+test_expect_success '--stdin from subdirectory with -v -n' '
+       expect_from_stdin <expected-all &&
+       (
+               cd a &&
+               test_check_ignore "--stdin -v -n" <../stdin
+       )
+'
+
 for opts in '--stdin -z' '-z --stdin'
 do
        test_expect_success "$opts from subdirectory" '
@@ -648,5 +688,23 @@ do
        '
 done
 
+test_expect_success PIPE 'streaming support for --stdin' '
+       mkfifo in out &&
+       (git check-ignore -n -v --stdin <in >out &) &&
+
+       # We cannot just "echo >in" because check-ignore would get EOF
+       # after echo exited; instead we open the descriptor in our
+       # shell, and then echo to the fd. We make sure to close it at
+       # the end, so that the subprocess does get EOF and dies
+       # properly.
+       exec 9>in &&
+       test_when_finished "exec 9>&-" &&
+       echo >&9 one &&
+       read response <out &&
+       echo "$response" | grep "^\.gitignore:1:one     one" &&
+       echo >&9 two &&
+       read response <out &&
+       echo "$response" | grep "^::    two"
+'
 
 test_done