Commit c9d59c6140fc365eb7ab950fb1a33187a949d403

Edward Thomson 2018-02-27T12:45:21

Merge pull request #4545 from libgit2/ethomson/checkout_filemode Respect core.filemode in checkout

diff --git a/src/checkout.c b/src/checkout.c
index 2ad736a..8ff5d89 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -70,6 +70,7 @@ typedef struct {
 	git_buf tmp;
 	unsigned int strategy;
 	int can_symlink;
+	int respect_filemode;
 	bool reload_submodules;
 	size_t total_steps;
 	size_t completed_steps;
@@ -159,17 +160,20 @@ GIT_INLINE(bool) is_workdir_base_or_new(
 		git_oid__cmp(&newitem->id, workdir_id) == 0);
 }
 
-GIT_INLINE(bool) is_file_mode_changed(git_filemode_t a, git_filemode_t b)
+GIT_INLINE(bool) is_filemode_changed(git_filemode_t a, git_filemode_t b, int respect_filemode)
 {
-#ifdef GIT_WIN32
-	/*
-	 * On Win32 we do not support the executable bit; the file will
-	 * always be 0100644 on disk, don't bother doing a test.
-	 */
-	return false;
-#else
-	return (S_ISREG(a) && S_ISREG(b) && a != b);
-#endif
+	/* If core.filemode = false, ignore links in the repository and executable bit changes */
+	if (!respect_filemode) {
+		if (a == S_IFLNK)
+			a = GIT_FILEMODE_BLOB;
+		if (b == S_IFLNK)
+			b = GIT_FILEMODE_BLOB;
+
+		a &= ~0111;
+		b &= ~0111;
+	}
+
+	return (a != b);
 }
 
 static bool checkout_is_workdir_modified(
@@ -217,11 +221,11 @@ static bool checkout_is_workdir_modified(
 	if (ie != NULL &&
 		git_index_time_eq(&wditem->mtime, &ie->mtime) &&
 		wditem->file_size == ie->file_size &&
-		!is_file_mode_changed(wditem->mode, ie->mode)) {
+		!is_filemode_changed(wditem->mode, ie->mode, data->respect_filemode)) {
 
 		/* The workdir is modified iff the index entry is modified */
 		return !is_workdir_base_or_new(&ie->id, baseitem, newitem) ||
-			is_file_mode_changed(baseitem->mode, ie->mode);
+			is_filemode_changed(baseitem->mode, ie->mode, data->respect_filemode);
 	}
 
 	/* depending on where base is coming from, we may or may not know
@@ -234,7 +238,7 @@ static bool checkout_is_workdir_modified(
 	if (S_ISDIR(wditem->mode))
 		return false;
 
-	if (is_file_mode_changed(baseitem->mode, wditem->mode))
+	if (is_filemode_changed(baseitem->mode, wditem->mode, data->respect_filemode))
 		return true;
 
 	if (git_diff__oid_for_entry(&oid, data->diff, wditem, wditem->mode, NULL) < 0)
@@ -2454,6 +2458,10 @@ static int checkout_data_init(
 			 &data->can_symlink, repo, GIT_CVAR_SYMLINKS)) < 0)
 		goto cleanup;
 
+	if ((error = git_repository__cvar(
+			 &data->respect_filemode, repo, GIT_CVAR_FILEMODE)) < 0)
+		goto cleanup;
+
 	if (!data->opts.baseline && !data->opts.baseline_index) {
 		data->opts_free_baseline = true;
 		error = 0;
diff --git a/tests/checkout/head.c b/tests/checkout/head.c
index 46b2257..9906146 100644
--- a/tests/checkout/head.c
+++ b/tests/checkout/head.c
@@ -181,3 +181,86 @@ void test_checkout_head__typechange_index_and_workdir(void)
 	git_object_free(target);
 	git_index_free(index);
 }
+
+void test_checkout_head__workdir_filemode_is_simplified(void)
+{
+	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_object *target, *branch;
+
+	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+
+	cl_git_pass(git_revparse_single(&target, g_repo, "a38d028f71eaa590febb7d716b1ca32350cf70da"));
+	cl_git_pass(git_reset(g_repo, target, GIT_RESET_HARD, NULL));
+
+	cl_must_pass(p_chmod("testrepo/branch_file.txt", 0666));
+
+	/*
+	 * Checkout should not fail with a conflict; though the file mode
+	 * on disk is literally different to the base (0666 vs 0644), Git
+	 * ignores the actual mode and simply treats both as non-executable.
+	 */
+	cl_git_pass(git_revparse_single(&branch, g_repo, "099fabac3a9ea935598528c27f866e34089c2eff"));
+
+	opts.checkout_strategy &= ~GIT_CHECKOUT_FORCE;
+	opts.checkout_strategy |=  GIT_CHECKOUT_SAFE;
+	cl_git_pass(git_checkout_tree(g_repo, branch, NULL));
+
+	git_object_free(branch);
+	git_object_free(target);
+}
+
+void test_checkout_head__obeys_filemode_true(void)
+{
+	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_object *target, *branch;
+
+	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+
+	/* In this commit, `README` is executable */
+	cl_git_pass(git_revparse_single(&target, g_repo, "f9ed4af42472941da45a3c"));
+	cl_git_pass(git_reset(g_repo, target, GIT_RESET_HARD, NULL));
+
+	cl_repo_set_bool(g_repo, "core.filemode", true);
+	cl_must_pass(p_chmod("testrepo/README", 0644));
+
+	/*
+	 * Checkout will fail with a conflict; the file mode is updated in
+	 * the checkout target, but the contents have changed in our branch.
+	 */
+	cl_git_pass(git_revparse_single(&branch, g_repo, "099fabac3a9ea935598528c27f866e34089c2eff"));
+
+	opts.checkout_strategy &= ~GIT_CHECKOUT_FORCE;
+	opts.checkout_strategy |=  GIT_CHECKOUT_SAFE;
+	cl_git_fail_with(GIT_ECONFLICT, git_checkout_tree(g_repo, branch, NULL));
+
+	git_object_free(branch);
+	git_object_free(target);
+}
+
+void test_checkout_head__obeys_filemode_false(void)
+{
+	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_object *target, *branch;
+
+	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+
+	/* In this commit, `README` is executable */
+	cl_git_pass(git_revparse_single(&target, g_repo, "f9ed4af42472941da45a3c"));
+	cl_git_pass(git_reset(g_repo, target, GIT_RESET_HARD, NULL));
+
+	cl_repo_set_bool(g_repo, "core.filemode", false);
+	cl_must_pass(p_chmod("testrepo/README", 0644));
+
+	/*
+	 * Checkout will fail with a conflict; the file contents are updated
+	 * in the checkout target, but the filemode has changed in our branch.
+	 */
+	cl_git_pass(git_revparse_single(&branch, g_repo, "099fabac3a9ea935598528c27f866e34089c2eff"));
+
+	opts.checkout_strategy &= ~GIT_CHECKOUT_FORCE;
+	opts.checkout_strategy |=  GIT_CHECKOUT_SAFE;
+	cl_git_pass(git_checkout_tree(g_repo, branch, NULL));
+
+	git_object_free(branch);
+	git_object_free(target);
+}
diff --git a/tests/iterator/workdir.c b/tests/iterator/workdir.c
index 198edc7..8101675 100644
--- a/tests/iterator/workdir.c
+++ b/tests/iterator/workdir.c
@@ -610,6 +610,7 @@ void test_iterator_workdir__filesystem2(void)
 	static const char *expect_base[] = {
 		"heads/br2",
 		"heads/dir",
+		"heads/executable",
 		"heads/ident",
 		"heads/long-file-name",
 		"heads/master",
@@ -630,7 +631,7 @@ void test_iterator_workdir__filesystem2(void)
 
 	cl_git_pass(git_iterator_for_filesystem(
 		&i, "testrepo/.git/refs", NULL));
-	expect_iterator_items(i, 15, expect_base, 15, expect_base);
+	expect_iterator_items(i, 16, expect_base, 16, expect_base);
 	git_iterator_free(i);
 }
 
diff --git a/tests/refs/list.c b/tests/refs/list.c
index f7ca3f7..97461fd 100644
--- a/tests/refs/list.c
+++ b/tests/refs/list.c
@@ -36,7 +36,7 @@ void test_refs_list__all(void)
 	/* We have exactly 12 refs in total if we include the packed ones:
 	 * there is a reference that exists both in the packfile and as
 	 * loose, but we only list it once */
-	cl_assert_equal_i((int)ref_list.count, 17);
+	cl_assert_equal_i((int)ref_list.count, 18);
 
 	git_strarray_free(&ref_list);
 }
@@ -51,7 +51,7 @@ void test_refs_list__do_not_retrieve_references_which_name_end_with_a_lock_exten
 		"144344043ba4d4a405da03de3844aa829ae8be0e\n");
 
 	cl_git_pass(git_reference_list(&ref_list, g_repo));
-	cl_assert_equal_i((int)ref_list.count, 17);
+	cl_assert_equal_i((int)ref_list.count, 18);
 
 	git_strarray_free(&ref_list);
 }
diff --git a/tests/resources/testrepo/.gitted/objects/57/43a3ef145d3638a0fa28233ca92897117ad74f b/tests/resources/testrepo/.gitted/objects/57/43a3ef145d3638a0fa28233ca92897117ad74f
new file mode 100644
index 0000000..85abb27
Binary files /dev/null and b/tests/resources/testrepo/.gitted/objects/57/43a3ef145d3638a0fa28233ca92897117ad74f differ
diff --git a/tests/resources/testrepo/.gitted/objects/f9/ed4af42472941da45a3ce44458455ed227a6be b/tests/resources/testrepo/.gitted/objects/f9/ed4af42472941da45a3ce44458455ed227a6be
new file mode 100644
index 0000000..69c52fc
--- /dev/null
+++ b/tests/resources/testrepo/.gitted/objects/f9/ed4af42472941da45a3ce44458455ed227a6be
@@ -0,0 +1,2 @@
+xN[
+B!Uzꁈ~A8W 3K-?v|f&R.]61K-p% d&S6;5u3	9΄h|`Uh8gAk_jyQor#ZR;*1*j@wgǵ|eO
\ No newline at end of file
diff --git a/tests/resources/testrepo/.gitted/refs/heads/executable b/tests/resources/testrepo/.gitted/refs/heads/executable
new file mode 100644
index 0000000..2bdccea
--- /dev/null
+++ b/tests/resources/testrepo/.gitted/refs/heads/executable
@@ -0,0 +1 @@
+f9ed4af42472941da45a3ce44458455ed227a6be
diff --git a/tests/revwalk/basic.c b/tests/revwalk/basic.c
index 547050c..6a23701 100644
--- a/tests/revwalk/basic.c
+++ b/tests/revwalk/basic.c
@@ -177,7 +177,7 @@ void test_revwalk_basic__glob_heads_with_invalid(void)
 		/* walking */;
 
 	/* git log --branches --oneline | wc -l => 16 */
-	cl_assert_equal_i(19, i);
+	cl_assert_equal_i(20, i);
 }
 
 void test_revwalk_basic__push_head(void)