diff --git a/src/attr_file.c b/src/attr_file.c index 4eb732436..ea92336f7 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -61,8 +61,7 @@ int git_attr_file__parse_buffer( git_repository *repo, void *parsedata, const char *buffer, git_attr_file *attrs) { int error = 0; - const char *scan = NULL; - char *context = NULL; + const char *scan = NULL, *context = NULL; git_attr_rule *rule = NULL; GIT_UNUSED(parsedata); @@ -72,10 +71,10 @@ int git_attr_file__parse_buffer( scan = buffer; /* if subdir file path, convert context for file paths */ - if (attrs->key && git__suffixcmp(attrs->key, "/" GIT_ATTR_FILE) == 0) { + if (attrs->key && + git_path_root(attrs->key + 2) < 0 && + git__suffixcmp(attrs->key, "/" GIT_ATTR_FILE) == 0) context = attrs->key + 2; - context[strlen(context) - strlen(GIT_ATTR_FILE)] = '\0'; - } while (!error && *scan) { /* allocate rule if needed */ @@ -115,10 +114,6 @@ int git_attr_file__parse_buffer( git_attr_rule__free(rule); - /* restore file path used for context */ - if (context) - context[strlen(context)] = '.'; /* first char of GIT_ATTR_FILE */ - return error; } @@ -414,7 +409,10 @@ int git_attr_fnmatch__parse( if ((spec->flags & GIT_ATTR_FNMATCH_FULLPATH) != 0 && source != NULL && git_path_root(pattern) < 0) { - size_t sourcelen = strlen(source); + /* use context path minus the trailing filename */ + char *slash = strrchr(source, '/'); + size_t sourcelen = slash ? slash - source + 1 : 0; + /* given an unrooted fullpath match from a file inside a repo, * prefix the pattern with the relative directory of the source file */ diff --git a/src/ignore.c b/src/ignore.c index aef3e39b4..5cf4fca5c 100644 --- a/src/ignore.c +++ b/src/ignore.c @@ -14,8 +14,7 @@ static int parse_ignore_file( { int error = 0; git_attr_fnmatch *match = NULL; - const char *scan = NULL; - char *context = NULL; + const char *scan = NULL, *context = NULL; int ignore_case = false; /* Prefer to have the caller pass in a git_ignores as the parsedata @@ -25,10 +24,10 @@ static int parse_ignore_file( else if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0) return error; - if (ignores->key && git__suffixcmp(ignores->key, "/" GIT_IGNORE_FILE) == 0) { + if (ignores->key && + git_path_root(ignores->key + 2) < 0 && + git__suffixcmp(ignores->key, "/" GIT_IGNORE_FILE) == 0) context = ignores->key + 2; - context[strlen(context) - strlen(GIT_IGNORE_FILE)] = '\0'; - } scan = buffer; @@ -64,9 +63,6 @@ static int parse_ignore_file( } git__free(match); - /* restore file path used for context */ - if (context) - context[strlen(context)] = '.'; /* first char of GIT_IGNORE_FILE */ return error; } diff --git a/tests/attr/ignore.c b/tests/attr/ignore.c index 4ed92387c..a83c5bd74 100644 --- a/tests/attr/ignore.c +++ b/tests/attr/ignore.c @@ -130,22 +130,18 @@ void test_attr_ignore__skip_gitignore_directory(void) void test_attr_ignore__expand_tilde_to_homedir(void) { - git_buf path = GIT_BUF_INIT; + git_buf cleanup = GIT_BUF_INIT; git_config *cfg; assert_is_ignored(false, "example.global_with_tilde"); + cl_fake_home(&cleanup); + /* construct fake home with fake global excludes */ - - cl_must_pass(p_mkdir("home", 0777)); - cl_git_pass(git_path_prettify(&path, "home", NULL)); - cl_git_pass(git_libgit2_opts( - GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, path.ptr)); - - cl_git_mkfile("home/globalexcludes", "# found me\n*.global_with_tilde\n"); + cl_git_mkfile("home/globalexclude", "# found me\n*.global_with_tilde\n"); cl_git_pass(git_repository_config(&cfg, g_repo)); - cl_git_pass(git_config_set_string(cfg, "core.excludesfile", "~/globalexcludes")); + cl_git_pass(git_config_set_string(cfg, "core.excludesfile", "~/globalexclude")); git_config_free(cfg); git_attr_cache_flush(g_repo); /* must reset to pick up change */ @@ -154,8 +150,9 @@ void test_attr_ignore__expand_tilde_to_homedir(void) cl_git_pass(git_futils_rmdir_r("home", NULL, GIT_RMDIR_REMOVE_FILES)); - cl_git_pass(git_libgit2_opts( - GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, NULL)); + cl_fake_home_cleanup(&cleanup); - git_buf_free(&path); + git_attr_cache_flush(g_repo); /* must reset to pick up change */ + + assert_is_ignored(false, "example.global_with_tilde"); } diff --git a/tests/clar_libgit2.c b/tests/clar_libgit2.c index 9b062ef78..90e53c5e6 100644 --- a/tests/clar_libgit2.c +++ b/tests/clar_libgit2.c @@ -490,3 +490,24 @@ void clar__assert_equal_file( clar__assert_equal(file, line, "mismatched file length", 1, "%"PRIuZ, (size_t)expected_bytes, (size_t)total_bytes); } + +void cl_fake_home(git_buf *restore) +{ + git_buf path = GIT_BUF_INIT; + + cl_git_pass(git_libgit2_opts( + GIT_OPT_GET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, restore)); + + cl_must_pass(p_mkdir("home", 0777)); + cl_git_pass(git_path_prettify(&path, "home", NULL)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, path.ptr)); + git_buf_free(&path); +} + +void cl_fake_home_cleanup(git_buf *restore) +{ + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, restore->ptr)); + git_buf_free(restore); +} diff --git a/tests/clar_libgit2.h b/tests/clar_libgit2.h index 3de80bfa0..c2489db38 100644 --- a/tests/clar_libgit2.h +++ b/tests/clar_libgit2.h @@ -120,4 +120,7 @@ int cl_repo_get_bool(git_repository *repo, const char *cfg); void cl_repo_set_string(git_repository *repo, const char *cfg, const char *value); +void cl_fake_home(git_buf *restore); +void cl_fake_home_cleanup(git_buf *restore); + #endif diff --git a/tests/status/ignore.c b/tests/status/ignore.c index 1fefcba5f..d6c26a847 100644 --- a/tests/status/ignore.c +++ b/tests/status/ignore.c @@ -364,6 +364,62 @@ void test_status_ignore__subdirectories_not_at_root(void) cl_assert_equal_i(0, counts.wrong_sorted_path); } +void test_status_ignore__leading_slash_ignores(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + git_buf home = GIT_BUF_INIT; + static const char *paths_2[] = { + "dir/.gitignore", + "dir/a/ignore_me", + "dir/b/ignore_me", + "dir/ignore_me", + "ignore_also/file", + "ignore_me", + "test/.gitignore", + "test/ignore_me/and_me/file", + "test/ignore_me/file", + "test/ignore_me/file2", + }; + static const unsigned int statuses_2[] = { + GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, + GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, + }; + + make_test_data(); + + cl_fake_home(&home); + cl_git_mkfile("home/.gitignore", "/ignore_me\n"); + { + git_config *cfg; + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_set_string( + cfg, "core.excludesfile", "~/.gitignore")); + git_config_free(cfg); + } + + cl_git_rewritefile("empty_standard_repo/.git/info/exclude", "/ignore_also\n"); + cl_git_rewritefile("empty_standard_repo/dir/.gitignore", "/ignore_me\n"); + cl_git_rewritefile("empty_standard_repo/test/.gitignore", "/and_me\n"); + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 10; + counts.expected_paths = paths_2; + counts.expected_statuses = statuses_2; + + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_RECURSE_IGNORED_DIRS; + + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__normal, &counts)); + + cl_assert_equal_i(counts.expected_entry_count, counts.entry_count); + cl_assert_equal_i(0, counts.wrong_status_flags_count); + cl_assert_equal_i(0, counts.wrong_sorted_path); + + cl_fake_home_cleanup(&home); +} + void test_status_ignore__adding_internal_ignores(void) { int ignored;