[PATCH 9/9] Add git-check-ignores

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This works in a similar manner to git-check-attr.  Some code
was reused from add.c by refactoring out into pathspec.c.

Thanks to Jeff King and Junio C Hamano for the idea:
http://thread.gmane.org/gmane.comp.version-control.git/108671/focus=108815

Signed-off-by: Adam Spiers <git@xxxxxxxxxxxxxx>
---
 .gitignore                             |   1 +
 Documentation/git-check-ignore.txt     |  58 +++++++
 Documentation/gitignore.txt            |   6 +-
 Makefile                               |   1 +
 builtin.h                              |   1 +
 builtin/add.c                          |   2 +-
 builtin/check-ignore.c                 | 150 ++++++++++++++++
 command-list.txt                       |   1 +
 contrib/completion/git-completion.bash |   1 +
 git.c                                  |   1 +
 t/t0007-ignores.sh                     | 301 +++++++++++++++++++++++++++++++++
 11 files changed, 520 insertions(+), 3 deletions(-)
 create mode 100644 Documentation/git-check-ignore.txt
 create mode 100644 builtin/check-ignore.c
 create mode 100755 t/t0007-ignores.sh

diff --git a/.gitignore b/.gitignore
index bb5c91e..0cbe94c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@
 /git-bundle
 /git-cat-file
 /git-check-attr
+/git-check-ignore
 /git-check-ref-format
 /git-checkout
 /git-checkout-index
diff --git a/Documentation/git-check-ignore.txt b/Documentation/git-check-ignore.txt
new file mode 100644
index 0000000..3a85dbb
--- /dev/null
+++ b/Documentation/git-check-ignore.txt
@@ -0,0 +1,58 @@
+git-check-ignore(1)
+=================
+
+NAME
+----
+git-check-ignore - Debug gitignore / exclude files
+
+
+SYNOPSIS
+--------
+[verse]
+'git check-ignore' pathname...
+'git check-ignore' --stdin [-z] < <list-of-paths>
+
+DESCRIPTION
+-----------
+
+For each pathname given via the command-line or from a file via
+`--stdin`, this command will list the first exclude pattern found (if
+any) which explicitly excludes or includes that pathname.  Note that
+within any given exclude file, later patterns take precedence over
+earlier ones, so any matching pattern which this command outputs may
+not be the one you would immediately expect.
+
+OPTIONS
+-------
+--stdin::
+	Read file names from stdin instead of from the command-line.
+
+-z::
+	Only meaningful with `--stdin`; paths are separated with a
+	NUL character instead of a linefeed character.
+
+OUTPUT
+------
+
+The output is a series of lines of the form:
+
+<path> COLON SP <type> SP <pattern> SP <source> SP <position> LF
+
+<path> is the path of a file being queried, <type> is either
+'excluded' or 'included' (for patterns prefixed with '!'), <pattern>
+is the matching pattern, <source> is the pattern's source file (either
+as an absolute path or relative to the repository root), and
+<position> is the position of the pattern within that source.
+
+If no pattern matches a given path, nothing will be output for that
+path.
+
+SEE ALSO
+--------
+linkgit:gitignore[5]
+linkgit:gitconfig[5]
+linkgit:git-ls-files[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index c1f692a..7ba16fe 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -155,8 +155,10 @@ The second .gitignore prevents git from ignoring
 
 SEE ALSO
 --------
-linkgit:git-rm[1], linkgit:git-update-index[1],
-linkgit:gitrepository-layout[5]
+linkgit:git-rm[1],
+linkgit:git-update-index[1],
+linkgit:gitrepository-layout[5],
+linkgit:git-check-ignore[1]
 
 GIT
 ---
diff --git a/Makefile b/Makefile
index 272ab69..ce5e42f 100644
--- a/Makefile
+++ b/Makefile
@@ -825,6 +825,7 @@ BUILTIN_OBJS += builtin/branch.o
 BUILTIN_OBJS += builtin/bundle.o
 BUILTIN_OBJS += builtin/cat-file.o
 BUILTIN_OBJS += builtin/check-attr.o
+BUILTIN_OBJS += builtin/check-ignore.o
 BUILTIN_OBJS += builtin/check-ref-format.o
 BUILTIN_OBJS += builtin/checkout-index.o
 BUILTIN_OBJS += builtin/checkout.o
diff --git a/builtin.h b/builtin.h
index 8e37752..f812c97 100644
--- a/builtin.h
+++ b/builtin.h
@@ -57,6 +57,7 @@ extern int cmd_cat_file(int argc, const char **argv, const char *prefix);
 extern int cmd_checkout(int argc, const char **argv, const char *prefix);
 extern int cmd_checkout_index(int argc, const char **argv, const char *prefix);
 extern int cmd_check_attr(int argc, const char **argv, const char *prefix);
+extern int cmd_check_ignore(int argc, const char **argv, const char *prefix);
 extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);
diff --git a/builtin/add.c b/builtin/add.c
index a7ed2ad..af68c32 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -273,7 +273,7 @@ static int add_files(struct dir_struct *dir, int flags)
 		fprintf(stderr, _(ignore_error));
 		for (i = 0; i < dir->ignored_nr; i++)
 			fprintf(stderr, "%s\n", dir->ignored[i]->name);
-		fprintf(stderr, _("Use -f if you really want to add them.\n"));
+		fprintf(stderr, _("Use -f if you really want to add them, or git check-ignore to see\nwhy they're ignored.\n"));
 		die(_("no files added"));
 	}
 
diff --git a/builtin/check-ignore.c b/builtin/check-ignore.c
new file mode 100644
index 0000000..146c00b
--- /dev/null
+++ b/builtin/check-ignore.c
@@ -0,0 +1,150 @@
+#include "builtin.h"
+#include "cache.h"
+#include "dir.h"
+#include "quote.h"
+#include "pathspec.h"
+#include "parse-options.h"
+
+static int stdin_paths;
+static const char * const check_ignore_usage[] = {
+"git check-ignore pathname...",
+"git check-ignore --stdin [-z] < <list-of-paths>",
+NULL
+};
+
+static int null_term_line;
+
+static const struct option check_ignore_options[] = {
+	OPT_BOOLEAN(0 , "stdin", &stdin_paths, "read file names from stdin"),
+	OPT_BOOLEAN('z', NULL, &null_term_line,
+		"input paths are terminated by a null character"),
+	OPT_END()
+};
+
+static void output_exclude(const char *path, struct exclude *exclude)
+{
+	char *type = exclude->to_exclude ? "excluded" : "included";
+	char *bang = exclude->to_exclude ? "" : "!";
+	char *dir  = (exclude->flags & EXC_FLAG_MUSTBEDIR) ? "/" : "";
+	printf(_("%s: %s %s%s%s "), path, type, bang, exclude->pattern, dir);
+	if (exclude->srcpos > 0) {
+		printf("%s %d", exclude->src, exclude->srcpos);
+	}
+	else {
+		/* Exclude was from CLI parameter.  This code path is
+		 * currently impossible to hit, but later on we might
+		 * want to add ignore tracing to other commands such
+		 * as git clean, which does accept --exclude.
+		 */
+		/* printf("%s %d", exclude->src, -exclude->srcpos); */
+	}
+	printf("\n");
+}
+
+static void check_ignore(const char *prefix, const char **pathspec)
+{
+	struct dir_struct dir;
+	const char *path;
+	char *seen = NULL;
+
+	/* 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));
+	dir.flags |= DIR_COLLECT_IGNORED;
+	setup_standard_excludes(&dir);
+
+	if (pathspec) {
+		int i;
+		struct path_exclude_check check;
+		struct exclude *exclude;
+
+		path_exclude_check_init(&check, &dir);
+		if (!seen)
+			seen = find_used_pathspec(pathspec);
+		for (i = 0; pathspec[i]; i++) {
+			path = pathspec[i];
+			char *full_path =
+				prefix_path(prefix, prefix ? strlen(prefix) : 0, path);
+			treat_gitlink(full_path);
+			validate_path(prefix, full_path);
+			if (!seen[i] && path[0]) {
+				int dtype = DT_UNKNOWN;
+				exclude = path_excluded_1(&check, full_path, -1, &dtype);
+				if (exclude) {
+					output_exclude(path, exclude);
+				}
+			}
+		}
+		free(seen);
+		free_directory(&dir);
+		path_exclude_check_clear(&check);
+	}
+	else {
+		printf("no pathspec\n");
+	}
+}
+
+static void check_ignore_stdin_paths(const char *prefix)
+{
+	struct strbuf buf, nbuf;
+	char **pathspec = NULL;
+	size_t nr = 0, alloc = 0;
+	int line_termination = null_term_line ? 0 : '\n';
+
+	strbuf_init(&buf, 0);
+	strbuf_init(&nbuf, 0);
+	while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
+		if (line_termination && buf.buf[0] == '"') {
+			strbuf_reset(&nbuf);
+			if (unquote_c_style(&nbuf, buf.buf, NULL))
+				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);
+	}
+	ALLOC_GROW(pathspec, nr + 1, alloc);
+	pathspec[nr] = NULL;
+	check_ignore(prefix, (const char **)pathspec);
+	maybe_flush_or_die(stdout, "attribute to stdout");
+	strbuf_release(&buf);
+	strbuf_release(&nbuf);
+	free(pathspec);
+}
+
+static NORETURN void error_with_usage(const char *msg)
+{
+	error("%s", msg);
+	usage_with_options(check_ignore_usage, check_ignore_options);
+}
+
+int cmd_check_ignore(int argc, const char **argv, const char *prefix)
+{
+	git_config(git_default_config, NULL);
+
+	argc = parse_options(argc, argv, prefix, check_ignore_options,
+			     check_ignore_usage, 0);
+
+	if (stdin_paths) {
+		if (0 < argc)
+			error_with_usage("Can't specify files with --stdin");
+	} else {
+		if (null_term_line)
+			error_with_usage("-z only makes sense with --stdin");
+
+		if (argc == 0)
+			error_with_usage("No path specified");
+	}
+
+	if (stdin_paths)
+		check_ignore_stdin_paths(prefix);
+	else {
+		check_ignore(prefix, argv);
+		maybe_flush_or_die(stdout, "ignore to stdout");
+	}
+
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index 7e8cfec..bf83303 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -12,6 +12,7 @@ git-branch                              mainporcelain common
 git-bundle                              mainporcelain
 git-cat-file                            plumbinginterrogators
 git-check-attr                          purehelpers
+git-check-ignore                        purehelpers
 git-checkout                            mainporcelain common
 git-checkout-index                      plumbingmanipulators
 git-check-ref-format                    purehelpers
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 222b804..26d798f 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -594,6 +594,7 @@ __git_list_porcelain_commands ()
 		archimport)       : import;;
 		cat-file)         : plumbing;;
 		check-attr)       : plumbing;;
+		check-ignore)     : plumbing;;
 		check-ref-format) : plumbing;;
 		checkout-index)   : plumbing;;
 		commit-tree)      : plumbing;;
diff --git a/git.c b/git.c
index 8788b32..6caf78a 100644
--- a/git.c
+++ b/git.c
@@ -338,6 +338,7 @@ static void handle_internal_command(int argc, const char **argv)
 		{ "bundle", cmd_bundle, RUN_SETUP_GENTLY },
 		{ "cat-file", cmd_cat_file, RUN_SETUP },
 		{ "check-attr", cmd_check_attr, RUN_SETUP },
+		{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
 		{ "check-ref-format", cmd_check_ref_format },
 		{ "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
 		{ "checkout-index", cmd_checkout_index,
diff --git a/t/t0007-ignores.sh b/t/t0007-ignores.sh
new file mode 100755
index 0000000..4407634
--- /dev/null
+++ b/t/t0007-ignores.sh
@@ -0,0 +1,301 @@
+#!/bin/sh
+
+test_description=gitignores
+
+. ./test-lib.sh
+
+init_vars () {
+	global_excludes="$HOME/global-excludes"
+}
+
+enable_global_excludes () {
+	init_vars
+	git config core.excludesfile "$global_excludes"
+}
+
+ignore_check () {
+	paths="$1" expected="$2" global_args="$3"
+
+	if test -z "$expected"; then
+	    >"$HOME/expected" # avoid newline
+	else
+	    echo "$expected" >"$HOME/expected"
+	fi &&
+	run_check_ignore "$paths" "$global_args"
+}
+
+expect () {
+	echo "$*" >"$HOME/expected"
+}
+
+run_check_ignore () {
+	args="$1" global_args="$2"
+
+	init_vars &&
+	rm -f "$HOME/stdout" "$HOME/stderr" "$HOME/cmd" &&
+	echo `which git` $global_args check-ignore $args >"$HOME/cmd" &&
+	pwd >"$HOME/pwd" &&
+	git $global_args check-ignore $args >"$HOME/stdout" 2>"$HOME/stderr" &&
+	test_cmp "$HOME/expected" "$HOME/stdout" &&
+	test_line_count = 0 "$HOME/stderr"
+}
+
+test_expect_success 'setup' '
+	init_vars
+	mkdir -p a/b/ignored-dir a/submodule b &&
+	ln -s b a/symlink &&
+	(
+		cd a/submodule &&
+		git init &&
+		echo a > a &&
+		git add a &&
+		git commit -m"commit in submodule"
+	) &&
+	git add a/submodule &&
+	cat <<-EOF >.gitignore &&
+		one
+	EOF
+	cat <<-EOF >a/.gitignore &&
+		two*
+		*three
+	EOF
+	cat <<-EOF >a/b/.gitignore &&
+		four
+		five
+		# this comment should affect the line numbers
+		six
+		ignored-dir/
+		# and so should this blank line:
+
+		!on*
+		!two
+	EOF
+	echo "seven" >a/b/ignored-dir/.gitignore &&
+	test -n "$HOME" &&
+	cat <<-EOF >"$global_excludes"
+		globalone
+		!globaltwo
+		globalthree
+	EOF
+'
+
+test_expect_success 'empty command line' '
+	test_must_fail git check-ignore 2>"$HOME/stderr" &&
+	grep -q "error: No path specified" "$HOME/stderr"
+'
+
+test_expect_success 'erroneous use of --' '
+	test_must_fail git check-ignore -- 2>"$HOME/stderr" &&
+	grep -q "error: No path specified" "$HOME/stderr"
+'
+
+test_expect_success '--stdin with superfluous arg' '
+	test_must_fail git check-ignore --stdin foo 2>"$HOME/stderr" &&
+	grep -q "Can'\''t specify files with --stdin" "$HOME/stderr"
+'
+
+test_expect_success '--stdin -z with superfluous arg' '
+	test_must_fail git check-ignore --stdin -z foo 2>"$HOME/stderr" &&
+	grep -q "Can'\''t specify files with --stdin" "$HOME/stderr"
+'
+
+test_expect_success '-z without --stdin' '
+	test_must_fail git check-ignore -z 2>"$HOME/stderr" &&
+	grep -q "error: -z only makes sense with --stdin" "$HOME/stderr"
+'
+
+test_expect_success '-z without --stdin and superfluous arg' '
+	test_must_fail git check-ignore -z foo 2>"$HOME/stderr" &&
+	grep -q "error: -z only makes sense with --stdin" "$HOME/stderr"
+'
+
+test_expect_success 'needs work tree' '
+	(
+		cd .git &&
+		test_must_fail git check-ignore foo 2>"$HOME/stderr"
+	) &&
+	grep -q "fatal: This operation must be run in a work tree" "$HOME/stderr"
+
+'
+test_expect_success 'top-level not ignored' '
+	ignore_check foo ""
+'
+
+test_expect_success 'top-level ignored' '
+	ignore_check one "one: excluded one .gitignore 1"
+'
+
+test_expect_success 'sub-directory ignore from top' '
+	expect "a/one: excluded one .gitignore 1" &&
+	run_check_ignore a/one
+'
+
+test_expect_success 'sub-directory local ignore' '
+	expect "a/3-three: excluded *three a/.gitignore 2" &&
+	run_check_ignore "a/3-three a/three-not-this-one"
+'
+
+test_expect_success 'sub-directory local ignore inside a' '
+	expect "3-three: excluded *three a/.gitignore 2" &&
+	(
+		cd a &&
+		run_check_ignore "3-three three-not-this-one"
+	)
+'
+
+test_expect_success 'nested include' '
+	expect "a/b/one: included !on* a/b/.gitignore 8" &&
+	run_check_ignore "a/b/one"
+'
+
+test_expect_success 'ignored sub-directory' '
+	expect "a/b/ignored-dir: excluded ignored-dir/ a/b/.gitignore 5" &&
+	run_check_ignore "a/b/ignored-dir"
+'
+
+test_expect_success 'multiple files inside ignored sub-directory' '
+	cat <<-EOF >"$HOME/expected" &&
+		a/b/ignored-dir/foo: excluded ignored-dir/ a/b/.gitignore 5
+		a/b/ignored-dir/twoooo: excluded ignored-dir/ a/b/.gitignore 5
+		a/b/ignored-dir/seven: excluded ignored-dir/ a/b/.gitignore 5
+	EOF
+	run_check_ignore "a/b/ignored-dir/foo a/b/ignored-dir/twoooo a/b/ignored-dir/seven"
+'
+
+test_expect_success 'cd to ignored sub-directory' '
+	cat <<-EOF >"$HOME/expected" &&
+		foo: excluded ignored-dir/ a/b/.gitignore 5
+		twoooo: excluded ignored-dir/ a/b/.gitignore 5
+		../one: included !on* a/b/.gitignore 8
+		seven: excluded ignored-dir/ a/b/.gitignore 5
+		../../one: excluded one .gitignore 1
+	EOF
+	(
+		cd a/b/ignored-dir &&
+		run_check_ignore "foo twoooo ../one seven ../../one"
+	)
+'
+
+test_expect_success 'symlink' '
+	ignore_check "a/symlink" ""
+'
+
+test_expect_success 'beyond a symlink' '
+	test_must_fail git check-ignore "a/symlink/foo"
+'
+
+test_expect_success 'beyond a symlink from subdirectory' '
+	(
+		cd a &&
+		test_must_fail git check-ignore "symlink/foo"
+	)
+'
+
+test_expect_success 'submodule' '
+	test_must_fail git check-ignore "a/submodule/one" 2>"$HOME/stderr" &&
+	expect "fatal: Path '\''a/submodule/one'\'' is in submodule '\''a/submodule'\''" &&
+	test_cmp "$HOME/expected" "$HOME/stderr"
+'
+
+test_expect_success 'submodule from subdirectory' '
+	(
+		cd a &&
+		test_must_fail git check-ignore "submodule/one" 2>"$HOME/stderr"
+	) &&
+	expect "fatal: Path '\''a/submodule/one'\'' is in submodule '\''a/submodule'\''" &&
+	test_cmp "$HOME/expected" "$HOME/stderr"
+'
+
+test_expect_success 'global ignore not yet enabled' '
+	expect "a/globalthree: excluded *three a/.gitignore 2" &&
+	run_check_ignore "globalone a/globalthree a/globaltwo"
+'
+
+test_expect_success 'global ignore' '
+	enable_global_excludes &&
+	cat <<-EOF >"$HOME/expected" &&
+		globalone: excluded globalone $global_excludes 1
+		globalthree: excluded globalthree $global_excludes 3
+		a/globalthree: excluded *three a/.gitignore 2
+		globaltwo: included !globaltwo $global_excludes 2
+	EOF
+	run_check_ignore "globalone globalthree a/globalthree globaltwo"
+'
+
+test_expect_success '--stdin' '
+	cat <<-EOF >in.txt &&
+		one
+		a/one
+		a/b/on
+		a/b/one
+		a/b/two
+		a/b/twooo
+		globaltwo
+		a/globaltwo
+		a/b/globaltwo
+		b/globaltwo
+	EOF
+	cat <<-EOF >"$HOME/expected" &&
+		one: excluded one .gitignore 1
+		a/one: excluded one .gitignore 1
+		a/b/on: included !on* a/b/.gitignore 8
+		a/b/one: included !on* a/b/.gitignore 8
+		a/b/two: included !two a/b/.gitignore 9
+		a/b/twooo: excluded two* a/.gitignore 1
+		globaltwo: included !globaltwo $global_excludes 2
+		a/globaltwo: included !globaltwo $global_excludes 2
+		a/b/globaltwo: included !globaltwo $global_excludes 2
+		b/globaltwo: included !globaltwo $global_excludes 2
+	EOF
+	run_check_ignore --stdin < in.txt
+'
+
+test_expect_success '--stdin -z' '
+	tr "\n" "\0" < in.txt | run_check_ignore "--stdin -z"
+'
+
+test_expect_success '-z --stdin' '
+	tr "\n" "\0" < in.txt | run_check_ignore "-z --stdin"
+'
+
+test_expect_success '--stdin from subdirectory' '
+	cat <<-EOF >in.txt &&
+		../one
+		one
+		b/on
+		b/one
+		b/two
+		b/twooo
+		../globaltwo
+		globaltwo
+		b/globaltwo
+		../b/globaltwo
+	EOF
+	cat <<-EOF >"$HOME/expected" &&
+		../one: excluded one .gitignore 1
+		one: excluded one .gitignore 1
+		b/on: included !on* a/b/.gitignore 8
+		b/one: included !on* a/b/.gitignore 8
+		b/two: included !two a/b/.gitignore 9
+		b/twooo: excluded two* a/.gitignore 1
+		../globaltwo: included !globaltwo $global_excludes 2
+		globaltwo: included !globaltwo $global_excludes 2
+		b/globaltwo: included !globaltwo $global_excludes 2
+		../b/globaltwo: included !globaltwo $global_excludes 2
+	EOF
+	(
+		cd a &&
+		run_check_ignore --stdin < ../in.txt
+	)
+'
+
+test_expect_success '--stdin -z from subdirectory' '
+	tr "\n" "\0" < in.txt | ( cd a && run_check_ignore "--stdin -z" )
+'
+
+test_expect_success '-z --stdin from subdirectory' '
+	tr "\n" "\0" < in.txt | ( cd a && run_check_ignore "-z --stdin" )
+'
+
+
+test_done
-- 
1.7.12.155.ge5750d5.dirty

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]