diff options
| author | Edward Thomson <ethomson@edwardthomson.com> | 2021-11-16 23:29:22 -0500 |
|---|---|---|
| committer | Edward Thomson <ethomson@edwardthomson.com> | 2022-02-22 22:07:45 -0500 |
| commit | 3344fddc97bbdea9c1b6ebb6f7fb6dbd70b41dfb (patch) | |
| tree | fd6368a72944571c51627b40c592e7d58e0036e1 /tests/libgit2/ignore | |
| parent | 91ba089663f5efc3bd4ba14a5099372cf5ce57a6 (diff) | |
| download | libgit2-3344fddc97bbdea9c1b6ebb6f7fb6dbd70b41dfb.tar.gz | |
refactor: `tests` is now `tests/libgit2`
Like we want to separate libgit2 and utility source code, we want to
separate libgit2 and utility tests. Start by moving all the tests into
libgit2.
Diffstat (limited to 'tests/libgit2/ignore')
| -rw-r--r-- | tests/libgit2/ignore/path.c | 585 | ||||
| -rw-r--r-- | tests/libgit2/ignore/status.c | 1337 |
2 files changed, 1922 insertions, 0 deletions
diff --git a/tests/libgit2/ignore/path.c b/tests/libgit2/ignore/path.c new file mode 100644 index 000000000..a574d1d79 --- /dev/null +++ b/tests/libgit2/ignore/path.c @@ -0,0 +1,585 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "path.h" +#include "futils.h" + +static git_repository *g_repo = NULL; + +void test_ignore_path__initialize(void) +{ + g_repo = cl_git_sandbox_init("attr"); +} + +void test_ignore_path__cleanup(void) +{ + cl_git_sandbox_cleanup(); + g_repo = NULL; +} + +static void assert_is_ignored_( + bool expected, const char *filepath, + const char *file, const char *func, int line) +{ + int is_ignored = 0; + + cl_git_expect( + git_ignore_path_is_ignored(&is_ignored, g_repo, filepath), 0, file, func, line); + + clar__assert_equal( + file, func, line, "expected != is_ignored", 1, "%d", + (int)(expected != 0), (int)(is_ignored != 0)); +} +#define assert_is_ignored(expected, filepath) \ + assert_is_ignored_(expected, filepath, __FILE__, __func__, __LINE__) + +void test_ignore_path__honor_temporary_rules(void) +{ + cl_git_rewritefile("attr/.gitignore", "/NewFolder\n/NewFolder/NewFolder"); + + assert_is_ignored(false, "File.txt"); + assert_is_ignored(true, "NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); +} + +void test_ignore_path__allow_root(void) +{ + cl_git_rewritefile("attr/.gitignore", "/"); + + assert_is_ignored(false, "File.txt"); + assert_is_ignored(false, "NewFolder"); + assert_is_ignored(false, "NewFolder/NewFolder"); + assert_is_ignored(false, "NewFolder/NewFolder/File.txt"); +} + +void test_ignore_path__ignore_space(void) +{ + cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder \n/NewFolder/NewFolder"); + + assert_is_ignored(false, "File.txt"); + assert_is_ignored(true, "NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); +} + +void test_ignore_path__intermittent_space(void) +{ + cl_git_rewritefile("attr/.gitignore", "foo bar\n"); + + assert_is_ignored(false, "foo"); + assert_is_ignored(false, "bar"); + assert_is_ignored(true, "foo bar"); +} + +void test_ignore_path__trailing_space(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "foo \n" + "bar \n" + ); + + assert_is_ignored(true, "foo"); + assert_is_ignored(false, "foo "); + assert_is_ignored(true, "bar"); + assert_is_ignored(false, "bar "); + assert_is_ignored(false, "bar "); +} + +void test_ignore_path__escaped_trailing_spaces(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "foo\\ \n" + "bar\\ \\ \n" + "baz \\ \n" + "qux\\ \n" + ); + + assert_is_ignored(false, "foo"); + assert_is_ignored(true, "foo "); + assert_is_ignored(false, "bar"); + assert_is_ignored(false, "bar "); + assert_is_ignored(true, "bar "); + assert_is_ignored(true, "baz "); + assert_is_ignored(false, "baz "); + assert_is_ignored(true, "qux "); + assert_is_ignored(false, "qux"); + assert_is_ignored(false, "qux "); +} + +void test_ignore_path__ignore_dir(void) +{ + cl_git_rewritefile("attr/.gitignore", "dir/\n"); + + assert_is_ignored(true, "dir"); + assert_is_ignored(true, "dir/file"); +} + +void test_ignore_path__ignore_dir_with_trailing_space(void) +{ + cl_git_rewritefile("attr/.gitignore", "dir/ \n"); + + assert_is_ignored(true, "dir"); + assert_is_ignored(true, "dir/file"); +} + +void test_ignore_path__ignore_root(void) +{ + cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder\n/NewFolder/NewFolder"); + + assert_is_ignored(false, "File.txt"); + assert_is_ignored(true, "NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); +} + +void test_ignore_path__full_paths(void) +{ + cl_git_rewritefile("attr/.gitignore", "Folder/*/Contained"); + + assert_is_ignored(true, "Folder/Middle/Contained"); + assert_is_ignored(false, "Folder/Middle/More/More/Contained"); + + cl_git_rewritefile("attr/.gitignore", "Folder/**/Contained"); + + assert_is_ignored(true, "Folder/Middle/Contained"); + assert_is_ignored(true, "Folder/Middle/More/More/Contained"); + + cl_git_rewritefile("attr/.gitignore", "Folder/**/Contained/*/Child"); + + assert_is_ignored(true, "Folder/Middle/Contained/Happy/Child"); + assert_is_ignored(false, "Folder/Middle/Contained/Not/Happy/Child"); + assert_is_ignored(true, "Folder/Middle/More/More/Contained/Happy/Child"); + assert_is_ignored(false, "Folder/Middle/More/More/Contained/Not/Happy/Child"); +} + +void test_ignore_path__more_starstar_cases(void) +{ + cl_must_pass(p_unlink("attr/.gitignore")); + cl_git_mkfile( + "attr/dir/.gitignore", + "sub/**/*.html\n"); + + assert_is_ignored(false, "aaa.html"); + assert_is_ignored(false, "dir"); + assert_is_ignored(false, "dir/sub"); + assert_is_ignored(true, "dir/sub/sub2/aaa.html"); + assert_is_ignored(true, "dir/sub/aaa.html"); + assert_is_ignored(false, "dir/aaa.html"); + assert_is_ignored(false, "sub"); + assert_is_ignored(false, "sub/aaa.html"); + assert_is_ignored(false, "sub/sub2/aaa.html"); +} + +void test_ignore_path__leading_stars(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "*/onestar\n" + "**/twostars\n" + "*/parent1/kid1/*\n" + "**/parent2/kid2/*\n"); + + assert_is_ignored(true, "dir1/onestar"); + assert_is_ignored(true, "dir1/onestar/child"); /* in ignored dir */ + assert_is_ignored(false, "dir1/dir2/onestar"); + + assert_is_ignored(true, "dir1/twostars"); + assert_is_ignored(true, "dir1/twostars/child"); /* in ignored dir */ + assert_is_ignored(true, "dir1/dir2/twostars"); + assert_is_ignored(true, "dir1/dir2/twostars/child"); /* in ignored dir */ + assert_is_ignored(true, "dir1/dir2/dir3/twostars"); + + assert_is_ignored(true, "dir1/parent1/kid1/file"); + assert_is_ignored(true, "dir1/parent1/kid1/file/inside/parent"); + assert_is_ignored(false, "dir1/dir2/parent1/kid1/file"); + assert_is_ignored(false, "dir1/parent1/file"); + assert_is_ignored(false, "dir1/kid1/file"); + + assert_is_ignored(true, "dir1/parent2/kid2/file"); + assert_is_ignored(true, "dir1/parent2/kid2/file/inside/parent"); + assert_is_ignored(true, "dir1/dir2/parent2/kid2/file"); + assert_is_ignored(true, "dir1/dir2/dir3/parent2/kid2/file"); + assert_is_ignored(false, "dir1/parent2/file"); + assert_is_ignored(false, "dir1/kid2/file"); +} + +void test_ignore_path__globs_and_path_delimiters(void) +{ + cl_git_rewritefile("attr/.gitignore", "foo/bar/**"); + assert_is_ignored(true, "foo/bar/baz"); + assert_is_ignored(true, "foo/bar/baz/quux"); + + cl_git_rewritefile("attr/.gitignore", "_*/"); + assert_is_ignored(true, "sub/_test/a/file"); + assert_is_ignored(false, "test_folder/file"); + assert_is_ignored(true, "_test/file"); + assert_is_ignored(true, "_test/a/file"); + + cl_git_rewritefile("attr/.gitignore", "**/_*/"); + assert_is_ignored(true, "sub/_test/a/file"); + assert_is_ignored(false, "test_folder/file"); + assert_is_ignored(true, "_test/file"); + assert_is_ignored(true, "_test/a/file"); + + cl_git_rewritefile("attr/.gitignore", "**/_*/foo/bar/*ux"); + + assert_is_ignored(true, "sub/_test/foo/bar/qux/file"); + assert_is_ignored(true, "_test/foo/bar/qux/file"); + assert_is_ignored(true, "_test/foo/bar/crux/file"); + assert_is_ignored(false, "_test/foo/bar/code/file"); +} + +void test_ignore_path__globs_without_star(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "*.foo\n" + "**.bar\n" + ); + + assert_is_ignored(true, ".foo"); + assert_is_ignored(true, "xyz.foo"); + assert_is_ignored(true, ".bar"); + assert_is_ignored(true, "x.bar"); + assert_is_ignored(true, "xyz.bar"); + + assert_is_ignored(true, "test/.foo"); + assert_is_ignored(true, "test/x.foo"); + assert_is_ignored(true, "test/xyz.foo"); + assert_is_ignored(true, "test/.bar"); + assert_is_ignored(true, "test/x.bar"); + assert_is_ignored(true, "test/xyz.bar"); +} + +void test_ignore_path__skip_gitignore_directory(void) +{ + cl_git_rewritefile("attr/.git/info/exclude", "/NewFolder\n/NewFolder/NewFolder"); + cl_must_pass(p_unlink("attr/.gitignore")); + cl_assert(!git_fs_path_exists("attr/.gitignore")); + p_mkdir("attr/.gitignore", 0777); + cl_git_mkfile("attr/.gitignore/garbage.txt", "new_file\n"); + + assert_is_ignored(false, "File.txt"); + assert_is_ignored(true, "NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder"); + assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); +} + +void test_ignore_path__subdirectory_gitignore(void) +{ + cl_must_pass(p_unlink("attr/.gitignore")); + cl_assert(!git_fs_path_exists("attr/.gitignore")); + cl_git_mkfile( + "attr/.gitignore", + "file1\n"); + cl_git_mkfile( + "attr/dir/.gitignore", + "file2/\n"); + + assert_is_ignored(true, "file1"); + assert_is_ignored(true, "dir/file1"); + assert_is_ignored(true, "dir/file2/actual_file"); /* in ignored dir */ + assert_is_ignored(false, "dir/file3"); +} + +void test_ignore_path__expand_tilde_to_homedir(void) +{ + git_config *cfg; + + assert_is_ignored(false, "example.global_with_tilde"); + + cl_fake_home(); + + /* construct fake home with fake global excludes */ + 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", "~/globalexclude")); + git_config_free(cfg); + + git_attr_cache_flush(g_repo); /* must reset to pick up change */ + + assert_is_ignored(true, "example.global_with_tilde"); + + cl_git_pass(git_futils_rmdir_r("home", NULL, GIT_RMDIR_REMOVE_FILES)); + + cl_fake_home_cleanup(NULL); + + git_attr_cache_flush(g_repo); /* must reset to pick up change */ + + assert_is_ignored(false, "example.global_with_tilde"); +} + +/* Ensure that the .gitignore in the subdirectory only affects + * items in the subdirectory. */ +void test_ignore_path__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_ignore_path__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"); +} + +void test_ignore_path__symlink_to_outside(void) +{ +#ifdef GIT_WIN32 + cl_skip(); +#endif + + cl_git_rewritefile("attr/.gitignore", "symlink\n"); + cl_git_mkfile("target", "target"); + cl_git_pass(p_symlink("../target", "attr/symlink")); + assert_is_ignored(true, "symlink"); + assert_is_ignored(true, "lala/../symlink"); +} + +void test_ignore_path__test(void) +{ + cl_git_rewritefile("attr/.gitignore", + "/*/\n" + "!/src\n"); + assert_is_ignored(false, "src/foo.c"); + assert_is_ignored(false, "src/foo/foo.c"); + assert_is_ignored(false, "README.md"); + assert_is_ignored(true, "dist/foo.o"); + assert_is_ignored(true, "bin/foo"); +} + +void test_ignore_path__unignore_dir_succeeds(void) +{ + cl_git_rewritefile("attr/.gitignore", + "*.c\n" + "!src/*.c\n"); + assert_is_ignored(false, "src/foo.c"); + assert_is_ignored(true, "src/foo/foo.c"); +} + +void test_ignore_path__case_insensitive_unignores_previous_rule(void) +{ + git_config *cfg; + + cl_git_rewritefile("attr/.gitignore", + "/case\n" + "!/Case/\n"); + + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", true)); + + cl_must_pass(p_mkdir("attr/case", 0755)); + cl_git_mkfile("attr/case/file", "content"); + + assert_is_ignored(false, "case/file"); +} + +void test_ignore_path__case_sensitive_unignore_does_nothing(void) +{ + git_config *cfg; + + cl_git_rewritefile("attr/.gitignore", + "/case\n" + "!/Case/\n"); + + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", false)); + + cl_must_pass(p_mkdir("attr/case", 0755)); + cl_git_mkfile("attr/case/file", "content"); + + assert_is_ignored(true, "case/file"); +} + +void test_ignore_path__ignored_subdirfiles_with_subdir_rule(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "dir/*\n" + "!dir/sub1/sub2/**\n"); + + assert_is_ignored(true, "dir/a.test"); + assert_is_ignored(true, "dir/sub1/a.test"); + assert_is_ignored(true, "dir/sub1/sub2"); +} + +void test_ignore_path__ignored_subdirfiles_with_negations(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "dir/*\n" + "!dir/a.test\n"); + + assert_is_ignored(false, "dir/a.test"); + assert_is_ignored(true, "dir/b.test"); + assert_is_ignored(true, "dir/sub1/c.test"); +} + +void test_ignore_path__negative_directory_rules_only_match_directories(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "*\n" + "!/**/\n" + "!*.keep\n" + "!.gitignore\n" + ); + + assert_is_ignored(true, "src"); + assert_is_ignored(true, "src/A"); + assert_is_ignored(false, "src/"); + assert_is_ignored(false, "src/A.keep"); + assert_is_ignored(false, ".gitignore"); +} + +void test_ignore_path__escaped_character(void) +{ + cl_git_rewritefile("attr/.gitignore", "\\c\n"); + assert_is_ignored(true, "c"); + assert_is_ignored(false, "\\c"); +} + +void test_ignore_path__escaped_newline(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "\\\nnewline\n" + ); + + assert_is_ignored(true, "\nnewline"); +} + +void test_ignore_path__escaped_glob(void) +{ + cl_git_rewritefile("attr/.gitignore", "\\*\n"); + assert_is_ignored(true, "*"); + assert_is_ignored(false, "foo"); +} + +void test_ignore_path__escaped_comments(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "#foo\n" + "\\#bar\n" + "\\##baz\n" + "\\#\\\\#qux\n" + ); + + assert_is_ignored(false, "#foo"); + assert_is_ignored(true, "#bar"); + assert_is_ignored(false, "\\#bar"); + assert_is_ignored(true, "##baz"); + assert_is_ignored(false, "\\##baz"); + assert_is_ignored(true, "#\\#qux"); + assert_is_ignored(false, "##qux"); + assert_is_ignored(false, "\\##qux"); +} + +void test_ignore_path__escaped_slash(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "\\\\\n" + "\\\\preceding\n" + "inter\\\\mittent\n" + "trailing\\\\\n" + ); + +#ifndef GIT_WIN32 + assert_is_ignored(true, "\\"); + assert_is_ignored(true, "\\preceding"); +#endif + assert_is_ignored(true, "inter\\mittent"); + assert_is_ignored(true, "trailing\\"); +} + +void test_ignore_path__escaped_space(void) +{ + cl_git_rewritefile( + "attr/.gitignore", + "foo\\\\ \n" + "bar\\\\\\ \n"); + assert_is_ignored(true, "foo\\"); + assert_is_ignored(false, "foo\\ "); + assert_is_ignored(false, "foo\\\\ "); + assert_is_ignored(false, "foo\\\\"); + assert_is_ignored(true, "bar\\ "); + assert_is_ignored(false, "bar\\\\"); + assert_is_ignored(false, "bar\\\\ "); + assert_is_ignored(false, "bar\\\\\\"); + assert_is_ignored(false, "bar\\\\\\ "); +} + +void test_ignore_path__invalid_pattern(void) +{ + cl_git_rewritefile("attr/.gitignore", "["); + assert_is_ignored(false, "[f"); + assert_is_ignored(false, "f"); +} + +void test_ignore_path__negative_prefix_rule(void) +{ + cl_git_rewritefile("attr/.gitignore", "ff*\n!f\n"); + assert_is_ignored(true, "fff"); + assert_is_ignored(true, "ff"); + assert_is_ignored(false, "f"); +} + +void test_ignore_path__negative_more_specific(void) +{ + cl_git_rewritefile("attr/.gitignore", "*.txt\n!/dir/test.txt\n"); + assert_is_ignored(true, "test.txt"); + assert_is_ignored(false, "dir/test.txt"); + assert_is_ignored(true, "outer/dir/test.txt"); +} diff --git a/tests/libgit2/ignore/status.c b/tests/libgit2/ignore/status.c new file mode 100644 index 000000000..deb717590 --- /dev/null +++ b/tests/libgit2/ignore/status.c @@ -0,0 +1,1337 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "git2/attr.h" +#include "ignore.h" +#include "attr.h" +#include "status/status_helpers.h" + +static git_repository *g_repo = NULL; + +void test_ignore_status__initialize(void) +{ +} + +void test_ignore_status__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static void assert_ignored_( + bool expected, const char *filepath, + const char *file, const char *func, int line) +{ + int is_ignored = 0; + cl_git_expect( + git_status_should_ignore(&is_ignored, g_repo, filepath), 0, file, func, line); + clar__assert( + (expected != 0) == (is_ignored != 0), + file, func, line, "expected != is_ignored", filepath, 1); +} +#define assert_ignored(expected, filepath) \ + assert_ignored_(expected, filepath, __FILE__, __func__, __LINE__) +#define assert_is_ignored(filepath) \ + assert_ignored_(true, filepath, __FILE__, __func__, __LINE__) +#define refute_is_ignored(filepath) \ + assert_ignored_(false, filepath, __FILE__, __func__, __LINE__) + +void test_ignore_status__0(void) +{ + struct { + const char *path; + int expected; + } test_cases[] = { + /* pattern "ign" from .gitignore */ + { "file", 0 }, + { "ign", 1 }, + { "sub", 0 }, + { "sub/file", 0 }, + { "sub/ign", 1 }, + { "sub/ign/file", 1 }, + { "sub/ign/sub", 1 }, + { "sub/ign/sub/file", 1 }, + { "sub/sub", 0 }, + { "sub/sub/file", 0 }, + { "sub/sub/ign", 1 }, + { "sub/sub/sub", 0 }, + /* pattern "dir/" from .gitignore */ + { "dir", 1 }, + { "dir/", 1 }, + { "sub/dir", 1 }, + { "sub/dir/", 1 }, + { "sub/dir/file", 1 }, /* contained in ignored parent */ + { "sub/sub/dir", 0 }, /* dir is not actually a dir, but a file */ + { NULL, 0 } + }, *one_test; + + g_repo = cl_git_sandbox_init("attr"); + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_ignored(one_test->expected, one_test->path); + + /* confirm that ignore files were cached */ + cl_assert(git_attr_cache__is_cached( + g_repo, GIT_ATTR_FILE_SOURCE_FILE, ".git/info/exclude")); + cl_assert(git_attr_cache__is_cached( + g_repo, GIT_ATTR_FILE_SOURCE_FILE, ".gitignore")); +} + + +void test_ignore_status__1(void) +{ + g_repo = cl_git_sandbox_init("attr"); + + cl_git_rewritefile("attr/.gitignore", "/*.txt\n/dir/\n"); + git_attr_cache_flush(g_repo); + + assert_is_ignored("root_test4.txt"); + refute_is_ignored("sub/subdir_test2.txt"); + assert_is_ignored("dir"); + assert_is_ignored("dir/"); + refute_is_ignored("sub/dir"); + refute_is_ignored("sub/dir/"); +} + +void test_ignore_status__empty_repo_with_gitignore_rewrite(void) +{ + status_entry_single st; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_mkfile( + "empty_standard_repo/look-ma.txt", "I'm going to be ignored!"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert(st.count == 1); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&st.status, g_repo, "look-ma.txt")); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + refute_is_ignored("look-ma.txt"); + + cl_git_rewritefile("empty_standard_repo/.gitignore", "*.nomatch\n"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert(st.count == 2); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&st.status, g_repo, "look-ma.txt")); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + refute_is_ignored("look-ma.txt"); + + cl_git_rewritefile("empty_standard_repo/.gitignore", "*.txt\n"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert(st.count == 2); + cl_assert(st.status == GIT_STATUS_IGNORED); + + cl_git_pass(git_status_file(&st.status, g_repo, "look-ma.txt")); + cl_assert(st.status == GIT_STATUS_IGNORED); + + assert_is_ignored("look-ma.txt"); +} + +void test_ignore_status__ignore_pattern_contains_space(void) +{ + unsigned int flags; + const mode_t mode = 0777; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + cl_git_rewritefile("empty_standard_repo/.gitignore", "foo bar.txt\n"); + + cl_git_mkfile( + "empty_standard_repo/foo bar.txt", "I'm going to be ignored!"); + + cl_git_pass(git_status_file(&flags, g_repo, "foo bar.txt")); + cl_assert(flags == GIT_STATUS_IGNORED); + + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/foo", mode)); + cl_git_mkfile("empty_standard_repo/foo/look-ma.txt", "I'm not going to be ignored!"); + + cl_git_pass(git_status_file(&flags, g_repo, "foo/look-ma.txt")); + cl_assert(flags == GIT_STATUS_WT_NEW); +} + +void test_ignore_status__ignore_pattern_ignorecase(void) +{ + unsigned int flags; + bool ignore_case; + git_index *index; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + cl_git_rewritefile("empty_standard_repo/.gitignore", "a.txt\n"); + + cl_git_mkfile("empty_standard_repo/A.txt", "Differs in case"); + + cl_git_pass(git_repository_index(&index, g_repo)); + ignore_case = (git_index_caps(index) & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0; + git_index_free(index); + + cl_git_pass(git_status_file(&flags, g_repo, "A.txt")); + cl_assert(flags == ignore_case ? GIT_STATUS_IGNORED : GIT_STATUS_WT_NEW); +} + +void test_ignore_status__subdirectories(void) +{ + status_entry_single st; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_mkfile( + "empty_standard_repo/ignore_me", "I'm going to be ignored!"); + + cl_git_rewritefile("empty_standard_repo/.gitignore", "ignore_me\n"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(2, st.count); + cl_assert(st.status == GIT_STATUS_IGNORED); + + cl_git_pass(git_status_file(&st.status, g_repo, "ignore_me")); + cl_assert(st.status == GIT_STATUS_IGNORED); + + assert_is_ignored("ignore_me"); + + /* I've changed libgit2 so that the behavior here now differs from + * core git but seems to make more sense. In core git, the following + * items are skipped completed, even if --ignored is passed to status. + * It you mirror these steps and run "git status -uall --ignored" then + * you will not see "test/ignore_me/" in the results. + * + * However, we had a couple reports of this as a bug, plus there is a + * similar circumstance where we were differing for core git when you + * used a rooted path for an ignore, so I changed this behavior. + */ + cl_git_pass(git_futils_mkdir_r( + "empty_standard_repo/test/ignore_me", 0775)); + cl_git_mkfile( + "empty_standard_repo/test/ignore_me/file", "I'm going to be ignored!"); + cl_git_mkfile( + "empty_standard_repo/test/ignore_me/file2", "Me, too!"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(3, st.count); + + cl_git_pass(git_status_file(&st.status, g_repo, "test/ignore_me/file")); + cl_assert(st.status == GIT_STATUS_IGNORED); + + assert_is_ignored("test/ignore_me/file"); +} + +static void make_test_data(const char *reponame, const char **files) +{ + const char **scan; + size_t repolen = strlen(reponame) + 1; + + g_repo = cl_git_sandbox_init(reponame); + + for (scan = files; *scan != NULL; ++scan) { + cl_git_pass(git_futils_mkdir_relative( + *scan + repolen, reponame, + 0777, GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST, NULL)); + cl_git_mkfile(*scan, "contents"); + } +} + +static const char *test_repo_1 = "empty_standard_repo"; +static const char *test_files_1[] = { + "empty_standard_repo/dir/a/ignore_me", + "empty_standard_repo/dir/b/ignore_me", + "empty_standard_repo/dir/ignore_me", + "empty_standard_repo/ignore_also/file", + "empty_standard_repo/ignore_me", + "empty_standard_repo/test/ignore_me/file", + "empty_standard_repo/test/ignore_me/file2", + "empty_standard_repo/test/ignore_me/and_me/file", + NULL +}; + +void test_ignore_status__subdirectories_recursion(void) +{ + /* Let's try again with recursing into ignored dirs turned on */ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *paths_r[] = { + ".gitignore", + "dir/a/ignore_me", + "dir/b/ignore_me", + "dir/ignore_me", + "ignore_also/file", + "ignore_me", + "test/ignore_me/and_me/file", + "test/ignore_me/file", + "test/ignore_me/file2", + }; + static const unsigned int statuses_r[] = { + GIT_STATUS_WT_NEW, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + }; + static const char *paths_nr[] = { + ".gitignore", + "dir/a/ignore_me", + "dir/b/ignore_me", + "dir/ignore_me", + "ignore_also/", + "ignore_me", + "test/ignore_me/", + }; + static const unsigned int statuses_nr[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, GIT_STATUS_IGNORED, + }; + + make_test_data(test_repo_1, test_files_1); + cl_git_rewritefile("empty_standard_repo/.gitignore", "ignore_me\n/ignore_also\n"); + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 9; + counts.expected_paths = paths_r; + counts.expected_statuses = statuses_r; + + 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); + + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 7; + counts.expected_paths = paths_nr; + counts.expected_statuses = statuses_nr; + + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + 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); +} + +void test_ignore_status__subdirectories_not_at_root(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *paths_1[] = { + "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_1[] = { + 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_IGNORED, GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, + }; + + make_test_data(test_repo_1, test_files_1); + cl_git_rewritefile("empty_standard_repo/dir/.gitignore", "ignore_me\n/ignore_also\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_1; + counts.expected_statuses = statuses_1; + + 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); +} + +void test_ignore_status__leading_slash_ignores(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + 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(test_repo_1, test_files_1); + + cl_fake_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); +} + +void test_ignore_status__multiple_leading_slash(void) +{ + static const char *test_files[] = { + "empty_standard_repo/a.test", + "empty_standard_repo/b.test", + "empty_standard_repo/c.test", + "empty_standard_repo/d.test", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "a.test\n" + "/b.test\n" + "//c.test\n" + "///d.test\n"); + + assert_is_ignored("a.test"); + assert_is_ignored("b.test"); + refute_is_ignored("c.test"); + refute_is_ignored("d.test"); +} + +void test_ignore_status__contained_dir_with_matching_name(void) +{ + static const char *test_files[] = { + "empty_standard_repo/subdir_match/aaa/subdir_match/file", + "empty_standard_repo/subdir_match/zzz_ignoreme", + NULL + }; + static const char *expected_paths[] = { + "subdir_match/.gitignore", + "subdir_match/aaa/subdir_match/file", + "subdir_match/zzz_ignoreme", + }; + static const unsigned int expected_statuses[] = { + GIT_STATUS_WT_NEW, GIT_STATUS_WT_NEW, GIT_STATUS_IGNORED + }; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/subdir_match/.gitignore", "*_ignoreme\n"); + + refute_is_ignored("subdir_match/aaa/subdir_match/file"); + assert_is_ignored("subdir_match/zzz_ignoreme"); + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 3; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + 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); +} + +void test_ignore_status__trailing_slash_star(void) +{ + static const char *test_files[] = { + "empty_standard_repo/file", + "empty_standard_repo/subdir/file", + "empty_standard_repo/subdir/sub2/sub3/file", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/subdir/.gitignore", "/**/*\n"); + + refute_is_ignored("file"); + assert_is_ignored("subdir/sub2/sub3/file"); + assert_is_ignored("subdir/file"); +} + +void test_ignore_status__adding_internal_ignores(void) +{ + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + refute_is_ignored("one.txt"); + refute_is_ignored("two.bar"); + + cl_git_pass(git_ignore_add_rule(g_repo, "*.nomatch\n")); + + refute_is_ignored("one.txt"); + refute_is_ignored("two.bar"); + + cl_git_pass(git_ignore_add_rule(g_repo, "*.txt\n")); + + assert_is_ignored("one.txt"); + refute_is_ignored("two.bar"); + + cl_git_pass(git_ignore_add_rule(g_repo, "*.bar\n")); + + assert_is_ignored("one.txt"); + assert_is_ignored("two.bar"); + + cl_git_pass(git_ignore_clear_internal_rules(g_repo)); + + refute_is_ignored("one.txt"); + refute_is_ignored("two.bar"); + + cl_git_pass(git_ignore_add_rule( + g_repo, "multiple\n*.rules\n# comment line\n*.bar\n")); + + refute_is_ignored("one.txt"); + assert_is_ignored("two.bar"); +} + +void test_ignore_status__add_internal_as_first_thing(void) +{ + const char *add_me = "\n#################\n## Eclipse\n#################\n\n*.pydevproject\n.project\n.metadata\nbin/\ntmp/\n*.tmp\n\n"; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_ignore_add_rule(g_repo, add_me)); + + assert_is_ignored("one.tmp"); + refute_is_ignored("two.bar"); +} + +void test_ignore_status__internal_ignores_inside_deep_paths(void) +{ + const char *add_me = "Debug\nthis/is/deep\npatterned*/dir\n"; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_ignore_add_rule(g_repo, add_me)); + + assert_is_ignored("Debug"); + assert_is_ignored("and/Debug"); + assert_is_ignored("really/Debug/this/file"); + assert_is_ignored("Debug/what/I/say"); + + refute_is_ignored("and/NoDebug"); + refute_is_ignored("NoDebug/this"); + refute_is_ignored("please/NoDebug/this"); + + assert_is_ignored("this/is/deep"); + /* pattern containing slash gets FNM_PATHNAME so all slashes must match */ + refute_is_ignored("and/this/is/deep"); + assert_is_ignored("this/is/deep/too"); + /* pattern containing slash gets FNM_PATHNAME so all slashes must match */ + refute_is_ignored("but/this/is/deep/and/ignored"); + + refute_is_ignored("this/is/not/deep"); + refute_is_ignored("is/this/not/as/deep"); + refute_is_ignored("this/is/deepish"); + refute_is_ignored("xthis/is/deep"); +} + +void test_ignore_status__automatically_ignore_bad_files(void) +{ + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + assert_is_ignored(".git"); + assert_is_ignored("this/file/."); + assert_is_ignored("path/../funky"); + refute_is_ignored("path/whatever.c"); + + cl_git_pass(git_ignore_add_rule(g_repo, "*.c\n")); + + assert_is_ignored(".git"); + assert_is_ignored("this/file/."); + assert_is_ignored("path/../funky"); + assert_is_ignored("path/whatever.c"); + + cl_git_pass(git_ignore_clear_internal_rules(g_repo)); + + assert_is_ignored(".git"); + assert_is_ignored("this/file/."); + assert_is_ignored("path/../funky"); + refute_is_ignored("path/whatever.c"); +} + +void test_ignore_status__filenames_with_special_prefixes_do_not_interfere_with_status_retrieval(void) +{ + status_entry_single st; + char *test_cases[] = { + "!file", + "#blah", + "[blah]", + "[attr]", + "[attr]blah", + NULL + }; + int i; + + for (i = 0; *(test_cases + i) != NULL; i++) { + git_str file = GIT_STR_INIT; + char *file_name = *(test_cases + i); + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_str_joinpath(&file, "empty_standard_repo", file_name)); + cl_git_mkfile(git_str_cstr(&file), "Please don't ignore me!"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &st)); + cl_assert(st.count == 1); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&st.status, repo, file_name)); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + cl_git_sandbox_cleanup(); + git_str_dispose(&file); + } +} + +void test_ignore_status__issue_1766_negated_ignores(void) +{ + unsigned int status; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_futils_mkdir_r( + "empty_standard_repo/a", 0775)); + cl_git_mkfile( + "empty_standard_repo/a/.gitignore", "*\n!.gitignore\n"); + cl_git_mkfile( + "empty_standard_repo/a/ignoreme", "I should be ignored\n"); + + refute_is_ignored("a/.gitignore"); + assert_is_ignored("a/ignoreme"); + + cl_git_pass(git_futils_mkdir_r( + "empty_standard_repo/b", 0775)); + cl_git_mkfile( + "empty_standard_repo/b/.gitignore", "*\n!.gitignore\n"); + cl_git_mkfile( + "empty_standard_repo/b/ignoreme", "I should be ignored\n"); + + refute_is_ignored("b/.gitignore"); + assert_is_ignored("b/ignoreme"); + + /* shouldn't have changed results from first couple either */ + refute_is_ignored("a/.gitignore"); + assert_is_ignored("a/ignoreme"); + + /* status should find the two ignore files and nothing else */ + + cl_git_pass(git_status_file(&status, g_repo, "a/.gitignore")); + cl_assert_equal_i(GIT_STATUS_WT_NEW, (int)status); + + cl_git_pass(git_status_file(&status, g_repo, "a/ignoreme")); + cl_assert_equal_i(GIT_STATUS_IGNORED, (int)status); + + cl_git_pass(git_status_file(&status, g_repo, "b/.gitignore")); + cl_assert_equal_i(GIT_STATUS_WT_NEW, (int)status); + + cl_git_pass(git_status_file(&status, g_repo, "b/ignoreme")); + cl_assert_equal_i(GIT_STATUS_IGNORED, (int)status); + + { + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *paths[] = { + "a/.gitignore", + "a/ignoreme", + "b/.gitignore", + "b/ignoreme", + }; + static const unsigned int statuses[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_NEW, + GIT_STATUS_IGNORED, + }; + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 4; + counts.expected_paths = paths; + counts.expected_statuses = statuses; + + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + 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); + } +} + +static void add_one_to_index(const char *file) +{ + git_index *index; + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_add_bypath(index, file)); + git_index_free(index); +} + +/* Some further broken scenarios that have been reported */ +void test_ignore_status__more_breakage(void) +{ + static const char *test_files[] = { + "empty_standard_repo/d1/pfx-d2/d3/d4/d5/tracked", + "empty_standard_repo/d1/pfx-d2/d3/d4/d5/untracked", + "empty_standard_repo/d1/pfx-d2/d3/d4/untracked", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "/d1/pfx-*\n" + "!/d1/pfx-d2/\n" + "/d1/pfx-d2/*\n" + "!/d1/pfx-d2/d3/\n" + "/d1/pfx-d2/d3/*\n" + "!/d1/pfx-d2/d3/d4/\n"); + add_one_to_index("d1/pfx-d2/d3/d4/d5/tracked"); + + { + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *files[] = { + ".gitignore", + "d1/pfx-d2/d3/d4/d5/tracked", + "d1/pfx-d2/d3/d4/d5/untracked", + "d1/pfx-d2/d3/d4/untracked", + }; + static const unsigned int statuses[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + }; + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 4; + counts.expected_paths = files; + counts.expected_statuses = statuses; + opts.flags = GIT_STATUS_OPT_DEFAULTS | + GIT_STATUS_OPT_INCLUDE_IGNORED | + 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); + } + + refute_is_ignored("d1/pfx-d2/d3/d4/d5/tracked"); + refute_is_ignored("d1/pfx-d2/d3/d4/d5/untracked"); + refute_is_ignored("d1/pfx-d2/d3/d4/untracked"); +} + +void test_ignore_status__negative_ignores_inside_ignores(void) +{ + static const char *test_files[] = { + "empty_standard_repo/top/mid/btm/tracked", + "empty_standard_repo/top/mid/btm/untracked", + "empty_standard_repo/zoo/bar", + "empty_standard_repo/zoo/foo/bar", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "top\n" + "!top/mid/btm\n" + "zoo/*\n" + "!zoo/bar\n" + "!zoo/foo/bar\n"); + add_one_to_index("top/mid/btm/tracked"); + + { + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *files[] = { + ".gitignore", "top/mid/btm/tracked", "top/mid/btm/untracked", + "zoo/bar", "zoo/foo/bar", + }; + static const unsigned int statuses[] = { + GIT_STATUS_WT_NEW, GIT_STATUS_INDEX_NEW, GIT_STATUS_IGNORED, + GIT_STATUS_WT_NEW, GIT_STATUS_IGNORED, + }; + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = 5; + counts.expected_paths = files; + counts.expected_statuses = statuses; + opts.flags = GIT_STATUS_OPT_DEFAULTS | + GIT_STATUS_OPT_INCLUDE_IGNORED | + 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); + } + + assert_is_ignored("top/mid/btm/tracked"); + assert_is_ignored("top/mid/btm/untracked"); + refute_is_ignored("foo/bar"); +} + +void test_ignore_status__negative_ignores_in_slash_star(void) +{ + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *list; + int found_look_ma = 0, found_what_about = 0; + size_t i; + static const char *test_files[] = { + "empty_standard_repo/bin/look-ma.txt", + "empty_standard_repo/bin/what-about-me.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "bin/*\n" + "!bin/w*\n"); + + assert_is_ignored("bin/look-ma.txt"); + refute_is_ignored("bin/what-about-me.txt"); + + status_opts.flags = GIT_STATUS_OPT_DEFAULTS; + cl_git_pass(git_status_list_new(&list, g_repo, &status_opts)); + for (i = 0; i < git_status_list_entrycount(list); i++) { + const git_status_entry *entry = git_status_byindex(list, i); + + if (!strcmp("bin/look-ma.txt", entry->index_to_workdir->new_file.path)) + found_look_ma = 1; + + if (!strcmp("bin/what-about-me.txt", entry->index_to_workdir->new_file.path)) + found_what_about = 1; + } + git_status_list_free(list); + + cl_assert(found_look_ma); + cl_assert(found_what_about); +} + +void test_ignore_status__negative_ignores_without_trailing_slash_inside_ignores(void) +{ + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *list; + int found_parent_file = 0, found_parent_child1_file = 0, found_parent_child2_file = 0; + size_t i; + static const char *test_files[] = { + "empty_standard_repo/parent/file.txt", + "empty_standard_repo/parent/force.txt", + "empty_standard_repo/parent/child1/file.txt", + "empty_standard_repo/parent/child2/file.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "parent/*\n" + "!parent/force.txt\n" + "!parent/child1\n" + "!parent/child2/\n"); + + add_one_to_index("parent/force.txt"); + + assert_is_ignored("parent/file.txt"); + refute_is_ignored("parent/force.txt"); + refute_is_ignored("parent/child1/file.txt"); + refute_is_ignored("parent/child2/file.txt"); + + status_opts.flags = GIT_STATUS_OPT_DEFAULTS; + cl_git_pass(git_status_list_new(&list, g_repo, &status_opts)); + for (i = 0; i < git_status_list_entrycount(list); i++) { + const git_status_entry *entry = git_status_byindex(list, i); + + if (!entry->index_to_workdir) + continue; + + if (!strcmp("parent/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_file = 1; + + if (!strcmp("parent/force.txt", entry->index_to_workdir->new_file.path)) + found_parent_file = 1; + + if (!strcmp("parent/child1/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_child1_file = 1; + + if (!strcmp("parent/child2/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_child2_file = 1; + } + git_status_list_free(list); + + cl_assert(found_parent_file); + cl_assert(found_parent_child1_file); + cl_assert(found_parent_child2_file); +} + +void test_ignore_status__negative_directory_ignores(void) +{ + static const char *test_files[] = { + "empty_standard_repo/parent/child1/bar.txt", + "empty_standard_repo/parent/child2/bar.txt", + "empty_standard_repo/parent/child3/foo.txt", + "empty_standard_repo/parent/child4/bar.txt", + "empty_standard_repo/parent/nested/child5/bar.txt", + "empty_standard_repo/parent/nested/child6/bar.txt", + "empty_standard_repo/parent/nested/child7/bar.txt", + "empty_standard_repo/padded_parent/child8/bar.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "foo.txt\n" + "parent/child1\n" + "parent/child2\n" + "parent/child4\n" + "parent/nested/child5\n" + "nested/child6\n" + "nested/child7\n" + "padded_parent/child8\n" + /* test simple exact match */ + "!parent/child1\n" + /* test negating file without negating dir */ + "!parent/child2/bar.txt\n" + /* test negative pattern on dir with its content + * being ignored */ + "!parent/child3\n" + /* test with partial match at end */ + "!child4\n" + /* test with partial match with '/' at end */ + "!nested/child5\n" + /* test with complete match */ + "!nested/child6\n" + /* test with trailing '/' */ + "!child7/\n" + /* test with partial dir match */ + "!_parent/child8\n"); + + refute_is_ignored("parent/child1/bar.txt"); + assert_is_ignored("parent/child2/bar.txt"); + assert_is_ignored("parent/child3/foo.txt"); + refute_is_ignored("parent/child4/bar.txt"); + assert_is_ignored("parent/nested/child5/bar.txt"); + refute_is_ignored("parent/nested/child6/bar.txt"); + refute_is_ignored("parent/nested/child7/bar.txt"); + assert_is_ignored("padded_parent/child8/bar.txt"); +} + +void test_ignore_status__unignore_entry_in_ignored_dir(void) +{ + static const char *test_files[] = { + "empty_standard_repo/bar.txt", + "empty_standard_repo/parent/bar.txt", + "empty_standard_repo/parent/child/bar.txt", + "empty_standard_repo/nested/parent/child/bar.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "bar.txt\n" + "!parent/child/bar.txt\n"); + + assert_is_ignored("bar.txt"); + assert_is_ignored("parent/bar.txt"); + refute_is_ignored("parent/child/bar.txt"); + assert_is_ignored("nested/parent/child/bar.txt"); +} + +void test_ignore_status__do_not_unignore_basename_prefix(void) +{ + static const char *test_files[] = { + "empty_standard_repo/foo_bar.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "foo_bar.txt\n" + "!bar.txt\n"); + + assert_is_ignored("foo_bar.txt"); +} + +void test_ignore_status__filename_with_cr(void) +{ + int ignored; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + cl_git_mkfile("empty_standard_repo/.gitignore", "Icon\r\r\n"); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Icon\r")); + cl_assert_equal_i(1, ignored); + + cl_git_mkfile("empty_standard_repo/.gitignore", "Ico\rn\n"); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Ico\rn")); + cl_assert_equal_i(1, ignored); + + cl_git_mkfile("empty_standard_repo/.gitignore", "Ico\rn\r\n"); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Ico\rn")); + cl_assert_equal_i(1, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Ico\rn\r")); + cl_assert_equal_i(0, ignored); + + cl_git_mkfile("empty_standard_repo/.gitignore", "Ico\rn\r\r\n"); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Ico\rn\r")); + cl_assert_equal_i(1, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Icon\r")); + cl_assert_equal_i(0, ignored); + + cl_git_mkfile("empty_standard_repo/.gitignore", "Icon\r\n"); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Icon\r")); + cl_assert_equal_i(0, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "Icon")); + cl_assert_equal_i(1, ignored); +} + +void test_ignore_status__subdir_doesnt_match_above(void) +{ + int ignored, icase = 0, error; + git_config *cfg; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_repository_config_snapshot(&cfg, g_repo)); + error = git_config_get_bool(&icase, cfg, "core.ignorecase"); + git_config_free(cfg); + if (error == GIT_ENOTFOUND) + error = 0; + + cl_git_pass(error); + + cl_git_pass(p_mkdir("empty_standard_repo/src", 0777)); + cl_git_pass(p_mkdir("empty_standard_repo/src/src", 0777)); + cl_git_mkfile("empty_standard_repo/src/.gitignore", "src\n"); + cl_git_mkfile("empty_standard_repo/.gitignore", ""); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "src/test.txt")); + cl_assert_equal_i(0, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "src/src/test.txt")); + cl_assert_equal_i(1, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "src/foo/test.txt")); + cl_assert_equal_i(0, ignored); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "SRC/src/test.txt")); + cl_assert_equal_i(icase, ignored); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "src/SRC/test.txt")); + cl_assert_equal_i(icase, ignored); +} + +void test_ignore_status__negate_exact_previous(void) +{ + int ignored; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_mkfile("empty_standard_repo/.gitignore", "*.com\ntags\n!tags/\n.buildpath"); + cl_git_mkfile("empty_standard_repo/.buildpath", ""); + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, ".buildpath")); + cl_assert_equal_i(1, ignored); +} + +void test_ignore_status__negate_starstar(void) +{ + int ignored; + + g_repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_mkfile("empty_standard_repo/.gitignore", + "code/projects/**/packages/*\n" + "!code/projects/**/packages/repositories.config"); + + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/code/projects/foo/bar/packages", 0777)); + cl_git_mkfile("empty_standard_repo/code/projects/foo/bar/packages/repositories.config", ""); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "code/projects/foo/bar/packages/repositories.config")); + cl_assert_equal_i(0, ignored); +} + +void test_ignore_status__ignore_all_toplevel_dirs_include_files(void) +{ + static const char *test_files[] = { + "empty_standard_repo/README.md", + "empty_standard_repo/src/main.c", + "empty_standard_repo/src/foo/foo.c", + "empty_standard_repo/dist/foo.o", + "empty_standard_repo/dist/main.o", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "/*/\n" + "!/src\n"); + + assert_is_ignored("dist/foo.o"); + assert_is_ignored("dist/main.o"); + + refute_is_ignored("README.md"); + refute_is_ignored("src/foo.c"); + refute_is_ignored("src/foo/foo.c"); +} + +void test_ignore_status__subdir_ignore_all_toplevel_dirs_include_files(void) +{ + static const char *test_files[] = { + "empty_standard_repo/project/README.md", + "empty_standard_repo/project/src/main.c", + "empty_standard_repo/project/src/foo/foo.c", + "empty_standard_repo/project/dist/foo.o", + "empty_standard_repo/project/dist/main.o", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/project/.gitignore", + "/*/\n" + "!/src\n"); + + assert_is_ignored("project/dist/foo.o"); + assert_is_ignored("project/dist/main.o"); + + refute_is_ignored("project/src/foo.c"); + refute_is_ignored("project/src/foo/foo.c"); + refute_is_ignored("project/README.md"); +} + +void test_ignore_status__subdir_ignore_everything_except_certain_files(void) +{ + static const char *test_files[] = { + "empty_standard_repo/project/README.md", + "empty_standard_repo/project/some_file", + "empty_standard_repo/project/src/main.c", + "empty_standard_repo/project/src/foo/foo.c", + "empty_standard_repo/project/dist/foo.o", + "empty_standard_repo/project/dist/main.o", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/project/.gitignore", + "/*\n" + "!/src\n" + "!README.md\n"); + + assert_is_ignored("project/some_file"); + assert_is_ignored("project/dist/foo.o"); + assert_is_ignored("project/dist/main.o"); + + refute_is_ignored("project/README.md"); + refute_is_ignored("project/src/foo.c"); + refute_is_ignored("project/src/foo/foo.c"); +} + +void test_ignore_status__deeper(void) +{ + const char *test_files[] = { + "empty_standard_repo/foo.data", + "empty_standard_repo/bar.data", + "empty_standard_repo/dont_ignore/foo.data", + "empty_standard_repo/dont_ignore/bar.data", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile("empty_standard_repo/.gitignore", + "*.data\n" + "!dont_ignore/*.data\n"); + + assert_is_ignored("foo.data"); + assert_is_ignored("bar.data"); + + refute_is_ignored("dont_ignore/foo.data"); + refute_is_ignored("dont_ignore/bar.data"); +} + +void test_ignore_status__unignored_dir_with_ignored_contents(void) +{ + static const char *test_files[] = { + "empty_standard_repo/dir/a.test", + "empty_standard_repo/dir/subdir/a.test", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "*.test\n" + "!dir/*\n"); + + refute_is_ignored("dir/a.test"); + assert_is_ignored("dir/subdir/a.test"); +} + +void test_ignore_status__unignored_subdirs(void) +{ + static const char *test_files[] = { + "empty_standard_repo/dir/a.test", + "empty_standard_repo/dir/subdir/a.test", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "dir/*\n" + "!dir/*/\n"); + + assert_is_ignored("dir/a.test"); + refute_is_ignored("dir/subdir/a.test"); +} + +void test_ignore_status__skips_bom(void) +{ + static const char *test_files[] = { + "empty_standard_repo/a.test", + "empty_standard_repo/b.test", + "empty_standard_repo/c.test", + "empty_standard_repo/foo.txt", + "empty_standard_repo/bar.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "\xEF\xBB\xBF*.test\n"); + + assert_is_ignored("a.test"); + assert_is_ignored("b.test"); + assert_is_ignored("c.test"); + refute_is_ignored("foo.txt"); + refute_is_ignored("bar.txt"); +} + +void test_ignore_status__leading_spaces_are_significant(void) +{ + static const char *test_files[] = { + "empty_standard_repo/a.test", + "empty_standard_repo/b.test", + "empty_standard_repo/c.test", + "empty_standard_repo/d.test", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + " a.test\n" + "# this is a comment\n" + "b.test\n" + "\tc.test\n" + " # not a comment\n" + "d.test\n"); + + refute_is_ignored("a.test"); + assert_is_ignored(" a.test"); + refute_is_ignored("# this is a comment"); + assert_is_ignored("b.test"); + refute_is_ignored("c.test"); + assert_is_ignored("\tc.test"); + assert_is_ignored(" # not a comment"); + assert_is_ignored("d.test"); +} + +void test_ignore_status__override_nested_wildcard_unignore(void) +{ + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + const git_status_entry *status; + + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/dir", 0777)); + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/dir/subdir", 0777)); + cl_git_mkfile("empty_standard_repo/.gitignore", "a.test\n"); + cl_git_mkfile("empty_standard_repo/dir/.gitignore", "!*.test\n"); + cl_git_mkfile("empty_standard_repo/dir/subdir/.gitignore", "a.test\n"); + cl_git_mkfile("empty_standard_repo/dir/a.test", "pong"); + cl_git_mkfile("empty_standard_repo/dir/subdir/a.test", "pong"); + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + cl_git_pass(git_status_list_new(&statuslist, repo, &opts)); + cl_assert_equal_sz(4, git_status_list_entrycount(statuslist)); + + status = git_status_byindex(statuslist, 0); + cl_assert(status != NULL); + cl_assert_equal_s(".gitignore", status->index_to_workdir->old_file.path); + cl_assert_equal_i(GIT_STATUS_WT_NEW, status->status); + + status = git_status_byindex(statuslist, 1); + cl_assert(status != NULL); + cl_assert_equal_s("dir/.gitignore", status->index_to_workdir->old_file.path); + cl_assert_equal_i(GIT_STATUS_WT_NEW, status->status); + + status = git_status_byindex(statuslist, 2); + cl_assert(status != NULL); + cl_assert_equal_s("dir/a.test", status->index_to_workdir->old_file.path); + cl_assert_equal_i(GIT_STATUS_WT_NEW, status->status); + + status = git_status_byindex(statuslist, 3); + cl_assert(status != NULL); + cl_assert_equal_s("dir/subdir/.gitignore", status->index_to_workdir->old_file.path); + cl_assert_equal_i(GIT_STATUS_WT_NEW, status->status); + + git_status_list_free(statuslist); +} |
