diff --git a/src/attr.c b/src/attr.c index 102d0248c..d43a15f50 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 ef98aacc2..89706865a 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 aa5b87098..27fed2539 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"); +}