Merge pull request #2000 from ethomson/overwrite_ignored 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 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
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..e642c97 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))
@@ -431,6 +435,7 @@ static int checkout_action_with_wd_dir(
int *action,
checkout_data *data,
const git_diff_delta *delta,
+ git_iterator *workdir,
const git_index_entry *wd)
{
*action = CHECKOUT_ACTION__NONE;
@@ -447,7 +452,9 @@ static int checkout_action_with_wd_dir(
if (delta->old_file.mode == GIT_FILEMODE_COMMIT)
/* expected submodule (and maybe found one) */;
else if (delta->new_file.mode != GIT_FILEMODE_TREE)
- *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT);
+ *action = git_iterator_current_is_ignored(workdir) ?
+ CHECKOUT_ACTION_IF(DONT_OVERWRITE_IGNORED, CONFLICT, REMOVE_AND_UPDATE) :
+ CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT);
break;
case GIT_DELTA_DELETED: /* case 11 (and 27 for dir) */
if (delta->old_file.mode != GIT_FILEMODE_TREE)
@@ -541,7 +548,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 +561,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,13 +570,13 @@ 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;
}
}
- return checkout_action_with_wd_dir(action, data, delta, wd);
+ return checkout_action_with_wd_dir(action, data, delta, workdir, wd);
}
/* case 6 - wd is after delta */
@@ -1017,8 +1024,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..06aa6a5 100644
--- a/tests/checkout/tree.c
+++ b/tests/checkout/tree.c
@@ -235,6 +235,113 @@ 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, bool isdir)
+{
+ 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;
+
+ if (isdir) {
+ cl_must_pass(p_mkdir("testrepo/ab", 0777));
+ cl_must_pass(p_mkdir("testrepo/ab/4.txt", 0777));
+
+ cl_git_mkfile("testrepo/ab/4.txt/file1.txt", "as you wish");
+ cl_git_mkfile("testrepo/ab/4.txt/file2.txt", "foo bar foo");
+ cl_git_mkfile("testrepo/ab/4.txt/file3.txt", "inky blinky pinky clyde");
+
+ cl_assert(git_path_isdir("testrepo/ab/4.txt"));
+ } else {
+ cl_must_pass(p_mkdir("testrepo/ab", 0777));
+ cl_git_mkfile("testrepo/ab/4.txt", "as you wish");
+
+ cl_assert(git_path_isfile("testrepo/ab/4.txt"));
+ }
+
+ 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_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, false));
+
+ 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, false));
+
+ 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__conflict_on_ignored_folder_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, true));
+
+ cl_assert_equal_i(GIT_EMERGECONFLICT, error);
+}
+
+void test_checkout_tree__can_overwrite_ignored_folder_by_default(void)
+{
+ cl_git_pass(checkout_tree_with_blob_ignored_in_workdir(GIT_CHECKOUT_SAFE, true));
+
+ 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;