diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 0e9d338c6..b94a5e2ff 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -99,6 +99,11 @@ GIT_BEGIN_DECL * files with unmerged index entries instead. GIT_CHECKOUT_USE_OURS and * GIT_CHECKOUT_USE_THEIRS to proceed with the checkout using either the * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. + * + * - GIT_CHECKOUT_DONT_OVERWRITE_IGNORED prevents ignored files from being + * overwritten. Normally, files that are ignored in the working directory + * are not considered "precious" and may be overwritten if the checkout + * target contains that file. */ typedef enum { GIT_CHECKOUT_NONE = 0, /** default is a dry run, no actual updates */ @@ -144,6 +149,9 @@ typedef enum { /** Ignore directories in use, they will be left empty */ GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES = (1u << 18), + /** Don't overwrite ignored files that exist in the checkout target */ + GIT_CHECKOUT_DONT_OVERWRITE_IGNORED = (1u << 19), + /** * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED */ diff --git a/src/checkout.c b/src/checkout.c index 0f30d16f3..50da83a4a 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -333,6 +333,7 @@ static int checkout_action_with_wd( int *action, checkout_data *data, const git_diff_delta *delta, + git_iterator *workdir, const git_index_entry *wd) { *action = CHECKOUT_ACTION__NONE; @@ -346,7 +347,10 @@ static int checkout_action_with_wd( } break; case GIT_DELTA_ADDED: /* case 3, 4 or 6 */ - *action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + if (git_iterator_current_is_ignored(workdir)) + *action = CHECKOUT_ACTION_IF(DONT_OVERWRITE_IGNORED, CONFLICT, UPDATE_BLOB); + else + *action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); break; case GIT_DELTA_DELETED: /* case 9 or 10 (or 26 but not really) */ if (checkout_is_workdir_modified(data, &delta->old_file, wd)) @@ -541,7 +545,7 @@ static int checkout_action( if (cmp == 0) { /* case 4 */ - error = checkout_action_with_wd(action, data, delta, wd); + error = checkout_action_with_wd(action, data, delta, workdir, wd); advance = git_iterator_advance; goto done; } @@ -554,7 +558,7 @@ static int checkout_action( if (delta->status == GIT_DELTA_TYPECHANGE) { if (delta->old_file.mode == GIT_FILEMODE_TREE) { - error = checkout_action_with_wd(action, data, delta, wd); + error = checkout_action_with_wd(action, data, delta, workdir, wd); advance = git_iterator_advance_into; goto done; } @@ -563,7 +567,7 @@ static int checkout_action( delta->new_file.mode == GIT_FILEMODE_COMMIT || delta->old_file.mode == GIT_FILEMODE_COMMIT) { - error = checkout_action_with_wd(action, data, delta, wd); + error = checkout_action_with_wd(action, data, delta, workdir, wd); advance = git_iterator_advance; goto done; } @@ -1017,8 +1021,10 @@ static int checkout_get_actions( if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && (data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) { - giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout", - (int)counts[CHECKOUT_ACTION__CONFLICT]); + giterr_set(GITERR_CHECKOUT, "%d %s checkout", + (int)counts[CHECKOUT_ACTION__CONFLICT], + counts[CHECKOUT_ACTION__CONFLICT] == 1 ? + "conflict prevents" : "conflicts prevent"); error = GIT_EMERGECONFLICT; goto fail; } diff --git a/tests/checkout/tree.c b/tests/checkout/tree.c index d2e92f8e8..10a44b6b9 100644 --- a/tests/checkout/tree.c +++ b/tests/checkout/tree.c @@ -235,6 +235,80 @@ void test_checkout_tree__can_remove_ignored(void) cl_assert(!git_path_isfile("testrepo/ignored_file")); } +static int checkout_tree_with_blob_ignored_in_workdir(int strategy) +{ + git_oid oid; + git_object *obj = NULL; + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + int ignored = 0, error; + + assert_on_branch(g_repo, "master"); + + /* do first checkout with FORCE because we don't know if testrepo + * base data is clean for a checkout or not + */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/dir")); + cl_git_pass(git_object_lookup(&obj, g_repo, &oid, GIT_OBJ_ANY)); + + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/dir")); + + cl_assert(git_path_isfile("testrepo/README")); + cl_assert(git_path_isfile("testrepo/branch_file.txt")); + cl_assert(git_path_isfile("testrepo/new.txt")); + cl_assert(git_path_isfile("testrepo/a/b.txt")); + + cl_assert(!git_path_isdir("testrepo/ab")); + + assert_on_branch(g_repo, "dir"); + + git_object_free(obj); + + opts.checkout_strategy = strategy; + + cl_must_pass(p_mkdir("testrepo/ab", 0777)); + cl_git_mkfile("testrepo/ab/4.txt", "as you wish"); + + cl_git_pass(git_ignore_add_rule(g_repo, "ab/4.txt\n")); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "ab/4.txt")); + cl_assert_equal_i(1, ignored); + + cl_assert(git_path_isfile("testrepo/ab/4.txt")); + + cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/subtrees")); + cl_git_pass(git_object_lookup(&obj, g_repo, &oid, GIT_OBJ_ANY)); + + error = git_checkout_tree(g_repo, obj, &opts); + + git_object_free(obj); + + return error; +} + +void test_checkout_tree__conflict_on_ignored_when_not_overwriting(void) +{ + int error; + + cl_git_fail(error = checkout_tree_with_blob_ignored_in_workdir( + GIT_CHECKOUT_SAFE | GIT_CHECKOUT_DONT_OVERWRITE_IGNORED)); + + cl_assert_equal_i(GIT_EMERGECONFLICT, error); +} + +void test_checkout_tree__can_overwrite_ignored_by_default(void) +{ + cl_git_pass(checkout_tree_with_blob_ignored_in_workdir(GIT_CHECKOUT_SAFE)); + + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/subtrees")); + + cl_assert(git_path_isfile("testrepo/ab/4.txt")); + + assert_on_branch(g_repo, "subtrees"); +} + void test_checkout_tree__can_update_only(void) { git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;