Commit 950a70915930342d18286c6d6350929662a978e2

Edward Thomson 2014-07-15T10:23:10

Introduce git_rebase_next `git_rebase_next` will apply the next patch (or cherry-pick) operation, leaving the results checked out in the index / working directory so that consumers can resolve any conflicts, as appropriate.

diff --git a/include/git2/rebase.h b/include/git2/rebase.h
index c760fbe..e4300eb 100644
--- a/include/git2/rebase.h
+++ b/include/git2/rebase.h
@@ -69,6 +69,19 @@ GIT_EXTERN(int) git_rebase(
 	const git_rebase_options *opts);
 
 /**
+ * Applies the next patch, updating the index and working directory with the
+ * changes.  If there are conflicts, you will need to address those before
+ * committing the changes.
+ *
+ * @param repo The repository with a rebase in progress
+ * @param checkout_opts Options to specify how the patch should be checked out
+ * @return Zero on success; -1 on failure.
+ */
+GIT_EXTERN(int) git_rebase_next(
+	git_repository *repo,
+	git_checkout_options *checkout_opts);
+
+/**
  * Aborts a rebase that is currently in progress, resetting the repository
  * and working directory to their state before rebase began.
  *
diff --git a/src/rebase.c b/src/rebase.c
index 38ced24..1062b2a 100644
--- a/src/rebase.c
+++ b/src/rebase.c
@@ -32,6 +32,7 @@
 #define MSGNUM_FILE			"msgnum"
 #define END_FILE			"end"
 #define CMT_FILE_FMT		"cmt.%d"
+#define CURRENT_FILE		"current"
 
 #define ORIG_DETACHED_HEAD	"detached HEAD"
 
@@ -42,8 +43,14 @@ typedef enum {
 	GIT_REBASE_TYPE_NONE = 0,
 	GIT_REBASE_TYPE_APPLY = 1,
 	GIT_REBASE_TYPE_MERGE = 2,
+	GIT_REBASE_TYPE_INTERACTIVE = 3,
 } git_rebase_type_t;
 
+struct git_rebase_state_merge {
+	int32_t msgnum;
+	int32_t end;
+};
+
 typedef struct {
 	git_rebase_type_t type;
 	char *state_path;
@@ -52,6 +59,10 @@ typedef struct {
 
 	char *orig_head_name;
 	git_oid orig_head_id;
+
+	union {
+		struct git_rebase_state_merge merge;
+	};
 } git_rebase_state;
 
 #define GIT_REBASE_STATE_INIT {0}
@@ -92,6 +103,50 @@ done:
 	return 0;
 }
 
+static int rebase_state_merge(git_rebase_state *state, git_repository *repo)
+{
+	git_buf path = GIT_BUF_INIT, msgnum = GIT_BUF_INIT, end = GIT_BUF_INIT;
+	int state_path_len, error;
+
+	GIT_UNUSED(repo);
+
+	if ((error = git_buf_puts(&path, state->state_path)) < 0)
+		goto done;
+
+	state_path_len = git_buf_len(&path);
+
+	if ((error = git_buf_joinpath(&path, path.ptr, MSGNUM_FILE)) < 0)
+		goto done;
+
+	if (git_path_isfile(path.ptr)) {
+		if ((error = git_futils_readbuffer(&msgnum, path.ptr)) < 0)
+			goto done;
+
+		git_buf_rtrim(&msgnum);
+
+		if ((error = git__strtol32(&state->merge.msgnum, msgnum.ptr, NULL, 10)) < 0)
+			goto done;
+	}
+
+	git_buf_truncate(&path, state_path_len);
+
+	if ((error = git_buf_joinpath(&path, path.ptr, END_FILE)) < 0 ||
+		(error = git_futils_readbuffer(&end, path.ptr)) < 0)
+		goto done;
+
+	git_buf_rtrim(&end);
+
+	if ((error = git__strtol32(&state->merge.end, end.ptr, NULL, 10)) < 0)
+		goto done;
+
+done:
+	git_buf_free(&path);
+	git_buf_free(&msgnum);
+	git_buf_free(&end);
+
+	return error;
+}
+
 static int rebase_state(git_rebase_state *state, git_repository *repo)
 {
 	git_buf path = GIT_BUF_INIT, orig_head_name = GIT_BUF_INIT,
@@ -146,6 +201,22 @@ static int rebase_state(git_rebase_state *state, git_repository *repo)
 	if (!state->head_detached)
 		state->orig_head_name = git_buf_detach(&orig_head_name);
 
+	switch (state->type) {
+	case GIT_REBASE_TYPE_INTERACTIVE:
+		giterr_set(GITERR_REBASE, "Interactive rebase is not supported");
+		error = -1;
+		break;
+	case GIT_REBASE_TYPE_MERGE:
+		error = rebase_state_merge(state, repo);
+		break;
+	case GIT_REBASE_TYPE_APPLY:
+		giterr_set(GITERR_REBASE, "Patch application rebase is not supported");
+		error = -1;
+		break;
+	default:
+		abort();
+	}
+
 done:
 	git_buf_free(&path);
 	git_buf_free(&orig_head_name);
@@ -421,6 +492,91 @@ done:
 	return error;
 }
 
+static int rebase_next_merge(
+	git_repository *repo,
+	git_rebase_state *state,
+	git_checkout_options *checkout_opts)
+{
+	git_buf path = GIT_BUF_INIT, current = GIT_BUF_INIT;
+	git_oid current_id;
+	git_commit *current_commit = NULL, *parent_commit = NULL;
+	git_tree *current_tree = NULL, *head_tree = NULL, *parent_tree = NULL;
+	git_index *index = NULL;
+	unsigned int parent_count;
+	int error;
+
+	if (state->merge.msgnum == state->merge.end)
+		return GIT_ITEROVER;
+
+	state->merge.msgnum++;
+
+	if ((error = git_buf_joinpath(&path, state->state_path, "cmt.")) < 0 ||
+		(error = git_buf_printf(&path, "%d", state->merge.msgnum)) < 0 ||
+		(error = git_futils_readbuffer(&current, path.ptr)) < 0)
+		goto done;
+
+	git_buf_rtrim(&current);
+
+	if ((error = git_oid_fromstr(&current_id, current.ptr)) < 0 ||
+		(error = git_commit_lookup(&current_commit, repo, &current_id)) < 0 ||
+		(error = git_commit_tree(&current_tree, current_commit)) < 0 ||
+		(error = git_repository_head_tree(&head_tree, repo)) < 0)
+		goto done;
+
+	if ((parent_count = git_commit_parentcount(current_commit)) > 1) {
+		giterr_set(GITERR_REBASE, "Cannot rebase a merge commit");
+		error = -1;
+		goto done;
+	} else if (parent_count) {
+		if ((error = git_commit_parent(&parent_commit, current_commit, 0)) < 0 ||
+			(error = git_commit_tree(&parent_tree, parent_commit)) < 0)
+			goto done;
+	}
+
+	if ((error = rebase_setupfile(repo, MSGNUM_FILE, "%d\n", state->merge.msgnum)) < 0 ||
+		(error = rebase_setupfile(repo, CURRENT_FILE, "%s\n", current.ptr)) < 0)
+		goto done;
+
+	if ((error = git_merge_trees(&index, repo, parent_tree, head_tree, current_tree, NULL)) < 0 ||
+		(error = git_merge__check_result(repo, index)) < 0 ||
+		(error = git_checkout_index(repo, index, checkout_opts)) < 0)
+		goto done;
+
+done:
+	git_index_free(index);
+	git_tree_free(current_tree);
+	git_tree_free(head_tree);
+	git_tree_free(parent_tree);
+	git_commit_free(current_commit);
+	git_commit_free(parent_commit);
+	git_buf_free(&path);
+	git_buf_free(&current);
+
+	return error;
+}
+
+int git_rebase_next(git_repository *repo, git_checkout_options *opts)
+{
+	git_rebase_state state = GIT_REBASE_STATE_INIT;
+	int error;
+
+	assert(repo);
+
+	if ((error = rebase_state(&state, repo)) < 0)
+		return -1;
+
+	switch (state.type) {
+	case GIT_REBASE_TYPE_MERGE:
+		error = rebase_next_merge(repo, &state, opts);
+		break;
+	default:
+		abort();
+	}
+
+	rebase_state_free(&state);
+	return error;
+}
+
 int git_rebase_abort(git_repository *repo, const git_signature *signature)
 {
 	git_rebase_state state = GIT_REBASE_STATE_INIT;
diff --git a/tests/rebase/merge.c b/tests/rebase/merge.c
new file mode 100644
index 0000000..e44fdd3
--- /dev/null
+++ b/tests/rebase/merge.c
@@ -0,0 +1,59 @@
+#include "clar_libgit2.h"
+#include "git2/rebase.h"
+#include "posix.h"
+
+#include <fcntl.h>
+
+static git_repository *repo;
+static git_signature *signature;
+
+// Fixture setup and teardown
+void test_rebase_merge__initialize(void)
+{
+	repo = cl_git_sandbox_init("rebase");
+}
+
+void test_rebase_merge__cleanup(void)
+{
+	cl_git_sandbox_cleanup();
+}
+
+void test_rebase_merge__next(void)
+{
+	git_reference *branch_ref, *upstream_ref;
+	git_merge_head *branch_head, *upstream_head;
+	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_status_list *status_list;
+	const git_status_entry *status_entry;
+	git_oid file1_id;
+
+	checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
+
+	cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/beef"));
+	cl_git_pass(git_reference_lookup(&upstream_ref, repo, "refs/heads/master"));
+
+	cl_git_pass(git_merge_head_from_ref(&branch_head, repo, branch_ref));
+	cl_git_pass(git_merge_head_from_ref(&upstream_head, repo, upstream_ref));
+
+	cl_git_pass(git_rebase(repo, branch_head, upstream_head, NULL, signature, NULL));
+
+	cl_git_pass(git_rebase_next(repo, &checkout_opts));
+
+	cl_assert_equal_file("da9c51a23d02d931a486f45ad18cda05cf5d2b94\n", 41, "rebase/.git/rebase-merge/current");
+	cl_assert_equal_file("1\n", 2, "rebase/.git/rebase-merge/msgnum");
+
+	cl_git_pass(git_status_list_new(&status_list, repo, NULL));
+	cl_assert_equal_i(1, git_status_list_entrycount(status_list));
+	cl_assert(status_entry = git_status_byindex(status_list, 0));
+
+	cl_assert_equal_s("beef.txt", status_entry->head_to_index->new_file.path);
+
+	git_oid_fromstr(&file1_id, "8d95ea62e621f1d38d230d9e7d206e41096d76af");
+	cl_assert_equal_oid(&file1_id, &status_entry->head_to_index->new_file.id);
+
+	git_status_list_free(status_list);
+	git_merge_head_free(branch_head);
+	git_merge_head_free(upstream_head);
+	git_reference_free(branch_ref);
+	git_reference_free(upstream_ref);
+}