Commit a7ecd1a9e36df5d6843c1863542c02d777e9e8b5

Vicent Marti 2013-12-13T07:29:27

Merge pull request #2000 from ethomson/overwrite_ignored Overwrite ignored files on checkout

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;