Merge branch 'as/check-ignore'

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
This commit is contained in:
Junio C Hamano 2013-05-29 14:23:39 -07:00
commit c51afbbd18
5 changed files with 188 additions and 96 deletions

View file

@ -56,6 +56,11 @@ being queried and <info> can be either:
'set';; when the attribute is defined as true. 'set';; when the attribute is defined as true.
<value>;; when a value has been assigned to the attribute. <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 EXAMPLES
-------- --------

View file

@ -39,6 +39,12 @@ OPTIONS
below). If `--stdin` is also given, input paths are separated below). If `--stdin` is also given, input paths are separated
with a NUL character instead of a linefeed character. 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 OUTPUT
------ ------
@ -65,6 +71,20 @@ are also used instead of colons and hard tabs:
<source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL> <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 EXIT STATUS
----------- -----------

View file

@ -811,8 +811,9 @@ for further details.
'GIT_FLUSH':: 'GIT_FLUSH'::
If this environment variable is set to "1", then commands such If this environment variable is set to "1", then commands such
as 'git blame' (in incremental mode), 'git rev-list', 'git log', as 'git blame' (in incremental mode), 'git rev-list', 'git log',
and 'git whatchanged' will force a flush of the output stream 'git check-attr', 'git check-ignore', and 'git whatchanged' will
after each commit-oriented record have been flushed. If this 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 variable is set to "0", the output of these commands will be done
using completely buffered I/O. If this environment variable is using completely buffered I/O. If this environment variable is
not set, Git will choose buffered or record-oriented flushing not set, Git will choose buffered or record-oriented flushing

View file

@ -5,7 +5,7 @@
#include "pathspec.h" #include "pathspec.h"
#include "parse-options.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[] = { static const char * const check_ignore_usage[] = {
"git check-ignore [options] pathname...", "git check-ignore [options] pathname...",
"git check-ignore [options] --stdin < <list-of-paths>", "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")), N_("read file names from stdin")),
OPT_BOOLEAN('z', NULL, &null_term_line, OPT_BOOLEAN('z', NULL, &null_term_line,
N_("input paths are terminated by a null character")), 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() OPT_END()
}; };
static void output_exclude(const char *path, struct exclude *exclude) static void output_exclude(const char *path, struct exclude *exclude)
{ {
char *bang = exclude->flags & EXC_FLAG_NEGATIVE ? "!" : ""; char *bang = (exclude && exclude->flags & EXC_FLAG_NEGATIVE) ? "!" : "";
char *slash = exclude->flags & EXC_FLAG_MUSTBEDIR ? "/" : ""; char *slash = (exclude && exclude->flags & EXC_FLAG_MUSTBEDIR) ? "/" : "";
if (!null_term_line) { if (!null_term_line) {
if (!verbose) { if (!verbose) {
write_name_quoted(path, stdout, '\n'); write_name_quoted(path, stdout, '\n');
} else { } else {
quote_c_style(exclude->el->src, NULL, stdout, 0); if (exclude) {
printf(":%d:%s%s%s\t", quote_c_style(exclude->el->src, NULL, stdout, 0);
exclude->srcpos, printf(":%d:%s%s%s\t",
bang, exclude->pattern, slash); exclude->srcpos,
bang, exclude->pattern, slash);
}
else {
printf("::\t");
}
quote_c_style(path, NULL, stdout, 0); quote_c_style(path, NULL, stdout, 0);
fputc('\n', stdout); fputc('\n', stdout);
} }
@ -44,30 +51,26 @@ static void output_exclude(const char *path, struct exclude *exclude)
if (!verbose) { if (!verbose) {
printf("%s%c", path, '\0'); printf("%s%c", path, '\0');
} else { } else {
printf("%s%c%d%c%s%s%s%c%s%c", if (exclude)
exclude->el->src, '\0', printf("%s%c%d%c%s%s%s%c%s%c",
exclude->srcpos, '\0', exclude->el->src, '\0',
bang, exclude->pattern, slash, '\0', exclude->srcpos, '\0',
path, '\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; const char *path, *full_path;
char *seen; char *seen;
int num_ignored = 0, dtype = DT_UNKNOWN, i; int num_ignored = 0, dtype = DT_UNKNOWN, i;
struct exclude *exclude; 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 (!pathspec || !*pathspec) {
if (!quiet) if (!quiet)
fprintf(stderr, "no pathspec given.\n"); fprintf(stderr, "no pathspec given.\n");
@ -86,28 +89,26 @@ static int check_ignore(const char *prefix, const char **pathspec)
? strlen(prefix) : 0, path); ? strlen(prefix) : 0, path);
full_path = check_path_for_gitlink(full_path); full_path = check_path_for_gitlink(full_path);
die_if_path_beyond_symlink(full_path, prefix); die_if_path_beyond_symlink(full_path, prefix);
exclude = NULL;
if (!seen[i]) { if (!seen[i]) {
exclude = last_exclude_matching(&dir, full_path, &dtype); exclude = last_exclude_matching(dir, full_path, &dtype);
if (exclude) {
if (!quiet)
output_exclude(path, exclude);
num_ignored++;
}
} }
if (!quiet && (exclude || show_non_matching))
output_exclude(path, exclude);
if (exclude)
num_ignored++;
} }
free(seen); free(seen);
clear_directory(&dir);
return num_ignored; 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; struct strbuf buf, nbuf;
char **pathspec = NULL; char *pathspec[2] = { NULL, NULL };
size_t nr = 0, alloc = 0;
int line_termination = null_term_line ? 0 : '\n'; int line_termination = null_term_line ? 0 : '\n';
int num_ignored; int num_ignored = 0;
strbuf_init(&buf, 0); strbuf_init(&buf, 0);
strbuf_init(&nbuf, 0); strbuf_init(&nbuf, 0);
@ -118,23 +119,19 @@ static int check_ignore_stdin_paths(const char *prefix)
die("line is badly quoted"); die("line is badly quoted");
strbuf_swap(&buf, &nbuf); strbuf_swap(&buf, &nbuf);
} }
ALLOC_GROW(pathspec, nr + 1, alloc); pathspec[0] = buf.buf;
pathspec[nr] = xcalloc(strlen(buf.buf) + 1, sizeof(*buf.buf)); num_ignored += check_ignore(dir, prefix, (const char **)pathspec);
strcpy(pathspec[nr++], buf.buf); 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(&buf);
strbuf_release(&nbuf); strbuf_release(&nbuf);
free(pathspec);
return num_ignored; return num_ignored;
} }
int cmd_check_ignore(int argc, const char **argv, const char *prefix) int cmd_check_ignore(int argc, const char **argv, const char *prefix)
{ {
int num_ignored; int num_ignored;
struct dir_struct dir;
git_config(git_default_config, NULL); git_config(git_default_config, NULL);
@ -156,13 +153,24 @@ int cmd_check_ignore(int argc, const char **argv, const char *prefix)
if (verbose) if (verbose)
die(_("cannot have both --quiet and --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) { if (stdin_paths) {
num_ignored = check_ignore_stdin_paths(prefix); num_ignored = check_ignore_stdin_paths(&dir, prefix);
} else { } else {
num_ignored = check_ignore(prefix, argv); num_ignored = check_ignore(&dir, prefix, argv);
maybe_flush_or_die(stdout, "ignore to stdout"); maybe_flush_or_die(stdout, "ignore to stdout");
} }
clear_directory(&dir);
return !num_ignored; return !num_ignored;
} }

View file

@ -66,16 +66,23 @@ test_check_ignore () {
init_vars && init_vars &&
rm -f "$HOME/stdout" "$HOME/stderr" "$HOME/cmd" && 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" && >"$HOME/cmd" &&
echo "$expect_code" >"$HOME/expected-exit-code" &&
test_expect_code "$expect_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" && >"$HOME/stdout" 2>"$HOME/stderr" &&
test_cmp "$HOME/expected-stdout" "$HOME/stdout" && test_cmp "$HOME/expected-stdout" "$HOME/stdout" &&
stderr_empty_on_success "$expect_code" 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 # expecting success each time. Takes advantage of the fact that
# check-ignore --verbose output is the same as normal output except # check-ignore --verbose output is the same as normal output except
# for the extra first column. # for the extra first column.
@ -83,7 +90,9 @@ test_check_ignore () {
# Arguments: # Arguments:
# - (optional) prereqs for this test, e.g. 'SYMLINKS' # - (optional) prereqs for this test, e.g. 'SYMLINKS'
# - test name # - 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) # - code to run (should invoke test_check_ignore)
test_expect_success_multi () { test_expect_success_multi () {
prereq= prereq=
@ -92,8 +101,9 @@ test_expect_success_multi () {
prereq=$1 prereq=$1
shift shift
fi 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/.* //' ) expect=$( echo "$expect_verbose" | sed -e 's/.* //' )
test_expect_success $prereq "$testname" ' test_expect_success $prereq "$testname" '
@ -101,23 +111,40 @@ test_expect_success_multi () {
eval "$code" eval "$code"
' '
for quiet_opt in '-q' '--quiet' # --quiet is only valid when a single pattern is passed
do if test $( echo "$expect_all" | wc -l ) = 1
test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" " then
for quiet_opt in '-q' '--quiet'
do
test_expect_success $prereq "$testname${quiet_opt:+ with $quiet_opt}" "
expect '' && expect '' &&
$code $code
" "
done done
quiet_opt= quiet_opt=
fi
for verbose_opt in '-v' '--verbose' for verbose_opt in '-v' '--verbose'
do do
test_expect_success $prereq "$testname${verbose_opt:+ with $verbose_opt}" " for non_matching_opt in '' ' -n' ' --non-matching'
expect '$expect_verbose' && do
$code 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 done
verbose_opt= verbose_opt=
non_matching_opt=
} }
test_expect_success 'setup' ' test_expect_success 'setup' '
@ -178,7 +205,7 @@ test_expect_success 'setup' '
# #
# test invalid inputs # test invalid inputs
test_expect_success_multi '. corner-case' '' ' test_expect_success_multi '. corner-case' ':: .' '
test_check_ignore . 1 test_check_ignore . 1
' '
@ -189,11 +216,7 @@ test_expect_success_multi 'empty command line' '' '
test_expect_success_multi '--stdin with empty STDIN' '' ' test_expect_success_multi '--stdin with empty STDIN' '' '
test_check_ignore "--stdin" 1 </dev/null && test_check_ignore "--stdin" 1 </dev/null &&
if test -n "$quiet_opt"; then test_stderr ""
test_stderr ""
else
test_stderr "no pathspec given."
fi
' '
test_expect_success '-q with multiple args' ' test_expect_success '-q with multiple args' '
@ -276,27 +299,39 @@ do
where="in subdir $subdir" where="in subdir $subdir"
fi fi
test_expect_success_multi "non-existent file $where not ignored" '' " test_expect_success_multi "non-existent file $where not ignored" \
test_check_ignore '${subdir}non-existent' 1 ":: ${subdir}non-existent" \
" "test_check_ignore '${subdir}non-existent' 1"
test_expect_success_multi "non-existent file $where ignored" \ test_expect_success_multi "non-existent file $where ignored" \
".gitignore:1:one ${subdir}one" " ".gitignore:1:one ${subdir}one" \
test_check_ignore '${subdir}one' "test_check_ignore '${subdir}one'"
"
test_expect_success_multi "existing untracked file $where not ignored" '' " test_expect_success_multi "existing untracked file $where not ignored" \
test_check_ignore '${subdir}not-ignored' 1 ":: ${subdir}not-ignored" \
" "test_check_ignore '${subdir}not-ignored' 1"
test_expect_success_multi "existing tracked file $where not ignored" '' " test_expect_success_multi "existing tracked file $where not ignored" \
test_check_ignore '${subdir}ignored-but-in-index' 1 ":: ${subdir}ignored-but-in-index" \
" "test_check_ignore '${subdir}ignored-but-in-index' 1"
test_expect_success_multi "existing untracked file $where ignored" \ test_expect_success_multi "existing untracked file $where ignored" \
".gitignore:2:ignored-* ${subdir}ignored-and-untracked" " ".gitignore:2:ignored-* ${subdir}ignored-and-untracked" \
test_check_ignore '${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 done
# Having established the above, from now on we mostly test against # 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 handling of symlinks
test_expect_success_multi SYMLINKS 'symlink' '' ' test_expect_success_multi SYMLINKS 'symlink' ':: a/symlink' '
test_check_ignore "a/symlink" 1 test_check_ignore "a/symlink" 1
' '
@ -574,37 +609,34 @@ cat <<-\EOF >stdin
globaltwo globaltwo
b/globaltwo b/globaltwo
../b/globaltwo ../b/globaltwo
c/not-ignored
EOF EOF
cat <<-\EOF >expected-default # N.B. we deliberately end STDIN with a non-matching pattern in order
../one # to test that the exit code indicates that one or more of the
one # provided paths is ignored - in other words, that it represents an
b/on # aggregation of all the results, not just the final result.
b/one
b/one one cat <<-EOF >expected-all
b/one two
"b/one\"three"
b/two
b/twooo
../globaltwo
globaltwo
b/globaltwo
../b/globaltwo
EOF
cat <<-EOF >expected-verbose
.gitignore:1:one ../one .gitignore:1:one ../one
:: ../not-ignored
.gitignore:1:one one .gitignore:1:one one
:: not-ignored
a/b/.gitignore:8:!on* b/on a/b/.gitignore:8:!on* b/on
a/b/.gitignore:8:!on* b/one a/b/.gitignore:8:!on* b/one
a/b/.gitignore:8:!on* b/one one a/b/.gitignore:8:!on* b/one one
a/b/.gitignore:8:!on* b/one two a/b/.gitignore:8:!on* b/one two
a/b/.gitignore:8:!on* "b/one\"three" a/b/.gitignore:8:!on* "b/one\"three"
a/b/.gitignore:9:!two b/two a/b/.gitignore:9:!two b/two
:: b/not-ignored
a/.gitignore:1:two* b/twooo a/.gitignore:1:two* b/twooo
$global_excludes:2:!globaltwo ../globaltwo $global_excludes:2:!globaltwo ../globaltwo
$global_excludes:2:!globaltwo globaltwo $global_excludes:2:!globaltwo globaltwo
$global_excludes:2:!globaltwo b/globaltwo $global_excludes:2:!globaltwo b/globaltwo
$global_excludes:2:!globaltwo ../b/globaltwo $global_excludes:2:!globaltwo ../b/globaltwo
:: c/not-ignored
EOF EOF
grep -v '^:: ' expected-all >expected-verbose
sed -e 's/.* //' expected-verbose >expected-default
sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \ sed -e 's/^"//' -e 's/\\//' -e 's/"$//' stdin | \
tr "\n" "\0" >stdin0 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' for opts in '--stdin -z' '-z --stdin'
do do
test_expect_success "$opts from subdirectory" ' test_expect_success "$opts from subdirectory" '
@ -648,5 +688,23 @@ do
' '
done 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 test_done