Commit 16d742ebdb91492b784555cb528408531995634c

Carlos Martín Nieto 2015-05-13T21:43:58

Merge pull request #3119 from ethomson/ignore Attributes: don't match files for folders

diff --git a/src/attr.c b/src/attr.c
index 102d024..d43a15f 100644
--- a/src/attr.c
+++ b/src/attr.c
@@ -309,7 +309,8 @@ static int attr_setup(git_repository *repo, git_attr_session *attr_session)
 	if (error == 0)
 		error = preload_attr_file(
 			repo, attr_session, GIT_ATTR_FILE__FROM_FILE, NULL, sys.ptr);
-	else if (error != GIT_ENOTFOUND)
+
+	if (error != GIT_ENOTFOUND)
 		return error;
 
 	git_buf_free(&sys);
diff --git a/src/attr_file.c b/src/attr_file.c
index ef98aac..8970686 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -359,6 +359,7 @@ bool git_attr_fnmatch__match(
 	git_attr_fnmatch *match,
 	git_attr_path *path)
 {
+	const char *relpath = path->path;
 	const char *filename;
 	int flags = 0;
 
@@ -375,6 +376,8 @@ bool git_attr_fnmatch__match(
 			if (git__prefixcmp(path->path, match->containing_dir))
 				return 0;
 		}
+
+		relpath += match->containing_dir_length;
 	}
 
 	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
@@ -383,7 +386,7 @@ bool git_attr_fnmatch__match(
 		flags |= FNM_LEADING_DIR;
 
 	if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) {
-		filename = path->path;
+		filename = relpath;
 		flags |= FNM_PATHNAME;
 	} else {
 		filename = path->basename;
@@ -393,35 +396,33 @@ bool git_attr_fnmatch__match(
 	}
 
 	if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) {
-		int matchval;
-		char *matchpath;
+		bool samename;
 
 		/* for attribute checks or root ignore checks, fail match */
 		if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) ||
 			path->basename == path->path)
 			return false;
 
-		/* for ignore checks, use container of current item for check */
-		path->basename[-1] = '\0';
 		flags |= FNM_LEADING_DIR;
 
-		if (match->containing_dir)
-			matchpath = path->basename;
-		else
-			matchpath = path->path;
+		/* fail match if this is a file with same name as ignored folder */
+		samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ?
+			!strcasecmp(match->pattern, relpath) :
+			!strcmp(match->pattern, relpath);
 
-		matchval = p_fnmatch(match->pattern, matchpath, flags);
-		path->basename[-1] = '/';
-		return (matchval != FNM_NOMATCH);
+		if (samename)
+			return false;
+
+		return (p_fnmatch(match->pattern, relpath, flags) != FNM_NOMATCH);
 	}
 
 	/* if path is a directory prefix of a negated pattern, then match */
 	if ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) && path->is_dir) {
-		size_t pathlen = strlen(path->path);
+		size_t pathlen = strlen(relpath);
 		bool prefixed = (pathlen <= match->length) &&
 			((match->flags & GIT_ATTR_FNMATCH_ICASE) ?
-			 !strncasecmp(match->pattern, path->path, pathlen) :
-			 !strncmp(match->pattern, path->path, pathlen));
+			!strncasecmp(match->pattern, relpath, pathlen) :
+			!strncmp(match->pattern, relpath, pathlen));
 
 		if (prefixed && git_path_at_end_of_segment(&match->pattern[pathlen]))
 			return true;
@@ -640,7 +641,7 @@ int git_attr_fnmatch__parse(
 	}
 
 	if (context) {
-		char *slash = strchr(context, '/');
+		char *slash = strrchr(context, '/');
 		size_t len;
 		if (slash) {
 			/* include the slash for easier matching */
@@ -650,27 +651,7 @@ int git_attr_fnmatch__parse(
 		}
 	}
 
-	if ((spec->flags & GIT_ATTR_FNMATCH_FULLPATH) != 0 &&
-		context != NULL && git_path_root(pattern) < 0)
-	{
-		/* use context path minus the trailing filename */
-		char *slash = strrchr(context, '/');
-		size_t contextlen = slash ? slash - context + 1 : 0;
-
-		/* given an unrooted fullpath match from a file inside a repo,
-		 * prefix the pattern with the relative directory of the source file
-		 */
-		spec->pattern = git_pool_malloc(
-			pool, (uint32_t)(contextlen + spec->length + 1));
-		if (spec->pattern) {
-			memcpy(spec->pattern, context, contextlen);
-			memcpy(spec->pattern + contextlen, pattern, spec->length);
-			spec->length += contextlen;
-			spec->pattern[spec->length] = '\0';
-		}
-	} else {
-		spec->pattern = git_pool_strndup(pool, pattern, spec->length);
-	}
+	spec->pattern = git_pool_strndup(pool, pattern, spec->length);
 
 	if (!spec->pattern) {
 		*base = git__next_line(pattern);
diff --git a/tests/attr/ignore.c b/tests/attr/ignore.c
index aa5b870..27fed25 100644
--- a/tests/attr/ignore.c
+++ b/tests/attr/ignore.c
@@ -160,7 +160,7 @@ void test_attr_ignore__subdirectory_gitignore(void)
 
 	assert_is_ignored(true, "file1");
 	assert_is_ignored(true, "dir/file1");
-	assert_is_ignored(true, "dir/file2");  /* in ignored dir */
+	assert_is_ignored(true, "dir/file2/actual_file");  /* in ignored dir */
 	assert_is_ignored(false, "dir/file3");
 }
 
@@ -191,3 +191,64 @@ void test_attr_ignore__expand_tilde_to_homedir(void)
 
 	assert_is_ignored(false, "example.global_with_tilde");
 }
+
+/* Ensure that the .gitignore in the subdirectory only affects
+ * items in the subdirectory. */
+void test_attr_ignore__gitignore_in_subdir(void)
+{
+	cl_git_rmfile("attr/.gitignore");
+
+	cl_must_pass(p_mkdir("attr/dir1", 0777));
+	cl_must_pass(p_mkdir("attr/dir1/dir2", 0777));
+	cl_must_pass(p_mkdir("attr/dir1/dir2/dir3", 0777));
+
+	cl_git_mkfile("attr/dir1/dir2/dir3/.gitignore", "dir1/\ndir1/subdir/");
+
+	assert_is_ignored(false, "dir1/file");
+	assert_is_ignored(false, "dir1/dir2/file");
+	assert_is_ignored(false, "dir1/dir2/dir3/file");
+	assert_is_ignored(true,  "dir1/dir2/dir3/dir1/file");
+	assert_is_ignored(true,  "dir1/dir2/dir3/dir1/subdir/foo");
+
+	if (cl_repo_get_bool(g_repo, "core.ignorecase")) {
+		cl_git_mkfile("attr/dir1/dir2/dir3/.gitignore", "DiR1/\nDiR1/subdir/\n");
+
+		assert_is_ignored(false, "dir1/file");
+		assert_is_ignored(false, "dir1/dir2/file");
+		assert_is_ignored(false, "dir1/dir2/dir3/file");
+		assert_is_ignored(true,  "dir1/dir2/dir3/dir1/file");
+		assert_is_ignored(true,  "dir1/dir2/dir3/dir1/subdir/foo");
+	}
+}
+
+/* Ensure that files do not match folder cases */
+void test_attr_ignore__dont_ignore_files_for_folder(void)
+{
+	cl_git_rmfile("attr/.gitignore");
+
+	cl_git_mkfile("attr/dir/.gitignore", "test/\n");
+
+	/* Create "test" as a file; ensure it is not ignored. */
+	cl_git_mkfile("attr/dir/test", "This is a file.");
+
+	assert_is_ignored(false, "dir/test");
+	if (cl_repo_get_bool(g_repo, "core.ignorecase"))
+		assert_is_ignored(false, "dir/TeSt");
+
+	/* Create "test" as a directory; ensure it is ignored. */
+	cl_git_rmfile("attr/dir/test");
+	cl_must_pass(p_mkdir("attr/dir/test", 0777));
+
+	assert_is_ignored(true, "dir/test");
+	if (cl_repo_get_bool(g_repo, "core.ignorecase"))
+		assert_is_ignored(true, "dir/TeSt");
+
+	/* Remove "test" entirely; ensure it is not ignored.
+	 * (As it doesn't exist, it is not a directory.)
+	 */
+	cl_must_pass(p_rmdir("attr/dir/test"));
+
+	assert_is_ignored(false, "dir/test");
+	if (cl_repo_get_bool(g_repo, "core.ignorecase"))
+		assert_is_ignored(false, "dir/TeSt");
+}