Overwrite ignored files on checkout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
diff --git a/include/git2/checkout.h b/include/git2/checkout.h
index 0e9d338..b94a5e2 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 0f30d16..50da83a 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 d2e92f8..10a44b6 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;