Commit 0d70f650518163af2b4d46028b1ce9cef71fbc99

Russell Belfer 2013-01-03T10:51:18

Fixing checkout UPDATE_ONLY and adding tests This adds a bunch of new checkout tests and in the process I found a bug in the GIT_CHECKOUT_UPDATE_ONLY flag which I fixed.

diff --git a/src/checkout.c b/src/checkout.c
index 2e13294..3428527 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -810,6 +810,27 @@ static void report_progress(
 			data->opts.progress_payload);
 }
 
+static int checkout_safe_for_update_only(const char *path, mode_t expected_mode)
+{
+	struct stat st;
+
+	if (p_lstat(path, &st) < 0) {
+		/* if doesn't exist, then no error and no update */
+		if (errno == ENOENT || errno == ENOTDIR)
+			return 0;
+
+		/* otherwise, stat error and no update */
+		giterr_set(GITERR_OS, "Failed to stat file '%s'", path);
+		return -1;
+	}
+
+	/* only safe for update if this is the same type of file */
+	if ((st.st_mode & ~0777) == (expected_mode & ~0777))
+		return 1;
+
+	return 0;
+}
+
 static int checkout_blob(
 	checkout_data *data,
 	const git_diff_file *file)
@@ -822,6 +843,13 @@ static int checkout_blob(
 	if (git_buf_puts(&data->path, file->path) < 0)
 		return -1;
 
+	if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) {
+		int rval = checkout_safe_for_update_only(
+			git_buf_cstr(&data->path), file->mode);
+		if (rval <= 0)
+			return rval;
+	}
+
 	if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0)
 		return error;
 
diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c
index 6e7175a..ff5c43a 100644
--- a/tests-clar/checkout/tree.c
+++ b/tests-clar/checkout/tree.c
@@ -2,6 +2,8 @@
 
 #include "git2/checkout.h"
 #include "repository.h"
+#include "buffer.h"
+#include "fileops.h"
 
 static git_repository *g_repo;
 static git_checkout_opts g_opts;
@@ -134,3 +136,172 @@ void test_checkout_tree__doesnt_write_unrequested_files_to_worktree(void)
   git_checkout_tree(g_repo, (git_object*)p_chomped_commit, &opts);
   cl_assert_equal_i(false, git_path_isfile("testrepo/readme.txt"));
 }
+
+static void assert_on_branch(git_repository *repo, const char *branch)
+{
+	git_reference *head;
+	git_buf bname = GIT_BUF_INIT;
+
+	cl_git_pass(git_reference_lookup(&head, repo, GIT_HEAD_FILE));
+	cl_assert_(git_reference_type(head) == GIT_REF_SYMBOLIC, branch);
+
+	cl_git_pass(git_buf_joinpath(&bname, "refs/heads", branch));
+	cl_assert_equal_s(bname.ptr, git_reference_symbolic_target(head));
+
+	git_reference_free(head);
+	git_buf_free(&bname);
+}
+
+void test_checkout_tree__can_switch_branches(void)
+{
+	git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
+	git_oid oid;
+	git_object *obj = NULL;
+
+	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);
+
+	/* do second checkout safe because we should be clean after first */
+	opts.checkout_strategy = GIT_CHECKOUT_SAFE;
+
+	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));
+
+	cl_git_pass(git_checkout_tree(g_repo, obj, &opts));
+	cl_git_pass(git_repository_set_head(g_repo, "refs/heads/subtrees"));
+
+	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/ab/4.txt"));
+	cl_assert(git_path_isfile("testrepo/ab/c/3.txt"));
+	cl_assert(git_path_isfile("testrepo/ab/de/2.txt"));
+	cl_assert(git_path_isfile("testrepo/ab/de/fgh/1.txt"));
+
+	cl_assert(!git_path_isdir("testrepo/a"));
+
+	assert_on_branch(g_repo, "subtrees");
+
+	git_object_free(obj);
+}
+
+void test_checkout_tree__can_remove_untracked(void)
+{
+	git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
+
+	opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_REMOVE_UNTRACKED;
+
+	cl_git_mkfile("testrepo/untracked_file", "as you wish");
+	cl_assert(git_path_isfile("testrepo/untracked_file"));
+
+	cl_git_pass(git_checkout_head(g_repo, &opts));
+
+	cl_assert(!git_path_isfile("testrepo/untracked_file"));
+}
+
+void test_checkout_tree__can_remove_ignored(void)
+{
+	git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
+	int ignored = 0;
+
+	opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_REMOVE_IGNORED;
+
+	cl_git_mkfile("testrepo/ignored_file", "as you wish");
+
+	cl_git_pass(git_ignore_add_rule(g_repo, "ignored_file\n"));
+
+	cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "ignored_file"));
+	cl_assert_equal_i(1, ignored);
+
+	cl_assert(git_path_isfile("testrepo/ignored_file"));
+
+	cl_git_pass(git_checkout_head(g_repo, &opts));
+
+	cl_assert(!git_path_isfile("testrepo/ignored_file"));
+}
+
+/* this is essentially the code from git__unescape modified slightly */
+static void strip_cr_from_buf(git_buf *buf)
+{
+	char *scan, *pos = buf->ptr;
+
+	for (scan = pos; *scan; pos++, scan++) {
+		if (*scan == '\r')
+			scan++; /* skip '\r' */
+		if (pos != scan)
+			*pos = *scan;
+	}
+
+	*pos = '\0';
+	buf->size = (pos - buf->ptr);
+}
+
+void test_checkout_tree__can_update_only(void)
+{
+	git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
+	git_oid oid;
+	git_object *obj = NULL;
+	git_buf buf = GIT_BUF_INIT;
+
+	/* first let's get things into a known state - by checkout out the HEAD */
+
+	assert_on_branch(g_repo, "master");
+
+	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+	cl_git_pass(git_checkout_head(g_repo, &opts));
+
+	cl_assert(!git_path_isdir("testrepo/a"));
+
+	cl_git_pass(git_futils_readbuffer(&buf, "testrepo/branch_file.txt"));
+	strip_cr_from_buf(&buf);
+	cl_assert_equal_s("hi\nbye!\n", buf.ptr);
+	git_buf_free(&buf);
+
+	/* now checkout branch but with update only */
+
+	opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_UPDATE_ONLY;
+
+	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"));
+
+	assert_on_branch(g_repo, "dir");
+
+	/* this normally would have been created (which was tested separately in
+	 * the test_checkout_tree__can_switch_branches test), but with
+	 * UPDATE_ONLY it will not have been created.
+	 */
+	cl_assert(!git_path_isdir("testrepo/a"));
+
+	/* but this file still should have been updated */
+	cl_git_pass(git_futils_readbuffer(&buf, "testrepo/branch_file.txt"));
+	strip_cr_from_buf(&buf);
+	cl_assert_equal_s("hi\n", buf.ptr);
+
+	git_buf_free(&buf);
+
+	git_object_free(obj);
+}