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/status | |
| 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/status')
| -rw-r--r-- | tests/libgit2/status/renames.c | 844 | ||||
| -rw-r--r-- | tests/libgit2/status/single.c | 45 | ||||
| -rw-r--r-- | tests/libgit2/status/status_data.h | 326 | ||||
| -rw-r--r-- | tests/libgit2/status/status_helpers.c | 97 | ||||
| -rw-r--r-- | tests/libgit2/status/status_helpers.h | 51 | ||||
| -rw-r--r-- | tests/libgit2/status/submodules.c | 563 | ||||
| -rw-r--r-- | tests/libgit2/status/worktree.c | 1356 | ||||
| -rw-r--r-- | tests/libgit2/status/worktree_init.c | 338 |
8 files changed, 3620 insertions, 0 deletions
diff --git a/tests/libgit2/status/renames.c b/tests/libgit2/status/renames.c new file mode 100644 index 000000000..d5cf87d07 --- /dev/null +++ b/tests/libgit2/status/renames.c @@ -0,0 +1,844 @@ +#include "clar_libgit2.h" +#include "path.h" +#include "posix.h" +#include "status_helpers.h" +#include "util.h" +#include "status.h" + +static git_repository *g_repo = NULL; + +void test_status_renames__initialize(void) +{ + g_repo = cl_git_sandbox_init("renames"); + + cl_repo_set_bool(g_repo, "core.autocrlf", false); +} + +void test_status_renames__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static void _rename_helper( + git_repository *repo, const char *from, const char *to, const char *extra) +{ + git_str oldpath = GIT_STR_INIT, newpath = GIT_STR_INIT; + + cl_git_pass(git_str_joinpath( + &oldpath, git_repository_workdir(repo), from)); + cl_git_pass(git_str_joinpath( + &newpath, git_repository_workdir(repo), to)); + + cl_git_pass(p_rename(oldpath.ptr, newpath.ptr)); + + if (extra) + cl_git_append2file(newpath.ptr, extra); + + git_str_dispose(&oldpath); + git_str_dispose(&newpath); +} + +#define rename_file(R,O,N) _rename_helper((R), (O), (N), NULL) +#define rename_and_edit_file(R,O,N) \ + _rename_helper((R), (O), (N), "Added at the end to keep similarity!") + +struct status_entry { + git_status_t status; + const char *oldname; + const char *newname; +}; + +static void check_status( + git_status_list *status_list, + struct status_entry *expected_list, + size_t expected_len) +{ + const git_status_entry *actual; + const struct status_entry *expected; + const char *oldname, *newname; + size_t i, files_in_status = git_status_list_entrycount(status_list); + + cl_assert_equal_sz(expected_len, files_in_status); + + for (i = 0; i < expected_len; i++) { + actual = git_status_byindex(status_list, i); + expected = &expected_list[i]; + + oldname = actual->head_to_index ? actual->head_to_index->old_file.path : + actual->index_to_workdir ? actual->index_to_workdir->old_file.path : NULL; + + newname = actual->index_to_workdir ? actual->index_to_workdir->new_file.path : + actual->head_to_index ? actual->head_to_index->new_file.path : NULL; + + cl_assert_equal_i_fmt(expected->status, actual->status, "%04x"); + + if (expected->oldname) { + cl_assert(oldname != NULL); + cl_assert_equal_s(oldname, expected->oldname); + } else { + cl_assert(oldname == NULL); + } + + if (actual->status & (GIT_STATUS_INDEX_RENAMED|GIT_STATUS_WT_RENAMED)) { + if (expected->newname) { + cl_assert(newname != NULL); + cl_assert_equal_s(newname, expected->newname); + } else { + cl_assert(newname == NULL); + } + } + } +} + +void test_status_renames__head2index_one(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "newname.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "newname.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "newname.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__head2index_two(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED, + "sixserving.txt", "aaa.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED, + "untimely.txt", "bbb.txt" }, + { GIT_STATUS_INDEX_RENAMED, "songof7cities.txt", "ccc.txt" }, + { GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "ddd.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "ddd.txt"); + rename_and_edit_file(g_repo, "sixserving.txt", "aaa.txt"); + rename_file(g_repo, "songof7cities.txt", "ccc.txt"); + rename_and_edit_file(g_repo, "untimely.txt", "bbb.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_remove_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_remove_bypath(index, "songof7cities.txt")); + cl_git_pass(git_index_remove_bypath(index, "untimely.txt")); + cl_git_pass(git_index_add_bypath(index, "ddd.txt")); + cl_git_pass(git_index_add_bypath(index, "aaa.txt")); + cl_git_pass(git_index_add_bypath(index, "ccc.txt")); + cl_git_pass(git_index_add_bypath(index, "bbb.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 4); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__head2index_no_rename_from_rewrite(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_MODIFIED, "ikeepsix.txt", "ikeepsix.txt" }, + { GIT_STATUS_INDEX_MODIFIED, "sixserving.txt", "sixserving.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "_temp_.txt"); + rename_file(g_repo, "sixserving.txt", "ikeepsix.txt"); + rename_file(g_repo, "_temp_.txt", "sixserving.txt"); + + cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__head2index_rename_from_rewrite(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED, "sixserving.txt", "ikeepsix.txt" }, + { GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "sixserving.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "_temp_.txt"); + rename_file(g_repo, "sixserving.txt", "ikeepsix.txt"); + rename_file(g_repo, "_temp_.txt", "sixserving.txt"); + + cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__index2workdir_one(void) +{ + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "newname.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + rename_file(g_repo, "ikeepsix.txt", "newname.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); +} + +void test_status_renames__index2workdir_two(void) +{ + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, + "sixserving.txt", "aaa.txt" }, + { GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, + "untimely.txt", "bbb.txt" }, + { GIT_STATUS_WT_RENAMED, "songof7cities.txt", "ccc.txt" }, + { GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "ddd.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + rename_file(g_repo, "ikeepsix.txt", "ddd.txt"); + rename_and_edit_file(g_repo, "sixserving.txt", "aaa.txt"); + rename_file(g_repo, "songof7cities.txt", "ccc.txt"); + rename_and_edit_file(g_repo, "untimely.txt", "bbb.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 4); + git_status_list_free(statuslist); +} + +void test_status_renames__index2workdir_rename_from_rewrite(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_WT_RENAMED, "sixserving.txt", "ikeepsix.txt" }, + { GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "sixserving.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "_temp_.txt"); + rename_file(g_repo, "sixserving.txt", "ikeepsix.txt"); + rename_file(g_repo, "_temp_.txt", "sixserving.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__both_one(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "ikeepsix.txt", "newname-workdir.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "newname-index.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "newname-index.txt")); + cl_git_pass(git_index_write(index)); + + rename_file(g_repo, "newname-index.txt", "newname-workdir.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__both_two(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED | + GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, + "ikeepsix.txt", "ikeepsix-both.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED, + "sixserving.txt", "sixserving-index.txt" }, + { GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, + "songof7cities.txt", "songof7cities-workdir.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "untimely.txt", "untimely-both.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_and_edit_file(g_repo, "ikeepsix.txt", "ikeepsix-index.txt"); + rename_and_edit_file(g_repo, "sixserving.txt", "sixserving-index.txt"); + rename_file(g_repo, "untimely.txt", "untimely-index.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_remove_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_remove_bypath(index, "untimely.txt")); + cl_git_pass(git_index_add_bypath(index, "ikeepsix-index.txt")); + cl_git_pass(git_index_add_bypath(index, "sixserving-index.txt")); + cl_git_pass(git_index_add_bypath(index, "untimely-index.txt")); + cl_git_pass(git_index_write(index)); + + rename_and_edit_file(g_repo, "ikeepsix-index.txt", "ikeepsix-both.txt"); + rename_and_edit_file(g_repo, "songof7cities.txt", "songof7cities-workdir.txt"); + rename_file(g_repo, "untimely-index.txt", "untimely-both.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 4); + git_status_list_free(statuslist); + + git_index_free(index); +} + + +void test_status_renames__both_rename_from_rewrite(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "songof7cities.txt", "ikeepsix.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "ikeepsix.txt", "sixserving.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "sixserving.txt", "songof7cities.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES; + + cl_git_pass(git_repository_index(&index, g_repo)); + + rename_file(g_repo, "ikeepsix.txt", "_temp_.txt"); + rename_file(g_repo, "sixserving.txt", "ikeepsix.txt"); + rename_file(g_repo, "songof7cities.txt", "sixserving.txt"); + rename_file(g_repo, "_temp_.txt", "songof7cities.txt"); + + cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_add_bypath(index, "songof7cities.txt")); + cl_git_pass(git_index_write(index)); + + rename_file(g_repo, "songof7cities.txt", "_temp_.txt"); + rename_file(g_repo, "ikeepsix.txt", "songof7cities.txt"); + rename_file(g_repo, "sixserving.txt", "ikeepsix.txt"); + rename_file(g_repo, "_temp_.txt", "sixserving.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 3); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__rewrites_only_for_renames(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected[] = { + { GIT_STATUS_WT_MODIFIED, "ikeepsix.txt", "ikeepsix.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES; + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_git_rewritefile("renames/ikeepsix.txt", + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n" \ + "This is enough content for the file to be rewritten.\n"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__both_casechange_one(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + int index_caps; + struct status_entry expected_icase[] = { + { GIT_STATUS_INDEX_RENAMED, + "ikeepsix.txt", "IKeepSix.txt" }, + }; + struct status_entry expected_case[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "ikeepsix.txt", "IKEEPSIX.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_repository_index(&index, g_repo)); + index_caps = git_index_caps(index); + + rename_file(g_repo, "ikeepsix.txt", "IKeepSix.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, "IKeepSix.txt")); + cl_git_pass(git_index_write(index)); + + /* on a case-insensitive file system, this change won't matter. + * on a case-sensitive one, it will. + */ + rename_file(g_repo, "IKeepSix.txt", "IKEEPSIX.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + + check_status(statuslist, (index_caps & GIT_INDEX_CAPABILITY_IGNORE_CASE) ? + expected_icase : expected_case, 1); + + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__both_casechange_two(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + int index_caps; + struct status_entry expected_icase[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED | + GIT_STATUS_WT_MODIFIED, + "ikeepsix.txt", "IKeepSix.txt" }, + { GIT_STATUS_INDEX_MODIFIED, + "sixserving.txt", "sixserving.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_MODIFIED, + "songof7cities.txt", "songof7.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "untimely.txt", "untimeliest.txt" } + }; + struct status_entry expected_case[] = { + { GIT_STATUS_INDEX_RENAMED | + GIT_STATUS_WT_MODIFIED | GIT_STATUS_WT_RENAMED, + "songof7cities.txt", "SONGOF7.txt" }, + { GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_RENAMED, + "sixserving.txt", "SixServing.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_MODIFIED | + GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, + "ikeepsix.txt", "ikeepsix.txt" }, + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, + "untimely.txt", "untimeliest.txt" } + }; + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_repository_index(&index, g_repo)); + index_caps = git_index_caps(index); + + rename_and_edit_file(g_repo, "ikeepsix.txt", "IKeepSix.txt"); + rename_and_edit_file(g_repo, "sixserving.txt", "sixserving.txt"); + rename_file(g_repo, "songof7cities.txt", "songof7.txt"); + rename_file(g_repo, "untimely.txt", "untimelier.txt"); + + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_remove_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_remove_bypath(index, "songof7cities.txt")); + cl_git_pass(git_index_remove_bypath(index, "untimely.txt")); + cl_git_pass(git_index_add_bypath(index, "IKeepSix.txt")); + cl_git_pass(git_index_add_bypath(index, "sixserving.txt")); + cl_git_pass(git_index_add_bypath(index, "songof7.txt")); + cl_git_pass(git_index_add_bypath(index, "untimelier.txt")); + cl_git_pass(git_index_write(index)); + + rename_and_edit_file(g_repo, "IKeepSix.txt", "ikeepsix.txt"); + rename_file(g_repo, "sixserving.txt", "SixServing.txt"); + rename_and_edit_file(g_repo, "songof7.txt", "SONGOF7.txt"); + rename_file(g_repo, "untimelier.txt", "untimeliest.txt"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + + check_status(statuslist, (index_caps & GIT_INDEX_CAPABILITY_IGNORE_CASE) ? + expected_icase : expected_case, 4); + + git_status_list_free(statuslist); + + git_index_free(index); +} + +void test_status_renames__zero_byte_file_does_not_fail(void) +{ + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + struct status_entry expected[] = { + { GIT_STATUS_WT_DELETED, "ikeepsix.txt", "ikeepsix.txt" }, + { GIT_STATUS_WT_NEW, "zerobyte.txt", "zerobyte.txt" }, + }; + + opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES | + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR | + GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | + GIT_STATUS_SHOW_INDEX_AND_WORKDIR | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS; + + p_unlink("renames/ikeepsix.txt"); + cl_git_mkfile("renames/zerobyte.txt", ""); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); +} + +#ifdef GIT_USE_ICONV +static char *nfc = "\xC3\x85\x73\x74\x72\xC3\xB6\x6D"; +static char *nfd = "\x41\xCC\x8A\x73\x74\x72\x6F\xCC\x88\x6D"; +#endif + +/* + * Create a file in NFD (canonically decomposed) format. Ensure + * that when core.precomposeunicode is false that we return paths + * in NFD, but when core.precomposeunicode is true, then we + * return paths precomposed (in NFC). + */ +void test_status_renames__precomposed_unicode_rename(void) +{ +#ifdef GIT_USE_ICONV + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected0[] = { + { GIT_STATUS_WT_NEW, nfd, NULL }, + { GIT_STATUS_WT_DELETED, "sixserving.txt", NULL }, + }; + struct status_entry expected1[] = { + { GIT_STATUS_WT_RENAMED, "sixserving.txt", nfd }, + }; + struct status_entry expected2[] = { + { GIT_STATUS_WT_DELETED, "sixserving.txt", NULL }, + { GIT_STATUS_WT_NEW, nfc, NULL }, + }; + struct status_entry expected3[] = { + { GIT_STATUS_WT_RENAMED, "sixserving.txt", nfc }, + }; + + rename_file(g_repo, "sixserving.txt", nfd); + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + + cl_repo_set_bool(g_repo, "core.precomposeunicode", false); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected0, ARRAY_SIZE(expected0)); + git_status_list_free(statuslist); + + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected1, ARRAY_SIZE(expected1)); + git_status_list_free(statuslist); + + cl_repo_set_bool(g_repo, "core.precomposeunicode", true); + + opts.flags &= ~GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected2, ARRAY_SIZE(expected2)); + git_status_list_free(statuslist); + + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected3, ARRAY_SIZE(expected3)); + git_status_list_free(statuslist); +#endif +} + +void test_status_renames__precomposed_unicode_toggle_is_rename(void) +{ +#ifdef GIT_USE_ICONV + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_entry expected0[] = { + { GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", nfd }, + }; + struct status_entry expected1[] = { + { GIT_STATUS_WT_RENAMED, nfd, nfc }, + }; + struct status_entry expected2[] = { + { GIT_STATUS_INDEX_RENAMED, nfd, nfc }, + }; + struct status_entry expected3[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, nfd, nfd }, + }; + + cl_repo_set_bool(g_repo, "core.precomposeunicode", false); + rename_file(g_repo, "ikeepsix.txt", nfd); + + { + git_index *index; + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt")); + cl_git_pass(git_index_add_bypath(index, nfd)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + } + + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected0, ARRAY_SIZE(expected0)); + git_status_list_free(statuslist); + + cl_repo_commit_from_index(NULL, g_repo, NULL, 0, "commit nfd"); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + cl_assert_equal_sz(0, git_status_list_entrycount(statuslist)); + git_status_list_free(statuslist); + + cl_repo_set_bool(g_repo, "core.precomposeunicode", true); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected1, ARRAY_SIZE(expected1)); + git_status_list_free(statuslist); + + { + git_index *index; + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_remove_bypath(index, nfd)); + cl_git_pass(git_index_add_bypath(index, nfc)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + } + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected2, ARRAY_SIZE(expected2)); + git_status_list_free(statuslist); + + cl_repo_set_bool(g_repo, "core.precomposeunicode", false); + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected3, ARRAY_SIZE(expected3)); + git_status_list_free(statuslist); +#endif +} + +void test_status_renames__rename_threshold(void) +{ + git_index *index; + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + _rename_helper(g_repo, "ikeepsix.txt", "newname.txt", + "Line 1\n" \ + "Line 2\n" \ + "Line 3\n" \ + "Line 4\n" \ + "Line 5\n" \ + "Line 6\n" \ + "Line 7\n" \ + "Line 8\n" \ + "Line 9\n" + ); + + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + + cl_git_pass(git_repository_index(&index, g_repo)); + + /* Default threshold */ + { + struct status_entry expected[] = { + { GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, "ikeepsix.txt", "newname.txt" }, + }; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); + } + + /* Threshold set to 90 */ + { + struct status_entry expected[] = { + { GIT_STATUS_WT_DELETED, "ikeepsix.txt", NULL }, + { GIT_STATUS_WT_NEW, "newname.txt", NULL } + }; + + opts.rename_threshold = 90; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); + } + + /* Threshold set to 25 */ + { + struct status_entry expected[] = { + { GIT_STATUS_WT_RENAMED | GIT_STATUS_WT_MODIFIED, "ikeepsix.txt", "newname.txt" }, + }; + + opts.rename_threshold = 25; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 1); + git_status_list_free(statuslist); + } + + git_index_free(index); +} + +void test_status_renames__case_insensitive_h2i_and_i2wc(void) +{ + git_status_list *statuslist; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_reference *head, *test_branch; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + git_str path_to_delete = GIT_STR_INIT; + git_str path_to_edit = GIT_STR_INIT; + git_index *index; + git_strarray paths = { NULL, 0 }; + + struct status_entry expected[] = { + { GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_MODIFIED, "sixserving.txt", "sixserving-renamed.txt" }, + { GIT_STATUS_INDEX_DELETED, "Wow.txt", "Wow.txt" } + }; + + + /* Checkout the correct branch */ + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD")); + cl_git_pass(git_reference_symbolic_set_target( + &test_branch, head, "refs/heads/case-insensitive-status", NULL)); + cl_git_pass(git_checkout_head(g_repo, &checkout_opts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + + /* Rename sixserving.txt, delete Wow.txt, and stage those changes */ + rename_file(g_repo, "sixserving.txt", "sixserving-renamed.txt"); + cl_git_pass(git_str_joinpath( + &path_to_delete, git_repository_workdir(g_repo), "Wow.txt")); + cl_git_rmfile(path_to_delete.ptr); + + cl_git_pass(git_index_add_all(index, &paths, GIT_INDEX_ADD_FORCE, NULL, NULL)); + cl_git_pass(git_index_write(index)); + + + /* Change content of sixserving-renamed.txt */ + cl_git_pass(git_str_joinpath( + &path_to_edit, git_repository_workdir(g_repo), "sixserving-renamed.txt")); + cl_git_append2file(path_to_edit.ptr, "New content\n"); + + /* Run status */ + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; + opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR; + opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX; + opts.flags |= GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY; + + cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts)); + check_status(statuslist, expected, 2); + git_status_list_free(statuslist); + + git_index_free(index); + + git_str_dispose(&path_to_delete); + git_str_dispose(&path_to_edit); + + git_reference_free(head); + git_reference_free(test_branch); +} diff --git a/tests/libgit2/status/single.c b/tests/libgit2/status/single.c new file mode 100644 index 000000000..e7f92097c --- /dev/null +++ b/tests/libgit2/status/single.c @@ -0,0 +1,45 @@ +#include "clar_libgit2.h" +#include "posix.h" + +static void +cleanup__remove_file(void *_file) +{ + cl_must_pass(p_unlink((char *)_file)); +} + +/* test retrieving OID from a file apart from the ODB */ +void test_status_single__hash_single_file(void) +{ + static const char file_name[] = "new_file"; + static const char file_contents[] = "new_file\n"; + static const char file_hash[] = "d4fa8600b4f37d7516bef4816ae2c64dbf029e3a"; + + git_oid expected_id, actual_id; + + /* initialization */ + git_oid_fromstr(&expected_id, file_hash); + cl_git_mkfile(file_name, file_contents); + cl_set_cleanup(&cleanup__remove_file, (void *)file_name); + + cl_git_pass(git_odb_hashfile(&actual_id, file_name, GIT_OBJECT_BLOB)); + cl_assert_equal_oid(&expected_id, &actual_id); +} + +/* test retrieving OID from an empty file apart from the ODB */ +void test_status_single__hash_single_empty_file(void) +{ + static const char file_name[] = "new_empty_file"; + static const char file_contents[] = ""; + static const char file_hash[] = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"; + + git_oid expected_id, actual_id; + + /* initialization */ + git_oid_fromstr(&expected_id, file_hash); + cl_git_mkfile(file_name, file_contents); + cl_set_cleanup(&cleanup__remove_file, (void *)file_name); + + cl_git_pass(git_odb_hashfile(&actual_id, file_name, GIT_OBJECT_BLOB)); + cl_assert_equal_oid(&expected_id, &actual_id); +} + diff --git a/tests/libgit2/status/status_data.h b/tests/libgit2/status/status_data.h new file mode 100644 index 000000000..09b9827f2 --- /dev/null +++ b/tests/libgit2/status/status_data.h @@ -0,0 +1,326 @@ +#include "status_helpers.h" + +/* A utf-8 string with 83 characters, but 249 bytes. */ +static const char *longname = "\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97\xe5\x8f\x97"; + + +/* entries for a plain copy of tests/resources/status */ + +static const char *entry_paths0[] = { + "file_deleted", + "ignored_file", + "modified_file", + "new_file", + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + + "subdir/deleted_file", + "subdir/modified_file", + "subdir/new_file", + + "\xe8\xbf\x99", +}; + +static const unsigned int entry_statuses0[] = { + GIT_STATUS_WT_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_DELETED, + GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_DELETED, + GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_MODIFIED, + + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + + GIT_STATUS_WT_NEW, +}; + +static const int entry_count0 = 16; + +/* entries for a copy of tests/resources/status with all content + * deleted from the working directory + */ + +static const char *entry_paths2[] = { + "current_file", + "file_deleted", + "modified_file", + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + "subdir.txt", + "subdir/current_file", + "subdir/deleted_file", + "subdir/modified_file", +}; + +static const unsigned int entry_statuses2[] = { + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, +}; + +static const int entry_count2 = 15; + +/* entries for a copy of tests/resources/status with some mods */ + +static const char *entry_paths3_icase[] = { + ".HEADER", + "42-is-not-prime.sigh", + "current_file", + "current_file/", + "file_deleted", + "ignored_file", + "modified_file", + "new_file", + "README.md", + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + "subdir", + "subdir/current_file", + "subdir/deleted_file", + "subdir/modified_file", + "\xe8\xbf\x99", +}; + +static const unsigned int entry_statuses3_icase[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_NEW | GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, +}; + +static const char *entry_paths3[] = { + ".HEADER", + "42-is-not-prime.sigh", + "README.md", + "current_file", + "current_file/", + "file_deleted", + "ignored_file", + "modified_file", + "new_file", + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + "subdir", + "subdir/current_file", + "subdir/deleted_file", + "subdir/modified_file", + "\xe8\xbf\x99", +}; + +static const unsigned int entry_statuses3[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_NEW | GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, +}; + +static const int entry_count3 = 22; + + +/* entries for a copy of tests/resources/status with some mods + * and different options to the status call + */ + +static const char *entry_paths4[] = { + ".new_file", + "current_file", + "current_file/current_file", + "current_file/modified_file", + "current_file/new_file", + "file_deleted", + "modified_file", + "new_file", + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + "subdir", + "subdir/current_file", + "subdir/deleted_file", + "subdir/modified_file", + "zzz_new_dir/new_file", + "zzz_new_file", + "\xe8\xbf\x99", +}; + +static const unsigned int entry_statuses4[] = { + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_NEW | GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, +}; + +static const int entry_count4 = 23; + + +/* entries for a copy of tests/resources/status with options + * passed to the status call in order to only get the differences + * between the HEAD and the index (changes to be committed) + */ + +static const char *entry_paths5[] = { + "staged_changes", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_file_deleted", + "staged_delete_modified_file", + "staged_new_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", +}; + +static const unsigned int entry_statuses5[] = { + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_NEW, +}; + +static const int entry_count5 = 8; + + +/* entries for a copy of tests/resources/status with options + * passed to the status call in order to only get the differences + * between the workdir and the index (changes not staged, untracked files) + */ + +static const char *entry_paths6[] = { + "file_deleted", + "ignored_file", + "modified_file", + "new_file", + "staged_changes_file_deleted", + "staged_changes_modified_file", + "staged_delete_modified_file", + "staged_new_file_deleted_file", + "staged_new_file_modified_file", + "subdir/deleted_file", + "subdir/modified_file", + "subdir/new_file", + "\xe8\xbf\x99", +}; + +static const unsigned int entry_statuses6[] = { + GIT_STATUS_WT_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_NEW, +}; + +static const int entry_count6 = 13; diff --git a/tests/libgit2/status/status_helpers.c b/tests/libgit2/status/status_helpers.c new file mode 100644 index 000000000..5d13caa9a --- /dev/null +++ b/tests/libgit2/status/status_helpers.c @@ -0,0 +1,97 @@ +#include "clar_libgit2.h" +#include "status_helpers.h" + +int cb_status__normal( + const char *path, unsigned int status_flags, void *payload) +{ + status_entry_counts *counts = payload; + + if (counts->debug) + cb_status__print(path, status_flags, NULL); + + if (counts->entry_count >= counts->expected_entry_count) + counts->wrong_status_flags_count++; + else if (strcmp(path, counts->expected_paths[counts->entry_count])) + counts->wrong_sorted_path++; + else if (status_flags != counts->expected_statuses[counts->entry_count]) + counts->wrong_status_flags_count++; + + counts->entry_count++; + return 0; +} + +int cb_status__count(const char *p, unsigned int s, void *payload) +{ + volatile int *count = (int *)payload; + + GIT_UNUSED(p); + GIT_UNUSED(s); + + (*count)++; + + return 0; +} + +int cb_status__single(const char *p, unsigned int s, void *payload) +{ + status_entry_single *data = (status_entry_single *)payload; + + if (data->debug) + fprintf(stderr, "%02d: %s (%04x)\n", data->count, p, s); + + data->count++; + data->status = s; + + return 0; +} + +int cb_status__print( + const char *path, unsigned int status_flags, void *payload) +{ + char istatus = ' ', wstatus = ' '; + int icount = 0, wcount = 0; + + if (status_flags & GIT_STATUS_INDEX_NEW) { + istatus = 'A'; icount++; + } + if (status_flags & GIT_STATUS_INDEX_MODIFIED) { + istatus = 'M'; icount++; + } + if (status_flags & GIT_STATUS_INDEX_DELETED) { + istatus = 'D'; icount++; + } + if (status_flags & GIT_STATUS_INDEX_RENAMED) { + istatus = 'R'; icount++; + } + if (status_flags & GIT_STATUS_INDEX_TYPECHANGE) { + istatus = 'T'; icount++; + } + + if (status_flags & GIT_STATUS_WT_NEW) { + wstatus = 'A'; wcount++; + } + if (status_flags & GIT_STATUS_WT_MODIFIED) { + wstatus = 'M'; wcount++; + } + if (status_flags & GIT_STATUS_WT_DELETED) { + wstatus = 'D'; wcount++; + } + if (status_flags & GIT_STATUS_WT_TYPECHANGE) { + wstatus = 'T'; wcount++; + } + if (status_flags & GIT_STATUS_IGNORED) { + wstatus = 'I'; wcount++; + } + if (status_flags & GIT_STATUS_WT_UNREADABLE) { + wstatus = 'X'; wcount++; + } + + fprintf(stderr, "%c%c %s (%d/%d%s)\n", + istatus, wstatus, path, icount, wcount, + (icount > 1 || wcount > 1) ? " INVALID COMBO" : ""); + + if (payload) + *((int *)payload) += 1; + + return 0; +} diff --git a/tests/libgit2/status/status_helpers.h b/tests/libgit2/status/status_helpers.h new file mode 100644 index 000000000..464266af3 --- /dev/null +++ b/tests/libgit2/status/status_helpers.h @@ -0,0 +1,51 @@ +#ifndef INCLUDE_cl_status_helpers_h__ +#define INCLUDE_cl_status_helpers_h__ + +typedef struct { + int wrong_status_flags_count; + int wrong_sorted_path; + int entry_count; + const unsigned int* expected_statuses; + const char** expected_paths; + int expected_entry_count; + const char *file; + const char *func; + int line; + bool debug; +} status_entry_counts; + +#define status_counts_init(counts, paths, statuses) do { \ + memset(&(counts), 0, sizeof(counts)); \ + (counts).expected_statuses = (statuses); \ + (counts).expected_paths = (paths); \ + (counts).file = __FILE__; \ + (counts).func = __func__; \ + (counts).line = __LINE__; \ + } while (0) + +/* cb_status__normal takes payload of "status_entry_counts *" */ + +extern int cb_status__normal( + const char *path, unsigned int status_flags, void *payload); + + +/* cb_status__count takes payload of "int *" */ + +extern int cb_status__count(const char *p, unsigned int s, void *payload); + + +typedef struct { + int count; + unsigned int status; + bool debug; +} status_entry_single; + +/* cb_status__single takes payload of "status_entry_single *" */ + +extern int cb_status__single(const char *p, unsigned int s, void *payload); + +/* cb_status__print takes optional payload of "int *" */ + +extern int cb_status__print(const char *p, unsigned int s, void *payload); + +#endif diff --git a/tests/libgit2/status/submodules.c b/tests/libgit2/status/submodules.c new file mode 100644 index 000000000..d223657b4 --- /dev/null +++ b/tests/libgit2/status/submodules.c @@ -0,0 +1,563 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "status_helpers.h" +#include "../submodule/submodule_helpers.h" + +static git_repository *g_repo = NULL; + +void test_status_submodules__initialize(void) +{ +} + +void test_status_submodules__cleanup(void) +{ +} + +void test_status_submodules__api(void) +{ + git_submodule *sm; + + g_repo = setup_fixture_submodules(); + + cl_assert(git_submodule_lookup(NULL, g_repo, "nonexistent") == GIT_ENOTFOUND); + + cl_assert(git_submodule_lookup(NULL, g_repo, "modified") == GIT_ENOTFOUND); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + cl_assert(sm != NULL); + cl_assert_equal_s("testrepo", git_submodule_name(sm)); + cl_assert_equal_s("testrepo", git_submodule_path(sm)); + git_submodule_free(sm); +} + +void test_status_submodules__0(void) +{ + int counts = 0; + + g_repo = setup_fixture_submodules(); + + cl_assert(git_fs_path_isdir("submodules/.git")); + cl_assert(git_fs_path_isdir("submodules/testrepo/.git")); + cl_assert(git_fs_path_isfile("submodules/.gitmodules")); + + cl_git_pass( + git_status_foreach(g_repo, cb_status__count, &counts) + ); + + cl_assert_equal_i(6, counts); +} + +static const char *expected_files[] = { + ".gitmodules", + "added", + "deleted", + "ignored", + "modified", + "untracked" +}; + +static unsigned int expected_status[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW +}; + +static int cb_status__match(const char *p, unsigned int s, void *payload) +{ + status_entry_counts *counts = payload; + int idx = counts->entry_count++; + + clar__assert_equal( + counts->file, counts->func, counts->line, + "Status path mismatch", 1, + "%s", counts->expected_paths[idx], p); + + clar__assert_equal( + counts->file, counts->func, counts->line, + "Status code mismatch", 1, + "%o", counts->expected_statuses[idx], s); + + return 0; +} + +void test_status_submodules__1(void) +{ + status_entry_counts counts; + + g_repo = setup_fixture_submodules(); + + cl_assert(git_fs_path_isdir("submodules/.git")); + cl_assert(git_fs_path_isdir("submodules/testrepo/.git")); + cl_assert(git_fs_path_isfile("submodules/.gitmodules")); + + status_counts_init(counts, expected_files, expected_status); + + cl_git_pass( git_status_foreach(g_repo, cb_status__match, &counts) ); + + cl_assert_equal_i(6, counts.entry_count); +} + +void test_status_submodules__single_file(void) +{ + unsigned int status = 0; + g_repo = setup_fixture_submodules(); + cl_git_pass( git_status_file(&status, g_repo, "testrepo") ); + cl_assert(!status); +} + +void test_status_submodules__moved_head(void) +{ + git_submodule *sm; + git_repository *smrepo; + git_oid oid; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *expected_files_with_sub[] = { + ".gitmodules", + "added", + "deleted", + "ignored", + "modified", + "testrepo", + "untracked" + }; + static unsigned int expected_status_with_sub[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW + }; + + g_repo = setup_fixture_submodules(); + + cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo")); + cl_git_pass(git_submodule_open(&smrepo, sm)); + git_submodule_free(sm); + + /* move submodule HEAD to c47800c7266a2be04c571c04d5a6614691ea99bd */ + cl_git_pass( + git_oid_fromstr(&oid, "c47800c7266a2be04c571c04d5a6614691ea99bd")); + cl_git_pass(git_repository_set_head_detached(smrepo, &oid)); + + /* first do a normal status, which should now include the submodule */ + + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + status_counts_init( + counts, expected_files_with_sub, expected_status_with_sub); + cl_git_pass( + git_status_foreach_ext(g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); + + /* try again with EXCLUDE_SUBMODULES which should skip it */ + + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_EXCLUDE_SUBMODULES; + + status_counts_init(counts, expected_files, expected_status); + cl_git_pass( + git_status_foreach_ext(g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); + + git_repository_free(smrepo); +} + +void test_status_submodules__dirty_workdir_only(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + static const char *expected_files_with_sub[] = { + ".gitmodules", + "added", + "deleted", + "ignored", + "modified", + "testrepo", + "untracked" + }; + static unsigned int expected_status_with_sub[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW + }; + + g_repo = setup_fixture_submodules(); + + cl_git_rewritefile("submodules/testrepo/README", "heyheyhey"); + cl_git_mkfile("submodules/testrepo/all_new.txt", "never seen before"); + + /* first do a normal status, which should now include the submodule */ + + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + status_counts_init( + counts, expected_files_with_sub, expected_status_with_sub); + cl_git_pass( + git_status_foreach_ext(g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); + + /* try again with EXCLUDE_SUBMODULES which should skip it */ + + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_EXCLUDE_SUBMODULES; + + status_counts_init(counts, expected_files, expected_status); + cl_git_pass( + git_status_foreach_ext(g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); +} + +void test_status_submodules__uninitialized(void) +{ + git_repository *cloned_repo; + git_status_list *statuslist; + + g_repo = cl_git_sandbox_init("submod2"); + + cl_git_pass(git_clone(&cloned_repo, "submod2", "submod2-clone", NULL)); + + cl_git_pass(git_status_list_new(&statuslist, cloned_repo, NULL)); + cl_assert_equal_i(0, git_status_list_entrycount(statuslist)); + + git_status_list_free(statuslist); + git_repository_free(cloned_repo); + cl_git_sandbox_cleanup(); +} + +void test_status_submodules__contained_untracked_repo(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + git_repository *contained; + static const char *expected_files_not_ignored[] = { + ".gitmodules", + "added", + "deleted", + "modified", + "untracked" + }; + static unsigned int expected_status_not_ignored[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + }; + static const char *expected_files_with_untracked[] = { + ".gitmodules", + "added", + "deleted", + "dir/file.md", + "modified", + "untracked" + }; + static const char *expected_files_with_untracked_dir[] = { + ".gitmodules", + "added", + "deleted", + "dir/", + "modified", + "untracked" + }; + static unsigned int expected_status_with_untracked[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_WT_NEW, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW + }; + + g_repo = setup_fixture_submodules(); + + /* skip empty directory */ + + cl_must_pass(p_mkdir("submodules/dir", 0777)); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED; + + status_counts_init( + counts, expected_files_not_ignored, expected_status_not_ignored); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(5, counts.entry_count); + + /* still skipping because empty == ignored */ + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_not_ignored, expected_status_not_ignored); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(5, counts.entry_count); + + /* find non-ignored contents of directory */ + + cl_git_mkfile("submodules/dir/file.md", "hello"); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_with_untracked, expected_status_with_untracked); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); + + /* but skip if all content is ignored */ + + cl_git_append2file("submodules/.git/info/exclude", "\n*.md\n\n"); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_not_ignored, expected_status_not_ignored); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(5, counts.entry_count); + + /* same is true if it contains a git link */ + + cl_git_mkfile("submodules/dir/.git", "gitlink: ../.git"); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_not_ignored, expected_status_not_ignored); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(5, counts.entry_count); + + /* but if it contains tracked files, it should just show up as a + * directory and exclude the files in it + */ + + cl_git_mkfile("submodules/dir/another_file", "hello"); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_with_untracked_dir, + expected_status_with_untracked); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); + + /* that applies to a git repo with a .git directory too */ + + cl_must_pass(p_unlink("submodules/dir/.git")); + cl_git_pass(git_repository_init(&contained, "submodules/dir", false)); + git_repository_free(contained); + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + status_counts_init( + counts, expected_files_with_untracked_dir, + expected_status_with_untracked); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); + + /* same result even if we don't recurse into subdirectories */ + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED; + + status_counts_init( + counts, expected_files_with_untracked_dir, + expected_status_with_untracked); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(6, counts.entry_count); + + /* and if we remove the untracked file, it goes back to ignored */ + + cl_must_pass(p_unlink("submodules/dir/another_file")); + + status_counts_init( + counts, expected_files_not_ignored, expected_status_not_ignored); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(5, counts.entry_count); +} + +void test_status_submodules__broken_stuff_that_git_allows(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts; + git_repository *contained; + static const char *expected_files_with_broken[] = { + ".gitmodules", + "added", + "broken/tracked", + "deleted", + "ignored", + "modified", + "untracked" + }; + static unsigned int expected_status_with_broken[] = { + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_NEW, + GIT_STATUS_INDEX_DELETED, + GIT_STATUS_IGNORED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + }; + + g_repo = setup_fixture_submodules(); + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | + GIT_STATUS_OPT_INCLUDE_IGNORED; + + /* make a directory and stick a tracked item into the index */ + { + git_index *idx; + cl_must_pass(p_mkdir("submodules/broken", 0777)); + cl_git_mkfile("submodules/broken/tracked", "tracked content"); + cl_git_pass(git_repository_index(&idx, g_repo)); + cl_git_pass(git_index_add_bypath(idx, "broken/tracked")); + cl_git_pass(git_index_write(idx)); + git_index_free(idx); + } + + status_counts_init( + counts, expected_files_with_broken, expected_status_with_broken); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); + + /* directory with tracked items that looks a little bit like a repo */ + + cl_must_pass(p_mkdir("submodules/broken/.git", 0777)); + cl_must_pass(p_mkdir("submodules/broken/.git/info", 0777)); + cl_git_mkfile("submodules/broken/.git/info/exclude", "# bogus"); + + status_counts_init( + counts, expected_files_with_broken, expected_status_with_broken); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); + + /* directory with tracked items that is a repo */ + + cl_git_pass(git_futils_rmdir_r( + "submodules/broken/.git", NULL, GIT_RMDIR_REMOVE_FILES)); + cl_git_pass(git_repository_init(&contained, "submodules/broken", false)); + git_repository_free(contained); + + status_counts_init( + counts, expected_files_with_broken, expected_status_with_broken); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); + + /* directory with tracked items that claims to be a submodule but is not */ + + cl_git_pass(git_futils_rmdir_r( + "submodules/broken/.git", NULL, GIT_RMDIR_REMOVE_FILES)); + cl_git_append2file("submodules/.gitmodules", + "\n[submodule \"broken\"]\n" + "\tpath = broken\n" + "\turl = https://github.com/not/used\n\n"); + + status_counts_init( + counts, expected_files_with_broken, expected_status_with_broken); + cl_git_pass(git_status_foreach_ext( + g_repo, &opts, cb_status__match, &counts)); + cl_assert_equal_i(7, counts.entry_count); +} + +void test_status_submodules__entry_but_dir_tracked(void) +{ + git_repository *repo; + git_status_list *status; + git_diff *diff; + git_index *index; + git_tree *tree; + + cl_git_pass(git_repository_init(&repo, "mixed-submodule", 0)); + cl_git_mkfile("mixed-submodule/.gitmodules", "[submodule \"sub\"]\n path = sub\n url = ../foo\n"); + cl_git_pass(p_mkdir("mixed-submodule/sub", 0777)); + cl_git_mkfile("mixed-submodule/sub/file", ""); + + /* Create the commit with sub/file as a file, and an entry for sub in the modules list */ + { + git_oid tree_id, commit_id; + git_signature *sig; + git_reference *ref; + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, ".gitmodules")); + cl_git_pass(git_index_add_bypath(index, "sub/file")); + cl_git_pass(git_index_write(index)); + cl_git_pass(git_index_write_tree(&tree_id, index)); + cl_git_pass(git_signature_now(&sig, "Sloppy Submoduler", "sloppy@example.com")); + cl_git_pass(git_tree_lookup(&tree, repo, &tree_id)); + cl_git_pass(git_commit_create(&commit_id, repo, NULL, sig, sig, NULL, "message", tree, 0, NULL)); + cl_git_pass(git_reference_create(&ref, repo, "refs/heads/master", &commit_id, 1, "commit: foo")); + git_reference_free(ref); + git_signature_free(sig); + } + + cl_git_pass(git_diff_tree_to_index(&diff, repo, tree, index, NULL)); + cl_assert_equal_i(0, git_diff_num_deltas(diff)); + git_diff_free(diff); + + cl_git_pass(git_diff_index_to_workdir(&diff, repo, index, NULL)); + cl_assert_equal_i(0, git_diff_num_deltas(diff)); + git_diff_free(diff); + + cl_git_pass(git_status_list_new(&status, repo, NULL)); + cl_assert_equal_i(0, git_status_list_entrycount(status)); + + git_status_list_free(status); + git_index_free(index); + git_tree_free(tree); + git_repository_free(repo); +} + +void test_status_submodules__mixed_case(void) +{ + git_status_list *status; + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + const git_status_entry *s; + size_t i; + + status_opts.flags = + GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS | + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR | + GIT_STATUS_OPT_RENAMES_FROM_REWRITES | + GIT_STATUS_OPT_INCLUDE_UNREADABLE | + GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED; + + g_repo = setup_fixture_submod3(); + + cl_git_pass(git_status_list_new(&status, g_repo, &status_opts)); + + for (i = 0; i < git_status_list_entrycount(status); i++) { + s = git_status_byindex(status, i); + + if (s->head_to_index && + strcmp(s->head_to_index->old_file.path, ".gitmodules") == 0) + continue; + + cl_assert_equal_i(0, s->status); + } + + git_status_list_free(status); +} + diff --git a/tests/libgit2/status/worktree.c b/tests/libgit2/status/worktree.c new file mode 100644 index 000000000..00c6ec2d5 --- /dev/null +++ b/tests/libgit2/status/worktree.c @@ -0,0 +1,1356 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "ignore.h" +#include "status_data.h" +#include "posix.h" +#include "util.h" +#include "path.h" +#include "../diff/diff_helpers.h" +#include "../checkout/checkout_helpers.h" +#include "git2/sys/diff.h" + +/** + * Cleanup + * + * This will be called once after each test finishes, even + * if the test failed + */ +void test_status_worktree__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +/** + * Tests - Status determination on a working tree + */ +/* this test is equivalent to t18-status.c:statuscb0 */ +void test_status_worktree__whole_repository(void) +{ + status_entry_counts counts; + git_repository *repo = cl_git_sandbox_init("status"); + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_count0; + counts.expected_paths = entry_paths0; + counts.expected_statuses = entry_statuses0; + + cl_git_pass( + git_status_foreach(repo, 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 assert_show( + const int entry_counts, + const char *entry_paths[], + const unsigned int entry_statuses[], + git_repository *repo, + git_status_show_t show, + unsigned int extra_flags) +{ + status_entry_counts counts; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_counts; + counts.expected_paths = entry_paths; + counts.expected_statuses = entry_statuses; + + opts.flags = GIT_STATUS_OPT_DEFAULTS | extra_flags; + opts.show = show; + + cl_git_pass( + git_status_foreach_ext(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_status_worktree__show_index_and_workdir(void) +{ + assert_show(entry_count0, entry_paths0, entry_statuses0, + cl_git_sandbox_init("status"), GIT_STATUS_SHOW_INDEX_AND_WORKDIR, 0); +} + +void test_status_worktree__show_index_only(void) +{ + assert_show(entry_count5, entry_paths5, entry_statuses5, + cl_git_sandbox_init("status"), GIT_STATUS_SHOW_INDEX_ONLY, 0); +} + +void test_status_worktree__show_workdir_only(void) +{ + assert_show(entry_count6, entry_paths6, entry_statuses6, + cl_git_sandbox_init("status"), GIT_STATUS_SHOW_WORKDIR_ONLY, 0); +} + +/* this test is equivalent to t18-status.c:statuscb1 */ +void test_status_worktree__empty_repository(void) +{ + int count = 0; + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_pass(git_status_foreach(repo, cb_status__count, &count)); + + cl_assert_equal_i(0, count); +} + +static int remove_file_cb(void *data, git_str *file) +{ + const char *filename = git_str_cstr(file); + + GIT_UNUSED(data); + + if (git__suffixcmp(filename, ".git") == 0) + return 0; + + if (git_fs_path_isdir(filename)) + cl_git_pass(git_futils_rmdir_r(filename, NULL, GIT_RMDIR_REMOVE_FILES)); + else + cl_git_pass(p_unlink(git_str_cstr(file))); + + return 0; +} + +/* this test is equivalent to t18-status.c:statuscb2 */ +void test_status_worktree__purged_worktree(void) +{ + status_entry_counts counts; + git_repository *repo = cl_git_sandbox_init("status"); + git_str workdir = GIT_STR_INIT; + + /* first purge the contents of the worktree */ + cl_git_pass(git_str_sets(&workdir, git_repository_workdir(repo))); + cl_git_pass(git_fs_path_direach(&workdir, 0, remove_file_cb, NULL)); + git_str_dispose(&workdir); + + /* now get status */ + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_count2; + counts.expected_paths = entry_paths2; + counts.expected_statuses = entry_statuses2; + + cl_git_pass( + git_status_foreach(repo, 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); +} + +/* this test is similar to t18-status.c:statuscb3 */ +void test_status_worktree__swap_subdir_and_file(void) +{ + status_entry_counts counts; + git_repository *repo = cl_git_sandbox_init("status"); + git_index *index; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + bool ignore_case; + + cl_git_pass(git_repository_index(&index, repo)); + ignore_case = (git_index_caps(index) & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0; + git_index_free(index); + + /* first alter the contents of the worktree */ + cl_git_pass(p_rename("status/current_file", "status/swap")); + cl_git_pass(p_rename("status/subdir", "status/current_file")); + cl_git_pass(p_rename("status/swap", "status/subdir")); + + cl_git_mkfile("status/.HEADER", "dummy"); + cl_git_mkfile("status/42-is-not-prime.sigh", "dummy"); + cl_git_mkfile("status/README.md", "dummy"); + + /* now get status */ + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_count3; + counts.expected_paths = ignore_case ? entry_paths3_icase : entry_paths3; + counts.expected_statuses = ignore_case ? entry_statuses3_icase : entry_statuses3; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_INCLUDE_IGNORED; + + cl_git_pass( + git_status_foreach_ext(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_status_worktree__swap_subdir_with_recurse_and_pathspec(void) +{ + status_entry_counts counts; + git_repository *repo = cl_git_sandbox_init("status"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + /* first alter the contents of the worktree */ + cl_git_pass(p_rename("status/current_file", "status/swap")); + cl_git_pass(p_rename("status/subdir", "status/current_file")); + cl_git_pass(p_rename("status/swap", "status/subdir")); + cl_git_mkfile("status/.new_file", "dummy"); + cl_git_pass(git_futils_mkdir_r("status/zzz_new_dir", 0777)); + cl_git_mkfile("status/zzz_new_dir/new_file", "dummy"); + cl_git_mkfile("status/zzz_new_file", "dummy"); + + /* now get status */ + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_count4; + counts.expected_paths = entry_paths4; + counts.expected_statuses = entry_statuses4; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + /* TODO: set pathspec to "current_file" eventually */ + + cl_git_pass( + git_status_foreach_ext(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 stage_and_commit(git_repository *repo, const char *path) +{ + git_index *index; + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, path)); + cl_repo_commit_from_index(NULL, repo, NULL, 1323847743, "Initial commit\n"); + git_index_free(index); +} + +void test_status_worktree__within_subdir(void) +{ + status_entry_counts counts; + git_repository *repo = cl_git_sandbox_init("status"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + char *paths[] = { "zzz_new_dir" }; + git_strarray pathsArray; + + /* first alter the contents of the worktree */ + cl_git_mkfile("status/.new_file", "dummy"); + cl_git_pass(git_futils_mkdir_r("status/zzz_new_dir", 0777)); + cl_git_mkfile("status/zzz_new_dir/new_file", "dummy"); + cl_git_mkfile("status/zzz_new_file", "dummy"); + cl_git_mkfile("status/wut", "dummy"); + + stage_and_commit(repo, "zzz_new_dir/new_file"); + + /* now get status */ + memset(&counts, 0x0, sizeof(status_entry_counts)); + counts.expected_entry_count = entry_count4; + counts.expected_paths = entry_paths4; + counts.expected_statuses = entry_statuses4; + counts.debug = true; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH; + + pathsArray.count = 1; + pathsArray.strings = paths; + opts.pathspec = pathsArray; + + /* We committed zzz_new_dir/new_file above. It shouldn't be reported. */ + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__normal, &counts) + ); + + cl_assert_equal_i(0, counts.entry_count); + cl_assert_equal_i(0, counts.wrong_status_flags_count); + cl_assert_equal_i(0, counts.wrong_sorted_path); +} + +/* this test is equivalent to t18-status.c:singlestatus0 */ +void test_status_worktree__single_file(void) +{ + int i; + unsigned int status_flags; + git_repository *repo = cl_git_sandbox_init("status"); + + for (i = 0; i < (int)entry_count0; i++) { + cl_git_pass( + git_status_file(&status_flags, repo, entry_paths0[i]) + ); + cl_assert(entry_statuses0[i] == status_flags); + } +} + +/* this test is equivalent to t18-status.c:singlestatus1 */ +void test_status_worktree__single_nonexistent_file(void) +{ + int error; + unsigned int status_flags; + git_repository *repo = cl_git_sandbox_init("status"); + + error = git_status_file(&status_flags, repo, "nonexistent"); + cl_git_fail(error); + cl_assert(error == GIT_ENOTFOUND); +} + +/* this test is equivalent to t18-status.c:singlestatus2 */ +void test_status_worktree__single_nonexistent_file_empty_repo(void) +{ + int error; + unsigned int status_flags; + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + + error = git_status_file(&status_flags, repo, "nonexistent"); + cl_git_fail(error); + cl_assert(error == GIT_ENOTFOUND); +} + +/* this test is equivalent to t18-status.c:singlestatus3 */ +void test_status_worktree__single_file_empty_repo(void) +{ + unsigned int status_flags; + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + + cl_git_mkfile("empty_standard_repo/new_file", "new_file\n"); + + cl_git_pass(git_status_file(&status_flags, repo, "new_file")); + cl_assert(status_flags == GIT_STATUS_WT_NEW); +} + +/* this test is equivalent to t18-status.c:singlestatus4 */ +void test_status_worktree__single_folder(void) +{ + int error; + unsigned int status_flags; + git_repository *repo = cl_git_sandbox_init("status"); + + error = git_status_file(&status_flags, repo, "subdir"); + cl_git_fail(error); + cl_assert(error != GIT_ENOTFOUND); +} + + +void test_status_worktree__ignores(void) +{ + int i, ignored; + git_repository *repo = cl_git_sandbox_init("status"); + + for (i = 0; i < (int)entry_count0; i++) { + cl_git_pass( + git_status_should_ignore(&ignored, repo, entry_paths0[i]) + ); + cl_assert(ignored == (entry_statuses0[i] == GIT_STATUS_IGNORED)); + } + + cl_git_pass( + git_status_should_ignore(&ignored, repo, "nonexistent_file") + ); + cl_assert(!ignored); + + cl_git_pass( + git_status_should_ignore(&ignored, repo, "ignored_nonexistent_file") + ); + cl_assert(ignored); +} + +static int cb_status__check_592(const char *p, unsigned int s, void *payload) +{ + if (s != GIT_STATUS_WT_DELETED || + (payload != NULL && strcmp(p, (const char *)payload) != 0)) + return -1; + + return 0; +} + +void test_status_worktree__issue_592(void) +{ + git_repository *repo; + git_str path = GIT_STR_INIT; + + repo = cl_git_sandbox_init("issue_592"); + cl_git_pass(git_str_joinpath(&path, git_repository_workdir(repo), "l.txt")); + cl_git_pass(p_unlink(git_str_cstr(&path))); + cl_assert(!git_fs_path_exists("issue_592/l.txt")); + + cl_git_pass(git_status_foreach(repo, cb_status__check_592, "l.txt")); + + git_str_dispose(&path); +} + +void test_status_worktree__issue_592_2(void) +{ + git_repository *repo; + git_str path = GIT_STR_INIT; + + repo = cl_git_sandbox_init("issue_592"); + cl_git_pass(git_str_joinpath(&path, git_repository_workdir(repo), "c/a.txt")); + cl_git_pass(p_unlink(git_str_cstr(&path))); + cl_assert(!git_fs_path_exists("issue_592/c/a.txt")); + + cl_git_pass(git_status_foreach(repo, cb_status__check_592, "c/a.txt")); + + git_str_dispose(&path); +} + +void test_status_worktree__issue_592_3(void) +{ + git_repository *repo; + git_str path = GIT_STR_INIT; + + repo = cl_git_sandbox_init("issue_592"); + + cl_git_pass(git_str_joinpath(&path, git_repository_workdir(repo), "c")); + cl_git_pass(git_futils_rmdir_r(git_str_cstr(&path), NULL, GIT_RMDIR_REMOVE_FILES)); + cl_assert(!git_fs_path_exists("issue_592/c/a.txt")); + + cl_git_pass(git_status_foreach(repo, cb_status__check_592, "c/a.txt")); + + git_str_dispose(&path); +} + +void test_status_worktree__issue_592_4(void) +{ + git_repository *repo; + git_str path = GIT_STR_INIT; + + repo = cl_git_sandbox_init("issue_592"); + + cl_git_pass(git_str_joinpath(&path, git_repository_workdir(repo), "t/b.txt")); + cl_git_pass(p_unlink(git_str_cstr(&path))); + + cl_git_pass(git_status_foreach(repo, cb_status__check_592, "t/b.txt")); + + git_str_dispose(&path); +} + +void test_status_worktree__issue_592_5(void) +{ + git_repository *repo; + git_str path = GIT_STR_INIT; + + repo = cl_git_sandbox_init("issue_592"); + + cl_git_pass(git_str_joinpath(&path, git_repository_workdir(repo), "t")); + cl_git_pass(git_futils_rmdir_r(git_str_cstr(&path), NULL, GIT_RMDIR_REMOVE_FILES)); + cl_git_pass(p_mkdir(git_str_cstr(&path), 0777)); + + cl_git_pass(git_status_foreach(repo, cb_status__check_592, NULL)); + + git_str_dispose(&path); +} + +void test_status_worktree__issue_592_ignores_0(void) +{ + int count = 0; + status_entry_single st; + git_repository *repo = cl_git_sandbox_init("issue_592"); + + cl_git_pass(git_status_foreach(repo, cb_status__count, &count)); + cl_assert_equal_i(0, count); + + cl_git_rewritefile("issue_592/.gitignore", + ".gitignore\n*.txt\nc/\n[tT]*/\n"); + + cl_git_pass(git_status_foreach(repo, cb_status__count, &count)); + cl_assert_equal_i(1, count); + + /* This is a situation where the behavior of libgit2 is + * different from core git. Core git will show ignored.txt + * in the list of ignored files, even though the directory + * "t" is ignored and the file is untracked because we have + * the explicit "*.txt" ignore rule. Libgit2 just excludes + * all untracked files that are contained within ignored + * directories without explicitly listing them. + */ + cl_git_rewritefile("issue_592/t/ignored.txt", "ping"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_IGNORED); + + cl_git_rewritefile("issue_592/c/ignored_by_dir", "ping"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_IGNORED); + + cl_git_rewritefile("issue_592/t/ignored_by_dir_pattern", "ping"); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_IGNORED); +} + +void test_status_worktree__issue_592_ignored_dirs_with_tracked_content(void) +{ + int count = 0; + git_repository *repo = cl_git_sandbox_init("issue_592b"); + + cl_git_pass(git_status_foreach(repo, cb_status__count, &count)); + cl_assert_equal_i(1, count); + + /* if we are really mimicking core git, then only ignored1.txt + * at the top level will show up in the ignores list here. + * everything else will be unmodified or skipped completely. + */ +} + +void test_status_worktree__conflict_with_diff3(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + git_index *index; + unsigned int status; + git_index_entry ancestor_entry, our_entry, their_entry; + + memset(&ancestor_entry, 0x0, sizeof(git_index_entry)); + memset(&our_entry, 0x0, sizeof(git_index_entry)); + memset(&their_entry, 0x0, sizeof(git_index_entry)); + + ancestor_entry.path = "modified_file"; + ancestor_entry.mode = 0100644; + git_oid_fromstr(&ancestor_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + our_entry.path = "modified_file"; + our_entry.mode = 0100644; + git_oid_fromstr(&our_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + their_entry.path = "modified_file"; + their_entry.mode = 0100644; + git_oid_fromstr(&their_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + cl_git_pass(git_status_file(&status, repo, "modified_file")); + cl_assert_equal_i(GIT_STATUS_WT_MODIFIED, status); + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_remove(index, "modified_file", 0)); + cl_git_pass(git_index_conflict_add( + index, &ancestor_entry, &our_entry, &their_entry)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_status_file(&status, repo, "modified_file")); + + cl_assert_equal_i(GIT_STATUS_CONFLICTED, status); +} + +static const char *filemode_paths[] = { + "exec_off", + "exec_off2on_staged", + "exec_off2on_workdir", + "exec_off_untracked", + "exec_on", + "exec_on2off_staged", + "exec_on2off_workdir", + "exec_on_untracked", +}; + +static unsigned int filemode_statuses[] = { + GIT_STATUS_CURRENT, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW, + GIT_STATUS_CURRENT, + GIT_STATUS_INDEX_MODIFIED, + GIT_STATUS_WT_MODIFIED, + GIT_STATUS_WT_NEW +}; + +static const int filemode_count = 8; + +void test_status_worktree__filemode_changes(void) +{ + git_repository *repo = cl_git_sandbox_init("filemodes"); + status_entry_counts counts; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + /* overwrite stored filemode with platform appropriate value */ + if (cl_is_chmod_supported()) + cl_repo_set_bool(repo, "core.filemode", true); + else { + int i; + + cl_repo_set_bool(repo, "core.filemode", false); + + /* won't trust filesystem mode diffs, so these will appear unchanged */ + for (i = 0; i < filemode_count; ++i) + if (filemode_statuses[i] == GIT_STATUS_WT_MODIFIED) + filemode_statuses[i] = GIT_STATUS_CURRENT; + } + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = filemode_count; + counts.expected_paths = filemode_paths; + counts.expected_statuses = filemode_statuses; + + cl_git_pass( + git_status_foreach_ext(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_status_worktree__filemode_non755(void) +{ + git_repository *repo = cl_git_sandbox_init("filemodes"); + status_entry_counts counts; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_str executable_path = GIT_STR_INIT; + git_str nonexecutable_path = GIT_STR_INIT; + + if (!cl_is_chmod_supported()) + return; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + + git_str_joinpath(&executable_path, git_repository_workdir(repo), "exec_on"); + cl_must_pass(p_chmod(git_str_cstr(&executable_path), 0744)); + git_str_dispose(&executable_path); + + git_str_joinpath(&nonexecutable_path, git_repository_workdir(repo), "exec_off"); + + cl_must_pass(p_chmod(git_str_cstr(&nonexecutable_path), 0655)); + git_str_dispose(&nonexecutable_path); + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = filemode_count; + counts.expected_paths = filemode_paths; + counts.expected_statuses = filemode_statuses; + + cl_git_pass( + git_status_foreach_ext(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 int cb_status__interrupt(const char *p, unsigned int s, void *payload) +{ + volatile int *count = (int *)payload; + + GIT_UNUSED(p); + GIT_UNUSED(s); + + (*count)++; + + return (*count == 8) ? -111 : 0; +} + +void test_status_worktree__interruptable_foreach(void) +{ + int count = 0; + git_repository *repo = cl_git_sandbox_init("status"); + + cl_assert_equal_i( + -111, git_status_foreach(repo, cb_status__interrupt, &count) + ); + + cl_assert_equal_i(8, count); +} + +void test_status_worktree__line_endings_dont_count_as_changes_with_autocrlf(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + unsigned int status; + + cl_repo_set_bool(repo, "core.autocrlf", true); + + cl_git_rewritefile("status/current_file", "current_file\r\n"); + + cl_git_pass(git_status_file(&status, repo, "current_file")); + + /* stat data on file should no longer match stat cache, even though + * file diff will be empty because of line-ending conversion - matches + * the Git command-line behavior here. + */ + cl_assert_equal_i(GIT_STATUS_WT_MODIFIED, status); +} + +void test_status_worktree__line_endings_dont_count_as_changes_with_autocrlf_issue_1397(void) +{ + git_repository *repo = cl_git_sandbox_init("issue_1397"); + unsigned int status; + + cl_repo_set_bool(repo, "core.autocrlf", true); + + cl_git_pass(git_status_file(&status, repo, "crlf_file.txt")); + + cl_assert_equal_i(GIT_STATUS_CURRENT, status); +} + +void test_status_worktree__conflicted_item(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + git_index *index; + unsigned int status; + git_index_entry ancestor_entry, our_entry, their_entry; + + memset(&ancestor_entry, 0x0, sizeof(git_index_entry)); + memset(&our_entry, 0x0, sizeof(git_index_entry)); + memset(&their_entry, 0x0, sizeof(git_index_entry)); + + ancestor_entry.mode = 0100644; + ancestor_entry.path = "modified_file"; + git_oid_fromstr(&ancestor_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + our_entry.mode = 0100644; + our_entry.path = "modified_file"; + git_oid_fromstr(&our_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + their_entry.mode = 0100644; + their_entry.path = "modified_file"; + git_oid_fromstr(&their_entry.id, + "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + cl_git_pass(git_status_file(&status, repo, "modified_file")); + cl_assert_equal_i(GIT_STATUS_WT_MODIFIED, status); + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_conflict_add(index, &ancestor_entry, + &our_entry, &their_entry)); + + cl_git_pass(git_status_file(&status, repo, "modified_file")); + cl_assert_equal_i(GIT_STATUS_CONFLICTED, status); + + git_index_free(index); +} + +void test_status_worktree__conflict_has_no_oid(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + git_index *index; + git_index_entry entry = {{0}}; + git_status_list *statuslist; + const git_status_entry *status; + git_oid zero_id = {{0}}; + + entry.mode = 0100644; + entry.path = "modified_file"; + git_oid_fromstr(&entry.id, "452e4244b5d083ddf0460acf1ecc74db9dcfa11a"); + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_conflict_add(index, &entry, &entry, &entry)); + + git_status_list_new(&statuslist, repo, NULL); + + cl_assert_equal_i(16, git_status_list_entrycount(statuslist)); + + status = git_status_byindex(statuslist, 2); + + cl_assert_equal_i(GIT_STATUS_CONFLICTED, status->status); + cl_assert_equal_s("modified_file", status->head_to_index->old_file.path); + cl_assert(!git_oid_equal(&zero_id, &status->head_to_index->old_file.id)); + cl_assert(0 != status->head_to_index->old_file.mode); + cl_assert_equal_s("modified_file", status->head_to_index->new_file.path); + cl_assert_equal_oid(&zero_id, &status->head_to_index->new_file.id); + cl_assert_equal_i(0, status->head_to_index->new_file.mode); + cl_assert_equal_i(0, status->head_to_index->new_file.size); + + cl_assert_equal_s("modified_file", status->index_to_workdir->old_file.path); + cl_assert_equal_oid(&zero_id, &status->index_to_workdir->old_file.id); + cl_assert_equal_i(0, status->index_to_workdir->old_file.mode); + cl_assert_equal_i(0, status->index_to_workdir->old_file.size); + cl_assert_equal_s("modified_file", status->index_to_workdir->new_file.path); + cl_assert( + !git_oid_equal(&zero_id, &status->index_to_workdir->new_file.id) || + !(status->index_to_workdir->new_file.flags & GIT_DIFF_FLAG_VALID_ID)); + cl_assert(0 != status->index_to_workdir->new_file.mode); + cl_assert(0 != status->index_to_workdir->new_file.size); + + git_index_free(index); + git_status_list_free(statuslist); +} + +static void assert_ignore_case( + bool should_ignore_case, + int expected_lower_cased_file_status, + int expected_camel_cased_file_status) +{ + unsigned int status; + git_str lower_case_path = GIT_STR_INIT, camel_case_path = GIT_STR_INIT; + git_repository *repo, *repo2; + + repo = cl_git_sandbox_init("empty_standard_repo"); + cl_git_remove_placeholders(git_repository_path(repo), "dummy-marker.txt"); + + cl_repo_set_bool(repo, "core.ignorecase", should_ignore_case); + + cl_git_pass(git_str_joinpath(&lower_case_path, + git_repository_workdir(repo), "plop")); + + cl_git_mkfile(git_str_cstr(&lower_case_path), ""); + + stage_and_commit(repo, "plop"); + + cl_git_pass(git_repository_open(&repo2, "./empty_standard_repo")); + + cl_git_pass(git_status_file(&status, repo2, "plop")); + cl_assert_equal_i(GIT_STATUS_CURRENT, status); + + cl_git_pass(git_str_joinpath(&camel_case_path, + git_repository_workdir(repo), "Plop")); + + cl_git_pass(p_rename(git_str_cstr(&lower_case_path), git_str_cstr(&camel_case_path))); + + cl_git_pass(git_status_file(&status, repo2, "plop")); + cl_assert_equal_i(expected_lower_cased_file_status, status); + + cl_git_pass(git_status_file(&status, repo2, "Plop")); + cl_assert_equal_i(expected_camel_cased_file_status, status); + + git_repository_free(repo2); + git_str_dispose(&lower_case_path); + git_str_dispose(&camel_case_path); +} + +void test_status_worktree__file_status_honors_core_ignorecase_true(void) +{ + assert_ignore_case(true, GIT_STATUS_CURRENT, GIT_STATUS_CURRENT); +} + +void test_status_worktree__file_status_honors_core_ignorecase_false(void) +{ + assert_ignore_case(false, GIT_STATUS_WT_DELETED, GIT_STATUS_WT_NEW); +} + +void test_status_worktree__file_status_honors_case_ignorecase_regarding_untracked_files(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + unsigned int status; + git_index *index; + + cl_repo_set_bool(repo, "core.ignorecase", false); + + repo = cl_git_sandbox_reopen(); + + /* Actually returns GIT_STATUS_IGNORED on Windows */ + cl_git_fail_with(git_status_file(&status, repo, "NEW_FILE"), GIT_ENOTFOUND); + + cl_git_pass(git_repository_index(&index, repo)); + + cl_git_pass(git_index_add_bypath(index, "new_file")); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + /* Actually returns GIT_STATUS_IGNORED on Windows */ + cl_git_fail_with(git_status_file(&status, repo, "NEW_FILE"), GIT_ENOTFOUND); +} + +void test_status_worktree__simple_delete(void) +{ + git_repository *repo = cl_git_sandbox_init("renames"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + int count; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH | + GIT_STATUS_OPT_EXCLUDE_SUBMODULES | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + count = 0; + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__count, &count) ); + cl_assert_equal_i(0, count); + + cl_must_pass(p_unlink("renames/untimely.txt")); + + count = 0; + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__count, &count) ); + cl_assert_equal_i(1, count); +} + +void test_status_worktree__simple_delete_indexed(void) +{ + git_repository *repo = cl_git_sandbox_init("renames"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *status; + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH | + GIT_STATUS_OPT_EXCLUDE_SUBMODULES | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + cl_git_pass(git_status_list_new(&status, repo, &opts)); + cl_assert_equal_sz(0, git_status_list_entrycount(status)); + git_status_list_free(status); + + cl_must_pass(p_unlink("renames/untimely.txt")); + + cl_git_pass(git_status_list_new(&status, repo, &opts)); + cl_assert_equal_sz(1, git_status_list_entrycount(status)); + cl_assert_equal_i( + GIT_STATUS_WT_DELETED, git_status_byindex(status, 0)->status); + git_status_list_free(status); +} + +static const char *icase_paths[] = { "B", "c", "g", "H" }; +static unsigned int icase_statuses[] = { + GIT_STATUS_WT_MODIFIED, GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_MODIFIED, GIT_STATUS_WT_DELETED, +}; + +static const char *case_paths[] = { "B", "H", "c", "g" }; +static unsigned int case_statuses[] = { + GIT_STATUS_WT_MODIFIED, GIT_STATUS_WT_DELETED, + GIT_STATUS_WT_DELETED, GIT_STATUS_WT_MODIFIED, +}; + +void test_status_worktree__sorting_by_case(void) +{ + git_repository *repo = cl_git_sandbox_init("icase"); + git_index *index; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + bool native_ignore_case; + status_entry_counts counts; + + cl_git_pass(git_repository_index(&index, repo)); + native_ignore_case = + (git_index_caps(index) & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0; + git_index_free(index); + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = 0; + counts.expected_paths = NULL; + counts.expected_statuses = NULL; + cl_git_pass( + git_status_foreach_ext(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_git_rewritefile("icase/B", "new stuff"); + cl_must_pass(p_unlink("icase/c")); + cl_git_rewritefile("icase/g", "new stuff"); + cl_must_pass(p_unlink("icase/H")); + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = 4; + if (native_ignore_case) { + counts.expected_paths = icase_paths; + counts.expected_statuses = icase_statuses; + } else { + counts.expected_paths = case_paths; + counts.expected_statuses = case_statuses; + } + cl_git_pass( + git_status_foreach_ext(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); + + opts.flags = GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = 4; + counts.expected_paths = case_paths; + counts.expected_statuses = case_statuses; + cl_git_pass( + git_status_foreach_ext(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); + + opts.flags = GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY; + + memset(&counts, 0, sizeof(counts)); + counts.expected_entry_count = 4; + counts.expected_paths = icase_paths; + counts.expected_statuses = icase_statuses; + cl_git_pass( + git_status_foreach_ext(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_status_worktree__long_filenames(void) +{ + char path[260*4+1] = {0}; + const char *expected_paths[] = {path}; + const unsigned int expected_statuses[] = {GIT_STATUS_WT_NEW}; + + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts = {0}; + + /* Create directory with amazingly long filename */ + sprintf(path, "empty_standard_repo/%s", longname); + cl_git_pass(git_futils_mkdir_r(path, 0777)); + sprintf(path, "empty_standard_repo/%s/foo", longname); + cl_git_mkfile(path, "dummy"); + + sprintf(path, "%s/foo", longname); + counts.expected_entry_count = 1; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY; + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + cl_git_pass( + git_status_foreach_ext(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); +} + +/* The update stat cache tests mostly just mirror other tests and try + * to make sure that updating the stat cache doesn't change the results + * while reducing the amount of work that needs to be done + */ + +static void check_status0(git_status_list *status) +{ + size_t i, max_i = git_status_list_entrycount(status); + cl_assert_equal_sz(entry_count0, max_i); + for (i = 0; i < max_i; ++i) { + const git_status_entry *entry = git_status_byindex(status, i); + cl_assert_equal_i(entry_statuses0[i], entry->status); + } +} + +void test_status_worktree__update_stat_cache_0(void) +{ + git_repository *repo = cl_git_sandbox_init("status"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *status; + git_diff_perfdata perf = GIT_DIFF_PERFDATA_INIT; + git_index *index; + + opts.flags = GIT_STATUS_OPT_DEFAULTS; + + cl_git_pass(git_status_list_new(&status, repo, &opts)); + check_status0(status); + cl_git_pass(git_status_list_get_perfdata(&perf, status)); + cl_assert_equal_sz(13 + 3, perf.stat_calls); + cl_assert_equal_sz(5, perf.oid_calculations); + + git_status_list_free(status); + + /* tick the index so we avoid recalculating racily-clean entries */ + cl_git_pass(git_repository_index__weakptr(&index, repo)); + tick_index(index); + + opts.flags |= GIT_STATUS_OPT_UPDATE_INDEX; + + cl_git_pass(git_status_list_new(&status, repo, &opts)); + check_status0(status); + cl_git_pass(git_status_list_get_perfdata(&perf, status)); + cl_assert_equal_sz(13 + 3, perf.stat_calls); + cl_assert_equal_sz(5, perf.oid_calculations); + + git_status_list_free(status); + + opts.flags &= ~GIT_STATUS_OPT_UPDATE_INDEX; + + /* tick again as the index updating from the previous diff might have reset the timestamp */ + tick_index(index); + cl_git_pass(git_status_list_new(&status, repo, &opts)); + check_status0(status); + cl_git_pass(git_status_list_get_perfdata(&perf, status)); + cl_assert_equal_sz(13 + 3, perf.stat_calls); + cl_assert_equal_sz(0, perf.oid_calculations); + + git_status_list_free(status); +} + +void test_status_worktree__unreadable(void) +{ +#ifndef GIT_WIN32 + const char *expected_paths[] = { "no_permission/foo" }; + const unsigned int expected_statuses[] = {GIT_STATUS_WT_UNREADABLE}; + + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts = {0}; + + if (geteuid() == 0) + cl_skip(); + + /* Create directory with no read permission */ + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/no_permission", 0777)); + cl_git_mkfile("empty_standard_repo/no_permission/foo", "dummy"); + p_chmod("empty_standard_repo/no_permission", 0644); + + counts.expected_entry_count = 1; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY; + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_INCLUDE_UNREADABLE; + + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__normal, &counts) ); + + /* Restore permissions so we can cleanup :) */ + p_chmod("empty_standard_repo/no_permission", 0777); + + 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); +#else + cl_skip(); +#endif +} + +void test_status_worktree__unreadable_not_included(void) +{ +#ifndef GIT_WIN32 + const char *expected_paths[] = { "no_permission/" }; + const unsigned int expected_statuses[] = {GIT_STATUS_WT_NEW}; + + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts = {0}; + + /* Create directory with no read permission */ + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/no_permission", 0777)); + cl_git_mkfile("empty_standard_repo/no_permission/foo", "dummy"); + p_chmod("empty_standard_repo/no_permission", 0644); + + counts.expected_entry_count = 1; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY; + opts.flags = (GIT_STATUS_OPT_INCLUDE_IGNORED | GIT_STATUS_OPT_INCLUDE_UNTRACKED); + + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__normal, &counts) ); + + /* Restore permissions so we can cleanup :) */ + p_chmod("empty_standard_repo/no_permission", 0777); + + 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); +#else + cl_skip(); +#endif +} + +void test_status_worktree__unreadable_as_untracked(void) +{ + const char *expected_paths[] = { "no_permission/foo" }; + const unsigned int expected_statuses[] = {GIT_STATUS_WT_NEW}; + + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts = {0}; + + /* Create directory with no read permission */ + cl_git_pass(git_futils_mkdir_r("empty_standard_repo/no_permission", 0777)); + cl_git_mkfile("empty_standard_repo/no_permission/foo", "dummy"); + p_chmod("empty_standard_repo/no_permission", 0644); + + counts.expected_entry_count = 1; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY; + opts.flags = GIT_STATUS_OPT_DEFAULTS | + GIT_STATUS_OPT_INCLUDE_UNREADABLE | + GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED; + + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__normal, &counts) ); + + /* Restore permissions so we can cleanup :) */ + p_chmod("empty_standard_repo/no_permission", 0777); + + 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_status_worktree__update_index_with_symlink_doesnt_change_mode(void) +{ + git_repository *repo = cl_git_sandbox_init("testrepo"); + git_reference *head; + git_object *head_object; + git_index *index; + const git_index_entry *idx_entry; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_counts counts = {0}; + const char *expected_paths[] = { "README" }; + const unsigned int expected_statuses[] = {GIT_STATUS_WT_NEW}; + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_UPDATE_INDEX; + + cl_git_pass(git_repository_head(&head, repo)); + cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJECT_COMMIT)); + + cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); + + cl_git_rewritefile("testrepo/README", "This was rewritten."); + + /* this status rewrites the index because we have changed the + * contents of a tracked file + */ + counts.expected_entry_count = 1; + counts.expected_paths = expected_paths; + counts.expected_statuses = expected_statuses; + + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__normal, &counts)); + cl_assert_equal_i(1, counts.entry_count); + + /* now ensure that the status's rewrite of the index did not screw + * up the mode of the symlink `link_to_new.txt`, particularly + * on platforms that don't support symlinks + */ + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_read(index, true)); + + cl_assert(idx_entry = git_index_get_bypath(index, "link_to_new.txt", 0)); + cl_assert(S_ISLNK(idx_entry->mode)); + + git_index_free(index); + git_object_free(head_object); + git_reference_free(head); +} + +static const char *testrepo2_subdir_paths[] = { + "subdir/README", + "subdir/new.txt", + "subdir/subdir2/README", + "subdir/subdir2/new.txt", +}; + +static const char *testrepo2_subdir_paths_icase[] = { + "subdir/new.txt", + "subdir/README", + "subdir/subdir2/new.txt", + "subdir/subdir2/README" +}; + +void test_status_worktree__with_directory_in_pathlist(void) +{ + git_repository *repo = cl_git_sandbox_init("testrepo2"); + git_index *index; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *statuslist; + const git_status_entry *status; + size_t i, entrycount; + bool native_ignore_case; + char *subdir_path = "subdir"; + + cl_git_pass(git_repository_index(&index, repo)); + native_ignore_case = + (git_index_caps(index) & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0; + git_index_free(index); + + opts.pathspec.strings = &subdir_path; + opts.pathspec.count = 1; + opts.flags = + GIT_STATUS_OPT_DEFAULTS | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH; + + opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY; + git_status_list_new(&statuslist, repo, &opts); + + entrycount = git_status_list_entrycount(statuslist); + cl_assert_equal_i(4, entrycount); + + for (i = 0; i < entrycount; i++) { + status = git_status_byindex(statuslist, i); + cl_assert_equal_i(0, status->status); + cl_assert_equal_s(native_ignore_case ? + testrepo2_subdir_paths_icase[i] : + testrepo2_subdir_paths[i], + status->index_to_workdir->old_file.path); + } + + git_status_list_free(statuslist); + + opts.show = GIT_STATUS_SHOW_INDEX_ONLY; + git_status_list_new(&statuslist, repo, &opts); + + entrycount = git_status_list_entrycount(statuslist); + cl_assert_equal_i(4, entrycount); + + for (i = 0; i < entrycount; i++) { + status = git_status_byindex(statuslist, i); + cl_assert_equal_i(0, status->status); + cl_assert_equal_s(native_ignore_case ? + testrepo2_subdir_paths_icase[i] : + testrepo2_subdir_paths[i], + status->head_to_index->old_file.path); + } + + git_status_list_free(statuslist); + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + git_status_list_new(&statuslist, repo, &opts); + + entrycount = git_status_list_entrycount(statuslist); + cl_assert_equal_i(4, entrycount); + + for (i = 0; i < entrycount; i++) { + status = git_status_byindex(statuslist, i); + cl_assert_equal_i(0, status->status); + cl_assert_equal_s(native_ignore_case ? + testrepo2_subdir_paths_icase[i] : + testrepo2_subdir_paths[i], + status->index_to_workdir->old_file.path); + } + + git_status_list_free(statuslist); +} + +void test_status_worktree__at_head_parent(void) +{ + git_repository *repo = cl_git_sandbox_init("empty_standard_repo"); + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *statuslist; + git_tree *parent_tree; + const git_status_entry *status; + + cl_git_mkfile("empty_standard_repo/file1", "ping"); + stage_and_commit(repo, "file1"); + + cl_git_pass(git_repository_head_tree(&parent_tree, repo)); + + cl_git_mkfile("empty_standard_repo/file2", "pong"); + stage_and_commit(repo, "file2"); + + cl_git_rewritefile("empty_standard_repo/file2", "pyng"); + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.baseline = parent_tree; + cl_git_pass(git_status_list_new(&statuslist, repo, &opts)); + + cl_assert_equal_sz(1, git_status_list_entrycount(statuslist)); + status = git_status_byindex(statuslist, 0); + cl_assert(status != NULL); + cl_assert_equal_s("file2", status->index_to_workdir->old_file.path); + cl_assert_equal_i(GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_NEW, status->status); + + git_tree_free(parent_tree); + git_status_list_free(statuslist); +} diff --git a/tests/libgit2/status/worktree_init.c b/tests/libgit2/status/worktree_init.c new file mode 100644 index 000000000..40f1b2a31 --- /dev/null +++ b/tests/libgit2/status/worktree_init.c @@ -0,0 +1,338 @@ +#include "clar_libgit2.h" +#include "git2/sys/repository.h" + +#include "futils.h" +#include "ignore.h" +#include "status_helpers.h" +#include "posix.h" +#include "util.h" +#include "path.h" + +static void cleanup_new_repo(void *path) +{ + cl_fixture_cleanup((char *)path); +} + +void test_status_worktree_init__cannot_retrieve_the_status_of_a_bare_repository(void) +{ + git_repository *repo; + unsigned int status = 0; + + cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git"))); + cl_assert_equal_i(GIT_EBAREREPO, git_status_file(&status, repo, "dummy")); + git_repository_free(repo); +} + +void test_status_worktree_init__first_commit_in_progress(void) +{ + git_repository *repo; + git_index *index; + status_entry_single result; + + cl_set_cleanup(&cleanup_new_repo, "getting_started"); + + cl_git_pass(git_repository_init(&repo, "getting_started", 0)); + cl_git_mkfile("getting_started/testfile.txt", "content\n"); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(1, result.count); + cl_assert(result.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, "testfile.txt")); + cl_git_pass(git_index_write(index)); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(1, result.count); + cl_assert(result.status == GIT_STATUS_INDEX_NEW); + + git_index_free(index); + git_repository_free(repo); +} + + + +void test_status_worktree_init__status_file_without_index_or_workdir(void) +{ + git_repository *repo; + unsigned int status = 0; + git_index *index; + + cl_git_pass(p_mkdir("wd", 0777)); + + cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git"))); + cl_git_pass(git_repository_set_workdir(repo, "wd", false)); + + cl_git_pass(git_index_open(&index, "empty-index")); + cl_assert_equal_i(0, (int)git_index_entrycount(index)); + git_repository_set_index(repo, index); + + cl_git_pass(git_status_file(&status, repo, "branch_file.txt")); + + cl_assert_equal_i(GIT_STATUS_INDEX_DELETED, status); + + git_repository_free(repo); + git_index_free(index); + cl_git_pass(p_rmdir("wd")); +} + +static void fill_index_wth_head_entries(git_repository *repo, git_index *index) +{ + git_oid oid; + git_commit *commit; + git_tree *tree; + + cl_git_pass(git_reference_name_to_id(&oid, repo, "HEAD")); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_commit_tree(&tree, commit)); + + cl_git_pass(git_index_read_tree(index, tree)); + cl_git_pass(git_index_write(index)); + + git_tree_free(tree); + git_commit_free(commit); +} + +void test_status_worktree_init__status_file_with_clean_index_and_empty_workdir(void) +{ + git_repository *repo; + unsigned int status = 0; + git_index *index; + + cl_git_pass(p_mkdir("wd", 0777)); + + cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git"))); + cl_git_pass(git_repository_set_workdir(repo, "wd", false)); + + cl_git_pass(git_index_open(&index, "my-index")); + fill_index_wth_head_entries(repo, index); + + git_repository_set_index(repo, index); + + cl_git_pass(git_status_file(&status, repo, "branch_file.txt")); + + cl_assert_equal_i(GIT_STATUS_WT_DELETED, status); + + git_repository_free(repo); + git_index_free(index); + cl_git_pass(p_rmdir("wd")); + cl_git_pass(p_unlink("my-index")); +} + +void test_status_worktree_init__bracket_in_filename(void) +{ + git_repository *repo; + git_index *index; + status_entry_single result; + unsigned int status_flags; + + #define FILE_WITH_BRACKET "LICENSE[1].md" + #define FILE_WITHOUT_BRACKET "LICENSE1.md" + + cl_set_cleanup(&cleanup_new_repo, "with_bracket"); + + cl_git_pass(git_repository_init(&repo, "with_bracket", 0)); + cl_git_mkfile("with_bracket/" FILE_WITH_BRACKET, "I have a bracket in my name\n"); + + /* file is new to working directory */ + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(1, result.count); + cl_assert(result.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET)); + cl_assert(status_flags == GIT_STATUS_WT_NEW); + + /* ignore the file */ + + cl_git_rewritefile("with_bracket/.gitignore", "*.md\n.gitignore\n"); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_IGNORED); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET)); + cl_assert(status_flags == GIT_STATUS_IGNORED); + + /* don't ignore the file */ + + cl_git_rewritefile("with_bracket/.gitignore", ".gitignore\n"); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET)); + cl_assert(status_flags == GIT_STATUS_WT_NEW); + + /* add the file to the index */ + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, FILE_WITH_BRACKET)); + cl_git_pass(git_index_write(index)); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_INDEX_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET)); + cl_assert(status_flags == GIT_STATUS_INDEX_NEW); + + /* Create file without bracket */ + + cl_git_mkfile("with_bracket/" FILE_WITHOUT_BRACKET, "I have no bracket in my name!\n"); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITHOUT_BRACKET)); + cl_assert(status_flags == GIT_STATUS_WT_NEW); + + cl_git_fail_with(git_status_file(&status_flags, repo, "LICENSE\\[1\\].md"), GIT_ENOTFOUND); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_BRACKET)); + cl_assert(status_flags == GIT_STATUS_INDEX_NEW); + + git_index_free(index); + git_repository_free(repo); +} + +void test_status_worktree_init__space_in_filename(void) +{ + git_repository *repo; + git_index *index; + status_entry_single result; + unsigned int status_flags; + +#define FILE_WITH_SPACE "LICENSE - copy.md" + + cl_set_cleanup(&cleanup_new_repo, "with_space"); + cl_git_pass(git_repository_init(&repo, "with_space", 0)); + cl_git_mkfile("with_space/" FILE_WITH_SPACE, "I have a space in my name\n"); + + /* file is new to working directory */ + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(1, result.count); + cl_assert(result.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_SPACE)); + cl_assert(status_flags == GIT_STATUS_WT_NEW); + + /* ignore the file */ + + cl_git_rewritefile("with_space/.gitignore", "*.md\n.gitignore\n"); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_IGNORED); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_SPACE)); + cl_assert(status_flags == GIT_STATUS_IGNORED); + + /* don't ignore the file */ + + cl_git_rewritefile("with_space/.gitignore", ".gitignore\n"); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_WT_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_SPACE)); + cl_assert(status_flags == GIT_STATUS_WT_NEW); + + /* add the file to the index */ + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, FILE_WITH_SPACE)); + cl_git_pass(git_index_write(index)); + + memset(&result, 0, sizeof(result)); + cl_git_pass(git_status_foreach(repo, cb_status__single, &result)); + cl_assert_equal_i(2, result.count); + cl_assert(result.status == GIT_STATUS_INDEX_NEW); + + cl_git_pass(git_status_file(&status_flags, repo, FILE_WITH_SPACE)); + cl_assert(status_flags == GIT_STATUS_INDEX_NEW); + + git_index_free(index); + git_repository_free(repo); +} + +static int cb_status__expected_path(const char *p, unsigned int s, void *payload) +{ + const char *expected_path = (const char *)payload; + + GIT_UNUSED(s); + + if (payload == NULL) + cl_fail("Unexpected path"); + + cl_assert_equal_s(expected_path, p); + + return 0; +} + +void test_status_worktree_init__disable_pathspec_match(void) +{ + git_repository *repo; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + char *file_with_bracket = "LICENSE[1].md", + *imaginary_file_with_bracket = "LICENSE[1-2].md"; + + cl_set_cleanup(&cleanup_new_repo, "pathspec"); + cl_git_pass(git_repository_init(&repo, "pathspec", 0)); + cl_git_mkfile("pathspec/LICENSE[1].md", "screaming bracket\n"); + cl_git_mkfile("pathspec/LICENSE1.md", "no bracket\n"); + + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH; + opts.pathspec.count = 1; + opts.pathspec.strings = &file_with_bracket; + + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__expected_path, + file_with_bracket) + ); + + /* Test passing a pathspec matching files in the workdir. */ + /* Must not match because pathspecs are disabled. */ + opts.pathspec.strings = &imaginary_file_with_bracket; + cl_git_pass( + git_status_foreach_ext(repo, &opts, cb_status__expected_path, NULL) + ); + + git_repository_free(repo); +} + +void test_status_worktree_init__new_staged_file_must_handle_crlf(void) +{ + git_repository *repo; + git_index *index; + unsigned int status; + + cl_set_cleanup(&cleanup_new_repo, "getting_started"); + cl_git_pass(git_repository_init(&repo, "getting_started", 0)); + + /* Ensure that repo has core.autocrlf=true */ + cl_repo_set_bool(repo, "core.autocrlf", true); + + cl_git_mkfile("getting_started/testfile.txt", "content\r\n"); /* Content with CRLF */ + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_add_bypath(index, "testfile.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_status_file(&status, repo, "testfile.txt")); + cl_assert_equal_i(GIT_STATUS_INDEX_NEW, status); + + git_index_free(index); + git_repository_free(repo); +} + |
